@astrosheep/keiyaku 2.9.8 → 2.9.10

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 (230) hide show
  1. package/README.md +50 -182
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/harness/control-types.js +18 -1
  4. package/build/agents/harness/index.js +2 -0
  5. package/build/agents/harness/outcome-error.js +81 -0
  6. package/build/agents/harness/projection.js +10 -7
  7. package/build/agents/harness/router.js +5 -1
  8. package/build/agents/providers/claude-agent-sdk/adapter.js +18 -4
  9. package/build/agents/providers/claude-agent-sdk/registration.js +11 -1
  10. package/build/agents/providers/claude-agent-sdk/session.js +19 -5
  11. package/build/agents/providers/codex-app-server/adapter.js +11 -6
  12. package/build/agents/providers/codex-app-server/events.js +67 -6
  13. package/build/agents/providers/codex-app-server/registration.js +11 -1
  14. package/build/agents/providers/opencode-sdk/adapter.js +8 -1
  15. package/build/agents/providers/opencode-sdk/events.js +24 -2
  16. package/build/agents/providers/opencode-sdk/registration.js +11 -1
  17. package/build/agents/providers/pi/checkpoint.js +9 -1
  18. package/build/agents/providers/pi/events.js +2 -0
  19. package/build/agents/providers/pi/registration.js +14 -1
  20. package/build/cli/commands/akuma/akuma/handler.js +1 -1
  21. package/build/cli/commands/akuma/akuma/meta.js +3 -3
  22. package/build/cli/commands/akuma/catalog.js +2 -2
  23. package/build/cli/commands/akuma/{view → show}/handler.js +5 -5
  24. package/build/cli/commands/akuma/{view → show}/meta.js +3 -3
  25. package/build/cli/commands/akuma.js +1 -1
  26. package/build/cli/commands/contract/amend/handler.js +5 -4
  27. package/build/cli/commands/contract/arc/handler.js +5 -4
  28. package/build/cli/commands/contract/audit/handler.js +2 -2
  29. package/build/cli/commands/contract/bind/handler.js +12 -3
  30. package/build/cli/commands/contract/bind/meta.js +45 -15
  31. package/build/cli/commands/contract/forfeit/handler.js +9 -5
  32. package/build/cli/commands/contract/log/handler.js +2 -2
  33. package/build/cli/commands/contract/petition/handler.js +34 -7
  34. package/build/cli/commands/contract/renew/handler.js +7 -7
  35. package/build/cli/commands/contract/renew/meta.js +3 -3
  36. package/build/cli/commands/metadata.js +10 -6
  37. package/build/cli/commands/projection/call/meta.js +1 -1
  38. package/build/cli/commands/projection/catalog.js +2 -0
  39. package/build/cli/commands/projection/history/handler.js +21 -0
  40. package/build/cli/commands/projection/history/meta.js +9 -0
  41. package/build/cli/commands/projection/shared.js +3 -4
  42. package/build/cli/commands/projection/status/handler.js +3 -3
  43. package/build/cli/commands/projection/wait/handler.js +20 -11
  44. package/build/cli/commands/task/add/handler.js +14 -5
  45. package/build/cli/commands/task/add/meta.js +2 -2
  46. package/build/cli/commands/task/catalog.js +4 -0
  47. package/build/cli/commands/task/done/handler.js +1 -4
  48. package/build/cli/commands/task/done/meta.js +1 -1
  49. package/build/cli/commands/task/drop/meta.js +1 -1
  50. package/build/cli/commands/task/hold/handler.js +3 -0
  51. package/build/cli/commands/task/hold/meta.js +8 -0
  52. package/build/cli/commands/task/log/handler.js +1 -1
  53. package/build/cli/commands/task/log/meta.js +2 -2
  54. package/build/cli/commands/task/resume/handler.js +3 -0
  55. package/build/cli/commands/task/resume/meta.js +8 -0
  56. package/build/cli/commands/task/shared.js +39 -14
  57. package/build/cli/commands/task/show/handler.js +1 -1
  58. package/build/cli/commands/task/show/meta.js +1 -1
  59. package/build/cli/commands/task/start/meta.js +1 -1
  60. package/build/cli/commands/task/stop/handler.js +1 -4
  61. package/build/cli/commands/task/stop/meta.js +1 -1
  62. package/build/cli/commands/task/update/handler.js +11 -3
  63. package/build/cli/commands/task/update/meta.js +3 -3
  64. package/build/cli/completion.js +3 -2
  65. package/build/cli/flags.js +51 -14
  66. package/build/cli/help.js +4 -1
  67. package/build/cli/index.js +40 -25
  68. package/build/cli/parse-flags.js +40 -7
  69. package/build/cli/parse-metadata.js +5 -2
  70. package/build/cli/parse-selectors.js +2 -3
  71. package/build/cli/parse.js +40 -6
  72. package/build/cli/projection-address.js +12 -10
  73. package/build/cli/render/amendment-warning.js +8 -0
  74. package/build/cli/render/arc.js +6 -2
  75. package/build/cli/render/audit.js +400 -81
  76. package/build/cli/render/kanshi.js +242 -79
  77. package/build/cli/render/lifecycle.js +56 -0
  78. package/build/cli/render/misc.js +24 -28
  79. package/build/cli/render/petition.js +5 -0
  80. package/build/cli/render/projection-activity.js +65 -29
  81. package/build/cli/render/projection-history.js +64 -0
  82. package/build/cli/render/shared.js +23 -9
  83. package/build/cli/render/status.js +13 -35
  84. package/build/cli/render/task.js +51 -26
  85. package/build/cli/render/terminal-failure.js +3 -1
  86. package/build/cli/render/wait.js +45 -69
  87. package/build/cli/subagent-guard.js +1 -1
  88. package/build/cli/types.js +1 -1
  89. package/build/config/env-keys.js +3 -1
  90. package/build/config/env.js +24 -1
  91. package/build/config/settings/schema.js +3 -1
  92. package/build/core/address-carrier.js +89 -0
  93. package/build/core/addressing.js +255 -196
  94. package/build/core/amend.js +115 -38
  95. package/build/core/amendment-criteria.js +17 -0
  96. package/build/core/amendment-warning.js +10 -0
  97. package/build/core/arc.js +184 -83
  98. package/build/core/audit/candidate.js +110 -9
  99. package/build/core/audit/coordinates.js +120 -65
  100. package/build/core/audit/evidence.js +19 -23
  101. package/build/core/audit/facade.js +34 -11
  102. package/build/core/audit/report.js +5 -5
  103. package/build/core/bind-reconciliation.js +390 -0
  104. package/build/core/bind-workspace.js +12 -20
  105. package/build/core/bind.js +242 -273
  106. package/build/core/call/call.js +1 -0
  107. package/build/core/call/context.js +46 -71
  108. package/build/core/call/prompt.js +6 -3
  109. package/build/core/context.js +49 -35
  110. package/build/core/contract-carrier-runtime.js +408 -0
  111. package/build/core/contract-carrier.js +270 -0
  112. package/build/core/contract-view.js +33 -27
  113. package/build/core/contract.js +5 -1
  114. package/build/core/derived-replay.js +207 -0
  115. package/build/core/draft.js +125 -57
  116. package/build/core/forfeit.js +113 -56
  117. package/build/core/identifier-slug.js +63 -13
  118. package/build/core/ids.js +4 -4
  119. package/build/core/lifecycle-history.js +35 -0
  120. package/build/core/lifecycle-recovery.js +117 -0
  121. package/build/core/lifecycle-runner.js +377 -0
  122. package/build/core/log.js +37 -11
  123. package/build/core/outcome-base.js +181 -0
  124. package/build/core/process-group.js +139 -0
  125. package/build/core/projection/generation/database.js +82 -7
  126. package/build/core/projection/generation/model.js +9 -36
  127. package/build/core/projection/generation/projection-generation-continuation.js +10 -22
  128. package/build/core/projection/generation/projection-generation-execution.js +13 -6
  129. package/build/core/projection/index.js +5 -4
  130. package/build/core/projection/projection-activity.js +29 -8
  131. package/build/core/projection/projection-alias.js +23 -10
  132. package/build/core/projection/projection-core.js +21 -7
  133. package/build/core/projection/projection-execution-observer.js +91 -39
  134. package/build/core/projection/projection-history.js +474 -0
  135. package/build/core/projection/projection-runner-lock.js +12 -0
  136. package/build/core/projection/projection-status-observation.js +365 -0
  137. package/build/core/projection/projection-status.js +35 -337
  138. package/build/core/projection/projection-terminal-failure.js +13 -8
  139. package/build/core/projection/projection-wait.js +7 -2
  140. package/build/core/projection/tell/launch-store.js +244 -0
  141. package/build/core/projection/tell/model.js +3 -3
  142. package/build/core/projection/tell/store.js +70 -118
  143. package/build/core/registry.js +22 -87
  144. package/build/core/renew-build.js +424 -0
  145. package/build/core/renew-plan.js +239 -0
  146. package/build/core/renew-rewrite.js +77 -0
  147. package/build/core/renew.js +295 -520
  148. package/build/core/repository-ledger/accepted-fold-read.js +19 -0
  149. package/build/core/repository-ledger/accepted-tail.js +116 -0
  150. package/build/core/repository-ledger/admission.js +38 -0
  151. package/build/core/repository-ledger/authoritative-transaction.js +1 -0
  152. package/build/core/repository-ledger/claim-evidence.js +147 -0
  153. package/build/core/repository-ledger/codec.js +297 -0
  154. package/build/core/repository-ledger/current-state-store.js +853 -0
  155. package/build/core/repository-ledger/fold-repository.js +375 -0
  156. package/build/core/repository-ledger/fold.js +248 -0
  157. package/build/core/repository-ledger/git-object-store.js +243 -0
  158. package/build/core/repository-ledger/history-read.js +15 -0
  159. package/build/core/repository-ledger/incremental-read.js +153 -0
  160. package/build/core/repository-ledger/index.js +15 -0
  161. package/build/core/repository-ledger/inventory-plan.js +75 -0
  162. package/build/core/repository-ledger/inventory.js +310 -0
  163. package/build/core/repository-ledger/publication-admission.js +125 -0
  164. package/build/core/repository-ledger/publication.js +353 -0
  165. package/build/core/repository-ledger/read-model.js +469 -0
  166. package/build/core/repository-ledger/transaction-membership.js +78 -0
  167. package/build/core/repository-ledger/traversal.js +404 -0
  168. package/build/core/repository-ledger/write-transaction.js +339 -0
  169. package/build/core/seal.js +168 -161
  170. package/build/core/settlement/claim-delivery.js +42 -215
  171. package/build/core/settlement/claim.js +235 -153
  172. package/build/core/settlement/index.js +4 -4
  173. package/build/core/settlement/petition-claim-gates.js +52 -33
  174. package/build/core/settlement/petition-forfeit.js +12 -5
  175. package/build/core/settlement/petition-head-guard.js +8 -5
  176. package/build/core/settlement/petition-preview.js +68 -56
  177. package/build/core/settlement/petition-window.js +50 -0
  178. package/build/core/settlement/petition.js +252 -131
  179. package/build/core/settlement/queue-read-model.js +72 -95
  180. package/build/core/settlement/queue-seat-allocation.js +35 -126
  181. package/build/core/settlement/queue.js +1 -1
  182. package/build/core/settlement/settlement.js +172 -80
  183. package/build/core/settlement/verdict.js +112 -66
  184. package/build/core/settlement/verification-coordination.js +305 -0
  185. package/build/core/settlement/verification-supervisor.js +275 -0
  186. package/build/core/settlement/verification.js +398 -31
  187. package/build/core/status/board.js +125 -127
  188. package/build/core/status/lifecycle.js +7 -80
  189. package/build/core/structured-query.js +23 -0
  190. package/build/core/target-ref.js +1 -1
  191. package/build/core/task/board.js +59 -24
  192. package/build/core/task/commands.js +181 -33
  193. package/build/core/task/coordinate.js +56 -0
  194. package/build/core/task/document.js +25 -9
  195. package/build/core/task/index.js +7 -4
  196. package/build/core/task/query.js +17 -27
  197. package/build/core/task/settlement-git.js +164 -179
  198. package/build/core/task/settlement-policy.js +15 -11
  199. package/build/core/task/source-board.js +31 -13
  200. package/build/core/task/task-bind-preparation.js +193 -0
  201. package/build/core/task/task-contract.js +102 -193
  202. package/build/core/task/task-file-transaction.js +171 -0
  203. package/build/core/task/task-git-runtime.js +76 -14
  204. package/build/core/task/task-git-store.js +68 -169
  205. package/build/core/task/task-store-repository.js +13 -6
  206. package/build/core/task/task-worktree.js +6 -33
  207. package/build/core/task/task.js +1 -1
  208. package/build/core/task/tree.js +3 -3
  209. package/build/core/task/validation.js +4 -3
  210. package/build/core/transcripts.js +93 -1
  211. package/build/core/worktree-bootstrap.js +44 -36
  212. package/build/core/worktree-path.js +210 -77
  213. package/build/flow-error.js +7 -7
  214. package/build/generated/version.js +2 -2
  215. package/build/git/core.js +76 -13
  216. package/build/git/refs.js +168 -46
  217. package/build/git/streaming-batch.js +73 -13
  218. package/package.json +1 -1
  219. package/skills/keiyaku/SKILL.md +1 -1
  220. package/skills/keiyaku-akuma/SKILL.md +3 -3
  221. package/skills/keiyaku-task/SKILL.md +4 -2
  222. package/skills/keiyaku-workflow/SKILL.md +88 -203
  223. package/build/core/entry.js +0 -305
  224. package/build/core/ledger-batch.js +0 -194
  225. package/build/core/ledger.js +0 -84
  226. package/build/core/projection/leash.js +0 -87
  227. package/build/core/ref-log.js +0 -119
  228. package/build/core/renew-session.js +0 -128
  229. package/build/core/status/ledger-batch.js +0 -1
  230. package/build/core/status/reconciliation.js +0 -137
@@ -1 +1,18 @@
1
- export {};
1
+ /**
2
+ * Adapters call this only with durable admissions their native event proved.
3
+ * A duplicate tell coordinate is ambiguous and intentionally yields no receipt.
4
+ */
5
+ export function exactLaunchInputReceipts(stage, fence, admissions) {
6
+ const counts = new Map();
7
+ for (const admission of admissions) {
8
+ for (const tellId of admission.tellIds)
9
+ counts.set(tellId, (counts.get(tellId) ?? 0) + 1);
10
+ }
11
+ if ([...counts.values()].some((count) => count !== 1))
12
+ return [];
13
+ return [...counts.keys()].sort().map((tellId) => ({
14
+ stage,
15
+ correlation: { kind: "exact", tellId },
16
+ launchInput: { fence },
17
+ }));
18
+ }
@@ -1,8 +1,10 @@
1
1
  export { activityEventBodySchema, AGENT_EVENT_TEXT_MAX_CHARS, AGENT_UNKNOWN_PAYLOAD_MAX_CHARS, } from "./events.js";
2
2
  export { EXEC_EVENT_BUFFER_HARD_CAP } from "./event-channel.js";
3
3
  export { createAbortError } from "./runtime.js";
4
+ export { exactLaunchInputReceipts } from "./control-types.js";
4
5
  export { createProviderFailure, isProviderTerminalError, ProviderTerminalError, toProviderTerminalError, } from "./provider-adapter.js";
