Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/clear-rice-attack.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions .changeset/frank-times-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fleetfrog/web": patch
---

Expect T3 Code's database schema at migration 62.
53 changes: 36 additions & 17 deletions apps/agent-rs/src/t3code/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -211,31 +211,50 @@ fn decode_project(row: &Row) -> Option<ProjectRow> {
})
}

fn worktree_from_payload(json: &str) -> Option<Option<String>> {
match serde_json::from_str::<serde_json::Value>(json)
.ok()?
.get("worktreePath")?
{
struct ThreadPayload {
worktree_path: Option<String>,
subagent: bool,
}

fn read_thread_payload(json: &str) -> Option<ThreadPayload> {
let payload = serde_json::from_str::<serde_json::Value>(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<ThreadRow> {
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,
})
}

Expand Down
20 changes: 15 additions & 5 deletions apps/agent-ts/src/t3Code/readT3Code.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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();

Expand Down Expand Up @@ -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);
}),
);
Expand Down
9 changes: 8 additions & 1 deletion apps/agent-ts/src/t3Code/readT3Code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) }),
Comment thread
jameswyse marked this conversation as resolved.
}),
),
});

Expand All @@ -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,
}),
);
Expand Down
4 changes: 2 additions & 2 deletions packages/protocol/src/domain/t3Code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down
2 changes: 1 addition & 1 deletion tools/e2e/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading