ai-foreman 1.7.29 → 2.0.0

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 (136) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude.d.ts +23 -12
  3. package/dist/adapters/claude.js +227 -23
  4. package/dist/adapters/codex.d.ts +17 -7
  5. package/dist/adapters/codex.js +195 -17
  6. package/dist/adapters/recovering.d.ts +2 -0
  7. package/dist/adapters/recovering.js +5 -1
  8. package/dist/adapters/structured.d.ts +71 -0
  9. package/dist/adapters/structured.js +213 -0
  10. package/dist/adapters/types.d.ts +85 -9
  11. package/dist/agentRun.d.ts +2 -0
  12. package/dist/agentRun.js +2 -1
  13. package/dist/boundedTurnEvidence.d.ts +18 -0
  14. package/dist/boundedTurnEvidence.js +76 -0
  15. package/dist/branch/currentGuard.d.ts +2 -0
  16. package/dist/branch/currentGuard.js +2 -0
  17. package/dist/branch/planner.js +1 -4
  18. package/dist/branch/runner.d.ts +2 -1
  19. package/dist/branch/runner.js +10 -7
  20. package/dist/buildAdmission.js +4 -0
  21. package/dist/buildApproval.d.ts +15 -0
  22. package/dist/buildApproval.js +25 -0
  23. package/dist/buildAssignment.d.ts +1 -0
  24. package/dist/buildAssignment.js +82 -23
  25. package/dist/buildInterventions.d.ts +2 -2
  26. package/dist/buildInterventions.js +11 -4
  27. package/dist/buildLaunchGate.js +3 -2
  28. package/dist/builderGuidanceFollowup.js +17 -6
  29. package/dist/cli/manager.js +22 -20
  30. package/dist/cli/start.js +22 -26
  31. package/dist/cli/tickets.d.ts +0 -7
  32. package/dist/cli/tickets.js +41 -40
  33. package/dist/continuity.d.ts +10 -14
  34. package/dist/continuity.js +198 -179
  35. package/dist/continuityOrigin.d.ts +13 -0
  36. package/dist/continuityOrigin.js +91 -0
  37. package/dist/foreman.d.ts +10 -4
  38. package/dist/foreman.js +51 -193
  39. package/dist/graph/boundary.d.ts +1 -1
  40. package/dist/graph/boundary.js +1 -1
  41. package/dist/graph/host.js +8 -19
  42. package/dist/graph/manager.d.ts +1 -1
  43. package/dist/graph/manager.js +1 -1
  44. package/dist/graph/preparation.d.ts +38 -0
  45. package/dist/graph/preparation.js +7 -6
  46. package/dist/graph/semantic.d.ts +1 -0
  47. package/dist/graph/semantic.js +4 -1
  48. package/dist/graph/session.js +7 -2
  49. package/dist/graph/turn.d.ts +2 -3
  50. package/dist/graph/turn.js +28 -40
  51. package/dist/handoffProtocol.js +2 -2
  52. package/dist/handoffs.d.ts +3 -1
  53. package/dist/handoffs.js +90 -59
  54. package/dist/hostGraphApplicability.d.ts +2 -1
  55. package/dist/hostGraphApplicability.js +17 -19
  56. package/dist/hostQaAugmentation.js +21 -4
  57. package/dist/hostQaComparison.d.ts +21 -2
  58. package/dist/hostQaComparison.js +43 -29
  59. package/dist/hostSourcePreservation.d.ts +38 -0
  60. package/dist/hostSourcePreservation.js +72 -0
  61. package/dist/interviews.d.ts +7 -0
  62. package/dist/interviews.js +18 -1
  63. package/dist/legacyCommunicationDisplay.d.ts +2 -0
  64. package/dist/legacyCommunicationDisplay.js +2 -0
  65. package/dist/managerPacket.js +3 -3
  66. package/dist/markers.d.ts +4 -3
  67. package/dist/markers.js +4 -20
  68. package/dist/probeIpc.d.ts +14 -0
  69. package/dist/probeIpc.js +29 -0
  70. package/dist/qaBuildGate.d.ts +21 -0
  71. package/dist/qaBuildGate.js +47 -12
  72. package/dist/qaBuilderCoverage.js +22 -13
  73. package/dist/qaBuilderEvidence.d.ts +16 -1
  74. package/dist/qaBuilderEvidence.js +48 -8
  75. package/dist/qaContractCoverage.d.ts +2 -2
  76. package/dist/qaContractCoverage.js +4 -9
  77. package/dist/qaContractDelivery.js +34 -20
  78. package/dist/qaCoverageAssembly.d.ts +22 -0
  79. package/dist/qaCoverageAssembly.js +16 -0
  80. package/dist/qaDeliveryJournal.d.ts +2 -0
  81. package/dist/qaDeliveryJournal.js +19 -0
  82. package/dist/qaFailureDelivery.js +97 -85
  83. package/dist/qaHostTransformation.d.ts +26 -0
  84. package/dist/qaHostTransformation.js +100 -0
  85. package/dist/qaPreparation.d.ts +8 -20
  86. package/dist/qaPreparation.js +133 -248
  87. package/dist/qaPreparationInputs.js +19 -0
  88. package/dist/qaPrerequisites.d.ts +2 -1
  89. package/dist/qaPrerequisites.js +21 -1
  90. package/dist/qaProtocolV2.d.ts +15 -0
  91. package/dist/qaRecovery.js +21 -4
  92. package/dist/qaReview.js +220 -395
  93. package/dist/qaReviewEvidence.d.ts +7 -3
  94. package/dist/qaReviewEvidence.js +15 -4
  95. package/dist/qaRuntime.js +1 -1
  96. package/dist/qaSnapshot.js +71 -21
  97. package/dist/qaStructuredResponse.d.ts +10 -0
  98. package/dist/qaStructuredResponse.js +57 -0
  99. package/dist/qaTimeline.js +5 -3
  100. package/dist/qaVerificationContract.d.ts +1 -2
  101. package/dist/qaVerificationContract.js +24 -35
  102. package/dist/readinessGate.js +5 -4
  103. package/dist/runtimeReadiness.js +23 -15
  104. package/dist/statusReporter.d.ts +2 -0
  105. package/dist/statusReporter.js +2 -0
  106. package/dist/structuredCutover.d.ts +31 -0
  107. package/dist/structuredCutover.js +510 -0
  108. package/dist/structuredCutoverFiles.d.ts +10 -0
  109. package/dist/structuredCutoverFiles.js +181 -0
  110. package/dist/structuredDispatchAuthority.d.ts +22 -0
  111. package/dist/structuredDispatchAuthority.js +110 -0
  112. package/dist/structuredDispatchRecord.d.ts +29 -0
  113. package/dist/structuredDispatchRecord.js +36 -0
  114. package/dist/structuredLiveBudget.d.ts +2 -0
  115. package/dist/structuredLiveBudget.js +23 -0
  116. package/dist/structuredOperation.d.ts +18 -0
  117. package/dist/structuredOperation.js +257 -0
  118. package/dist/supervisedStart.js +35 -21
  119. package/dist/ticketPlanning.d.ts +1 -373
  120. package/dist/ticketPlanning.js +2 -26
  121. package/dist/ticketPopulation.d.ts +0 -3
  122. package/dist/ticketPopulation.js +4 -21
  123. package/dist/ticketPopulationPageAuthority.d.ts +13 -0
  124. package/dist/ticketPopulationPageAuthority.js +30 -0
  125. package/dist/ticketPopulationPaging.d.ts +27 -0
  126. package/dist/ticketPopulationPaging.js +105 -0
  127. package/dist/tickets/renderMarkdown.d.ts +14 -0
  128. package/dist/tickets/renderMarkdown.js +39 -5
  129. package/dist/tickets/ticketLoader.js +10 -0
  130. package/dist/tickets/ticketSchema.d.ts +107 -0
  131. package/dist/tickets/ticketSchema.js +4 -1
  132. package/dist/workflowDb.d.ts +100 -2
  133. package/dist/workflowDb.js +763 -39
  134. package/dist/workflowReader.d.ts +1 -0
  135. package/dist/workflowReader.js +9 -0
  136. package/package.json +15 -7
