Commit Graph
542 Commits
Author SHA1 Message Date
ZacharyZhang-NY 95c539380b Restore native document center grammar 2026-04-19 15:59:10 -04:00
ZacharyZhang-NY bd8c420718 Restore native operational surface grammar 2026-04-19 15:57:04 -04:00
ZacharyZhang-NY edbebe6109 Make document lifecycle fan-out durable and atomic
Pre-v47 `createDocumentWithLifecycle` wrapped each hook in its own
try/catch and always returned success, so a failure in any of the
downstream hooks (thread event, checkpoint approval, deliverable
review, automation dispatch, reviewer wake) left a live document row
behind with a silently incomplete lifecycle.

Two invariants now hold:

- DB-writing hooks (thread integration, checkpoint approval,
  deliverable review) run inside the same `BEGIN IMMEDIATE`
  transaction as `saveDocument`. A throw rolls back the document row
  along with every partial write, so an operator never sees a
  persisted document with a missing thread or skipped approval.

- Cross-process side effects (automation dispatch, reviewer wake) are
  enqueued as `document_lifecycle_events` rows inside the same
  transaction and processed post-commit. Success stamps
  `status = 'succeeded'`; failure stamps `status = 'failed'` with
  `last_error`, so partial lifecycle state is observable via
  `listFailedForCompany` instead of disappearing into the log tail.

Migration v47 installs the ledger; `buildDocumentLifecycleEventMethods`
exposes the enqueue/running/succeeded/failed transitions; the lifecycle
service drives both.
2026-04-19 15:53:55 -04:00
ZacharyZhang-NY a36884360a Key inbound correlation by messageId with monotonic pi-timestamps
`registerInboundInjection` used to index correlations by
`Date.parse(record.createdAt)` — a millisecond-precision wall-clock
value. Two inbox rows that landed in the same millisecond collapsed
onto one Map slot, and the thread-event writer lost one peer's
identity on `message_end`.

Identity now lives on `record.id`. `projectInboundMessage` allocates a
monotonic `piTimestamp` per run (base = `Date.parse(createdAt)`,
bumped to `max(base, lastAllocated + 1)` so it is always unique
within the run) and stamps that value onto the projected pi message.
The correlation registry keeps `byMessageId` as the primary index and
`byPiTimestamp` as a reverse lookup so the `message_end` callback —
which only surfaces the pi-message shape — still resolves back to the
author without depending on wall-clock precision.

The runtime message injector now defers projection to
`RuntimeBridge.projectInboundMessage` so the steered message, the
correlation entry, and the allocated timestamp stay in lockstep.
2026-04-19 15:50:39 -04:00
ZacharyZhang-NY 320706fecf Restore native onboarding and dashboard grammar 2026-04-19 15:50:28 -04:00
ZacharyZhang-NY cb0ac485da Close inbox projection under one transaction
The old `prepareRun` projection drifted: append-to-shadow, thread-event,
and `markAgentInboxDelivered` ran as three independent writes, so a
crash between the append and the delivered mark replayed the same
inbox row on the next run and double-populated the transcript.

v46 installs `agent_message_projections` keyed by (message_id,
session_id) as the idempotent gate. The projection closure now wraps
the ledger claim, the shadow append, the thread-event write, and the
delivered-mark inside one `BEGIN IMMEDIATE` transaction (exposed via
`AppDatabase.withTransaction`). `claimProjection` returns false on a
replay, so the whole closure short-circuits — no double transcript,
no double thread event.
2026-04-19 15:46:12 -04:00
ZacharyZhang-NY 213837af1d Rebuild native onboarding bootstrap flow 2026-04-19 15:44:35 -04:00
ZacharyZhang-NY d9b35d73ab Pin work_products identity to (company, workspace, path)
The natural key used to be (companyId, provider, externalId), so two
workspaces with the same workspace-relative path collided onto one
row. Provenance was also frozen at the first write: `created_by_run_id`
never refreshed, so a run that edited a pre-existing file never
qualified as a deliverable and the DocumentCenter "open" link could
still jump into the first workspace after a repo move.

