@sublang/playbook 16.0.0 → 17.1.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.
- package/README.md +2 -0
- package/docs/cli.md +36 -18
- package/docs/configuration.md +26 -20
- package/docs/embedding.md +28 -7
- package/package.json +4 -4
- package/reference/sdlc/captain.md +19 -10
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +6 -2
- package/reference/sdlc/captain.playbook/captain.fsm.js +19 -7
- package/reference/sdlc/captain.playbook/captain.fsm.ts +25 -10
- package/reference/sdlc/captain.playbook/captain.gears.md +16 -8
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +1 -1
- package/reference/sdlc/captain.playbook/captain.playbook.js +3 -2
- package/reference/sdlc/captain.playbook/captain.playbook.ts +4 -3
- package/reference/sdlc/code.playbook/bin/interactive-session.js +14 -1
- package/reference/sdlc/code.playbook/bin/launch-config.js +5 -3
- package/reference/sdlc/code.playbook/bin/portable-codec.js +3 -0
- package/reference/sdlc/code.playbook/bin/repository-effects.js +1 -0
- package/reference/sdlc/code.playbook/bin/run.js +128 -85
- package/reference/sdlc/code.playbook/bin/session-host.js +40 -8
- package/reference/sdlc/code.playbook/bin/session-store.js +162 -34
- package/reference/sdlc/code.playbook/code.fsm.d.ts +5 -5
- package/reference/sdlc/code.playbook/host-capabilities.d.ts +3 -0
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +32 -2
- package/reference/sdlc/code.playbook/playbook-captain.js +729 -284
- package/reference/sdlc/code.playbook/playbook-captain.ts +771 -358
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +4 -4
- package/reference/sdlc/code.playbook/session-host.d.ts +4 -1
- package/reference/sdlc/code.playbook/session-store.d.ts +17 -0
- package/reference/sdlc/code.playbook/session-store.js +1 -0
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +5 -5
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +5 -5
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +5 -5
- package/slc/gears2fsm.md +5 -5
- package/slc/link.md +77 -33
- package/src/runtime.d.ts +46 -2
- package/src/runtime.ts +42 -2
- package/src/xstate-playbook-runtime.d.ts +9 -8
- package/src/xstate-playbook-runtime.js +471 -215
- package/src/xstate-playbook-runtime.ts +525 -238
- package/src/xstate-runtime.d.ts +3 -4
- package/src/xstate-runtime.js +52 -0
- package/src/xstate-runtime.ts +59 -4
|
@@ -966,6 +966,7 @@ function buildGovernedJudgePrompt(input, finalText, outcomes, correction) {
|
|
|
966
966
|
'```',
|
|
967
967
|
'',
|
|
968
968
|
'Pick exactly one declared `guard` and reply with exactly that outcome\'s reply shape below: `guard` plus its semantic-owned fields and nothing else.',
|
|
969
|
+
'If no declared outcome matches the player output, instead reply exactly {"blocked":"explain the missing outcome or contradictory requirements"}. Never force a match or invent a guard. A blocked reply pauses the playbook for Boss; it is not completion.',
|
|
969
970
|
'A field listed as runtime-supplied is owned by presentation, effect, or runtime evidence; the runtime fills it itself, and a reply that includes one is structurally invalid.',
|
|
970
971
|
'',
|
|
971
972
|
];
|
|
@@ -1045,7 +1046,7 @@ function validateBossReplyOutput(input, output, resumableStateIds) {
|
|
|
1045
1046
|
}
|
|
1046
1047
|
}
|
|
1047
1048
|
export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onControlPlaneError) {
|
|
1048
|
-
|
|
1049
|
+
const execute = async ({ input, signal }) => {
|
|
1049
1050
|
const activeSignal = combineAbortSignals(signal, getActiveSignal?.());
|
|
1050
1051
|
let roleId;
|
|
1051
1052
|
let prompt;
|
|
@@ -1113,7 +1114,9 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
|
|
|
1113
1114
|
}
|
|
1114
1115
|
throw error;
|
|
1115
1116
|
}
|
|
1116
|
-
}
|
|
1117
|
+
};
|
|
1118
|
+
return fromPromise((args) => spec.run
|
|
1119
|
+
? spec.run(args.input, args.signal, () => execute(args)) : execute(args));
|
|
1117
1120
|
}
|
|
1118
1121
|
// ---------------------------------------------------------------------------
|
|
1119
1122
|
// Direct-Captain adjudication (slc/link.md §Captain adjudication). The judge
|
|
@@ -1975,10 +1978,11 @@ function makeDefaultStatusesForState(roleStates) {
|
|
|
1975
1978
|
}
|
|
1976
1979
|
return [
|
|
1977
1980
|
...statuses,
|
|
1978
|
-
{ message: `${askerLabel(pending.asker)} asks: ${pending.question}
|
|
1981
|
+
{ message: `${askerLabel(pending.asker)} asks: ${pending.question}`, data: { kind: 'boss-question' } },
|
|
1979
1982
|
{
|
|
1980
1983
|
message: `◆ awaiting Boss reply · ${pending.resumeStateId} · ` +
|
|
1981
1984
|
`${askerLabel(pending.asker)} · ${pending.sourceItem}`,
|
|
1985
|
+
data: { kind: 'boss-question' },
|
|
1982
1986
|
},
|
|
1983
1987
|
];
|
|
1984
1988
|
}
|
|
@@ -2572,9 +2576,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2572
2576
|
for (const pending of questions) {
|
|
2573
2577
|
statuses.push({
|
|
2574
2578
|
message: `${askerLabel(pending.asker)} asks: ${pending.question}`,
|
|
2579
|
+
data: { kind: 'boss-question' },
|
|
2575
2580
|
}, {
|
|
2576
2581
|
message: `◆ awaiting Boss reply · ${pending.resumeStateId} · ` +
|
|
2577
2582
|
`${askerLabel(pending.asker)} · ${pending.sourceItem}`,
|
|
2583
|
+
data: { kind: 'boss-question' },
|
|
2578
2584
|
});
|
|
2579
2585
|
}
|
|
2580
2586
|
continue;
|
|
@@ -2638,6 +2644,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2638
2644
|
}
|
|
2639
2645
|
const currentEffectLedger = () => assertPlaybookEffectLedger(effectLedgerCapability.snapshot(), `${label} current host effect ledger`);
|
|
2640
2646
|
let effectLedgerMirror = currentEffectLedger();
|
|
2647
|
+
let recoveryCheckpoint;
|
|
2641
2648
|
let retainedEffectSourceSessionId;
|
|
2642
2649
|
let retainedEffectReconciliation;
|
|
2643
2650
|
let retainedEffectReconciliationRequired = false;
|
|
@@ -2739,12 +2746,6 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2739
2746
|
let playbookCallSequence = 0;
|
|
2740
2747
|
let captainCallSequence = 0;
|
|
2741
2748
|
let applyCallSequence = 0;
|
|
2742
|
-
// DR-029: the last event a public Boss boundary sent into the
|
|
2743
|
-
// machine — classified, deterministic entry, or Boss reply — kept with
|
|
2744
|
-
// its recorded payload so a failure-state retry action can replay the
|
|
2745
|
-
// event that drove the run into `failed`. Process-local: the durable
|
|
2746
|
-
// runtime snapshot does not persist it (PBRT-50).
|
|
2747
|
-
let lastBossEvent;
|
|
2748
2749
|
// DR-029: process-local at-most-once `apply` execution — the accepted receipt
|
|
2749
2750
|
// recorded for each idempotency key, returned verbatim on a repeated
|
|
2750
2751
|
// key. A key whose call settled `rejected` or threw before reaching
|
|
@@ -2919,7 +2920,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2919
2920
|
// cumulative receipt from its original baseline, not by its own step.
|
|
2920
2921
|
function boundaryExcludesEffect(boundary) {
|
|
2921
2922
|
return (boundary.logicalOperationId === undefined &&
|
|
2922
|
-
boundary.physicalReceipt?.classification === 'unchanged');
|
|
2923
|
+
(boundary.physicalReceipt?.classification === 'unchanged' || boundary.restored !== undefined));
|
|
2923
2924
|
}
|
|
2924
2925
|
function boundaryNeedsSemanticReconciliation(candidate, ledger) {
|
|
2925
2926
|
if (!runtimeBoundaryIsOwned(candidate))
|
|
@@ -2974,7 +2975,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2974
2975
|
const candidate = reconstructedGovernedPrefixSequence === undefined
|
|
2975
2976
|
? owned.at(-1)
|
|
2976
2977
|
: owned.find(({ sequence }) => sequence > reconstructedGovernedPrefixSequence);
|
|
2977
|
-
if (candidate === undefined || candidate.sourceStateId !== state.stateId) {
|
|
2978
|
+
if (candidate === undefined || candidate.restored !== undefined || candidate.sourceStateId !== state.stateId) {
|
|
2978
2979
|
return;
|
|
2979
2980
|
}
|
|
2980
2981
|
const persisted = persistedBoundaryReconciliation(candidate, ledger);
|
|
@@ -4090,6 +4091,356 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4090
4091
|
return (boundaries.length > 0 &&
|
|
4091
4092
|
boundaries.every(({ physicalReceipt }) => physicalReceipt?.classification === 'unchanged'));
|
|
4092
4093
|
}
|
|
4094
|
+
let acceptedStepResult;
|
|
4095
|
+
let requiredRecoveryBaseline;
|
|
4096
|
+
function captureRecoveryCheckpoint(stateId, prompt, boundaryPrefix) {
|
|
4097
|
+
if (requiredRecoveryBaseline !== undefined)
|
|
4098
|
+
return;
|
|
4099
|
+
// Capture after the entry emissions drain: the invocation is now a
|
|
4100
|
+
// real active child in XState's persisted snapshot, before host work.
|
|
4101
|
+
const id = recoveryCheckpoint?.stateId === stateId ? recoveryCheckpoint.id : undefined;
|
|
4102
|
+
recoveryCheckpoint = undefined;
|
|
4103
|
+
if (!actor || currentState().stateId !== stateId)
|
|
4104
|
+
return;
|
|
4105
|
+
try {
|
|
4106
|
+
const current = currentEffectLedger();
|
|
4107
|
+
recoveryCheckpoint = deepFreeze({
|
|
4108
|
+
stateId,
|
|
4109
|
+
prompt,
|
|
4110
|
+
...(id === undefined ? {} : { id }),
|
|
4111
|
+
machine: detachPersistedMachineSnapshot(actor.getPersistedSnapshot()),
|
|
4112
|
+
boundaryPrefix: boundaryPrefix ?? current.boundaries.length,
|
|
4113
|
+
});
|
|
4114
|
+
}
|
|
4115
|
+
catch {
|
|
4116
|
+
// An in-memory-only context is still executable; it simply cannot
|
|
4117
|
+
// promise a durable step retry. Keep the legacy control behavior.
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
function stepReplayIsSafe() {
|
|
4121
|
+
if (!recoveryCheckpoint || hasUnresolvedReconciliation())
|
|
4122
|
+
return false;
|
|
4123
|
+
const current = currentEffectLedger();
|
|
4124
|
+
return current.boundaries
|
|
4125
|
+
.slice(recoveryCheckpoint.boundaryPrefix)
|
|
4126
|
+
.every((boundary) => runtimeBoundaryIsOwned(boundary) &&
|
|
4127
|
+
boundary.logicalOperationId === undefined &&
|
|
4128
|
+
(boundary.physicalReceipt?.classification === 'unchanged' || boundary.restored !== undefined));
|
|
4129
|
+
}
|
|
4130
|
+
function repairableReadOnlyBoundary() {
|
|
4131
|
+
if (!recoveryCheckpoint || !repositoryCapability?.observe || !repositoryCapability.acquire ||
|
|
4132
|
+
retainedEffectReconciliationRequired || deferredReconciliationOperationId !== undefined)
|
|
4133
|
+
return undefined;
|
|
4134
|
+
const suffix = currentEffectLedger().boundaries.slice(recoveryCheckpoint.boundaryPrefix);
|
|
4135
|
+
if (suffix.length !== 1)
|
|
4136
|
+
return undefined;
|
|
4137
|
+
const saved = suffix[0];
|
|
4138
|
+
if (!runtimeBoundaryIsOwned(saved) || saved.restored !== undefined ||
|
|
4139
|
+
saved.sourceStateId !== recoveryCheckpoint.stateId || saved.cohortId !== undefined ||
|
|
4140
|
+
saved.logicalOperationId !== undefined || saved.dispositions.some((item) => item !== 'unchanged') ||
|
|
4141
|
+
saved.physicalReceipt === undefined || saved.after?.head !== saved.baseline.head ||
|
|
4142
|
+
saved.physicalReceipt.classification === 'unchanged' ||
|
|
4143
|
+
[...unresolvedSemanticBoundaryIds].some((id) => id !== saved.boundaryId))
|
|
4144
|
+
return undefined;
|
|
4145
|
+
return saved;
|
|
4146
|
+
}
|
|
4147
|
+
async function confirmReadOnlyRestoration(saved, signal) {
|
|
4148
|
+
const claim = await repositoryCapability.acquire({ signal });
|
|
4149
|
+
try {
|
|
4150
|
+
await claim.assertOwner();
|
|
4151
|
+
const restored = snapshotJsonValue(await repositoryCapability.observe());
|
|
4152
|
+
if (!isDeepStrictEqual(restored, saved.baseline)) {
|
|
4153
|
+
throw new Error(`${label} preparation has not restored the stopped step's repository; it remains paused`);
|
|
4154
|
+
}
|
|
4155
|
+
signal.throwIfAborted();
|
|
4156
|
+
effectLedgerMirror = assertPlaybookEffectLedger(await effectLedgerCapability.writeAhead([
|
|
4157
|
+
{ kind: 'replace-boundaries', replacements: [{ expected: saved, next: { ...saved, restored } }] },
|
|
4158
|
+
]));
|
|
4159
|
+
if (!isDeepStrictEqual(effectLedgerMirror.boundaries.find(({ boundaryId }) => boundaryId === saved.boundaryId)?.restored, restored)) {
|
|
4160
|
+
throw new Error(`${label} repository restoration was not durably acknowledged`);
|
|
4161
|
+
}
|
|
4162
|
+
refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
|
|
4163
|
+
}
|
|
4164
|
+
finally {
|
|
4165
|
+
await claim.release();
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
// A completed commit with saved presentation needs only the missing
|
|
4169
|
+
// tool-free judgment. Never repeat the player to recover that evidence.
|
|
4170
|
+
function recoverableJudgment() {
|
|
4171
|
+
if (!recoveryCheckpoint || recoveryCheckpoint.result !== undefined || recoveryCheckpoint.delivered ||
|
|
4172
|
+
retainedEffectReconciliationRequired ||
|
|
4173
|
+
deferredReconciliationOperationId !== undefined)
|
|
4174
|
+
return undefined;
|
|
4175
|
+
const suffix = currentEffectLedger().boundaries.slice(recoveryCheckpoint.boundaryPrefix);
|
|
4176
|
+
if (suffix.length !== 1)
|
|
4177
|
+
return undefined;
|
|
4178
|
+
const saved = suffix[0];
|
|
4179
|
+
if (!runtimeBoundaryIsOwned(saved) ||
|
|
4180
|
+
saved.sourceStateId !== recoveryCheckpoint.stateId ||
|
|
4181
|
+
saved.logicalOperationId !== undefined ||
|
|
4182
|
+
saved.correctionBudget.spent ||
|
|
4183
|
+
saved.physicalReceipt?.classification !== 'one-descendant-commit' ||
|
|
4184
|
+
!saved.finalText?.trim() ||
|
|
4185
|
+
governedOutcomesForBoundary(saved) === undefined ||
|
|
4186
|
+
(saved.semanticCandidate !== undefined &&
|
|
4187
|
+
persistedBoundaryReconciliation(saved, currentEffectLedger())
|
|
4188
|
+
?.reconciliation.status !== 'resolved') ||
|
|
4189
|
+
[...unresolvedSemanticBoundaryIds].some((id) => id !== saved.boundaryId))
|
|
4190
|
+
return undefined;
|
|
4191
|
+
return saved;
|
|
4192
|
+
}
|
|
4193
|
+
async function recoverJudgment(saved, signal) {
|
|
4194
|
+
const checkpoint = recoveryCheckpoint;
|
|
4195
|
+
const persisted = checkpoint.machine;
|
|
4196
|
+
const input = Object.values(persisted.children)[0];
|
|
4197
|
+
const actorInput = input.snapshot.input;
|
|
4198
|
+
const outcomes = governedOutcomesForBoundary(saved);
|
|
4199
|
+
const candidate = saved.semanticCandidate ??
|
|
4200
|
+
parseGovernedSemanticCandidate(await boundary.callJudge('player-output-adjudication', saved.sourceStateId, buildGovernedJudgePrompt(actorInput, saved.finalText, outcomes), signal));
|
|
4201
|
+
const result = reconcilePlaybookSemanticEvidence({
|
|
4202
|
+
outcomes,
|
|
4203
|
+
semanticCandidate: candidate,
|
|
4204
|
+
finalText: saved.finalText,
|
|
4205
|
+
receipt: saved.physicalReceipt,
|
|
4206
|
+
});
|
|
4207
|
+
signal.throwIfAborted();
|
|
4208
|
+
const next = {
|
|
4209
|
+
...saved,
|
|
4210
|
+
semanticCandidate: snapshotJsonValue(result.evidence.semanticCandidate),
|
|
4211
|
+
};
|
|
4212
|
+
const acknowledged = assertPlaybookEffectLedger(await effectLedgerCapability.writeAhead([
|
|
4213
|
+
{
|
|
4214
|
+
kind: 'replace-boundaries',
|
|
4215
|
+
replacements: [{ expected: saved, next }],
|
|
4216
|
+
},
|
|
4217
|
+
]));
|
|
4218
|
+
if (!isDeepStrictEqual(acknowledged.boundaries.find((b) => b.boundaryId === saved.boundaryId), next)) {
|
|
4219
|
+
throw new Error(`${label} recovered judgment was not acknowledged exactly`);
|
|
4220
|
+
}
|
|
4221
|
+
effectLedgerMirror = acknowledged;
|
|
4222
|
+
refreshUnresolvedSemanticReconciliation(acknowledged);
|
|
4223
|
+
if (result.status !== 'resolved') {
|
|
4224
|
+
const error = attachPlaybookFailureCause(new Error(result.status === 'unresolved' && 'reason' in result.cause.evidence && result.cause.evidence.reason
|
|
4225
|
+
? String(result.cause.evidence.reason)
|
|
4226
|
+
: `${label} saved result could not be reconciled; the playbook remains parked`), result.status === 'unresolved' ? result.cause : runtimeDefectCause('Saved result still needs a Boss answer'));
|
|
4227
|
+
const paused = detachPersistedMachineSnapshot(actor.getPersistedSnapshot());
|
|
4228
|
+
stopActor();
|
|
4229
|
+
actor = buildActor(runtimePorts, {
|
|
4230
|
+
...paused, context: { ...paused.context, lastError: snapshotJsonValue(normalizeErrorFull(error)) },
|
|
4231
|
+
});
|
|
4232
|
+
suppressInspectionEmissions = false;
|
|
4233
|
+
actor.start();
|
|
4234
|
+
throw error;
|
|
4235
|
+
}
|
|
4236
|
+
await resumeCheckpoint({ ...checkpoint, result: snapshotJsonValue(result.output) });
|
|
4237
|
+
}
|
|
4238
|
+
async function resumeCheckpoint(checkpoint) {
|
|
4239
|
+
acceptedStepResult = checkpoint;
|
|
4240
|
+
try {
|
|
4241
|
+
stopActor();
|
|
4242
|
+
actor = buildActor(runtimePorts, checkpoint.machine);
|
|
4243
|
+
suppressInspectionEmissions = false;
|
|
4244
|
+
actor.start();
|
|
4245
|
+
await waitForPlaybookQuiescence(actor, { pendingCalls: nestedBridge });
|
|
4246
|
+
}
|
|
4247
|
+
finally {
|
|
4248
|
+
acceptedStepResult = undefined;
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
function exportRuntimeSnapshot(checkpoint) {
|
|
4252
|
+
if (!actor || !session || disposed || disposalPromise !== undefined) {
|
|
4253
|
+
return undefined;
|
|
4254
|
+
}
|
|
4255
|
+
if (activeSignal !== undefined && checkpoint === undefined)
|
|
4256
|
+
return undefined;
|
|
4257
|
+
if (deferredSettlementClosure !== undefined)
|
|
4258
|
+
return undefined;
|
|
4259
|
+
const pendingCall = checkpoint?.child ?? nestedBridge.getPendingCall();
|
|
4260
|
+
const bridgeSuspendedCall = checkpoint?.child ? nestedBridge.checkpointCall(checkpoint.child) : nestedBridge.getSuspendedCall();
|
|
4261
|
+
if ((pendingCall === undefined) !== (bridgeSuspendedCall === undefined)) {
|
|
4262
|
+
return undefined;
|
|
4263
|
+
}
|
|
4264
|
+
let suspendedCall;
|
|
4265
|
+
if (bridgeSuspendedCall !== undefined) {
|
|
4266
|
+
if (pendingCall?.callId !== bridgeSuspendedCall.callId ||
|
|
4267
|
+
pendingCall?.playbookId !== bridgeSuspendedCall.playbookId ||
|
|
4268
|
+
pendingCall?.childSessionId !== bridgeSuspendedCall.childSessionId) {
|
|
4269
|
+
return undefined;
|
|
4270
|
+
}
|
|
4271
|
+
if (!playbookCallTurnIds.has(bridgeSuspendedCall.callId)) {
|
|
4272
|
+
return undefined;
|
|
4273
|
+
}
|
|
4274
|
+
const turnId = playbookCallTurnIds.get(bridgeSuspendedCall.callId);
|
|
4275
|
+
if (bridgeSuspendedCall.turnId !== undefined &&
|
|
4276
|
+
bridgeSuspendedCall.turnId !== turnId) {
|
|
4277
|
+
return undefined;
|
|
4278
|
+
}
|
|
4279
|
+
suspendedCall = {
|
|
4280
|
+
...bridgeSuspendedCall,
|
|
4281
|
+
...(turnId === undefined ? {} : { turnId }),
|
|
4282
|
+
...(hasGovernedPlayerStates
|
|
4283
|
+
? {
|
|
4284
|
+
effectBoundaryPrefixSequence: playbookCallEffectPrefixes.get(bridgeSuspendedCall.callId) ?? null,
|
|
4285
|
+
}
|
|
4286
|
+
: {}),
|
|
4287
|
+
};
|
|
4288
|
+
}
|
|
4289
|
+
const interrupted = checkpoint?.interrupted && recoveryCheckpoint && machine.root.states.failed;
|
|
4290
|
+
const state = interrupted
|
|
4291
|
+
? normalizePlaybookSnapshot(machine.resolveState({ value: 'failed', context: {} }))
|
|
4292
|
+
: checkpoint?.child ? normalizePlaybookSnapshot(actor.getSnapshot(), { pendingCall }) : currentState();
|
|
4293
|
+
if (state.status !== 'active' || !state.quiescent)
|
|
4294
|
+
return undefined;
|
|
4295
|
+
const context = actor.getSnapshot()
|
|
4296
|
+
.context;
|
|
4297
|
+
// DR-063 §2: the failed state's `lastError` is persisted as every live
|
|
4298
|
+
// surface publishes it, completed from the runtime's own cause slot, so
|
|
4299
|
+
// a thrown value that refused the cause still explains itself after a
|
|
4300
|
+
// restart.
|
|
4301
|
+
const machineSnapshot = detachPersistedMachineSnapshot(interrupted ? { ...recoveryCheckpoint.machine, value: 'failed', children: {} } : actor.getPersistedSnapshot(), state.stateId === 'failed'
|
|
4302
|
+
? { lastError: interrupted ? { name: 'InterruptedStep', message: 'The process stopped during this step. Review the recorded work before choosing how to continue.', cause: runtimeDefectCause('The process stopped during this step. Review the recorded work before choosing how to continue.') } : failedStateError(context ?? {}) }
|
|
4303
|
+
: {});
|
|
4304
|
+
effectLedgerMirror = currentEffectLedger();
|
|
4305
|
+
refreshRetainedEffectReconciliation(effectLedgerMirror);
|
|
4306
|
+
syncDeferredReconciliationOverlay();
|
|
4307
|
+
refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
|
|
4308
|
+
const pendingQuestions = !hasUnresolvedReconciliation()
|
|
4309
|
+
? pendingBossQuestionsFor(state, context ?? {})
|
|
4310
|
+
: [];
|
|
4311
|
+
const interruptedPrefix = interrupted ? activeEffectLedgerPrefixSequence : undefined;
|
|
4312
|
+
const interruptedAttempts = new Set(effectLedgerMirror.boundaries.filter(({ sequence }) => interruptedPrefix !== undefined && sequence > interruptedPrefix).map(({ attemptId }) => attemptId));
|
|
4313
|
+
const failedEffectAttempt = interrupted && interruptedPrefix !== undefined && interruptedAttempts.size <= 1
|
|
4314
|
+
? { boundaryPrefix: interruptedPrefix, attemptId: interruptedAttempts.values().next().value ?? null }
|
|
4315
|
+
:
|
|
4316
|
+
hasGovernedPlayerStates &&
|
|
4317
|
+
!interrupted && state.stateId === 'failed' &&
|
|
4318
|
+
failedAttemptMatchesCurrentLedger(effectLedgerMirror)
|
|
4319
|
+
? {
|
|
4320
|
+
boundaryPrefix: failedEffectBoundaryPrefix,
|
|
4321
|
+
attemptId: failedGovernedAttemptId ?? null,
|
|
4322
|
+
}
|
|
4323
|
+
: undefined;
|
|
4324
|
+
return {
|
|
4325
|
+
schemaVersion: 4,
|
|
4326
|
+
playbookId: session.playbookId,
|
|
4327
|
+
machine: machineSnapshot,
|
|
4328
|
+
roleResumeTokens: snapshotRoleResumeTokens(),
|
|
4329
|
+
sequences: {
|
|
4330
|
+
trace: traceSequence,
|
|
4331
|
+
turn: turnSequence,
|
|
4332
|
+
judgeCall: judgeCallSequence,
|
|
4333
|
+
playerCall: playerCallSequence,
|
|
4334
|
+
playbookCall: playbookCallSequence,
|
|
4335
|
+
...(declaredActors.has('captain')
|
|
4336
|
+
? { captainCall: captainCallSequence }
|
|
4337
|
+
: {}),
|
|
4338
|
+
},
|
|
4339
|
+
state,
|
|
4340
|
+
pendingBossQuestions: pendingQuestions.map((pending) => ({
|
|
4341
|
+
questionId: pending.questionId,
|
|
4342
|
+
asker: pending.asker,
|
|
4343
|
+
question: pending.question,
|
|
4344
|
+
sourceItem: pending.sourceItem,
|
|
4345
|
+
})),
|
|
4346
|
+
effectLedger: effectLedgerMirror,
|
|
4347
|
+
...(retainedEffectSourceSessionId === undefined
|
|
4348
|
+
? {}
|
|
4349
|
+
: { retainedEffectSourceSessionId }),
|
|
4350
|
+
...(retainedEffectReconciliation === undefined
|
|
4351
|
+
? {}
|
|
4352
|
+
: { retainedEffectReconciliation }),
|
|
4353
|
+
...(failedEffectAttempt === undefined
|
|
4354
|
+
? {}
|
|
4355
|
+
: { failedEffectAttempt }),
|
|
4356
|
+
...((state.stateId === 'failed' || pendingQuestions.length > 0) && recoveryCheckpoint !== undefined
|
|
4357
|
+
? { recoveryCheckpoint } : {}),
|
|
4358
|
+
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
4359
|
+
};
|
|
4360
|
+
}
|
|
4361
|
+
async function runJournalledStep(kind, input, signal, execute) {
|
|
4362
|
+
const saved = acceptedStepResult;
|
|
4363
|
+
let output;
|
|
4364
|
+
if (saved?.stateId === input.stateId && saved.result !== undefined) {
|
|
4365
|
+
acceptedStepResult = undefined;
|
|
4366
|
+
recoveryCheckpoint = saved;
|
|
4367
|
+
output = snapshotJsonValue(saved.result);
|
|
4368
|
+
}
|
|
4369
|
+
else {
|
|
4370
|
+
const record = savedPorts?.recordStep;
|
|
4371
|
+
await drainEmissions();
|
|
4372
|
+
signal.throwIfAborted();
|
|
4373
|
+
try {
|
|
4374
|
+
captureRecoveryCheckpoint(input.stateId, 'command' in input ? input.command : 'role' in input ? composeBoundPlayerPrompt(input) : composeCaptainPrompt(input));
|
|
4375
|
+
}
|
|
4376
|
+
catch (error) {
|
|
4377
|
+
if (!isAbortFailure(error, signal))
|
|
4378
|
+
controlPlaneError ??= error;
|
|
4379
|
+
throw error;
|
|
4380
|
+
}
|
|
4381
|
+
if (record) {
|
|
4382
|
+
const id = randomUUID();
|
|
4383
|
+
if (recoveryCheckpoint)
|
|
4384
|
+
recoveryCheckpoint = { ...recoveryCheckpoint, id };
|
|
4385
|
+
const step = { id, kind, stateId: input.stateId };
|
|
4386
|
+
await record(step, exportRuntimeSnapshot({ interrupted: true }));
|
|
4387
|
+
signal.throwIfAborted();
|
|
4388
|
+
const result = snapshotJsonValue(await execute());
|
|
4389
|
+
// Keep the known output if its save loses acknowledgement. A drained
|
|
4390
|
+
// failure can then preserve it without treating the call as unexecuted.
|
|
4391
|
+
if (recoveryCheckpoint?.id === id)
|
|
4392
|
+
recoveryCheckpoint = { ...recoveryCheckpoint, result };
|
|
4393
|
+
await record({ ...step, result });
|
|
4394
|
+
output = result;
|
|
4395
|
+
}
|
|
4396
|
+
else {
|
|
4397
|
+
output = await execute();
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
if (recoveryCheckpoint?.stateId === input.stateId) {
|
|
4401
|
+
const { result: _result, ...start } = recoveryCheckpoint;
|
|
4402
|
+
recoveryCheckpoint = { ...start, delivered: true };
|
|
4403
|
+
}
|
|
4404
|
+
return output;
|
|
4405
|
+
}
|
|
4406
|
+
function validateRecoveryCheckpoint(checkpoint) {
|
|
4407
|
+
if (!checkpoint)
|
|
4408
|
+
return;
|
|
4409
|
+
const node = machine.root.states[checkpoint.stateId];
|
|
4410
|
+
const persisted = checkpoint.machine;
|
|
4411
|
+
const children = persisted.children;
|
|
4412
|
+
if (!node ||
|
|
4413
|
+
node.invoke.length !== 1 ||
|
|
4414
|
+
!['player', 'captain', 'script'].includes(String(node.invoke[0].src)) ||
|
|
4415
|
+
!isPlainObject(children) ||
|
|
4416
|
+
Object.keys(children).length !== 1) {
|
|
4417
|
+
throw new TypeError(`${label} recovery checkpoint does not name one current invocation`);
|
|
4418
|
+
}
|
|
4419
|
+
const child = children[node.invoke[0].id];
|
|
4420
|
+
if (!child ||
|
|
4421
|
+
child.src !== node.invoke[0].src ||
|
|
4422
|
+
!isPlainObject(child.snapshot) ||
|
|
4423
|
+
child.snapshot.status !== 'active' ||
|
|
4424
|
+
!isPlainObject(child.snapshot.input) ||
|
|
4425
|
+
child.snapshot.input.stateId !== checkpoint.stateId) {
|
|
4426
|
+
throw new TypeError(`${label} recovery checkpoint invocation is incompatible`);
|
|
4427
|
+
}
|
|
4428
|
+
if (checkpoint.result !== undefined) {
|
|
4429
|
+
const output = checkpoint.result;
|
|
4430
|
+
const input = child.snapshot.input;
|
|
4431
|
+
if (!isPlainObject(output) || typeof output.guard !== 'string' ||
|
|
4432
|
+
!isPlainObject(input.result) || !Object.hasOwn(input.result, output.guard) ||
|
|
4433
|
+
(node.invoke[0].src === 'script' &&
|
|
4434
|
+
(!Number.isInteger(output.exitStatus) || Object.keys(output).sort().join(',') !== 'exitStatus,guard' ||
|
|
4435
|
+
output.guard !== Object.keys(input.result)[output.exitStatus === 0 ? 0 : Math.min(1, Object.keys(input.result).length - 1)]))) {
|
|
4436
|
+
throw new TypeError(`${label} saved step result is incompatible with its invocation`);
|
|
4437
|
+
}
|
|
4438
|
+
for (const field of extractFields(String(input.result[output.guard]))) {
|
|
4439
|
+
if (output[field] === undefined || output[field] === null)
|
|
4440
|
+
throw new TypeError(`${label} saved step result is missing ${field}`);
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
}
|
|
4093
4444
|
function captureEffectLedgerPrefixSequence() {
|
|
4094
4445
|
if (!hasGovernedPlayerStates)
|
|
4095
4446
|
return undefined;
|
|
@@ -4675,7 +5026,16 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4675
5026
|
// captured it under this exclusive claim. A host that supplies
|
|
4676
5027
|
// none states no pre-existing change, exactly as an empty
|
|
4677
5028
|
// baseline projection does.
|
|
4678
|
-
operation: (context) =>
|
|
5029
|
+
operation: async (context) => {
|
|
5030
|
+
if (requiredRecoveryBaseline !== undefined) {
|
|
5031
|
+
if (!isDeepStrictEqual(snapshotJsonValue(context.baseline), requiredRecoveryBaseline)) {
|
|
5032
|
+
throw new Error(`${label} repository changed after preparation; the stopped step remains paused`);
|
|
5033
|
+
}
|
|
5034
|
+
requiredRecoveryBaseline = undefined;
|
|
5035
|
+
captureRecoveryCheckpoint(input.stateId, freshPrompt, currentEffectLedger().boundaries.findIndex(({ boundaryId }) => boundaryId === effectBoundary.boundaryId));
|
|
5036
|
+
}
|
|
5037
|
+
return runTracedPlayerCall(selectedResume, effectAuthorizedPreExistingBlock(effectBoundary, context?.baseline));
|
|
5038
|
+
},
|
|
4679
5039
|
completeEffectBoundary: completionEvidenceFor(input, roleId, playerId, signal, undefined),
|
|
4680
5040
|
});
|
|
4681
5041
|
return acknowledgeGovernedPlayerResult(exclusive, effectBoundary.boundaryId);
|
|
@@ -4901,6 +5261,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4901
5261
|
}
|
|
4902
5262
|
function playerActor(ports) {
|
|
4903
5263
|
return createPlayerBridge({
|
|
5264
|
+
run: (input, signal, execute) => runJournalledStep('player', input, signal, execute),
|
|
4904
5265
|
resolveRoleId: requireRoleId,
|
|
4905
5266
|
validateInput: (input) => assertGovernedPlayerInput(outcomeAuthority, input, extractFields, label),
|
|
4906
5267
|
composePlayerPrompt: composeBoundPlayerPrompt,
|
|
@@ -4918,7 +5279,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4918
5279
|
// adjudication that injects the exact visible finalText as the selected
|
|
4919
5280
|
// output's question/response.
|
|
4920
5281
|
function captainActor() {
|
|
4921
|
-
return fromPromise(
|
|
5282
|
+
return fromPromise(({ input, signal }) => runJournalledStep('captain', input, signal, async () => {
|
|
4922
5283
|
const active = combineAbortSignals(signal, activeSignal);
|
|
4923
5284
|
try {
|
|
4924
5285
|
await drainEmissions();
|
|
@@ -4982,7 +5343,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4982
5343
|
}
|
|
4983
5344
|
throw error;
|
|
4984
5345
|
}
|
|
4985
|
-
});
|
|
5346
|
+
}));
|
|
4986
5347
|
}
|
|
4987
5348
|
// Deterministic script actor (slc/link.md §Script execution). Runs
|
|
4988
5349
|
// `input.command` through `sh -c`, resolves the declared guard
|
|
@@ -4990,7 +5351,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4990
5351
|
// `playbook.script` telemetry event. No agent call, no adjudication,
|
|
4991
5352
|
// no `*.call.*` trace.
|
|
4992
5353
|
function scriptActor() {
|
|
4993
|
-
return fromPromise(
|
|
5354
|
+
return fromPromise(({ input, signal }) => runJournalledStep('script', input, signal, async () => {
|
|
4994
5355
|
await drainEmissions();
|
|
4995
5356
|
const active = combineAbortSignals(signal, activeSignal);
|
|
4996
5357
|
const guards = Object.keys(input.result);
|
|
@@ -5162,7 +5523,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5162
5523
|
clearTimeout(killTimer);
|
|
5163
5524
|
}
|
|
5164
5525
|
}
|
|
5165
|
-
});
|
|
5526
|
+
}));
|
|
5166
5527
|
}
|
|
5167
5528
|
const nestedBridge = createNestedPlaybookBridge({
|
|
5168
5529
|
nextCallId: () => `playbook-${++playbookCallSequence}`,
|
|
@@ -5487,6 +5848,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5487
5848
|
latchRuntimeError(error);
|
|
5488
5849
|
}
|
|
5489
5850
|
if (state.stateId === 'failed') {
|
|
5851
|
+
if (previousState?.stateId !== 'failed' &&
|
|
5852
|
+
previousState?.stateId !== recoveryCheckpoint?.stateId) {
|
|
5853
|
+
recoveryCheckpoint = undefined;
|
|
5854
|
+
}
|
|
5490
5855
|
if (previousState?.stateId !== 'failed' ||
|
|
5491
5856
|
activeGovernedBoundarySeen) {
|
|
5492
5857
|
latchFailedGovernedAttempt();
|
|
@@ -5734,7 +6099,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5734
6099
|
activeDeferredContinuation = undefined;
|
|
5735
6100
|
deferInspectionEmissions = false;
|
|
5736
6101
|
deferredInspectionEmissions = [];
|
|
5737
|
-
|
|
6102
|
+
recoveryCheckpoint = undefined;
|
|
5738
6103
|
suppressInspectionEmissions = false;
|
|
5739
6104
|
initialized = false;
|
|
5740
6105
|
traceSequence = 0;
|
|
@@ -5751,77 +6116,26 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5751
6116
|
can.call(snapshot, event) ===
|
|
5752
6117
|
true);
|
|
5753
6118
|
}
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
// snapshot is the retry payload's source. The persisted machine snapshot
|
|
5757
|
-
// carries it, so the candidate derives identically in the process that
|
|
5758
|
-
// exported the snapshot and in one that restored it, and a failure
|
|
5759
|
-
// reached after a Boss reply — whose recorded event the failure state
|
|
5760
|
-
// refuses — is recoverable too. Naming the member is the artifact's
|
|
5761
|
-
// statement that it holds the entry text: a same-named member is never
|
|
5762
|
-
// assumed, since inferring one would turn any matching context member
|
|
5763
|
-
// into a replay payload without its author saying so.
|
|
5764
|
-
// Declared and absent or empty excludes the candidate rather than
|
|
5765
|
-
// falling back to the record, which would make the action depend on the
|
|
5766
|
-
// process again — the very thing this source exists to end.
|
|
5767
|
-
function retryEventFrom(snapshot) {
|
|
5768
|
-
const entryEvent = spec.entryEvent;
|
|
5769
|
-
if (entryEvent?.contextField === undefined)
|
|
5770
|
-
return lastBossEvent;
|
|
5771
|
-
const context = snapshot?.context;
|
|
5772
|
-
const text = isPlainObject(context)
|
|
5773
|
-
? context[entryEvent.contextField]
|
|
5774
|
-
: undefined;
|
|
5775
|
-
if (typeof text !== 'string' || text.trim() === '')
|
|
5776
|
-
return undefined;
|
|
5777
|
-
return { type: entryEvent.type, [entryEvent.textField]: text };
|
|
5778
|
-
}
|
|
5779
|
-
// The failure-state retry entry replays the recorded last classified
|
|
5780
|
-
// event with its recorded payload, or the entry event the declared
|
|
5781
|
-
// context member above sources. A candidate whose event the live
|
|
5782
|
-
// snapshot does not accept — or whose payload the runtime can source
|
|
5783
|
-
// from neither — is excluded rather than completed with invented text.
|
|
5784
|
-
// The caller has already applied the PBRT-71 replay fence.
|
|
5785
|
-
function retryActionFor(snapshot, stateId) {
|
|
5786
|
-
if (stateId !== 'failed')
|
|
6119
|
+
function retryActionFor(stateId) {
|
|
6120
|
+
if (stateId !== 'failed' || !recoveryCheckpoint)
|
|
5787
6121
|
return undefined;
|
|
5788
|
-
const
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
// multi-arm list keyed on `targetId`, so the first configured arm
|
|
5796
|
-
// may label a different state than the one the recorded event
|
|
5797
|
-
// actually resumes.
|
|
5798
|
-
const recordedTargetId = retryEvent.type === JUMP_EVENT_TYPE
|
|
5799
|
-
? retryEvent.targetId
|
|
5800
|
-
: undefined;
|
|
5801
|
-
const target = typeof recordedTargetId === 'string' &&
|
|
5802
|
-
recordedTargetId.trim().length > 0
|
|
5803
|
-
? recordedTargetId
|
|
5804
|
-
: firstTransitionTarget(machine, stateId, retryEvent.type);
|
|
5805
|
-
// PBRT-52: a label is written from a source state description, never
|
|
5806
|
-
// from an identifier. Falling back to the target id — or, with no
|
|
5807
|
-
// resolvable target, to the FSM event type — makes the label *be* the
|
|
5808
|
-
// internal name, which defeats the substitution the label exists for
|
|
5809
|
-
// and puts a machine identifier into Boss-facing text
|
|
5810
|
-
// (CAPPLAY-5). A candidate whose label can only be an id is excluded
|
|
5811
|
-
// exactly like one whose payload cannot be sourced.
|
|
5812
|
-
const description = (target === undefined ? undefined : stateDescriptions.get(target)) ??
|
|
5813
|
-
stateDescriptions.get(stateId);
|
|
5814
|
-
if (description === undefined)
|
|
6122
|
+
const description = stateDescriptions.get(recoveryCheckpoint.stateId);
|
|
6123
|
+
const savedResult = recoveryCheckpoint.result !== undefined;
|
|
6124
|
+
const safe = savedResult
|
|
6125
|
+
? !hasUnresolvedReconciliation() && currentEffectLedger().boundaries.slice(recoveryCheckpoint.boundaryPrefix)
|
|
6126
|
+
.every((boundary) => runtimeBoundaryIsOwned(boundary) && boundary.sourceStateId === recoveryCheckpoint.stateId)
|
|
6127
|
+
: stepReplayIsSafe();
|
|
6128
|
+
if (description === undefined || !safe)
|
|
5815
6129
|
return undefined;
|
|
6130
|
+
const entry = spec.entryEvent;
|
|
6131
|
+
const target = entry && firstTransitionTarget(machine, stateId, entry.type);
|
|
5816
6132
|
return {
|
|
5817
6133
|
action: {
|
|
5818
|
-
id: `retry:${
|
|
5819
|
-
label:
|
|
5820
|
-
// DR-063 §3: retrying a failed call is `ready` — the runtime cannot
|
|
5821
|
-
// see whether the Boss changed the outside world since it failed.
|
|
6134
|
+
id: target === recoveryCheckpoint.stateId ? `retry:${entry.type}` : 'retry:step',
|
|
6135
|
+
label: `${savedResult ? 'Continue from saved result' : 'Retry'}: ${description}`,
|
|
5822
6136
|
standing: 'ready',
|
|
5823
6137
|
},
|
|
5824
|
-
|
|
6138
|
+
retryStep: true,
|
|
5825
6139
|
};
|
|
5826
6140
|
}
|
|
5827
6141
|
/**
|
|
@@ -5867,6 +6181,21 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5867
6181
|
return [];
|
|
5868
6182
|
}
|
|
5869
6183
|
const derived = [];
|
|
6184
|
+
const repairable = state.stateId === 'failed' ? repairableReadOnlyBoundary() : undefined;
|
|
6185
|
+
if (repairable !== undefined)
|
|
6186
|
+
derived.push({
|
|
6187
|
+
action: { id: 'retry:restored-step', label: 'Restore the repository and retry the read-only step', standing: 'ready' },
|
|
6188
|
+
retryStep: true,
|
|
6189
|
+
restoreReadOnly: repairable,
|
|
6190
|
+
});
|
|
6191
|
+
const judgment = state.stateId === 'failed' ? recoverableJudgment() : undefined;
|
|
6192
|
+
if (judgment !== undefined)
|
|
6193
|
+
derived.push({
|
|
6194
|
+
action: { id: 'retry:adjudication', label: 'Retry assessment of the saved result', standing: 'ready' },
|
|
6195
|
+
retryJudgment: judgment,
|
|
6196
|
+
});
|
|
6197
|
+
if (judgment !== undefined && !hasUnresolvedReconciliation())
|
|
6198
|
+
return derived;
|
|
5870
6199
|
if (hasUnresolvedReconciliation()) {
|
|
5871
6200
|
const operation = deferredReconciliationOperationId === undefined
|
|
5872
6201
|
? undefined
|
|
@@ -5895,18 +6224,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5895
6224
|
});
|
|
5896
6225
|
return derived;
|
|
5897
6226
|
}
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
//
|
|
5902
|
-
//
|
|
5903
|
-
// control can replay a player call whose effect may already exist.
|
|
6227
|
+
const retry = retryActionFor(state.stateId);
|
|
6228
|
+
if (retry !== undefined)
|
|
6229
|
+
derived.push(retry);
|
|
6230
|
+
// A jump can repeat earlier work, unlike checkpoint retry. Its whole
|
|
6231
|
+
// failed attempt must therefore still pass the replay fence.
|
|
5904
6232
|
if (state.stateId === 'failed' && !failedAttemptAllowsReplay()) {
|
|
5905
6233
|
return derived;
|
|
5906
6234
|
}
|
|
5907
|
-
const retry = retryActionFor(snapshot, state.stateId);
|
|
5908
|
-
if (retry !== undefined)
|
|
5909
|
-
derived.push(retry);
|
|
5910
6235
|
// Jump entries: resumable targets whose explicit-state-jump event the
|
|
5911
6236
|
// live snapshot accepts (state guards included), sent with the
|
|
5912
6237
|
// advertised target id and optional textual fields omitted.
|
|
@@ -6018,6 +6343,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6018
6343
|
}
|
|
6019
6344
|
const boundSession = bindSession(nextSession);
|
|
6020
6345
|
const boundSnapshot = assertPlaybookRuntimeSnapshot(snapshot, boundSession.playbookId, { allowSuspendedCall: true });
|
|
6346
|
+
validateRecoveryCheckpoint(boundSnapshot.recoveryCheckpoint);
|
|
6021
6347
|
if (kind === 'adopt' &&
|
|
6022
6348
|
effectLedgerCapability !== undefined &&
|
|
6023
6349
|
(boundSnapshot.retainedEffectReconciliation?.checkpoint ??
|
|
@@ -6084,6 +6410,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6084
6410
|
initInFlight = initialization;
|
|
6085
6411
|
const initTask = (async () => {
|
|
6086
6412
|
session = boundSession;
|
|
6413
|
+
recoveryCheckpoint = boundSnapshot.recoveryCheckpoint;
|
|
6087
6414
|
syncDeferredReconciliationOverlay();
|
|
6088
6415
|
refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
|
|
6089
6416
|
prepareReconstructedGovernedDelivery(boundSnapshot.state, effectLedgerMirror);
|
|
@@ -6278,7 +6605,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6278
6605
|
emission();
|
|
6279
6606
|
}
|
|
6280
6607
|
}
|
|
6281
|
-
async function continueBoundDeferredOperation(operation, event, signal, turnId, classificationLine) {
|
|
6608
|
+
async function continueBoundDeferredOperation(operation, event, signal, turnId, classificationLine, onAccepted) {
|
|
6282
6609
|
if (repositoryCapability === undefined) {
|
|
6283
6610
|
throw new Error(`${label} deferred continuation requires repository.runDeferred`);
|
|
6284
6611
|
}
|
|
@@ -6324,6 +6651,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6324
6651
|
continuation.baseline = context?.baseline;
|
|
6325
6652
|
continuation.playerContinuation = selectPlayerResume(effectBoundary.roleId, boundPlayerId);
|
|
6326
6653
|
continuationStarted = true;
|
|
6654
|
+
onAccepted?.();
|
|
6327
6655
|
actor.send(event);
|
|
6328
6656
|
// The host has durably started this boundary and the FSM has
|
|
6329
6657
|
// moved: publish the buffered classification line and the
|
|
@@ -6573,106 +6901,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6573
6901
|
// Defined only at a safe capture point — initialized, not disposing
|
|
6574
6902
|
// or disposed, no active public boundary, and the actor quiescent with
|
|
6575
6903
|
// status `active`.
|
|
6576
|
-
exportSnapshot() {
|
|
6577
|
-
if (!actor || !session || disposed || disposalPromise !== undefined) {
|
|
6578
|
-
return undefined;
|
|
6579
|
-
}
|
|
6580
|
-
if (activeSignal !== undefined)
|
|
6581
|
-
return undefined;
|
|
6582
|
-
if (deferredSettlementClosure !== undefined)
|
|
6583
|
-
return undefined;
|
|
6584
|
-
const pendingCall = nestedBridge.getPendingCall();
|
|
6585
|
-
const bridgeSuspendedCall = nestedBridge.getSuspendedCall();
|
|
6586
|
-
if ((pendingCall === undefined) !== (bridgeSuspendedCall === undefined)) {
|
|
6587
|
-
return undefined;
|
|
6588
|
-
}
|
|
6589
|
-
let suspendedCall;
|
|
6590
|
-
if (bridgeSuspendedCall !== undefined) {
|
|
6591
|
-
if (pendingCall?.callId !== bridgeSuspendedCall.callId ||
|
|
6592
|
-
pendingCall?.playbookId !== bridgeSuspendedCall.playbookId ||
|
|
6593
|
-
pendingCall?.childSessionId !== bridgeSuspendedCall.childSessionId) {
|
|
6594
|
-
return undefined;
|
|
6595
|
-
}
|
|
6596
|
-
if (!playbookCallTurnIds.has(bridgeSuspendedCall.callId)) {
|
|
6597
|
-
return undefined;
|
|
6598
|
-
}
|
|
6599
|
-
const turnId = playbookCallTurnIds.get(bridgeSuspendedCall.callId);
|
|
6600
|
-
if (bridgeSuspendedCall.turnId !== undefined &&
|
|
6601
|
-
bridgeSuspendedCall.turnId !== turnId) {
|
|
6602
|
-
return undefined;
|
|
6603
|
-
}
|
|
6604
|
-
suspendedCall = {
|
|
6605
|
-
...bridgeSuspendedCall,
|
|
6606
|
-
...(turnId === undefined ? {} : { turnId }),
|
|
6607
|
-
...(hasGovernedPlayerStates
|
|
6608
|
-
? {
|
|
6609
|
-
effectBoundaryPrefixSequence: playbookCallEffectPrefixes.get(bridgeSuspendedCall.callId) ?? null,
|
|
6610
|
-
}
|
|
6611
|
-
: {}),
|
|
6612
|
-
};
|
|
6613
|
-
}
|
|
6614
|
-
const state = currentState();
|
|
6615
|
-
if (state.status !== 'active' || !state.quiescent)
|
|
6616
|
-
return undefined;
|
|
6617
|
-
const context = actor.getSnapshot()
|
|
6618
|
-
.context;
|
|
6619
|
-
// DR-063 §2: the failed state's `lastError` is persisted as every live
|
|
6620
|
-
// surface publishes it, completed from the runtime's own cause slot, so
|
|
6621
|
-
// a thrown value that refused the cause still explains itself after a
|
|
6622
|
-
// restart.
|
|
6623
|
-
const machineSnapshot = detachPersistedMachineSnapshot(actor.getPersistedSnapshot(), state.stateId === 'failed'
|
|
6624
|
-
? { lastError: failedStateError(context ?? {}) }
|
|
6625
|
-
: {});
|
|
6626
|
-
effectLedgerMirror = currentEffectLedger();
|
|
6627
|
-
refreshRetainedEffectReconciliation(effectLedgerMirror);
|
|
6628
|
-
syncDeferredReconciliationOverlay();
|
|
6629
|
-
refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
|
|
6630
|
-
const pendingQuestions = !hasUnresolvedReconciliation()
|
|
6631
|
-
? pendingBossQuestionsFor(state, context ?? {})
|
|
6632
|
-
: [];
|
|
6633
|
-
const failedEffectAttempt = hasGovernedPlayerStates &&
|
|
6634
|
-
state.stateId === 'failed' &&
|
|
6635
|
-
failedAttemptMatchesCurrentLedger(effectLedgerMirror)
|
|
6636
|
-
? {
|
|
6637
|
-
boundaryPrefix: failedEffectBoundaryPrefix,
|
|
6638
|
-
attemptId: failedGovernedAttemptId ?? null,
|
|
6639
|
-
}
|
|
6640
|
-
: undefined;
|
|
6641
|
-
return {
|
|
6642
|
-
schemaVersion: 4,
|
|
6643
|
-
playbookId: session.playbookId,
|
|
6644
|
-
machine: machineSnapshot,
|
|
6645
|
-
roleResumeTokens: snapshotRoleResumeTokens(),
|
|
6646
|
-
sequences: {
|
|
6647
|
-
trace: traceSequence,
|
|
6648
|
-
turn: turnSequence,
|
|
6649
|
-
judgeCall: judgeCallSequence,
|
|
6650
|
-
playerCall: playerCallSequence,
|
|
6651
|
-
playbookCall: playbookCallSequence,
|
|
6652
|
-
...(declaredActors.has('captain')
|
|
6653
|
-
? { captainCall: captainCallSequence }
|
|
6654
|
-
: {}),
|
|
6655
|
-
},
|
|
6656
|
-
state,
|
|
6657
|
-
pendingBossQuestions: pendingQuestions.map((pending) => ({
|
|
6658
|
-
questionId: pending.questionId,
|
|
6659
|
-
asker: pending.asker,
|
|
6660
|
-
question: pending.question,
|
|
6661
|
-
sourceItem: pending.sourceItem,
|
|
6662
|
-
})),
|
|
6663
|
-
effectLedger: effectLedgerMirror,
|
|
6664
|
-
...(retainedEffectSourceSessionId === undefined
|
|
6665
|
-
? {}
|
|
6666
|
-
: { retainedEffectSourceSessionId }),
|
|
6667
|
-
...(retainedEffectReconciliation === undefined
|
|
6668
|
-
? {}
|
|
6669
|
-
: { retainedEffectReconciliation }),
|
|
6670
|
-
...(failedEffectAttempt === undefined
|
|
6671
|
-
? {}
|
|
6672
|
-
: { failedEffectAttempt }),
|
|
6673
|
-
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
6674
|
-
};
|
|
6675
|
-
},
|
|
6904
|
+
exportSnapshot: (checkpoint) => exportRuntimeSnapshot(checkpoint?.child ? { child: checkpoint.child } : undefined),
|
|
6676
6905
|
// DR-014 §1 / PBRT-45: alternative to `init` that rehydrates an
|
|
6677
6906
|
// exported snapshot under the same immutable session identity.
|
|
6678
6907
|
// Emits no `session.started`, transition trace, or human status —
|
|
@@ -6725,6 +6954,30 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6725
6954
|
const stateDescription = !hasUnresolvedReconciliation()
|
|
6726
6955
|
? stateDescriptionFor(state)
|
|
6727
6956
|
: undefined;
|
|
6957
|
+
const actions = deriveControlActions(snapshot).map(({ action }) => action);
|
|
6958
|
+
const retry = actions.find(({ id, standing }) => id.startsWith('retry:') &&
|
|
6959
|
+
(standing ?? 'ready') === 'ready');
|
|
6960
|
+
const stoppedBoundary = recoveryCheckpoint === undefined ? undefined :
|
|
6961
|
+
currentEffectLedger().boundaries.slice(recoveryCheckpoint.boundaryPrefix)
|
|
6962
|
+
.filter((boundary) => runtimeBoundaryIsOwned(boundary) && boundary.sourceStateId === recoveryCheckpoint.stateId).at(-1);
|
|
6963
|
+
const recovery = recoveryCheckpoint === undefined ||
|
|
6964
|
+
(pendingQuestions.length === 0 && retry === undefined) ? undefined : {
|
|
6965
|
+
prompt: recoveryCheckpoint.prompt,
|
|
6966
|
+
...(stoppedBoundary === undefined ? {} : { evidence: snapshotJsonValue({
|
|
6967
|
+
declaredResults: stoppedBoundary.sourceOutcomeSchema,
|
|
6968
|
+
...(stoppedBoundary.finalText === undefined ? {} : { savedPlayerText: stoppedBoundary.finalText }),
|
|
6969
|
+
...(stoppedBoundary.semanticCandidate === undefined ? {} : { selectedResult: stoppedBoundary.semanticCandidate }),
|
|
6970
|
+
...(stoppedBoundary.physicalReceipt === undefined ? {} : { repositoryReceipt: stoppedBoundary.physicalReceipt }),
|
|
6971
|
+
}) }),
|
|
6972
|
+
...(repairableReadOnlyBoundary() === undefined ? {} : {
|
|
6973
|
+
preparation: `This read-only step changed the repository. Restore its exact original HEAD and visible files before retrying; do not accept its saved result. Preserve user work. You may put generated outputs aside and prevent their return using local environment or ignore settings. Original observation: ${JSON.stringify(repairableReadOnlyBoundary().baseline)}. Observed after the failed step: ${JSON.stringify(repairableReadOnlyBoundary().after)}.`,
|
|
6974
|
+
}),
|
|
6975
|
+
...(stateDescriptions.get(recoveryCheckpoint.stateId) === undefined ? {} :
|
|
6976
|
+
{ description: stateDescriptions.get(recoveryCheckpoint.stateId) }),
|
|
6977
|
+
continuation: pendingQuestions.length === 0
|
|
6978
|
+
? { kind: 'runtime', actionId: retry.id }
|
|
6979
|
+
: { kind: 'reply' },
|
|
6980
|
+
};
|
|
6728
6981
|
return deepFreeze({
|
|
6729
6982
|
state,
|
|
6730
6983
|
...(stateDescription === undefined ? {} : { stateDescription }),
|
|
@@ -6738,7 +6991,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6738
6991
|
sourceItem: pending.sourceItem,
|
|
6739
6992
|
})),
|
|
6740
6993
|
...(lastError !== undefined ? { lastError } : {}),
|
|
6741
|
-
actions
|
|
6994
|
+
actions,
|
|
6995
|
+
...(recovery === undefined ? {} : { recovery }),
|
|
6742
6996
|
});
|
|
6743
6997
|
},
|
|
6744
6998
|
// DR-029 / PBRT-52: revalidate the named action against the live
|
|
@@ -6900,7 +7154,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6900
7154
|
accepted = true;
|
|
6901
7155
|
try {
|
|
6902
7156
|
let run;
|
|
6903
|
-
if (candidate.
|
|
7157
|
+
if (candidate.retryJudgment !== undefined) {
|
|
7158
|
+
await recoverJudgment(candidate.retryJudgment, signal);
|
|
7159
|
+
run = runResultFor(settledOutcome(signal));
|
|
7160
|
+
}
|
|
7161
|
+
else if (candidate.unresolvedEffectAction === 'abandon') {
|
|
6904
7162
|
signal.throwIfAborted();
|
|
6905
7163
|
run = runResultFor('unresolved-effect');
|
|
6906
7164
|
}
|
|
@@ -6919,6 +7177,21 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6919
7177
|
? 'no-action'
|
|
6920
7178
|
: 'quiescent');
|
|
6921
7179
|
}
|
|
7180
|
+
else if (candidate.retryStep) {
|
|
7181
|
+
if (candidate.restoreReadOnly !== undefined) {
|
|
7182
|
+
await confirmReadOnlyRestoration(candidate.restoreReadOnly, signal);
|
|
7183
|
+
}
|
|
7184
|
+
const checkpoint = recoveryCheckpoint;
|
|
7185
|
+
requiredRecoveryBaseline = currentEffectLedger().boundaries
|
|
7186
|
+
.slice(checkpoint.boundaryPrefix).find(({ restored }) => restored !== undefined)?.restored;
|
|
7187
|
+
try {
|
|
7188
|
+
await resumeCheckpoint(checkpoint);
|
|
7189
|
+
}
|
|
7190
|
+
finally {
|
|
7191
|
+
requiredRecoveryBaseline = undefined;
|
|
7192
|
+
}
|
|
7193
|
+
run = runResultFor(settledOutcome(signal));
|
|
7194
|
+
}
|
|
6922
7195
|
else {
|
|
6923
7196
|
actor.send(candidate.event);
|
|
6924
7197
|
await waitForPlaybookQuiescence(actor, {
|
|
@@ -6931,6 +7204,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6931
7204
|
receipt = settledReceipt(run.outcome === 'failed' || run.outcome === 'aborted'
|
|
6932
7205
|
? {
|
|
6933
7206
|
disposition: 'failed',
|
|
7207
|
+
run,
|
|
6934
7208
|
error: ('error' in run ? run.error : undefined) ??
|
|
6935
7209
|
normalizeError(new Error(`apply settled with outcome ${run.outcome}`)),
|
|
6936
7210
|
}
|
|
@@ -7039,7 +7313,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
7039
7313
|
return receipt;
|
|
7040
7314
|
},
|
|
7041
7315
|
unresolvedEffectEnvelopes: unresolvedEffectEnvelopeIdentities,
|
|
7042
|
-
async handleBossInput({ text, signal, }) {
|
|
7316
|
+
async handleBossInput({ text, signal, onAccepted, }) {
|
|
7043
7317
|
if (!actor || !savedPorts) {
|
|
7044
7318
|
throw new Error('createPlaybookRuntime.handleBossInput: init must be called first');
|
|
7045
7319
|
}
|
|
@@ -7143,14 +7417,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
7143
7417
|
deferredOperation !== undefined) {
|
|
7144
7418
|
if (isExactDeferredBossReply(classifiedSnapshot, event, deferredPending)) {
|
|
7145
7419
|
const statusLine = classificationStatus(event);
|
|
7146
|
-
const continuation = await continueBoundDeferredOperation(deferredOperation, event, signal, turnId, statusLine);
|
|
7420
|
+
const continuation = await continueBoundDeferredOperation(deferredOperation, event, signal, turnId, statusLine, onAccepted);
|
|
7147
7421
|
if (continuation === 'continued') {
|
|
7148
|
-
try {
|
|
7149
|
-
lastBossEvent = snapshotJsonValue(event, 'recorded Boss event');
|
|
7150
|
-
}
|
|
7151
|
-
catch {
|
|
7152
|
-
lastBossEvent = undefined;
|
|
7153
|
-
}
|
|
7154
7422
|
if (controlPlaneError !== undefined) {
|
|
7155
7423
|
throw controlPlaneError;
|
|
7156
7424
|
}
|
|
@@ -7159,7 +7427,6 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
7159
7427
|
: 'no-action');
|
|
7160
7428
|
}
|
|
7161
7429
|
else {
|
|
7162
|
-
lastBossEvent = undefined;
|
|
7163
7430
|
result = runResultFor('no-action');
|
|
7164
7431
|
}
|
|
7165
7432
|
handledDeferred = true;
|
|
@@ -7171,7 +7438,6 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
7171
7438
|
}
|
|
7172
7439
|
else {
|
|
7173
7440
|
await parkBoundDeferredOperation(deferredOperation.operationId, signal);
|
|
7174
|
-
lastBossEvent = undefined;
|
|
7175
7441
|
result = runResultFor('no-action');
|
|
7176
7442
|
handledDeferred = true;
|
|
7177
7443
|
}
|
|
@@ -7202,16 +7468,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
7202
7468
|
suppressInspectionEmissions = false;
|
|
7203
7469
|
actor.start();
|
|
7204
7470
|
}
|
|
7205
|
-
|
|
7206
|
-
// as the retry-replay source. Recording is sanitizing, not
|
|
7207
|
-
// load-bearing: an override classifier's non-JSON-safe event is
|
|
7208
|
-
// simply not recorded, and the turn proceeds unchanged.
|
|
7209
|
-
try {
|
|
7210
|
-
lastBossEvent = snapshotJsonValue(event, 'recorded Boss event');
|
|
7211
|
-
}
|
|
7212
|
-
catch {
|
|
7213
|
-
lastBossEvent = undefined;
|
|
7214
|
-
}
|
|
7471
|
+
onAccepted?.();
|
|
7215
7472
|
actor.send(event);
|
|
7216
7473
|
await waitForPlaybookQuiescence(actor, {
|
|
7217
7474
|
pendingCalls: nestedBridge,
|
|
@@ -7491,7 +7748,6 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
7491
7748
|
failedGovernedAttemptId = undefined;
|
|
7492
7749
|
controlPlaneError = undefined;
|
|
7493
7750
|
emissionFailure = undefined;
|
|
7494
|
-
lastBossEvent = undefined;
|
|
7495
7751
|
retainedEffectSourceSessionId = undefined;
|
|
7496
7752
|
retainedEffectReconciliation = undefined;
|
|
7497
7753
|
retainedEffectReconciliationRequired = false;
|