5
6
  export { restoreOutcome } from "./outcome.js";
7
+ export { OutcomeErrorValidationError, parseCurrentOutcomeError, } from "./outcome-error.js";
6
8
  export { call, revive } from "./projection.js";
7
9
  export { isRecoverableSessionEnvelope } from "./session.js";
8
10
  export { OUTCOME_ERROR_VERSION, SubagentFailure } from "../types.js";
@@ -0,0 +1,81 @@
1
+ import { OUTCOME_ERROR_VERSION, SUBAGENT_FAILURE_CODES, } from "../types.js";
2
+ const OUTCOME_ERROR_CODES = new Set(SUBAGENT_FAILURE_CODES);
3
+ const OUTCOME_ERROR_KEYS = new Set([
4
+ "version",
5
+ "code",
6
+ "message",
7
+ "timeoutMs",
8
+ "exitCode",
9
+ "publicCause",
10
+ ]);
11
+ export class OutcomeErrorValidationError extends Error {
12
+ incompatible;
13
+ constructor(message, incompatible = false) {
14
+ super(message);
15
+ this.incompatible = incompatible;
16
+ this.name = "OutcomeErrorValidationError";
17
+ }
18
+ }
19
+ function isPlainObject(value) {
20
+ if (value === null || typeof value !== "object" || Array.isArray(value))
21
+ return false;
22
+ const prototype = Object.getPrototypeOf(value);
23
+ return prototype === Object.prototype || prototype === null;
24
+ }
25
+ function isJsonCompatible(value, ancestors) {
26
+ if (value === null || typeof value === "boolean" || typeof value === "string")
27
+ return true;
28
+ if (typeof value === "number")
29
+ return Number.isFinite(value);
30
+ if (typeof value !== "object")
31
+ return false;
32
+ if (ancestors.has(value))
33
+ return false;
34
+ ancestors.add(value);
35
+ try {
36
+ if (Array.isArray(value))
37
+ return value.every((item) => isJsonCompatible(item, ancestors));
38
+ return isPlainObject(value) && Object.values(value).every((item) => isJsonCompatible(item, ancestors));
39
+ }
40
+ finally {
41
+ ancestors.delete(value);
42
+ }
43
+ }
44
+ /** Parses the one current persisted OutcomeError schema at the harness boundary. */
45
+ export function parseCurrentOutcomeError(value, location) {
46
+ const error = isPlainObject(value) ? value : undefined;
47
+ const version = error?.version;
48
+ if (!error || version !== OUTCOME_ERROR_VERSION) {
49
+ throw new OutcomeErrorValidationError(`${location} has incompatible version ${version === undefined ? "missing" : String(version)}; expected ${OUTCOME_ERROR_VERSION}`, true);
50
+ }
51
+ if (Object.keys(error).some((key) => !OUTCOME_ERROR_KEYS.has(key))) {
52
+ throw new OutcomeErrorValidationError(`${location} is not the current closed OutcomeError shape`);
53
+ }
54
+ if (typeof error.code !== "string" || !OUTCOME_ERROR_CODES.has(error.code)) {
55
+ throw new OutcomeErrorValidationError(`${location}.code is not a recognized subagent failure code`);
56
+ }
57
+ if (typeof error.message !== "string") {
58
+ throw new OutcomeErrorValidationError(`${location}.message must be a string`);
59
+ }
60
+ if (error.timeoutMs !== null && (typeof error.timeoutMs !== "number"
61
+ || !Number.isSafeInteger(error.timeoutMs)
62
+ || error.timeoutMs < 0)) {
63
+ throw new OutcomeErrorValidationError(`${location}.timeoutMs must be null or a non-negative safe integer`);
64
+ }
65
+ if (error.exitCode !== null && (typeof error.exitCode !== "number"
66
+ || !Number.isSafeInteger(error.exitCode))) {
67
+ throw new OutcomeErrorValidationError(`${location}.exitCode must be null or a safe integer`);
68
+ }
69
+ if (error.publicCause !== undefined && (!isPlainObject(error.publicCause)
70
+ || !isJsonCompatible(error.publicCause, new Set()))) {
71
+ throw new OutcomeErrorValidationError(`${location}.publicCause must be a JSON-compatible harness failure cause`);
72
+ }
73
+ return {
74
+ version: OUTCOME_ERROR_VERSION,
75
+ code: error.code,
76
+ message: error.message,
77
+ timeoutMs: error.timeoutMs,
78
+ exitCode: error.exitCode,
79
+ ...(error.publicCause === undefined ? {} : { publicCause: error.publicCause }),
80
+ };
81
+ }
@@ -91,6 +91,7 @@ function createProjectionState(terms, reviveTerms, options) {
91
91
  forceWaiters: new Set(),
92
92
  onLive() { },
93
93
  observer: options?.observer,
94
+ launchTellIds: options?.launchTellIds,
94
95
  };