Migration v45 adds `workspace_id` + `last_touched_by_run_id`, backfills
`workspace_id` via the task/run join, seeds `last_touched_by_run_id =
created_by_run_id`, and installs a partial unique index on
`(company_id, workspace_id, provider, external_id)` scoped to rows
that have a workspace. The artifact projector now records
`workspace_id` on insert and refreshes `last_touched_by_run_id` on
every upsert. The deliverable signal query reads either provenance
column so a run that only edited a pre-existing file still counts.
`resolveWorkProductTarget` prefers the stored `workspace_id` and falls
back to the task/run join for legacy NULL rows.
2026-04-19 15:43:48 -04:00
ZacharyZhang-NY d1483dcd5d Add final native verification matrix 2026-04-19 15:15:43 -04:00
ZacharyZhang-NY 36ccb2c08f Wire native runtime flows into GPUI runs 2026-04-19 15:14:10 -04:00
ZacharyZhang-NY 686140ae55 Route every document save through one lifecycle pipeline
Desktop IPC saves used to fire only automation and the reviewer wake,
while HTTP API saves fired automation, thread integration, checkpoint
engine, deliverable-review approval, and the reviewer wake. Two
divergent paths produced two different lifecycle states for the same
document, and a future fix to either branch would miss the other.

Introduce `document-lifecycle.ts` as the single owner of document
creation side effects: save, derive task binding from the originating
run, fan out to automation/thread/checkpoint, wake the reviewer, then
raise a deliverable-review approval when the document qualifies. The
HTTP route and the IPC handler both call the pipeline — the API
server's `onDocumentCreated` callback is replaced with a
`createDocument` hook that delegates to the pipeline.

Wire `checkpointEngine` through `bootstrap-application` →
`main-runtime` → `main-runtime-support` so the IPC handler bag carries
both thread integration and the checkpoint engine required by the
pipeline. The document-run-provenance integration test harness is
updated to pass a `createDocument` implementation backed by the same
pipeline the production path uses.
2026-04-19 15:13:53 -04:00
ZacharyZhang-NY ff9a52324e Route work-product clicks through a resolver IPC
Filesystem work products store `external_id` as a workspace-relative
path so artifacts survive a workspace-root move, but the renderer was
handing that relative fragment straight to `shell.openPath`, which
needs an absolute managed path. Clicks flickered into a failed open.

Add a dedicated `openWorkProduct({ companyId, id })` IPC backed by
`resolveWorkProductTarget` on the DB layer. The resolver joins the
work-product row back to its originating task → workspace (or run →
workspace for taskless runs), composes an absolute path, and rejects
any relative fragment that would escape the workspace root. The
renderer now passes the work product's id — path resolution stays on
the main process where the managed-path policy already lives.
2026-04-19 15:05:21 -04:00
ZacharyZhang-NY 1a0fdc6f31 Collapse the GET /api/agents/me/standup duplicate to one module
Two registrations of the same route lived in the codebase: one inline
in routes-agents.ts that filtered three full company-wide SELECTs in
memory, and the dedicated routes-agents-standup.ts that uses targeted
standup-shaped DB queries. The router keeps the first match so the
inline copy was serving every request and the focused module was dead
code — any future fix to standup would have silently missed
production.

Remove the inline standup handler and its three companion helpers
(`listAgentsForStandup`, `listTasksForStandup`, `listApprovalsForStandup`)
plus the unused `AgentStandupRow` type. The dedicated standup module is
now the single source of truth.
2026-04-19 15:01:41 -04:00
ZacharyZhang-NY efb4bddab6 Count work_products toward a run's deliverable judgment
The deliverable check used to compare a run against knowledge entries,
documents, and substantial task comments only, so a research or content
task that produced its output as a markdown file on disk (via
write/edit/bash) stayed pinned in_progress even though the deliverable
was already sitting in `work_products`.

Split the DB query into `collectRunDeliverableSignals`, which returns
the four surfaces (knowledge / document / work product / substantial
comment) explicitly, and push the per-task-type acceptance logic into
`automation-run-completion`. Each task type declares which signal set
counts as a valid deliverable: content tasks now require a document or
an on-disk artifact and will not advance on a knowledge entry alone;
research and general accept any of the four; code and management
preserve their broader surface. Tests updated to cover the new signal
and the content/knowledge rejection case.
2026-04-19 14:59:05 -04:00
ZacharyZhang-NY 23a19a8180 Promote artifact registration to a primary run outcome
Per-tool `afterToolCall` projection stays the primary write path but is
now backed by an end-of-run reconciliation sweep on `RuntimeBridge`.
`reconcileWorkspaceArtifacts` re-scans the workspace via `git status`
before the run's resources are released, idempotently upserting any
file the per-tool hook skipped — the exact gap that was leaving the
DocumentCenter empty while the disk filled up.

Projection failures stop being swallowed silently: the hook now logs
a tool-level summary at INFO and throws are routed to ERROR so they are
visible in the desktop log tail. The reconciliation sweep surfaces its
own failures the same way, so an operator can tell the difference
between "no files produced" and "files produced but registration
broken".
2026-04-19 14:55:23 -04:00
ZacharyZhang-NY db9375fdfe Update lockfile for runtime storage changes 2026-04-19 14:52:01 -04:00
ZacharyZhang-NY 1b8e6d384a Write thread events for taskless communication runs
recordThreadEvent used to drop the event whenever `envelope.taskId` was
null, so communication wakes produced nothing in the thread view even
while the agent actively replied to peers. Taskless runs now write into
a per-agent general thread (`ensureAgentCommunicationThread`), so the UI
surfaces peer conversations alongside task activity.

