Commit Graph
189 Commits
Author SHA1 Message Date
ZacharyZhang-NY 466dac117d perf(api-server): scope agent standup route to caller's tasks and reports
Replace full-company SELECT * + JS filtering with targeted scoped queries:

  - direct reports = agents WHERE reports_to = me
  - my tasks = tasks WHERE assignee_agent_id = me
  - report tasks = tasks WHERE assignee_agent_id IN (...directReports)
  - report status counts = LEFT JOIN tasks GROUP BY report agent
  - approvals scoped to requested_by = me with separate pending /
    resolved queries (resolved capped at 5 in SQL)

JSON response shape unchanged.
2026-04-16 22:22:36 -04:00
ZacharyZhang-NY 94a9e457ce perf(database): replace listSnapshot+JS filter with scoped SQL in getInbox
Replace 17-table snapshot fetch + 6 JS filter passes with 5 scoped
SELECTs targeting only inbox-relevant rows:

  - approvals where state = 'pending'
  - runs where status in ('failed', 'timed_out') and created_at > 24h
  - agents where budget breach > 80%
  - tasks where status in ('blocked', 'in_review')
  - agent_messages with __board__ read-receipt left join filtered to
    company/incident channel or urgent priority

Agent name lookup is a single id->name map built from one scoped agents
read. Return shape unchanged.
2026-04-16 22:21:30 -04:00
ZacharyZhang-NY 19c575933d perf(database): replace listSnapshot+JS filter with scoped SQL in getStandupReport
Replace 17-table snapshot fetch + per-agent JS filters with:

  - scoped agents row read (excluding terminated/pending_approval)
  - tasks GROUP BY assignee_agent_id with conditional sums for
    in_progress / blocked / done-in-24h counts
  - runs GROUP BY agent_id over 24h window with correlated subquery
    for last status, plus max(created_at) for last run timestamp
  - cost_entries GROUP BY agent_id over 24h window for cost
  - activity GROUP BY actor for review-cycle counts
  - single SELECTs for company-level counters (completed tasks, run
    aggregates, blocked tasks, pending approvals, urgent unread, active
    sprint)

Return shape unchanged.
2026-04-16 22:20:09 -04:00
ZacharyZhang-NY 8f7054630b fix(heartbeat): atomic find-or-create + checkout to eliminate duplicate task race
Replace the racy three-step heartbeat path (listAgentTasks ->
findReusableHeartbeatTask -> saveTask -> checkoutTask) with a single
transactional acquireHeartbeatTask method. The prior flow allowed two
concurrent scheduler ticks to both observe "no reusable heartbeat" and
both INSERT a fresh [Heartbeat] task before either CAS-checked out,
producing duplicates that cleanupDuplicateHeartbeatTasks had to mop up.

acquireHeartbeatTask runs inside BEGIN IMMEDIATE so concurrent ticks
serialize on the writer lock. Inside the transaction it first attempts
to claim any existing claimable [Heartbeat] task (todo/backlog/done/
in_review with checkout_run_id IS NULL), reviving done/in_review by
resetting status. If nothing is claimable but an in-flight heartbeat
exists (status NOT IN done/cancelled), it returns null instead of
creating a duplicate. Only when no heartbeat task exists at all does it
INSERT a new one, already in 'in_progress' with checkout_run_id set.

cleanupDuplicateHeartbeatTasks remains as deletion-side defense.
2026-04-16 22:19:42 -04:00
ZacharyZhang-NY 659a2026de perf(database): replace listSnapshot+JS filter with scoped SQL in getCompanyMetrics
Replace 17-table snapshot fetch + 50+ JS filters per agent with five
scoped aggregation queries:

  - tasks GROUP BY assignee_agent_id with conditional sums for done /
    assigned / cancelled counts
  - runs (within cutoff) GROUP BY agent_id with conditional sums for
    succeeded / failed counts
  - runs GROUP BY agent_id over started_at/finished_at for avg duration
    + max(created_at) as last active
  - cost_entries GROUP BY agent_id for total cost
  - activity GROUP BY actor restricted to status-change actions for
    review cycle count

