Skip to content

Perf/avoid shell snapshot for thread detail - #33

Merged
ronak-guliani merged 8 commits into
mainfrom
perf/avoid-shell-snapshot-for-thread-detail
Jul 20, 2026
Merged

ronak-guliani merged 8 commits into
mainfrom
perf/avoid-shell-snapshot-for-thread-detail

Conversation

@ronak-guliani

Copy link
Copy Markdown
Owner

What Changed

Why

UI Changes

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

ronak-guliani and others added 5 commits July 17, 2026 13:48
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Jul 19, 2026

@ronak-guliani ronak-guliani left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

The core perf change (thread-detail route now reads getSnapshotSequence() instead of building the full getShellSnapshot()) is a clean, well-targeted win — nice. The new durable workflow-coordinator machinery is generally solid: stable idempotency keys for crash recovery, event-store/projector/read-model all threaded through, and good test coverage.

I focused on bugs, reliability, performance, and maintainability. One high-severity correctness bug in the decider (the missing-parent finalize path is unreachable and leaves a run stuck forever), plus a few medium/low items around the review-snapshot behavior change and N+1 query cost. Details inline.

!node ||
(node.status !== "completed" && node.status !== "failed" && node.status !== "pending") ||
(node.status === "pending" && command.status !== "failed") ||
command.status !== node.status ||

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug (high): the pending → failed finalize path is unreachable, so "parent deleted before worker started" runs get stuck forever.

WorkflowCoordinatorReactor.startWorker dispatches workflow.run.finalize with status: "failed" while the node is still pending (the finalize-missing-parent command). The check just above at lines 1378–1379 explicitly allows node.status === "pending" as long as command.status === "failed", and the projector at projector.ts (workflow.run-finalized) has dedicated logic to flip a pending node to failed. But this line unconditionally requires command.status === node.status, so for a pending node + failed command it evaluates "failed" !== "pending" → true → invariant error.

Because the engine records a permanent rejected receipt for invariant failures (OrchestrationEngine.processEnvelope), the finalize can never succeed on retry. The run stays pending/incomplete, listIncomplete() keeps returning it, and every subsequent workflow/thread domain event re-triggers reconcile → startWorker → dispatch finalize → PreviouslyRejected → logWarning — a permanently stuck run plus recurring log noise.

Suggested fix: scope the equality check to non-pending nodes, e.g.

(node.status !== "pending" && command.status !== node.status) ||

This path isn't covered by tests today — the reactor test stubs the engine dispatch, so it verifies the command is emitted but not that the decider accepts it. Worth a decider-level test for pending+failed.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the local follow-up. The finalize invariant now permits the intentional pending -> failed transition while still requiring non-pending node status to match the command. A decider-level regression exercises the emitted finalize-missing-parent command against the real decider and verifies the run reaches failed. Pending commit/push to the PR branch.

const bytes = Buffer.from(fullDiff);
const truncated = bytes.byteLength > REVIEW_SNAPSHOT_MAX_BYTES;
return {
if (Buffer.byteLength(fullDiff) > REVIEW_SNAPSHOT_MAX_BYTES) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavior change (medium): silent truncation replaced by a hard failure. Previously a diff over the limit was truncated (truncated: true) and review continued; now anything over 4MB fails with a GitCommandError and truncated is always false.

Raising the cap to 4MB and reviewing the full diff is a good correctness improvement, but please confirm the two consumers degrade gracefully:

  • ws.ts review-changes workflow start — a >4MB repo diff now fails the RPC/workflow instead of producing a (truncated) review. Is that the intended UX vs. e.g. a workflowSkipped reason?
  • ReviewSnapshotVerifier.isCurrent — see the note in ProviderRuntimeIngestion.ts.

Also, ReviewSnapshot.truncated is now hard-coded false everywhere. If nothing can ever set it true, consider removing the field and any UI that renders a "truncated" indicator so it doesn't become dead/misleading code.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed intentional: reviewing a partial diff is correctness-unsafe, so snapshots over 4 MiB fail explicitly rather than producing a misleading partial review. The local follow-up aligns each review-diff command output ceiling with the 4 MiB aggregate limit, removes the impossible ReviewSnapshot.truncated field, makes verification failures discard the result without aborting turn completion, and uses NUL for native Windows untracked diffs. Pending commit/push.

});
if (
cwd === null ||
!(yield* reviewSnapshotVerifier.isCurrent({ cwd, snapshot: thread.reviewSnapshot }))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reliability (medium/low): a git failure in isCurrent is not the same as "worktree changed". reviewSnapshotVerifier.isCurrent now calls resolveReviewChangesContext, which can fail (git error, or the diff having grown past the new 4MB cap). Only the boolean-false result is handled here (log + discard). A failure instead propagates and is swallowed by processInputSafely's catchCause as a generic "provider runtime ingestion failed to process event" — the turn.completed handling for that event is aborted and the review result is permanently dropped (the event isn't retried).

