@wix/pathgrade 1.0.17 → 1.0.19

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 (50) hide show
  1. package/README.md +7 -4
  2. package/dist/agents/claude/sdk-message-projector.js +12 -4
  3. package/dist/agents/claude/tool-results.d.ts +1 -2
  4. package/dist/agents/claude/tool-results.js +9 -40
  5. package/dist/agents/claude.js +6 -4
  6. package/dist/agents/codex-app-server/agent.d.ts +5 -0
  7. package/dist/agents/codex-app-server/agent.js +145 -179
  8. package/dist/agents/codex-app-server/item-lifecycle.d.ts +30 -0
  9. package/dist/agents/codex-app-server/item-lifecycle.js +95 -0
  10. package/dist/agents/codex-app-server/item-projection.d.ts +62 -0
  11. package/dist/agents/codex-app-server/item-projection.js +135 -0
  12. package/dist/agents/codex-app-server/managed-auth.d.ts +12 -0
  13. package/dist/agents/codex-app-server/managed-auth.js +53 -0
  14. package/dist/agents/codex-app-server/transport.d.ts +2 -0
  15. package/dist/agents/codex-app-server/transport.js +30 -3
  16. package/dist/agents/opencode/host-safety.d.ts +3 -0
  17. package/dist/agents/opencode/host-safety.js +30 -0
  18. package/dist/agents/opencode.d.ts +2 -4
  19. package/dist/agents/opencode.js +47 -38
  20. package/dist/openai-oauth/chatgpt-oauth-llm.d.ts +25 -0
  21. package/dist/openai-oauth/chatgpt-oauth-llm.js +398 -0
  22. package/dist/openai-oauth/codex-auth-broker.d.ts +32 -0
  23. package/dist/openai-oauth/codex-auth-broker.js +110 -0
  24. package/dist/openai-oauth/index.d.ts +2 -0
  25. package/dist/openai-oauth/index.js +1 -0
  26. package/dist/providers/credentials.d.ts +4 -1
  27. package/dist/providers/credentials.js +4 -3
  28. package/dist/providers/sandbox.d.ts +2 -0
  29. package/dist/providers/scripted-mcp-mock-host.js +6 -3
  30. package/dist/providers/workspace.d.ts +1 -0
  31. package/dist/providers/workspace.js +9 -1
  32. package/dist/sdk/agent-result-log.js +4 -2
  33. package/dist/sdk/agent.js +7 -0
  34. package/dist/sdk/judge-tools.js +14 -3
  35. package/dist/sdk/managed-session.d.ts +2 -0
  36. package/dist/sdk/managed-session.js +22 -5
  37. package/dist/sdk/mcp-safety.js +2 -18
  38. package/dist/sdk/snapshots.d.ts +1 -0
  39. package/dist/sdk/snapshots.js +3 -2
  40. package/dist/sdk/tool-event-log.js +5 -2
  41. package/dist/sdk/tool-event-secrets.d.ts +4 -0
  42. package/dist/sdk/tool-event-secrets.js +14 -0
  43. package/dist/sdk/turn-result-secrets.d.ts +5 -0
  44. package/dist/sdk/turn-result-secrets.js +12 -0
  45. package/dist/tool-event-results.d.ts +10 -0
  46. package/dist/tool-event-results.js +171 -0
  47. package/dist/types.d.ts +2 -0
  48. package/dist/utils/llm.js +11 -0
  49. package/docs/OPENAI_OAUTH_JUDGE.md +91 -0
  50. package/package.json +13 -2
@@ -1,133 +1,22 @@
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';
18
+ import { createCodexManagedAuth } from './managed-auth.js';
14
19
  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
