From 1a0c1716bddadf10779f8c3973765ef4af80252a Mon Sep 17 00:00:00 2001 From: James Wyse Date: Fri, 16 Oct 2026 17:15:34 +1000 Subject: [PATCH 1/3] fix: support T3 Code's schema at migration 62 T3 Code's migration 62 (ThreadFailureLookupIndex) only adds a partial index on orchestration_v2_projection_turn_items, which neither agent reads. Both agents read live databases at migration 62 on EpicDev and Studio identically with no unread records, so the supported schema moves to 62. --- .changeset/frank-times-wave.md | 5 +++++ packages/protocol/src/domain/t3Code.ts | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/frank-times-wave.md diff --git a/.changeset/frank-times-wave.md b/.changeset/frank-times-wave.md new file mode 100644 index 0000000..63d0817 --- /dev/null +++ b/.changeset/frank-times-wave.md @@ -0,0 +1,5 @@ +--- +"@fleetfrog/web": patch +--- + +Expect T3 Code's database schema at migration 62. diff --git a/packages/protocol/src/domain/t3Code.ts b/packages/protocol/src/domain/t3Code.ts index eba3f78..b0dd7a6 100644 --- a/packages/protocol/src/domain/t3Code.ts +++ b/packages/protocol/src/domain/t3Code.ts @@ -7,8 +7,8 @@ export const T3CodeSchema = Schema.Struct({ migration: Schema.Int, name: Reporte export type T3CodeSchema = typeof T3CodeSchema.Type; export const supportedT3CodeSchema: T3CodeSchema = { - migration: 61, - name: "PullRequestReviewGuides", + migration: 62, + name: "ThreadFailureLookupIndex", }; export function schemaDrift(schema: T3CodeSchema): "Current" | "Newer" | "Older" { From d90948a1971d87f13ae8d69542ccf56f536f8b27 Mon Sep 17 00:00:00 2001 From: James Wyse Date: Fri, 16 Oct 2026 17:15:36 +1000 Subject: [PATCH 2/3] fix: stop showing finished T3 Code subagents as waiting A Codex subagent can ask a question and then finish its run, leaving a user_input request pending that nobody will answer. T3 Code only surfaces a subagent's pending request while the subagent's run is active, so it showed the settled parent threads as done while FleetFrog showed the subagents as waiting for you indefinitely. Both agents now read the thread's lineage from its payload and count a subagent as waiting only while it has an active run. A thread payload without lineage is unreadable, as T3 Code's own schema requires it. --- .changeset/clear-rice-attack.md | 6 +++ apps/agent-rs/src/t3code/mod.rs | 53 ++++++++++++++------- apps/agent-ts/src/t3Code/readT3Code.test.ts | 20 ++++++-- apps/agent-ts/src/t3Code/readT3Code.ts | 9 +++- 4 files changed, 65 insertions(+), 23 deletions(-) create mode 100644 .changeset/clear-rice-attack.md diff --git a/.changeset/clear-rice-attack.md b/.changeset/clear-rice-attack.md new file mode 100644 index 0000000..c315ad0 --- /dev/null +++ b/.changeset/clear-rice-attack.md @@ -0,0 +1,6 @@ +--- +"@fleetfrog/agent-rs": patch +"@fleetfrog/agent-ts": patch +--- + +Stop showing a T3 Code subagent as waiting for you once its run has finished. diff --git a/apps/agent-rs/src/t3code/mod.rs b/apps/agent-rs/src/t3code/mod.rs index 84973d2..18b887a 100644 --- a/apps/agent-rs/src/t3code/mod.rs +++ b/apps/agent-rs/src/t3code/mod.rs @@ -63,7 +63,7 @@ const REQUIRED_COLUMNS: Columns = &[ struct Layout { columns: Columns, threads: &'static str, - worktree_in_payload: bool, + thread_payload: bool, } const V1_LAYOUT: Layout = Layout { @@ -95,7 +95,7 @@ const V1_LAYOUT: Layout = Layout { from projection_threads as thread left join projection_thread_sessions as session on session.thread_id = thread.thread_id where thread.deleted_at is null", - worktree_in_payload: false, + thread_payload: false, }; const V2_LAYOUT: Layout = Layout { @@ -132,7 +132,7 @@ const V2_LAYOUT: Layout = Layout { ) as working from orchestration_v2_projection_threads as thread where thread.deleted_at is null", - worktree_in_payload: true, + thread_payload: true, }; /// The colours T3 Code offers for project icons. @@ -211,31 +211,50 @@ fn decode_project(row: &Row) -> Option { }) } -fn worktree_from_payload(json: &str) -> Option> { - match serde_json::from_str::(json) - .ok()? - .get("worktreePath")? - { +struct ThreadPayload { + worktree_path: Option, + subagent: bool, +} + +fn read_thread_payload(json: &str) -> Option { + let payload = serde_json::from_str::(json).ok()?; + let nullable_text = |value: &serde_json::Value| match value { serde_json::Value::Null => Some(None), - serde_json::Value::String(path) => Some(Some(path.clone())), + serde_json::Value::String(text) => Some(Some(text.clone())), _ => None, - } + }; + let relationship = nullable_text(payload.get("lineage")?.get("relationshipToParent")?)?; + + Some(ThreadPayload { + worktree_path: nullable_text(payload.get("worktreePath")?)?, + subagent: relationship.as_deref() == Some("subagent"), + }) } fn decode_thread(row: &Row, layout: &Layout) -> Option { + let payload = if layout.thread_payload { + read_thread_payload(&row.text(3)?)? + } else { + ThreadPayload { + worktree_path: row.nullable_text(3)?, + subagent: false, + } + }; + let (waiting, working) = (row.integer(6)?, row.integer(7)?); + Some(ThreadRow { thread_id: row.text(0)?, project_id: row.text(1)?, title: row.text(2)?, - worktree_path: if layout.worktree_in_payload { - worktree_from_payload(&row.text(3)?)? - } else { - row.nullable_text(3)? - }, + worktree_path: payload.worktree_path, archived: row.integer(4)?, updated_at: row.time(5)?, - waiting: row.integer(6)?, - working: row.integer(7)?, + waiting: if payload.subagent && working == 0 { + 0 + } else { + waiting + }, + working, }) } diff --git a/apps/agent-ts/src/t3Code/readT3Code.test.ts b/apps/agent-ts/src/t3Code/readT3Code.test.ts index 82fa1ac..da73592 100644 --- a/apps/agent-ts/src/t3Code/readT3Code.test.ts +++ b/apps/agent-ts/src/t3Code/readT3Code.test.ts @@ -188,7 +188,9 @@ describe("readT3Code", () => { const shop = path.join(home, "shop"); const worktree = path.join(home, "worktrees", "shop-fix"); const database = createV2Database(path.join(home, "statev2.sqlite")); - const payload = (worktreePath: string | null) => JSON.stringify({ worktreePath }); + + const payload = (worktreePath: string | null, relationshipToParent: string | null = null) => + JSON.stringify({ worktreePath, lineage: { relationshipToParent } }); yield* TestClock.setTime(now); mkdirSync(worktree, { recursive: true }); @@ -203,16 +205,22 @@ describe("readT3Code", () => { ('t4', 'p1', 'Wrapping up', '${payload(null)}', null, null, '2026-09-26T03:00:00.000Z'), ('t5', 'p1', 'Archived', '${payload(null)}', '2026-09-26T07:00:00.000Z', null, '2026-09-26T07:00:00.000Z'), ('t6', 'p1', 'Deleted', '${payload(null)}', null, '2026-09-26T08:00:00.000Z', '2026-09-26T08:00:00.000Z'), - ('t7', 'p1', 'Changed shape', '{}', null, null, '2026-09-26T09:00:00.000Z'); + ('t7', 'p1', 'Changed shape', '{}', null, null, '2026-09-26T09:00:00.000Z'), + ('t8', 'p1', 'Finished subagent', '${payload(null, "subagent")}', null, null, '2026-09-26T02:00:00.000Z'), + ('t9', 'p1', 'Asking subagent', '${payload(null, "subagent")}', null, null, '2026-09-26T01:30:00.000Z'); insert into orchestration_v2_projection_runs values ('r1', 't1', 'running'), ('r2', 't2', 'completed'), ('r3', 't3', 'completed'), - ('r4', 't4', 'waiting'); + ('r4', 't4', 'waiting'), + ('r8', 't8', 'completed'), + ('r9', 't9', 'running'); insert into orchestration_v2_projection_runtime_requests values ('q1', 't2', 'user_input', 'pending'), ('q2', 't3', 'auth_refresh', 'pending'), - ('q3', 't1', 'permission', 'resolved'); + ('q3', 't1', 'permission', 'resolved'), + ('q8', 't8', 'user_input', 'pending'), + ('q9', 't9', 'user_input', 'pending'); `); database.close(); @@ -240,8 +248,10 @@ describe("readT3Code", () => { { title: "Fix checkout", folder: worktree, own: true, state: "Working" }, { title: "Signed out", folder: shop, own: false, state: "Idle" }, { title: "Wrapping up", folder: shop, own: false, state: "Working" }, + { title: "Finished subagent", folder: shop, own: false, state: "Idle" }, + { title: "Asking subagent", folder: shop, own: false, state: "Waiting" }, ]); - expect(status.reading.threadCount).toBe(4); + expect(status.reading.threadCount).toBe(6); expect(status.reading.unreadRecords).toBe(1); }), ); diff --git a/apps/agent-ts/src/t3Code/readT3Code.ts b/apps/agent-ts/src/t3Code/readT3Code.ts index d1c984c..f00b2a9 100644 --- a/apps/agent-ts/src/t3Code/readT3Code.ts +++ b/apps/agent-ts/src/t3Code/readT3Code.ts @@ -80,7 +80,10 @@ type ThreadRow = typeof ThreadRow.Type; const V2ThreadRow = Schema.Struct({ ...threadColumns, payload_json: Schema.fromJsonString( - Schema.Struct({ worktreePath: Schema.NullOr(Schema.String) }), + Schema.Struct({ + worktreePath: Schema.NullOr(Schema.String), + lineage: Schema.Struct({ relationshipToParent: Schema.NullOr(Schema.String) }), + }), ), }); @@ -92,6 +95,10 @@ const decodeV2Thread = (row: unknown) => Schema.decodeUnknownOption(V2ThreadRow)(row), ({ payload_json, ...thread }): ThreadRow => ({ ...thread, + waiting: + payload_json.lineage.relationshipToParent === "subagent" && thread.working === 0 + ? 0 + : thread.waiting, worktree_path: payload_json.worktreePath, }), ); From c2356f0843b0813e38aca797313aa7b5902ca910 Mon Sep 17 00:00:00 2001 From: James Wyse Date: Fri, 16 Oct 2026 17:20:03 +1000 Subject: [PATCH 3/3] test(e2e): seed T3 Code's thread lineage in the e2e database --- tools/e2e/repositories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/e2e/repositories.ts b/tools/e2e/repositories.ts index 6a7b16e..badb787 100644 --- a/tools/e2e/repositories.ts +++ b/tools/e2e/repositories.ts @@ -241,7 +241,7 @@ export function createRepositories(directory: string): void { "e2e-thread", "e2e-project", "E2E coding thread", - JSON.stringify({ worktreePath: null }), + JSON.stringify({ worktreePath: null, lineage: { relationshipToParent: null } }), now, ); database