@astrosheep/keiyaku 2.9.4 → 2.9.6

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 (143) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/call-terms.js +1 -0
  3. package/build/agents/harness/execution-handle.js +1 -0
  4. package/build/agents/harness/outcome.js +31 -5
  5. package/build/agents/harness/projection.js +151 -78
  6. package/build/agents/harness/router.js +2 -1
  7. package/build/agents/providers/claude-agent-sdk/adapter.js +13 -1
  8. package/build/agents/providers/claude-agent-sdk/registration.js +2 -1
  9. package/build/agents/providers/codex-app-server/adapter.js +43 -10
  10. package/build/agents/providers/codex-app-server/events.js +23 -0
  11. package/build/agents/providers/codex-app-server/registration.js +2 -1
  12. package/build/agents/providers/opencode-sdk/adapter.js +7 -0
  13. package/build/agents/providers/opencode-sdk/registration.js +2 -1
  14. package/build/agents/providers/pi/adapter.js +10 -0
  15. package/build/agents/providers/pi/events.js +4 -3
  16. package/build/agents/providers/pi/registration.js +3 -1
  17. package/build/agents/types.js +2 -0
  18. package/build/cli/commands/akuma/akuma/meta.js +1 -1
  19. package/build/cli/commands/akuma/list/meta-ls.js +1 -1
  20. package/build/cli/commands/akuma/view/meta.js +1 -1
  21. package/build/cli/commands/contract/amend/meta.js +4 -4
  22. package/build/cli/commands/contract/arc/handler.js +1 -1
  23. package/build/cli/commands/contract/arc/meta.js +4 -4
  24. package/build/cli/commands/contract/audit/handler.js +16 -0
  25. package/build/cli/commands/contract/audit/meta.js +16 -0
  26. package/build/cli/commands/contract/bind/meta.js +20 -11
  27. package/build/cli/commands/contract/catalog.js +2 -0
  28. package/build/cli/commands/contract/forfeit/meta.js +1 -1
  29. package/build/cli/commands/contract/log/meta.js +1 -1
  30. package/build/cli/commands/contract/petition/handler.js +1 -2
  31. package/build/cli/commands/contract/petition/meta.js +6 -6
  32. package/build/cli/commands/contract/renew/handler.js +2 -2
  33. package/build/cli/commands/contract/renew/meta.js +4 -4
  34. package/build/cli/commands/metadata.js +3 -2
  35. package/build/cli/commands/projection/call/meta.js +4 -1
  36. package/build/cli/commands/projection/kill/meta.js +1 -1
  37. package/build/cli/commands/projection/revive/meta.js +4 -1
  38. package/build/cli/commands/projection/status/meta.js +1 -1
  39. package/build/cli/commands/projection/tell/handler.js +1 -1
  40. package/build/cli/commands/projection/tell/meta.js +4 -1
  41. package/build/cli/commands/projection/wait/meta.js +1 -1
  42. package/build/cli/commands/skills/install/meta.js +1 -1
  43. package/build/cli/commands/skills/skills/meta.js +1 -1
  44. package/build/cli/commands/system/completion/meta.js +1 -1
  45. package/build/cli/commands/system/dump-env/meta.js +1 -1
  46. package/build/cli/commands/system/guide/meta.js +1 -1
  47. package/build/cli/commands/task/add/handler.js +2 -2
  48. package/build/cli/commands/task/add/meta.js +4 -1
  49. package/build/cli/commands/task/doctor/meta.js +1 -1
  50. package/build/cli/commands/task/done/meta.js +1 -1
  51. package/build/cli/commands/task/drop/meta.js +1 -1
  52. package/build/cli/commands/task/ls/handler.js +10 -4
  53. package/build/cli/commands/task/ls/meta.js +3 -3
  54. package/build/cli/commands/task/show/meta.js +1 -1
  55. package/build/cli/commands/task/start/meta.js +1 -1
  56. package/build/cli/commands/task/stop/meta.js +1 -1
  57. package/build/cli/commands/task/task/meta.js +1 -1
  58. package/build/cli/commands/task/update/handler.js +2 -3
  59. package/build/cli/commands/task/update/meta.js +4 -1
  60. package/build/cli/completion.js +9 -2
  61. package/build/cli/flags.js +19 -3
  62. package/build/cli/help.js +3 -3
  63. package/build/cli/index.js +59 -75
  64. package/build/cli/output-stream-error-policy.js +19 -5
  65. package/build/cli/parse-flags.js +19 -2
  66. package/build/cli/parse-metadata.js +1 -7
  67. package/build/cli/parse-selectors.js +1 -0
  68. package/build/cli/parse.js +16 -7
  69. package/build/cli/projection-address.js +6 -1
  70. package/build/cli/render/audit.js +121 -0
  71. package/build/cli/render/errors.js +18 -1
  72. package/build/cli/render/format.js +5 -2
  73. package/build/cli/render/misc.js +3 -1
  74. package/build/cli/render/projection-activity.js +12 -6
  75. package/build/cli/render/status.js +28 -18
  76. package/build/cli/render/success-response.js +6 -5
  77. package/build/cli/render/task.js +12 -43
  78. package/build/cli/render/terminal-failure.js +7 -2
  79. package/build/cli/render/wait.js +36 -8
  80. package/build/config/env-keys.js +2 -0
  81. package/build/config/env.js +3 -0
  82. package/build/core/amend.js +20 -94
  83. package/build/core/arc.js +14 -6
  84. package/build/core/audit/candidate.js +48 -0
  85. package/build/core/audit/coordinates.js +180 -0
  86. package/build/core/audit/evidence.js +158 -0
  87. package/build/core/audit/facade.js +42 -0
  88. package/build/core/audit/readiness.js +4 -0
  89. package/build/core/audit/report.js +38 -0
  90. package/build/core/audit.js +1 -0
  91. package/build/core/bind.js +1 -1
  92. package/build/core/call/execution.js +1 -0
  93. package/build/core/contract-view.js +2 -3
  94. package/build/core/draft.js +33 -20
  95. package/build/core/entry.js +3 -4
  96. package/build/core/markdown/lex.js +2 -1
  97. package/build/core/projection/generation/database.js +41 -16
  98. package/build/core/projection/generation/model.js +22 -2
  99. package/build/core/projection/generation/projection-generation-execution.js +20 -7
  100. package/build/core/projection/generation/projection-generation-launcher.js +4 -4
  101. package/build/core/projection/generation/projection-generation-runner.js +1 -0
  102. package/build/core/projection/generation/store.js +1 -1
  103. package/build/core/projection/generation/transitions.js +19 -3
  104. package/build/core/projection/index.js +2 -2
  105. package/build/core/projection/projection-execution-observer.js +35 -21
  106. package/build/core/projection/projection-life-observer.js +16 -2
  107. package/build/core/projection/projection-status.js +67 -22
  108. package/build/core/projection/projection-terminal-failure.js +2 -0
  109. package/build/core/projection/projection-wait.js +6 -9
  110. package/build/core/projection/projection-wake.js +30 -6
  111. package/build/core/renew.js +41 -22
  112. package/build/core/scope.js +129 -2
  113. package/build/core/seal.js +36 -15
  114. package/build/core/settlement/petition-claim-gates.js +4 -33
  115. package/build/core/settlement/petition-preview.js +1 -1
  116. package/build/core/settlement/petition.js +10 -3
  117. package/build/core/settlement/settlement.js +3 -10
  118. package/build/core/settlement/verification.js +59 -0
  119. package/build/core/status/board.js +48 -28
  120. package/build/core/stored-agent-event.js +3 -2
  121. package/build/core/task/board.js +1 -0
  122. package/build/core/task/commands.js +20 -12
  123. package/build/core/task/document.js +5 -0
  124. package/build/core/task/index.js +1 -1
  125. package/build/core/task/model.js +8 -1
  126. package/build/core/task/query.js +214 -0
  127. package/build/core/task/settlement-git.js +115 -16
  128. package/build/core/task/settlement-policy.js +17 -9
  129. package/build/core/task/source-board.js +1 -0
  130. package/build/core/task/task-contract.js +125 -51
  131. package/build/core/task/task-store-repository.js +1 -0
  132. package/build/core/task/task.js +2 -0
  133. package/build/flow-error.js +2 -4
  134. package/build/generated/version.js +2 -2
  135. package/build/git/diff/parsers.js +23 -13
  136. package/build/git/diff/structured.js +127 -0
  137. package/build/index.js +7 -7
  138. package/build/telemetry/logger.js +4 -2
  139. package/package.json +1 -1
  140. package/skills/keiyaku/SKILL.md +2 -2
  141. package/skills/keiyaku-akuma/SKILL.md +17 -6
  142. package/skills/keiyaku-task/SKILL.md +16 -7
  143. package/skills/keiyaku-workflow/SKILL.md +48 -13