Consider catching the verifier's error and treating it as "could not confirm snapshot is current → discard" with the specific warning, so a transient git hiccup doesn't masquerade as an ingestion failure.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the local follow-up. isCurrent failures are logged as snapshot-verification failures and converted to false; the review result is discarded fail-closed, but turn.completed ingestion continues instead of being aborted by the generic catch. Pending commit/push.

Effect.mapError(toPersistenceSqlError("ProjectionWorkflowRepository.listIncomplete:query")),
);

const listAll: ProjectionWorkflowRepositoryShape["listAll"] = () =>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance (low/medium): N+1 node queries. Both listAll and listIncomplete fetch runs, then issue one listNodeRows per run with concurrency: 1 (sequential). listAll() runs in the full-snapshot path (ProjectionSnapshotQuery.getSnapshot/getShellSnapshot), which is exactly the hot path this PR is trying to keep cheap. With many runs this is O(runs) sequential round-trips.

Consider a single SELECT ... FROM projection_workflow_nodes WHERE run_id IN (...) (or a JOIN) and group in memory, matching how the other projections avoid per-row fan-out.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the local follow-up. listAll and listIncomplete now fetch all node rows for the selected run IDs in one IN (...) query and group them in memory. The reconnect shell path also uses this batched query through a bounded lightweight snapshot. Pending commit/push.