Peer-authored user turns inside the transcript used to be labelled with
the recipient's id — pi erases the sender once the message is projected.
A new `inboundCorrelations` map on `RuntimeBridge` pins each pi-message
timestamp to its source `agent_messages` row at injection time, so the
thread-event writer can recover the real author id and `senderKind` on
`message_end`. prepareRun projections (no `message_end` fires for
priorMessages) write their thread event directly as the projection lands
so they are not silently lost.
2026-04-19 14:51:26 -04:00
ZacharyZhang-NY 1959d218d9 Ingest runtime contract events into storage 2026-04-19 14:50:33 -04:00
ZacharyZhang-NY 98f975a4ca Persist runtime events and artifacts 2026-04-19 14:46:54 -04:00
ZacharyZhang-NY 271c6cfec8 Bind communication wakes to concrete inbox rows
Replace the worker's generic `"Continue."` default with a dispatch-time
`initialUserTurn` that is resolved per trigger. Message and peer-request
wakes now enumerate the actual unread messages (id, sender, channel,
subject, reply-to) so the agent's first user turn carries the entities
it is expected to act on, instead of idling into a status summary.

Drop the system-prompt `heartbeatFallbackSection`: its "respond with a
brief status summary" line fought the wake-reason block above it and
was the main reason communication wakes degraded into self-narration.
The wake-reason section plus the bound initial user turn now cover
every case that section was carrying.
2026-04-19 14:43:57 -04:00
ZacharyZhang-NY e917c614c9 Gate inbox delivery on worker-side steer ACK
Replace the fire-and-forget steer with a correlated ACK round-trip so main
only marks an inbox row delivered once the worker confirms the message
landed on the live Agent. Steer against a run that is still in the
`preparing` window or has already started `closing` now falls back to the
wake + inbox projection path instead of being silently dropped.

Worker publishes `run-agent-state` transitions at Agent construction and
teardown; main tracks these on `ActiveRunContext.state` and refuses to
steer runs that are not `agent_active`. A rejected or timed-out ACK
triggers `wakeAgentIfPossible` so the next `prepareRun` replays the row.
2026-04-19 14:38:04 -04:00
ZacharyZhang-NY ad9205fcc9 Add native interaction-state animations 2026-04-19 14:37:42 -04:00
ZacharyZhang-NY a291e93608 Wire native settings and workspace mutations 2026-04-19 14:28:17 -04:00
ZacharyZhang-NY 0a5be5ea97 Align settings and workspaces native surfaces 2026-04-19 14:16:01 -04:00
ZacharyZhang-NY e1b7f52444 Align runs and inbox native surfaces 2026-04-19 13:53:51 -04:00
ZacharyZhang-NY b3d5bcdde5 Align approvals and documents with native shell 2026-04-19 13:46:18 -04:00
ZacharyZhang-NY 6809df15de Refine GPUI shell UX surfaces 2026-04-19 13:32:58 -04:00
ZacharyZhang-NY 22ba7a9004 feat(documents): surface filesystem work-products in Document Center
The Document Center only read the `documents` table, which is prose
content POSTed via /api/documents. Everything the agents wrote via
`bash` / `write` / `edit` — the bulk of their output on a real
project — landed in `work_products` and never appeared on the page.
Operators saw "0 items" after a full night's run.

Lift work_products into the same UI:

  - `listWorkProductsForCompany` returns a newest-first slice with a
    500-row cap so the snapshot stays bounded.
  - `ProfileSnapshot.workProducts` flows through `useCompanyData` as
    `companyWorkProducts`, filtered to the current company.
  - `DocumentCenter` renders two labelled sections in order — prose
    documents (editable) on top, files & deliverables below. Each
    work-product row opens the underlying URL or filesystem path
    when clickable, so the agent's bash-generated markdown, PRs,
    and artifacts are one click away.

`WorkProductRecord` lifts into `@shared/types/records-extended` so
the renderer, snapshot, and database shells all reference the same
shape. The store-test fixture learns about the new field.
2026-04-19 13:24:50 -04:00
ZacharyZhang-NY 734a2b04e0 fix(runtime): narrow beforeToolCall gate to operational approvals
Previous Task 2 pass gated every destructive tool on any blocking
approval or any open wait-state for the agent. That was too coarse:
a pending deliverable_review or approve_ceo_strategy (both decision-
type approvals, waiting for a human to respond to completed or
proposed work) froze the agent's entire toolchain — including the
`bash` call the agent uses to POST /api/messages. Result: CEO
talked, employees woke on heartbeat, tried to reply, the message
POST was blocked as a "destructive tool", the run ended silent.
Every reply collapsed to an empty heartbeat.

