Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f49c1a9
feat(contracts): thread agent snapshot schemas for subagent observabi…
t3dotgg Jul 21, 2026
45783d5
feat(web): agents panel, live strip, and thread-agent derivation
t3dotgg Jul 21, 2026
7e898d2
feat(codex): track v2 collab child threads as agent telemetry
t3dotgg Jul 21, 2026
5472e9d
feat(server): Claude agent lifecycle fields + ingestion agent roster …
t3dotgg Jul 21, 2026
877bcca
test(client-runtime): thread agent derivation tests against persisted…
t3dotgg Jul 21, 2026
c513a40
fix(web): truncate long agent-type badges in agent cards
t3dotgg Jul 21, 2026
f99429b
fix: emit running on Codex wait resolution; hide agent.snapshot in mo…
t3dotgg Jul 21, 2026
0892ce3
fix: address PR review feedback; remove HTML plan docs
t3dotgg Jul 21, 2026
ce1f104
fix(ingestion): agent roster lifecycle hardening from round-2 review
t3dotgg Jul 21, 2026
4b9b7c5
fix(ingestion): non-blocking snapshot appends + roster refresh before…
t3dotgg Jul 21, 2026
a3248a1
fix(ingestion): seed roster refresh counter from snapshot age; log ap…
t3dotgg Jul 21, 2026
4586d5f
fix: address review handoff P1/P2 findings
t3dotgg Jul 21, 2026
812aee9
fix(ingestion+claude): round-5 review — hydration revision parity, am…
t3dotgg Jul 21, 2026
ffd0104
fix(ingestion): merge task usage field-wise instead of wholesale replace
t3dotgg Jul 21, 2026
61d0c38
fix(claude): consume undeclared background_tasks_changed silently
t3dotgg Jul 22, 2026
e9a498e
chore: drop test fragment superseded by dedicated coverage test from …
t3dotgg Jul 22, 2026
a40376b
fix: dual-review pass (Fable + gpt-5.6-sol high) on rebased branch
t3dotgg Jul 22, 2026
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
1 change: 1 addition & 0 deletions apps/mobile/src/lib/threadActivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ function deriveWorkLogEntries(
if (activity.kind === "tool.started") continue;
if (activity.kind === "task.started") continue;
if (activity.kind === "context-window.updated") continue;
if (activity.kind === "agent.snapshot") continue;
if (activity.summary === "Checkpoint captured") continue;
if (isPlanBoundaryToolActivity(activity)) continue;
entries.push(toDerivedWorkLogEntry(activity));
Expand Down
135 changes: 134 additions & 1 deletion apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
ProviderRuntimeEvent,
ProviderSession,
ProviderInstanceId,
RuntimeTaskId,
type ThreadAgentSnapshot,
} from "@t3tools/contracts";
import {
ApprovalRequestId,
Expand Down Expand Up @@ -43,7 +45,11 @@ import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityRes
import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";
import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";
import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts";
import { ProviderRuntimeIngestionLive } from "./ProviderRuntimeIngestion.ts";
import {
foldTaskAgentEvent,
ProviderRuntimeIngestionLive,
pruneSettledAgents,
} from "./ProviderRuntimeIngestion.ts";
import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
import { ProviderRuntimeIngestionService } from "../Services/ProviderRuntimeIngestion.ts";
import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts";
Expand Down Expand Up @@ -318,6 +324,133 @@ describe("ProviderRuntimeIngestion", () => {
};
}

it("folds material transitions, reactivations, and settled retention", () => {
const agents = new Map<string, ThreadAgentSnapshot>();
const started: Extract<ProviderRuntimeEvent, { type: "task.started" }> = {
type: "task.started",
eventId: asEventId("agent-started"),
provider: ProviderDriverKind.make("claudeAgent"),
threadId: asThreadId("thread-1"),
createdAt: "2026-01-01T00:00:00.000Z",
payload: {
taskId: RuntimeTaskId.make("agent-1"),
description: "Reviewer",
taskType: "local_agent",
},
};
expect(foldTaskAgentEvent(agents, started)).toBe(true);
const usageTick: Extract<ProviderRuntimeEvent, { type: "task.progress" }> = {
...started,
type: "task.progress",
eventId: asEventId("agent-progress"),
createdAt: "2026-01-01T00:00:01.000Z",
payload: {
taskId: RuntimeTaskId.make("agent-1"),
description: "Reviewer",
usage: { totalTokens: 10 },
},
};
expect(foldTaskAgentEvent(agents, usageTick)).toBe(false);
const completed: Extract<ProviderRuntimeEvent, { type: "task.completed" }> = {
...started,
type: "task.completed",
eventId: asEventId("agent-completed"),
createdAt: "2026-01-01T00:00:02.000Z",
payload: { taskId: RuntimeTaskId.make("agent-1"), status: "completed" },
};
expect(foldTaskAgentEvent(agents, completed)).toBe(true);
expect(
foldTaskAgentEvent(agents, { ...started, eventId: asEventId("agent-reactivated") }),
).toBe(true);
expect(agents.get("agent-1")?.activationCount).toBe(2);

for (let index = 0; index < 51; index += 1) {
agents.set(`settled-${index}`, {
agentId: `settled-${index}`,
provider: ProviderDriverKind.make("claudeAgent"),
kind: "subagent",
name: `Agent ${index}`,
status: "completed",
firstStartedAt: `2026-01-01T00:00:${String(index).padStart(2, "0")}.000Z`,
lastActivityAt: `2026-01-01T00:00:${String(index).padStart(2, "0")}.000Z`,
activationCount: 1,
recentActivity: [],
updatedAt: `2026-01-01T00:00:${String(index).padStart(2, "0")}.000Z`,
});
}
pruneSettledAgents(agents);
expect(
Array.from(agents.values()).filter((agent) => agent.status === "completed"),
).toHaveLength(50);
expect(agents.has("settled-0")).toBe(false);
});

it("appends latest-wins agent rosters only on material changes and sweeps orphans", async () => {
const harness = await createHarness();
harness.emit({
type: "task.started",
eventId: asEventId("evt-agent-start"),
provider: ProviderDriverKind.make("claudeAgent"),
threadId: asThreadId("thread-1"),
createdAt: "2026-01-01T00:00:01.000Z",
payload: { taskId: "agent-1", description: "Reviewer", taskType: "local_agent" },
});
await harness.drain();
harness.emit({
type: "task.progress",
eventId: asEventId("evt-agent-usage"),
provider: ProviderDriverKind.make("claudeAgent"),
threadId: asThreadId("thread-1"),
createdAt: "2026-01-01T00:00:02.000Z",
payload: { taskId: "agent-1", description: "Reviewer", usage: { totalTokens: 10 } },
});
await harness.drain();
let thread = (await harness.readModel()).threads.find((entry) => entry.id === "thread-1");
expect(
thread?.activities.filter((activity) => activity.kind === "agent.snapshot"),
).toHaveLength(1);

harness.emit({
type: "session.exited",
eventId: asEventId("evt-session-exit-agents"),
provider: ProviderDriverKind.make("claudeAgent"),
threadId: asThreadId("thread-1"),
createdAt: "2026-01-01T00:00:03.000Z",
payload: { reason: "process_exit" },
});
await harness.drain();
thread = (await harness.readModel()).threads.find((entry) => entry.id === "thread-1");
const snapshots =
thread?.activities.filter((activity) => activity.kind === "agent.snapshot") ?? [];
expect(snapshots).toHaveLength(2);
const latest = snapshots.at(-1)?.payload as { agents: Array<ThreadAgentSnapshot> } | undefined;
expect(latest?.agents).toHaveLength(1);
expect(latest?.agents[0]?.status).toBe("stopped");
expect(latest?.agents[0]?.errorMessage).toBe("orphaned on session exit");

// The session-exit sweep releases the in-memory roster; a later session on
// the same thread re-hydrates from the persisted snapshot instead of the
// evicted map (agent-1 stays stopped, not resurrected).
harness.emit({
type: "task.started",
eventId: asEventId("evt-agent-restart"),
provider: ProviderDriverKind.make("claudeAgent"),
threadId: asThreadId("thread-1"),
createdAt: "2026-01-01T00:00:04.000Z",
payload: { taskId: "agent-2", description: "Follow-up", taskType: "local_agent" },
});
await harness.drain();
thread = (await harness.readModel()).threads.find((entry) => entry.id === "thread-1");
const rehydrated =
thread?.activities.filter((activity) => activity.kind === "agent.snapshot") ?? [];
expect(rehydrated).toHaveLength(3);
const roster = rehydrated.at(-1)?.payload as { agents: Array<ThreadAgentSnapshot> } | undefined;
expect(roster?.agents.map((agent) => [agent.agentId, agent.status])).toEqual([
["agent-1", "stopped"],
["agent-2", "running"],
]);
});

it("maps turn started/completed events into thread session updates", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";
Expand Down
Loading
Loading