Skip to content

perf(server): t3_task_read returns only changed tasks after a cursor - #178

Merged
jayleaton merged 1 commit into
mainfrom
perf/task-read-deltas
Oct 31, 2026
Merged

jayleaton merged 1 commit into
mainfrom
perf/task-read-deltas

Conversation

@jayleaton

Copy link
Copy Markdown
Owner

Summary

Captains call t3_task_read with no threadId on every wake, and each call returns every task they own in full. The read now takes an optional afterCursor:

t3_task_read {}                    -> every task + cursor            (unchanged)
t3_task_read { afterCursor: 3301 } -> tasks changed since 3301
                                      + removed: [chats whose task moved to another owner]
                                      + cursor
  • Tasks are never deleted, so a task leaves an owner's view only when it moves to another owner. ThreadTaskStore.put now records that move in a new thread_task_departures table (fork migration 14) under the move's cursor, in the same transaction as the write. If the task later returns to that owner, the departure row is deleted.
  • Settlement already writes the task under a new cursor, so a settled task shows up in the delta like any other change.
  • read and t3_task_watch now share one function, visibleChanges. As a result, watch also reports removed. The watch catalog sentence is unchanged; another PR is editing it.
  • Remote (other-environment) tasks go through the same path: an agent's read runs on its own environment, and tasks on other machines are local mirror rows written through the same put.
  • Changes to the contract (ThreadTaskReadInput.afterCursor, optional ThreadTaskListResult.removed), the gateway's zod schema, and the t3_task_read description.

Evidence

  • Before: on main, t3_task_read { afterCursor } is rejected by the gateway's strict input schema (afterCursor is not a key there), so a Captain has to read every task on every wake.
  • After: a new case in ThreadTaskService.test.ts uses three owned tasks. The full read returns all three. A read after the cursor returns only the one task that changed. A settled task appears with settlement.state = "settled". A task moved to another owner comes back as removed: ["gamma"] and is gone from the full read. Moved back, it comes back as a changed task, not as a removal.
vp test run apps/server/src/threadTask packages/mcp-gateway
 Tests  283 passed

Typecheck passes for contracts, mcp-gateway, and server; vp fmt --check and vp lint pass on the touched files. ForkMigrations.test.ts > keeps upstream's migration ids and names fails locally only because this machine's upstream/main ref is 6 days stale (69 upstream migrations vs 72 on main). It checks the upstream ledger, which this PR does not touch.

No eval covers t3_task_read's text, so no eval was run.

Merge Danger

Door: two-way

The migration only adds a table (CREATE TABLE IF NOT EXISTS). Reverting leaves it unused.

Blast Radius: tasks

Every task write now does one more indexed select inside a transaction. Readers that don't pass afterCursor get exactly what they got before.

🤖 Generated with Claude Code

Captains call t3_task_read with no threadId on every wake and get every
task they own in full. The read now takes an optional afterCursor and
returns only tasks changed since then, plus the new cursor. A task that
moves to another owner is recorded as a departure under the move's
cursor and comes back in `removed`, so a delta reader drops it.
Settlement already bumps the cursor. t3_task_watch uses the same
visibility and cursor logic, so it reports departures too. Without
afterCursor the read is the full list, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Oct 31, 2026
@github-actions

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ The exact PR base did not have a successful artifact. Baseline uses the latest successful main measurement shown below.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB −6 B (−0.1%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.3 KiB 1.2 KiB −6 B (−0.5%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 21.6 KiB 21.6 KiB 0 B (0.0%) 24.4 KiB ✅
Codex Live turn messages 2 2 0 (0.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.1 KiB +6 B (+0.1%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.3 KiB 1.3 KiB +6 B (+0.5%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.9 KiB 21.9 KiB 0 B (0.0%) 24.4 KiB ✅
Claude Live turn messages 2 2 0 (0.0%) 8 ✅

Baseline: e2d81ca · PR result: c250f61 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@jayleaton
jayleaton marked this pull request as ready for review October 31, 2026 03:17
@jayleaton
jayleaton merged commit 8d1b3cf into main Oct 31, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 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