@@ -0,0 +1,257 @@
1
+ import { sealTicketPopulationPage } from "./ticketPopulationPageAuthority.js";
2
+ import { boundedUtf8 } from "./boundedTurnEvidence.js";
3
+ import { randomUUID } from "node:crypto";
4
+ import { assertIntermediateResultV2, historicProviderWireContract, providerWireContract, renderProviderWireInstruction, selectedDispatchTupleV2, structuredDigest, validateStructuredCandidate } from "rafi-spec";
5
+ import { WorkflowDb } from "./workflowDb.js";
6
+ import { withSealedDispatch, currentFreshRecoveryValidation } from "./structuredDispatchAuthority.js";
7
+ import { sealedDispatchRecord, verifyPersistedDispatch } from "./structuredDispatchRecord.js";
8
+ export function operationInstruction(contract) {
9
+ return `Rafi operation ${contract.operation}, protocol 2, schema ${contract.schemaVersion}. Submit only the native JSON selected by the provider wire contract appended by Rafi. Do not emit prose delimiters, status lines, host identifiers, host defaults, or fields outside that selected schema. Keep provider-authored summaries and evidence concise. Human progress is presentation only.`;
10
+ }
11
+ /**
12
+ * Actionable host message for a failed structured turn. The response text of an
13
+ * otherwise valid turn is evidence, not the error; prefer retained host
14
+ * failures, then the native rejection, then provider diagnostics.
15
+ */
16
+ export function structuredTurnErrorMessage(turn, label = "Structured provider turn") {
17
+ if (turn.continuityErrors?.length)
18
+ return `${label} failed continuity postprocessing: ${turn.continuityErrors.join("; ")}`;
19
+ if (turn.structured?.state === "rejected")
20
+ return `${label} was rejected (${turn.structured.reason}): ${turn.structured.detail}`;
21
+ if (turn.failure)
22
+ return `${label} failed (${turn.failure.category}, ${turn.failure.phase}): ${boundedUtf8(turn.failure.diagnostics, 2000).toString()}`;
23
+ return turn.text || `${label} failed without diagnostics`;
24
+ }
25
+ export function operationResponse(turn, contract) {
26
+ if (turn.isError || turn.failure)
27
+ throw new Error(turn.structured?.state === "rejected" ? turn.structured.detail : turn.text || "Provider execution did not complete");
28
+ if (!turn.selectedDispatch)
29
+ throw new Error("Structured provider turn has no sealed selected dispatch tuple");
30
+ const descriptor = historicProviderWireContract(contract, turn.selectedDispatch);
31
+ const response = validateStructuredCandidate(contract, turn.structured, descriptor, turn.selectedDispatch);
32
+ if (response.result.kind.endsWith("-request"))
33
+ assertIntermediateResultV2(response.result);
34
+ return response;
35
+ }
36
+ export function operationStatus(turn, contract, ticket) {
37
+ try {
38
+ const { result } = operationResponse(turn, contract);
39
+ if (result.kind === "completed") {
40
+ return { kind: "done", summary: result.summary, ticket };
41
+ }
42
+ if (result.kind === "blocked")
43
+ return { kind: "blocked", reason: result.reason, next: result.nextAction, ticket, ...(result.branchDependency ? { branchDependency: result.branchDependency } : {}) };
44
+ if (result.kind === "needs-input")
45
+ return { kind: "needs_input", question: result.question, choices: result.choices, ticket };
46
+ if (result.kind === "source-request")
47
+ return { kind: "needs_input", question: "Additional source material is required. Supply or authorize the requested source content.", choices: [], ticket };
48
+ return { kind: "unknown", error: `Unconsumed intermediate result: ${result.kind}` };
49
+ }
50
+ catch (error) {
51
+ return { kind: "unknown", error: boundedUtf8(String(error), 4000).toString() };
52
+ }
53
+ }
54
+ export function operationPayload(turn, contract) {
55
+ const response = operationResponse(turn, contract);
56
+ if (response.result.kind !== "completed")
57
+ throw new Error(`Operation ${contract.operation} returned ${response.result.kind}`);
58
+ return response.result.payload;
59
+ }
60
+ export function nativePolicy(output, policy = {}) {
61
+ return { ...policy, output, logicalActionId: policy.logicalActionId ?? randomUUID(), dispatchId: policy.dispatchId ?? randomUUID() };
62
+ }
63
+ /** Reuses the workflow journal. A receipt is retained before domain interpretation. */
64
+ export async function journalStructuredDispatch(root, adapter, instruction, policy, dispatch) {
65
+ if (!policy.output)
66
+ return dispatch(instruction, policy);
67
+ if (!adapter.prepareSession)
68
+ throw new Error("Structured dispatch requires an adapter that prepares a scoped provider session");
69
+ const selected = nativePolicy(policy.output, policy);
70
+ // Decorator-owned requirements (such as mandatory continuity) become part of
71
+ // the sealed provider instruction; they are never appended after sealing.
72
+ const requirement = adapter.structuredDispatchRequirement?.();
73
+ const providerInstruction = requirement ? `${instruction}\n\n${requirement.instruction}` : instruction;
74
+ const key = selected.dispatchId;
75
+ const runId = selected.journalRunId ?? `operation:${selected.logicalActionId}`;
76
+ const db = new WorkflowDb(root);
77
+ try {
78
+ db.enableStructuredCommunication();
79
+ db.ensureRun(runId);
80
+ if (adapter.postConfirmedDispatch)
81
+ for (const pendingId of db.pendingStructuredPostprocessing(runId)) {
82
+ if (pendingId === selected.parentDispatchId)
83
+ continue;
84
+ const pending = db.operation(pendingId);
85
+ if (pending?.status !== "confirmed")
86
+ throw new Error("Postprocessing origin is not confirmed");
87
+ const tuple = db.structuredOwnerDispatch(pendingId);
88
+ // Rehydrate and publish locally; this path never calls raw transport.
89
+ await adapter.postConfirmedDispatch({ dispatchId: pendingId, runId, tupleDigest: tuple.tupleDigest });
90
+ db.completeStructuredPostprocessing(pendingId);
91
+ db.finishStructuredContinuation(pendingId);
92
+ }
93
+ db.assertStructuredDispatchAdmission(runId, key, selected.parentDispatchId, policy.output.operation);
94
+ const prior = db.planOperation({ runId, idempotencyKey: key, kind: "provider-dispatch", intent: {
95
+ protocolVersion: 2, state: "preparing", logicalActionId: selected.logicalActionId,
96
+ operation: policy.output.operation, schemaVersion: policy.output.schemaVersion, schemaDigest: policy.output.schemaDigest,
97
+ instructionDigest: structuredDigest(providerInstruction), purpose: selected.purpose ?? "implementation",
98
+ } });
99
+ if (prior.status !== "planned")
100
+ throw new Error("Existing structured dispatch requires reconciliation; it cannot be replayed");
101
+ // Session bootstrap can itself contact the provider. The non-authoritative
102
+ // preparation record above is durable before that boundary is crossed.
103
+ const prepared = await adapter.prepareSession();
104
+ if (!prepared.sessionId || !Number.isSafeInteger(prepared.generation) || prepared.generation < 0)
105
+ throw new Error("Adapter did not prepare a valid provider session for structured dispatch");
106
+ const wireContract = providerWireContract(policy.output, adapter.agent, "default", { requireRegistered: true });
107
+ if (policy.wireContract && policy.wireContract.descriptorDigest !== wireContract.descriptorDigest)
108
+ throw new Error("New dispatch may select only the static active descriptor");
109
+ if (wireContract.provider !== adapter.agent || wireContract.canonical.schemaDigest !== policy.output.schemaDigest)
110
+ throw new Error("Structured dispatch wire contract does not match its adapter/output contract");
111
+ if (policy.renderedWireContractDigest && policy.renderedWireContractDigest !== wireContract.descriptorDigest)
112
+ throw new Error("Structured dispatch guidance does not match its selected wire contract");
113
+ const renderedInstruction = renderProviderWireInstruction(providerInstruction, wireContract);
114
+ const sealedSelection = nativePolicy(policy.output, { ...selected, wireContract, renderedWireContractDigest: wireContract.descriptorDigest });
115
+ const before = adapter.sessionRef?.();
116
+ if (!before || before.sessionId !== prepared.sessionId || before.generation !== prepared.generation)
117
+ throw new Error("Prepared provider session is not the adapter's active scoped session");
118
+ const tuple = selectedDispatchTupleV2({
119
+ operation: policy.output.operation, schemaVersion: policy.output.schemaVersion, schemaDigest: policy.output.schemaDigest,
120
+ provider: adapter.agent, profile: wireContract.profile, variant: wireContract.variant, descriptorDigest: wireContract.descriptorDigest,
121
+ wireSchemaDigest: wireContract.wireSchemaDigest, guidanceDigest: wireContract.guidanceDigest,
122
+ normalizerId: wireContract.normalizerId, normalizerVersion: wireContract.normalizerVersion,
123
+ maximumSchemaBytes: wireContract.maximumSchemaBytes, maximumResponseBytes: wireContract.maximumResponseBytes,
124
+ maximumPageBytes: wireContract.maximumPageBytes ?? null, instructionDigest: structuredDigest(renderedInstruction),
125
+ actionId: sealedSelection.logicalActionId, dispatchId: sealedSelection.dispatchId, sessionId: before.sessionId, generation: before.generation,
126
+ });
127
+ const sealedPolicy = Object.freeze({ ...sealedSelection, selectedDispatch: tuple });
128
+ const compactionBefore = adapter.contractCompactionSequence?.() ?? 0;
129
+ const intent = { protocolVersion: 2, tuple, sealedDispatch: sealedDispatchRecord(tuple), logicalActionId: selected.logicalActionId, parentDispatchId: selected.parentDispatchId ?? null,
130
+ // The durable role is derived from the scoped provider session, never a
131
+ // caller-controlled string on TurnRequest.
132
+ role: before.role, sessionRef: before, operation: policy.output.operation,
133
+ schemaVersion: policy.output.schemaVersion, schemaDigest: policy.output.schemaDigest, instructionDigest: structuredDigest(renderedInstruction),
134
+ purpose: selected.purpose ?? "implementation", responseOnly: selected.responseOnly === true,
135
+ freshRecoveryValidation: currentFreshRecoveryValidation(root, runId),
136
+ wire: { profile: wireContract.profile, variant: wireContract.variant, wireVersion: wireContract.wireVersion,
137
+ descriptorDigest: wireContract.descriptorDigest, wireSchemaDigest: wireContract.wireSchemaDigest,
138
+ maximumSchemaBytes: wireContract.maximumSchemaBytes, maximumResponseBytes: wireContract.maximumResponseBytes,
139
+ maximumPageBytes: wireContract.maximumPageBytes ?? null, normalizerId: wireContract.normalizerId,
140
+ normalizerVersion: wireContract.normalizerVersion, guidanceId: wireContract.guidanceId } };
141
+ const priorRecord = db.operation(key);
142
+ if (priorRecord.status !== "planned")
143
+ throw new Error("Existing structured dispatch requires reconciliation; it cannot be replayed");
144
+ db.atomic(() => {
145
+ db.linkStructuredHandoffDispatch(key, selected.parentDispatchId, policy.output.operation);
146
+ db.sealPlannedOperationIntent(key, intent);
147
+ db.sealStructuredOwnerDispatch(key, tuple, renderedInstruction);
148
+ sealTicketPopulationPage(db, root, tuple);
149
+ db.updateOperation(key, "in_progress");
150
+ });
151
+ let turn;
152
+ try {
153
+ turn = await withSealedDispatch(root, tuple, dispatchCapability => dispatch(renderedInstruction, { ...sealedPolicy, journalRunId: runId, dispatchCapability }), before);
154
+ }
155
+ catch (error) {
156
+ if (db.operation(key)?.status === "in_progress")
157
+ db.updateOperation(key, "uncertain", { error: boundedUtf8(String(error), 4000).toString() });
158
+ throw error;
159
+ }
160
+ const after = adapter.sessionRef?.();
161
+ const sameScope = before && after && before.provider === after.provider && before.role === after.role &&
162
+ before.stream === after.stream && before.cwd === after.cwd && before.configRoot === after.configRoot &&
163
+ before.workspaceIdentity === after.workspaceIdentity && before.ticketId === after.ticketId && before.deliveryUnitId === after.deliveryUnitId;
164
+ const observedCompaction = sameScope && (adapter.contractCompactionSequence?.() ?? 0) > compactionBefore && after.generation >= before.generation;
165
+ if (turn.structured?.state === "candidate" && (turn.structured.profile !== wireContract.profile || turn.structured.wireContractDigest !== wireContract.descriptorDigest ||
166
+ turn.structured.wireSchemaDigest !== wireContract.wireSchemaDigest || turn.structured.wireVariant !== wireContract.variant || turn.structured.dispatchId !== key || turn.structured.sessionId !== adapter.sessionId() ||
167
+ before && (!sameScope || !observedCompaction && (before.sessionId !== after.sessionId || before.generation !== after.generation)))) {
168
+ turn = { ...turn, selectedDispatch: tuple, isError: true, structured: { state: "rejected", reason: "invalid-contract", detail: "Dispatch/session/generation receipt mismatch", dispatchId: key } };
169
+ }
170
+ // Exact raw provider material is retained once in the evidence store. The
171
+ // journal only carries the digest/reference and validation fact, so it
172
+ // cannot become a second, differently-redacted copy of the provider turn.
173
+ turn = { ...turn, selectedDispatch: tuple };
174
+ let receiptValidated = false;
175
+ if (turn.structured?.state === "rejected")
176
+ turn = { ...turn, isError: true };
177
+ else if (!turn.isError && !turn.failure) {
178
+ try {
179
+ const response = verifyPersistedDispatch(policy.output, db.structuredOwnerDispatch(key), turn.structured, tuple);
180
+ // A response that cannot satisfy host requirements is rejected before
181
+ // confirmation instead of failing in postprocessing after it.
182
+ requirement?.validate(response);
183
+ receiptValidated = true;
184
+ }
185
+ catch (error) {
186
+ turn = { ...turn, selectedDispatch: tuple, isError: true, structured: { state: "rejected", reason: "invalid-contract", detail: boundedUtf8(String(error), 4000).toString(), dispatchId: key } };
187
+ }
188
+ }
189
+ const retained = { version: 2, tupleDigest: tuple.digest, hostTurnId: turn.turnId, receipt: turn.structured ?? null };
190
+ if (turn.structured?.state === "candidate") {
191
+ const { value, ...receipt } = turn.structured;
192
+ const bytes = Buffer.from(receipt.representation === "json-text" ? value : JSON.stringify(value));
193
+ if (bytes.length > wireContract.maximumResponseBytes)
194
+ throw new Error("Native receipt exceeds selected evidence budget");
195
+ retained.valueDigest = db.putEvidence("handoff", bytes);
196
+ retained.receipt = receipt;
197
+ }
198
+ if (turn.structured?.state === "rejected") {
199
+ const { observedValue, ...receipt } = turn.structured;
200
+ retained.receipt = { ...receipt, detail: boundedUtf8(receipt.detail, 4000).toString() };
201
+ if (observedValue) {
202
+ const bytes = Buffer.from(observedValue.representation === "json-text" ? String(observedValue.value) : JSON.stringify(observedValue.value));
203
+ const bounded = boundedUtf8(bytes.toString(), wireContract.maximumResponseBytes);
204
+ retained.rejectedValue = { representation: observedValue.representation, digest: observedValue.digest,
205
+ evidenceDigest: db.putEvidence("handoff", bounded), originalBytes: bytes.length, retainedBytes: bounded.length };
206
+ }
207
+ }
208
+ const raw = Buffer.from(turn.rawResponse ?? turn.text);
209
+ const retainedRaw = boundedUtf8(turn.rawResponse ?? turn.text, wireContract.maximumResponseBytes);
210
+ const rawDigest = db.putEvidence("handoff", retainedRaw);
211
+ const responseDigest = db.putEvidence("handoff", Buffer.from(JSON.stringify(retained)));
212
+ db.recordCommunicationEvidenceRefs(key, [responseDigest, rawDigest, ...(retained.valueDigest ? [retained.valueDigest] : []), ...(retained.rejectedValue ? [retained.rejectedValue.evidenceDigest] : [])]);
213
+ const status = turn.failure?.dispatchState === "not-sent" ? "failed" : turn.failure?.dispatchState === "unknown" || !receiptValidated ? "uncertain" : "confirmed";
214
+ turn = { ...turn, transportEvidence: { rawDigest, responseDigest, rawBytes: raw.length, retainedRawBytes: retainedRaw.length,
215
+ valueDigest: retained.valueDigest, rejectedValueDigest: retained.rejectedValue?.evidenceDigest } };
216
+ db.atomic(() => {
217
+ if (status === "confirmed" && adapter.postConfirmedDispatch)
218
+ db.requireStructuredPostprocessing(key, { instruction, policy: continuationPolicy(selected, key) });
219
+ db.updateOperation(key, status, { result: { responseDigest, rawDigest, rawBytes: raw.length, retainedRawBytes: retainedRaw.length, validation: receiptValidated ? "accepted" : turn.structured?.state ?? "missing-receipt", wireContractDigest: wireContract.descriptorDigest } });
220
+ });
221
+ if (status === "confirmed" && adapter.postConfirmedDispatch) {
222
+ try {
223
+ const processed = await adapter.postConfirmedDispatch({ dispatchId: key, runId, tupleDigest: tuple.digest });
224
+ db.completeStructuredPostprocessing(key);
225
+ db.finishStructuredContinuation(key);
226
+ if (processed)
227
+ return processed.selectedDispatch?.dispatchId === key
228
+ ? { ...turn, continuity: processed.continuity, continuityErrors: processed.continuityErrors, hostInstruction: processed.hostInstruction, isError: processed.isError }
229
+ : processed;
230
+ }
231
+ catch (error) {
232
+ db.failStructuredPostprocessing(key, boundedUtf8(String(error), 4000).toString());
233
+ return { ...turn, isError: true, continuityErrors: [boundedUtf8(String(error), 4000).toString()] };
234
+ }
235
+ }
236
+ if (status === "confirmed")
237
+ db.finishStructuredContinuation(key);
238
+ return turn;
239
+ }
240
+ catch (error) {
241
+ // A failed bootstrap stays an explicit, non-resumable preparation record.
242
+ const planned = db.operation(key);
243
+ if (planned?.status === "planned")
244
+ db.updateOperation(key, "failed", { error: boundedUtf8(String(error), 4000).toString() });
245
+ else if (planned?.status === "in_progress")
246
+ db.updateOperation(key, "uncertain", { error: boundedUtf8(String(error), 4000).toString() });
247
+ throw error;
248
+ }
249
+ finally {
250
+ db.close();
251
+ }
252
+ }
253
+ /** Only unsealed host request fields survive a handoff or a restart. */
254
+ export function continuationPolicy(policy, parentDispatchId) {
255
+ return { logicalActionId: policy.logicalActionId, journalRunId: policy.journalRunId, purpose: policy.purpose,
256
+ responseOnly: policy.responseOnly, handback: policy.handback, parentDispatchId };
257
+ }
@@ -6,7 +6,7 @@ import { join } from "node:path";
6
6
  import { hostname } from "node:os";
