ai-foreman 1.7.29 → 2.0.1
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.
- package/README.md +1 -1
- package/dist/adapters/claude.d.ts +23 -12
- package/dist/adapters/claude.js +231 -26
- package/dist/adapters/codex.d.ts +19 -7
- package/dist/adapters/codex.js +202 -19
- package/dist/adapters/recovering.d.ts +2 -0
- package/dist/adapters/recovering.js +5 -1
- package/dist/adapters/structured.d.ts +71 -0
- package/dist/adapters/structured.js +213 -0
- package/dist/adapters/types.d.ts +85 -9
- package/dist/agentRun.d.ts +2 -0
- package/dist/agentRun.js +2 -1
- package/dist/boundedTurnEvidence.d.ts +18 -0
- package/dist/boundedTurnEvidence.js +76 -0
- package/dist/branch/currentGuard.d.ts +2 -0
- package/dist/branch/currentGuard.js +2 -0
- package/dist/branch/git.js +3 -8
- package/dist/branch/planner.js +1 -4
- package/dist/branch/runner.d.ts +2 -1
- package/dist/branch/runner.js +10 -7
- package/dist/buildAdmission.js +4 -0
- package/dist/buildApproval.d.ts +15 -0
- package/dist/buildApproval.js +28 -9
- package/dist/buildAssignment.d.ts +1 -0
- package/dist/buildAssignment.js +82 -23
- package/dist/buildInterventions.d.ts +2 -2
- package/dist/buildInterventions.js +11 -4
- package/dist/buildLaunchGate.js +3 -2
- package/dist/buildRuns.d.ts +1 -0
- package/dist/buildRuns.js +6 -1
- package/dist/builderGuidanceFollowup.js +17 -6
- package/dist/cli/doctor.js +3 -1
- package/dist/cli/manager.js +22 -20
- package/dist/cli/start.js +85 -39
- package/dist/cli/tickets.d.ts +0 -8
- package/dist/cli/tickets.js +50 -60
- package/dist/continuity.d.ts +10 -16
- package/dist/continuity.js +223 -208
- package/dist/continuityOrigin.d.ts +13 -0
- package/dist/continuityOrigin.js +91 -0
- package/dist/dispatchNotSent.d.ts +4 -0
- package/dist/dispatchNotSent.js +10 -0
- package/dist/foreman.d.ts +15 -16
- package/dist/foreman.js +70 -278
- package/dist/gitExclude.d.ts +16 -0
- package/dist/gitExclude.js +46 -0
- package/dist/graph/boundary.d.ts +1 -1
- package/dist/graph/boundary.js +1 -1
- package/dist/graph/host.js +8 -19
- package/dist/graph/manager.d.ts +1 -1
- package/dist/graph/manager.js +1 -1
- package/dist/graph/preparation.d.ts +38 -0
- package/dist/graph/preparation.js +7 -6
- package/dist/graph/semantic.d.ts +1 -0
- package/dist/graph/semantic.js +4 -1
- package/dist/graph/session.js +7 -2
- package/dist/graph/turn.d.ts +3 -4
- package/dist/graph/turn.js +29 -41
- package/dist/handoffProtocol.js +2 -2
- package/dist/handoffs.d.ts +5 -3
- package/dist/handoffs.js +92 -88
- package/dist/hostGraphApplicability.d.ts +2 -1
- package/dist/hostGraphApplicability.js +17 -19
- package/dist/hostQaAugmentation.js +21 -4
- package/dist/hostQaComparison.d.ts +21 -2
- package/dist/hostQaComparison.js +43 -29
- package/dist/hostSourcePreservation.d.ts +38 -0
- package/dist/hostSourcePreservation.js +72 -0
- package/dist/humanDecision.d.ts +7 -0
- package/dist/humanDecision.js +11 -0
- package/dist/interviews.d.ts +7 -0
- package/dist/interviews.js +18 -1
- package/dist/legacyCommunicationDisplay.d.ts +2 -0
- package/dist/legacyCommunicationDisplay.js +2 -0
- package/dist/managerPacket.js +3 -3
- package/dist/markers.d.ts +4 -3
- package/dist/markers.js +4 -20
- package/dist/observability.js +3 -8
- package/dist/probeIpc.d.ts +14 -0
- package/dist/probeIpc.js +29 -0
- package/dist/qaBuildGate.d.ts +21 -0
- package/dist/qaBuildGate.js +60 -13
- package/dist/qaBuilderCoverage.js +22 -13
- package/dist/qaBuilderEvidence.d.ts +16 -1
- package/dist/qaBuilderEvidence.js +48 -8
- package/dist/qaContractCoverage.d.ts +2 -2
- package/dist/qaContractCoverage.js +4 -9
- package/dist/qaContractDelivery.js +34 -20
- package/dist/qaCoverageAssembly.d.ts +22 -0
- package/dist/qaCoverageAssembly.js +16 -0
- package/dist/qaDeliveryJournal.d.ts +2 -0
- package/dist/qaDeliveryJournal.js +19 -0
- package/dist/qaFailureDelivery.js +97 -85
- package/dist/qaHostTransformation.d.ts +26 -0
- package/dist/qaHostTransformation.js +100 -0
- package/dist/qaPreparation.d.ts +8 -20
- package/dist/qaPreparation.js +153 -254
- package/dist/qaPreparationInputs.js +19 -0
- package/dist/qaPreparationStore.d.ts +11 -0
- package/dist/qaPreparationStore.js +44 -4
- package/dist/qaPrerequisites.d.ts +2 -1
- package/dist/qaPrerequisites.js +21 -1
- package/dist/qaProtocolV2.d.ts +15 -0
- package/dist/qaRecovery.d.ts +0 -3
- package/dist/qaRecovery.js +21 -27
- package/dist/qaReview.js +241 -406
- package/dist/qaReviewEvidence.d.ts +7 -3
- package/dist/qaReviewEvidence.js +15 -4
- package/dist/qaRuntime.js +1 -1
- package/dist/qaSnapshot.js +76 -21
- package/dist/qaStructuredResponse.d.ts +18 -0
- package/dist/qaStructuredResponse.js +81 -0
- package/dist/qaTimeline.js +2 -2
- package/dist/qaVerificationContract.d.ts +1 -2
- package/dist/qaVerificationContract.js +24 -35
- package/dist/readinessGate.js +5 -4
- package/dist/recoveryOwners.d.ts +32 -0
- package/dist/recoveryOwners.js +87 -0
- package/dist/runtimePreflight.d.ts +11 -0
- package/dist/runtimePreflight.js +77 -0
- package/dist/runtimeReadiness.js +23 -15
- package/dist/sources/sourceRegistry.d.ts +0 -3
- package/dist/sources/sourceRegistry.js +0 -42
- package/dist/statusReporter.d.ts +2 -0
- package/dist/statusReporter.js +2 -0
- package/dist/structuredCutover.d.ts +31 -0
- package/dist/structuredCutover.js +510 -0
- package/dist/structuredCutoverFiles.d.ts +10 -0
- package/dist/structuredCutoverFiles.js +181 -0
- package/dist/structuredDispatchAuthority.d.ts +22 -0
- package/dist/structuredDispatchAuthority.js +110 -0
- package/dist/structuredDispatchRecord.d.ts +29 -0
- package/dist/structuredDispatchRecord.js +36 -0
- package/dist/structuredLiveBudget.d.ts +2 -0
- package/dist/structuredLiveBudget.js +23 -0
- package/dist/structuredOperation.d.ts +18 -0
- package/dist/structuredOperation.js +279 -0
- package/dist/supervisedStart.js +35 -21
- package/dist/ticketPlanning.d.ts +1 -376
- package/dist/ticketPlanning.js +2 -49
- package/dist/ticketPopulation.d.ts +0 -3
- package/dist/ticketPopulation.js +4 -21
- package/dist/ticketPopulationPageAuthority.d.ts +13 -0
- package/dist/ticketPopulationPageAuthority.js +30 -0
- package/dist/ticketPopulationPaging.d.ts +27 -0
- package/dist/ticketPopulationPaging.js +127 -0
- package/dist/tickets/renderMarkdown.d.ts +14 -0
- package/dist/tickets/renderMarkdown.js +39 -5
- package/dist/tickets/ticketLoader.js +10 -0
- package/dist/tickets/ticketSchema.d.ts +107 -0
- package/dist/tickets/ticketSchema.js +4 -1
- package/dist/workflowDb.d.ts +116 -3
- package/dist/workflowDb.js +852 -61
- package/dist/workflowReader.d.ts +1 -0
- package/dist/workflowReader.js +9 -0
- package/package.json +23 -7
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { sealTicketPopulationPage } from "./ticketPopulationPageAuthority.js";
|
|
2
|
+
import { dispatchWasNotSent } from "./dispatchNotSent.js";
|
|
3
|
+
import { boundedUtf8 } from "./boundedTurnEvidence.js";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { assertIntermediateResultV2, historicProviderWireContract, providerWireContract, renderProviderWireInstruction, selectedDispatchTupleV2, structuredDigest, validateStructuredCandidate } from "rafi-spec";
|
|
6
|
+
import { WorkflowDb } from "./workflowDb.js";
|
|
7
|
+
import { withSealedDispatch, currentFreshRecoveryValidation } from "./structuredDispatchAuthority.js";
|
|
8
|
+
import { sealedDispatchRecord, verifyPersistedDispatch } from "./structuredDispatchRecord.js";
|
|
9
|
+
export function operationInstruction(contract) {
|
|
10
|
+
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.`;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Actionable host message for a failed structured turn. The response text of an
|
|
14
|
+
* otherwise valid turn is evidence, not the error; prefer retained host
|
|
15
|
+
* failures, then the native rejection, then provider diagnostics.
|
|
16
|
+
*/
|
|
17
|
+
export function structuredTurnErrorMessage(turn, label = "Structured provider turn") {
|
|
18
|
+
if (turn.continuityErrors?.length)
|
|
19
|
+
return `${label} failed continuity postprocessing: ${turn.continuityErrors.join("; ")}`;
|
|
20
|
+
if (turn.structured?.state === "rejected")
|
|
21
|
+
return `${label} was rejected (${turn.structured.reason}): ${turn.structured.detail}`;
|
|
22
|
+
if (turn.failure)
|
|
23
|
+
return `${label} failed (${turn.failure.category}, ${turn.failure.phase}): ${boundedUtf8(turn.failure.diagnostics, 2000).toString()}`;
|
|
24
|
+
return turn.text || `${label} failed without diagnostics`;
|
|
25
|
+
}
|
|
26
|
+
export function operationResponse(turn, contract) {
|
|
27
|
+
if (turn.isError || turn.failure)
|
|
28
|
+
throw new Error(turn.structured?.state === "rejected" ? turn.structured.detail : turn.text || "Provider execution did not complete");
|
|
29
|
+
if (!turn.selectedDispatch)
|
|
30
|
+
throw new Error("Structured provider turn has no sealed selected dispatch tuple");
|
|
31
|
+
const descriptor = historicProviderWireContract(contract, turn.selectedDispatch);
|
|
32
|
+
const response = validateStructuredCandidate(contract, turn.structured, descriptor, turn.selectedDispatch);
|
|
33
|
+
if (response.result.kind.endsWith("-request"))
|
|
34
|
+
assertIntermediateResultV2(response.result);
|
|
35
|
+
return response;
|
|
36
|
+
}
|
|
37
|
+
export function operationStatus(turn, contract, ticket) {
|
|
38
|
+
try {
|
|
39
|
+
const { result } = operationResponse(turn, contract);
|
|
40
|
+
if (result.kind === "completed") {
|
|
41
|
+
return { kind: "done", summary: result.summary, ticket };
|
|
42
|
+
}
|
|
43
|
+
if (result.kind === "blocked")
|
|
44
|
+
return { kind: "blocked", reason: result.reason, next: result.nextAction, ticket, ...(result.branchDependency ? { branchDependency: result.branchDependency } : {}) };
|
|
45
|
+
if (result.kind === "needs-input")
|
|
46
|
+
return { kind: "needs_input", question: result.question, choices: result.choices, ticket };
|
|
47
|
+
if (result.kind === "source-request")
|
|
48
|
+
return { kind: "needs_input", question: "Additional source material is required. Supply or authorize the requested source content.", choices: [], ticket };
|
|
49
|
+
return { kind: "unknown", error: `Unconsumed intermediate result: ${result.kind}` };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return { kind: "unknown", error: boundedUtf8(String(error), 4000).toString() };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function operationPayload(turn, contract) {
|
|
56
|
+
const response = operationResponse(turn, contract);
|
|
57
|
+
if (response.result.kind !== "completed")
|
|
58
|
+
throw new Error(`Operation ${contract.operation} returned ${response.result.kind}`);
|
|
59
|
+
return response.result.payload;
|
|
60
|
+
}
|
|
61
|
+
export function nativePolicy(output, policy = {}) {
|
|
62
|
+
return { ...policy, output, logicalActionId: policy.logicalActionId ?? randomUUID(), dispatchId: policy.dispatchId ?? randomUUID() };
|
|
63
|
+
}
|
|
64
|
+
/** Reuses the workflow journal. A receipt is retained before domain interpretation. */
|
|
65
|
+
export async function journalStructuredDispatch(root, adapter, instruction, policy, dispatch) {
|
|
66
|
+
if (!policy.output)
|
|
67
|
+
return dispatch(instruction, policy);
|
|
68
|
+
if (!adapter.prepareSession)
|
|
69
|
+
throw new Error("Structured dispatch requires an adapter that prepares a scoped provider session");
|
|
70
|
+
const selected = nativePolicy(policy.output, policy);
|
|
71
|
+
// Decorator-owned requirements (such as mandatory continuity) become part of
|
|
72
|
+
// the sealed provider instruction; they are never appended after sealing.
|
|
73
|
+
const requirement = adapter.structuredDispatchRequirement?.();
|
|
74
|
+
const providerInstruction = requirement ? `${instruction}\n\n${requirement.instruction}` : instruction;
|
|
75
|
+
const key = selected.dispatchId;
|
|
76
|
+
const runId = selected.journalRunId ?? `operation:${selected.logicalActionId}`;
|
|
77
|
+
const db = new WorkflowDb(root);
|
|
78
|
+
try {
|
|
79
|
+
db.enableStructuredCommunication();
|
|
80
|
+
// A single agent call outside a build journals under its own run, which is
|
|
81
|
+
// never listed or resumed as a build.
|
|
82
|
+
db.ensureRun(runId);
|
|
83
|
+
if (adapter.postConfirmedDispatch)
|
|
84
|
+
for (const pendingId of db.pendingStructuredPostprocessing(runId)) {
|
|
85
|
+
if (pendingId === selected.parentDispatchId)
|
|
86
|
+
continue;
|
|
87
|
+
const pending = db.operation(pendingId);
|
|
88
|
+
if (pending?.status !== "confirmed")
|
|
89
|
+
throw new Error("Postprocessing origin is not confirmed");
|
|
90
|
+
const tuple = db.structuredOwnerDispatch(pendingId);
|
|
91
|
+
// Rehydrate and publish locally; this path never calls raw transport.
|
|
92
|
+
await adapter.postConfirmedDispatch({ dispatchId: pendingId, runId, tupleDigest: tuple.tupleDigest });
|
|
93
|
+
db.completeStructuredPostprocessing(pendingId);
|
|
94
|
+
db.finishStructuredContinuation(pendingId);
|
|
95
|
+
}
|
|
96
|
+
db.assertStructuredDispatchAdmission(runId, key, selected.parentDispatchId, policy.output.operation);
|
|
97
|
+
const prior = db.planOperation({ runId, idempotencyKey: key, kind: "provider-dispatch", intent: {
|
|
98
|
+
protocolVersion: 2, state: "preparing", logicalActionId: selected.logicalActionId,
|
|
99
|
+
operation: policy.output.operation, schemaVersion: policy.output.schemaVersion, schemaDigest: policy.output.schemaDigest,
|
|
100
|
+
instructionDigest: structuredDigest(providerInstruction), purpose: selected.purpose ?? "implementation",
|
|
101
|
+
} });
|
|
102
|
+
if (prior.status !== "planned")
|
|
103
|
+
throw new Error("Existing structured dispatch requires reconciliation; it cannot be replayed");
|
|
104
|
+
// Session bootstrap can itself contact the provider. The non-authoritative
|
|
105
|
+
// preparation record above is durable before that boundary is crossed.
|
|
106
|
+
const prepared = await adapter.prepareSession();
|
|
107
|
+
if (!prepared.sessionId || !Number.isSafeInteger(prepared.generation) || prepared.generation < 0)
|
|
108
|
+
throw new Error("Adapter did not prepare a valid provider session for structured dispatch");
|
|
109
|
+
const wireContract = providerWireContract(policy.output, adapter.agent, "default", { requireRegistered: true });
|
|
110
|
+
if (policy.wireContract && policy.wireContract.descriptorDigest !== wireContract.descriptorDigest)
|
|
111
|
+
throw new Error("New dispatch may select only the static active descriptor");
|
|
112
|
+
if (wireContract.provider !== adapter.agent || wireContract.canonical.schemaDigest !== policy.output.schemaDigest)
|
|
113
|
+
throw new Error("Structured dispatch wire contract does not match its adapter/output contract");
|
|
114
|
+
if (policy.renderedWireContractDigest && policy.renderedWireContractDigest !== wireContract.descriptorDigest)
|
|
115
|
+
throw new Error("Structured dispatch guidance does not match its selected wire contract");
|
|
116
|
+
const renderedInstruction = renderProviderWireInstruction(providerInstruction, wireContract);
|
|
117
|
+
const sealedSelection = nativePolicy(policy.output, { ...selected, wireContract, renderedWireContractDigest: wireContract.descriptorDigest });
|
|
118
|
+
const before = adapter.sessionRef?.();
|
|
119
|
+
if (!before || before.sessionId !== prepared.sessionId || before.generation !== prepared.generation)
|
|
120
|
+
throw new Error("Prepared provider session is not the adapter's active scoped session");
|
|
121
|
+
const tuple = selectedDispatchTupleV2({
|
|
122
|
+
operation: policy.output.operation, schemaVersion: policy.output.schemaVersion, schemaDigest: policy.output.schemaDigest,
|
|
123
|
+
provider: adapter.agent, profile: wireContract.profile, variant: wireContract.variant, descriptorDigest: wireContract.descriptorDigest,
|
|
124
|
+
wireSchemaDigest: wireContract.wireSchemaDigest, guidanceDigest: wireContract.guidanceDigest,
|
|
125
|
+
normalizerId: wireContract.normalizerId, normalizerVersion: wireContract.normalizerVersion,
|
|
126
|
+
maximumSchemaBytes: wireContract.maximumSchemaBytes, maximumResponseBytes: wireContract.maximumResponseBytes,
|
|
127
|
+
maximumPageBytes: wireContract.maximumPageBytes ?? null, instructionDigest: structuredDigest(renderedInstruction),
|
|
128
|
+
actionId: sealedSelection.logicalActionId, dispatchId: sealedSelection.dispatchId, sessionId: before.sessionId, generation: before.generation,
|
|
129
|
+
});
|
|
130
|
+
const sealedPolicy = Object.freeze({ ...sealedSelection, selectedDispatch: tuple });
|
|
131
|
+
const compactionBefore = adapter.contractCompactionSequence?.() ?? 0;
|
|
132
|
+
const intent = { protocolVersion: 2, tuple, sealedDispatch: sealedDispatchRecord(tuple), logicalActionId: selected.logicalActionId, parentDispatchId: selected.parentDispatchId ?? null,
|
|
133
|
+
// The durable role is derived from the scoped provider session, never a
|
|
134
|
+
// caller-controlled string on TurnRequest.
|
|
135
|
+
role: before.role, sessionRef: before, operation: policy.output.operation,
|
|
136
|
+
schemaVersion: policy.output.schemaVersion, schemaDigest: policy.output.schemaDigest, instructionDigest: structuredDigest(renderedInstruction),
|
|
137
|
+
purpose: selected.purpose ?? "implementation", responseOnly: selected.responseOnly === true,
|
|
138
|
+
freshRecoveryValidation: currentFreshRecoveryValidation(root, runId),
|
|
139
|
+
wire: { profile: wireContract.profile, variant: wireContract.variant, wireVersion: wireContract.wireVersion,
|
|
140
|
+
descriptorDigest: wireContract.descriptorDigest, wireSchemaDigest: wireContract.wireSchemaDigest,
|
|
141
|
+
maximumSchemaBytes: wireContract.maximumSchemaBytes, maximumResponseBytes: wireContract.maximumResponseBytes,
|
|
142
|
+
maximumPageBytes: wireContract.maximumPageBytes ?? null, normalizerId: wireContract.normalizerId,
|
|
143
|
+
normalizerVersion: wireContract.normalizerVersion, guidanceId: wireContract.guidanceId } };
|
|
144
|
+
const priorRecord = db.operation(key);
|
|
145
|
+
if (priorRecord.status !== "planned")
|
|
146
|
+
throw new Error("Existing structured dispatch requires reconciliation; it cannot be replayed");
|
|
147
|
+
db.atomic(() => {
|
|
148
|
+
db.linkStructuredHandoffDispatch(key, selected.parentDispatchId, policy.output.operation);
|
|
149
|
+
db.sealPlannedOperationIntent(key, intent);
|
|
150
|
+
db.sealStructuredOwnerDispatch(key, tuple, renderedInstruction);
|
|
151
|
+
sealTicketPopulationPage(db, root, tuple);
|
|
152
|
+
db.updateOperation(key, "in_progress");
|
|
153
|
+
});
|
|
154
|
+
let turn;
|
|
155
|
+
try {
|
|
156
|
+
turn = await withSealedDispatch(root, tuple, dispatchCapability => dispatch(renderedInstruction, { ...sealedPolicy, journalRunId: runId, dispatchCapability }), before);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
// A host refusal before transport is a known unsent turn, not an unknown outcome.
|
|
160
|
+
if (db.operation(key)?.status === "in_progress")
|
|
161
|
+
db.updateOperation(key, dispatchWasNotSent(error) ? "failed" : "uncertain", { error: boundedUtf8(String(error), 4000).toString(), ...(dispatchWasNotSent(error) ? { result: { dispatchState: "not-sent" } } : {}) });
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
const after = adapter.sessionRef?.();
|
|
165
|
+
const sameScope = before && after && before.provider === after.provider && before.role === after.role &&
|
|
166
|
+
before.stream === after.stream && before.cwd === after.cwd && before.configRoot === after.configRoot &&
|
|
167
|
+
before.workspaceIdentity === after.workspaceIdentity && before.ticketId === after.ticketId && before.deliveryUnitId === after.deliveryUnitId;
|
|
168
|
+
// A receipt from another dispatch, session or generation leaves this
|
|
169
|
+
// dispatch's own outcome unknown.
|
|
170
|
+
let receiptMismatch = false;
|
|
171
|
+
const observedCompaction = sameScope && (adapter.contractCompactionSequence?.() ?? 0) > compactionBefore && after.generation >= before.generation;
|
|
172
|
+
if (turn.structured?.state === "candidate" && (turn.structured.profile !== wireContract.profile || turn.structured.wireContractDigest !== wireContract.descriptorDigest ||
|
|
173
|
+
turn.structured.wireSchemaDigest !== wireContract.wireSchemaDigest || turn.structured.wireVariant !== wireContract.variant || turn.structured.dispatchId !== key || turn.structured.sessionId !== adapter.sessionId() ||
|
|
174
|
+
before && (!sameScope || !observedCompaction && (before.sessionId !== after.sessionId || before.generation !== after.generation)))) {
|
|
175
|
+
turn = { ...turn, selectedDispatch: tuple, isError: true, structured: { state: "rejected", reason: "invalid-contract", detail: "Dispatch/session/generation receipt mismatch", dispatchId: key } };
|
|
176
|
+
receiptMismatch = true;
|
|
177
|
+
}
|
|
178
|
+
// Exact raw provider material is retained once in the evidence store. The
|
|
179
|
+
// journal only carries the digest/reference and validation fact, so it
|
|
180
|
+
// cannot become a second, differently-redacted copy of the provider turn.
|
|
181
|
+
turn = { ...turn, selectedDispatch: tuple };
|
|
182
|
+
let receiptValidated = false;
|
|
183
|
+
let requirementRejected = false;
|
|
184
|
+
if (turn.structured?.state === "rejected")
|
|
185
|
+
turn = { ...turn, isError: true };
|
|
186
|
+
else if (!turn.isError && !turn.failure) {
|
|
187
|
+
try {
|
|
188
|
+
const response = verifyPersistedDispatch(policy.output, db.structuredOwnerDispatch(key), turn.structured, tuple);
|
|
189
|
+
// A response that cannot satisfy host requirements is rejected before
|
|
190
|
+
// confirmation instead of failing in postprocessing after it.
|
|
191
|
+
try {
|
|
192
|
+
requirement?.validate(response);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
requirementRejected = true;
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
receiptValidated = true;
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
turn = { ...turn, selectedDispatch: tuple, isError: true, structured: { state: "rejected", reason: "invalid-contract", detail: boundedUtf8(String(error), 4000).toString(), dispatchId: key } };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const retained = { version: 2, tupleDigest: tuple.digest, hostTurnId: turn.turnId, receipt: turn.structured ?? null };
|
|
205
|
+
if (turn.structured?.state === "candidate") {
|
|
206
|
+
const { value, ...receipt } = turn.structured;
|
|
207
|
+
const bytes = Buffer.from(receipt.representation === "json-text" ? value : JSON.stringify(value));
|
|
208
|
+
if (bytes.length > wireContract.maximumResponseBytes)
|
|
209
|
+
throw new Error("Native receipt exceeds selected evidence budget");
|
|
210
|
+
retained.valueDigest = db.putEvidence("handoff", bytes);
|
|
211
|
+
retained.receipt = receipt;
|
|
212
|
+
}
|
|
213
|
+
if (turn.structured?.state === "rejected") {
|
|
214
|
+
const { observedValue, ...receipt } = turn.structured;
|
|
215
|
+
retained.receipt = { ...receipt, detail: boundedUtf8(receipt.detail, 4000).toString() };
|
|
216
|
+
if (observedValue) {
|
|
217
|
+
const bytes = Buffer.from(observedValue.representation === "json-text" ? String(observedValue.value) : JSON.stringify(observedValue.value));
|
|
218
|
+
const bounded = boundedUtf8(bytes.toString(), wireContract.maximumResponseBytes);
|
|
219
|
+
retained.rejectedValue = { representation: observedValue.representation, digest: observedValue.digest,
|
|
220
|
+
evidenceDigest: db.putEvidence("handoff", bounded), originalBytes: bytes.length, retainedBytes: bounded.length };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const raw = Buffer.from(turn.rawResponse ?? turn.text);
|
|
224
|
+
const retainedRaw = boundedUtf8(turn.rawResponse ?? turn.text, wireContract.maximumResponseBytes);
|
|
225
|
+
const rawDigest = db.putEvidence("handoff", retainedRaw);
|
|
226
|
+
const responseDigest = db.putEvidence("handoff", Buffer.from(JSON.stringify(retained)));
|
|
227
|
+
db.recordCommunicationEvidenceRefs(key, [responseDigest, rawDigest, ...(retained.valueDigest ? [retained.valueDigest] : []), ...(retained.rejectedValue ? [retained.rejectedValue.evidenceDigest] : [])]);
|
|
228
|
+
// A provider that finished its turn with a response that fails validation
|
|
229
|
+
// has a known outcome: the answer was rejected. Only a missing or
|
|
230
|
+
// interrupted turn leaves the outcome unknown.
|
|
231
|
+
// Only the answer's own content counts: a wire-schema violation or a failed
|
|
232
|
+
// host requirement. Binding and receipt failures stay unknown.
|
|
233
|
+
const answeredInvalid = !receiptValidated && !turn.failure && !receiptMismatch && turn.structured?.state === "rejected" && turn.structured.reason === "invalid-contract"
|
|
234
|
+
&& (requirementRejected || turn.structured.detail.includes("Structured result violates wire schema"));
|
|
235
|
+
const status = turn.failure?.dispatchState === "not-sent" || answeredInvalid ? "failed" : turn.failure?.dispatchState === "unknown" || !receiptValidated ? "uncertain" : "confirmed";
|
|
236
|
+
turn = { ...turn, transportEvidence: { rawDigest, responseDigest, rawBytes: raw.length, retainedRawBytes: retainedRaw.length,
|
|
237
|
+
valueDigest: retained.valueDigest, rejectedValueDigest: retained.rejectedValue?.evidenceDigest } };
|
|
238
|
+
db.atomic(() => {
|
|
239
|
+
if (status === "confirmed" && adapter.postConfirmedDispatch)
|
|
240
|
+
db.requireStructuredPostprocessing(key, { instruction, policy: continuationPolicy(selected, key) });
|
|
241
|
+
db.updateOperation(key, status, { result: { responseDigest, rawDigest, rawBytes: raw.length, retainedRawBytes: retainedRaw.length, validation: receiptValidated ? "accepted" : turn.structured?.state ?? "missing-receipt", wireContractDigest: wireContract.descriptorDigest, ...(answeredInvalid ? { dispatchState: "answered-invalid" } : {}) } });
|
|
242
|
+
});
|
|
243
|
+
if (status === "confirmed" && adapter.postConfirmedDispatch) {
|
|
244
|
+
try {
|
|
245
|
+
const processed = await adapter.postConfirmedDispatch({ dispatchId: key, runId, tupleDigest: tuple.digest });
|
|
246
|
+
db.completeStructuredPostprocessing(key);
|
|
247
|
+
db.finishStructuredContinuation(key);
|
|
248
|
+
if (processed)
|
|
249
|
+
return processed.selectedDispatch?.dispatchId === key
|
|
250
|
+
? { ...turn, continuity: processed.continuity, continuityErrors: processed.continuityErrors, hostInstruction: processed.hostInstruction, isError: processed.isError }
|
|
251
|
+
: processed;
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
db.failStructuredPostprocessing(key, boundedUtf8(String(error), 4000).toString());
|
|
255
|
+
return { ...turn, isError: true, continuityErrors: [boundedUtf8(String(error), 4000).toString()] };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (status === "confirmed")
|
|
259
|
+
db.finishStructuredContinuation(key);
|
|
260
|
+
return turn;
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
// A failed bootstrap stays an explicit, non-resumable preparation record.
|
|
264
|
+
const planned = db.operation(key);
|
|
265
|
+
if (planned?.status === "planned")
|
|
266
|
+
db.updateOperation(key, "failed", { error: boundedUtf8(String(error), 4000).toString() });
|
|
267
|
+
else if (planned?.status === "in_progress")
|
|
268
|
+
db.updateOperation(key, "uncertain", { error: boundedUtf8(String(error), 4000).toString() });
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
db.close();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** Only unsealed host request fields survive a handoff or a restart. */
|
|
276
|
+
export function continuationPolicy(policy, parentDispatchId) {
|
|
277
|
+
return { logicalActionId: policy.logicalActionId, journalRunId: policy.journalRunId, purpose: policy.purpose,
|
|
278
|
+
responseOnly: policy.responseOnly, handback: policy.handback, parentDispatchId };
|
|
279
|
+
}
|
package/dist/supervisedStart.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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 (
|
|
231
|
-
readinessTags.set(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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 => {
|