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
Original file line number Diff line number Diff line change
Expand Up @@ -574,4 +574,46 @@ describe("ProviderActivityProjection", () => {
}),
]);
});

it("projects structured approval-review outcomes with their real tone", () => {
const [activity] = projectRuntimeEventToActivities({
type: "task.completed",
eventId: EventId.make("evt-approval-review-denied"),
provider: ProviderDriverKind.make("codex"),
threadId: ThreadId.make("thread-1"),
turnId: TurnId.make("turn-1"),
createdAt: "2026-08-28T12:00:00.000Z",
payload: {
taskId: RuntimeTaskId.make("review-1"),
status: "failed",
taskType: "approval-review",
summary: "Auto-review denied command",
approvalReview: {
status: "denied",
rationale: "The command exceeded the authorized scope.",
riskLevel: "high",
userAuthorization: "low",
},
},
} satisfies ProviderRuntimeEvent);

expect(activity).toMatchObject({
kind: "task.completed",
tone: "warning",
summary: "Auto-review denied command",
payload: {
taskId: "review-1",
status: "failed",
taskType: "approval-review",
summary: "Auto-review denied command",
detail: "The command exceeded the authorized scope.",
approvalReview: {
status: "denied",
rationale: "The command exceeded the authorized scope.",
riskLevel: "high",
userAuthorization: "low",
},
},
});
});
});
28 changes: 23 additions & 5 deletions apps/server/src/orchestration/Layers/ProviderActivityProjection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -786,22 +786,39 @@ export function projectRuntimeEventToActivities(
}),
];