Company-level totals also collapse to single COUNT/SUM queries instead
of in-memory reductions over the full snapshot. Return shape unchanged.
2026-04-16 22:18:52 -04:00
ZacharyZhang-NY 47731f61a0 perf(database): scope listSnapshot table reads to current company
Add explicit LIMIT to all hot-path tables in listSnapshot regardless of
companyId, raise cross-company caps from 500/250 to 2000/1000 so dashboard
no longer truncates silently at 500.

Add migration 28 with composite indexes covering snapshot WHERE/ORDER BY
columns: tasks(company_id, created_at|updated_at), approvals(company_id,
created_at), task_comments(company_id, created_at), documents(company_id,
updated_at), meetings(company_id, scheduled_at), sprints(company_id,
start_date), runs(agent_id, created_at), cost_entries(agent_id,
created_at), activity(company_id, actor, action), tasks(company_id,
assignee_agent_id, status). Indexes match exact ORDER BY direction so
SQLite can satisfy reads via index scan without temp sort.
2026-04-16 22:17:46 -04:00
ZacharyZhang-NY e2253b92fa refactor(renderer): dismantle useAppController 47-prop god-hook
Sections now subscribe directly to the Zustand store via useSectionContext
(shared data derivation) and useAppStore fine-grained selectors. Delete
useAppController, useExecutionController, useOrganizationController, and
useCommunicationController; the three domain forwarders held no logic
beyond re-exporting store slices.

SectionContentView becomes a 50-line layout dispatcher taking only
{section, error, pageTransition}; cross-domain run actions (startRun /
cancelRun) move into App.tsx and are exposed to sections through a new
AppActionsContext alongside refresh / setError / openPathWithFeedback.
App.tsx now calls useUIController + useCompanyController directly and
composes palette + navigation plumbing itself.
2026-04-16 22:12:51 -04:00
ZacharyZhang-NY de77f5f38e refactor(onboarding): replace 50-prop drilling with OnboardingContext
OnboardingStepPanel previously forwarded ~50 props to seven step
components, so adding a single wizard field meant touching four layers.
Introduce a renderer-local React Context (OnboardingProvider /
useOnboarding) that carries all wizard state and actions (connectors,
node status, company inputs, model selection, execution mode, mission,
launch summary, locale, t, error/loading). Step panels now receive only
their navigation callbacks (onContinue / onBack / onLaunch / onFinish)
and read everything else via useOnboarding(). CompanyOnboarding wraps
the step panel tree in OnboardingProvider with a memoised value.

Business logic is unchanged: each onContinueFrom* transition, the
bootstrap call, and the welcome-step branch remain exactly as before;
this refactor only moves the data-passing mechanism.
2026-04-16 22:11:45 -04:00
ZacharyZhang-NY f3816c6115 refactor(ui): unify success/danger/warning color tokens across Toast/StatusPill/InlineNotice 2026-04-16 22:10:43 -04:00
ZacharyZhang-NY a443312f1a refactor(automation): replace ACCENT/LINE constants with design tokens 2026-04-16 22:10:04 -04:00
ZacharyZhang-NY f177e6c877 refactor(settings): use Modal for OAuthPromptDialog 2026-04-16 22:06:59 -04:00
ZacharyZhang-NY 0f1b313272 refactor(execution): split ApprovalInspector 346-line god-component
Decompose monolithic src/renderer/components/execution/ApprovalInspector.tsx
into src/renderer/components/execution/approval-inspector/:
- ApprovalInspector.tsx (168 lines, orchestrator only)
- ApprovalSummary.tsx, ProposedHireCard.tsx, ApprovalDocumentsPanel.tsx
- DeliverableReviewPanel.tsx, SchedulePicker.tsx, DecisionForm.tsx
- TypeBadge.tsx, useApprovalPayload.ts, index.ts (barrel)

Hoist JSON.parse out of JSX IIFE into useApprovalPayload hook with typed
return. Replace roll-your-own role=radio spans with native
input[type=radio]. Extract getTypeBadgeClass switch into TypeBadge
component. Update execution.tsx barrel to re-export from new path.

Business logic preserved: approval decision flow, schedule validation,
deliverable review, document editing, delete confirmation.
2026-04-16 22:06:39 -04:00
ZacharyZhang-NY cc02b45864 refactor(worker): split god-handler into per-message type files
Extract handleWorkerMessage (380 lines, 12 positional args) into dispatcher
+ focused handler modules. Dispatcher now routes by message.type to
run-log / run-status / run-finished handlers, each receiving (event, deps,
context) triple. run-finished is further decomposed into 5 steps:
finalizeRun, accumulateAndRotateSession, emitAndNotifyCompletion,
runPostCompletionAutomation, triggerSelfWake.

No behavior change: all branches, notifications, lease renewals, circuit
breaker, and escalation semantics preserved. Schema validation stays at
dispatcher top level.
2026-04-16 22:04:37 -04:00
ZacharyZhang-NY 69724f0188 refactor(social): use Modal for AddAccountDialog 2026-04-16 22:04:24 -04:00
ZacharyZhang-NY 8541836b26 refactor(automation): use Modal for WorkflowEditor 2026-04-16 22:03:57 -04:00
ZacharyZhang-NY 183f22e922 fix(onboarding): derive step counter from STEP_ORDER
All six step panels previously hard-coded "X / 5" or "X / 6" in their
header captions, which drifted out of sync with the 7-step STEP_ORDER in
OnboardingHero. Export STEP_ORDER / TOTAL_STEPS / getStepNumber from
OnboardingHero and consume them in every step so the counter is always
the single source of truth.
2026-04-16 22:03:57 -04:00
ZacharyZhang-NY 5a5587c2a1 refactor(automation): use Modal for RuleFormModal 2026-04-16 22:03:24 -04:00
ZacharyZhang-NY 8243f58ef7 refactor(automation): use Modal/ConfirmDialog for DeleteConfirmDialog 2026-04-16 22:02:29 -04:00
ZacharyZhang-NY d0c6d1f6d0 perf(db): resolve chain of command in a single recursive CTE to eliminate N+1 queries 2026-04-16 21:59:18 -04:00
ZacharyZhang-NY 2ec77677cb fix(renderer): classify DesktopEvent types to stop full-refresh fallthrough (D-05)
All 14+ non-specially-handled events were pushing into the batcher with
entityKeyFromEvent returning null, which set hasDomainChanged = true and
triggered a full snapshot refresh on every agent-message, run-status, and
heartbeat-status -- defeating the granular patch machinery.

Per-type classification:
- run-status         -> granular run-updated (patchRun by runId)
- heartbeat-status   -> granular agent-updated (patchAgent by agentId)
- agent-message      -> granular task-updated (patchTask refetches comments)
- new-message, thread-wait-state-updated, domain-changed
                     -> full refresh, coalesced via 500ms batcher window
- automation-fired, recovery-needed
                     -> snapshot-irrelevant, skipped at event source

Widen batcher window from 50ms to 500ms so bursts coalesce into one reload.
Granular batches still flush at the same window cadence.
2026-04-16 21:58:32 -04:00
ZacharyZhang-NY 8cf6be05bb fix(skills): enforce per-file and aggregate size caps for imported SKILL.md content 2026-04-16 21:58:32 -04:00
ZacharyZhang-NY c81d72fb9c fix(api-server): SSE stream pulls incremental events via seq cursor
Replace per-poll 5000-row scan + JS filter with indexed WHERE seq > ?
query (uses idx_run_events_run). Batch up to 500 per call, drain loop
on reconnect. Poll interval raised 500ms -> 1000ms.
2026-04-16 21:57:35 -04:00
ZacharyZhang-NY c5c3919c78 fix(skills): bound local skill walk with depth, file count, and symlink loop protection 2026-04-16 21:57:29 -04:00
ZacharyZhang-NY 0123103f58 fix(renderer): subscribe ConsoleView to logRevision to trigger re-render (D-04)
logRevision was bumped on every run-log event but no component subscribed,
so ref mutations to logBufferRef never triggered a re-render -- the console
pre stayed stale until something else forced a render.