20
  function projectMcpStartupStatusIntoTurn(params, turn) {
132
21
  const name = params.name ?? 'unknown';
133
22
  const status = params.status ?? 'unknown';
@@ -156,6 +45,18 @@ function failTurn(turn, message) {
156
45
  turn.failureMessage = message;
157
46
  turn.signalFailure?.(message);
158
47
  }
48
+ function extractTurnCompletionIdentity(params) {
49
+ if (!params || typeof params !== 'object' || Array.isArray(params))
50
+ return {};
51
+ const record = params;
52
+ const turnId = typeof record.turn?.id === 'string'
53
+ ? record.turn.id
54
+ : typeof record.turnId === 'string' ? record.turnId : undefined;
55
+ return {
56
+ ...(typeof record.threadId === 'string' ? { threadId: record.threadId } : {}),
57
+ ...(turnId ? { turnId } : {}),
58
+ };
59
+ }
159
60
  function isLiveMcpSafetyMode(options) {
160
61
  const runMode = options?.runMode ?? 'mock';
161
62
  return runMode === 'live-readonly' || runMode === 'live-sandbox' || runMode === 'live';
@@ -170,6 +71,7 @@ export class CodexAppServerAgent extends BaseAgent {
170
71
  const askBus = requireAskBusForLiveBatches(options, 'CodexAppServerAgent');
171
72
  const workspacePath = getWorkspacePath(runtime);
172
73
  const runtimeEnv = getRuntimeEnv(runtime);
74
+ const sensitiveValues = collectSensitiveEnvValues(runtimeEnv);
173
75
  const model = resolveCodexModel(options?.model);
174
76
  const sandboxMode = this.deps.sandboxMode ?? 'workspace-write';
175
77
  let handle = null;
@@ -179,6 +81,12 @@ export class CodexAppServerAgent extends BaseAgent {
179
81
  let activeTurn = null;
180
82
  let disposed = false;
181
83
  let codexUserAgent = 'unknown';
84
+ const managedAuth = createCodexManagedAuth({
85
+ enabled: !runtimeEnv.OPENAI_API_KEY && !runtimeEnv.OPENAI_BASE_URL
86
+ && (this.deps.authBroker !== undefined || this.deps.createTransport === undefined),
87
+ ...(this.deps.authBroker ? { broker: this.deps.authBroker } : {}),
88
+ ...(runtimeEnv.CODEX_HOME ? { codexHome: runtimeEnv.CODEX_HOME } : {}),
89
+ });
182
90
  const scriptedHost = options?.scriptedMcpHost;
183
91
  const correlator = scriptedHost
184
92
  ? new CodexMcpApprovalCorrelator(scriptedHost, () => turnCounter, () => threadId, () => options?.getRemainingMs?.() ?? Number.POSITIVE_INFINITY)
@@ -191,8 +99,9 @@ export class CodexAppServerAgent extends BaseAgent {
191
99
  const factory = this.deps.createTransport
192
100
  ?? (async (ctx) => spawnAppServerTransport({ cwd: ctx.workspacePath, env: ctx.env,
193
101
  args: scriptedHost ? ['--enable', 'tool_call_mcp_elicitation'] : [] }));
194
- handle = await factory({ workspacePath, env: runtimeEnv });
195
- const transport = handle.transport;
102
+ const spawnedHandle = await factory({ workspacePath, env: runtimeEnv });
103
+ handle = spawnedHandle;
104
+ const transport = spawnedHandle.transport;
196
105
  transport.onServerRequest((req) => this.dispatchServerRequest(req, {
197
106
  transport,
198
107
  askBus,
@@ -201,13 +110,15 @@ export class CodexAppServerAgent extends BaseAgent {
201
110
  mcpSafety: options?.mcpSafety,
202
111
  scriptedHost,
203
112
  correlator,
113
+ managedAuth,
204
114
  }));
205
115
  transport.onClose((info) => {
206
116
  closeInfo = info;
207
117
  });
208
118
  transport.onNotification((n) => {
209
119
  if (process.env.PATHGRADE_CODEX_DEBUG) {
210
- console.error(`[codex app-server] notification method=${n.method} params=${JSON.stringify(n.params).slice(0, 300)}`);
120
+ const debugParams = sanitizePersistenceValue(n.params, sensitiveValues);
121
+ console.error(`[codex app-server] notification method=${n.method} params=${JSON.stringify(debugParams).slice(0, 300)}`);
211
122
  }
212
123
  if (n.method === 'mcpServer/startupStatus/updated') {
213
124
  const turn = activeTurn;
@@ -221,37 +132,36 @@ export class CodexAppServerAgent extends BaseAgent {
221
132
  return;
222
133
  const params = n.params;
223
134
  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
- }
135
+ if (params)
136
+ turn.itemLifecycle?.receiveStarted(params);
230
137
  return;
231
138
  }
232
139
  if (n.method !== 'item/completed')
233
140
  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
- }
141
+ if (params)
142
+ turn.itemLifecycle?.receiveCompleted(params);
243
143
  });
244
- const initialized = await transport.sendRequest('initialize', {
245
- clientInfo: { name: 'pathgrade', version: '0.5.0', title: null },
246
- capabilities: { experimentalApi: true, optOutNotificationMethods: null },
144
+ const initialize = async () => {
145
+ const initialized = await transport.sendRequest('initialize', {
146
+ clientInfo: { name: 'pathgrade', version: '0.5.0', title: null },
147
+ capabilities: { experimentalApi: true, optOutNotificationMethods: null },
148
+ });
149
+ if (typeof initialized.userAgent === 'string') {
150
+ codexUserAgent = initialized.userAgent.slice(0, 200);
151
+ }
152
+ // Upstream ClientNotification = { method: "initialized" }: send it
153
+ // before any thread/start so the handshake matches the v0.124
154
+ // contract and is forward-compatible with servers that enforce it.
155
+ transport.sendNotification('initialized', null);
156
+ await managedAuth.login(transport);
157
+ return transport;
158
+ };
159
+ return initialize().catch(async (error) => {
160
+ if (handle === spawnedHandle)
161
+ handle = null;
162
+ await spawnedHandle.close().catch(() => undefined);
163
+ throw error;
247
164
  });
248
- if (typeof initialized.userAgent === 'string')
249
- codexUserAgent = initialized.userAgent.slice(0, 200);
250
- // Upstream ClientNotification = { method: "initialized" }: send it
251
- // before any thread/start so the handshake matches the v0.124
252
- // contract and is forward-compatible with servers that enforce it.
253
- transport.sendNotification('initialized', null);
254
- return transport;
255
165
  };
256
166
  const runTurn = async (message) => {
257
167
  const t = await ensureTransport();
@@ -262,6 +172,7 @@ export class CodexAppServerAgent extends BaseAgent {
262
172
  nonAskToolEvents: [],
263
173
  assistantMessageParts: [],
264
174
  turnFailed: false,
175
+ pendingTurnCompletions: [],
265
176
  };
266
177
  activeTurn = turn;
267
178
  correlator?.beginTurn();
@@ -286,6 +197,31 @@ export class CodexAppServerAgent extends BaseAgent {
286
197
  }
287
198
  threadId = resp.thread.id;
288
199
  }
200
+ const authoritativeThreadId = threadId;
201
+ turn.authoritativeThreadId = authoritativeThreadId;
202
+ turn.itemLifecycle = new CodexItemLifecycle({
203
+ threadId: authoritativeThreadId,
204
+ clock: this.deps.clock,
205
+ onFailure: (message) => failTurn(turn, message),
206
+ onStarted: (params) => {
207
+ try {
208
+ correlator?.started(params);
209
+ }
210
+ catch (error) {
211
+ failTurn(turn, error instanceof Error ? error.message : String(error));
212
+ }
213
+ },
214
+ onCompleted: (item, timing, params) => {
215
+ try {
216
+ if (correlator?.completed(params) !== false) {
217
+ projectItemIntoTurn(item, turn, sensitiveValues, timing);
218
+ }
219
+ }
220
+ catch (error) {
221
+ failTurn(turn, error instanceof Error ? error.message : String(error));
222
+ }
223
+ },
224
+ });
289
225
  // Wait for TurnCompleted OR subprocess crash OR dispatcher failure.
290
226
  const turnCompletion = new Promise((resolve, reject) => {
291
227
  let settled = false;
@@ -293,15 +229,30 @@ export class CodexAppServerAgent extends BaseAgent {
293
229
  if (settled)
294
230
  return;
295
231
  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();
232
+ turn.acceptTurnCompletion?.(n.params);
303
233
  }
304
234
  });
235
+ const acceptTurnCompletion = (params) => {
236
+ if (settled)
237
+ return;
238
+ const identity = extractTurnCompletionIdentity(params);
239
+ if (identity.threadId && identity.threadId !== turn.authoritativeThreadId)
240
+ return;
241
+ if (identity.turnId && !turn.authoritativeTurnId) {
242
+ turn.pendingTurnCompletions.push(params);
243
+ return;
244
+ }
245
+ if (identity.turnId && identity.turnId !== turn.authoritativeTurnId)
246
+ return;
247
+ settled = true;
248
+ off();
249
+ closeOff();
250
+ const failure = extractTurnCompletionFailure(params);
251
+ if (failure)
252
+ Object.assign(turn, { turnFailed: true, failureMessage: failure });
253
+ resolve();
254
+ };
255
+ turn.acceptTurnCompletion = acceptTurnCompletion;
305
256
  const closeOff = t.onClose((info) => {
306
257
  if (settled)
307
258
  return;
@@ -339,9 +290,25 @@ export class CodexAppServerAgent extends BaseAgent {
339
290
  }
340
291
  }
341
292
  });
342
- const startTurnResp = t.sendRequest('turn/start', {
293
+ const authoritativeTurn = t.sendRequest('turn/start', {
343
294
  threadId,
344
295
  input: [{ type: 'text', text: message, text_elements: [] }],
296
+ })
297
+ .then((started) => {
298
+ const authoritativeTurnId = started.turn?.id ?? started.turnId;
299
+ if (!authoritativeTurnId) {
300
+ scriptedHost?.failProtocol('turn/start did not return an authoritative turn id');
301
+ turn.signalFailure?.('turn/start did not return an authoritative turn id');
302
+ return;
303
+ }
304
+ turn.itemLifecycle?.setAuthoritativeTurn(authoritativeTurnId);
305
+ turn.authoritativeTurnId = authoritativeTurnId;
306
+ if (correlator?.setAuthoritativeTurn(authoritativeThreadId, authoritativeTurnId) === false) {
307
+ turn.signalFailure?.('MCP lifecycle did not match the authoritative turn');
308
+ }
309
+ for (const pending of turn.pendingTurnCompletions.splice(0)) {
310
+ turn.acceptTurnCompletion?.(pending);
311
+ }
345
312
  })
346
313
  .catch((err) => {
347
314
  // A JSON-RPC error on turn/start means the server will
@@ -355,18 +322,6 @@ export class CodexAppServerAgent extends BaseAgent {
355
322
  : 'turn/start failed';
356
323
  turn.signalFailure?.(msg);
357
324
  });
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
325
  try {
371
326
  await turnCompletion;
372
327
  }
@@ -379,16 +334,25 @@ export class CodexAppServerAgent extends BaseAgent {
379
334
  message: info.message ?? 'app-server exited',
380
335
  pid: info.pid,
381
336
  signal: info.signal,
337
+ sensitiveValues,
382
338
  });
383
339
  }
384
- if (!scriptedHost)
385
- void startTurnResp;
386
340
  if (turn.turnFailed) {
387
341
  return assembleTurnResult({
388
342
  askBus,
389
343
  activeTurn: turn,
390
344
  exitCode: 1,
391
345
  message: turn.failureMessage ?? 'turn failed',
346
+ sensitiveValues,
347
+ });
348
+ }
349
+ // Ensure pre-response lifecycle notifications have been correlated
350
+ // and projected before exposing the successful turn result.
351
+ await authoritativeTurn;
352
+ if (turn.turnFailed) {
353
+ return assembleTurnResult({
354
+ askBus, activeTurn: turn, exitCode: 1,
355
+ message: turn.failureMessage ?? 'turn failed', sensitiveValues,
392
356
  });
393
357
  }
394
358
  return assembleTurnResult({
@@ -396,6 +360,7 @@ export class CodexAppServerAgent extends BaseAgent {
396
360
  activeTurn: turn,
397
361
  exitCode: 0,
398
362
  message: turn.assistantMessageParts.join('\n\n'),
363
+ sensitiveValues,
399
364
  });
400
365
  }
401
366
  finally {
@@ -516,11 +481,11 @@ export class CodexAppServerAgent extends BaseAgent {
516
481
  }
517
482
  return;
518
483
  case 'account/chatgptAuthTokens/refresh': {
519
- const message = 'codex app-server requires OPENAI_API_KEY for pathgrade and honors OPENAI_BASE_URL when set; ChatGPT/cached auth unsupported under transport=app-server';
520
- transport.sendErrorResponse(req.id, -32001, message);
521
- const turn = activeTurn();
522
- if (turn)
523
- failTurn(turn, message);
484
+ ctx.managedAuth.respondToRefresh(req.id, transport, (message) => {
485
+ const turn = activeTurn();
486
+ if (turn)
487
+ failTurn(turn, message);
488
+ });
524
489
  return;
525
490
  }
526
491
  default:
@@ -577,14 +542,15 @@ function buildThreadStartParams(opts) {
577
542
  };
578
543
  }
579
544
  function assembleTurnResult(args) {
580
- const { askBus, activeTurn, exitCode, message, pid, signal } = args;
545
+ const { askBus, activeTurn, exitCode, message, pid, signal, sensitiveValues = [] } = args;
581
546
  const askBatchIds = new Set(activeTurn.askBatchIds);
582
547
  const askEvents = askBus
583
548
  .snapshot()
584
549
  .filter((s) => askBatchIds.has(s.batchId))
585
550
  .map((s) => toAskUserToolEvent(s));
586
- const toolEvents = enrichSkillEvents([...askEvents, ...activeTurn.nonAskToolEvents]);
587
- const rawOutput = exitCode === 0
551
+ const toolEvents = enrichSkillEvents([...askEvents, ...activeTurn.nonAskToolEvents])
552
+ .map((event) => attachToolEventSensitiveValues(event, sensitiveValues));
553
+ const rawOutput = sanitizePersistenceValue(exitCode === 0
588
554
  ? message
589
555
  : [
590
556
  message,
@@ -593,8 +559,8 @@ function assembleTurnResult(args) {
593
559
  `exitCode=${exitCode}`,
594
560
  ]
595
561
  .filter(Boolean)
596
- .join(' ');
597
- return {
562
+ .join(' '), sensitiveValues);
563
+ return attachTurnResultSensitiveValues({
598
564
  rawOutput,
599
565
  assistantMessage: exitCode === 0 ? message : '',
600
566
  visibleAssistantMessage: exitCode === 0 ? message : '',
@@ -611,5 +577,5 @@ function assembleTurnResult(args) {
611
577
  },
612
578
  }
613
579
  : {}),
614
- };
580
+ }, sensitiveValues);
615
581
  }
@@ -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
+ }
@@ -0,0 +1,95 @@
1
+ import { performance } from 'node:perf_hooks';
2
+ import { canonicalizeJson } from '../../core/canonical-json.js';
3
+ export class CodexItemLifecycle {
4
+ options;
5
+ turnId;
6
+ pending = [];
7
+ started = new Map();
8
+ completed = new Map();
9
+ constructor(options) {
10
+ this.options = options;
11
+ }
12
+ receiveStarted(params) {
13
+ this.receive('started', params);
14
+ }
15
+ receiveCompleted(params) {
16
+ this.receive('completed', params);
17
+ }
18
+ setAuthoritativeTurn(turnId) {
19
+ if (!turnId) {
20
+ this.options.onFailure('turn/start did not return an authoritative turn id');
21
+ return false;
22
+ }
23
+ if (this.turnId && this.turnId !== turnId) {
24
+ this.options.onFailure('turn/start returned conflicting authoritative turn ids');
25
+ return false;
26
+ }
27
+ this.turnId = turnId;
28
+ for (const entry of this.pending.splice(0))
29
+ this.process(entry.phase, entry.receipt);
30
+ return true;
31
+ }
32
+ receive(phase, params) {
33
+ const clock = this.options.clock ?? DEFAULT_CLOCK;
34
+ const receipt = { params, wallMs: clock.wallNow(), monotonicMs: clock.monotonicNow() };
35
+ if (!this.validateEnvelope(params))
36
+ return;
37
+ if (!this.turnId) {
38
+ this.pending.push({ phase, receipt });
39
+ return;
40
+ }
41
+ this.process(phase, receipt);
42
+ }
43
+ validateEnvelope(params) {
44
+ if (!params.item || typeof params.item.id !== 'string' || !params.item.id
45
+ || typeof params.threadId !== 'string' || !params.threadId
46
+ || typeof params.turnId !== 'string' || !params.turnId) {
47
+ this.options.onFailure('Codex item lifecycle notification is missing threadId, turnId, or item.id');
48
+ return false;
49
+ }
50
+ // Notifications for an earlier thread are stale, not failures of this turn.
51
+ return params.threadId === this.options.threadId;
52
+ }
53
+ process(phase, receipt) {
54
+ const { params } = receipt;
55
+ if (params.turnId !== this.turnId || !params.item || typeof params.item.id !== 'string')
56
+ return;
57
+ const itemId = params.item.id;
58
+ const fingerprint = canonicalizeJson(params.item);
59
+ if (phase === 'started') {
60
+ const prior = this.started.get(itemId);
61
+ if (prior) {
62
+ if (prior.fingerprint !== fingerprint) {
63
+ this.options.onFailure(`Conflicting item/started notification for item ${itemId}`);
64
+ }
65
+ return;
66
+ }
67
+ if (this.completed.has(itemId)) {
68
+ this.options.onFailure(`item/started arrived after item/completed for item ${itemId}`);
69
+ return;
70
+ }
71
+ this.started.set(itemId, { ...receipt, fingerprint });
72
+ this.options.onStarted?.(params);
73
+ return;
74
+ }
75
+ const priorTerminal = this.completed.get(itemId);
76
+ if (priorTerminal !== undefined) {
77
+ if (priorTerminal !== fingerprint) {
78
+ this.options.onFailure(`Conflicting item/completed notification for item ${itemId}`);
79
+ }
80
+ return;
81
+ }
82
+ this.completed.set(itemId, fingerprint);
83
+ const start = this.started.get(itemId);
84
+ this.started.delete(itemId);
85
+ this.options.onCompleted(params.item, {
86
+ ...(start ? { startedAtWallMs: start.wallMs } : {}),
87
+ completedAtWallMs: receipt.wallMs,
88
+ ...(start ? { observedDurationMs: Math.max(0, receipt.monotonicMs - start.monotonicMs) } : {}),
89
+ }, params);
90
+ }
91
+ }
92
+ const DEFAULT_CLOCK = {
93
+ wallNow: () => Date.now(),
94
+ monotonicNow: () => performance.now(),
95
+ };
@@ -0,0 +1,62 @@
1
+ import { type ToolEvent } from '../../tool-events.js';
2
+ export interface CodexTurnProjection {
3
+ turnNumber: number;
4
+ nonAskToolEvents: ToolEvent[];
5
+ assistantMessageParts: string[];
6
+ }
7
+ interface CodexCommandExecutionAction {
8
+ type?: string;
9
+ name?: string;
10
+ path?: string;
11
+ command?: string;
12
+ }
13
+ interface CodexCommandExecutionItem {
14
+ type: 'commandExecution';
15
+ id: string;
16
+ command: string;
17
+ status?: 'inProgress' | 'completed' | 'failed' | 'declined';
18
+ cwd?: string;
19
+ commandActions?: CodexCommandExecutionAction[];
20
+ aggregatedOutput?: string | null;
21
+ exitCode?: number | null;
22
+ durationMs?: number | null;
23
+ }
24
+ interface CodexMcpToolCallItem {
25
+ type: 'mcpToolCall';
26
+ id: string;
27
+ server: string;
28
+ tool: string;
29
+ status?: string;
30
+ arguments?: unknown;
31
+ result?: unknown;
32
+ error?: {
33
+ message?: string;
34
+ } | null;
35
+ durationMs?: number | null;
36
+ }
37
+ export type CodexItem = {
38
+ type: 'agentMessage';
39
+ id: string;
40
+ text: string;
41
+ phase?: string;
42
+ } | CodexCommandExecutionItem | {
43
+ type: 'fileChange';
44
+ id: string;
45
+ changes: Array<{
46
+ path: string;
47
+ kind?: unknown;
48
+ diff?: string;
49
+ }>;
50
+ } | CodexMcpToolCallItem | {
51
+ type: string;
52
+ id?: string;
53
+ };
54
+ export interface ItemTiming {
55
+ startedAtWallMs?: number;
56
+ completedAtWallMs?: number;
57
+ observedDurationMs?: number;
58
+ }
59
+ export declare function projectItemIntoTurn(item: CodexItem, turn: CodexTurnProjection, sensitiveValues: readonly string[], timing?: ItemTiming): void;
60
+ /** Provider duration is authoritative; observed monotonic duration is the fallback. */
61
+ export declare function projectToolTiming(timing: ItemTiming, providerDurationMs: number | undefined): Pick<ToolEvent, 'startedAt' | 'completedAt' | 'durationMs'>;
62
+ export {};