Two corrections land together:

1. Approval gate narrows to the five approval types that actually
   gate EXECUTION — `dangerous_command`, `secret_access`,
   `production_deploy`, `social_post`, `budget_override`. Review /
   decision types are ignored at the tool layer; they remain visible
   in the board UI and still block via the heartbeat scheduler when
   the execution mode demands it.

2. Wait-state check leaves the hook entirely. The heartbeat scheduler
   (`checkpoint-engine.shouldPauseHeartbeat`) is the single owner of
   "agent should pause because a thread is waiting" — mirroring the
   rule at tool level produced the same company-wide freeze for
   any reply on any thread. Heartbeat is the right scope; tools
   are not.

The destructive-tool set and the operational-approval set are both
named, readonly, and centralised so the next addition reads like a
whitelist instead of a patch.
2026-04-19 13:20:14 -04:00
ZacharyZhang-NY f976b59b8f test: extend profile store end-to-end coverage 2026-04-19 01:37:27 -04:00
ZacharyZhang-NY 308be17264 refactor: add native ui theme tokens 2026-04-19 01:36:21 -04:00
ZacharyZhang-NY 5e6e929779 refactor: prune session-store and agent-list dead code
The session_entries supersede pipeline was a design that never
shipped: `SqliteSessionStore.replaceMessages` / `appendCompaction`,
`sessionEntries.replace` / `listAll` / `deleteForSession`, and the
`"compaction" | "summary"` kind variants had zero live callers.
Compaction wires through pi's `transformContext` and leaves the
persisted transcript alone, so the shadow-row machinery was pure
cruft with two tests propping it up.

Rip it out: the store exposes only `loadMessages` and
`appendMessage`, `sessionEntries` exposes only `append` and
`listActive`, and `SessionEntryKind` narrows to `"message"` so new
writers can't re-introduce a dead kind by accident.
`is_superseded` stays on the table — the DB column is harmless
with no writer, and dropping it is a separate schema migration
concern.

`listTopLevelAgentIds` loses its only caller in the previous
commit (company channel now fans out through
`listCompanyBroadcastAgentIds`), so it goes too. Three tests that
validated the removed behaviour are deleted; the one remaining
session-store test still covers the live `append + load` path.
2026-04-19 01:34:14 -04:00
ZacharyZhang-NY 5097591725 refactor: split native app root by screen 2026-04-19 01:32:40 -04:00
ZacharyZhang-NY 6508e75a62 docs: align README and prompt docs with the real runtime semantics
README described target-state features that the previous migration
left half-implemented. Now that the runtime actually matches the
promise, the docs can stop overselling: agent messaging is
described as the real live-steer + inbox flow, approvals as the
beforeToolCall gate that actually blocks destructive tools,
sessions as the transformContext-driven compaction plus rotation.
The architecture bullet list grows to name the seams that landed
across this migration — the pi contract wiring, the approval gate,
the workspace lock model, and the stripped startup mutations — so
the README reads like documentation rather than roadmap.

The filesystem prompt doc (`files.en.md`) adds the missing
preamble: every bash/write/edit succeeds into work_products
automatically, and the API endpoints below are only needed for
non-filesystem deliverables. Agents were routinely posting
duplicate work products because nothing told them the projection
already ran.
2026-04-19 01:25:19 -04:00
ZacharyZhang-NY 0746664f72 feat(review-panel): split reviews by author — board / human / manager
One flat list of `status = in_review` tasks couldn't tell the
operator whether the next decision belonged to the board (approval
pending), to the human user (top-level assignee or explicit
`requires_user_review`), or to a middle manager walking up the
`reports_to` chain. Three very different escalation paths rendered
identically, so "reviewed by X" was the only signal and operators
missed the ones actually waiting on them.

`PendingReviewList` now classifies each in-review task into one of
three kinds (`board` / `user` / `manager`) using the pending
approval set plus the existing assignee/manager lookup. Rendering
groups them under labelled sub-headings in priority order (board
first — the operator is the bottleneck there), each with its own
awaiting-text so the row's status is unambiguous at a glance.

The component accepts approvals via prop; the caller passes the
existing `pendingApprovals` it already holds. Translations for both
locales carry the new group labels.
2026-04-19 01:24:01 -04:00
ZacharyZhang-NY d47e637a45 fix(messages): company channel fans out to every active agent
"Company" channel fell through to `listTopLevelAgentIds`, so a
broadcast on the most ambient-sounding channel in the product only
reached agents with `reports_to IS NULL`. The label said company;
the semantics said board-only. The rest of the org never knew a
message existed.