Subscribe to logRevision via useAppStore and depend on it in the useMemo
that reads logBufferRef, keeping the ref-based buffer (cheap writes) while
restoring re-renders.
2026-04-16 21:57:08 -04:00
ZacharyZhang-NY 0119872db6 fix(renderer): use snapshot.currentCompanyId as single source of truth (D-03)
CompanySlice's standalone currentCompanyId field + setCurrentCompanyId action
were never written by any caller, so useDesktopEvents.ts read null and fell
through to full refresh for every entity event -- the granular patch
machinery was dead code.

- Remove currentCompanyId field + setCurrentCompanyId from CompanySlice
- refreshBadges reads from snapshot.currentCompanyId
- useDesktopEvents reads companyId from snapshot.currentCompanyId
- Update store unit test to seed snapshot instead of calling removed action
2026-04-16 21:56:40 -04:00
ZacharyZhang-NY a130dc29f5 fix(tasks): route updateTaskSession/getTaskSession through task_sessions table
Stop writing new session data back to the legacy tasks.session_json
column; updateTaskSession now upserts the active task_sessions row
(updating the session pointed to by tasks.active_session_id, creating
one when absent), and getTaskSession reads from the same row so
session data has a single source of truth. Adds migration 27 to
backfill pre-existing tasks.session_json values into task_sessions
idempotently via INSERT OR IGNORE guarded on task_id. The legacy
column is kept for read-only history and is not dropped.
2026-04-16 21:53:12 -04:00
ZacharyZhang-NY ccf0c2a500 fix(schema): remove duplicate-column fallback; make migrations idempotent
Stop silently swallowing duplicate-column errors in the migration
runner; any real migration failure now throws. Replace try/catch-based
column additions in ensureOptionalColumns and migrations 19-22 with
pragma table_info checks so repeated startup is idempotent without
tolerating error signals. Reorder initializeDatabase so runMigrations
runs before ensureOptionalColumns to avoid migration 13 adding columns
that ensureOptionalColumns already created on fresh databases.
2026-04-16 21:52:43 -04:00
ZacharyZhang-NY 5201b6623b fix(database): enable WAL mode and busy_timeout on every DB connection
Applies WAL journal, 5s busy_timeout, synchronous=NORMAL, and
foreign_keys=ON pragmas immediately after opening each connection so
concurrent main+worker writers share the same locking configuration
and avoid SQLITE_BUSY under contention.
2026-04-16 21:52:25 -04:00
ZacharyZhang-NY a6d0a567a8 fix(ipc-credentials): use shared registerHandle and precise redactSecrets (I-02)
Two tightly-coupled hardening changes to the credential IPC module.

(1) Drop the self-built `DesktopOk/DesktopErr/ok/err/handle` helpers
that called `ipcMain.handle` directly, bypassing the trusted-renderer
check in `ipc-support.registerHandle` and the `ZodError ->
IPC_INVALID_PAYLOAD` sanitizer. The module now takes `registerHandle`
and `fail` as deps (wired from `createIpcSupport`) and imports `ok`
from `ipc-core-common`, mirroring the OAuth handler layout.

(2) Tighten `redactSecrets` to issuer-prefixed patterns only (Bearer,
`sk-`, `sk-ant-`, `sk-proj-`, AKIA access key IDs, three-segment JWTs).
The previous blanket `[A-Fa-f0-9]{40,}` and `[A-Za-z0-9+/]{48,}={0,2}`
rules were shredding legitimate diagnostic text — git SHAs, UUIDs,
Fireworks model identifiers like `accounts/fireworks/models/...`, and
any long opaque ID — into `[REDACTED-HEX]` / `[REDACTED-B64]`, which
made provider errors unreadable in the renderer.
2026-04-16 21:49:53 -04:00
ZacharyZhang-NY ff0c01fe87 fix(worker): claim workspace lock in main before enqueue (D-02)
The lock was previously claimed when the worker reported `running`, which
is after `agent.prompt` had already started streaming. Two concurrent
runs targeting the same workspace could both pass the main-side checks,
both get enqueued, and both read half-written state before one of them
lost the lock race and was cancelled mid-flight.