listIncompleteRunRows(undefined).pipe(
Effect.flatMap((rows) =>
Effect.forEach(
rows.filter((row) => isIncompleteWorkflowRun(row.status)),

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: this .filter(isIncompleteWorkflowRun(...)) is redundant with the SQL WHERE status IN ('pending','running') in listIncompleteRunRows. Keep one source of truth (either the SQL predicate or the code filter) to avoid the two drifting.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the local follow-up. The redundant application-level incomplete-status filter was removed; the SQL predicate is now the single source of truth. Pending commit/push.

typeof value.body !== "string" ||
typeof value.confidence_score !== "number"
) {
return value;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavior change (confirm intent): a single non-normalizable finding now invalidates the entire review. Previously non-matching findings were dropped (flatMap → []); now the raw value is returned, which won't match the ReviewFinding schema, so decodeReviewModelOutput throws and the whole result becomes invalid-output. Combined with returning null at line 27 for non-Codex output, this makes parsing much stricter.

That's defensible (don't silently swallow findings), but it's a meaningful semantic shift — one malformed finding discards an otherwise-valid review. Please confirm this is intended rather than per-finding filtering.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The local follow-up retains per-finding tolerance: non-normalizable findings are filtered while the overall envelope still must satisfy the review schema. One malformed finding therefore no longer discards otherwise valid findings. Pending commit/push.

Comment thread apps/web/src/store.ts Outdated
createWorkflowRuntimeState(),
ref.threadId,
)
: [];

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the : [] fallback returns a fresh array each call, which is the exact footgun the new AGENTS.md scar ("selectors must return a referentially stable snapshot… fresh arrays can trigger React pingdotgg#185") warns about. It's currently safe because the only consumer wraps it in useShallow, but a shared frozen EMPTY constant would make it robust regardless of how future callers subscribe.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the local follow-up. The selector now returns a shared frozen EMPTY_WORKFLOW_RUNS value, so referential stability no longer depends on every caller remembering useShallow. Pending commit/push.

- decider: allow the pending -> failed finalize path so runs whose parent
  was deleted before the worker started can be finalized instead of getting
  stuck on a permanent invariant rejection
- provider runtime ingestion: treat a git failure while verifying the review
  snapshot as "cannot confirm current -> discard" instead of letting it
  surface as a generic ingestion failure that drops the event without retry
- projection workflows: batch node lookups for listAll/listIncomplete with a
  single WHERE run_id IN (...) query and drop the redundant status filter now
  covered by SQL
- review result: drop non-normalizable Codex findings per-finding instead of
  invalidating the entire review
- web store: return a referentially stable frozen EMPTY_WORKFLOW_RUNS snapshot
- tests: cover pending+failed finalize decider path and batched workflow node
  grouping

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ronak-guliani

Copy link
Copy Markdown
Owner Author

Consolidated follow-up review

Status: the fixes below are implemented and validated in the local follow-up working tree, but are not yet committed or pushed to the PR branch. I replied to each of the seven existing inline threads with its concrete disposition.

Existing review feedback

  • Fixed the unreachable pending -> failed workflow finalization path and added a real-decider regression.
  • Kept the explicit failure for review snapshots over 4 MiB because a partial review is correctness-unsafe; aligned command output limits, removed the dead truncated field, and made verification failures discard safely without aborting turn completion.
  • Replaced sequential per-run node queries with one batched query and removed the redundant incomplete-run filter.
  • Preserved per-finding tolerance for malformed review findings.
  • Replaced the fresh selector fallback array with a shared frozen empty value.

Additional issues found in the fresh review

  • Recovered workflows could remain stuck when a provider runtime was alive but its turn was idle. Reconciliation now uses activeTurnId and latest-turn state rather than runtime liveness.
  • Tool-only completed turns never finalized. They now emit a completed worker result with a deterministic fallback body.
  • Archiving a parent left descendant provider sessions and terminals alive. Archive traversal now covers the full subtree, including active descendants below archived intermediaries, and cleanup stops every affected runtime/terminal.
  • Review diffs between the old 1 MiB process limit and the advertised 4 MiB snapshot limit failed prematurely; command limits now match the aggregate cap.
  • Native Windows untracked-diff capture used /dev/null; it now uses NUL on Windows.
  • Live client workflow status could diverge from the server projection. Worker-start and failed-finalization transitions are now mirrored client-side.
  • Running virtual agents could be dismissed. Dismissal is limited to terminal runs and persisted using stable run keys.
  • Reconnect shell snapshots loaded unbounded workflow history and unnecessary payloads. They now retain all active runs plus the latest 20 terminal runs per parent and load only result/final artifacts.
  • The ChatMarkdown browser regression used an unreliable role lookup; it now targets the rendered table header directly.
  • A provider-reactor ordering test could race under full-suite load; it now waits for the ordering side effect it asserts.

Local validation

pnpm fmt:check, pnpm lint, pnpm typecheck, pnpm test, and the targeted ChatMarkdown Chromium test pass. No known high-confidence issue remains from the completed review passes.

The current GitHub Windows Smoke failure is against the existing remote head and should be rerun after the Windows follow-up is pushed.

ronak-guliani and others added 2 commits July 19, 2026 18:21
- reconcile recovered workflows via activeTurnId/latest-turn state instead of
  runtime liveness so alive-but-idle provider runtimes no longer stay stuck
- finalize tool-only completed turns with a deterministic fallback worker body
- cascade thread archival across the full active subtree (including active
  descendants beneath already-archived intermediaries) and stop every affected
  provider runtime/terminal; extract collectActiveThreadSubtree
- populate parentThreadId on thread.created so subtree traversal finds children
- align git command output limits with the 4 MiB aggregate review-snapshot cap
  and use NUL instead of /dev/null for untracked-diff capture on Windows
- remove the now-dead ReviewSnapshot.truncated field
- mirror worker-start and failed-finalization workflow transitions client-side
- limit sidebar agent-run dismissal to terminal runs, persisted by stable keys
- bound reconnect shell snapshots to all active runs plus the latest 20 terminal
  runs per parent and load only result/final artifacts
- collapse subtree session mapping into a single flatMap pass
- target the rendered table header directly in the ChatMarkdown browser test
- wait for the asserted ordering side effect in the provider-reactor test

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…shot-for-thread-detail

# Conflicts:
#	apps/web/src/components/ChatMarkdown.browser.tsx
@ronak-guliani
ronak-guliani merged commit 1d261be into main Jul 20, 2026
5 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant