authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-02 22:25:00-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-02 22:25:00-07:00
logddcd3102827f520e3dff53b902111744c0ba2ced
tree6558d617b96947fabc5be0e326d841bd1de594fb
parentc5a929e1ac05dec15945a36905ab8bb2f1982e59
signature Signed by PGP key B5690EEEBB952194

chore: docs, dev scripts and CI catch up with orchestration V2 (#15041)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

29 files changed, 305 insertions(+), 417 deletions(-)

.agents/skills/test-t3-app/references/sqlite-fixtures.md+7-16
...@@ -4,7 +4,9 @@ Load this reference only when inspecting or seeding local T3 state directly....@@ -4,7 +4,9 @@ Load this reference only when inspecting or seeding local T3 state directly.
44
5## Select the correct database5## Select the correct database
66
7When `--base-dir` or `--home-dir` is explicit, runtime state lives under `<base-dir>/userdata` and the database path is `<base-dir>/userdata/state.sqlite`. The `<base-dir>/dev` state directory is only the fallback for an implicit development home, preventing an ordinary `vp run dev` from touching production state.7When `--base-dir` or `--home-dir` is explicit, runtime state lives under `<base-dir>/userdata` and the database path is `<base-dir>/userdata/statev2.sqlite`. The `<base-dir>/dev` state directory is only the fallback for an implicit development home, preventing an ordinary `vp run dev` from touching production state.
8
9The server copies the V1 `state.sqlite` into `statev2.sqlite` only when `statev2.sqlite` is missing. After the first start, edits to `state.sqlite` change nothing.
810
9Start the target runtime once before seeding so all migrations have run. Use an isolated base directory. Stop the server before writes to avoid racing application state or an active projection.11Start the target runtime once before seeding so all migrations have run. Use an isolated base directory. Stop the server before writes to avoid racing application state or an active projection.
1012
...@@ -23,7 +25,7 @@ Inspect current columns before writing a fixture:...@@ -23,7 +25,7 @@ Inspect current columns before writing a fixture:
23```bash25```bash
24node apps/server/scripts/t3-sqlite-state.ts query \26node apps/server/scripts/t3-sqlite-state.ts query \
25 --base-dir <base-dir> \27 --base-dir <base-dir> \
26 --sql "PRAGMA table_info(projection_threads)"28 --sql "PRAGMA table_info(orchestration_v2_projection_threads)"
27```29```
2830
29Apply a SQL fixture from a file:31Apply a SQL fixture from a file:
...@@ -38,21 +40,10 @@ Use one statement per invocation for both `query` and `exec`; the helper wraps w...@@ -38,21 +40,10 @@ Use one statement per invocation for both `query` and `exec`; the helper wraps w
3840
39## Seed projection data carefully41## Seed projection data carefully
4042
41The web UI primarily reads these projection tables:43Clients read projects from `projection_projects` and everything else from the `orchestration_v2_projection_*` tables: threads, runs, messages, turn items, runtime requests, plans, and provider sessions. The older `projection_thread*` tables hold V1 history, which the server imports once per thread at startup; later edits there do not reach the UI.
42
43- `projection_projects`
44- `projection_threads`
45- `projection_thread_messages`
46- `projection_thread_activities`
47- `projection_thread_sessions`
48- `projection_turns`
49- `projection_pending_approvals`
50- `projection_thread_proposed_plans`
51
52Inspect `PRAGMA table_info(<table>)` and the current migrations under `apps/server/src/persistence/Migrations/` before constructing inserts. Keep identifiers unique, timestamps as ISO strings, JSON columns valid, and related project/thread/turn IDs consistent.
5344
54For a substantial current example, inspect `seedDatabase` in `scripts/mobile-showcase-environment.ts`. Adapt its column set to the target database instead of assuming copied SQL remains current.45Most V2 rows carry a `payload_json` that must decode against the schemas in `packages/contracts/src/orchestrationV2.ts`. The safest start is a row the app wrote itself: create a thread through the UI, copy its rows, and edit them. Keep identifiers unique, timestamps as ISO strings, and related project, thread, and run IDs consistent.
5546
56Direct projection writes are appropriate for ephemeral visual states, edge-case counts, long titles, activity lists, and similar UI fixtures. They do not create a coherent orchestration event history. Do not modify `orchestration_events` unless the test specifically exercises projector internals, and do not use direct projection writes to claim backend business behavior works.47Direct projection writes are appropriate for ephemeral visual states, edge-case counts, long titles, long timelines, and similar UI fixtures. They do not create a coherent event history. Leave the event log (`orchestration_events`) unchanged, and do not use direct projection writes to claim backend business behavior works.
5748
58Use the app's commands or APIs for behavior tests. Use `node apps/server/src/bin.ts auth ...` for auth state rather than editing `auth_pairing_links` or `auth_sessions`.49Use the app's commands or APIs for behavior tests. Use `node apps/server/src/bin.ts auth ...` for auth state rather than editing `auth_pairing_links` or `auth_sessions`.
.github/workflows/ci.yml+1
...@@ -487,6 +487,7 @@ jobs:...@@ -487,6 +487,7 @@ jobs:
487 test,487 test,
488 test_web,488 test_web,
489 test_server,489 test_server,
490 transfer-report,
490 rust,491 rust,
491 mobile_native_changes,492 mobile_native_changes,
492 mobile_native_static_analysis,493 mobile_native_static_analysis,
AGENTS.md+6-15
...@@ -88,17 +88,8 @@ The most common defect in this repo is a change that works on the path you teste...@@ -88,17 +88,8 @@ The most common defect in this repo is a change that works on the path you teste
8888
89An empty database is a bad test. Seed your worktree's `.t3` with a copy of real data instead of pointing at live state:89An empty database is a bad test. Seed your worktree's `.t3` with a copy of real data instead of pointing at live state:
9090
91- Copy from `~/.t3/userdata` (the developer's real data, the most realistic test set) or `~/.t3/dev`. Worktree state lives at `<worktree>/.t3/userdata`.91- Run `vp run migrate-dev-db` with your dev server stopped. It rebuilds `<worktree>/.t3/userdata/statev2.sqlite` from a read-only snapshot of `~/.t3/userdata/statev2.sqlite`, the developer's real data. It keeps recent projects and their stopped threads, and drops scheduled tasks, pending work, and auth sessions, so your dev server never runs the developer's agents. Raise `--projects` and `--threads-per-project` for more data.
92- Snapshot the database with `VACUUM INTO`, which is safe even while a server has the source open and yields one consistent file:92- Refresh `statev2.sqlite`, not `state.sqlite`. The server copies the V1 `state.sqlite` only when `statev2.sqlite` is missing.
93
94 ```bash
95 mkdir -p .t3/userdata
96 rm -f .t3/userdata/state.sqlite* # VACUUM INTO refuses to overwrite
97 bun -e "new (require('bun:sqlite').Database)(process.env.HOME + '/.t3/userdata/state.sqlite', { readonly: true }).run(\"VACUUM INTO '.t3/userdata/state.sqlite'\")"
98 ```
99
100 A plain `cp` is only safe when no server has the source open, and must bring the `-wal` and `-shm` siblings along. A live file copy is a corrupt copy.
101
102- Bring `secrets` and `settings.json` only if the flow under test needs them.93- Bring `secrets` and `settings.json` only if the flow under test needs them.
103- Copy in, never symlink. Data flows one way: into your sandbox, never back out.94- Copy in, never symlink. Data flows one way: into your sandbox, never back out.
10495
...@@ -108,7 +99,7 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real...@@ -108,7 +99,7 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real
108- Test meaningful logic or observable behavior. Do not render components to static markup to assert props or attributes, or add tests that merely assert callback wiring or mirror the implementation.99- Test meaningful logic or observable behavior. Do not render components to static markup to assert props or attributes, or add tests that merely assert callback wiring or mirror the implementation.
109- **Do not run repo-wide checks.** No `vp check`, no `vp run -r test`, no `vp run -r typecheck` unless I ask. CI owns the full suite.100- **Do not run repo-wide checks.** No `vp check`, no `vp run -r test`, no `vp run -r typecheck` unless I ask. CI owns the full suite.
110- Backend behavior changes ship with focused tests for that behavior.101- Backend behavior changes ship with focused tests for that behavior.
111- The server is event-sourced and its async flows emit typed receipts. Wait on receipts and worker drains, never on sleeps or polling. A test that needs a timeout to pass is wrong.102- The server is event-sourced, and side effects run after the command commits. In tests, drain the effect worker (`OrchestrationEffectWorkerV2.drain`) or await the specific persisted event or `Deferred` that marks the milestone. Never wait on sleeps or polling. A test that needs a timeout to pass is wrong.
112- Upon request, user-visible frontend changes should get one integrated pass in a real client: `test-t3-app` for web, `test-t3-mobile` for mobile. The primary agent does this once after integrating. Subagents do not launch their own dev servers. Ask permission before doing computer use or spinning up browsers.103- Upon request, user-visible frontend changes should get one integrated pass in a real client: `test-t3-app` for web, `test-t3-mobile` for mobile. The primary agent does this once after integrating. Subagents do not launch their own dev servers. Ask permission before doing computer use or spinning up browsers.
113104
114For authorized mobile verification, a missing or outdated native client is a build step, not a blocker. Run `node scripts/mobile-native-client.ts ensure <ios|android> <device-id>` on the simulator host before starting Metro. It checks the local Expo fingerprint and builds/installs when needed. See `test-t3-mobile` for the full workflow.105For authorized mobile verification, a missing or outdated native client is a build step, not a blocker. Run `node scripts/mobile-native-client.ts ensure <ios|android> <device-id>` on the simulator host before starting Metro. It checks the local Expo fingerprint and builds/installs when needed. See `test-t3-mobile` for the full workflow.
...@@ -120,7 +111,7 @@ For authorized mobile verification, a missing or outdated native client is a bui...@@ -120,7 +111,7 @@ For authorized mobile verification, a missing or outdated native client is a bui
120- Body: the problem in a sentence or two, then how you fixed it. End with the model and harness that did the work.111- Body: the problem in a sentence or two, then how you fixed it. End with the model and harness that did the work.
121- UI changes need before/after images. Motion or timing needs a short video.112- UI changes need before/after images. Motion or timing needs a short video.
122- Upload PR evidence to GitHub. Never commit PR-only screenshots or assets such as `.github/pr-assets/`.113- Upload PR evidence to GitHub. Never commit PR-only screenshots or assets such as `.github/pr-assets/`.
123- One concern per PR. If the description says "also", split it.114- One request is one PR. Split it only when the maintainer asks. Outside contributions follow the stricter [one problem per PR](CONTRIBUTING.md#one-problem) rule.
124- When babysitting: poll checks and comments newer than the last push, verify each bot finding against the source, fix real ones, dismiss false positives with a written reason. Stay quiet when nothing is new. Stop when the bots are green on the latest commit.115- When babysitting: poll checks and comments newer than the last push, verify each bot finding against the source, fix real ones, dismiss false positives with a written reason. Stay quiet when nothing is new. Stop when the bots are green on the latest commit.
125116
126## Documentation117## Documentation
...@@ -143,9 +134,9 @@ Most code changes do not need an internal documentation change. Agents can read...@@ -143,9 +134,9 @@ Most code changes do not need an internal documentation change. Agents can read
143134
144## How it works135## How it works
145136
146Clients send typed WebSocket requests. The server turns them into _commands_, a pure _decider_ turns commands into persisted _events_, and a _projector_ derives the read model the UI renders. Provider CLIs run as subprocesses; per-provider _adapters_ translate their native protocols into orchestration events. Side effects run in queue-backed _reactors_ that emit _receipts_ when milestones land. Each turn ends with a _checkpoint_, a hidden git ref, so the app can diff and restore.137Clients send typed WebSocket requests. The server turns them into _commands_. The _orchestrator_ (`apps/server/src/orchestration-v2/Orchestrator.ts`) serializes commands and decides _events_ without doing any I/O. The _event sink_ commits those events, the _projections_ the UI reads, the _command receipt_, and _outbox_ effects in one transaction. The _effect worker_ then runs the effects, such as starting a provider turn or capturing a checkpoint, and feeds results back as commands. Provider CLIs run as subprocesses; per-provider _adapters_ translate their native protocols into orchestration events. Each turn ends with a _checkpoint_, a hidden git ref, so the app can diff and restore.
147138
148Full glossary with file links: `docs/internals/glossary.md`139Architecture and its constraints: `docs/internals/overview.md`. Glossary: `docs/internals/glossary.md`
149140
150## Where code lives141## Where code lives
151142
CONTRIBUTING.md+3
...@@ -70,6 +70,9 @@ Explain their relationship when it is not obvious. An adjacent cleanup, refactor...@@ -70,6 +70,9 @@ Explain their relationship when it is not obvious. An adjacent cleanup, refactor
70its own PR unless it is necessary to solve the same problem. A large diff alone does not establish that70its own PR unless it is necessary to solve the same problem. A large diff alone does not establish that
71the PR contains unrelated work.71the PR contains unrelated work.
7272
73This rule is for outside contributions. Maintainers, the logins in
74[.github/TRIAGE_EXEMPTIONS.td](.github/TRIAGE_EXEMPTIONS.td), may batch related fixes in one PR.
75
73Follow the [documentation rules](AGENTS.md#documentation). Keep internal docs for decisions and76Follow the [documentation rules](AGENTS.md#documentation). Keep internal docs for decisions and
74hard-to-discover constraints. Update user guides when how to use a feature changes; skip descriptions77hard-to-discover constraints. Update user guides when how to use a feature changes; skip descriptions
75of obvious controls and cosmetic changes.78of obvious controls and cosmetic changes.
apps/desktop/src/backend/DesktopBackendConfiguration.ts+1-1
...@@ -644,7 +644,7 @@ const resolveWslStartConfig = Effect.fn("desktop.backendConfiguration.resolveWsl...@@ -644,7 +644,7 @@ const resolveWslStartConfig = Effect.fn("desktop.backendConfiguration.resolveWsl
644 // The packaged sidecar is a Windows executable and cannot run inside the644 // The packaged sidecar is a Windows executable and cannot run inside the
645 // Linux WSL backend. Keep the field absent instead of passing an unusable645 // Linux WSL backend. Keep the field absent instead of passing an unusable
646 // `/mnt/.../*.exe` path; WSL resource telemetry is reported unavailable.646 // `/mnt/.../*.exe` path; WSL resource telemetry is reported unavailable.
647 // See docs/architecture/resource-telemetry.md.647 // See docs/internals/resource-telemetry.md.
648 ...buildObservabilityFragment(input.observabilitySettings),648 ...buildObservabilityFragment(input.observabilitySettings),
649 };649 };
650650
apps/server/scripts/migrate-dev-db.test.ts+66-27
...@@ -14,24 +14,21 @@ const withDatabase = <A, E>(...@@ -14,24 +14,21 @@ const withDatabase = <A, E>(
14 effect: Effect.Effect<A, E, SqlClient.SqlClient>,14 effect: Effect.Effect<A, E, SqlClient.SqlClient>,
15) => effect.pipe(Effect.provide(NodeSqliteClient.layer({ filename: databasePath })));15) => effect.pipe(Effect.provide(NodeSqliteClient.layer({ filename: databasePath })));
1616
17/** A migrated source db with one thread per lifecycle state. Only17/** A migrated source db with one V2 thread per lifecycle state. Only
18 * `stopped-thread` qualifies for the clone. */18 * `stopped-thread` and its fork qualify for the clone. */
19const createFixtureSource = Effect.fn("createMigrateDevDbFixtureSource")(function* (19const createFixtureSource = Effect.fn("createMigrateDevDbFixtureSource")(function* (
20 baseDir: string,20 baseDir: string,
21) {21) {
22 const fs = yield* FileSystem.FileSystem;22 const fs = yield* FileSystem.FileSystem;
23 const path = yield* Path.Path;23 const path = yield* Path.Path;
24 const stateDir = path.join(baseDir, "userdata");24 const stateDir = path.join(baseDir, "userdata");
25 const databasePath = path.join(stateDir, "state.sqlite");25 const databasePath = path.join(stateDir, "statev2.sqlite");
26 yield* fs.makeDirectory(stateDir, { recursive: true });26 yield* fs.makeDirectory(stateDir, { recursive: true });
27 yield* withDatabase(27 yield* withDatabase(
28 databasePath,28 databasePath,
29 Effect.gen(function* () {29 Effect.gen(function* () {
30 const sql = yield* SqlClient.SqlClient;30 const sql = yield* SqlClient.SqlClient;
31 yield* runMigrations();31 yield* runMigrations();
32 // The real shared db carries this column from a branch build without a
33 // matching migration; reproduce that drift so the filter is exercised.
34 yield* sql`ALTER TABLE projection_threads ADD COLUMN monitor_json TEXT`;
3532
36 yield* sql`INSERT INTO projection_projects33 yield* sql`INSERT INTO projection_projects
37 (project_id, title, workspace_root, scripts_json, created_at, updated_at, deleted_at)34 (project_id, title, workspace_root, scripts_json, created_at, updated_at, deleted_at)
...@@ -39,23 +36,56 @@ const createFixtureSource = Effect.fn("createMigrateDevDbFixtureSource")(functio...@@ -39,23 +36,56 @@ const createFixtureSource = Effect.fn("createMigrateDevDbFixtureSource")(functio
39 ('project-kept', 'Kept', '/tmp/kept', '[]', '2026-08-01', '2026-08-01', NULL),36 ('project-kept', 'Kept', '/tmp/kept', '[]', '2026-08-01', '2026-08-01', NULL),
40 ('project-deleted', 'Deleted', '/tmp/deleted', '[]', '2026-08-01', '2026-08-02', '2026-08-02')`;37 ('project-deleted', 'Deleted', '/tmp/deleted', '[]', '2026-08-01', '2026-08-02', '2026-08-02')`;
4138
39 const forkPayload =
40 '{"lineage":{"parentThreadId":"stopped-thread","relationshipToParent":"fork","rootThreadId":"stopped-thread"}}';
41 const subagentPayload =
42 '{"lineage":{"parentThreadId":"subagent-parent","relationshipToParent":"subagent","rootThreadId":"subagent-parent"},"forkedFrom":{"type":"node","nodeId":"node-1"}}';
43 // Excluded threads are newer than the kept family, so only the filters
44 // can keep them out of a one-family-per-project clone.
42 const threads = [45 const threads = [
43 ["stopped-thread", "project-kept", "stopped", null, null],46 ["stopped-thread", "project-kept", "completed", "{}", "2026-08-01"],
44 ["running-thread", "project-kept", "running", null, null],47 ["fork-thread", "project-kept", "completed", forkPayload, "2026-08-02"],
45 ["settled-thread", "project-kept", "stopped", "2026-08-01", null],48 ["running-thread", "project-kept", "running", "{}", "2026-08-05"],
46 ["monitored-thread", "project-kept", "stopped", null, '{"kind":"pr"}'],49 ["settled-thread", "project-kept", "completed", '{"settledAt":"2026-08-01"}', "2026-08-05"],
47 ["deleted-project-thread", "project-deleted", "stopped", null, null],50 [
51 "limit-thread",
52 "project-kept",
53 "completed",
54 '{"limitRecovery":{"autoResume":true}}',
55 "2026-08-05",
56 ],
57 // Its result never reached the parent, so startup would deliver it.
58 ["subagent-parent", "project-kept", "completed", "{}", "2026-08-05"],
59 ["subagent-child", "project-kept", "completed", subagentPayload, "2026-08-05"],
60 ["deleted-project-thread", "project-deleted", "completed", "{}", "2026-08-05"],
48 ] as const;61 ] as const;
49 for (const [threadId, projectId, status, settledAt, monitorJson] of threads) {62 for (const [threadId, projectId, runStatus, payload, updatedAt] of threads) {
50 yield* sql`INSERT INTO projection_threads63 yield* sql`INSERT INTO orchestration_v2_projection_threads
51 (thread_id, project_id, title, created_at, updated_at, settled_at, monitor_json)64 (thread_id, project_id, title, default_provider, runtime_mode, interaction_mode, created_at, updated_at, payload_json)
52 VALUES (${threadId}, ${projectId}, ${threadId}, '2026-08-01', '2026-08-01', ${settledAt}, ${monitorJson})`;65 VALUES (${threadId}, ${projectId}, ${threadId}, 'codex', 'full-access', 'default', '2026-08-01', ${updatedAt}, ${payload})`;
53 yield* sql`INSERT INTO projection_thread_sessions (thread_id, status, updated_at)66 yield* sql`INSERT INTO orchestration_v2_projection_runs
54 VALUES (${threadId}, ${status}, '2026-08-01')`;67 (run_id, thread_id, ordinal, provider, status, requested_at, payload_json)
68 VALUES (${`run-${threadId}`}, ${threadId}, 1, 'codex', ${runStatus}, '2026-08-01', '{}')`;
55 yield* sql`INSERT INTO orchestration_events69 yield* sql`INSERT INTO orchestration_events
56 (event_id, aggregate_kind, stream_id, stream_version, event_type, occurred_at, actor_kind, payload_json, metadata_json)70 (event_id, aggregate_kind, stream_id, stream_version, event_type, occurred_at, actor_kind, payload_json, metadata_json)
57 VALUES (${`event-${threadId}`}, 'thread', ${threadId}, 0, 'thread.created', '2026-08-01', 'user', '{}', '{}')`;71 VALUES (${`event-${threadId}`}, 'thread', ${threadId}, 0, 'thread.created', '2026-08-01', 'user', '{}', '{}')`;
58 }72 }
73 // A provider session shared by two threads names its latest writer.
74 yield* sql`INSERT INTO orchestration_v2_projection_provider_sessions
75 (provider_session_id, thread_id, provider, status, updated_at, payload_json)
76 VALUES ('session-shared', 'running-thread', 'codex', 'stopped', '2026-08-01', '{}')`;
77 yield* sql`INSERT INTO orchestration_v2_projection_provider_session_bindings
78 (provider_session_id, thread_id)
79 VALUES ('session-shared', 'running-thread'), ('session-shared', 'stopped-thread')`;
80 yield* sql`INSERT INTO orchestration_v2_projection_context_transfers
81 (context_transfer_id, source_thread_id, target_thread_id, type, status, updated_at, payload_json)
82 VALUES ('transfer-1', 'settled-thread', 'stopped-thread', 'provider_handoff', 'completed', '2026-08-01', '{}')`;
83 yield* sql`INSERT INTO scheduled_tasks
84 (task_id, title, prompt, enabled, schedule_json, project_id, workspace_strategy_json,
85 model_selection_json, runtime_mode, interaction_mode, created_by, creation_source,
86 created_at, updated_at, last_run_status, run_count)
87 VALUES ('task-1', 'Nightly', 'Run it', 1, '{}', 'project-kept', '{}', '{}',
88 'full-access', 'default', 'user', 'user', '2026-08-01', '2026-08-01', 'never', 0)`;
59 yield* sql`INSERT INTO auth_sessions (session_id, subject, scopes, method, issued_at, expires_at)89 yield* sql`INSERT INTO auth_sessions (session_id, subject, scopes, method, issued_at, expires_at)
60 VALUES ('session-1', 'user', '[]', 'pairing', '2026-08-01', '2027-08-01')`;90 VALUES ('session-1', 'user', '[]', 'pairing', '2026-08-01', '2027-08-01')`;
61 }),91 }),
...@@ -64,7 +94,7 @@ const createFixtureSource = Effect.fn("createMigrateDevDbFixtureSource")(functio...@@ -64,7 +94,7 @@ const createFixtureSource = Effect.fn("createMigrateDevDbFixtureSource")(functio
64});94});
6595
66it.layer(NodeServices.layer)("migrate-dev-db", (it) => {96it.layer(NodeServices.layer)("migrate-dev-db", (it) => {
67 it.effect("keeps only stopped threads from live projects and clears auth state", () =>97 it.effect("keeps stopped thread families from live projects and clears pending work", () =>
68 Effect.gen(function* () {98 Effect.gen(function* () {
69 const fs = yield* FileSystem.FileSystem;99 const fs = yield* FileSystem.FileSystem;
70 const path = yield* Path.Path;100 const path = yield* Path.Path;
...@@ -73,7 +103,7 @@ it.layer(NodeServices.layer)("migrate-dev-db", (it) => {...@@ -73,7 +103,7 @@ it.layer(NodeServices.layer)("migrate-dev-db", (it) => {
73 const source = yield* createFixtureSource(sourceDir);103 const source = yield* createFixtureSource(sourceDir);
74104
75 const result = yield* runMigrateDevDb(105 const result = yield* runMigrateDevDb(
76 { baseDir: destDir, source, projects: 5, threadsPerProject: 10 },106 { baseDir: destDir, source, projects: 5, threadsPerProject: 1 },
77 { sharedHome: sourceDir },107 { sharedHome: sourceDir },
78 );108 );
79109
...@@ -83,23 +113,32 @@ it.layer(NodeServices.layer)("migrate-dev-db", (it) => {...@@ -83,23 +113,32 @@ it.layer(NodeServices.layer)("migrate-dev-db", (it) => {
83 Effect.gen(function* () {113 Effect.gen(function* () {
84 const sql = yield* SqlClient.SqlClient;114 const sql = yield* SqlClient.SqlClient;
85 const threads = yield* sql<{ thread_id: string }>`115 const threads = yield* sql<{ thread_id: string }>`
86 SELECT thread_id FROM projection_threads ORDER BY thread_id`;116 SELECT thread_id FROM orchestration_v2_projection_threads ORDER BY thread_id`;
87 const events = yield* sql<{ stream_id: string }>`117 const events = yield* sql<{ stream_id: string }>`
88 SELECT stream_id FROM orchestration_events`;118 SELECT stream_id FROM orchestration_events ORDER BY stream_id`;
89 const [auth] = yield* sql<{ count: number }>`119 const sessions = yield* sql<{ provider_session_id: string }>`
90 SELECT COUNT(*) AS count FROM auth_sessions`;120 SELECT provider_session_id FROM orchestration_v2_projection_provider_sessions`;
91 return { threads, events, authCount: auth?.count ?? 0 };121 const [leftovers] = yield* sql<{ auth: number; tasks: number; transfers: number }>`
122 SELECT
123 (SELECT COUNT(*) FROM auth_sessions) AS auth,
124 (SELECT COUNT(*) FROM scheduled_tasks) AS tasks,
125 (SELECT COUNT(*) FROM orchestration_v2_projection_context_transfers) AS transfers`;
126 return { threads, events, sessions, leftovers };
92 }),127 }),
93 );128 );
94 assert.deepStrictEqual(129 assert.deepStrictEqual(
95 kept.threads.map((row) => row.thread_id),130 kept.threads.map((row) => row.thread_id),
96 ["stopped-thread"],131 ["fork-thread", "stopped-thread"],
97 );132 );
98 assert.deepStrictEqual(133 assert.deepStrictEqual(
99 kept.events.map((row) => row.stream_id),134 kept.events.map((row) => row.stream_id),
100 ["stopped-thread"],135 ["fork-thread", "stopped-thread"],
136 );
137 assert.deepStrictEqual(
138 kept.sessions.map((row) => row.provider_session_id),
139 ["session-shared"],
101 );140 );
102 assert.equal(kept.authCount, 0);141 assert.deepStrictEqual(kept.leftovers, { auth: 0, tasks: 0, transfers: 0 });
103 }),142 }),
104 );143 );
105144
apps/server/scripts/migrate-dev-db.ts+108-59
...@@ -6,10 +6,12 @@...@@ -6,10 +6,12 @@
6 *6 *
7 * `vp run migrate-dev-db` from a worktree:7 * `vp run migrate-dev-db` from a worktree:
8 * 1. Nukes `<worktree>/.t3/userdata/statev2.sqlite`.8 * 1. Nukes `<worktree>/.t3/userdata/statev2.sqlite`.
9 * 2. Snapshots the real db (read-only VACUUM INTO) and prunes it to the9 * 2. Snapshots the real db (`~/.t3/userdata/statev2.sqlite`, read-only
10 * most recently updated projects and, per project, the most recent10 * VACUUM INTO) and prunes it to the most recently updated projects and,
11 * threads that have fully stopped. Working, settled, and monitored11 * per project, the most recent threads that have fully stopped, with
12 * threads are skipped so the dev server never adopts live work.12 * their forks and subagents. Working, settled, and archived threads, and
13 * threads with pending recovery, are skipped, and scheduled tasks and
14 * queued effects are dropped, so the dev server never adopts live work.
13 * Auth sessions, pairing links, command receipts, and provider15 * Auth sessions, pairing links, command receipts, and provider
14 * runtime rows are dropped — pair a fresh browser against dev.16 * runtime rows are dropped — pair a fresh browser against dev.
15 * 3. Runs migrations on the result. Because the clone carries the real17 * 3. Runs migrations on the result. Because the clone carries the real
...@@ -19,9 +21,8 @@...@@ -19,9 +21,8 @@
19 * `Migrations/NNN_` id (the second one's CREATE TABLE is skipped).21 * `Migrations/NNN_` id (the second one's CREATE TABLE is skipped).
20 *22 *
21 * The event log (`orchestration_events`) is pruned per stream while23 * The event log (`orchestration_events`) is pruned per stream while
22 * `sqlite_sequence` and `projection_state` carry over untouched, so new24 * `sqlite_sequence` and the projection cursors carry over untouched, so new events keep appending
23 * events keep appending after the old high-water mark and projection25 * after the old high-water mark and projection cursors never rewind.
24 * cursors never rewind.
25 */26 */
2627
27import * as NodeRuntime from "@effect/platform-node/NodeRuntime";28import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
...@@ -31,12 +32,14 @@ import { resolveWorktreeT3Home } from "@t3tools/shared/devHome";...@@ -31,12 +32,14 @@ import { resolveWorktreeT3Home } from "@t3tools/shared/devHome";
31import * as Console from "effect/Console";32import * as Console from "effect/Console";
32import * as Effect from "effect/Effect";33import * as Effect from "effect/Effect";
33import * as FileSystem from "effect/FileSystem";34import * as FileSystem from "effect/FileSystem";
35import * as Layer from "effect/Layer";
34import * as Option from "effect/Option";36import * as Option from "effect/Option";
35import * as Path from "effect/Path";37import * as Path from "effect/Path";
36import * as Schema from "effect/Schema";38import * as Schema from "effect/Schema";
37import * as SqlClient from "effect/unstable/sql/SqlClient";39import * as SqlClient from "effect/unstable/sql/SqlClient";
38import { Command, Flag } from "effect/unstable/cli";40import { Command, Flag } from "effect/unstable/cli";
3941
42import * as ProjectionStore from "../src/orchestration-v2/ProjectionStore.ts";
40import { migrationManifest, runMigrations } from "../src/persistence/Migrations.ts";43import { migrationManifest, runMigrations } from "../src/persistence/Migrations.ts";
41import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";44import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
4245
...@@ -143,7 +146,7 @@ export class MigrateDevDbPhaseError extends Schema.TaggedError<MigrateDevDbPhase...@@ -143,7 +146,7 @@ export class MigrateDevDbPhaseError extends Schema.TaggedError<MigrateDevDbPhase
143export interface RunMigrateDevDbInput {146export interface RunMigrateDevDbInput {
144 /** Isolated .t3 directory. Defaults to `<worktree>/.t3` of the cwd. */147 /** Isolated .t3 directory. Defaults to `<worktree>/.t3` of the cwd. */
145 readonly baseDir?: string | undefined;148 readonly baseDir?: string | undefined;
146 /** Source database. Defaults to `~/.t3/userdata/state.sqlite`. */149 /** Source database. Defaults to `~/.t3/userdata/statev2.sqlite`. */
147 readonly source?: string | undefined;150 readonly source?: string | undefined;
148 readonly projects: number;151 readonly projects: number;
149 readonly threadsPerProject: number;152 readonly threadsPerProject: number;
...@@ -232,31 +235,56 @@ const ensureNotInUse = Effect.fn("ensureDevDbNotInUse")(function* (databasePath:...@@ -232,31 +235,56 @@ const ensureNotInUse = Effect.fn("ensureDevDbNotInUse")(function* (databasePath:
232 }235 }
233});236});
234237
238const RECOVERY_KINDS: ReadonlyArray<ProjectionStore.ProjectionRecoveryKind> = [
239 "queued-runs",
240 "runtime",
241 "subagent-results",
242 "delegated-completions",
243];
244
235const pruneSnapshot = Effect.fn("pruneDevDbSnapshot")(function* (input: RunMigrateDevDbInput) {245const pruneSnapshot = Effect.fn("pruneDevDbSnapshot")(function* (input: RunMigrateDevDbInput) {
236 const sql = yield* SqlClient.SqlClient;246 const sql = yield* SqlClient.SqlClient;
237247
238 // The shared db can carry monitor_json from a branch build even though no248 // Threads the server would resume or start work on by itself. Its own
239 // migration in this checkout creates it, so filter it only when present.249 // recovery queries find most of them; usage-limit recovery also depends on
240 const threadColumns = yield* sql<{ name: string }>`250 // settings, so any thread whose latest run failed or that has a recovery
241 SELECT name FROM pragma_table_info('projection_threads')`;251 // choice counts as live too.
242 const monitorFilter = threadColumns.some((column) => column.name === "monitor_json")252 const projections = yield* ProjectionStore.ProjectionStoreV2;
243 ? "AND t.monitor_json IS NULL"253 const recoveryThreadIds = yield* Effect.forEach(RECOVERY_KINDS, (kind) =>
244 : "";254 projections.getRecoveryThreadIds(kind),
245255 );
246 // "Stopped" is the persisted subset of the UI's thread status: the session256 yield* sql`CREATE TEMP TABLE live_threads (thread_id TEXT PRIMARY KEY)`;
247 // reached status 'stopped' and nothing marks the thread settled or257 for (const threadId of new Set(recoveryThreadIds.flat())) {
248 // monitored. The in-memory working/monitoring liveness never persists, so258 yield* sql`INSERT INTO live_threads (thread_id) VALUES (${threadId})`;
249 // filtering the session status is sufficient.259 }
250 yield* sql.unsafe(`CREATE TEMP TABLE stopped_threads AS260 yield* sql`INSERT OR IGNORE INTO live_threads (thread_id)
251 SELECT t.thread_id, t.project_id, t.updated_at261 SELECT r.thread_id FROM orchestration_v2_projection_runs r
252 FROM projection_threads t262 WHERE r.status = 'failed' AND r.ordinal = (
253 JOIN projection_thread_sessions s ON s.thread_id = t.thread_id263 SELECT MAX(latest.ordinal) FROM orchestration_v2_projection_runs latest
254 WHERE t.deleted_at IS NULL264 WHERE latest.thread_id = r.thread_id)
255 AND t.archived_at IS NULL265 UNION
256 AND t.settled_at IS NULL266 SELECT thread_id FROM orchestration_v2_projection_threads
257 AND (t.settled_override IS NULL OR t.settled_override <> 'settled')267 WHERE json_extract(payload_json, '$.limitRecovery') IS NOT NULL`;
258 ${monitorFilter}268
259 AND s.status = 'stopped'`).unprepared;269 // Forks and subagents read history and results through their lineage, so
270 // a thread family is cloned or dropped as a whole. A family is stopped when
271 // its root is visible and unsettled and none of its threads is live.
272 yield* sql`CREATE TEMP TABLE thread_families AS
273 SELECT thread_id, updated_at,
274 COALESCE(json_extract(payload_json, '$.lineage.rootThreadId'), thread_id) AS root_id
275 FROM orchestration_v2_projection_threads`;
276
277 yield* sql`CREATE TEMP TABLE stopped_families AS
278 SELECT f.root_id, root.project_id, MAX(f.updated_at) AS updated_at
279 FROM thread_families f
280 JOIN orchestration_v2_projection_threads root ON root.thread_id = f.root_id
281 WHERE root.deleted_at IS NULL
282 AND json_extract(root.payload_json, '$.deletedAt') IS NULL
283 AND json_extract(root.payload_json, '$.archivedAt') IS NULL
284 AND json_extract(root.payload_json, '$.settledAt') IS NULL
285 AND json_extract(root.payload_json, '$.settledOverride') IS NOT 'settled'
286 GROUP BY f.root_id, root.project_id
287 HAVING SUM(f.thread_id IN (SELECT thread_id FROM live_threads)) = 0`;
260288
261 // Projects with clonable threads outrank empty-but-recent ones: the point289 // Projects with clonable threads outrank empty-but-recent ones: the point
262 // of the exercise is thread data, not the project list.290 // of the exercise is thread data, not the project list.
...@@ -265,7 +293,7 @@ const pruneSnapshot = Effect.fn("pruneDevDbSnapshot")(function* (input: RunMigra...@@ -265,7 +293,7 @@ const pruneSnapshot = Effect.fn("pruneDevDbSnapshot")(function* (input: RunMigra
265 FROM projection_projects p293 FROM projection_projects p
266 LEFT JOIN (294 LEFT JOIN (
267 SELECT project_id, MAX(updated_at) AS last_stopped_at295 SELECT project_id, MAX(updated_at) AS last_stopped_at
268 FROM stopped_threads296 FROM stopped_families
269 GROUP BY project_id297 GROUP BY project_id
270 ) q ON q.project_id = p.project_id298 ) q ON q.project_id = p.project_id
271 WHERE p.deleted_at IS NULL299 WHERE p.deleted_at IS NULL
...@@ -274,42 +302,56 @@ const pruneSnapshot = Effect.fn("pruneDevDbSnapshot")(function* (input: RunMigra...@@ -274,42 +302,56 @@ const pruneSnapshot = Effect.fn("pruneDevDbSnapshot")(function* (input: RunMigra
274 LIMIT ${input.projects}`;302 LIMIT ${input.projects}`;
275303
276 yield* sql`CREATE TEMP TABLE kept_threads AS304 yield* sql`CREATE TEMP TABLE kept_threads AS
277 SELECT thread_id FROM (305 SELECT f.thread_id FROM thread_families f
278 SELECT306 WHERE f.root_id IN (
279 st.thread_id,307 SELECT root_id FROM (
280 ROW_NUMBER() OVER (308 SELECT
281 PARTITION BY st.project_id309 sf.root_id,
282 ORDER BY st.updated_at DESC310 ROW_NUMBER() OVER (
283 ) AS recency_rank311 PARTITION BY sf.project_id
284 FROM stopped_threads st312 ORDER BY sf.updated_at DESC
285 JOIN kept_projects kp ON kp.project_id = st.project_id313 ) AS recency_rank
286 )314 FROM stopped_families sf
287 WHERE recency_rank <= ${input.threadsPerProject}`;315 JOIN kept_projects kp ON kp.project_id = sf.project_id
316 )
317 WHERE recency_rank <= ${input.threadsPerProject}
318 )`;
319
320 // Every V2 table and every V1 table the lazy importer reads is keyed by
321 // thread_id, so one sweep covers both and new tables need no change here.
322 // Provider sessions are shared between threads and pruned by binding below.
323 const threadTables = yield* sql<{ name: string }>`
324 SELECT m.name FROM sqlite_master m, pragma_table_info(m.name) c
325 WHERE m.type = 'table' AND c.name = 'thread_id'
326 AND m.name <> 'orchestration_v2_projection_provider_sessions'`;
288327
289 yield* sql.withTransaction(328 yield* sql.withTransaction(
290 Effect.gen(function* () {329 Effect.gen(function* () {
291 yield* sql`DELETE FROM projection_projects330 yield* sql`DELETE FROM projection_projects
292 WHERE project_id NOT IN (SELECT project_id FROM kept_projects)`;331 WHERE project_id NOT IN (SELECT project_id FROM kept_projects)`;
293 yield* sql`DELETE FROM projection_threads332 yield* sql`DELETE FROM orchestration_v2_projection_provider_sessions
294 WHERE thread_id NOT IN (SELECT thread_id FROM kept_threads)`;333 WHERE COALESCE(thread_id, '') NOT IN (SELECT thread_id FROM kept_threads)
295 for (const table of [334 AND provider_session_id NOT IN (
296 "projection_thread_messages",335 SELECT provider_session_id FROM orchestration_v2_projection_provider_session_bindings
297 "projection_thread_activities",336 WHERE thread_id IN (SELECT thread_id FROM kept_threads)
298 "projection_thread_sessions",337 )`;
299 "projection_turns",338 for (const { name } of threadTables) {
300 "projection_pending_approvals",
301 "projection_thread_proposed_plans",
302 "checkpoint_diff_blobs",
303 ]) {
304 yield* sql.unsafe(339 yield* sql.unsafe(
305 `DELETE FROM ${table} WHERE thread_id NOT IN (SELECT thread_id FROM kept_threads)`,340 `DELETE FROM "${name}" WHERE thread_id NOT IN (SELECT thread_id FROM kept_threads)`,
306 ).unprepared;341 ).unprepared;
307 }342 }
343 yield* sql`DELETE FROM orchestration_v2_projection_context_transfers
344 WHERE source_thread_id NOT IN (SELECT thread_id FROM kept_threads)
345 OR target_thread_id NOT IN (SELECT thread_id FROM kept_threads)`;
308 yield* sql`DELETE FROM orchestration_events346 yield* sql`DELETE FROM orchestration_events
309 WHERE (aggregate_kind = 'thread'347 WHERE (aggregate_kind = 'thread'
310 AND stream_id NOT IN (SELECT thread_id FROM kept_threads))348 AND stream_id NOT IN (SELECT thread_id FROM kept_threads))
311 OR (aggregate_kind = 'project'349 OR (aggregate_kind = 'project'
312 AND stream_id NOT IN (SELECT project_id FROM kept_projects))`;350 AND stream_id NOT IN (SELECT project_id FROM kept_projects))`;
351 // Pending work the dev server would otherwise pick up and run.
352 yield* sql`DELETE FROM scheduled_tasks`;
353 yield* sql`DELETE FROM orchestration_v2_effect_outbox`;
354 yield* sql`DELETE FROM orchestration_v2_thread_launch_workflows`;
313 yield* sql`DELETE FROM orchestration_command_receipts`;355 yield* sql`DELETE FROM orchestration_command_receipts`;
314 yield* sql`DELETE FROM provider_session_runtime`;356 yield* sql`DELETE FROM provider_session_runtime`;
315 yield* sql`DELETE FROM auth_sessions`;357 yield* sql`DELETE FROM auth_sessions`;
...@@ -320,7 +362,8 @@ const pruneSnapshot = Effect.fn("pruneDevDbSnapshot")(function* (input: RunMigra...@@ -320,7 +362,8 @@ const pruneSnapshot = Effect.fn("pruneDevDbSnapshot")(function* (input: RunMigra
320 const keptProjects = yield* sql<{ title: string; threads: number }>`362 const keptProjects = yield* sql<{ title: string; threads: number }>`
321 SELECT363 SELECT
322 p.title,364 p.title,
323 (SELECT COUNT(*) FROM projection_threads t WHERE t.project_id = p.project_id) AS threads365 (SELECT COUNT(*) FROM orchestration_v2_projection_threads t
366 WHERE t.project_id = p.project_id) AS threads
324 FROM projection_projects p367 FROM projection_projects p
325 ORDER BY p.updated_at DESC`;368 ORDER BY p.updated_at DESC`;
326 const [events] = yield* sql<{ count: number }>`369 const [events] = yield* sql<{ count: number }>`
...@@ -362,7 +405,7 @@ export const runMigrateDevDb = Effect.fn("runMigrateDevDb")(function* (...@@ -362,7 +405,7 @@ export const runMigrateDevDb = Effect.fn("runMigrateDevDb")(function* (
362405
363 const sharedHome = path.resolve(options.sharedHome ?? path.join(NodeOS.homedir(), ".t3"));406 const sharedHome = path.resolve(options.sharedHome ?? path.join(NodeOS.homedir(), ".t3"));
364 const sourcePath = path.resolve(407 const sourcePath = path.resolve(
365 input.source ?? path.join(sharedHome, "userdata", "state.sqlite"),408 input.source ?? path.join(sharedHome, "userdata", "statev2.sqlite"),
366 );409 );
367410
368 const baseDir =411 const baseDir =
...@@ -458,7 +501,11 @@ export const runMigrateDevDb = Effect.fn("runMigrateDevDb")(function* (...@@ -458,7 +501,11 @@ export const runMigrateDevDb = Effect.fn("runMigrateDevDb")(function* (
458 `Pruning to ${input.projects} projects, ${input.threadsPerProject} stopped threads each...`,501 `Pruning to ${input.projects} projects, ${input.threadsPerProject} stopped threads each...`,
459 );502 );
460 const result = yield* pruneSnapshot(input).pipe(503 const result = yield* pruneSnapshot(input).pipe(
461 Effect.provide(NodeSqliteClient.layer({ filename: snapshotPath })),504 Effect.provide(
505 ProjectionStore.layer.pipe(
506 Layer.provideMerge(NodeSqliteClient.layer({ filename: snapshotPath })),
507 ),
508 ),
462 wrapPhase("prune", snapshotPath),509 wrapPhase("prune", snapshotPath),
463 );510 );
464511
...@@ -512,7 +559,9 @@ export const migrateDevDbCommand = Command.make(...@@ -512,7 +559,9 @@ export const migrateDevDbCommand = Command.make(
512 ),559 ),
513 threadsPerProject: Flag.Int("threads-per-project").pipe(560 threadsPerProject: Flag.Int("threads-per-project").pipe(
514 Flag.withDefault(10),561 Flag.withDefault(10),
515 Flag.withDescription("How many recent stopped threads to keep per project."),562 Flag.withDescription(
563 "How many recent stopped threads, with their forks and subagents, to keep per project.",
564 ),
516 ),565 ),
517 baseDir: Flag.String("base-dir").pipe(566 baseDir: Flag.String("base-dir").pipe(
518 Flag.optional,567 Flag.optional,
...@@ -520,7 +569,7 @@ export const migrateDevDbCommand = Command.make(...@@ -520,7 +569,7 @@ export const migrateDevDbCommand = Command.make(
520 ),569 ),
521 source: Flag.String("source").pipe(570 source: Flag.String("source").pipe(
522 Flag.optional,571 Flag.optional,
523 Flag.withDescription("Source database. Defaults to ~/.t3/userdata/state.sqlite."),572 Flag.withDescription("Source database. Defaults to ~/.t3/userdata/statev2.sqlite."),
524 ),573 ),
525 },574 },
526 ({ projects, threadsPerProject, baseDir, source }) =>575 ({ projects, threadsPerProject, baseDir, source }) =>
apps/server/src/orchestration-v2/Orchestrator.ts+1-26
...@@ -8029,32 +8029,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio...@@ -8029,32 +8029,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
8029 }),8029 }),
8030 );8030 );
80318031
8032 /*8032 // Open interrupt edge cases are tracked in https://github.com/pingdotgg/t3code/issues/15013.
8033 * TODO(interrupt-hardening): before shipping, make these interrupt
8034 * semantics explicit in tests and policy.
8035 *
8036 * Current behavior:
8037 * - emit a `run_interrupt_request` item as user intent;
8038 * - call the provider interrupt RPC;
8039 * - keep the run active and continue ingesting provider chunks;
8040 * - let RunExecutionService emit `run_interrupt_result` only if the
8041 * provider later reports terminal status `interrupted`.
8042 *
8043 * Known scenarios we do not fully harden yet:
8044 * - provider accepts interrupt, then emits more chunks before terminal;
8045 * - provider accepts interrupt, then completes normally instead;
8046 * - provider accepts interrupt but never terminalizes;
8047 * - user queues, steers, or starts another message while the interrupted
8048 * provider turn is still active.
8049 *
8050 * Likely policy:
8051 * - queue should wait behind the still-active provider turn;
8052 * - explicit steer may target the active turn if provider steering is
8053 * supported;
8054 * - starting a new root turn before provider terminalization should be
8055 * an explicit policy decision because it can weaken native-item
8056 * correlation.
8057 */
8058 yield* emitEvent({8033 yield* emitEvent({
8059 type: "turn-item.updated",8034 type: "turn-item.updated",
8060 threadId: command.threadId,8035 threadId: command.threadId,
apps/server/src/orchestration-v2/TODO.md deleted-172
...@@ -1,172 +0,0 @@
1# Orchestration V2 TODO
2
3This file tracks remaining backend-oriented V2 work. Architecture-level intent lives in
4[`docs/orchestration-v2`](../../../../docs/orchestration-v2); this file is the local
5implementation checklist for `apps/server/src/orchestration-v2`.
6
7## Current Baseline
8
9- V2 commands/events/projections are server-owned and replayable.
10- Message dispatch supports start, steer, queue, queue reorder, promote queued message to steer,
11 interrupt, and provider switch command shapes.
12- Checkpoint rollback is currently a full revert: filesystem checkpoint restore, provider thread
13 rollback, stale checkpoint marking, and later run/node `rolled_back` projection state.
14- Codex same-provider fork is lazy: `thread.fork` records lineage and pending transfer, and first
15 dispatch resolves native Codex fork. Earlier source-point forks pass the source turn's native id
16 to `thread/fork` as `lastTurnId`; the fork-then-rollback fallback remains only for source turns
17 without a native turn reference and only on legacy-history threads.
18- Codex provider conversation rollback supports only legacy-history threads. The adapter probes
19 `historyMode` and fails explicitly on paginated threads. Closing the gap means paging
20 `thread/turns/list` to find the first removed turn, then `thread/revert` with `beforeTurnId`.
21- Native Codex fork-from-earlier-run has a real replay-backed test fixture:
22 `testkit/fixtures/thread_fork_native_prior_turn`.
23- Merge-back from a fork into its source thread records a `merge_back` context transfer, materializes
24 a `fork_delta_summary` context handoff, and injects that handoff into the next source-thread run.
25
26## Projection Hardening
27
28Target docs:
29
30- [`core-graph-and-data-model.md`](../../../../docs/orchestration-v2/core-graph-and-data-model.md)
31- [`thread-lineage-and-context-transfer.md`](../../../../docs/orchestration-v2/thread-lineage-and-context-transfer.md)
32- [`testing-strategy.md`](../../../../docs/orchestration-v2/testing-strategy.md)
33
34TODO:
35
36- [x] Add end-to-end projection assertions for forked threads, not only provider-context behavior.
37 The fork projection should show inherited user-visible items through the source point plus an
38 explicit fork marker.
39- [x] Decide and implement the projection representation for inherited fork history:
40 referenced lineage overlay vs physically duplicated projection items. Prefer referenced overlay
41 unless product requirements need independent editable history.
42- [x] Add local visible projection assertions to non-fork replay fixtures:
43 `visibleTurnItems` mirrors canonical local `turnItems` for simple, multi-turn, queue,
44 steering, interrupt, rollback, planning, tool, and web-search fixtures.
45- [ ] Ensure projections render rollback state consistently:
46 rolled-back runs/nodes/items, stale checkpoints, and active provider thread cursor.
47- [ ] Add projection tests for interrupt edge cases:
48 provider emits chunks after interrupt requested, provider ignores/delays interrupt, interrupt is
49 immediately followed by queue/steer/start.
50- [ ] Add projection tests for queue/steer flows:
51 queued message visibility, queue reorder, promote queued message to steer, and post-interrupt
52 dispatch visibility.
53
54## Context Transfer And Merge-Back
55
56Target docs:
57
58- [`thread-lineage-and-context-transfer.md`](../../../../docs/orchestration-v2/thread-lineage-and-context-transfer.md)
59- [`provider-switching-and-context.md`](../../../../docs/orchestration-v2/provider-switching-and-context.md)
60
61TODO:
62
63- [x] Implement merge-back from a fork into its source thread:
64 source fork point as `basePoint`, fork latest stable point as `sourcePoint`, and source-thread
65 next user message as the consuming run.
66- [x] Materialize delta context artifacts for merge-back and persist them as auditable
67 `ContextHandoff` records.
68- [x] Add replay-backed integration coverage for merge-back:
69 fork, explore in fork, merge back, then assert source provider receives only the fork delta plus
70 the new user message.
71- [ ] Implement portable context handoff for cross-provider forks and same-thread provider switches.
72- [ ] Define explicit failure states for unresolved context transfers:
73 missing source point, unsupported provider capability, context too large, source projection not
74 stable, and adapter resolution failure.
75
76## Capability And Policy Model
77
78Target docs:
79
80- [`provider-capability-system.md`](../../../../docs/orchestration-v2/provider-capability-system.md)
81- [`feature-lifecycles.md`](../../../../docs/orchestration-v2/feature-lifecycles.md)
82
83TODO:
84
85- [ ] Audit capability use against the documented nested `OrchestrationV2ProviderCapabilities`
86 shape and fill any behavior gaps.
87- [ ] Keep orchestration decisions capability/policy driven. Shared runtime code should not branch on
88 provider name except at adapter registration/resolution boundaries.
89- [ ] Make optional adapter methods impossible to call without going through a policy wrapper or a
90 capability-checked branch.
91- [ ] Add typed capability/policy errors for:
92 native fork unavailable, rollback unavailable, steering unavailable, interrupt unavailable,
93 context handoff unavailable, and weak terminal status.
94- [ ] Add capability-aware tests using real adapter/test layers at provider boundaries only. Do not mock
95 core orchestration policy.
96
97## Checkpoint And Rollback
98
99Target docs:
100
101- [`feature-lifecycles.md`](../../../../docs/orchestration-v2/feature-lifecycles.md)
102- [`core-graph-and-data-model.md`](../../../../docs/orchestration-v2/core-graph-and-data-model.md)
103
104TODO:
105
106- [ ] Document current `checkpoint.rollback` command semantics in contracts/docs as full revert:
107 filesystem restore plus provider conversation rollback.
108- [ ] Decide whether we need separate commands for conversation-only rollback and filesystem-only
109 restore. Do not add them until a real product flow needs them.
110- [ ] Add tests for rollback after fork and rollback inside fork:
111 source rollback should not corrupt child lineage, and fork rollback should not mutate source
112 provider state.
113- [ ] Validate rollback behavior when no active provider thread exists. Current behavior fails; decide
114 whether a filesystem-only rollback fallback is useful or too surprising.
115
116## Provider Switching And Second Adapter
117
118Target docs:
119
120- [`provider-switching-and-context.md`](../../../../docs/orchestration-v2/provider-switching-and-context.md)
121- [`provider-capability-system.md`](../../../../docs/orchestration-v2/provider-capability-system.md)
122- [`testing-strategy.md`](../../../../docs/orchestration-v2/testing-strategy.md)
123
124TODO:
125
126- [x] Add Claude replay fixture definitions for recorded fixtures, with TODO fixture slots that
127 reference the corresponding Codex transcripts and V2 docs.
128- [ ] Promote Claude `simple` from the replay adapter to a real `ClaudeAdapterV2` replay test:
129 live and replay both consume an injected Agent SDK `query()` async iterable.
130- [ ] Record Claude `multi_turn` from real usage and prove native session/thread continuation.
131- [ ] Record Claude `tool_call_read_only` from real usage and prove read-only tool projection without
132 approvals.
133- [ ] Keep unrecorded Claude fixtures out of `testkit/fixtures/index.ts` until each has a real
134 transcript and real adapter assertions.
135- [ ] Add the second adapter once these vv0 Claude slices are stable enough to validate
136 cross-provider behavior.
137- [ ] Use the second adapter to test:
138 cross-provider fork, same-thread provider switch, returning to a previous provider thread with
139 delta handoff, and unsupported capability fallback paths.
140
141## Subagents
142
143Target docs:
144
145- [`thread-lineage-and-context-transfer.md`](../../../../docs/orchestration-v2/thread-lineage-and-context-transfer.md)
146- [`provider-capability-system.md`](../../../../docs/orchestration-v2/provider-capability-system.md)
147
148TODO:
149
150- [x] Add provider-native subagent observation for Codex and Claude replay-backed fixtures.
151- [ ] Model native subagents and app-owned cross-provider subagents as related thread/subthread graph
152 entries with different creator/lifecycle policy.
153- [ ] Preserve native provider subagent refs where available, but do not make the app graph depend on
154 provider-native ids as primary ids.
155- [ ] Add deeper tests for subagent wait, close, result transfer, pending approvals, failed/stopped
156 tasks, and fork-from-subagent behavior.
157
158FOOD FOR THOUGHT:
159
160- Custom t3code tools/mcp_server that lets agents spawn subagents of other providers powered by the T3 Orchestrator
161
162## Debugger-Only Work
163
164- Keep debugger UI useful but temporary. Backend semantics and projections should be the source of
165 truth.
166- [x] Wire the debugger thread tree to persisted V2 shell state through websocket RPC instead of
167 debugger-local thread discovery state.
168- Continue exposing lightweight controls for new backend surfaces:
169 fork from response, new thread, full revert from user message checkpoint, merge-back, and provider
170 switch.
171- Avoid adding mock backend behavior for debugger convenience. In-memory debugger state is fine for
172 layout affordances, but backend behavior must route through V2 commands/projections.
apps/server/src/persistence/Migrations/OrchestrationV2/ApplicationEventSequenceIndexes.ts+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import * as Effect from "effect/Effect";1import * as Effect from "effect/Effect";
2import * as SqlClient from "effect/unstable/sql/SqlClient";2import * as SqlClient from "effect/unstable/sql/SqlClient";
33
4// Index setup composed by migration 050.4// Index setup composed by migration 055.
5export default Effect.gen(function* () {5export default Effect.gen(function* () {
6 const sql = yield* SqlClient.SqlClient;6 const sql = yield* SqlClient.SqlClient;
77
apps/server/src/persistence/Migrations/OrchestrationV2/ApplicationEventSource.ts+1-1
...@@ -30,7 +30,7 @@ interface ProjectProjectionRow {...@@ -30,7 +30,7 @@ interface ProjectProjectionRow {
30const decodeJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown));30const decodeJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown));
31const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));31const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
3232
33// Event-store setup and V1 project baseline composed by migration 050.33// Event-store setup and V1 project baseline composed by migration 055.
34export default Effect.gen(function* () {34export default Effect.gen(function* () {
35 const sql = yield* SqlClient.SqlClient;35 const sql = yield* SqlClient.SqlClient;
3636
apps/server/src/persistence/Migrations/OrchestrationV2/EffectCancellation.ts+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import * as Effect from "effect/Effect";1import * as Effect from "effect/Effect";
2import * as SqlClient from "effect/unstable/sql/SqlClient";2import * as SqlClient from "effect/unstable/sql/SqlClient";
33
4// Schema setup composed by migration 050.4// Schema setup composed by migration 055.
5export default Effect.gen(function* () {5export default Effect.gen(function* () {
6 const sql = yield* SqlClient.SqlClient;6 const sql = yield* SqlClient.SqlClient;
77
apps/server/src/persistence/Migrations/OrchestrationV2/Foundation.ts+1-1
...@@ -2,7 +2,7 @@ import * as Effect from "effect/Effect";...@@ -2,7 +2,7 @@ import * as Effect from "effect/Effect";
2import * as SqlClient from "effect/unstable/sql/SqlClient";2import * as SqlClient from "effect/unstable/sql/SqlClient";
33
4/**4/**
5 * Production-facing V2 persistence setup composed by migration 050.5 * Production-facing V2 persistence setup composed by migration 055.
6 *6 *
7 * The original V2 schema used a single `provider` column for both configured7 * The original V2 schema used a single `provider` column for both configured
8 * instance routing and driver identity. Keep those columns in place for8 * instance routing and driver identity. Keep those columns in place for
apps/server/src/persistence/Migrations/OrchestrationV2/LegacyV1ImportState.ts+1-1
...@@ -5,7 +5,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient";...@@ -5,7 +5,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient";
5 * Tracks the incremental import of v1 materialized thread state into the v25 * Tracks the incremental import of v1 materialized thread state into the v2
6 * event model. Shells are imported synchronously at startup; full transcripts6 * event model. Shells are imported synchronously at startup; full transcripts
7 * are hydrated on demand and by a low-priority background pass. This table7 * are hydrated on demand and by a low-priority background pass. This table
8 * setup is composed by migration 050.8 * setup is composed by migration 055.
9 */9 */
10export default Effect.gen(function* () {10export default Effect.gen(function* () {
11 const sql = yield* SqlClient.SqlClient;11 const sql = yield* SqlClient.SqlClient;
apps/server/src/persistence/Migrations/OrchestrationV2/ProviderSessionBindings.ts+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import * as Effect from "effect/Effect";1import * as Effect from "effect/Effect";
2import * as SqlClient from "effect/unstable/sql/SqlClient";2import * as SqlClient from "effect/unstable/sql/SqlClient";
33
4// Schema setup composed by migration 050.4// Schema setup composed by migration 055.
5export default Effect.gen(function* () {5export default Effect.gen(function* () {
6 const sql = yield* SqlClient.SqlClient;6 const sql = yield* SqlClient.SqlClient;
77
apps/server/src/persistence/Migrations/OrchestrationV2/RecoveryIndexes.ts+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import * as Effect from "effect/Effect";1import * as Effect from "effect/Effect";
2import * as SqlClient from "effect/unstable/sql/SqlClient";2import * as SqlClient from "effect/unstable/sql/SqlClient";
33
4// Index setup composed by migration 050.4// Index setup composed by migration 055.
5export default Effect.gen(function* () {5export default Effect.gen(function* () {
6 const sql = yield* SqlClient.SqlClient;6 const sql = yield* SqlClient.SqlClient;
77
apps/server/src/persistence/Migrations/OrchestrationV2/ScheduledTasks.ts+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import * as Effect from "effect/Effect";1import * as Effect from "effect/Effect";
2import * as SqlClient from "effect/unstable/sql/SqlClient";2import * as SqlClient from "effect/unstable/sql/SqlClient";
33
4// Schema setup composed by migration 050.4// Schema setup composed by migration 055.
5export default Effect.gen(function* () {5export default Effect.gen(function* () {
6 const sql = yield* SqlClient.SqlClient;6 const sql = yield* SqlClient.SqlClient;
77
apps/server/src/persistence/Migrations/OrchestrationV2/ShellIndexes.ts+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import * as Effect from "effect/Effect";1import * as Effect from "effect/Effect";
2import * as SqlClient from "effect/unstable/sql/SqlClient";2import * as SqlClient from "effect/unstable/sql/SqlClient";
33
4// Index setup composed by migration 050.4// Index setup composed by migration 055.
5export default Effect.gen(function* () {5export default Effect.gen(function* () {
6 const sql = yield* SqlClient.SqlClient;6 const sql = yield* SqlClient.SqlClient;
77
apps/server/src/persistence/Migrations/OrchestrationV2/Subagents.ts+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import * as Effect from "effect/Effect";1import * as Effect from "effect/Effect";
2import * as SqlClient from "effect/unstable/sql/SqlClient";2import * as SqlClient from "effect/unstable/sql/SqlClient";
33
4// Schema setup composed by migration 050.4// Schema setup composed by migration 055.
5export default Effect.gen(function* () {5export default Effect.gen(function* () {
6 const sql = yield* SqlClient.SqlClient;6 const sql = yield* SqlClient.SqlClient;
77
apps/server/src/persistence/Migrations/OrchestrationV2/ThreadLaunchWorkflows.ts+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import * as Effect from "effect/Effect";1import * as Effect from "effect/Effect";
2import * as SqlClient from "effect/unstable/sql/SqlClient";2import * as SqlClient from "effect/unstable/sql/SqlClient";
33
4// Schema setup composed by migration 050.4// Schema setup composed by migration 055.
5export default Effect.gen(function* () {5export default Effect.gen(function* () {
6 const sql = yield* SqlClient.SqlClient;6 const sql = yield* SqlClient.SqlClient;
7 yield* sql`7 yield* sql`
apps/server/src/storageCleanup.ts+1-1
...@@ -371,7 +371,7 @@ export const make = Effect.gen(function* () {...@@ -371,7 +371,7 @@ export const make = Effect.gen(function* () {
371 return;371 return;
372 yield* git.removeWorktree({ cwd: project.workspaceRoot, path: worktreePath, force: false });372 yield* git.removeWorktree({ cwd: project.workspaceRoot, path: worktreePath, force: false });
373 yield* gitManager.invalidateStatus(project.workspaceRoot);373 yield* gitManager.invalidateStatus(project.workspaceRoot);
374 // Preserve branch and path: ProviderCommandReactor recreates the checkout374 // Preserve branch and path: ProviderTurnStartService recreates the checkout
375 // from that branch when the thread is resumed.375 // from that branch when the thread is resumed.
376 yield* Effect.logInfo("storage cleanup removed worktree", { threadId: thread.id });376 yield* Effect.logInfo("storage cleanup removed worktree", { threadId: thread.id });
377 }).pipe(377 }).pipe(
apps/server/src/testUtils/shardWeights.json+57-35
...@@ -1,40 +1,62 @@...@@ -1,40 +1,62 @@
1{1{
2 "integration/orchestrationEngine.integration.test.ts": 1.9,2 "integration/transferBudgetV2.integration.test.ts": 0.7,
3 "integration/providerService.integration.test.ts": 0.5,3 "src/auth/SessionStore.test.ts": 0.5,
4 "src/bin.test.ts": 0.8,4 "src/checkpointing/CheckpointStore.test.ts": 0.7,
5 "src/checkpointing/CheckpointStore.test.ts": 0.6,5 "src/cli/project.test.ts": 0.7,
6 "src/device/sshDeviceScript.test.ts": 2.9,6 "src/device/sshDeviceScript.test.ts": 3,
7 "src/git/GitManager.test.ts": 18.7,7 "src/git/GitManager.test.ts": 22.7,
8 "src/orchestration/Layers/CheckpointReactor.test.ts": 7.1,8 "src/git/detachStackFrame.memory.test.ts": 0.5,
9 "src/orchestration/Layers/OrchestrationEngine.test.ts": 1.4,9 "src/mcp/OrchestratorMcpToolkit.integration.test.ts": 9.1,
10 "src/orchestration/Layers/ProjectionPipeline.test.ts": 0.5,10 "src/orchestration-v2/Adapters/AcpAdapterV2.test.ts": 52.3,
11 "src/orchestration/Layers/ProviderCommandReactor.test.ts": 3.8,11 "src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts": 1.8,
12 "src/orchestration/Layers/ProviderRuntimeIngestion.test.ts": 2.4,12 "src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts": 1.1,
13 "src/persistence/Layers/OrchestrationEventStore.test.ts": 2.2,13 "src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts": 0.6,
14 "src/process/externalLauncher.test.ts": 2.2,14 "src/orchestration-v2/Adapters/CodexAdapterV2.test.ts": 0.6,
15 "src/project/AgentSessionScanner.test.ts": 4.3,15 "src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts": 0.8,
16 "src/provider/CodexChatGptAuth.test.ts": 5.9,16 "src/orchestration-v2/FoundationPersistence.test.ts": 2.6,
17 "src/provider/Drivers/AntigravityDriver.test.ts": 2,17 "src/orchestration-v2/OpenCode2OrchestratorV2.integration.test.ts": 6,
18 "src/provider/Layers/AntigravityAdapter.test.ts": 0.5,18 "src/orchestration-v2/ProjectionStore.test.ts": 0.7,
19 "src/provider/Layers/ClaudeAdapter.test.ts": 0.7,19 "src/orchestration-v2/ProviderSessionManager.test.ts": 0.9,
20 "src/provider/Layers/CodexCollabRuntime.integration.test.ts": 3.4,20 "src/orchestration-v2/ProviderTurnStartService.memory.test.ts": 6.5,
21 "src/provider/Layers/CursorAdapter.test.ts": 8,21 "src/orchestration-v2/SelectionRestart.integration.test.ts": 2.5,
22 "src/provider/Layers/CursorProvider.test.ts": 1.4,22 "src/orchestration-v2/SteeringCompletion.integration.test.ts": 2.7,
23 "src/provider/Layers/GrokAdapter.test.ts": 9,23 "src/orchestration-v2/ThreadLaunchService.test.ts": 2,
24 "src/provider/Layers/GrokProvider.test.ts": 0.7,24 "src/orchestration-v2/ThreadTitleRegenerationService.test.ts": 0.5,
25 "src/provider/Layers/ProviderService.test.ts": 0.5,25 "src/orchestration-v2/legacy/LegacyV1Cutover.integration.test.ts": 0.5,
26 "src/provider/acp/AcpJsonRpcConnection.test.ts": 6.2,26 "src/orchestration-v2/runtimeLayer.test.ts": 0.9,
27 "src/provider/acp/XAiAcpExtension.test.ts": 0.6,27 "src/orchestration-v2/testkit/ClaudeReplayFixtures.integration.test.ts": 0.6,
28 "src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts": 53.4,
29 "src/orchestration-v2/testkit/OrchestratorReplayRecovery.integration.test.ts": 1.4,
30 "src/orchestration-v2/testkit/OrchestratorReplayRestartBackgroundNote.integration.test.ts": 1.9,
31 "src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts": 25.4,
32 "src/orchestration-v2/testkit/ThreadFork.integration.test.ts": 3.7,
33 "src/orchestration-v2/testkit/ThreadMergeBack.integration.test.ts": 4.3,
34 "src/persistence/Layers/OrchestrationEventStore.test.ts": 1.9,
35 "src/persistence/Layers/Sqlite.test.ts": 0.5,
36 "src/process/externalLauncher.test.ts": 2.3,
37 "src/project/AgentSessionScanner.test.ts": 4.2,
38 "src/project/ManagedProjectFolders.test.ts": 0.5,
39 "src/provider/CodexChatGptAuth.test.ts": 5.4,
40 "src/provider/Drivers/AntigravityDriver.test.ts": 2.3,
41 "src/provider/Layers/GrokProvider.test.ts": 0.9,
42 "src/provider/Layers/ProviderInstanceRegistryLive.test.ts": 0.5,
43 "src/provider/OpenCodeServerLedger.test.ts": 1.7,
44 "src/provider/acp/AcpClientTerminals.test.ts": 1.3,
45 "src/provider/acp/AcpJsonRpcConnection.test.ts": 10.2,
46 "src/provider/acp/AcpRegistryProbe.test.ts": 1,
47 "src/provider/acp/AcpSessionRuntime.processTree.test.ts": 4.3,
48 "src/provider/cursorSdk.test.ts": 0.7,
49 "src/provider/opencode2/OpenCode2Server.test.ts": 1.2,
28 "src/provider/opencodeRuntime.environment.test.ts": 1.1,50 "src/provider/opencodeRuntime.environment.test.ts": 1.1,
29 "src/pullRequest/PullRequestService.test.ts": 3.1,51 "src/pullRequest/PullRequestService.test.ts": 5.1,
30 "src/server.test.ts": 10.2,52 "src/rpcInitialItems.memory.test.ts": 5.3,
31 "src/serverSettings.test.ts": 0.5,53 "src/serverSettings.test.ts": 1,
32 "src/serviceLauncher.test.ts": 6.2,54 "src/serviceLauncher.test.ts": 6.2,
33 "src/terminal/Manager.test.ts": 1.6,55 "src/sourceControl/PrTemplateDetection.test.ts": 0.5,
34 "src/textGeneration/CursorTextGeneration.test.ts": 0.8,56 "src/terminal/Manager.test.ts": 1.8,
35 "src/textGeneration/GrokTextGeneration.test.ts": 1.2,57 "src/textGeneration/CodexTextGeneration.test.ts": 0.5,
36 "src/usage/usageTranscriptStreaming.test.ts": 0.8,58 "src/usage/usageTranscriptStreaming.test.ts": 0.8,
37 "src/vcs/GitVcsDriver.test.ts": 3.7,59 "src/vcs/GitVcsDriver.test.ts": 4.5,
38 "src/vcs/GitVcsDriverCore.test.ts": 6.5,60 "src/vcs/GitVcsDriverCore.test.ts": 6.4,
39 "src/workspace/WorkspaceEntries.test.ts": 1.961 "src/workspace/WorkspaceEntries.test.ts": 2.1
40}62}
docs/internals/composer-context-references.md+1-1
...@@ -123,7 +123,7 @@ trailing `<terminal_context>` form is parsed only when reading messages sent by...@@ -123,7 +123,7 @@ trailing `<terminal_context>` form is parsed only when reading messages sent by
123The composer sends `message.text` as canonical prose with reference links and123The composer sends `message.text` as canonical prose with reference links and
124`message.context.records` built from the draft (`buildMessageContext` in124`message.context.records` built from the draft (`buildMessageContext` in
125`apps/web/src/lib/composerContextRecords.ts`). Expired terminal excerpts are dropped from both.125`apps/web/src/lib/composerContextRecords.ts`). Expired terminal excerpts are dropped from both.
126The server projects provider text at turn start (`ProviderCommandReactor`), so the persisted126The server projects provider text at turn start (`projectComposerContextForProvider`), so the persisted
127message stays readable and the provider receives markers plus one envelope.127message stays readable and the provider receives markers plus one envelope.
128128
129Review comments and preview annotations enter the draft through store mutators. A mounted composer129Review comments and preview annotations enter the draft through store mutators. A mounted composer
docs/internals/devices.md+1-1
...@@ -60,7 +60,7 @@ a shim directory to the provider's PATH. The CLI installs on the environment...@@ -60,7 +60,7 @@ a shim directory to the provider's PATH. The CLI installs on the environment
60server even when that server cannot run simulators. Hosts start on demand.60server even when that server cannot run simulators. Hosts start on demand.
6161
62That environment is fixed when the provider subprocess spawns, so62That environment is fixed when the provider subprocess spawns, so
63[`prepareMcpSession`](../../apps/server/src/provider/Layers/ProviderService.ts)63[`prepareMcpSession`](../../apps/server/src/orchestration-v2/ProviderSessionManager.ts)
64starts agent-device only when device support and agent access have both been64starts agent-device only when device support and agent access have both been
65enabled, the session has the `device` capability, and the machine can run at65enabled, the session has the `device` capability, and the machine can run at
66least one platform. Starting it later from `device_open` would leave the66least one platform. Starting it later from `device_open` would leave the
docs/internals/glossary.md+11-13
...@@ -13,23 +13,21 @@ Terms whose meaning matters across T3 Code. Architecture and lifecycle constrain...@@ -13,23 +13,21 @@ Terms whose meaning matters across T3 Code. Architecture and lifecycle constrain
13| Workspace root | The project's base filesystem directory on the environment. |13| Workspace root | The project's base filesystem directory on the environment. |
14| Worktree | A separate Git checkout a thread can use instead of the project's main checkout. |14| Worktree | A separate Git checkout a thread can use instead of the project's main checkout. |
15| Thread | The durable conversation and work history for a project. It survives provider process exits. |15| Thread | The durable conversation and work history for a project. It survives provider process exits. |
16| Turn | One user-to-agent work cycle. Provider work can finish before checkpoint and diff work settles. |16| Turn | One user-to-agent cycle, a V2 run. Provider work can end before checkpoint and diff work settles. |
17| Activity | A non-message timeline item, such as a tool action, approval, or failure. |17| Activity | A non-message timeline item, such as a tool action, approval, or failure. |
18| T3 home | The base data directory. Runtime state normally lives under its `userdata` directory. |18| T3 home | The base data directory. Runtime state normally lives under its `userdata` directory. |
1919
20## Orchestration20## Orchestration
2121
22| Term | Meaning |22| Term | Meaning |
23| ----------------------- | -------------------------------------------------------------------------------------------- |23| ----------------------- | --------------------------------------------------------------------------------------------------------- |
24| Command | A request to change domain state. Accepting it does not mean its side effects have finished. |24| Command | A request to change domain state. Accepting it does not mean its side effects have finished. |
25| Event | A persisted fact produced by a command. |25| Event | A persisted fact produced by a command. |
26| Decider | The pure logic that turns a command and current state into events. |26| Orchestrator | The service that serializes commands and decides their events from current state, without I/O. |
27| Projection / read model | A view of current state derived from persisted events. |27| Projection / read model | A persisted view of current state, committed in the same transaction as the events that change it. |
28| Projector | The logic that applies events to a read model. |28| Command receipt | A durable record of a command's result, used to make retries idempotent. |
29| Reactor | A worker that performs follow-up work in response to recorded intent or runtime signals. |29| Outbox effect | Side-effect intent committed with the events, such as starting a provider turn or capturing a checkpoint. |
30| Command receipt | A durable record of a command's result, used to make retries idempotent. |30| Effect worker | The worker that runs outbox effects after commit and feeds their results back as commands. |
31| Runtime receipt | A test-only signal that an asynchronous milestone completed. |
32| Quiesced | The relevant follow-up workers have finished, beyond the provider turn merely ending. |
3331
34## Providers and checkpoints32## Providers and checkpoints
3533
...@@ -51,7 +49,7 @@ Terms whose meaning matters across T3 Code. Architecture and lifecycle constrain...@@ -51,7 +49,7 @@ Terms whose meaning matters across T3 Code. Architecture and lifecycle constrain
51| Term | Meaning |49| Term | Meaning |
52| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |50| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53| Pull request link | A persisted thread association identified by host, repository, and number. Links can cross projects within an environment and carry a server-maintained snapshot. |51| Pull request link | A persisted thread association identified by host, repository, and number. Links can cross projects within an environment and carry a server-maintained snapshot. |
54| Pull request sync | The reactor that refreshes each distinct linked review once per cadence and discovers native stack layers. Explicit refreshes and failed stack reads trigger another read. |52| Pull request sync | The worker that refreshes each distinct linked review once per cadence and discovers native stack layers. Explicit refreshes and failed stack reads trigger another read. |
55| Current pull request | The link used by single-review controls and older clients. Open work takes precedence; a completed single chain points at its top layer. Unrelated terminal links use the latest update. |53| Current pull request | The link used by single-review controls and older clients. Open work takes precedence; a completed single chain points at its top layer. Unrelated terminal links use the latest update. |
5654
57## Composer context55## Composer context
docs/internals/providers.md+18-11
...@@ -3,7 +3,7 @@...@@ -3,7 +3,7 @@
3Orchestration records intent and state without knowing which provider runs a thread. Provider3Orchestration records intent and state without knowing which provider runs a thread. Provider
4protocols, account ownership, permissions, and capabilities belong at the4protocols, account ownership, permissions, and capabilities belong at the
5[adapter boundary](../../apps/server/src/orchestration-v2/ProviderAdapter.ts). Normalize there5[adapter boundary](../../apps/server/src/orchestration-v2/ProviderAdapter.ts). Normalize there
6instead of spreading provider checks through reactors and clients.6instead of spreading provider checks through orchestration and clients.
77
8A driver kind identifies an integration; an instance identifies one configuration and account8A driver kind identifies an integration; an instance identifies one configuration and account
9lifecycle. Route work by instance, so two accounts using the same driver do not share mutable9lifecycle. Route work by instance, so two accounts using the same driver do not share mutable
...@@ -11,16 +11,23 @@ session or catalog state....@@ -11,16 +11,23 @@ session or catalog state.
1111
12## Process and account isolation12## Process and account isolation
1313
14T3-managed OpenCode chat uses one server per thread. Its MCP registrations are directory-scoped, while14The `opencode` driver probes the installed version and runs the 1.x or 2.x runtime. OpenCode's MCP
15T3's MCP connection is thread-scoped. Sharing a chat server between threads in one directory would15registrations are directory-scoped, while T3's MCP connection is thread-scoped, so threads in one
16let them replace each other's connection. Catalog and text-generation work can share the16directory must not share one T3 MCP entry.
17[instance-owned helper](../../apps/server/src/provider/OpenCodeServerOwner.ts), which closes17
18after an idle period. External OpenCode servers remain externally owned and can require an18- **1.x** uses one T3-managed chat server per thread, so threads cannot replace each other's
19external restart to pick up configuration changes.19 connection. Catalog and text-generation work can share the
2020 [instance-owned helper](../../apps/server/src/provider/OpenCodeServerOwner.ts), which closes
21OpenCode also stores persistent approval grants per directory. Automatic full-access replies use21 after an idle period. See the [1.x adapter](../../apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.ts).
22`once` so they cannot widen a supervised thread's permissions on a shared external server.22- **2.x** serves every directory from one
23See the [adapter](../../apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.ts).23 [server per instance](../../apps/server/src/provider/opencode2/OpenCode2Server.ts). Each thread
24 registers its own `t3-code-<thread>` MCP entry, and session permission rules deny every other
25 thread's entry. See the [2.x adapter](../../apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts).
26
27External OpenCode servers remain externally owned and can require an external restart to pick up
28configuration changes. OpenCode stores "always" approval grants for the whole project. Automatic
29full-access replies use `once` so they cannot widen a supervised thread's permissions on a shared
30server. On 2.x, a session-wide approval also replies `once` and becomes T3's own rule on that session.
2431
25Pi runs the user's own `pi` install in RPC mode and owns native extension, package, and project32Pi runs the user's own `pi` install in RPC mode and owns native extension, package, and project
26trust discovery. T3 injects only its namespaced MCP bridge, so a Pi session behaves as it does in33trust discovery. T3 injects only its namespaced MCP bridge, so a Pi session behaves as it does in
oxlint-plugin-t3code/rules/no-manual-effect-runtime-in-tests.ts+3-3
...@@ -19,9 +19,9 @@ const EFFECT_RUNTIME_METHODS = new Set([...@@ -19,9 +19,9 @@ const EFFECT_RUNTIME_METHODS = new Set([
19 "runSyncWith",19 "runSyncWith",
20]);20]);
2121
22// Existing manual runners are tracked as debt through the `maxOccurrences`22// The lint config can set `maxOccurrences` per file to track existing manual
23// option, set per file in the lint config. The rule permits no net-new23// runners as debt. The rule permits no net-new occurrences in those files,
24// occurrences in those files, while every other test file must have zero.24// while every other test file must have zero.
25const readMaxOccurrences = (options: ReadonlyArray<unknown>): number => {25const readMaxOccurrences = (options: ReadonlyArray<unknown>): number => {
26 const [first] = options;26 const [first] = options;
27 return typeof first === "object" &&27 return typeof first === "object" &&
scripts/mobile-showcase-environment.ts+9-4
...@@ -427,7 +427,11 @@ function insertThread(...@@ -427,7 +427,11 @@ function insertThread(
427 .run(input.id, isWorking ? "running" : "ready", isWorking ? turnId : null, updatedAt);427 .run(input.id, isWorking ? "running" : "ready", isWorking ? turnId : null, updatedAt);
428}428}
429429
430const SEEDED_PROJECTION_TABLES = [430// V1 tables this seed owns. `projection_projects` is not listed: V2 still
431// stores projects there, so the seed upserts its own rows instead. V2 clients
432// do not read the V1 thread rows; moving the seed to V2 is tracked in
433// https://github.com/pingdotgg/t3code/issues/15013.
434const SEEDED_V1_TABLES = [
431 "projection_pending_approvals",435 "projection_pending_approvals",
432 "projection_thread_proposed_plans",436 "projection_thread_proposed_plans",
433 "projection_thread_activities",437 "projection_thread_activities",
...@@ -435,10 +439,11 @@ const SEEDED_PROJECTION_TABLES = [...@@ -435,10 +439,11 @@ const SEEDED_PROJECTION_TABLES = [
435 "projection_thread_sessions",439 "projection_thread_sessions",
436 "projection_turns",440 "projection_turns",
437 "projection_threads",441 "projection_threads",
438 "projection_projects",
439 "projection_state",442 "projection_state",
440] as const;443] as const;
441444
445const SEEDED_PROJECTION_TABLES = [...SEEDED_V1_TABLES, "projection_projects"] as const;
446
442const SEEDED_THREAD_COLUMNS = ["snoozed_until", "snoozed_at"] as const;447const SEEDED_THREAD_COLUMNS = ["snoozed_until", "snoozed_at"] as const;
443448
444function hasSeedableSchema(dbPath: string): boolean {449function hasSeedableSchema(dbPath: string): boolean {
...@@ -491,11 +496,11 @@ function seedDatabase(...@@ -491,11 +496,11 @@ function seedDatabase(
491 const database = new NodeSqlite.DatabaseSync(dbPath, { timeout: 30_000 });496 const database = new NodeSqlite.DatabaseSync(dbPath, { timeout: 30_000 });
492 try {497 try {
493 database.exec("BEGIN IMMEDIATE");498 database.exec("BEGIN IMMEDIATE");
494 for (const table of SEEDED_PROJECTION_TABLES) {499 for (const table of SEEDED_V1_TABLES) {
495 database.exec(`DELETE FROM ${table}`);500 database.exec(`DELETE FROM ${table}`);
496 }501 }
497 const insertProject = database.prepare(502 const insertProject = database.prepare(
498 `INSERT INTO projection_projects (503 `INSERT OR REPLACE INTO projection_projects (
499 project_id, title, workspace_root, default_model_selection_json, scripts_json,504 project_id, title, workspace_root, default_model_selection_json, scripts_json,
500 created_at, updated_at, deleted_at505 created_at, updated_at, deleted_at
501 ) VALUES (?, ?, ?, ?, ?, ?, ?, NULL)`,506 ) VALUES (?, ?, ?, ?, ?, ?, ?, NULL)`,
vite.config.ts-21
...@@ -333,27 +333,6 @@ export default defineConfig({...@@ -333,27 +333,6 @@ export default defineConfig({
333 "t3code/no-mobile-uniwind-theme-escape-hatches": ["error", { allowUniwindTheme: true }],333 "t3code/no-mobile-uniwind-theme-escape-hatches": ["error", { allowUniwindTheme: true }],
334 },334 },
335 },335 },
336 // Legacy manual Effect runners tracked as debt: no net-new occurrences.
337 // Lower a ceiling when you migrate a file, and delete its entry at zero.
338 ...Object.entries({
339 "apps/server/src/orchestration/Layers/CheckpointReactor.test.ts": 42,
340 "apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts": 5,
341 "apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts": 4,
342 "apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts": 66,
343 "apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts": 29,
344 "apps/server/src/orchestration/Layers/ThreadDeletionReactor.test.ts": 2,
345 "apps/server/src/orchestration/commandInvariants.test.ts": 5,
346 "apps/server/src/orchestration/projector.test.ts": 20,
347 "apps/server/src/provider/Layers/CodexAdapter.test.ts": 1,
348 "apps/server/src/provider/Layers/CursorAdapter.test.ts": 1,
349 "apps/server/src/provider/Layers/CursorProvider.test.ts": 1,
350 "apps/server/src/provider/Layers/ProviderService.test.ts": 2,
351 "apps/server/src/provider/Layers/ProviderSessionReaper.test.ts": 12,
352 "apps/server/src/provider/acp/CursorAcpSupport.test.ts": 1,
353 }).map(([file, maxOccurrences]) => {
354 const rule: ["error", { maxOccurrences: number }] = ["error", { maxOccurrences }];
355 return { files: [file], rules: { "t3code/no-manual-effect-runtime-in-tests": rule } };
356 }),
357 ],336 ],
358 options: {337 options: {
359 reportUnusedDisableDirectives: "error",338 reportUnusedDisableDirectives: "error",