@wix/pathgrade 1.0.17 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/agents/claude/sdk-message-projector.js +12 -4
  2. package/dist/agents/claude/tool-results.d.ts +1 -2
  3. package/dist/agents/claude/tool-results.js +9 -40
  4. package/dist/agents/claude.js +6 -4
  5. package/dist/agents/codex-app-server/agent.d.ts +3 -0
  6. package/dist/agents/codex-app-server/agent.js +109 -162
  7. package/dist/agents/codex-app-server/item-lifecycle.d.ts +30 -0
  8. package/dist/agents/codex-app-server/item-lifecycle.js +95 -0
  9. package/dist/agents/codex-app-server/item-projection.d.ts +62 -0
  10. package/dist/agents/codex-app-server/item-projection.js +135 -0
  11. package/dist/agents/opencode/host-safety.d.ts +3 -0
  12. package/dist/agents/opencode/host-safety.js +30 -0
  13. package/dist/agents/opencode.d.ts +2 -4
  14. package/dist/agents/opencode.js +47 -38
  15. package/dist/providers/credentials.d.ts +2 -0
  16. package/dist/providers/credentials.js +1 -0
  17. package/dist/providers/scripted-mcp-mock-host.js +6 -3
  18. package/dist/providers/workspace.d.ts +1 -0
  19. package/dist/providers/workspace.js +5 -0
  20. package/dist/sdk/agent-result-log.js +4 -2
  21. package/dist/sdk/agent.js +6 -0
  22. package/dist/sdk/managed-session.d.ts +2 -0
  23. package/dist/sdk/managed-session.js +22 -5
  24. package/dist/sdk/mcp-safety.js +2 -18
  25. package/dist/sdk/snapshots.d.ts +1 -0
  26. package/dist/sdk/snapshots.js +3 -2
  27. package/dist/sdk/tool-event-log.js +5 -2
  28. package/dist/sdk/tool-event-secrets.d.ts +4 -0
  29. package/dist/sdk/tool-event-secrets.js +14 -0
  30. package/dist/sdk/turn-result-secrets.d.ts +5 -0
  31. package/dist/sdk/turn-result-secrets.js +12 -0
  32. package/dist/tool-event-results.d.ts +10 -0
  33. package/dist/tool-event-results.js +171 -0
  34. package/dist/types.d.ts +2 -0
  35. package/package.json +2 -2
@@ -22,6 +22,9 @@ const SDK_ERROR_SUBTYPES = [
22
22
  'error_max_structured_output_retries',
23
23
  ];
24
24
  import { TOOL_NAME_MAP, buildSummary, enrichSkillEvents } from '../../tool-events.js';
25
+ import { sanitizePersistenceValue } from '../../tool-event-results.js';
26
+ import { attachTurnResultSensitiveValues } from '../../sdk/turn-result-secrets.js';
27
+ import { attachToolEventSensitiveValues } from '../../sdk/tool-event-secrets.js';
25
28
  import { attachOriginalMcpInput } from '../../sdk/mcp-event-input.js';
26
29
  import { parseClaudeSdkMcpToolName } from './mcp-tool-name.js';
27
30
  import { applyObservedToolResult, extractObservedToolResults, } from './tool-results.js';
@@ -132,13 +135,15 @@ export function projectSdkMessages(input) {
132
135
  const trimmedAssistant = assistantText.trim();
133
136
  const trimmedResult = resultText.trim();
134
137
  const visible = isError ? '' : (trimmedAssistant || trimmedResult);
135
- const rawOutput = resultText || assistantText;
138
+ const rawOutput = sanitizePersistenceValue(resultText || assistantText, input.sensitiveValues);
136
139
  const enriched = enrichSkillEvents([
137
140
  ...toolEvents,
138
141
  ...(input.deniedMcpEvents?.all() ?? []),
139
142
  ]);
140
- const finalToolEvents = prependSlashCommandSkillEvent(enriched, input.firstMessage, initSkills);
141
- const traceOutput = input.messages.map((m) => JSON.stringify(m)).join('\n');
143
+ const finalToolEvents = prependSlashCommandSkillEvent(enriched, input.firstMessage, initSkills).map((event) => attachToolEventSensitiveValues(event, input.sensitiveValues ?? []));
144
+ const traceOutput = sanitizePersistenceValue(input.messages, input.sensitiveValues)
145
+ .map((message) => JSON.stringify(message))
146
+ .join('\n');
142
147
  const result = {
143
148
  rawOutput,
144
149
  assistantMessage: visible,
@@ -155,7 +160,10 @@ export function projectSdkMessages(input) {
155
160
  ...(costUsd !== undefined ? { costUsd } : {}),
156
161
  ...(errorSubtype !== undefined ? { errorSubtype } : {}),
157
162
  };
158
- return { result, sessionId };
163
+ return {
164
+ result: attachTurnResultSensitiveValues(result, input.sensitiveValues ?? []),
165
+ sessionId,
166
+ };
159
167
  }
160
168
  /**
161
169
  * If the first user message of the turn is `/<name>` and `<name>` is one of
@@ -1,6 +1,6 @@
1
1
  import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk';
2
2
  import type { ToolEvent } from '../../tool-events.js';
3
- export declare const TOOL_RESULT_MAX_CHARS: number;
3
+ export { collectSensitiveEnvValues, TOOL_RESULT_MAX_CHARS } from '../../tool-event-results.js';
4
4
  export interface ClaudeSdkMessageTiming {
5
5
  receivedAt: string;
6
6
  receivedMonotonicMs: number;
@@ -16,4 +16,3 @@ export interface ObservedToolResult {
16
16
  export declare function hasToolLifecycleBoundary(message: SDKMessage): boolean;
17
17
  export declare function extractObservedToolResults(message: SDKMessage, timing?: ClaudeSdkMessageTiming): ObservedToolResult[];
18
18
  export declare function applyObservedToolResult(event: ToolEvent, observed: ObservedToolResult, startedMonotonicMs?: number, sensitiveValues?: readonly string[]): void;
19
- export declare function collectSensitiveEnvValues(env: Readonly<Record<string, string>>): string[];
@@ -1,5 +1,5 @@
1
- export const TOOL_RESULT_MAX_CHARS = 64 * 1024;
2
- const SECRET_ENV_KEY_PATTERN = /(^|[_-])(api[_-]?key|token|secret|password|authorization|auth|bearer)([_-]|$)/i;
1
+ import { sanitizeToolEventResult, } from '../../tool-event-results.js';
2
+ export { collectSensitiveEnvValues, TOOL_RESULT_MAX_CHARS } from '../../tool-event-results.js';
3
3
  export function hasToolLifecycleBoundary(message) {
4
4
  if (message.type !== 'assistant' && message.type !== 'user')
5
5
  return false;
@@ -52,50 +52,19 @@ export function applyObservedToolResult(event, observed, startedMonotonicMs, sen
52
52
  event.arguments.status = event.status;
53
53
  }
54
54
  }
55
- export function collectSensitiveEnvValues(env) {
56
- return [...new Set(Object.entries(env)
57
- .filter(([key, value]) => SECRET_ENV_KEY_PATTERN.test(key) && value.length > 0)
58
- .map(([, value]) => value))]
59
- .sort((a, b) => b.length - a.length);
60
- }
61
55
  function buildBoundedToolResult(observed, sensitiveValues) {
62
56
  const raw = observed.structuredResult;
63
- const result = {};
64
- let truncated = false;
65
- const addBounded = (key, value) => {
66
- if (typeof value !== 'string')
67
- return;
68
- const bounded = boundText(redactSensitiveValues(value, sensitiveValues));
69
- result[key] = bounded.value;
70
- truncated ||= bounded.truncated;
57
+ const result = {
58
+ ...(typeof raw?.stdout === 'string' ? { stdout: raw.stdout } : {}),
59
+ ...(typeof raw?.stderr === 'string' ? { stderr: raw.stderr } : {}),
60
+ ...(typeof observed.content === 'string' && observed.content !== raw?.stdout
61
+ ? { content: observed.content }
62
+ : {}),
71
63
  };
72
- addBounded('stdout', raw?.stdout);
73
- addBounded('stderr', raw?.stderr);
74
- if (observed.content !== raw?.stdout)
75
- addBounded('content', observed.content);
76
64
  const exitCode = raw?.exitCode ?? raw?.exit_code;
77
65
  if (typeof exitCode === 'number' && Number.isFinite(exitCode))
78
66
  result.exitCode = exitCode;
79
- if (truncated)
80
- result.truncated = true;
81
- return result;
82
- }
83
- function redactSensitiveValues(value, sensitiveValues) {
84
- let redacted = value;
85
- for (const secret of sensitiveValues) {
86
- if (secret.length > 0)
87
- redacted = redacted.split(secret).join('<redacted>');
88
- }
89
- return redacted;
90
- }
91
- function boundText(value) {
92
- if (value.length <= TOOL_RESULT_MAX_CHARS)
93
- return { value, truncated: false };
94
- const marker = '\n[truncated by PathGrade]';
95
- return {
96
- value: `${value.slice(0, TOOL_RESULT_MAX_CHARS - marker.length)}${marker}`,
97
- truncated: true,
98
- };
67
+ return sanitizeToolEventResult(result, sensitiveValues);
99
68
  }
100
69
  function extractTextContent(content) {
101
70
  if (typeof content === 'string')
@@ -28,6 +28,8 @@ import { createSandboxedClaudeSpawn } from '../providers/sandboxed-claude-spawn.
28
28
  import { assertClaudeLiveMcpSafetyPreflight, assertStdioMcpServersStartForClaudeSdk, mountMcpForClaudeSdk, } from '../providers/mcp-runtime-mounting.js';
29
29
  import { buildClaudeSdkOptions, resolveClaudeCodeExecutable, } from './claude/sdk-options.js';
30
30
  import { projectSdkMessages } from './claude/sdk-message-projector.js';
31
+ import { attachToolEventSensitiveValues } from '../sdk/tool-event-secrets.js';
32
+ import { cloneTurnResultWithSensitiveValues } from '../sdk/turn-result-secrets.js';
31
33
  import { collectSensitiveEnvValues, hasToolLifecycleBoundary, } from './claude/tool-results.js';
32
34
  import { createAskUserAnswerStore } from './claude/ask-user-answer-store.js';
33
35
  import { createClaudeToolPermissionBridge } from './claude/tool-permission-bridge.js';
@@ -195,7 +197,8 @@ export class ClaudeAgent extends BaseAgent {
195
197
  deniedMcpEvents,
196
198
  sensitiveValues,
197
199
  });
198
- projected.result.toolEvents = [...scriptedEvents, ...projected.result.toolEvents];
200
+ projected.result.toolEvents = [...scriptedEvents, ...projected.result.toolEvents]
201
+ .map((event) => attachToolEventSensitiveValues(event, sensitiveValues));
199
202
  // Capture the SDK-reported session id BEFORE checking for a bus
200
203
  // rejection so the next turn's `Options.resume` points at this
201
204
  // turn's session even when the turn ended in an ask-bus error.
@@ -214,12 +217,11 @@ export class ClaudeAgent extends BaseAgent {
214
217
  const errorMessage = bridgeError instanceof Error
215
218
  ? bridgeError.message
216
219
  : String(bridgeError);
217
- return {
218
- ...projected.result,
220
+ return cloneTurnResultWithSensitiveValues(projected.result, {
219
221
  exitCode: 1,
220
222
  errorSubtype: 'bus_rejection',
221
223
  rawOutput: errorMessage,
222
- };
224
+ });
223
225
  }
224
226
  return projected.result;
225
227
  };
@@ -1,5 +1,6 @@
1
1
  import { AgentCommandRunner, AgentSession, AgentSessionOptions, BaseAgent, EnvironmentHandle } from '../../types.js';
2
2
  import { type AppServerSessionHandle } from './transport.js';
3
+ import { type LifecycleClock } from './item-lifecycle.js';
3
4
  type SandboxMode = 'workspace-write' | 'danger-full-access';
4
5
  export interface PermissionGrantLogEntry {
5
6
  type: 'permissions_granted';
@@ -25,6 +26,8 @@ export interface CodexAppServerAgentDeps {
25
26
  sandboxMode?: SandboxMode;
26
27
  /** Observer for per-grant audit entries (§7 of design decisions). */
27
28
  onPermissionGrant?: (entry: PermissionGrantLogEntry) => void;
29
+ /** Injectable clocks keep wall timestamps and monotonic durations independently testable. */
30
+ clock?: LifecycleClock;
28
31
  }