95
96
  if (terms.signal) {
96
97
  const listener = () => {
@@ -239,12 +240,14 @@ async function settleProjection(shared) {
239
240
  }, shared.terms.idleTimeoutMs);
240
241
  idleTimeout.unref?.();
241
242
  };
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?.();
243
+ if (shared.terms.executionTimeoutMs > 0) {
244
+ executionTimeout = setTimeout(() => {
245
+ policyBudgetExceeded = true;
246
+ requestPolicyBudgetCleanup(shared, createAbortError(`${shared.terms.provider} execution policy budget expired`));
247
+ resolvePolicyBudget();
248
+ }, shared.terms.executionTimeoutMs);
249
+ executionTimeout.unref?.();
250
+ }
248
251
  const requestLiveAbort = () => {
249
252
  if (shared.explicitlyAborted)
250
253
  return;
@@ -259,7 +262,7 @@ async function settleProjection(shared) {
259
262
  const live = await startLiveProjection(shared.terms, shared.lifecycleAbort.signal, shared.reviveTerms, refreshIdleTimeout, shared.observer, (admission) => {
260
263
  recordSessionAdmission(state, admission.session);
261
264
  refreshIdleTimeout();
262
- });
265
+ }, shared.launchTellIds);
263
266
  liveDriver = live;
264
267
  shared.onLive(live);
265
268
  resolveForceWaiters(shared, live);
@@ -1,5 +1,5 @@
1
1
  import { getProviderRegistration } from "../providers/registry.js";
