Skip to content
Merged
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
42 changes: 42 additions & 0 deletions apps/server/src/orchestration/ActivityPayloadProjection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,45 @@ describe("projectActivityPayload agent-field survival", () => {
expect(projected.payload).toEqual(source.payload);
});
});

describe("projectActivityPayload legacy retraction compatibility", () => {
function retractionFailure(detail: string): OrchestrationThreadActivity {
return {
id: "retraction-failure-1",
tone: "error",
kind: "turn.retract.failed",
summary: "Message retract failed",
payload: {
requestId: "request-1",
stage: "provider-rollback",
retryable: false,
detail,
},
turnId: "turn-1",
createdAt: "2026-08-12T18:49:47.397Z",
} as unknown as OrchestrationThreadActivity;
}

it("marks replayed unavailable-boundary failures silent", () => {
const projected = projectActivityPayload(
retractionFailure(
"Provider adapter validation failed (claudeAgent) in rollbackThreadTo: Provider history has 3 turns, below retained boundary 11.",
),
);

expect(projected.payload).toMatchObject({
requestId: "request-1",
detail:
"Provider adapter validation failed (claudeAgent) in rollbackThreadTo: Provider history has 3 turns, below retained boundary 11.",
silent: true,
});
});

it("keeps unrelated retraction failures visible", () => {
const source = retractionFailure(
"Provider adapter validation failed (claudeAgent) in rollbackThreadTo: retainedTurnCount must be an integer >= 0.",
);

expect(projectActivityPayload(source)).toBe(source);
});
});
38 changes: 34 additions & 4 deletions apps/server/src/orchestration/ActivityPayloadProjection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,35 @@ function asTrimmedString(value: unknown): string | null {
return trimmed.length > 0 ? trimmed : null;
}

const UNAVAILABLE_RETAINED_BOUNDARY_DETAIL =
/^Provider adapter validation failed \([^)]+\) in rollbackThreadTo: Provider history has \d+ turns, below retained boundary \d+\.$/;

function normalizeLegacyRetractionFailure(
activity: OrchestrationThreadActivity,
): OrchestrationThreadActivity {
const payload = asRecord(activity.payload);
if (
activity.kind !== "turn.retract.failed" ||
!payload ||
typeof payload.detail !== "string" ||
!UNAVAILABLE_RETAINED_BOUNDARY_DETAIL.test(payload.detail)
) {
return activity;
}

// Boundary failures emitted before stale Esc retractions became silent are
// immutable event history. Normalize them at the wire boundary so every
// projection rebuild keeps the compatibility behavior without rewriting
// the event store or the persisted activity payload.
return {
...activity,
payload: {
...payload,
silent: true,
},
};
}

export const MAX_PROJECTED_TOOL_RESULT_CHARS = 50_000;

const PROJECTED_TOOL_RESULT_TRUNCATION_MARKER = "…[truncated]";
Expand Down Expand Up @@ -326,15 +355,16 @@ function projectMcpToolCallData(data: Record<string, unknown>): Record<string, u
export function projectActivityPayload(
activity: OrchestrationThreadActivity,
): OrchestrationThreadActivity {
const payload = asRecord(activity.payload);
const normalizedActivity = normalizeLegacyRetractionFailure(activity);
const payload = asRecord(normalizedActivity.payload);
const data = asRecord(payload?.data);
if (!payload || !data) {
return activity;
return normalizedActivity;
}

if (payload.itemType === "mcp_tool_call") {
return {
...activity,
...normalizedActivity,
payload: {
...payload,
data: projectMcpToolCallData(data),
Expand Down Expand Up @@ -370,7 +400,7 @@ export function projectActivityPayload(
}

return {
...activity,
...normalizedActivity,
payload: {
...payload,
data: projectedData,
Expand Down
Loading