Add `listCompanyBroadcastAgentIds` — returns every non-terminated,
non-paused, non-pending agent for the company — and wire it through
the `company` case in `resolveMessageWakeTargets`. Board-only
fan-outs are still reachable: the `incident` channel keeps the
broader pingable set (paused-by-budget included) and direct DMs to
top-level agents work as before.

The prompt doc catches up to the runtime: company is explicitly
framed as all-hands, with board-only guidance redirected to DMs or
incidents. The message-dispatch unit test asserts the new fan-out.
2026-04-19 01:21:26 -04:00
ZacharyZhang-NY 0a40c55b3c fix(thread-panel): wire the scoped reply composer to real participants
The thread-panel composer was pinned to `channel="direct"` with no
`directParticipantIds` and no `replyTo`. Clicking reply in a human
review thread sent a message with no recipient and the send button
stayed disabled — a silent dead-end for the entire Co-Founder
review UX.

The fix exposes the thread's participant list to the renderer:

  - `listThreadParticipantsSchema` and the matching IPC channel
    (`agent-company:list-thread-participants`) return the rows from
    `conversation_participants` for a given thread.
  - `threadIntegration.onTaskCreated` / `onApprovalCreated` already
    enrol the owning agent and the virtual user on thread creation,
    so the list is populated without any backfill.
  - `ThreadPanel` loads participants alongside events and forwards
    the agent ids to `MessageComposer.directParticipantIds`. The
    composer's existing `directRecipientId` memo picks the first
    non-board agent, so replies now target the correct agent — or
    surface the "no recipient" notice when the thread has none.

The Board/user member is deliberately excluded from the list the
composer consumes: the composer wants an agent to route to. All
other wiring (wait-state precision, threadId) stays untouched.
2026-04-19 01:19:23 -04:00
ZacharyZhang-NY e53ac36176 feat(session): auto-compact long transcripts via transformContext
pi's `transformContext` hook fired on every LLM call but returned
the transcript unchanged, so a long-running agent paid per-token
for its entire history on every turn. The README promised "sessions
persist + auto-compact" but the main loop had no compaction wired
in at all — only rotation at the long-cycle boundary.

`context-compactor` closes that gap. A pure, provider-free function
estimates tokens (~4 chars/token), short-circuits below a 120K
threshold, and on overflow keeps the last 20 messages verbatim
while collapsing older ones into a single "context summary" user
turn. The summary preserves the original task and the first twelve
assistant highlights so the agent has continuity without forwarding
every historical turn.

Compaction is non-destructive: the Agent's in-memory transcript
and the SQLite session store still hold the full history, feeding
UI timelines and audit. Only the LLM payload shrinks. Rotation
remains the long-cycle boundary (separate commit) — compaction is
what keeps each individual LLM call within budget.
2026-04-19 01:14:48 -04:00
ZacharyZhang-NY 7f23f40e87 fix(db): strip implicit data mutations from startup
`init()` ran three implicit data-repair queries on every boot:
`reconcileOrphanedTasks`, `reconcileUnreviewableTasks`, and a
secrets-table migration whose fallback glued legacy rows onto a
synthetic zero UUID when no current company existed. The first
two papered over state-machine leaks at init time — hiding the
real bug and making startup observably mutate production data;
the third silently connected ciphertext to a non-existent company
with no UI visibility and no deletion path.

Root fix: the startup path is now allowed exactly one mutation —
`interruptStaleRuns`, which is crash recovery (running-at-boot =
worker died mid-turn) and cannot be expressed anywhere else.

The dead-letter `in_review` case moves to the transition point:
`tasks-status.guardInReviewHasReviewer` refuses to advance a task
into `in_review` when its assignee is a top-level agent
(`reports_to IS NULL`) and the task is not flagged for user
review. Callers either pre-flag the task (the CEO-completion path
in `automation-run-completion.advanceToInReview` now does this)
or the transition is rejected. No more silent resurrection at
boot; the failure is visible and actionable at source.

The orphan-task reconcile disappears entirely — the `finishRun`
guard already enforces the invariant at the one place a run can
leave the in-progress state, and `recoverStaleLocks` handles
stuck executions at runtime.

The secrets migration now throws when neither
`settings.current_company_id` nor any company row exists, rather
than binding legacy ciphertext to `00000000-0000-0000-0000-000000000000`.
Legacy rows stay on the old table until the operator creates a
real company, at which point the upgrade succeeds deterministically.

