Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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: 4 additions & 2 deletions apps/mobile/src/lib/threadActivity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1624,7 +1624,7 @@ describe("retained v2 feed presentation", () => {
expect(afterPresentation[1]).toBe(beforePresentation[1]);
});

it.each(["running", "completed", "interrupted"] as const)(
it.each(["running", "completed", "failed", "interrupted", "cancelled"] as const)(
"uses the compaction row as the live activity only while %s",
(status) => {
const compact = projected(
Expand Down Expand Up @@ -1660,7 +1660,9 @@ describe("retained v2 feed presentation", () => {
? "Compacting context"
: status === "completed"
? "Context compacted 899K → 19K tokens"
: "Context compacted",
: status === "failed"
? "Context compaction failed"
: "Context compaction stopped",
},
],
});
Expand Down
140 changes: 140 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3161,6 +3161,146 @@ describe("CodexAdapterV2 post-settle continuation", () => {
),
);

it.effect.each([
{ status: "failed", completeItem: false },
{ status: "interrupted", completeItem: false },
{ status: "failed", completeItem: true },
] as const)(
"settles automatic compaction on $status (item completed: $completeItem)",
({ status, completeItem }) =>
Effect.scoped(
Effect.gen(function* () {
const nativeThreadId = `compact-${status}-thread`;
const nativeTurnId = `compact-${status}-turn`;
const item = { type: "contextCompaction", id: `compact-${status}-item` };
const error = {
message:
"Error running remote compact task: stream disconnected before completion: stream closed before response.completed",
codexErrorInfo: "other",
additionalDetails: null,
};
const transcript = makeCodexReplayTranscript({
scenario: `automatic-compaction-${status}`,
entries: [
...codexReplayPreamble({ nativeThreadId, nativeTurnId, prompt: "Continue." }),
{
type: "emit_inbound",
label: "item/started/compaction",
frame: {
method: "item/started",
params: {
threadId: nativeThreadId,
turnId: nativeTurnId,
item,
startedAtMs: 1782622441000,
},
},
},
...([1, 2] as const).map((attempt) => ({
type: "emit_inbound" as const,
label: `error/retry-${attempt}`,
frame: {
method: "error",
params: {
threadId: nativeThreadId,
turnId: nativeTurnId,
willRetry: true,
error: {
message: `Reconnecting... ${attempt}/2`,
additionalDetails: "stream disconnected before completion",
codexErrorInfo: { responseStreamDisconnected: { httpStatusCode: null } },
},
},
},
})),
...(completeItem
? [
{
type: "emit_inbound" as const,
label: "item/completed/compaction",
frame: {
method: "item/completed",
params: {
threadId: nativeThreadId,
turnId: nativeTurnId,
item,
completedAtMs: 1782622444000,
},
},
},
]
: []),
...(status === "failed"
? [
{
type: "emit_inbound" as const,
label: "error/terminal",
frame: {
method: "error",
params: {
threadId: nativeThreadId,
turnId: nativeTurnId,
willRetry: false,
error,
},
},
},
]
: []),
{
type: "emit_inbound",
label: "turn/completed",
frame: {
method: "turn/completed",
params: {
threadId: nativeThreadId,
turn: {
...makeCodexReplayTurn({ id: nativeTurnId, status }),
error: status === "failed" ? error : null,
},
},
},
},
],
});
const harness = yield* makeCodexReplayHarness(transcript);
yield* harness.runtime.startTurn(
makeCodexTestTurnInput({
threadId: harness.threadId,
providerThread: harness.providerThread,
now: yield* DateTime.now,
attemptId: RunAttemptId.make(`compact-${status}-attempt`),
text: "Continue.",
}),
);
yield* harness.firstTerminal;
const items = harness.events.flatMap((event) =>
event.type === "turn_item.updated" && event.turnItem.type === "compaction"
? [event.turnItem]
: [],
);
assert.deepEqual(
items.map((entry) => entry.status),
["running", completeItem ? "completed" : status],
);
assert.equal(items[0]?.id, items[1]?.id);
assert.deepEqual(items[1]?.startedAt, DateTime.makeUnsafe(1782622441000));
assert.deepEqual(
items[1]?.completedAt,
DateTime.makeUnsafe(completeItem ? 1782622444000 : 1782622450000),
);
assert.equal(harness.terminalEvents()[0]?.status, status);
if (status === "failed") {
assert.equal(harness.terminalEvents()[0]?.failure?.message, error.message);
}
}).pipe(
Effect.provide(
Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
),
),
),
);

it.effect("resumes a provider thread without requesting or decoding its history", () =>
Effect.scoped(
Effect.gen(function* () {
Expand Down
44 changes: 37 additions & 7 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,10 @@ interface ActiveCodexTurnContext {
readonly rootNodeId: OrchestrationV2ExecutionNode["id"];
readonly subagent: CodexSubagentThreadContext | null;
readonly startedAt: DateTime.Utc;
readonly runningCompactionItemIds: Set<string>;
// Item positions allocated in this turn. Later turns never look items up
// here: late background items resolve their settled turn's context, and a
// subagent's approvals allocate on the owning root turn.
readonly itemPositions: Map<
string,
{ readonly ordinal: number; readonly startedAt: DateTime.Utc }
Expand Down Expand Up @@ -2009,6 +2013,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
subagent: null,
startedAt: input.startedAt,
itemPositions: new Map(),
runningCompactionItemIds: new Set(),
};
yield* Ref.update(limitedTurnItems, (current) => {
const next = new Map(current);
Expand Down Expand Up @@ -2580,6 +2585,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
subagent,
startedAt: turn.startedAt,
itemPositions: new Map(),
runningCompactionItemIds: new Set(),
};
beginTurnTokenUsage(activeContext);
endSessionApprovalsOnModeChange(activeContext);
Expand Down Expand Up @@ -4574,15 +4580,17 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
const emitCompactionItem = Effect.fn("CodexAdapterV2.emitCompactionItem")(function* (
context: ActiveCodexTurnContext,
nativeItemId: string,
status: "running" | "completed",
nativeStartedAt?: DateTime.Utc,
status: "running" | "completed" | "failed" | "interrupted" | "cancelled",
nativeTimestamp?: DateTime.Utc,
) {
const now = yield* DateTime.now;
const now = nativeTimestamp ?? (yield* DateTime.now);
const { ordinal, startedAt } = yield* resolveItemPosition(
context,
nativeItemId,
nativeStartedAt,
nativeTimestamp,
);
if (status === "running") context.runningCompactionItemIds.add(nativeItemId);
else context.runningCompactionItemIds.delete(nativeItemId);
yield* emitProviderEvent({
type: "turn_item.updated",
driver: CODEX_PROVIDER,
Expand All @@ -4602,9 +4610,16 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
type: "compaction",
driver: CODEX_PROVIDER,
status,
title: status === "completed" ? "Context compacted" : "Compacting context",
title:
status === "running"
? "Compacting context"
: status === "completed"
? "Context compacted"
: status === "failed"
? "Context compaction failed"
: "Context compaction stopped",
startedAt,
completedAt: status === "completed" ? now : null,
completedAt: status === "running" ? null : now,
updatedAt: now,
},
});
Expand Down Expand Up @@ -4770,7 +4785,12 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
}

if (payload.item.type === "contextCompaction") {
yield* emitCompactionItem(context, payload.item.id, "completed");
yield* emitCompactionItem(
context,
payload.item.id,
"completed",
DateTime.makeUnsafe(payload.completedAtMs),
);
return;
}

Expand Down Expand Up @@ -5784,6 +5804,15 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
for (const [key, part] of reasoningParts) {
if (part.turnId === input.nativeTurnId) reasoningParts.delete(key);
}
// Failed compaction can end the turn without item/completed.
for (const nativeItemId of input.context.runningCompactionItemIds) {
yield* emitCompactionItem(
input.context,
nativeItemId,
providerTurnStatusToTerminal(input.status),
input.completedAt,
);
}
const nativeThreadId = input.context.providerThread.nativeThreadRef?.nativeId;
const activation =
nativeThreadId == null ? undefined : goalActivations.get(nativeThreadId);
Expand Down Expand Up @@ -6037,6 +6066,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
subagent: null,
startedAt: now,
itemPositions: new Map(),
runningCompactionItemIds: new Set(),
};
const providerTurn = {
id: context.providerTurnId,
Expand Down
21 changes: 21 additions & 0 deletions packages/client-runtime/src/work-log/presentation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { T3_MCP_TOOL_NAMES } from "@t3tools/shared/t3McpToolPresentation";

import {
commandDetailRepeatsCommand,
contextCompactionLabel,
extractCommandOutputText,
liveThoughtLine,
resolveViewedImageAsset,
Expand Down Expand Up @@ -47,6 +48,26 @@ function commandItem(
};
}

describe("contextCompactionLabel", () => {
it.each([
["pending", "Compacting context"],
["running", "Compacting context"],
["waiting", "Compacting context"],
["failed", "Context compaction failed"],
["cancelled", "Context compaction stopped"],
["interrupted", "Context compaction stopped"],
["completed", "Context compacted 20K → 5K tokens"],
] as const)("shows the outcome for %s even when token counts are present", (status, label) => {
expect(
contextCompactionLabel({ status, beforeTokenCount: 20_000, afterTokenCount: 5_000 }),
).toBe(label);
});

it("shows successful compaction without token counts", () => {
expect(contextCompactionLabel({ status: "completed" })).toBe("Context compacted");
});
});

describe("workEntryIndicatesToolFailure", () => {
const base = {
id: "w1",
Expand Down
8 changes: 7 additions & 1 deletion packages/client-runtime/src/work-log/presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,13 @@ export function contextCompactionLabel(
"status" | "beforeTokenCount" | "afterTokenCount"
>,
): string {
if (item.status === "running") return "Compacting context";
if (item.status === "failed") return "Context compaction failed";
if (item.status === "cancelled" || item.status === "interrupted") {
return "Context compaction stopped";
}
if (item.status === "pending" || item.status === "running" || item.status === "waiting") {
return "Compacting context";
}
if (item.beforeTokenCount !== undefined && item.afterTokenCount !== undefined) {
return `Context compacted ${formatTokens(item.beforeTokenCount)} → ${formatTokens(item.afterTokenCount)} tokens`;
}
Expand Down
Loading