case "task.completed":
case "task.completed": {
const approvalReview =
event.payload.taskType === "approval-review" ? event.payload.approvalReview : undefined;
return [
baseActivity(event, {
id: event.eventId,
tone: event.payload.status === "failed" ? "error" : "info",
tone:
approvalReview && approvalReview.status !== "approved"
? "warning"
: event.payload.status === "failed"
? "error"
: "info",
kind: "task.completed",
summary:
event.payload.status === "failed"
summary: approvalReview
? (event.payload.summary ?? "Approval review completed")
: event.payload.status === "failed"
? "Task failed"
: event.payload.status === "stopped"
? "Task stopped"
: "Task completed",
payload: {
taskId: event.payload.taskId,
status: event.payload.status,
...(event.payload.summary ? { detail: truncateDetail(event.payload.summary) } : {}),
...(event.payload.taskType ? { taskType: event.payload.taskType } : {}),
...(approvalReview ? { approvalReview } : {}),
...(approvalReview && event.payload.summary
? { summary: truncateDetail(event.payload.summary) }
: {}),
...(approvalReview?.rationale
? { detail: truncateDetail(approvalReview.rationale) }
: event.payload.summary
? { detail: truncateDetail(event.payload.summary) }
: {}),
...(event.payload.usage !== undefined ? { usage: event.payload.usage } : {}),
...(event.payload.toolUseId ? { toolUseId: event.payload.toolUseId } : {}),
...(event.payload.ownerAgentToolUseId
Expand All @@ -810,6 +827,7 @@ export function projectRuntimeEventToActivities(
},
}),
];
}

case "hook.started":
return [
Expand Down
94 changes: 94 additions & 0 deletions apps/server/src/provider/Layers/CodexAdapter.mapping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,100 @@ import {
} from "./CodexAdapter.ts";

describe("CodexAdapter item mapping", () => {
it("maps structured automatic approval review outcomes", () => {
const cases = [
{ reviewStatus: "approved", taskStatus: "completed", summary: "Auto-approved command" },
{ reviewStatus: "denied", taskStatus: "failed", summary: "Auto-review denied command" },
{
reviewStatus: "timedOut",
taskStatus: "failed",
summary: "Auto-review timed out for command",
},
{
reviewStatus: "aborted",
taskStatus: "stopped",
summary: "Auto-review stopped for command",
},
] as const;

for (const { reviewStatus, taskStatus, summary } of cases) {
const [runtimeEvent] = mapToRuntimeEvents(
{
id: EventId.make(`evt-review-${reviewStatus}`),
kind: "notification",
provider: ProviderDriverKind.make("codex"),
createdAt: "2026-08-28T12:00:00.000Z",
method: "item/autoApprovalReview/completed",
threadId: ThreadId.make("thread-1"),
payload: {
action: {
command: "Get-ChildItem",
cwd: "C:/repo",
source: "unifiedExec",
type: "command",
},
completedAtMs: 1_788_000_001_000,
decisionSource: "agent",
review: {
status: reviewStatus,
rationale: "The requested check is read-only.",
riskLevel: "low",
userAuthorization: "high",
},
reviewId: `review-${reviewStatus}`,
startedAtMs: 1_788_000_000_000,
targetItemId: "command-1",
threadId: "provider-thread-1",
turnId: "turn-1",
},
},
ThreadId.make("thread-1"),
);

assert.equal(runtimeEvent?.type, "task.completed");
if (runtimeEvent?.type !== "task.completed") {
continue;
}
assert.equal(runtimeEvent.turnId, "turn-1");
assert.equal(runtimeEvent.itemId, "command-1");
assert.deepStrictEqual(runtimeEvent.payload, {
taskId: `review-${reviewStatus}`,
status: taskStatus,
taskType: "approval-review",
summary,
approvalReview: {
status: reviewStatus,
rationale: "The requested check is read-only.",
riskLevel: "low",
userAuthorization: "high",
},
});
}
});

it("classifies guardian notices without inferring their outcome from text", () => {
const [runtimeEvent] = mapToRuntimeEvents(
{
id: EventId.make("evt-guardian-warning"),
kind: "notification",
provider: ProviderDriverKind.make("codex"),
createdAt: "2026-08-28T12:00:00.000Z",
method: "guardianWarning",
threadId: ThreadId.make("thread-1"),
payload: {
message: "Automatic approval review approved: safe read-only check.",
threadId: "provider-thread-1",
},
},
ThreadId.make("thread-1"),
);

assert.equal(runtimeEvent?.type, "runtime.warning");
if (runtimeEvent?.type === "runtime.warning") {
assert.equal(runtimeEvent.payload.warningKind, "guardian");
}
});

it("maps native subagent activity into the canonical collab-agent shape", () => {
const [runtimeEvent, metadataEvent] = mapToRuntimeEvents(
{
Expand Down
52 changes: 44 additions & 8 deletions apps/server/src/provider/Layers/CodexAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,37 @@ function summarizeReviewAction(action: unknown): string | undefined {
return firstStringField(action, ["type", "kind", "name", "command", "tool"]);
}

type ApprovalReviewStatus =
EffectCodexSchema.V2ItemGuardianApprovalReviewCompletedNotification["review"]["status"];

function approvalReviewTaskStatus(
status: ApprovalReviewStatus,
): "completed" | "failed" | "stopped" {
if (status === "approved") {
return "completed";
}
if (status === "aborted") {
return "stopped";
}
return "failed";
}

function approvalReviewSummary(status: ApprovalReviewStatus, action: string | undefined): string {
const target = action ? ` ${action}` : " request";
switch (status) {
case "approved":
return `Auto-approved${target}`;
case "denied":
return `Auto-review denied${target}`;
case "timedOut":
return `Auto-review timed out for${target}`;
case "aborted":
return `Auto-review stopped for${target}`;
case "inProgress":
return `Auto-review ended without a decision for${target}`;
}
}

function summarizePatchChanges(
changes: ReadonlyArray<{ readonly kind: unknown; readonly path: string }>,
): string {
Expand Down Expand Up @@ -1861,10 +1892,7 @@ export function mapToRuntimeEvents(
return [];
}
const action = summarizeReviewAction(payload.action);
const decision =
typeof payload.decisionSource === "string"
? payload.decisionSource
: firstStringField(payload.decisionSource, ["type", "kind", "decision"]);
const rationale = trimText(payload.review.rationale ?? undefined);
return [
{
...runtimeEventBase(event, canonicalThreadId),
Expand All @@ -1873,10 +1901,17 @@ export function mapToRuntimeEvents(
type: "task.completed",
payload: {
taskId: RuntimeTaskId.make(payload.reviewId),
status: "completed",
summary: action
? `Approval review completed for ${action}${decision ? `: ${decision}` : ""}`
: "Approval review completed",
status: approvalReviewTaskStatus(payload.review.status),
taskType: "approval-review",
summary: approvalReviewSummary(payload.review.status, action),
approvalReview: {
status: payload.review.status,
...(rationale ? { rationale } : {}),
...(payload.review.riskLevel ? { riskLevel: payload.review.riskLevel } : {}),
...(payload.review.userAuthorization
? { userAuthorization: payload.review.userAuthorization }
: {}),
},
},
},
];
Expand Down Expand Up @@ -2608,6 +2643,7 @@ export function mapToRuntimeEvents(
...runtimeEventBase(event, canonicalThreadId),
payload: {
message,
warningKind: "guardian",
...(event.payload !== undefined ? { detail: event.payload } : {}),
},
},
Expand Down
96 changes: 96 additions & 0 deletions apps/web/src/session-logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3299,6 +3299,102 @@ describe("deriveWorkLogEntries", () => {
});
});

it("replaces approved guardian warnings while preserving denied warnings", () => {
const pairedWarning = makeActivity({
id: "guardian-warning",
createdAt: "2026-08-28T12:00:00.000Z",
sequence: 1,
kind: "runtime.warning",
summary: "Runtime warning",
tone: "warning",
payload: {
message: "Automatic approval review completed.",
warningKind: "guardian",
},
});

const approved = deriveWorkLogEntries([
pairedWarning,
makeActivity({
id: "approval-review-approved",
createdAt: "2026-08-28T12:00:00.001Z",
sequence: 2,
kind: "task.completed",
summary: "Auto-approved command",
tone: "info",
payload: {
taskId: "review-approved",
status: "completed",
taskType: "approval-review",
summary: "Auto-approved command",
detail: "The requested check is read-only.",
approvalReview: { status: "approved" },
},
}),
]);

expect(approved).toHaveLength(1);
expect(approved[0]).toMatchObject({
id: "approval-review-approved",
label: "Auto-approved command",
detail: "The requested check is read-only.",
tone: "info",
});

const denied = deriveWorkLogEntries([
pairedWarning,
makeActivity({
id: "approval-review-denied",
createdAt: "2026-08-28T12:00:00.001Z",
sequence: 2,
kind: "task.completed",
summary: "Auto-review denied command",
tone: "warning",
payload: {
taskId: "review-denied",
status: "failed",
taskType: "approval-review",
summary: "Auto-review denied command",
detail: "The command exceeded the authorized scope.",
approvalReview: { status: "denied" },
},
}),
]);

expect(denied).toHaveLength(2);
expect(denied[0]).toMatchObject({
id: "guardian-warning",
tone: "warning",
});
expect(denied[1]).toMatchObject({
id: "approval-review-denied",
label: "Auto-review denied command",
detail: "The command exceeded the authorized scope.",
tone: "warning",
});
});

it("keeps standalone guardian warnings visible", () => {
const [entry] = deriveWorkLogEntries([
makeActivity({
id: "guardian-circuit-breaker",
kind: "runtime.warning",
summary: "Runtime warning",
tone: "warning",
payload: {
message: "Automatic approval review rejected too many requests; interrupting the turn.",
warningKind: "guardian",
},
}),
]);

expect(entry).toMatchObject({
id: "guardian-circuit-breaker",
tone: "warning",
detail: "Automatic approval review rejected too many requests; interrupting the turn.",
});
});

it("marks failed command executions distinctly", () => {
const [entry] = deriveWorkLogEntries([
makeActivity({
Expand Down
Loading
Loading