Skip to content
Open
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
21 changes: 12 additions & 9 deletions src/CodexAcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import type {QuotaMeta} from "./QuotaMeta";
import {logger} from "./Logger";
import {sanitizeMcpServerName} from "./McpServerName";
import {createResponseItemHistoryFallbackUpdates} from "./ResponseItemHistoryFallback";
import {getSubAgentActivityTracker} from "./SubAgentActivityTracker";
import {
GOAL_CONTROL_ACTIONS,
GOAL_CONTROL_METHOD,
Expand All @@ -59,7 +60,6 @@ import {
type SessionSteerRequest,
} from "./AcpExtensions";
import {
createCollabAgentToolCallUpdate,
createCommandExecutionCompleteUpdate,
createCommandExecutionUpdate,
createCompletedContextCompactionUpdate,
Expand All @@ -68,7 +68,6 @@ import {
createImageGenerationUpdate,
createImageViewUpdate,
createMcpToolCallUpdate,
createSubAgentActivityUpdate,
formatWebSearchTitle,
} from "./CodexToolCallMapper";
import {
Expand Down Expand Up @@ -245,7 +244,7 @@ export class CodexAcpServer {
this.sessionFailureEpoch = randomUUID();
this.clientInfo = null;
this.clientCapabilities = null;
this.terminalOutputMode = "terminal_output_delta";
this.terminalOutputMode = "content";
this.booleanConfigOptionsSupported = false;
this.availableCommands = new CodexCommands(
connection,
Expand Down Expand Up @@ -1576,6 +1575,7 @@ export class CodexAcpServer {
return normalized.length > 0 ? normalized : null;
}

// @spec: restored-sub-agent-tool-call-parity#replay-sub-agent-activity-with-live-structure
private async createHistoryUpdates(item: ThreadItem, sessionState: SessionState): Promise<UpdateSessionEvent[]> {
switch (item.type) {
case "userMessage":
Expand All @@ -1584,7 +1584,7 @@ export class CodexAcpServer {
case "sleep":
return [];
case "subAgentActivity":
return [createSubAgentActivityUpdate(item, "completed", "tool_call")];
return getSubAgentActivityTracker(sessionState).mapSubAgentActivity(item, "completed");
case "agentMessage": {
const meta = createCodexMessagePhaseMeta(item.phase);
return [{
Expand All @@ -1599,7 +1599,7 @@ export class CodexAcpServer {
case "fileChange":
return [await createFileChangeUpdate(item)];
case "commandExecution": {
const updates = [await createCommandExecutionUpdate(item)];
const updates = [await createCommandExecutionUpdate(item, sessionState.terminalOutputMode)];
const completeUpdate = createCommandExecutionCompleteUpdate(item, sessionState.terminalOutputMode);
if (completeUpdate) {
updates.push(completeUpdate);
Expand All @@ -1611,7 +1611,7 @@ export class CodexAcpServer {
case "dynamicToolCall":
return [await createDynamicToolCallUpdate(item)];
case "collabAgentToolCall":
return [createCollabAgentToolCallUpdate(item)];
return getSubAgentActivityTracker(sessionState).mapCollabAgentToolCall(item, "completed");
case "webSearch":
return [this.createWebSearchUpdate(item)];
case "imageView":
Expand Down Expand Up @@ -2553,9 +2553,9 @@ function mergeHistoryUpdates(
const seen = new Set<string>();
let fallbackIndex = 0;

const pushUpdate = (update: UpdateSessionEvent) => {
const pushUpdate = (update: UpdateSessionEvent, dedupe: boolean = true) => {
const key = historyUpdateKey(update);
if (key && seen.has(key)) {
if (dedupe && key && seen.has(key)) {
return;
}
if (key) {
Expand Down Expand Up @@ -2591,7 +2591,10 @@ function mergeHistoryUpdates(

for (const update of threadUpdates) {
flushFallbackBeforeMatchingDuplicate(update);
pushUpdate(update);
// Thread history is authoritative and can legitimately contain several
// progress updates for the same tool call. Only fallback records are
// deduplicated against those updates.
pushUpdate(update, false);
}

while (fallbackIndex < responseItemFallbackUpdates.length) {
Expand Down
44 changes: 44 additions & 0 deletions src/CodexAppServerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export class CodexAppServerClient {
private readonly threadGoalClearedCaptures = new Map<string, Set<() => void>>();
private readonly threadSettings = new Map<string, ThreadSettings>();
private readonly staleTurnIds = new Map<string, Set<string>>();
private readonly childThreadParents = new Map<string, string>();

constructor(connection: MessageConnection) {
this.connection = connection;
Expand Down Expand Up @@ -184,6 +185,7 @@ export class CodexAppServerClient {
if (this.handleStaleTurnNotification(serverNotification, routing)) {
return;
}
this.recordChildThreadParent(serverNotification);
this.recordTurnRouting(routing);
if (this.handleStaleTurnNotification(serverNotification, routing)) {
return;
Expand Down Expand Up @@ -262,6 +264,12 @@ export class CodexAppServerClient {
this.notificationHandlers.delete(threadId);
this.approvalHandlers.delete(threadId);
this.elicitationHandlers.delete(threadId);
this.childThreadParents.delete(threadId);
for (const [childThreadId, parentThreadId] of this.childThreadParents) {
if (parentThreadId === threadId) {
this.childThreadParents.delete(childThreadId);
}
}
}

async initialize(params: InitializeParams): Promise<InitializeResponse> {
Expand Down Expand Up @@ -687,13 +695,41 @@ export class CodexAppServerClient {
if (handler) {
handler(notification);
}
const parentThreadId = this.childThreadParents.get(threadId);
const parentHandler = parentThreadId ? this.notificationHandlers.get(parentThreadId) : undefined;
if (
parentHandler
&& parentHandler !== handler
&& isChildActivityNotification(notification)
) {
parentHandler(notification);
}
return;
}
for (const notificationHandler of this.notificationHandlers.values()) {
notificationHandler(notification);
}
}

private recordChildThreadParent(notification: ServerNotification): void {
if (notification.method !== "item/started" && notification.method !== "item/completed") {
return;
}
const item = notification.params.item;
if (item.type === "subAgentActivity") {
this.childThreadParents.set(item.agentThreadId, notification.params.threadId);
return;
}
if (
item.type === "collabAgentToolCall"
&& (item.tool === "spawnAgent" || item.tool === "resumeAgent")
) {
for (const childThreadId of item.receiverThreadIds) {
this.childThreadParents.set(childThreadId, notification.params.threadId);
}
}
}

private recordTurnCompleted(event: TurnCompletedNotification): void {
const threadResolvers = this.pendingTurnCompletionResolvers.get(event.threadId);
const resolve = threadResolvers?.get(event.turn.id);
Expand Down Expand Up @@ -1018,6 +1054,14 @@ function isTurnCompletedNotification(notification: ServerNotification): notifica
return notification.method === "turn/completed";
}

function isChildActivityNotification(notification: ServerNotification): boolean {
if (notification.method === "turn/completed") {
return true;
}
return notification.method === "item/completed"
&& notification.params.item.type === "agentMessage";
}

function isThreadStatusChangedNotification(notification: ServerNotification): notification is {
method: "thread/status/changed";
params: ThreadStatusChangedNotification;
Expand Down
Loading