Move the `claimWorkspaceLock` call into `dispatchRunToWorker` so main
coordinates exclusivity authoritatively. If the lock is unavailable,
the run is finished as failed and never enqueued. The worker-side
lease-renewal path is preserved since it is a lease, not a lock.
2026-04-16 21:49:30 -04:00
ZacharyZhang-NY f86b3ddbac fix(ipc-oauth): use shared registerHandle for trust and error sanitization (I-01)
The self-built `ok/err/handle` helpers called `ipcMain.handle` directly,
which meant every OAuth IPC channel skipped the trusted-renderer check
in `ipc-support.registerHandle` and its `ZodError -> IPC_INVALID_PAYLOAD`
sanitizer. Any renderer origin could invoke these channels, and thrown
zod errors would leak the raw validator message.

Take `registerHandle` and `fail` as dependencies so callers wire in the
hardened variants from `createIpcSupport`, drop the local helpers, and
import `ok` from `ipc-core-common`. Zod inputs stay on `safeParse` to
preserve the explicit `INVALID_INPUT` code + message path.
2026-04-16 21:48:47 -04:00
ZacharyZhang-NY a1f021ff4a fix(worker): invert connector bypass default and gate on packaged build (S-01)
Previously `AGENTCOMPANY_ALLOW_UNSAFE_CONNECTOR_BYPASS !== "false"` meant
packaged builds defaulted to passing `dangerouslyBypassApprovalsAndSandbox`,
`dangerouslySkipPermissions`, `yolo`, and `--sandbox danger-full-access`
to CLI connectors, bypassing ApprovalService entirely.

Require both explicit opt-in (`AGENTCOMPANY_ALLOW_UNSAFE_CONNECTOR_BYPASS=true`)
and a non-packaged build. Packaged builds hard-refuse regardless of the env
var. Main process forks the worker with `AGENTCOMPANY_PACKAGED=<app.isPackaged>`
so the worker has an authoritative packaged signal.
2026-04-16 21:48:10 -04:00
ZacharyZhang-NY f8052c3741 refactor(scheduler): split 15s tick into 4 independent intervals
Split the single 15s setInterval in scheduler.ts (6 serial jobs) into 4
independent schedulers so a slow job no longer stalls the others:

- scheduler-heartbeat.ts (15s): wake eligible agents, CEO bootstrap,
  schedule automation trigger
- scheduler-lock-recovery.ts (60s): stale task-lock recovery and stale
  workspace-lock release
- scheduler-idle-automation.ts (60s): idle-agent automation dispatch
- scheduler-housekeeping.ts (300s): monthly budget reset, expired budget
  incident auto-close, stale review sweep, expired wakeup purge

scheduler.ts is now a thin composer exposing startAllSchedulers /
stopAllSchedulers. Bootstrap and service-graph updated accordingly.
Business logic, error isolation, and atomic actions preserved.
2026-04-16 21:47:09 -04:00
ZacharyZhang-NY 2676400e0c fix(ipc-credentials): serialize testCredential env mutations (S-03)
Pi-ai's `streamSimple`/`SimpleStreamOptions` has no per-request env
escape hatch; its Azure/AWS/GCP ADC providers read `process.env`
directly. The previous implementation wrote `extraEnv` into
`process.env` globally and relied on a `finally` after
`stream.result()` to clean up, so two concurrent `testCredential`
calls would clobber each other's env, expose the wrong credential
to in-flight streams, and on error paths fail to restore
previously-set values.

Wrap the env-mutating branch in a module-level promise chain so at
most one `extraEnv` test runs at a time, snapshot the prior value
of every key we overwrite, and restore (or delete when unset) in
`finally`. Tests that resolve to `extraEnv: undefined` bypass the
mutex entirely.
2026-04-16 21:43:44 -04:00
ZacharyZhang-NY ff08ab3e35 fix(desktop-shell-window): stop blocking main process on untrusted-link dialog
showMessageBoxSync froze the main-process event loop while the user
decided whether to open an external link, stalling agent-event workers,
IPC, and the scheduler for the duration of the dialog. setWindowOpenHandler
still returns synchronously with action: "deny"; the confirmation dialog
runs in an async continuation that only reaches shell.openExternal when
the user approves.
2026-04-16 21:42:29 -04:00
ZacharyZhang-NY f803b0b826 fix(api-server): require Origin header when no bearer token
Origin-less requests previously skipped the allowlist entirely, meaning
any process that omitted the Origin header (trivial for a non-browser
attacker) could reach the auth layer. Browsers always send Origin, so
rejecting Origin-less browser-style requests closes the bypass without
breaking agent CLIs, which authenticate with a bearer token instead.