Tests rewritten: the reconciliation regression suite now asserts
the transition-time guard and the lone `interruptStaleRuns`
behavior. `resolve-workspace.test.ts` catches up with the
exclusive-by-default lock policy committed earlier.
2026-04-19 01:12:38 -04:00
ZacharyZhang-NY cdf783c0b8 fix(dispatch): fail explicitly when no enabled model qualifies
`pickModelForAgent` used to fall back to the CEO's pair when the
auto-strategy scoring returned no winner, and then silently route
the run on that pair. Three distinct problems rendered invisible by
that fallback: a missing capability row for a deployed model, a
mis-inferred task profile, a misconfigured model pool. Cost
attribution became meaningless — the agent ran on the CEO's model,
not its own tier — and operators had no signal to fix the root
cause.

The picker now returns a `ModelPickResult` tagged union. Success
yields the dispatch-ready assignment; failure yields a
`NO_QUALIFYING_CANDIDATE` reason with a human-readable message
naming the agent, the required domain, and the required tier.

Heartbeat maps the failure to a new `model_pick_failed` skip
reason, logs an activity row so it shows up in the company audit
trail, and defers. Task-runner returns `fail("MODEL_PICK_FAILED",
...)` so the UI surfaces the message directly. `getCeoAssignment`
drops out of `PickModelDeps` entirely — no caller needs a silent
CEO path any more, so the dep surface shrinks and the tests do
too. The test suite gains a regression assertion for the explicit
failure case.
2026-04-19 01:06:50 -04:00
ZacharyZhang-NY 76c2f085b6 fix(session): evaluate rotation against post-accumulation totals
`accumulateAndRotateSession` called `evaluateRotation` on the
pre-accumulation `session` object, then re-read the row only
inside the shouldRotate branch. A run that tipped the token
threshold on its own usage would read "below threshold" on this
cycle and wait a full additional run before rotating — long
enough for the transcript to keep growing past the point where
rotation was supposed to protect the context window.

Read once after accumulation, evaluate against the fresh totals,
rotate from the same object. No new queries, no new branches — the
failure mode is just an off-by-one-run bug in the original
read-eval-rotate sequence.
2026-04-19 01:03:44 -04:00
ZacharyZhang-NY 4842a9a2e2 fix(workspace): hold every resolved workspace exclusively
agent-, project-, and company-fallback workspaces resolved to
`shared`, so four agents sharing one repo wrote concurrently and
corrupted both the working tree and git's own index state before
the per-file `index.lock` even got a chance to kick in. The
previous comment justified this as "the normal case", but pi's
`createCodingTools()` hands every agent bash/write/edit — there is
no read-only category left where shared is safe.

This promotes every resolved workspace to `exclusive`. Contention
is still handled at the one place it must be: `run-dispatch.ts`
synchronously claims the workspace lock, and `finishRun`'s
never-started branch rolls the task back to `todo` on a lost
race. The "shared mode prevents deadlocks" story was really
"shared mode prevents one specific bug in the checkout rollback
path that has since been fixed"; the right invariant is single
writer per repo, enforced by the DB lock. Concurrent heartbeats
for the same workspace now queue through the wakeup table on the
next cycle rather than racing the filesystem.
2026-04-19 01:03:13 -04:00
ZacharyZhang-NY 2cddf89717 feat(agentcompany-rust): scaffold native shell and runtime bridge 2026-04-19 01:02:20 -04:00
ZacharyZhang-NY 34814b4dfd feat(artifacts): project bash filesystem changes into work_products
The artifact projector only registered write/edit. Everything bash
produced — build artifacts, files written via shell redirection,
git commits, generated code — stayed invisible to the desktop UI
and the audit layer. Filesystem truth and UI truth were
systematically out of sync.

This lifts bash onto the same filesystem artifact protocol as
write/edit. When a bash tool call succeeds in a git workspace, the
projector runs `git status --porcelain=v1 -z --untracked-files=all`
and upserts a work_products row for every modified/untracked entry,
reusing the existing `(companyId, provider='filesystem', externalId)`
identity so repeated edits never duplicate. Non-git workspaces fall
through with an explicit "not_a_git_repo" skip reason — there is no
authoritative baseline there to diff against, and silently
half-registering would be worse than skipping.

A 50-file cap per invocation bounds the scan so a runaway rebuild
cannot flood the table; anything past the cap is logged as a skip.
Rename entries (emitted as `R new\0old`) advance the iterator past
the old-path entry so deletions of the rename source are not
double-registered.

The projector now always returns an array so `write`/`edit` (one
path) and `bash` (many paths) share one shape, and the hook handler
can log uniform outcomes.