2
- export async function startLiveProjection(terms, signal, reviveTerms, onIngress, observer, onSessionAdmission) {
2
+ export async function startLiveProjection(terms, signal, reviveTerms, onIngress, observer, onSessionAdmission, launchTellIds) {
3
3
  const onSyncCheckpoint = observer
4
4
  ? (control, checkpoint) => {
5
5
  observer.onCheckpoint(control, checkpoint);
@@ -13,6 +13,9 @@ export async function startLiveProjection(terms, signal, reviveTerms, onIngress,
13
13
  }
14
14
  : undefined;
15
15
  const registration = getProviderRegistration(terms.provider);
16
+ if (observer && registration.tellReceiptCapabilities) {
17
+ await observer.onTellReceiptCapabilities(registration.tellReceiptCapabilities);
18
+ }
16
19
  return registration.startLive({
17
20
  terms,
18
21
  signal,
@@ -21,6 +24,7 @@ export async function startLiveProjection(terms, signal, reviveTerms, onIngress,
21
24
  onDeliveryOpportunity,
22
25
  onSessionAdmission,
23
26
  onIngress,
27
+ launchTellIds,
24
28
  observer,
25
29
  });
26
30
  }
@@ -1,6 +1,7 @@
1
1
  import { appendDebugLog } from "../../../telemetry/debug-log.js";
2
2
  import { SUBAGENT_SESSION_VERSION } from "../../session.js";
3
3
  import { createProjectionDriver } from "../../harness/execution-handle.js";
4
+ import { exactLaunchInputReceipts } from "../../harness/control-types.js";
4
5
  import { createProviderFailure } from "../../harness/provider-adapter.js";
5
6
  import { buildSubagentEnv, createAbortError, emitProviderEvidence, linkAbortSignal, sha256Utf8 } from "../../harness/runtime.js";
6
7
  import { claudeIngressDisposition, emitClaudeThoughts, emitClaudeToolResults, emitClaudeToolStarts, emitClaudeUsage, extractAssistantMessage, extractAssistantMessageId, extractAssistantMessageModel, extractOverloadFallbackWarning, extractResultText, extractResumeToken, isClaudeApiRetryEvent, isClaudeModelRefusalFallbackEvent, isClaudeModelRefusalNoFallbackEvent, mapClaudeApiRetryWarning, } from "./events.js";
@@ -93,6 +94,7 @@ export async function startClaudeAgentSdk(prompt, cwd, options) {
93
94
  const submitted = inputStream.push(toClaudeUserMessage(message.text));
94
95
  // submitted = NEXT source next() after this tell is yielded (not the yield pull itself).
95
96
  await submitted.waitSubmitted();
97
+ options.onTellReceipt?.({ stage: "seen", correlation: { kind: "exact", tellId: message.id } });
96
98
  emitTurnStarted?.();
97
99
  // 落账:记录提交时越过的 result ordinal(generation fence 落点)。
98
100
  const submittedAfterResultOrdinal = completedResultOrdinal;
@@ -143,10 +145,19 @@ export async function startClaudeAgentSdk(prompt, cwd, options) {
143
145
  const unlinkOuterAbort = linkAbortSignal(options.signal, queryAbortController, () => undefined);
144
146
  try {
145
147
  const initialMessage = toClaudeUserMessage(prompt);
146
- inputStream = createPushableUserMessageStream((message) => {
147
- if (!options.onEvidence)
148
+ inputStream = createPushableUserMessageStream(async (message) => {
149
+ if (message === initialMessage) {
150
+ // Claude's source pull is the provider-native launch-input fact.
151
+ // The adapter records its coordinate before asking heart to reduce
152
+ // the exact attributed receipt.
153
+ const fence = { kind: "ordinal", ordinal: 0 };
154
+ await options.onLaunchAdmission?.({ fence, tellIds: options.launchTellIds ?? [] });
155
+ const admissions = (options.listLaunchAdmissions?.() ?? []).filter((admission) => (admission.fence.kind === "ordinal" && admission.fence.ordinal === fence.ordinal));
156
+ for (const receipt of exactLaunchInputReceipts("seen", fence, admissions))
157
+ options.onTellReceipt?.(receipt);
148
158
  return;
149
- if (message === initialMessage)
159
+ }
160
+ if (!options.onEvidence)
150
161
  return;
151
162
  const content = message.message.content;
152
163
  const text = typeof content === "string"
@@ -286,7 +297,6 @@ export async function startClaudeAgentSdk(prompt, cwd, options) {
286
297
  bumpResultOrdinal();
287
298
  const checkpoint = {
288
299
  fence: { kind: "ordinal", ordinal: completedResultOrdinal },
289
- consumptionEvidence: true,
290
300
  };
291
301
  // Record the native result before the synchronous pump can
292
302
  // consume any tell fenced to an earlier result.
@@ -300,6 +310,10 @@ export async function startClaudeAgentSdk(prompt, cwd, options) {
300
310
  }
301
311
  // Sync checkpoint: claim inbox tells and deliver them via steer control.
302
312
  options.onSyncCheckpoint?.({ enqueue }, checkpoint);
313
+ const admissions = (options.listLaunchAdmissions?.() ?? []).filter((admission) => (admission.fence.kind === "ordinal" && admission.fence.ordinal < checkpoint.fence.ordinal));
314
+ for (const receipt of exactLaunchInputReceipts("consumed", checkpoint.fence, admissions))
315
+ options.onTellReceipt?.(receipt);
316
+ options.onTellReceipt?.({ stage: "consumed", correlation: { kind: "fence", fence: checkpoint.fence } });
303
317
  // Defer end so concurrent enqueue after this result can register openSteerCount.
304
318
  queueMicrotask(() => maybeEndInputStream());
305
319
  break;
@@ -179,7 +179,8 @@ export const claudeAgentSdkRegistration = {
179
179
  return loader ? { ...terms, loader } : terms;
180
180
  },
181
181
  nativePolicyOwnership: () => null,
182
- startLive: async ({ terms, signal, reviveTerms, onSyncCheckpoint, onSessionAdmission, onIngress }) => {
182
+ tellReceiptCapabilities: { seen: "exact", consumed: "fence" },
183
+ startLive: async ({ terms, signal, reviveTerms, onSyncCheckpoint, onSessionAdmission, onIngress, launchTellIds, observer }) => {
183
184
  if (terms.provider !== CLAUDE_AGENT_SDK_PROVIDER) {
184
185
  throw new Error(`startLive expected ${CLAUDE_AGENT_SDK_PROVIDER}`);
185
186
  }
@@ -196,6 +197,15 @@ export const claudeAgentSdkRegistration = {
196
197
  disallowedTools: claudeTerms.disallowedTools,
197
198
  systemPrompt: claudeTerms.systemPrompt,
198
199
  onSyncCheckpoint,
200
+ onTellReceipt: observer ? (receipt) => {
201
+ try {
202
+ observer.onTellReceipt(receipt);
203
+ }
204
+ catch { /* Receipt persistence cannot alter provider control. */ }
205
+ } : undefined,
206
+ launchTellIds,
207
+ onLaunchAdmission: observer ? (admission) => observer.onLaunchAdmission(admission) : undefined,
208
+ listLaunchAdmissions: observer ? () => observer.listLaunchAdmissions() : undefined,
199
209
  onEvidence: claudeTerms.onEvidence,
200
210
  onSessionAdmission,
201
211
  onIngress,
@@ -64,10 +64,20 @@ export function createPushableUserMessageStream(onYield) {
64
64
  pendingYieldAck.settleResolve();
65
65
  pendingYieldAck = undefined;
66
66
  };
67
- const yieldQueued = (entry) => {
67
+ const yieldQueued = async (entry) => {
68
68
  pullCount += 1;
69
69
  pendingYieldAck = entry.ack;
70
- onYield?.(entry.message);
70
+ try {
71
+ await onYield?.(entry.message);
72
+ }
73
+ catch (error) {
74
+ // A source-pull rejection means the SDK never received this item. Reject
75
+ // its post-yield acknowledgement before surfacing the provider failure.
76
+ entry.ack.settleReject(error);
77
+ if (pendingYieldAck === entry.ack)
78
+ pendingYieldAck = undefined;
79
+ throw error;
80
+ }
71
81
  return { value: entry.message, done: false };
72
82
  };
73
83
  return {
@@ -88,8 +98,12 @@ export function createPushableUserMessageStream(onYield) {
88
98
  // Consumer is already inside next() (previous yield already acked at entry).
89
99
  pullCount += 1;
90
100
  pendingYieldAck = ack;
91
- onYield?.(message);
92
- waiter.resolve({ value: message, done: false });
101
+ void Promise.resolve(onYield?.(message)).then(() => waiter.resolve({ value: message, done: false }), (error) => {
102
+ ack.settleReject(error);
103
+ if (pendingYieldAck === ack)
104
+ pendingYieldAck = undefined;
105
+ waiter.reject(error);
106
+ });
93
107
  return { waitSubmitted: () => ack.wait() };
94
108
  }
95
109
  queue.push({ message, ack });
@@ -128,7 +142,7 @@ export function createPushableUserMessageStream(onYield) {
128
142
  // Per-yield submitted fence: next entry acks the previously yielded message.
129
143
  acknowledgePreviousYield();
130
144
  if (queue.length > 0) {
131
- return Promise.resolve(yieldQueued(queue.shift()));
145
+ return yieldQueued(queue.shift());
132
146
  }
133
147
  if (failure !== undefined) {
134
148
  return Promise.reject(failure);
@@ -5,7 +5,7 @@ import { createProviderFailure } from "../../harness/provider-adapter.js";
5
5
  import { awaitProviderAbortSettlement, buildSubagentEnv, createAbortError, emitProviderEvidence, coerceString, } from "../../harness/runtime.js";
6
6
  import { CODEX_APP_SERVER_PROVIDER } from "./identity.js";
7
7
  import { CodexAppServerProcess } from "./session.js";
8
- import { createCodexAppServerTurnMonitor, createCodexAppServerTurnState, recordTurnStartResponse, } from "./events.js";
8
+ import { createCodexAppServerTurnMonitor, createCodexAppServerTurnState, codexAppServerFailureMessage, codexAppServerTurnFailureCause, recordTurnStartResponse, } from "./events.js";
9
9
  export { CODEX_APP_SERVER_PROVIDER } from "./identity.js";
10
10
  /** Identity-only opacity boundary for codex-app-server turn/start effort. */
11
11
  function toCodexAppServerEffort(effort) {
@@ -84,7 +84,7 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
84
84
  appendDebugLog(`startCodexAppServer: executable=${executable} model=${options.model ?? "default"} mode=${resumeThreadId ? "resume" : "fresh"}`, { cwd, section: CODEX_APP_SERVER_PROVIDER });
85
85
  const submitSteer = async (tell, turnId) => {
86
86
  // turn/steer response {turnId} = enqueue acceptance (submitted), not consumption.
87
- // clientUserMessageId = tell.id for later delivered proof via userMessage item clientId.
87
+ // clientUserMessageId = tell.id for later exact seen proof via userMessage item clientId.
88
88
  // tell.text is already core-rendered; send byte-for-byte — never parse or rewrite.
89
89
  const ack = await server.request("turn/steer", {
90
90
  threadId: activeThreadId,
@@ -111,6 +111,7 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
111
111
  const startTellTurn = async (tell) => {
112
112
  const started = await server.request("turn/start", {
113
113
  threadId: activeThreadId,
114
+ clientUserMessageId: tell.id,
114
115
  input: [{ type: "text", text: tell.text }],
115
116
  ...(options.model ? { model: options.model } : {}),
116
117
  ...(tell.effort ? { effort: toCodexAppServerEffort(tell.effort) } : {}),
@@ -249,7 +250,7 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
249
250
  onEvidence: options.onEvidence,
250
251
  onEvidenceDiagnostic: (text) => builder.emit({ type: "diagnostic", text }),
251
252
  });
252
- const { turnCompleted } = createCodexAppServerTurnMonitor({
253
+ const { turnCompleted, reconcileLaunchAdmissions } = createCodexAppServerTurnMonitor({
253
254
  server,
254
255
  builder,
255
256
  state: turnState,
@@ -257,6 +258,8 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
257
258
  onEvidence: options.onEvidence,
258
259
  onSyncCheckpoint: options.onSyncCheckpoint,
259
260
  onDeliveryOpportunity: options.onDeliveryOpportunity,
261
+ onTellReceipt: options.onTellReceipt,
262
+ listLaunchAdmissions: options.listLaunchAdmissions,
260
263
  enqueue,
261
264
  drainPendingTells: () => { void drainPendingTells(); },
262
265
  rejectPendingTells,
@@ -346,6 +349,8 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
346
349
  ? (turnStart.turn ?? undefined)
347
350
  : undefined;
348
351
  const turnId = recordTurnStartResponse(turnState, turn?.id);
352
+ await options.onLaunchAdmission?.({ fence: { kind: "turn", turnId }, tellIds: options.launchTellIds ?? [] });
353
+ reconcileLaunchAdmissions();
349
354
  if (options.onEvidence)
350
355
  emitProviderEvidence(options.onEvidence, { type: "checkpoint", provider: CODEX_APP_SERVER_PROVIDER, fence: { kind: "turn", turnId }, source: "response" }, builderDiagnostic);
351
356
  const completed = await turnCompleted;
@@ -354,9 +359,9 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
354
359
  throw createAbortError(`${CODEX_APP_SERVER_PROVIDER} turn interrupted`);
355
360
  }
356
361
  if (completed.status === "failed") {
357
- const message = `${CODEX_APP_SERVER_PROVIDER} turn failed`;
358
- throw createProviderFailure(message, {
359
- ...(turnState.failureCause ? { publicCause: turnState.failureCause } : {}),
362
+ const publicCause = codexAppServerTurnFailureCause(completed.turn) ?? turnState.failureCause;
363
+ throw createProviderFailure(codexAppServerFailureMessage(publicCause), {
364
+ ...(publicCause ? { publicCause } : {}),
360
365
  });
361
366
  }
362
367
  if (completed.status !== "completed") {
@@ -1,5 +1,6 @@
1
1
  import { boundActivityExcerpt, diffstatFromUnifiedPatch } from "../../harness/activity-values.js";
2
2
  import { AGENT_PLAN_ITEM_TEXT_MAX_CHARS, AGENT_PLAN_MAX_ITEMS, AGENT_THOUGHT_TEXT_MAX_CHARS, } from "../../harness/events.js";
3
+ import { exactLaunchInputReceipts } from "../../harness/control-types.js";
3
4
  import { createProviderFailure } from "../../harness/provider-adapter.js";
4
5
  import { createAbortError, emitProviderEvidence, coerceString } from "../../harness/runtime.js";
5
6
  import { CODEX_APP_SERVER_PROVIDER } from "./identity.js";
@@ -174,7 +175,7 @@ export function createCodexAppServerTurnState() {
174
175
  lastAgentMessage: "",
175
176
  };
176
177
  }
177
- /** Admit only positive native exhaustion evidence; no provider strings survive. */
178
+ /** Admit positive native exhaustion evidence for the typed activity warning. */
178
179
  export function codexAppServerRateLimitCause(params) {
179
180
  const rateLimits = appServerRecord(appServerRecord(params)?.rateLimits);
180
181
  if (!rateLimits)
@@ -182,11 +183,39 @@ export function codexAppServerRateLimitCause(params) {
182
183
  for (const value of Object.values(rateLimits)) {
183
184
  const window = appServerRecord(value);
184
185
  if (typeof window?.usedPercent === "number" && window.usedPercent >= 100) {
185
- return { kind: "rate_limit_exhausted" };
186
+ return { code: "rate_limit_exhausted", message: "rate limit exhausted" };
186
187
  }
187
188
  }
188
189
  return undefined;
189
190
  }
191
+ export function codexAppServerTurnFailureCause(turn) {
192
+ const error = appServerRecord(turn.error);
193
+ const httpStatus = [error?.httpStatus, error?.status, error?.statusCode, turn.httpStatus, turn.statusCode]
194
+ .find((value) => typeof value === "number" && Number.isSafeInteger(value));
195
+ const code = coerceString(error?.code) ?? coerceString(turn.errorCode);
196
+ const message = coerceString(error?.message) ?? coerceString(turn.error) ?? coerceString(turn.message);
197
+ return httpStatus === undefined && code === undefined && message === undefined
198
+ ? undefined
199
+ : {
200
+ ...(httpStatus === undefined ? {} : { httpStatus }),
201
+ ...(code === undefined ? {} : { code }),
202
+ ...(message === undefined ? {} : { message }),
203
+ };
204
+ }
205
+ export function codexAppServerFailureMessage(cause) {
206
+ if (!cause)
207
+ return `${CODEX_APP_SERVER_PROVIDER} turn failed`;
208
+ const httpStatus = typeof cause.httpStatus === "number" ? cause.httpStatus : undefined;
209
+ const code = typeof cause.code === "string" ? cause.code : undefined;
210
+ const message = typeof cause.message === "string" ? cause.message : undefined;
211
+ const label = [
212
+ httpStatus === undefined ? undefined : `HTTP ${httpStatus}`,
213
+ code,
214
+ ].filter((part) => part !== undefined).join(" ");
215
+ if (label && message)
216
+ return `${label}: ${message}`;
217
+ return label || message || `${CODEX_APP_SERVER_PROVIDER} turn failed`;
218
+ }
190
219
  export function recordActiveTurnId(state, candidate, source) {
191
220
  const turnId = coerceString(candidate);
192
221
  if (!turnId)
@@ -211,7 +240,24 @@ export function recordTurnStartResponse(state, candidate) {
211
240
  return recordActiveTurnId(state, turnId, "response");
212
241
  }
213
242
  export function createCodexAppServerTurnMonitor(options) {
214
- const { server, builder, state, builderDiagnostic, onEvidence, onSyncCheckpoint, onDeliveryOpportunity, enqueue, drainPendingTells, rejectPendingTells, setFailActiveTurn, pendingTellCount, isDrainingTells, setFinishAfterTellDrain, } = options;
243
+ const { server, builder, state, builderDiagnostic, onEvidence, onSyncCheckpoint, onDeliveryOpportunity, onTellReceipt, listLaunchAdmissions, enqueue, drainPendingTells, rejectPendingTells, setFailActiveTurn, pendingTellCount, isDrainingTells, setFinishAfterTellDrain, } = options;
244
+ const startedLaunchFences = new Map();
245
+ const completedLaunchFences = new Map();
246
+ const emitLaunchReceipts = (stage, fence) => {
247
+ const admissions = (listLaunchAdmissions?.() ?? []).filter((admission) => (admission.fence.kind === "turn" && fence.kind === "turn"
248
+ && admission.fence.turnId === fence.turnId));
249
+ for (const receipt of exactLaunchInputReceipts(stage, fence, admissions))
250
+ onTellReceipt?.(receipt);
251
+ };
252
+ // A notification may arrive between the native response and durable launch
253
+ // admission. Retain only explicit native turn fences and replay them after
254
+ // the adapter has recorded the coordinate; no timing inference is involved.
255
+ const reconcileLaunchAdmissions = () => {
256
+ for (const fence of startedLaunchFences.values())
257
+ emitLaunchReceipts("seen", fence);
258
+ for (const fence of completedLaunchFences.values())
259
+ emitLaunchReceipts("consumed", fence);
260
+ };
215
261
  const turnCompleted = new Promise((resolve, reject) => {
216
262
  let unsubNotifications = () => { };
217
263
  let unsubServerRequest = () => { };
@@ -277,6 +323,11 @@ export function createCodexAppServerTurnMonitor(options) {
277
323
  builder.emit({ type: "message", id, text });
278
324
  }
279
325
  }
326
+ else if (itemType === "userMessage") {
327
+ const tellId = coerceString(item.clientId);
328
+ if (tellId)
329
+ onTellReceipt?.({ stage: "seen", correlation: { kind: "exact", tellId } });
330
+ }
280
331
  else if (itemType === "reasoning") {
281
332
  const text = extractReasoningSummary(item);
282
333
  if (text) {
@@ -316,6 +367,11 @@ export function createCodexAppServerTurnMonitor(options) {
316
367
  const itemType = coerceString(item.type);
317
368
  if (!id || !itemType)
318
369
  throw new Error(`${CODEX_APP_SERVER_PROVIDER} item/started lacked required item id or type`);
370
+ if (itemType === "userMessage") {
371
+ const tellId = coerceString(item.clientId);
372
+ if (tellId)
373
+ onTellReceipt?.({ stage: "seen", correlation: { kind: "exact", tellId } });
374
+ }
319
375
  if (isCodexAppServerActivityItemType(itemType)) {
320
376
  builder.emit({
321
377
  type: "activity",
@@ -331,7 +387,7 @@ export function createCodexAppServerTurnMonitor(options) {
331
387
  const turnId = recordActiveTurnId(state, turn?.id, "notification");
332
388
  builder.emit({ type: "turn", phase: "started" });
333
389
  state.activeTurnAcceptingSteer = true;
334
- const checkpoint = { fence: { kind: "turn", turnId }, consumptionEvidence: false };
390
+ const checkpoint = { fence: { kind: "turn", turnId } };
335
391
  if (onEvidence) {
336
392
  emitProviderEvidence(onEvidence, {
337
393
  type: "checkpoint",
@@ -343,6 +399,8 @@ export function createCodexAppServerTurnMonitor(options) {
343
399
  // turn/start may respond before the server has installed its active
344
400
  // turn. The native notification is the first steer-safe boundary.
345
401
  onSyncCheckpoint?.({ enqueue }, checkpoint);
402
+ startedLaunchFences.set(turnId, checkpoint.fence);
403
+ emitLaunchReceipts("seen", checkpoint.fence);
346
404
  void drainPendingTells();
347
405
  }
348
406
  if (notification.method === "account/rateLimits/updated") {
@@ -370,7 +428,7 @@ export function createCodexAppServerTurnMonitor(options) {
370
428
  state.activeTurnAcceptingSteer = false;
371
429
  state.activeTurnId = undefined;
372
430
  state.activeTurnSource = undefined;
373
- const checkpoint = { fence: { kind: "turn", turnId }, consumptionEvidence: true };
431
+ const checkpoint = { fence: { kind: "turn", turnId } };
374
432
  if (onEvidence) {
375
433
  emitProviderEvidence(onEvidence, {
376
434
  type: "checkpoint",
@@ -380,6 +438,9 @@ export function createCodexAppServerTurnMonitor(options) {
380
438
  }, builderDiagnostic);
381
439
  }
382
440
  onSyncCheckpoint?.({ enqueue }, checkpoint);
441
+ completedLaunchFences.set(turnId, checkpoint.fence);
442
+ emitLaunchReceipts("consumed", checkpoint.fence);
443
+ onTellReceipt?.({ stage: "consumed", correlation: { kind: "fence", fence: checkpoint.fence } });
383
444
  void drainPendingTells();
384
445
  if (pendingTellCount() > 0 || isDrainingTells()) {
385
446
  setFinishAfterTellDrain(() => {
@@ -427,5 +488,5 @@ export function createCodexAppServerTurnMonitor(options) {
427
488
  // Abort can settle this before initialization reaches the await below.
428
489
  // Attach a handler immediately while preserving the original awaited error.
429
490
  turnCompleted.catch(() => undefined);
430
- return { turnCompleted };
491
+ return { turnCompleted, reconcileLaunchAdmissions };
431
492
  }
@@ -212,7 +212,8 @@ export const codexAppServerRegistration = {
212
212
  webSearch: Object.freeze(webSearch),
213
213
  });
214
214
  },
215
- startLive: async ({ terms, signal, reviveTerms, onSyncCheckpoint, onDeliveryOpportunity, onSessionAdmission, onIngress, }) => {
215
+ tellReceiptCapabilities: { seen: "exact", consumed: "fence" },
216
+ startLive: async ({ terms, signal, reviveTerms, onSyncCheckpoint, onDeliveryOpportunity, onSessionAdmission, onIngress, launchTellIds, observer, }) => {
216
217
  if (terms.provider !== CODEX_APP_SERVER_PROVIDER) {
217
218
  throw new Error(`startLive expected ${CODEX_APP_SERVER_PROVIDER}`);
218
219
  }
@@ -230,6 +231,15 @@ export const codexAppServerRegistration = {
230
231
  developerInstructions: codexTerms.developerInstructions,
231
232
  onSyncCheckpoint,
232
233
  onDeliveryOpportunity,
234
+ onTellReceipt: observer ? (receipt) => {
235
+ try {
236
+ observer.onTellReceipt(receipt);
237
+ }
238
+ catch { /* Receipt persistence cannot alter provider control. */ }
239
+ } : undefined,
240
+ launchTellIds,
241
+ onLaunchAdmission: observer ? (admission) => observer.onLaunchAdmission(admission) : undefined,
242
+ listLaunchAdmissions: observer ? () => observer.listLaunchAdmissions() : undefined,
233
243
  onEvidence: codexTerms.onEvidence,
234
244
  onSessionAdmission,
235
245
  onIngress,
@@ -280,6 +280,8 @@ export async function startOpencode(prompt, cwd, options = {}) {
280
280
  onEvidence: options.onEvidence,
281
281
  onSyncCheckpoint: options.onSyncCheckpoint,
282
282
  onDeliveryOpportunity: options.onDeliveryOpportunity,
283
+ onTellReceipt: options.onTellReceipt,
284
+ listLaunchAdmissions: options.listLaunchAdmissions,
283
285
  executionSignal: combined.signal,
284
286
  getSessionId: () => activeSessionId,
285
287
  client: sdk.client,
@@ -313,7 +315,12 @@ export async function startOpencode(prompt, cwd, options = {}) {
313
315
  // The acquisition itself lives inside streamTask, so local shutdown can
314
316
  // always settle it even when the SDK never resolves events().
315
317
  await eventsResultTask;
316
- await promptSteer(sdk.client, prompt);
318
+ const launchAcknowledgement = await promptSteer(sdk.client, prompt);
319
+ await options.onLaunchAdmission?.({
320
+ fence: launchAcknowledgement.fence,
321
+ tellIds: options.launchTellIds ?? [],
322
+ });
323
+ eventController?.reconcileLaunchAdmissions();
317
324
  await waitIdle();
318
325
  if (gracefulInterruptRequested) {
319
326
  await gracefulProviderSettlement;
@@ -1,3 +1,4 @@
1
+ import { exactLaunchInputReceipts } from "../../harness/control-types.js";
1
2
  import { activityCallFromNamedTool } from "../../harness/activity-values.js";
2
3
  import { AGENT_THOUGHT_TEXT_MAX_CHARS, } from "../../harness/events.js";
3
4
  import { createProviderFailure } from "../../harness/provider-adapter.js";
@@ -117,7 +118,7 @@ function extractFinalAssistantMessage(messages) {
117
118
  return messages.find((message) => message.type === "assistant");
118
119
  }
119
120
  export function createOpencodeEventController(options) {
120
- const { builder, control, idleMonitorIngress, idleMonitorTerminal, onEvidence, onSyncCheckpoint, onDeliveryOpportunity, getSessionId, client, } = options;
121
+ const { builder, control, idleMonitorIngress, idleMonitorTerminal, onEvidence, onSyncCheckpoint, onDeliveryOpportunity, onTellReceipt, listLaunchAdmissions, getSessionId, client, } = options;
121
122
  const recordedEventTypes = [];
122
123
  let totalEventCount = 0;
123
124
  let currentTurnAdmitted = false;
@@ -125,6 +126,8 @@ export function createOpencodeEventController(options) {
125
126
  let lastTurnFailure;
126
127
  let lastSeenSeq = 0;
127
128
  let turnOpen = false;
129
+ const promptedFences = [];
130
+ const emittedLaunchReceiptKeys = new Set();
128
131
  const toolCalls = new Map();
129
132
  const requestDelivery = (steerControl) => {
130
133
  try {
@@ -134,6 +137,21 @@ export function createOpencodeEventController(options) {
134
137
  // Delivery observation cannot alter provider control flow.
135
138
  }
136
139
  };
140
+ const reconcileLaunchAdmissions = () => {
141
+ for (const fence of promptedFences) {
142
+ const admissions = (listLaunchAdmissions?.() ?? []).filter((admission) => (admission.fence.kind === "ordinal" && fence.kind === "ordinal"
143
+ && admission.fence.ordinal <= fence.ordinal));
144
+ for (const receipt of exactLaunchInputReceipts("seen", fence, admissions)) {
145
+ if (fence.kind !== "ordinal" || receipt.correlation.kind !== "exact")
146
+ continue;
147
+ const key = `${receipt.stage}:${fence.ordinal}:${receipt.correlation.tellId}`;
148
+ if (emittedLaunchReceiptKeys.has(key))
149
+ continue;
150
+ emittedLaunchReceiptKeys.add(key);
151
+ onTellReceipt?.(receipt);
152
+ }
153
+ }
154
+ };
137
155
  const openTurn = () => {
138
156
  if (turnOpen)
139
157
  builder.emit({ type: "turn", phase: "completed" });
@@ -169,11 +187,14 @@ export function createOpencodeEventController(options) {
169
187
  currentTurnFailure = undefined;
170
188
  openTurn();
171
189
  if (seq !== undefined) {
172
- const checkpoint = { fence: { kind: "ordinal", ordinal: seq }, consumptionEvidence: true };
190
+ const checkpoint = { fence: { kind: "ordinal", ordinal: seq } };
173
191
  if (onEvidence) {
174
192
  emitProviderEvidence(onEvidence, { type: "checkpoint", provider: OPENCODE_SDK_PROVIDER, fence: checkpoint.fence, source: "notification" }, (text) => builder.emit({ type: "diagnostic", text }));
175
193
  }
176
194
  onSyncCheckpoint?.(control, checkpoint);
195
+ promptedFences.push(checkpoint.fence);
196
+ reconcileLaunchAdmissions();
197
+ onTellReceipt?.({ stage: "seen", correlation: { kind: "fence", fence: checkpoint.fence } });
177
198
  }
178
199
  break;
179
200
  }
@@ -346,6 +367,7 @@ export function createOpencodeEventController(options) {
346
367
  drainHistory,
347
368
  closeTurn,
348
369
  lastSeenSeq: () => lastSeenSeq,
370
+ reconcileLaunchAdmissions,
349
371
  lastTurnFailure: () => lastTurnFailure,
350
372
  recordedEventTypes: () => recordedEventTypes,
351
373
  totalEventCount: () => totalEventCount,