diff --git a/FORK.md b/FORK.md index 23a0c6bb147..49fc922ee21 100644 --- a/FORK.md +++ b/FORK.md @@ -59,6 +59,11 @@ This repository is a fork of `pingdotgg/t3code`. Keep this file focused on fork - The fork adds thread goal support, goal activity rendering, and goal sidebar/panel UI. +### Subagent Activity + +- Parent timelines keep subagent commands, file changes, tool calls, web searches, image views, and diffs. +- Subagent messages, reasoning, goals, plans, token usage, and thread/turn state stay out of the parent timeline. Codex child relationships are recognized from both `collabAgentToolCall` and `subAgentActivity` items. + ### Provider Launch Environment - Provider sessions use a shared launch environment pipeline instead of ad hoc environment assembly. diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index 20c918ca967..a80ab40d937 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -615,12 +615,12 @@ function readRouteFields(notification: CodexServerNotification): { } } -function rememberCollabReceiverTurns( - collabReceiverTurns: Map, +function rememberChildThreadTurns( + childThreadTurns: Map, notification: CodexServerNotification, parentTurnId: TurnId | undefined, ): void { - if (!parentTurnId) { + if (parentTurnId === undefined) { return; } @@ -628,34 +628,47 @@ function rememberCollabReceiverTurns( return; } - if (notification.params.item.type !== "collabAgentToolCall") { + const item = notification.params.item; + + if (item.type === "collabAgentToolCall") { + for (const receiverThreadId of item.receiverThreadIds) { + childThreadTurns.set(receiverThreadId, parentTurnId); + } return; } - for (const receiverThreadId of notification.params.item.receiverThreadIds) { - collabReceiverTurns.set(receiverThreadId, parentTurnId); + if (item.type === "subAgentActivity") { + childThreadTurns.set(item.agentThreadId, parentTurnId); } } -function shouldSuppressChildConversationNotification( - method: CodexRpc.ServerNotificationMethod, -): boolean { - return ( - method === "thread/started" || - method === "thread/status/changed" || - method === "thread/archived" || - method === "thread/unarchived" || - method === "thread/closed" || - method === "thread/compacted" || - method === "thread/name/updated" || - method === "thread/tokenUsage/updated" || - method === "thread/goal/updated" || - method === "thread/goal/cleared" || - method === "turn/started" || - method === "turn/completed" || - method === "turn/plan/updated" || - method === "item/plan/delta" - ); +function shouldKeepChildNotification(notification: CodexServerNotification): boolean { + switch (notification.method) { + case "item/started": + case "item/completed": + switch (notification.params.item.type) { + case "commandExecution": + case "fileChange": + case "mcpToolCall": + case "dynamicToolCall": + case "collabAgentToolCall": + case "webSearch": + case "imageView": + return true; + default: + return false; + } + case "item/commandExecution/outputDelta": + case "item/commandExecution/terminalInteraction": + case "item/fileChange/outputDelta": + case "item/fileChange/patchUpdated": + case "item/mcpToolCall/progress": + case "serverRequest/resolved": + case "turn/diff/updated": + return true; + default: + return false; + } } function toCodexUserInputAnswer( @@ -739,7 +752,7 @@ export const makeCodexSessionRuntime = ( const pendingApprovalsRef = yield* Ref.make(new Map()); const approvalCorrelationsRef = yield* Ref.make(new Map()); const pendingUserInputsRef = yield* Ref.make(new Map()); - const collabReceiverTurnsRef = yield* Ref.make(new Map()); + const childThreadTurnsRef = yield* Ref.make(new Map()); const closedRef = yield* Ref.make(false); // `~` is not shell-expanded when env vars are set via @@ -856,27 +869,35 @@ export const makeCodexSessionRuntime = ( ), ); + const currentSessionProviderThreadId = Effect.map(Ref.get(sessionRef), currentProviderThreadId); + const handleRawNotification = (notification: CodexServerNotification) => Effect.gen(function* () { const payload = notification.params; const route = readRouteFields(notification); - const collabReceiverTurns = yield* Ref.get(collabReceiverTurnsRef); - const childParentTurnId = (() => { - const providerConversationId = readNotificationThreadId(notification); - return providerConversationId - ? collabReceiverTurns.get(providerConversationId) - : undefined; - })(); - - rememberCollabReceiverTurns(collabReceiverTurns, notification, route.turnId); - if (childParentTurnId && shouldSuppressChildConversationNotification(notification.method)) { - yield* Ref.set(collabReceiverTurnsRef, collabReceiverTurns); + const providerThreadId = readNotificationThreadId(notification); + const rootProviderThreadId = yield* currentSessionProviderThreadId; + const childThreadTurns = yield* Ref.get(childThreadTurnsRef); + const childParentTurnId = + providerThreadId === undefined ? undefined : childThreadTurns.get(providerThreadId); + const parentTurnId = childParentTurnId ?? route.turnId; + + rememberChildThreadTurns(childThreadTurns, notification, parentTurnId); + + const belongsToChild = + childParentTurnId !== undefined || + (providerThreadId !== undefined && + rootProviderThreadId !== undefined && + providerThreadId !== rootProviderThreadId); + + if (belongsToChild && !shouldKeepChildNotification(notification)) { + yield* Ref.set(childThreadTurnsRef, childThreadTurns); return; } let requestId: ApprovalRequestId | undefined; let requestKind: ProviderRequestKind | undefined; - let turnId = childParentTurnId ?? route.turnId; + let turnId = parentTurnId; let itemId = route.itemId; if (notification.method === "serverRequest/resolved") { @@ -900,7 +921,7 @@ export const makeCodexSessionRuntime = ( } } - yield* Ref.set(collabReceiverTurnsRef, collabReceiverTurns); + yield* Ref.set(childThreadTurnsRef, childThreadTurns); yield* emitEvent({ kind: "notification", threadId: options.threadId, @@ -916,8 +937,6 @@ export const makeCodexSessionRuntime = ( }); }); - const currentSessionProviderThreadId = Effect.map(Ref.get(sessionRef), currentProviderThreadId); - yield* client.handleServerNotification("thread/started", (payload) => currentSessionProviderThreadId.pipe( Effect.flatMap((providerThreadId) => {