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/.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/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, }), ); 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" { 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