7
7
  import { DurableSupervisor } from "./supervisor.js";
8
8
  import { WorkflowDb } from "./workflowDb.js";
9
- import { stopCause } from "rafi-spec";
9
+ import { parseHostIpc, stopCause } from "rafi-spec";
10
10
  import { processStartIdentity } from "./processIdentity.js";
11
11
  import { cancelOwnedRuntimeProbes } from "./runtimeReadiness.js";
12
12
  /** Worker IPC is created by the parent; an environment flag alone cannot bypass supervision. */
@@ -38,8 +38,9 @@ export async function superviseStart(projectDir, runId, policy, selection) {
38
38
  }
39
39
  maintainBuildAdmission(localBuildAuthority(projectDir));
40
40
  if (supervisedRunId()) {
41
+ let sequence = 0;
41
42
  const heartbeat = setInterval(() => { if (process.connected)
42
- process.send?.({ kind: "rafi-worker-heartbeat", runId }); }, 5_000);
43
+ process.send?.({ version: 1, kind: "rafi-worker-heartbeat", runId, generation: Number(process.env.RAFI_BUILD_WORKER_GENERATION), pid: process.pid, sequence: ++sequence }); }, 5_000);
43
44
  heartbeat.unref();
44
45
  process.once("disconnect", () => {
45
46
  clearInterval(heartbeat);
@@ -118,10 +119,15 @@ export async function superviseStart(projectDir, runId, policy, selection) {
118
119
  try {
119
120
  await new Promise((resolve, reject) => {
120
121
  const timer = setTimeout(() => { parent.kill("SIGTERM"); reject(new Error("Detached supervisor startup timed out; use rafi resume to reconcile")); }, 15_000);
121
- parent.once("message", () => {
122
+ let acknowledged = false;
123
+ parent.on("message", value => {
124
+ const registered = parseHostIpc(value);
125
+ if (acknowledged || registered?.kind !== "rafi-launch-registered" || registered.token !== launch.token || registered.pid !== parent.pid)
126
+ return;
127
+ acknowledged = true;
122
128
  try {
123
129
  launchDb.acknowledgeBuildLaunchChild(authority, launch.token, parent.pid);
124
- parent.send("rafi-launch-ack", error => { clearTimeout(timer); if (error)
130
+ parent.send({ version: 1, kind: "rafi-launch-ack", token: launch.token, pid: parent.pid }, error => { clearTimeout(timer); if (error)
125
131
  reject(error);
126
132
  else
127
133
  resolve(); });
@@ -174,9 +180,12 @@ export async function superviseStart(projectDir, runId, policy, selection) {
174
180
  stdio: ["inherit", "inherit", "inherit", "ipc"], detached: process.platform !== "win32",
175
181
  });
176
182
  const startup = setTimeout(() => child.kill("SIGTERM"), 15_000);
177
- child.on("message", message => {
178
- if (!message || typeof message !== "object" || !("kind" in message) || message.kind !== "rafi-launch-registered")
183
+ let acknowledged = false;
184
+ child.on("message", value => {
185
+ const registered = parseHostIpc(value);
186
+ if (acknowledged || registered?.kind !== "rafi-launch-registered" || registered.token !== launch.token || registered.pid !== child.pid)
179
187
  return;
188
+ acknowledged = true;
180
189
  try {
181
190
  db.acknowledgeBuildLaunchChild(authority, launch.token, child.pid);
182
191
  // Record the incarnation before acknowledging the blocked launch gate.
@@ -185,7 +194,7 @@ export async function superviseStart(projectDir, runId, policy, selection) {
185
194
  checkpoint: db.getRun(runId)?.checkpoint ?? "preparing", event: "worker_started",
186
195
  payload: { workerPid: child.pid, processStart: processStartIdentity(child.pid), host: hostname(), generation },
187
196
  });
188
- child.send("rafi-launch-ack", () => { });
197
+ child.send({ version: 1, kind: "rafi-launch-ack", token: launch.token, pid: child.pid }, () => { });
189
198
  clearTimeout(startup);
190
199
  }
191
200
  catch {
@@ -219,22 +228,27 @@ export async function superviseStart(projectDir, runId, policy, selection) {
219
228
  stop();
220
229
  } }, Math.min(5_000, Math.max(25, heartbeatTimeout / 4)));
221
230
  watchdog.unref();
222
- child.on("message", message => { if (message && typeof message === "object" && "kind" in message && message.kind === "rafi-worker-heartbeat")
223
- lastHeartbeat = Date.now(); });
224
- child.on("message", message => {
225
- if (!message || typeof message !== "object" || !("kind" in message) || message.kind !== "rafi-readiness-child")
226
- return;
227
- const value = message;
228
- if (!Number.isSafeInteger(value.pid) || Number(value.pid) <= 1)
231
+ let heartbeatSequence = 0;
232
+ child.on("message", value => {
233
+ const message = parseHostIpc(value);
234
+ if (acknowledged && message?.kind === "rafi-worker-heartbeat" && message.runId === runId && message.generation === generation && message.pid === child.pid && message.sequence > heartbeatSequence) {
235
+ heartbeatSequence = message.sequence;
236
+ lastHeartbeat = Date.now();
237
+ }
238
+ });
239
+ child.on("message", raw => {
240
+ const value = parseHostIpc(raw);
241
+ if (!acknowledged || value?.kind !== "rafi-readiness-child" || value.runId !== runId || value.generation !== generation || value.workerPid !== child.pid)
229
242
  return;
230
- if (typeof value.tag === "string")
231
- readinessTags.set(Number(value.pid), value.tag);
232
- if (value.active === false) {
233
- readinessChildren.delete(Number(value.pid));
234
- readinessTags.delete(Number(value.pid));
243
+ if (value.active) {
244
+ readinessTags.set(value.pid, value.tag);
245
+ if (value.processStart)
246
+ readinessChildren.set(value.pid, value.processStart);
247
+ }
248
+ else if (readinessTags.get(value.pid) === value.tag) {
249
+ readinessChildren.delete(value.pid);
250
+ readinessTags.delete(value.pid);
235
251
  }
236
- else if (typeof value.processStart === "string")
237
- readinessChildren.set(Number(value.pid), value.processStart);
238
252
  });
239
253
  const result = new Promise(resolve => {
240
254
  child.once("error", error => {