.gitignore gains `AgentCompany-Rust/` — the separate Rust rewrite
tree lives in the same repo root but is not part of the desktop app
build and should not drift into feature commits.
2026-04-19 01:01:29 -04:00
ZacharyZhang-NY 5ee3d1394a feat(messaging): route messages to live runs via pi steer
Incoming messages (human → agent, agent → agent, channel broadcast)
persisted to the inbox and were only projected into the recipient's
transcript on the next `prepareRun`. When the recipient was already
running, every message waited for the next wake — a Slack-like
system operating at heartbeat cadence, not in real time.

`RuntimeMessageInjector` closes the gap: it looks up the recipient's
active run through `RuntimeBridge.findActiveRunForAgent`, projects
the persisted `AgentMessageRecord` via the existing inbox-projection
helper, and posts a `steer-run` ParentMessage into the worker. The
worker's runner enqueues the projection on the Agent's steering
queue; pi drains it at the next turn boundary and emits the usual
`message_end` → `persistMessage` sequence, so the session store
shadow is preserved without double-writing.

`dispatchAgentMessage` gains an optional `injectRuntimeMessage` hook
used by every API-side dispatch. Injection succeeds → we mark the
inbox row delivered and skip `wakeAgentIfPossible`. Injection fails
(no active run) → the legacy inbox + wake path still fires, so
sleeping agents continue to receive their messages on the next turn.

The injector is owned by the service graph alongside `workerService`
and `piRuntimeRef`, which gives it both the worker postMessage
channel and the RuntimeBridge active-run map without additional
coupling. Internal dispatch call sites (sweepStaleReviews,
automation actions) leave the hook unset; those system messages
target idle agents by design and do not need the runtime path.
2026-04-19 00:58:39 -04:00
ZacharyZhang-NY 795008f56b fix(runtime): enforce approval + wait-state gate in beforeToolCall
Approval creation already flowed through the API server, but pi's
`bash` / `write` / `edit` tools never traversed that layer, so the
gate was purely advisory: the agent kept mutating the workspace while
the board tried to hold it. The fix: policy runs at the one place
every destructive tool invocation must pass, pi's `beforeToolCall`.

`checkApprovalBlock` fails closed on lookup errors — an unreadable
approvals table is a safety incident, not a licence to execute. It
re-reads `getBlockingApproval` on every call so an approval created
mid-run takes effect on the next tool without any cache.

`checkWaitStateBlock` rides the unified
`hasUnresolvedWaitStateForAgent` query so task, approval, and
freeform thread wait states all gate the same tools. Read-only tools
are untouched; only bash/write/edit are gated because gating
observation would slow investigation without adding safety.

The destructive-tool set is promoted to a named predicate so the
Task 5 artifact projector can share the same definition when it
extends to bash outputs.
2026-04-19 00:51:54 -04:00
ZacharyZhang-NY 6a37f41dfd fix(threads): unify wait-state ownership across thread types
`hasUnresolvedWaitStatesForAgent` only joined through tasks, so
approval-only threads and ad-hoc threads where the agent is the
requesting party or an explicit participant were invisible. Result:
the heartbeat scheduler silently resumed agents that the board had
asked to wait on, Co-Founder approval chains deadlocked on the
agent side, and any new gate that relies on the query inherits the
same hole.

Root-fix the query at the SQL layer. `hasUnresolvedWaitStateForAgent`
now returns true when any of the three ownership predicates hold:

  - task thread whose task.assignee_agent_id = agent
  - approval thread whose approval.requested_by_agent_id = agent
  - any thread on which the agent is a recorded participant

Each branch is an EXISTS subquery so the query short-circuits on
first match. The thread-integration helper delegates here so the
heartbeat scheduler and the `beforeToolCall` gate share one source
of truth rather than reimplementing half-policies in parallel.
2026-04-19 00:51:40 -04:00
ZacharyZhang-NY e1693899a2 feat(runtime): wire pi-agent-core hooks end-to-end
The worker previously constructed `new Agent({systemPrompt, model, tools,
messages, streamFn})` and stopped there — transformContext,
beforeToolCall, afterToolCall, sessionId, thinkingLevel, and steer/
followUp were all dark. Approval enforcement, artifact projection,
auto-compaction, and mid-run steering all had to reach the agent
through other, non-canonical channels (event observers, prompt-block
injection at prepareRun, API-layer gates). That left the whole pi
contract at one-tenth utilisation and forced business logic into the
wrong seams.

This change rebuilds the contract assembly layer:

  - `pi-agent-factory` builds the Agent with the full option surface:
    sessionId (taskSessionId), thinkingLevel (mapped from
    agent.metadataJson), and three RPC-proxied hooks.
  - Each hook shuttles a thin payload over the existing worker RPC
    channel into `RuntimeBridge`, which routes to `hook-handlers`:
      * beforeToolCall → approval-gate policy (blocks destructive
        tools while a pending approval is outstanding).
      * afterToolCall  → filesystem artifact projection, previously
        nested in the tool_execution_end event observer.
      * transformContext → pass-through today; compaction wires in
        next commit.
  - ParentMessage gains `steer-run` / `followup-run`; `WorkerRunner`
    keeps a runId→Agent registry so main can inject mid-run messages
    into the active pi loop.