New policy:
 - Requests with a Bearer Authorization header: Origin optional; if
   present it must be in the allowlist.
 - All other requests (including CORS preflight): Origin required and
   must be in the allowlist.
2026-04-16 21:42:04 -04:00
ZacharyZhang-NY deb980448c fix(ipc-thread-handlers): drop redundant try/catch wrappers and duplicate emit
Every handler was wrapping its body in try/catch and returning fail(code,
error.message), leaking raw DB messages to the renderer and bypassing the
sanitizer in registerHandle. Removed the 14 inner try/catch shells so
errors surface through ipc-support's IPC_HANDLER_FAILED / IPC_INVALID_PAYLOAD
path. Also removed the duplicate emitEvent({ type: 'domain-changed' }) in
addThreadEvent — publishDomainChanged already emits the same event.
2026-04-16 21:37:58 -04:00
ZacharyZhang-NY 6c6cc96060 fix(agent-permissions): propagate DB errors instead of silently denying
safeGetAgent was catching all errors and returning null, causing every
permission check (canControlAgent / canManageOrganization / canRequestHire)
to silently deny when the database threw. Only swallow the 'Agent not found'
case and let real DB errors propagate up to registerHandle's sanitizer.
2026-04-16 21:37:16 -04:00
ZacharyZhang-NY 893a0ddec1 chore(preload): drop stale @planned notes on delete{Task,Goal,Project,Workspace}
Handlers for deleteTask/deleteGoal/deleteProject live in
ipc-core-task-graph-handlers.ts:166-168 (backed by
database/tasks-crud.ts and database/entity-crud.ts), and
deleteWorkspace lives at line 169 (backed by database/workspaces.ts).
The four @planned comments were misleading.
2026-04-16 21:36:55 -04:00
ZacharyZhang-NY 734c789f50 fix(orchestration): remove one-time heartbeat cleanup from scheduler startup 2026-04-16 21:36:43 -04:00
ZacharyZhang-NY 1a57622849 fix(automation): remove silent JSON.parse fallback in executeAutomationAction 2026-04-16 21:36:41 -04:00
ZacharyZhang-NY a612ea7bea fix(communication): set searchLoading=true in setSearchQuery
setSearchQuery only incremented searchRequestId without flipping
searchLoading, so the UI could not distinguish "searching" from "no
results". Now loading flips true on non-empty query and clears
(results + loading) on empty query. setSearchResults keeps the
existing id-match logic to flip loading false on arrival.
2026-04-16 21:36:37 -04:00
ZacharyZhang-NY aa1afd0db2 fix(vault): remove NODE_ENV=test and VITEST auto-enable of insecure fallback so only explicit opt-in unlocks it 2026-04-16 21:36:29 -04:00
ZacharyZhang-NY cfe5f9ff73 fix(oauth): scope failPending to matching connectorId so cancelling one OAuth does not reject others 2026-04-16 21:36:22 -04:00
ZacharyZhang-NY 1b578c111a update 2026-04-16 14:48:51 -04:00
ZacharyZhang-NY 9779c4d1ca update 2026-04-07 20:18:47 -04:00
ZacharyZhang-NY f5d4f8b705 update 2026-04-06 00:35:27 -04:00
ZacharyZhang-NY 72157f0703 update 2026-04-05 23:47:56 -04:00
ZacharyZhang-NY 9145572422 update 2026-04-05 20:14:48 -04:00
ZacharyZhang-NY b8dda3c8d0 update 2026-04-04 17:01:26 -04:00