29
32
  export declare class CodexAppServerAgent extends BaseAgent {
30
33
  private deps;
@@ -1,133 +1,21 @@
1
1
  import { BaseAgent, getRuntimeEnv, getWorkspacePath, } from '../../types.js';
2
2
  import { mountMcpForCodexAppServer } from '../../providers/mcp-runtime-mounting.js';
3
3
  import { assertMcpSecretReferencesReady } from '../../providers/mcp-config.js';
4
- import { buildSummary, enrichSkillEvents, extractSkillNameFromPath, inferCodexExecAction, } from '../../tool-events.js';
4
+ import { enrichSkillEvents, } from '../../tool-events.js';
5
+ import { collectSensitiveEnvValues, sanitizePersistenceValue, } from '../../tool-event-results.js';
5
6
  import { requireAskBusForLiveBatches } from '../../sdk/ask-bus/bus.js';
7
+ import { attachTurnResultSensitiveValues } from '../../sdk/turn-result-secrets.js';
8
+ import { attachToolEventSensitiveValues } from '../../sdk/tool-event-secrets.js';
6
9
  import { toAskUserToolEvent } from '../../sdk/ask-bus/projection.js';
7
10
  import { decideMcpToolCall } from '../../sdk/mcp-safety.js';
8
- import { attachOriginalMcpInput } from '../../sdk/mcp-event-input.js';
9
11
  import { spawnAppServerTransport, } from './transport.js';
10
12
  import { normalizeUpstreamQuestion, toWireAnswerMap, } from './wire-translators.js';
11
13
  import { extractTurnCompletionFailure } from './turn-completion.js';
12
14
  import { resolveCodexModel } from '../codex-model.js';
13
15
  import { CodexMcpApprovalCorrelator, extractMcpToolApprovalRequest, hasScriptedApprovalPolicy, isMcpToolCallApprovalRequest, recordPolicyDeniedMcpToolCall } from './mcp-approval-correlator.js';
16
+ import { projectItemIntoTurn } from './item-projection.js';
17
+ import { CodexItemLifecycle, } from './item-lifecycle.js';
14
18
  const TURN_COMPLETED_METHOD = 'turn/completed';
15
- function recordFromUnknown(value) {
16
- if (value && typeof value === 'object' && !Array.isArray(value)) {
17
- return value;
18
- }
19
- return {};
20
- }
21
- function extractCommandActionSkillName(action) {
22
- if (typeof action.path === 'string') {
23
- const direct = extractSkillNameFromPath(action.path);
24
- if (direct)
25
- return direct;
26
- const embedded = extractSkillNameFromText(action.path);
27
- if (embedded)
28
- return embedded;
29
- }
30
- return typeof action.command === 'string' ? extractSkillNameFromText(action.command) : undefined;
31
- }
32
- function extractSkillNameFromText(value) {
33
- if (!value)
34
- return undefined;
35
- return value.match(/(?:^|[/\s"'])\.(?:agents|claude)\/skills\/([^/\s"']+)\/SKILL\.md(?:$|[\s"'])/)?.[1];
36
- }
37
- function extractSkillPathFromText(value) {
38
- if (!value)
39
- return undefined;
40
- return value.match(/(?:^|[\s"'])(?<path>(?:\/|\.{1,2}\/)?[^\s"']*(?:\.agents|\.claude)\/skills\/[^/\s"']+\/SKILL\.md)(?:$|[\s"'])/)?.groups?.path;
41
- }
42
- function projectItemIntoTurn(item, turn) {
43
- if (item.type === 'agentMessage') {
44
- const msg = item;
45
- if (msg.text) {
46
- turn.assistantMessageParts.push(msg.text);
47
- }
48
- return;
49
- }
50
- if (item.type === 'commandExecution') {
51
- const cmd = item;
52
- const action = inferCodexExecAction(cmd.command);
53
- const skillPath = extractSkillPathFromText(cmd.command);
54
- const args = {
55
- command: cmd.command,
56
- ...(skillPath ? { path: skillPath } : {}),
57
- };
58
- turn.nonAskToolEvents.push({
59
- action,
60
- provider: 'codex',
61
- providerToolName: 'commandExecution',
62
- turnNumber: turn.turnNumber,
63
- arguments: args,
64
- summary: buildSummary(action, 'commandExecution', args),
65
- confidence: 'high',
66
- rawSnippet: JSON.stringify(cmd),
67
- });
68
- const recordedSkills = new Set();
69
- for (const action of cmd.commandActions ?? []) {
70
- const skillName = extractCommandActionSkillName(action) ?? extractSkillNameFromText(cmd.command);
71
- if (!skillName || recordedSkills.has(skillName))
72
- continue;
73
- recordedSkills.add(skillName);
74
- turn.nonAskToolEvents.push({
75
- action: 'use_skill',
76
- provider: 'codex',
77
- providerToolName: `commandExecution.commandActions.${action.type ?? 'unknown'}`,
78
- turnNumber: turn.turnNumber,
79
- arguments: { path: action.path, name: action.name },
80
- summary: `use_skill ${skillName}`,
81
- confidence: 'high',
82
- rawSnippet: JSON.stringify(action),
83
- skillName,
84
- });
85
- }
86
- return;
87
- }
88
- if (item.type === 'fileChange') {
89
- const fc = item;
90
- for (const change of fc.changes ?? []) {
91
- turn.nonAskToolEvents.push({
92
- action: 'edit_file',
93
- provider: 'codex',
94
- providerToolName: 'fileChange',
95
- turnNumber: turn.turnNumber,
96
- arguments: { file_path: change.path },
97
- summary: `edit_file: ${change.path}`,
98
- confidence: 'high',
99
- rawSnippet: JSON.stringify(change),
100
- });
101
- }
102
- return;
103
- }
104
- if (item.type === 'mcpToolCall') {
105
- const call = item;
106
- if (turn.nonAskToolEvents.some((event) => event.action === 'mcp_tool_call'
107
- && event.toolUseId === call.id && event.mcp?.invocation === 'not_invoked'))
108
- return;
109
- const args = recordFromUnknown(call.arguments);
110
- const providerToolName = `${call.server}.${call.tool}`;
111
- turn.nonAskToolEvents.push(attachOriginalMcpInput({
112
- action: 'mcp_tool_call',
113
- provider: 'codex',
114
- providerToolName,
115
- toolUseId: call.id,
116
- turnNumber: turn.turnNumber,
117
- arguments: {
118
- ...args,
119
- server: call.server,
120
- tool: call.tool,
121
- status: call.status ?? 'unknown',
122
- },
123
- summary: `MCP tool ${providerToolName} ${call.status ?? 'unknown'}`,
124
- confidence: 'high',
125
- rawSnippet: JSON.stringify(call),
126
- ...(call.result !== undefined ? { result: { content: JSON.stringify(call.result) } } : {}),
127
- }, args));
128
- return;
129
- }
130
- }
131
19
  function projectMcpStartupStatusIntoTurn(params, turn) {
132
20
  const name = params.name ?? 'unknown';
133
21
  const status = params.status ?? 'unknown';
@@ -156,6 +44,18 @@ function failTurn(turn, message) {
156
44
  turn.failureMessage = message;
157
45
  turn.signalFailure?.(message);
158
46
  }
47
+ function extractTurnCompletionIdentity(params) {
48
+ if (!params || typeof params !== 'object' || Array.isArray(params))
49
+ return {};
50
+ const record = params;
51
+ const turnId = typeof record.turn?.id === 'string'
52
+ ? record.turn.id
53
+ : typeof record.turnId === 'string' ? record.turnId : undefined;
54
+ return {
55
+ ...(typeof record.threadId === 'string' ? { threadId: record.threadId } : {}),
56
+ ...(turnId ? { turnId } : {}),
57
+ };
58
+ }
159
59
  function isLiveMcpSafetyMode(options) {
160
60
  const runMode = options?.runMode ?? 'mock';
161
61
  return runMode === 'live-readonly' || runMode === 'live-sandbox' || runMode === 'live';
@@ -170,6 +70,7 @@ export class CodexAppServerAgent extends BaseAgent {
170
70
  const askBus = requireAskBusForLiveBatches(options, 'CodexAppServerAgent');
171
71
  const workspacePath = getWorkspacePath(runtime);
172
72
  const runtimeEnv = getRuntimeEnv(runtime);
73
+ const sensitiveValues = collectSensitiveEnvValues(runtimeEnv);
173
74
  const model = resolveCodexModel(options?.model);
174
75
  const sandboxMode = this.deps.sandboxMode ?? 'workspace-write';
175
76
  let handle = null;
@@ -207,7 +108,8 @@ export class CodexAppServerAgent extends BaseAgent {
207
108
  });
208
109
  transport.onNotification((n) => {
209
110
  if (process.env.PATHGRADE_CODEX_DEBUG) {
210
- console.error(`[codex app-server] notification method=${n.method} params=${JSON.stringify(n.params).slice(0, 300)}`);
111
+ const debugParams = sanitizePersistenceValue(n.params, sensitiveValues);
112
+ console.error(`[codex app-server] notification method=${n.method} params=${JSON.stringify(debugParams).slice(0, 300)}`);
211
113
  }
212
114
  if (n.method === 'mcpServer/startupStatus/updated') {
213
115
  const turn = activeTurn;
@@ -221,25 +123,14 @@ export class CodexAppServerAgent extends BaseAgent {
221
123
  return;
222
124
  const params = n.params;
223
125
  if (n.method === 'item/started') {
224
- try {
225
- correlator?.started(n.params);
226
- }
227
- catch (error) {
228
- failTurn(turn, error instanceof Error ? error.message : String(error));
229
- }
126
+ if (params)
127
+ turn.itemLifecycle?.receiveStarted(params);
230
128
  return;
231
129
  }
232
130
  if (n.method !== 'item/completed')
233
131
  return;
234
- if (!params?.item)
235
- return;
236
- try {
237
- if (correlator?.completed(params) !== false)
238
- projectItemIntoTurn(params.item, turn);
239
- }
240
- catch (error) {
241
- failTurn(turn, error instanceof Error ? error.message : String(error));
242
- }
132
+ if (params)
133
+ turn.itemLifecycle?.receiveCompleted(params);
243
134
  });
244
135
  const initialized = await transport.sendRequest('initialize', {
245
136
  clientInfo: { name: 'pathgrade', version: '0.5.0', title: null },
@@ -262,6 +153,7 @@ export class CodexAppServerAgent extends BaseAgent {
262
153
  nonAskToolEvents: [],
263
154
  assistantMessageParts: [],
264
155
  turnFailed: false,
156
+ pendingTurnCompletions: [],
265
157
  };
266
158
  activeTurn = turn;
267
159
  correlator?.beginTurn();
@@ -286,6 +178,31 @@ export class CodexAppServerAgent extends BaseAgent {
286
178
  }
287
179
  threadId = resp.thread.id;
288
180
  }
181
+ const authoritativeThreadId = threadId;
182
+ turn.authoritativeThreadId = authoritativeThreadId;
183
+ turn.itemLifecycle = new CodexItemLifecycle({
184
+ threadId: authoritativeThreadId,
185
+ clock: this.deps.clock,
186
+ onFailure: (message) => failTurn(turn, message),
187
+ onStarted: (params) => {
188
+ try {
189
+ correlator?.started(params);
190
+ }
191
+ catch (error) {
192
+ failTurn(turn, error instanceof Error ? error.message : String(error));
193
+ }
194
+ },
195
+ onCompleted: (item, timing, params) => {
196
+ try {
197
+ if (correlator?.completed(params) !== false) {
198
+ projectItemIntoTurn(item, turn, sensitiveValues, timing);
199
+ }
200
+ }
201
+ catch (error) {
202
+ failTurn(turn, error instanceof Error ? error.message : String(error));
203
+ }
204
+ },
205
+ });
289
206
  // Wait for TurnCompleted OR subprocess crash OR dispatcher failure.
290
207
  const turnCompletion = new Promise((resolve, reject) => {
291
208
  let settled = false;
@@ -293,15 +210,30 @@ export class CodexAppServerAgent extends BaseAgent {
293
210
  if (settled)
294
211
  return;
295
212
  if (n.method === TURN_COMPLETED_METHOD) {
296
- settled = true;
297
- off();
298
- closeOff();
299
- const failure = extractTurnCompletionFailure(n.params);
300
- if (failure)
301
- Object.assign(turn, { turnFailed: true, failureMessage: failure });
302
- resolve();
213
+ turn.acceptTurnCompletion?.(n.params);
303
214
  }
304
215
  });
216
+ const acceptTurnCompletion = (params) => {
217
+ if (settled)
218
+ return;
219
+ const identity = extractTurnCompletionIdentity(params);
220
+ if (identity.threadId && identity.threadId !== turn.authoritativeThreadId)
221
+ return;
222
+ if (identity.turnId && !turn.authoritativeTurnId) {
223
+ turn.pendingTurnCompletions.push(params);
224
+ return;
225
+ }
226
+ if (identity.turnId && identity.turnId !== turn.authoritativeTurnId)
227
+ return;
228
+ settled = true;
229
+ off();
230
+ closeOff();
231
+ const failure = extractTurnCompletionFailure(params);
232
+ if (failure)
233
+ Object.assign(turn, { turnFailed: true, failureMessage: failure });
234
+ resolve();
235
+ };
236
+ turn.acceptTurnCompletion = acceptTurnCompletion;
305
237
  const closeOff = t.onClose((info) => {
306
238
  if (settled)
307
239
  return;
@@ -339,9 +271,25 @@ export class CodexAppServerAgent extends BaseAgent {
339
271
  }
340
272
  }
341
273
  });
342
- const startTurnResp = t.sendRequest('turn/start', {
274
+ const authoritativeTurn = t.sendRequest('turn/start', {
343
275
  threadId,
344
276
  input: [{ type: 'text', text: message, text_elements: [] }],
277
+ })
278
+ .then((started) => {
279
+ const authoritativeTurnId = started.turn?.id ?? started.turnId;
280
+ if (!authoritativeTurnId) {
281
+ scriptedHost?.failProtocol('turn/start did not return an authoritative turn id');
282
+ turn.signalFailure?.('turn/start did not return an authoritative turn id');
283
+ return;
284
+ }
285
+ turn.itemLifecycle?.setAuthoritativeTurn(authoritativeTurnId);
286
+ turn.authoritativeTurnId = authoritativeTurnId;
287
+ if (correlator?.setAuthoritativeTurn(authoritativeThreadId, authoritativeTurnId) === false) {
288
+ turn.signalFailure?.('MCP lifecycle did not match the authoritative turn');
289
+ }
290
+ for (const pending of turn.pendingTurnCompletions.splice(0)) {
291
+ turn.acceptTurnCompletion?.(pending);
292
+ }
345
293
  })
346
294
  .catch((err) => {
347
295
  // A JSON-RPC error on turn/start means the server will
@@ -355,18 +303,6 @@ export class CodexAppServerAgent extends BaseAgent {
355
303
  : 'turn/start failed';
356
304
  turn.signalFailure?.(msg);
357
305
  });
358
- if (scriptedHost) {
359
- const started = await startTurnResp;
360
- if (!started?.turn?.id || !threadId) {
361
- scriptedHost.failProtocol('turn/start did not return an authoritative turn id');
362
- turn.signalFailure?.('turn/start did not return an authoritative turn id');
363
- }
364
- else {
365
- if (correlator?.setAuthoritativeTurn(threadId, started.turn.id) === false) {
366
- turn.signalFailure?.('MCP lifecycle did not match the authoritative turn');
367
- }
368
- }
369
- }
370
306
  try {
371
307
  await turnCompletion;
372
308
  }
@@ -379,16 +315,25 @@ export class CodexAppServerAgent extends BaseAgent {
379
315
  message: info.message ?? 'app-server exited',
380
316
  pid: info.pid,
381
317
  signal: info.signal,
318
+ sensitiveValues,
382
319
  });
383
320
  }
384
- if (!scriptedHost)
385
- void startTurnResp;
386
321
  if (turn.turnFailed) {
387
322
  return assembleTurnResult({
388
323
  askBus,
389
324
  activeTurn: turn,
390
325
  exitCode: 1,
391
326
  message: turn.failureMessage ?? 'turn failed',
327
+ sensitiveValues,
328
+ });
329
+ }
330
+ // Ensure pre-response lifecycle notifications have been correlated
331
+ // and projected before exposing the successful turn result.
332
+ await authoritativeTurn;
333
+ if (turn.turnFailed) {
334
+ return assembleTurnResult({
335
+ askBus, activeTurn: turn, exitCode: 1,
336
+ message: turn.failureMessage ?? 'turn failed', sensitiveValues,
392
337
  });
393
338
  }
394
339
  return assembleTurnResult({
@@ -396,6 +341,7 @@ export class CodexAppServerAgent extends BaseAgent {
396
341
  activeTurn: turn,
397
342
  exitCode: 0,
398
343
  message: turn.assistantMessageParts.join('\n\n'),
344
+ sensitiveValues,
399
345
  });
400
346
  }
401
347
  finally {
@@ -577,14 +523,15 @@ function buildThreadStartParams(opts) {
577
523
  };
578
524
  }
579
525
  function assembleTurnResult(args) {
580
- const { askBus, activeTurn, exitCode, message, pid, signal } = args;
526
+ const { askBus, activeTurn, exitCode, message, pid, signal, sensitiveValues = [] } = args;
581
527
  const askBatchIds = new Set(activeTurn.askBatchIds);
582
528
  const askEvents = askBus
583
529
  .snapshot()
584
530
  .filter((s) => askBatchIds.has(s.batchId))
585
531
  .map((s) => toAskUserToolEvent(s));
586
- const toolEvents = enrichSkillEvents([...askEvents, ...activeTurn.nonAskToolEvents]);
587
- const rawOutput = exitCode === 0
532
+ const toolEvents = enrichSkillEvents([...askEvents, ...activeTurn.nonAskToolEvents])
533
+ .map((event) => attachToolEventSensitiveValues(event, sensitiveValues));
534
+ const rawOutput = sanitizePersistenceValue(exitCode === 0
588
535
  ? message
589
536
  : [
590
537
  message,
@@ -593,8 +540,8 @@ function assembleTurnResult(args) {
593
540
  `exitCode=${exitCode}`,
594
541
  ]
595
542
  .filter(Boolean)
596
- .join(' ');
597
- return {
543
+ .join(' '), sensitiveValues);
544
+ return attachTurnResultSensitiveValues({
598
545
  rawOutput,
599
546
  assistantMessage: exitCode === 0 ? message : '',
600
547
  visibleAssistantMessage: exitCode === 0 ? message : '',
@@ -611,5 +558,5 @@ function assembleTurnResult(args) {
611
558
  },
612
559
  }
613
560
  : {}),
614
- };
561
+ }, sensitiveValues);
615
562
  }
@@ -0,0 +1,30 @@
1
+ import type { CodexItem, ItemTiming } from './item-projection.js';
2
+ export interface ItemLifecycleParams {
3
+ item?: CodexItem;
4
+ threadId?: string;
5
+ turnId?: string;
6
+ }
7
+ export interface LifecycleClock {
8
+ wallNow(): number;
9
+ monotonicNow(): number;
10
+ }
11
+ export declare class CodexItemLifecycle {
12
+ private readonly options;
13
+ private turnId?;
14
+ private readonly pending;
15
+ private readonly started;
16
+ private readonly completed;
17
+ constructor(options: {
18
+ threadId: string;
19
+ clock?: LifecycleClock;
20
+ onStarted?: (params: ItemLifecycleParams) => void;
21
+ onCompleted: (item: CodexItem, timing: ItemTiming, params: ItemLifecycleParams) => void;
22
+ onFailure: (message: string) => void;
23
+ });
24
+ receiveStarted(params: ItemLifecycleParams): void;
25
+ receiveCompleted(params: ItemLifecycleParams): void;
26
+ setAuthoritativeTurn(turnId: string): boolean;
27
+ private receive;
28
+ private validateEnvelope;
29
+ private process;
30
+ }