@@ -15,6 +15,7 @@ export function buildCallTerms(snapshot, prompt, cwd, options) {
15
15
  prompt,
16
16
  sessionContinuationPromptPrefix,
17
17
  idleTimeoutMs: options.idleTimeoutMs ?? runtimeConfig.subagent.execIdleTimeoutMs,
18
+ executionTimeoutMs: options.executionTimeoutMs ?? runtimeConfig.subagent.execMaxDurationMs,
18
19
  maxCaptureChars: registration.maxCaptureChars(runtimeConfig),
19
20
  signal: options.signal,
20
21
  ...(options.initialTurn !== undefined ? { initialTurn: options.initialTurn } : {}),
@@ -167,5 +167,6 @@ export function createProjectionDriver(input) {
167
167
  })();
168
168
  await gracefulAbort;
169
169
  },
170
+ ...(input.probe ? { probe: input.probe } : {}),
170
171
  };
171
172
  }
@@ -3,13 +3,17 @@ import { SUBAGENT_SESSION_VERSION, } from "../session.js";
3
3
  import { OUTCOME_ERROR_VERSION, SubagentFailure, } from "../types.js";
4
4
  import { isProviderTerminalError, } from "./provider-adapter.js";
5
5
  import { renderCapturedText } from "./runtime.js";
6
- export function createOutcomeState(maxCaptureChars) {
6
+ export function createOutcomeState(maxCaptureChars, admittedSession) {
7
7
  return {
8
8
  outputBuffer: new StringTailBuffer(maxCaptureChars),
9
9
  diagnosticsBuffer: new StringTailBuffer(maxCaptureChars),
10
10
  warnings: [],
11
+ ...(admittedSession ? { session: admittedSession } : {}),
11
12
  };
12
13
  }
14
+ export function recordSessionAdmission(state, session) {
15
+ state.session = session;
16
+ }
13
17
  function mintOutcome(draft) {
14
18
  return draft;
15
19
  }
@@ -29,7 +33,6 @@ export function processEvent(state, event) {
29
33
  return;
30
34
  case "done":
31
35
  state.doneEvent = event;
32
- state.session = event.session;
33
36
  if (event.finalMessage !== undefined) {
34
37
  state.finalMessageCandidate = event.finalMessage;
35
38
  }
@@ -64,7 +67,15 @@ export function failFromFailEvent(shared, state, failEvent) {
64
67
  exitCode: failEvent.exitCode ?? null,
65
68
  }, state.finalMessageCandidate, failEvent.diagnostics ?? diagnostics);
66
69
  }