`worker-types` extends the rpc-request method enum; `runtime-bridge`
loses the per-event artifact projector and gains a `findActiveRunForAgent`
lookup used by the upcoming inbox → steer path. No public main API
changes yet — the wiring is the foundation for the next five commits.
2026-04-19 00:48:13 -04:00
ZacharyZhang-NY f041336721 refactor: nuke dead exports, dead files, dead DB column, dead zero-value wrappers
Full-codebase ts-prune + manual audit sweep. Every change below is a pure
deletion (or a 2-line re-wire where a dead function was supposed to be
alive). No scope additions.

Dead wrappers / phantom features
  - shouldAutoApproveApproval: zero-value wrapper over shouldAutoApprove.
    Four call sites now import shouldAutoApprove from execution-policy
    directly.
  - evaluateRotation(session, connectorId, thresholdsOverride): the
    connectorId arg was never read (underscored), and thresholdsOverride
    was only exercised by the test suite — no production caller. Signature
    collapses to evaluateRotation(session). DEFAULT_THRESHOLDS /
    RotationThresholds exports and their tests go with it.
  - installLogRedactor: defensive console wrapper that was never wired
    from worker-entry. buildRedactor handles the known threat (env-value
    leaks); the pattern-based redactTokens stays for tests and future
    composition.

Dead files
  - resume-interrupted-tasks.ts: helper function never imported.
  - service-graph-orchestration-deps.ts: three factory closures
    superseded by the inline closures in create-orchestration-service.

Dead exports
  - listConnectorsByAuthKind (shared/connector-catalog)
  - getLocalePromptInstruction + whole localization.ts file
  - ImportantDocType
  - main-side ConnectorAuthKind (duplicated the shared one; not referenced)
  - CheckpointEngineService, RunHousekeepingService types
  - WorkspaceResolution type
  - cmdOrCtrlSymbol
  - AgentEventPush, CredentialPayload type aliases
  - parseApprovalPayloadJson, parseEnvValue, ProviderCard
  - getSOP — buildSOPSection reads the SOPS map directly; the individual
    getter had no callers. Deduped to a flat array.
  - renderWakeReason in wake-reasons/en.ts: byte-for-byte duplicate of the
    one in wake-reasons/index.ts.

Forgotten wiring (not cleanup, but adjacent)
  - registerAgentStandupRoute: the GET /api/agents/me/standup endpoint is
    advertised in every system prompt and wake-reason doc, but the route
    handler was never registered in api-server/index.ts. Agents have been
    hitting 404 since Pi Mono. Registered.

Dead DB column
  - v44 migration drops runs.context_snapshot_json. Column was added by
    v16-v25, never written or read, and v43's deliberate preservation
    was an interim measure. Same idempotent table-rebuild pattern as
    v41/v43.

Test tree
  - Phantom-feature threshold override tests removed (3). The rest of
    the rotation suite re-expressed against the real maxRuns=20 floor.

495/495 unit tests pass. typecheck + build clean.
2026-04-18 23:37:37 -04:00
ZacharyZhang-NY e0e43d6127 refactor(session): nuke dead sessionParams / sessionDisplayId pipeline (v43)
These columns and the messages that plumbed them existed for the CLI-
adapter era — Gemini's resume id and provider-specific session params
rode the run-finished envelope back onto the session row so the next
turn could resume. Pi Mono dispatches every provider through pi-ai and
emits neither value. The worker has been posting literal null on every
finish, finalizeRun has been writing null into task_sessions, and
rotateSession has been carrying null forward.

Remove the wire, the writer, the carry-forward, and the columns:

  - v43 migration rebuilds task_sessions without session_params_json /
    session_display_id, and runs without session_display_id. Follows
    the idempotent "create new, copy, swap, reindex" pattern used by
    v41. context_snapshot_json is preserved (also unreferenced today
    but outside this cleanup's scope).
  - RunJob / EnqueueRunPayload / ChildMessage / worker-types zod
    schema no longer carry the fields. finishRun() / createNewSession()
    signatures tighten in lockstep.
  - rotateSession starts the new session with fresh counters and no
    carry-forward.
  - turn-context resolveSession returns just the session id.
  - TaskSessionRecord / RunRecord lose the dead fields.

498/498 unit tests pass. typecheck + build clean.
2026-04-18 23:22:17 -04:00