Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e1b562b
feat: native subagent & workflow observability (Agents panel, quiet t…
t3dotgg Aug 2, 2026
9be170a
feat(web): agent spawn CTA row replaces live strip and inline workflo…
t3dotgg Aug 2, 2026
16cdc33
fix(web): honest workflow status, shell exclusion, flat panel rows
t3dotgg Aug 2, 2026
4622c3a
fix(web): spawn CTA rows survive turn folding and work-group overflow
t3dotgg Aug 2, 2026
3ee8054
fix(web): all in-flight subagent states present as Working
t3dotgg Aug 2, 2026
4947acc
fix(web): show spawn CTA during the run, anchored at the spawn point
t3dotgg Aug 2, 2026
8ebdb72
feat(web): sidebar liveness beyond the turn — Working and Monitoring
t3dotgg Aug 2, 2026
3573c9c
fix(web): standard subagents join the roster — task-type denylist
t3dotgg Aug 2, 2026
95c2047
fix: Codex collab children reach the CTA and panel with real names
t3dotgg Aug 2, 2026
0984bab
fix: idle Codex agents present as settled; names survive progress rows
t3dotgg Aug 2, 2026
23f75ed
fix(server): never intercept the Codex root thread as its own child
t3dotgg Aug 2, 2026
eb0fd9c
fix(web): one CTA per Claude parallel batch — pin membership at first…
t3dotgg Aug 2, 2026
a779285
fix(server): keep subagent-owned Claude traffic out of the parent tra…
t3dotgg Aug 2, 2026
9262161
fix: subagent-internal shells attributed to their owning agent
t3dotgg Aug 2, 2026
b16ee95
feat(web): workflow phase rail + read-only script access
t3dotgg Aug 2, 2026
a266b19
feat: stop-everything interrupt + collapsible phase sections
t3dotgg Aug 2, 2026
8d20b68
fix: CI + review round — lint, service conventions, TOCTOU, stop binding
t3dotgg Aug 2, 2026
239a013
fix: effect-diagnostics conformance for CI Check
t3dotgg Aug 2, 2026
77fa3f4
refactor(server): canonical ThreadBackgroundLiveness module, visible …
t3dotgg Aug 2, 2026
3585e63
refactor(server): canonical make/layer exports + bucket reclassificat…
t3dotgg Aug 2, 2026
2758593
refactor(contracts): structured reason on OrchestrationGetWorkflowScr…
t3dotgg Aug 3, 2026
25de5b7
feat(web): background-work banner above composer with stop action
t3dotgg Aug 3, 2026
3b84a19
feat: per-subagent model + reasoning effort, persisted and shown in p…
t3dotgg Aug 3, 2026
4b6d276
fix: external review round — fold correctness, legacy rows, Codex bat…
t3dotgg Aug 3, 2026
5ff1638
fix: terminal enrichment, Codex status linkage, mobile task.updated c…
t3dotgg Aug 4, 2026
5d68958
feat(server): server-stamped task data shapes (stacked on #5219) (#5316)
t3dotgg Aug 4, 2026
54f481d
fix: post-merge review round — thread-scoped progress ids, status pri…
t3dotgg Aug 4, 2026
f19e900
fix(web): Plan Ready outranks Monitoring in project rollups
t3dotgg Aug 4, 2026
2da6e6c
Merge remote-tracking branch 'origin/main' into t3code/native-subagen…
t3dotgg Aug 5, 2026
3ea2d28
fix: pass child approval resolutions through, keep nested agent termi…
t3dotgg Aug 5, 2026
5ef3232
test(server): Codex multi-agent wire fixtures + explicit child routin…
t3dotgg Aug 5, 2026
4e2c93a
fix(server): child-owned thread lifecycle never routes to the parent
t3dotgg Aug 5, 2026
55b3dd6
fix: interception ordering, runtime collab regression test, spawn-gro…
t3dotgg Aug 5, 2026
178eaf7
Merge remote-tracking branch 'origin/main' into t3code/native-subagen…
t3dotgg Aug 5, 2026
e15c780
fix: phases merge into the coordinator's single progress row + Codex …
t3dotgg Aug 5, 2026
fa38351
fix: tagged containment reasons, JSON suppression, spawn-turn no-back…
t3dotgg Aug 5, 2026
cd7dfc8
Merge remote-tracking branch 'origin/main' into t3code/native-subagen…
t3dotgg Aug 6, 2026
404fb52
fix: merge-bar round — spawnTurnId both paths, member coalescing, Sto…
t3dotgg Aug 6, 2026
b4296f4
fix: Hermes-safe sorts in subagentRuntime + directive placement
t3dotgg Aug 6, 2026
4fb1c71
fix: drop no-effect diagnostics directive
t3dotgg Aug 6, 2026
aaf6916
fix: bounded Stop deadlines, NUL-free fingerprint delimiter, coalesci…
t3dotgg Aug 6, 2026
f7673f3
fix(server): retryable child errors keep the child live and interrupt…
t3dotgg Aug 6, 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
46 changes: 46 additions & 0 deletions apps/mobile/src/lib/threadActivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ interface WorkLogEntry {
interface DerivedWorkLogEntry extends WorkLogEntry {
activityKind: OrchestrationThreadActivity["kind"];
collapseKey?: string;
/** Grouping key for subagent lifecycle rows (one row per agent). */
taskId?: string;
}

type RawThreadFeedEntry =
Expand Down Expand Up @@ -235,6 +237,26 @@ function resolvePendingUserInputAnswer(
return normalizeDraftAnswer(draft?.selectedOptionLabel);
}

/**
* Quiet-timeline guarantee (mirrors web's session-logic): agent-internal
* activity lives in the Agents sheet, not the work log. task.completed rows
* are kept — with no workflow card on mobile they are the terminal signal
* (a surface that hides rows must keep its own terminal signal).
*/
function isAgentInternalActivity(activity: OrchestrationThreadActivity): boolean {
const payload =
activity.payload && typeof activity.payload === "object"
? (activity.payload as Record<string, unknown>)
: null;
if (!payload) {
return false;
}
if (payload.timelineBypass === true && activity.kind !== "task.completed") {
return true;
}
return typeof payload.agentId === "string" && payload.agentId.trim().length > 0;
}
Comment thread
cursor[bot] marked this conversation as resolved.

function deriveWorkLogEntries(
activities: ReadonlyArray<OrchestrationThreadActivity>,
): DerivedWorkLogEntry[] {
Expand All @@ -243,9 +265,12 @@ function deriveWorkLogEntries(
for (const activity of ordered) {
if (activity.kind === "tool.started") continue;
if (activity.kind === "task.started") continue;
if (activity.kind === "task.updated") continue;
if (activity.kind === "tool.progress") continue;
if (activity.kind === "context-window.updated") continue;
if (activity.summary === "Checkpoint captured") continue;
if (isPlanBoundaryToolActivity(activity)) continue;
if (isAgentInternalActivity(activity)) continue;
entries.push(toDerivedWorkLogEntry(activity));
}
return collapseDerivedWorkLogEntries(entries);
Expand Down Expand Up @@ -284,10 +309,15 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo
? payload.detail
: null;
const taskLabel = taskSummary || taskDetailAsLabel;
const taskId =
isTaskActivity && typeof payload?.taskId === "string" && payload.taskId.length > 0
? payload.taskId
: undefined;
const entry: DerivedWorkLogEntry = {
id: activity.id,
createdAt: activity.createdAt,
turnId: activity.turnId,
...(taskId ? { taskId } : {}),
label: taskLabel || activity.summary,
tone:
activity.kind === "task.progress"
Expand Down Expand Up @@ -352,7 +382,23 @@ function collapseDerivedWorkLogEntries(
entries: ReadonlyArray<DerivedWorkLogEntry>,
): DerivedWorkLogEntry[] {
const collapsed: DerivedWorkLogEntry[] = [];
// Subagent rows collapse by identity, not adjacency (quiet-timeline
// guarantee; mirrors web's session-logic).
const taskRowIndex = new Map<string, number>();
for (const entry of entries) {
const isTaskRow =
entry.taskId !== undefined &&
(entry.activityKind === "task.progress" || entry.activityKind === "task.completed");
if (isTaskRow && entry.taskId !== undefined) {
const existingIndex = taskRowIndex.get(entry.taskId);
if (existingIndex !== undefined) {
collapsed[existingIndex] = mergeDerivedWorkLogEntries(collapsed[existingIndex]!, entry);
continue;
}
taskRowIndex.set(entry.taskId, collapsed.length);
collapsed.push(entry);
continue;
}
const previous = collapsed.at(-1);
if (previous && shouldCollapseToolLifecycleEntries(previous, entry)) {
collapsed[collapsed.length - 1] = mergeDerivedWorkLogEntries(previous, entry);
Expand Down
62 changes: 62 additions & 0 deletions apps/server/src/orchestration/ActivityPayloadProjection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, expect, it } from "vite-plus/test";
import type { OrchestrationThreadActivity } from "@t3tools/contracts";
import { projectActivityPayload } from "./ActivityPayloadProjection.ts";

function activity(payload: Record<string, unknown>): OrchestrationThreadActivity {
return {
id: "activity-1",
tone: "tool",
kind: "tool.completed",
summary: "Tool",
payload,
turnId: null,
createdAt: "2026-08-01T10:00:00.000Z",
} as unknown as OrchestrationThreadActivity;
}

/**
* Wire-survival regression: the slimming pass rewrites payload.data but must
* never strip the top-level per-agent fields the subagent fold depends on.
* If slimming ever moves to an allowlist over the whole payload, these
* assertions are the tripwire.
*/
describe("projectActivityPayload agent-field survival", () => {
it("preserves tool attribution (agentId/parentToolUseId) through data slimming", () => {
const projected = projectActivityPayload(
activity({
itemType: "command_execution",
agentId: "task-123",
parentToolUseId: "toolu_abc",
data: {
toolName: "Bash",
input: { command: "ls" },
command: "ls",
rawOutput: { content: "x".repeat(10) },
somethingClientNeverReads: { big: "blob" },
},
}),
);
const payload = projected.payload as Record<string, unknown>;
expect(payload.agentId).toBe("task-123");
expect(payload.parentToolUseId).toBe("toolu_abc");
// Slimming itself still applies to data.
const data = payload.data as Record<string, unknown>;
expect(data.somethingClientNeverReads).toBeUndefined();
});

it("passes task lifecycle payloads (no data field) through untouched", () => {
const source = activity({
taskId: "task-9",
title: "Audit auth",
role: "explorer",
model: "opus",
workflowName: "audit-flow",
phases: [{ index: 0, title: "Audit" }],
typedUsage: { totalTokens: 1200 },
runHandles: { runId: "run-1", scriptPath: "/tmp/wf.js" },
timelineBypass: true,
});
const projected = projectActivityPayload(source);
expect(projected.payload).toEqual(source.payload);
});
});
111 changes: 111 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,41 @@ function requestKindFromCanonicalRequestType(
}
}

/**
* Copies the optional TaskAgentLinkage bundle from a task.* runtime payload
* into the persisted activity payload. Identity fields ride on every row so
* client folds survive activity retention; absent fields stay absent.
*/
function taskLinkageActivityFields(payload: Record<string, unknown>): Record<string, unknown> {
const fields: Record<string, unknown> = {};
for (const key of [
"taskType",
"title",
"role",
"model",
"toolUseId",
"parentAgentId",
"workflowName",
"agentIndex",
"phaseIndex",
"phaseTitle",
"phases",
"attempt",
"runHandles",
"outputFile",
"agentPath",
"timelineBypass",
"typedUsage",
"status",
"error",
] as const) {
if (payload[key] !== undefined) {
fields[key] = payload[key];
}
}
return fields;
}

export function runtimeEventToActivities(
event: ProviderRuntimeEvent,
taskTitle?: string,
Expand Down Expand Up @@ -505,6 +540,7 @@ export function runtimeEventToActivities(
...(event.payload.description
? { detail: truncateDetail(event.payload.description) }
: {}),
...taskLinkageActivityFields(event.payload as Record<string, unknown>),
},
turnId: toTurnId(event.turnId) ?? null,
...maybeSequence,
Expand Down Expand Up @@ -532,6 +568,68 @@ export function runtimeEventToActivities(
...(event.payload.summary ? { summary: truncateDetail(event.payload.summary) } : {}),
...(event.payload.lastToolName ? { lastToolName: event.payload.lastToolName } : {}),
...(event.payload.usage !== undefined ? { usage: event.payload.usage } : {}),
...taskLinkageActivityFields(event.payload as Record<string, unknown>),
},
turnId: toTurnId(event.turnId) ?? null,
...maybeSequence,
},
];
}

case "task.updated": {
return [
{
id: event.eventId,
createdAt: event.createdAt,
tone: event.payload.status === "failed" ? "error" : "info",
kind: "task.updated",
summary:
event.payload.status === "failed"
? "Task failed"
: event.payload.status
? `Task ${event.payload.status}`
: "Task updated",
payload: {
taskId: event.payload.taskId,
...(event.payload.description
? { detail: truncateDetail(event.payload.description) }
: {}),
...(event.payload.endedAt ? { endedAt: event.payload.endedAt } : {}),
...(event.payload.isBackgrounded !== undefined
? { isBackgrounded: event.payload.isBackgrounded }
: {}),
...taskLinkageActivityFields(event.payload as Record<string, unknown>),
},
turnId: toTurnId(event.turnId) ?? null,
...maybeSequence,
},
];
}

case "tool.progress": {
// Only agent-owned heartbeats are persisted: they feed the owning
// agent's activity line. Parent-conversation tool progress stays
// ephemeral (item lifecycle already covers it).
if (event.payload.taskId === undefined) {
return [];
}
return [
{
id: event.eventId,
createdAt: event.createdAt,
tone: "info",
kind: "tool.progress",
summary: event.payload.toolName ?? "Tool progress",
payload: {
taskId: event.payload.taskId,
...(event.payload.toolName ? { toolName: event.payload.toolName } : {}),
...(event.payload.toolUseId ? { toolUseId: event.payload.toolUseId } : {}),
...(event.payload.elapsedSeconds !== undefined
? { elapsedSeconds: event.payload.elapsedSeconds }
: {}),
...(event.payload.parentToolUseId
? { parentToolUseId: event.payload.parentToolUseId }
: {}),
},
turnId: toTurnId(event.turnId) ?? null,
...maybeSequence,
Expand Down Expand Up @@ -565,6 +663,7 @@ export function runtimeEventToActivities(
}
: {}),
...(event.payload.usage !== undefined ? { usage: event.payload.usage } : {}),
...taskLinkageActivityFields(event.payload as Record<string, unknown>),
},
turnId: toTurnId(event.turnId) ?? null,
...maybeSequence,
Expand Down Expand Up @@ -630,6 +729,10 @@ export function runtimeEventToActivities(
...(event.payload.status ? { status: event.payload.status } : {}),
...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}),
...(event.payload.data !== undefined ? { data: event.payload.data } : {}),
...(event.payload.agentId ? { agentId: event.payload.agentId } : {}),
...(event.payload.parentToolUseId
? { parentToolUseId: event.payload.parentToolUseId }
: {}),
},
turnId: toTurnId(event.turnId) ?? null,
...maybeSequence,
Expand All @@ -652,6 +755,10 @@ export function runtimeEventToActivities(
itemType: event.payload.itemType,
...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}),
...(event.payload.data !== undefined ? { data: event.payload.data } : {}),
...(event.payload.agentId ? { agentId: event.payload.agentId } : {}),
...(event.payload.parentToolUseId
? { parentToolUseId: event.payload.parentToolUseId }
: {}),
},
turnId: toTurnId(event.turnId) ?? null,
...maybeSequence,
Expand All @@ -673,6 +780,10 @@ export function runtimeEventToActivities(
payload: {
itemType: event.payload.itemType,
...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}),
...(event.payload.agentId ? { agentId: event.payload.agentId } : {}),
...(event.payload.parentToolUseId
? { parentToolUseId: event.payload.parentToolUseId }
: {}),
},
turnId: toTurnId(event.turnId) ?? null,
...maybeSequence,
Expand Down
Loading
Loading