67
- export function classifyFailure(shared, state, error, idleTimedOut) {
70
+ export function classifyFailure(shared, state, error, idleTimedOut, transportFailure, policyBudgetExceeded = false) {
71
+ if (policyBudgetExceeded) {
72
+ return failWithError(shared, state, {
73
+ code: "SUBAGENT_POLICY_BUDGET",
74
+ message: `${shared.terms.provider} execution policy budget expired`,
75
+ timeoutMs: shared.terms.executionTimeoutMs,
76
+ exitCode: null,
77
+ }, state.finalMessageCandidate);
78
+ }
68
79
  if (shared.dismissed || shared.externallyAborted) {
69
80
  return mintOutcome({
70
81
  status: "dismissed",
@@ -75,6 +86,18 @@ export function classifyFailure(shared, state, error, idleTimedOut) {
75
86
  warnings: [...state.warnings],
76
87
  });
77
88
  }
89
+ if (transportFailure) {
90
+ const message = transportFailure instanceof Error ? transportFailure.message : `${shared.terms.provider} session transport probe failed`;
91
+ const timeoutMs = isProviderTerminalError(transportFailure) && transportFailure.fact.kind === "failed"
92
+ ? transportFailure.fact.timeoutMs
93
+ : shared.terms.idleTimeoutMs;
94
+ return failWithError(shared, state, {
95
+ code: "SUBAGENT_TRANSPORT_ERROR",
96
+ message,
97
+ timeoutMs,
98
+ exitCode: null,
99
+ }, state.finalMessageCandidate);
100
+ }
78
101
  if (idleTimedOut) {
79
102
  return failWithError(shared, state, {
80
103
  code: "SUBAGENT_TIMEOUT",
@@ -153,13 +176,16 @@ export function failWithError(shared, state, error, finalMessage, diagnostics =
153
176
  warnings: [...state.warnings],
154
177
  });
155
178
  }
156
- export function completeOutcome(shared, state, finalMessage, session) {
179
+ export function completeOutcome(state, finalMessage) {
180
+ if (!state.session) {
181
+ throw new Error("Cannot complete an outcome before session admission");
182
+ }
157
183
  return mintOutcome({
158
184
  status: "completed",
159
185
  finalMessage,
160
186
  output: renderCapturedText(state.outputBuffer),
161
187
  diagnostics: renderCapturedText(state.diagnosticsBuffer),
162
- session,
188
+ session: state.session,
163
189
  warnings: [...state.warnings],
164
190
  });
165
191
  }
@@ -2,9 +2,10 @@ import { randomUUID } from "node:crypto";
2
2
  import { FlowError } from "../../flow-error.js";
3
3
  import { createEventChannel } from "./event-channel.js";
4
4
  import { overflowMarkerFromDropped, prepareAgentEventForPersistence, } from "./execution-handle.js";
5
- import { classifyFailure, classifySettlementFailure, completeOutcome, createOutcomeState, failFromFailEvent, failWithError, processEvent, renderOutcomeCapturedText, } from "./outcome.js";
5
+ import { classifyFailure, classifySettlementFailure, completeOutcome, createOutcomeState, failFromFailEvent, failWithError, processEvent, recordSessionAdmission, renderOutcomeCapturedText, } from "./outcome.js";
6
6
  import { startLiveProjection } from "./router.js";
7
7
  import { createAbortError } from "./runtime.js";
8
+ const POLICY_BUDGET_CLEANUP_GRACE_MS = 5_000;
8
9
  export function call(terms, options) {
9
10
  return createProjection(terms, undefined, options);
10
11
  }
@@ -154,7 +155,21 @@ function requestControlledGracefulAbort(shared, reason) {
154
155
  shared.gracefulAbort.catch(() => undefined);
155
156
  return shared.gracefulAbort;
156
157
  }
157
- function deliverDriverEvent(shared, state, live, event, refreshIdleTimeout, observerEnabled) {
158
+ function requestPolicyBudgetCleanup(shared, reason) {
159
+ const graceful = requestControlledGracefulAbort(shared, reason);
160
+ const force = () => {
161
+ void shared.live?.abort("force").catch(() => undefined);
162
+ };
163
+ const cleanupTimeout = setTimeout(force, POLICY_BUDGET_CLEANUP_GRACE_MS);
164
+ cleanupTimeout.unref?.();
165
+ void graceful.then(() => clearTimeout(cleanupTimeout), () => {
166
+ clearTimeout(cleanupTimeout);
167
+ force();
168
+ });
169
+ }
170
+ function deliverDriverEvent(shared, state, live, event, refreshIdleTimeout, knownActiveTools, observerEnabled) {
171
+ if (shared.terminal)
172
+ return;
158
173
  const prepared = prepareAgentEventForPersistence(event);
159
174
  if (observerEnabled.value && shared.observer) {
160
175
  try {
@@ -173,25 +188,63 @@ function deliverDriverEvent(shared, state, live, event, refreshIdleTimeout, obse
173
188
  }
174
189
  }
175
190
  shared.publicEvents.push(event);
191
+ if (event.type === "activity") {
192
+ if (event.phase === "started")
193
+ knownActiveTools.add(event.id);
194
+ if (event.phase === "completed")
195
+ knownActiveTools.delete(event.id);
196
+ }
176
197
  refreshIdleTimeout();
177
198
  processEvent(state, event);
178
199
  }
179
200
  async function settleProjection(shared) {
180
- const state = createOutcomeState(shared.terms.maxCaptureChars);
201
+ const state = createOutcomeState(shared.terms.maxCaptureChars, shared.reviveTerms?.session);
181
202
  let idleTimedOut = false;
203
+ let transportFailure;
204
+ let policyBudgetExceeded = false;
205
+ const knownActiveTools = new Set();
182
206
  let abortRequested = false;
183
207
  const observerEnabled = { value: shared.observer !== undefined };
184
208
  let idleTimeout;
209
+ let executionTimeout;
210
+ let liveDriver;
211
+ let probeInFlight;
212
+ let resolvePolicyBudget;
213
+ const policyBudget = new Promise((resolve) => {
214
+ resolvePolicyBudget = resolve;
215
+ });
185
216
  const refreshIdleTimeout = () => {
217
+ if (shared.terminal)
218
+ return;
186
219
  if (idleTimeout)
187
220
  clearTimeout(idleTimeout);
188
221
  idleTimeout = setTimeout(() => {
222
+ if (knownActiveTools.size > 0) {
223
+ if (!liveDriver?.probe || probeInFlight)
224
+ return;
225
+ probeInFlight = liveDriver.probe()
226
+ .then(() => refreshIdleTimeout())
227
+ .catch((error) => {
228
+ transportFailure = error;
229
+ return requestControlledGracefulAbort(shared, error);
230
+ })
231
+ .finally(() => {
232
+ probeInFlight = undefined;
233
+ });
234
+ probeInFlight.catch(() => undefined);
235
+ return;
236
+ }
189
237
  idleTimedOut = true;
190
238
  requestControlledGracefulAbort(shared, createAbortError(`${shared.terms.provider} exec idle timed out`));
191
239
  }, shared.terms.idleTimeoutMs);
192
240
  idleTimeout.unref?.();
193
241
  };
194
- refreshIdleTimeout();
242
+ executionTimeout = setTimeout(() => {
243
+ policyBudgetExceeded = true;
244
+ requestPolicyBudgetCleanup(shared, createAbortError(`${shared.terms.provider} execution policy budget expired`));
245
+ resolvePolicyBudget();
246
+ }, shared.terms.executionTimeoutMs);
247
+ executionTimeout.unref?.();
195
248
  const requestLiveAbort = () => {
196
249
  if (shared.explicitlyAborted)
197
250
  return;
@@ -201,90 +254,110 @@ async function settleProjection(shared) {
201
254
  requestControlledGracefulAbort(shared, shared.lifecycleAbort.signal.reason);
202
255
  };
203
256
  shared.lifecycleAbort.signal.addEventListener("abort", requestLiveAbort, { once: true });
204
- try {
205
- const live = await startLiveProjection(shared.terms, shared.lifecycleAbort.signal, shared.reviveTerms, refreshIdleTimeout, shared.observer);
206
- shared.onLive(live);
207
- resolveForceWaiters(shared, live);
208
- if (shared.lifecycleAbort.signal.aborted) {
209
- requestLiveAbort();
210
- }
257
+ const providerSettlement = (async () => {
211
258
  try {
212
- for await (const event of live.events) {
213
- deliverDriverEvent(shared, state, live, event, refreshIdleTimeout, observerEnabled);
259
+ const live = await startLiveProjection(shared.terms, shared.lifecycleAbort.signal, shared.reviveTerms, refreshIdleTimeout, shared.observer, (admission) => {
260
+ recordSessionAdmission(state, admission.session);
261
+ refreshIdleTimeout();
262
+ });
263
+ liveDriver = live;
264
+ shared.onLive(live);
265
+ resolveForceWaiters(shared, live);
266
+ if (shared.lifecycleAbort.signal.aborted) {
267
+ requestLiveAbort();
214
268
  }
215
- }
216
- catch (error) {
217
- if (error instanceof Error && error.code === "PROJECTION_STATE_ERROR") {
218
- const event = live.mintEvent({
219
- type: "fail",
220
- code: "PROJECTION_STATE_ERROR",
221
- message: error.message,
222
- });
223
- if (event)
224
- deliverDriverEvent(shared, state, live, event, refreshIdleTimeout, observerEnabled);
269
+ try {
270
+ for await (const event of live.events) {
271
+ deliverDriverEvent(shared, state, live, event, refreshIdleTimeout, knownActiveTools, observerEnabled);
272
+ }
225
273
  }
226
- else {
227
- const event = live.mintEvent({
228
- type: "diagnostic",
229
- text: `pump error: ${String(error)}`,
230
- });
231
- if (event)
232
- deliverDriverEvent(shared, state, live, event, refreshIdleTimeout, observerEnabled);
274
+ catch (error) {
275
+ if (error instanceof Error && error.code === "PROJECTION_STATE_ERROR") {
276
+ const event = live.mintEvent({
277
+ type: "fail",
278
+ code: "PROJECTION_STATE_ERROR",
279
+ message: error.message,
280
+ });
281
+ if (event)
282
+ deliverDriverEvent(shared, state, live, event, refreshIdleTimeout, knownActiveTools, observerEnabled);
283
+ }
284
+ else {
285
+ const event = live.mintEvent({
286
+ type: "diagnostic",
287
+ text: `pump error: ${String(error)}`,
288
+ });
289
+ if (event)
290
+ deliverDriverEvent(shared, state, live, event, refreshIdleTimeout, knownActiveTools, observerEnabled);
291
+ }
233
292
  }
293
+ const completionResult = await live.completion.then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
294
+ const completionOutcome = completionResult.ok ? completionResult.value : undefined;
295
+ // Outcome classification is the terminal publication boundary. A
296
+ // controlled abort may let the provider run settle before its graceful
297
+ // cleanup promise does, so prove that shared settlement before publishing
298
+ // dismissed, failed-for-another-reason, or completed truth.
299
+ const gracefulResult = shared.gracefulAbort
300
+ ? await shared.gracefulAbort.then(() => ({ ok: true }), (error) => ({ ok: false, error }))
301
+ : undefined;
302
+ if (gracefulResult && !gracefulResult.ok && !transportFailure) {
303
+ return classifySettlementFailure(shared, state, gracefulResult.error);
304
+ }
305
+ if (policyBudgetExceeded || transportFailure) {
306
+ return classifyFailure(shared, state, completionResult.ok ? undefined : completionResult.error, idleTimedOut, transportFailure, policyBudgetExceeded);
307
+ }
308
+ if (state.failEvent) {
309
+ return failFromFailEvent(shared, state, state.failEvent);
310
+ }
311
+ if (!completionResult.ok) {
312
+ return classifyFailure(shared, state, completionResult.error, idleTimedOut, transportFailure, policyBudgetExceeded);
313
+ }
314
+ if (shared.lifecycleAbort.signal.aborted) {
315
+ return classifyFailure(shared, state, shared.lifecycleAbort.signal.reason, idleTimedOut, transportFailure, policyBudgetExceeded);
316
+ }
317
+ if (!state.doneEvent && !completionOutcome) {
318
+ return failWithError(shared, state, {
319
+ code: "SUBAGENT_EXEC_ERROR",
320
+ message: `${shared.terms.provider} exec ended before completion`,
321
+ timeoutMs: null,
322
+ exitCode: null,
323
+ }, state.finalMessageCandidate);
324
+ }
325
+ const finalMessage = state.doneEvent?.finalMessage ??
326
+ completionOutcome?.finalMessage ??
327
+ state.finalMessageCandidate ??
328
+ renderOutcomeCapturedText(state.outputBuffer).trim();
329
+ if (!state.session) {
330
+ return failWithError(shared, state, {
331
+ code: "SUBAGENT_EXEC_ERROR",
332
+ message: `${shared.terms.provider} exec ended without a session handle`,
333
+ timeoutMs: null,
334
+ exitCode: null,
335
+ }, finalMessage);
336
+ }
337
+ return completeOutcome(state, finalMessage);
234
338
  }
235
- const completionResult = await live.completion.then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
236
- const completionOutcome = completionResult.ok ? completionResult.value : undefined;
237
- // Outcome classification is the terminal publication boundary. A
238
- // controlled abort may let the provider run settle before its graceful
239
- // cleanup promise does, so prove that shared settlement before publishing
240
- // dismissed, failed-for-another-reason, or completed truth.
241
- const gracefulResult = shared.gracefulAbort
242
- ? await shared.gracefulAbort.then(() => ({ ok: true }), (error) => ({ ok: false, error }))
243
- : undefined;
244
- if (gracefulResult && !gracefulResult.ok) {
245
- return classifySettlementFailure(shared, state, gracefulResult.error);
246
- }
247
- if (state.failEvent) {
248
- return failFromFailEvent(shared, state, state.failEvent);
249
- }
250
- if (!completionResult.ok) {
251
- return classifyFailure(shared, state, completionResult.error, idleTimedOut);
252
- }
253
- if (shared.lifecycleAbort.signal.aborted) {
254
- return classifyFailure(shared, state, shared.lifecycleAbort.signal.reason, idleTimedOut);
255
- }
256
- if (!state.doneEvent && !completionOutcome) {
257
- return failWithError(shared, state, {
258
- code: "SUBAGENT_EXEC_ERROR",
259
- message: `${shared.terms.provider} exec ended before completion`,
260
- timeoutMs: null,
261
- exitCode: null,
262
- }, state.finalMessageCandidate);
263
- }
264
- const finalMessage = state.doneEvent?.finalMessage ??
265
- completionOutcome?.finalMessage ??
266
- state.finalMessageCandidate ??
267
- renderOutcomeCapturedText(state.outputBuffer).trim();
268
- const session = state.doneEvent?.session ?? completionOutcome?.session;
269
- if (!session) {
270
- return failWithError(shared, state, {
271
- code: "SUBAGENT_EXEC_ERROR",
272
- message: `${shared.terms.provider} exec ended without a session handle`,
273
- timeoutMs: null,
274
- exitCode: null,
275
- }, finalMessage);
276
- }
277
- return completeOutcome(shared, state, finalMessage, session);
278
- }
279
- catch (error) {
280
- if (error instanceof FlowError) {
281
- throw error;
339
+ catch (error) {
340
+ if (error instanceof FlowError) {
341
+ throw error;
342
+ }
343
+ return classifyFailure(shared, state, error, idleTimedOut, transportFailure, policyBudgetExceeded);
282
344
  }
283
- return classifyFailure(shared, state, error, idleTimedOut);
345
+ })();
346
+ providerSettlement.catch(() => undefined);
347
+ try {
348
+ const settled = await Promise.race([
349
+ providerSettlement.then((outcome) => ({ kind: "provider", outcome })),
350
+ policyBudget.then(() => ({ kind: "policy-budget" })),
351
+ ]);
352
+ return settled.kind === "provider"
353
+ ? settled.outcome
354
+ : classifyFailure(shared, state, undefined, idleTimedOut, transportFailure, true);
284
355
  }
285
356
  finally {
286
357
  if (idleTimeout)
287
358
  clearTimeout(idleTimeout);
359
+ if (executionTimeout)
360
+ clearTimeout(executionTimeout);
288
361
  shared.lifecycleAbort.signal.removeEventListener("abort", requestLiveAbort);
289
362
  shared.terminal = true;
290
363
  resolveForceWaiters(shared, undefined);
@@ -1,5 +1,5 @@
1
1
  import { getProviderRegistration } from "../providers/registry.js";
2
- export async function startLiveProjection(terms, signal, reviveTerms, onIngress, observer) {
2
+ export async function startLiveProjection(terms, signal, reviveTerms, onIngress, observer, onSessionAdmission) {
3
3
  const onSyncCheckpoint = observer
4
4
  ? (control, checkpoint) => {
5
5
  observer.onCheckpoint(control, checkpoint);
@@ -19,6 +19,7 @@ export async function startLiveProjection(terms, signal, reviveTerms, onIngress,
19
19
  reviveTerms,
20
20
  onSyncCheckpoint,
21
21
  onDeliveryOpportunity,
22
+ onSessionAdmission,
22
23
  onIngress,
23
24
  observer,
24
25
  });
@@ -198,6 +198,7 @@ export async function startClaudeAgentSdk(prompt, cwd, options) {
198
198
  queryActive = true;
199
199
  notifyActive();
200
200
  let resumedSessionId = sessionId;
201
+ let admittedSessionId;
201
202
  let finalMessage;
202
203
  let latestAssistantMessageModel;
203
204
  // Provider acceptance / pre-terminal retry evidence for no-turn classification.
@@ -207,7 +208,18 @@ export async function startClaudeAgentSdk(prompt, cwd, options) {
207
208
  const seenToolUses = new Map();
208
209
  for await (const event of query) {
209
210
  builder.ingress(CLAUDE_AGENT_SDK_PROVIDER, event.type, event, claudeIngressDisposition(event.type), () => {
210
- resumedSessionId = extractResumeToken(event) ?? resumedSessionId;
211
+ const providerSessionId = extractResumeToken(event);
212
+ resumedSessionId = providerSessionId ?? resumedSessionId;
213
+ if (providerSessionId && providerSessionId !== admittedSessionId) {
214
+ admittedSessionId = providerSessionId;
215
+ options.onSessionAdmission?.({
216
+ session: {
217
+ provider: CLAUDE_AGENT_SDK_PROVIDER,
218
+ version: SUBAGENT_SESSION_VERSION,
219
+ handle: { sessionId: providerSessionId },
220
+ },
221
+ });
222
+ }
211
223
  if (isClaudeApiRetryEvent(event)) {
212
224
  lastPreTerminalEvidenceWasRetry = true;
213
225
  builder.emit({ type: "warning", warning: mapClaudeApiRetryWarning(event) });
@@ -179,7 +179,7 @@ export const claudeAgentSdkRegistration = {
179
179
  return loader ? { ...terms, loader } : terms;
180
180
  },
181
181
  nativePolicyOwnership: () => null,
182
- startLive: async ({ terms, signal, reviveTerms, onSyncCheckpoint, onIngress }) => {
182
+ startLive: async ({ terms, signal, reviveTerms, onSyncCheckpoint, onSessionAdmission, onIngress }) => {
183
183
  if (terms.provider !== CLAUDE_AGENT_SDK_PROVIDER) {
184
184
  throw new Error(`startLive expected ${CLAUDE_AGENT_SDK_PROVIDER}`);
185
185
  }
@@ -197,6 +197,7 @@ export const claudeAgentSdkRegistration = {
197
197
  systemPrompt: claudeTerms.systemPrompt,
198
198
  onSyncCheckpoint,
199
199
  onEvidence: claudeTerms.onEvidence,
200
+ onSessionAdmission,
200
201
  onIngress,
201
202
  initialTurn: claudeTerms.initialTurn,
202
203
  loader: claudeTerms.loader,
@@ -13,6 +13,7 @@ function toCodexAppServerEffort(effort) {
13
13
  }
14
14
  export { codexAppServerSessionSchema } from "./session-schema.js";
15
15
  const MINIMUM_CODEX_APP_SERVER_VERSION = { major: 0, minor: 143, patch: 0 };
16
+ const CODEX_APP_SERVER_PROBE_TIMEOUT_MS = 1_000;
16
17
  /** Non-interactive defaults for turn/start (faye #316 policy A). */
17
18
  export const CODEX_APP_SERVER_TURN_APPROVAL_POLICY = "never";
18
19
  export { buildCodexAppServerWorkspaceWriteSandboxPolicy, configOwnsApprovalPolicy, configOwnsSandboxPolicy, } from "./policy.js";
@@ -172,10 +173,41 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
172
173
  void drainPendingTells();
173
174
  });
174
175
  };
176
+ const probe = async () => {
177
+ if (!server || !activeThreadId) {
178
+ throw createProviderFailure(`${CODEX_APP_SERVER_PROVIDER} session probe has no active thread`, {
179
+ code: "SUBAGENT_TRANSPORT_ERROR",
180
+ timeoutMs: CODEX_APP_SERVER_PROBE_TIMEOUT_MS,
181
+ });
182
+ }
183
+ let timer;
184
+ const deadline = new Promise((_, reject) => {
185
+ timer = setTimeout(() => reject(new Error("session probe timed out")), CODEX_APP_SERVER_PROBE_TIMEOUT_MS);
186
+ timer.unref?.();
187
+ });
188
+ try {
189
+ await Promise.race([
190
+ server.request("thread/read", { threadId: activeThreadId }),
191
+ deadline,
192
+ ]);
193
+ }
194
+ catch (error) {
195
+ throw createProviderFailure(`${CODEX_APP_SERVER_PROVIDER} session transport probe failed`, {
196
+ code: "SUBAGENT_TRANSPORT_ERROR",
197
+ timeoutMs: CODEX_APP_SERVER_PROBE_TIMEOUT_MS,
198
+ cause: error,
199
+ });
200
+ }
201
+ finally {
202
+ if (timer)
203
+ clearTimeout(timer);
204
+ }
205
+ };
175
206
  return createProjectionDriver({
176
207
  steerMode: "mid-execution",
177
208
  initialTurn: options.initialTurn,
178
209
  enqueue,
210
+ probe,
179
211
  onIngress: options.onIngress,
180
212
  async abort(mode) {
181
213
  interruptRequested = true;
@@ -272,6 +304,15 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
272
304
  builder.emit({ type: "fail", message });
273
305
  throw createProviderFailure(message);
274
306
  }
307
+ const admittedSession = {
308
+ provider: "codex-app-server",
309
+ version: SUBAGENT_SESSION_VERSION,
310
+ handle: {
311
+ threadId: activeThreadId,
312
+ ...(serverCodexHome ? { codexHome: serverCodexHome } : {}),
313
+ },
314
+ };
315
+ options.onSessionAdmission?.({ session: admittedSession });
275
316
  if (options.onEvidence)
276
317
  emitProviderEvidence(options.onEvidence, { type: "session", provider: CODEX_APP_SERVER_PROVIDER, threadId: activeThreadId }, builderDiagnostic);
277
318
  // Policy A defaults, but omit when config.* already owns that surface (thread/start config wins).
@@ -319,17 +360,9 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
319
360
  throw createProviderFailure(message);
320
361
  }
321
362
  // Successful turn/completed is consumption evidence; interrupt ack alone is not terminal proof.
322
- const session = {
323
- provider: "codex-app-server",
324
- version: SUBAGENT_SESSION_VERSION,
325
- handle: {
326
- threadId: activeThreadId,
327
- ...(serverCodexHome ? { codexHome: serverCodexHome } : {}),
328
- },
329
- };
330
363
  const finalMessage = turnState.lastAgentMessage;
331
- builder.emit({ type: "done", finalMessage, session });
332
- return { finalMessage, session };
364
+ builder.emit({ type: "done", finalMessage, session: admittedSession });
365
+ return { finalMessage, session: admittedSession };
333
366
  }
334
367
  finally {
335
368
  activeTurnCompletion = undefined;
@@ -22,6 +22,11 @@ export function codexAppServerNotificationDisposition(method, params) {
22
22
  return "acknowledged-unpersisted";
23
23
  if (CODEX_APP_SERVER_NOTIFICATION_MAPPED.has(method))
24
24
  return "mapped";
25
+ if (method === "item/started") {
26
+ const item = appServerRecord(appServerRecord(params)?.item);
27
+ if (item && typeof item.type === "string" && isCodexAppServerActivityItemType(item.type))
28
+ return "mapped";
29
+ }
25
30
  if (method === "account/rateLimits/updated" && codexAppServerRateLimitCause(params))
26
31
  return "mapped";
27
32
  return "unknown";
@@ -303,6 +308,24 @@ export function createCodexAppServerTurnMonitor(options) {
303
308
  });
304
309
  }
305
310
  }
311
+ if (notification.method === "item/started") {
312
+ const item = notification.params?.item;
313
+ if (!item)
314
+ throw new Error(`${CODEX_APP_SERVER_PROVIDER} item/started lacked required item`);
315
+ const id = coerceString(item.id);
316
+ const itemType = coerceString(item.type);
317
+ if (!id || !itemType)
318
+ throw new Error(`${CODEX_APP_SERVER_PROVIDER} item/started lacked required item id or type`);
319
+ if (isCodexAppServerActivityItemType(itemType)) {
320
+ builder.emit({
321
+ type: "activity",
322
+ phase: "started",
323
+ id,
324
+ tool: itemType,
325
+ call: appServerActivityCall(itemType, item),
326
+ });
327
+ }
328
+ }
306
329
  if (notification.method === "turn/started") {
307
330
  const turn = notification.params?.turn;
308
331
  const turnId = recordActiveTurnId(state, turn?.id, "notification");
@@ -212,7 +212,7 @@ export const codexAppServerRegistration = {
212
212
  webSearch: Object.freeze(webSearch),
213
213
  });
214
214
  },
215
- startLive: async ({ terms, signal, reviveTerms, onSyncCheckpoint, onDeliveryOpportunity, onIngress, }) => {
215
+ startLive: async ({ terms, signal, reviveTerms, onSyncCheckpoint, onDeliveryOpportunity, onSessionAdmission, onIngress, }) => {
216
216
  if (terms.provider !== CODEX_APP_SERVER_PROVIDER) {
217
217
  throw new Error(`startLive expected ${CODEX_APP_SERVER_PROVIDER}`);
218
218
  }
@@ -231,6 +231,7 @@ export const codexAppServerRegistration = {
231
231
  onSyncCheckpoint,
232
232
  onDeliveryOpportunity,
233
233
  onEvidence: codexTerms.onEvidence,
234
+ onSessionAdmission,
234
235
  onIngress,
235
236
  initialTurn: codexTerms.initialTurn,
236
237
  };
@@ -264,6 +264,13 @@ export async function startOpencode(prompt, cwd, options = {}) {
264
264
  builder.emit({ type: "fail", message });
265
265
  throw createProviderFailure(message);
266
266
  }
267
+ options.onSessionAdmission?.({
268
+ session: {
269
+ provider: OPENCODE_SDK_PROVIDER,
270
+ version: SUBAGENT_SESSION_VERSION,
271
+ handle: { sessionId: activeSessionId },
272
+ },
273
+ });
267
274
  idleMonitor = new OpencodeIdleMonitor(sdk.client.v2.session, activeSessionId, combined.signal);
268
275
  eventController = createOpencodeEventController({
269
276
  builder,
@@ -104,7 +104,7 @@ export const opencodeSdkRegistration = {
104
104
  return loader ? { ...terms, loader } : terms;
105
105
  },
106
106
  nativePolicyOwnership: () => null,
107
- startLive: async ({ terms, signal, reviveTerms, onSyncCheckpoint, onDeliveryOpportunity, onIngress, }) => {
107
+ startLive: async ({ terms, signal, reviveTerms, onSyncCheckpoint, onDeliveryOpportunity, onSessionAdmission, onIngress, }) => {
108
108
  if (terms.provider !== OPENCODE_SDK_PROVIDER) {
109
109
  throw new Error(`startLive expected ${OPENCODE_SDK_PROVIDER}`);
110
110
  }
@@ -118,6 +118,7 @@ export const opencodeSdkRegistration = {
118
118
  onSyncCheckpoint,
119
119
  onDeliveryOpportunity,
120
120
  onEvidence: opencodeTerms.onEvidence,
121
+ onSessionAdmission,
121
122
  onIngress,
122
123
  initialTurn: opencodeTerms.initialTurn,
123
124
  loader: opencodeTerms.loader,
@@ -1,5 +1,6 @@
1
1
  import { FlowError } from "../../../flow-error.js";
2
2
  import { appendDebugLog } from "../../../telemetry/debug-log.js";
3
+ import { SUBAGENT_SESSION_VERSION, } from "../../session.js";
3
4
  import { createProjectionDriver } from "../../harness/execution-handle.js";
4
5
  import { createProviderFailure, toProviderTerminalError, } from "../../harness/provider-adapter.js";
5
6
  import { createAbortError, emitProviderEvidence, sha256Utf8 } from "../../harness/runtime.js";
@@ -103,6 +104,15 @@ async function openPiProjectionDriver(prompt, cwd, options) {
103
104
  ...(resourceLoader ? { resourceLoader } : {}),
104
105
  });
105
106
  const session = created.session;
107
+ if (session.sessionFile) {
108
+ options.onSessionAdmission?.({
109
+ session: {
110
+ provider: PI_PROVIDER,
111
+ version: SUBAGENT_SESSION_VERSION,
112
+ handle: { sessionId: session.sessionId, sessionFile: session.sessionFile },
113
+ },
114
+ });
115
+ }
106
116
  if (options.onEvidence)
107
117
  emitProviderEvidence(options.onEvidence, { type: "session", provider: PI_PROVIDER, sessionId: session.sessionId, ...(session.sessionFile ? { sessionFile: session.sessionFile } : {}) }, reportEvidenceDiagnostic);
108
118
  session.setSteeringMode("one-at-a-time");
@@ -142,14 +142,15 @@ function piUsageBody(event) {
142
142
  if (message?.role !== "assistant")
143
143
  return undefined;
144
144
  const usage = message.usage;
145
- if (!usage || typeof usage.input !== "number" || typeof usage.output !== "number")
145
+ if (!usage || typeof usage.input !== "number" || !Number.isFinite(usage.input)
146
+ || typeof usage.output !== "number" || !Number.isFinite(usage.output))
146
147
  return undefined;
147
148
  return {
148
149
  type: "usage",
149
150
  inputTokens: usage.input,
150
151
  outputTokens: usage.output,
151
- ...(typeof usage.cacheRead === "number" ? { cachedInputTokens: usage.cacheRead } : {}),
152
- ...(typeof usage.cost?.total === "number" ? { costUsd: usage.cost.total } : {}),
152
+ ...(typeof usage.cacheRead === "number" && Number.isFinite(usage.cacheRead) ? { cachedInputTokens: usage.cacheRead } : {}),
153
+ ...(typeof usage.cost?.total === "number" && Number.isFinite(usage.cost.total) ? { costUsd: usage.cost.total } : {}),
153
154
  ...(typeof message.responseModel === "string"
154
155
  ? { model: message.responseModel }
155
156
  : typeof message.model === "string"