@sublang/playbook 14.0.0 → 15.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.
- package/README.md +1 -1
- package/docs/cli.md +14 -14
- package/docs/configuration.md +15 -9
- package/package.json +1 -1
- package/reference/sdlc/branch.playbook/branch.fsm.d.ts +1 -1
- package/reference/sdlc/code.md +1 -1
- package/reference/sdlc/code.playbook/bin/launch-config.js +55 -16
- package/reference/sdlc/code.playbook/bin/playbook.js +14 -9
- package/reference/sdlc/code.playbook/bin/repository-effects.js +187 -14
- package/reference/sdlc/code.playbook/bin/run.js +12 -8
- package/reference/sdlc/code.playbook/code.fsm.d.ts +1 -1
- package/reference/sdlc/code.playbook/code.fsm.js +1 -1
- package/reference/sdlc/code.playbook/code.fsm.ts +1 -1
- package/reference/sdlc/code.playbook/code.gears.md +2 -2
- package/reference/sdlc/code.playbook/host-capabilities.d.ts +6 -0
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +3 -1
- package/reference/sdlc/code.playbook/playbook-captain.js +357 -16
- package/reference/sdlc/code.playbook/playbook-captain.ts +459 -34
- package/reference/sdlc/code.playbook/session-host.d.ts +6 -3
- package/reference/sdlc/decide.md +1 -1
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +1 -1
- package/reference/sdlc/decide.playbook/decide.fsm.js +1 -1
- package/reference/sdlc/decide.playbook/decide.fsm.ts +1 -1
- package/reference/sdlc/decide.playbook/decide.gears.md +1 -1
- package/reference/sdlc/decide.playbook/decide.playbook.js +25 -1
- package/reference/sdlc/decide.playbook/decide.playbook.ts +35 -2
- package/reference/sdlc/dev.md +15 -8
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +1 -1
- package/reference/sdlc/dev.playbook/dev.fsm.js +18 -12
- package/reference/sdlc/dev.playbook/dev.fsm.ts +18 -13
- package/reference/sdlc/dev.playbook/dev.gears.md +19 -12
- package/reference/sdlc/dev.playbook/dev.playbook.js +1 -1
- package/reference/sdlc/dev.playbook/dev.playbook.ts +1 -2
- package/reference/sdlc/dev.playbook/dev.registry.js +1 -1
- package/reference/sdlc/dev.playbook/dev.registry.ts +1 -1
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +1 -1
- package/reference/sdlc/review.md +1 -1
- package/reference/sdlc/review.playbook/review.fsm.d.ts +1 -1
- package/reference/sdlc/review.playbook/review.fsm.js +1 -1
- package/reference/sdlc/review.playbook/review.fsm.ts +1 -1
- package/reference/sdlc/review.playbook/review.gears.md +1 -1
- package/slc/link.md +32 -0
- package/src/runtime.d.ts +70 -0
- package/src/runtime.js +232 -1
- package/src/runtime.ts +358 -1
- package/src/xstate-playbook-runtime.d.ts +9 -10
- package/src/xstate-playbook-runtime.js +372 -20
- package/src/xstate-playbook-runtime.ts +460 -19
- package/src/xstate-runtime.d.ts +9 -1
- package/src/xstate-runtime.js +299 -19
- package/src/xstate-runtime.ts +362 -21
|
@@ -21,7 +21,8 @@ import { isDeepStrictEqual } from 'node:util';
|
|
|
21
21
|
import PQueue from 'p-queue';
|
|
22
22
|
import { createActor, fromPromise } from 'xstate';
|
|
23
23
|
import { createAcceptedOutcomeConsumer, } from './accepted-outcome.js';
|
|
24
|
-
import {
|
|
24
|
+
import { assertPlaybookFailureCause } from './runtime.js';
|
|
25
|
+
import { assertPlaybookRuntimeSnapshot, assertPlaybookEffectLedger, attachPlaybookFailureCause, combineAbortSignals, createNestedPlaybookBridge, detachPersistedMachineSnapshot, normalizeError, normalizePlaybookSnapshot, snapshotJsonValue, snapshotPlaybookSession, isPlaybookEffectLedgerMonotonicExtension, PlaybookSemanticCandidateStructureError, reconcilePlaybookSemanticEvidence, validateCaptainResult, validatePlayerResult, waitForPlaybookQuiescence, } from './xstate-runtime.js';
|
|
25
26
|
export const BOSS_REPLY_ERRORS = {
|
|
26
27
|
missingQuestion: "needsBossReply outcome missing 'question' field",
|
|
27
28
|
unregisteredState: (stateId) => `state ${stateId} declared needsBossReply but is not registered as resumable`,
|
|
@@ -38,6 +39,17 @@ export const BOSS_REPLY_ERRORS = {
|
|
|
38
39
|
const fsmResultFailures = new WeakSet();
|
|
39
40
|
function markFsmResultFailure(error) {
|
|
40
41
|
fsmResultFailures.add(error);
|
|
42
|
+
// DR-063 §2: there is no failure without a cause. A failure the runtime
|
|
43
|
+
// marks with nothing more specific is a runtime defect carrying its own
|
|
44
|
+
// message; a decision site that knows better attaches its cause afterwards.
|
|
45
|
+
attachPlaybookFailureCause(error, {
|
|
46
|
+
code: 'runtime-defect',
|
|
47
|
+
evidence: {
|
|
48
|
+
reason: typeof error.message === 'string' && error.message.length > 0
|
|
49
|
+
? error.message
|
|
50
|
+
: 'unknown runtime defect',
|
|
51
|
+
},
|
|
52
|
+
});
|
|
41
53
|
return error;
|
|
42
54
|
}
|
|
43
55
|
function isFsmResultFailure(error) {
|
|
@@ -46,6 +58,63 @@ function isFsmResultFailure(error) {
|
|
|
46
58
|
fsmResultFailures.has(error));
|
|
47
59
|
}
|
|
48
60
|
// ---------------------------------------------------------------------------
|
|
61
|
+
// DR-063 §1/§2: there is no failure without a cause. Each decision site builds
|
|
62
|
+
// the cause its own evidence states, the runtime attaches it to the error it
|
|
63
|
+
// marks as the FSM failure, and anything else the runtime marks becomes
|
|
64
|
+
// `runtime-defect` carrying its message as `reason`.
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
/** Adjudication reasons this runtime decides itself, and their codes. */
|
|
67
|
+
const GOVERNED_JUDGE_FAILURE_REASONS = [
|
|
68
|
+
'judge transport failed',
|
|
69
|
+
'corrective judge failed',
|
|
70
|
+
'corrective semantic candidate is invalid',
|
|
71
|
+
'semantic correction budget is unavailable',
|
|
72
|
+
];
|
|
73
|
+
const ABORTED_FAILURE_CAUSE = assertPlaybookFailureCause({
|
|
74
|
+
code: 'aborted',
|
|
75
|
+
evidence: {},
|
|
76
|
+
});
|
|
77
|
+
function runtimeDefectCause(reason) {
|
|
78
|
+
return assertPlaybookFailureCause({
|
|
79
|
+
code: 'runtime-defect',
|
|
80
|
+
evidence: { reason: reason.length === 0 ? 'unknown runtime defect' : reason },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function failureErrorEvidence(error) {
|
|
84
|
+
if (error === undefined || error === null)
|
|
85
|
+
return undefined;
|
|
86
|
+
const { name, message } = normalizeError(error);
|
|
87
|
+
return { name, message };
|
|
88
|
+
}
|
|
89
|
+
/** A string `code` the rejecting port reported, when it reported one. */
|
|
90
|
+
function failureErrorCode(error) {
|
|
91
|
+
if (typeof error !== 'object' || error === null)
|
|
92
|
+
return undefined;
|
|
93
|
+
try {
|
|
94
|
+
const code = error.code;
|
|
95
|
+
return typeof code === 'string' && code.length > 0 ? code : undefined;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function playerFailureCause(input) {
|
|
102
|
+
return assertPlaybookFailureCause({
|
|
103
|
+
code: 'player-failed',
|
|
104
|
+
evidence: {
|
|
105
|
+
roleId: input.roleId,
|
|
106
|
+
...(input.playerId === undefined ? {} : { playerId: input.playerId }),
|
|
107
|
+
error: failureErrorEvidence(input.error) ?? {
|
|
108
|
+
name: 'Error',
|
|
109
|
+
message: 'Unknown error',
|
|
110
|
+
},
|
|
111
|
+
...(failureErrorCode(input.error) === undefined
|
|
112
|
+
? {}
|
|
113
|
+
: { errorCode: failureErrorCode(input.error) }),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
49
118
|
// DR-028: both call boundaries treat an `ok` result whose `finalText` is
|
|
50
119
|
// missing, empty, or whitespace-only under one empty predicate, and that
|
|
51
120
|
// shape earns exactly one corrective re-ask — the same composed call
|
|
@@ -320,7 +389,13 @@ export function normalizeErrorCompact(err) {
|
|
|
320
389
|
if (err === undefined || err === null)
|
|
321
390
|
return undefined;
|
|
322
391
|
const normalized = normalizeError(err);
|
|
323
|
-
return {
|
|
392
|
+
return {
|
|
393
|
+
name: normalized.name,
|
|
394
|
+
message: normalized.message,
|
|
395
|
+
// DR-063 §2: on a failure the cause is why the compact form exists, so it
|
|
396
|
+
// survives where the stack does not.
|
|
397
|
+
...(normalized.cause === undefined ? {} : { cause: normalized.cause }),
|
|
398
|
+
};
|
|
324
399
|
}
|
|
325
400
|
export function normalizeErrorFull(err) {
|
|
326
401
|
if (err === undefined || err === null)
|
|
@@ -417,6 +492,97 @@ function continuationBlocks(input, resuming = false) {
|
|
|
417
492
|
export function composePlayerContinuation(input, body, resuming = false) {
|
|
418
493
|
return [...continuationBlocks(input, resuming), body].join('\n\n');
|
|
419
494
|
}
|
|
495
|
+
// DR-062 §4: the Coder is told which uncommitted changes were already in the
|
|
496
|
+
// worktree when its own call started. The block is composed from the call's
|
|
497
|
+
// own baseline observation, names paths only, and is bounded.
|
|
498
|
+
const PRE_EXISTING_CHANGE_GROUPS = [
|
|
499
|
+
'staged',
|
|
500
|
+
'modified',
|
|
501
|
+
'deleted',
|
|
502
|
+
'untracked',
|
|
503
|
+
'renamed',
|
|
504
|
+
'unmerged',
|
|
505
|
+
'other',
|
|
506
|
+
];
|
|
507
|
+
const PRE_EXISTING_CHANGE_PATH_LIMIT = 40;
|
|
508
|
+
/**
|
|
509
|
+
* The group one baseline projection entry belongs to. The shapes are the
|
|
510
|
+
* porcelain-v2 records the repository observer projects: an `untracked`,
|
|
511
|
+
* `rename-or-copy`, or `unmerged` kind, and otherwise an `ordinary` record
|
|
512
|
+
* whose two-character `xy` carries the index state first and the worktree
|
|
513
|
+
* state second.
|
|
514
|
+
*/
|
|
515
|
+
function preExistingChangeGroup(entry) {
|
|
516
|
+
if (!isPlainObject(entry))
|
|
517
|
+
return 'other';
|
|
518
|
+
if (entry.kind === 'untracked')
|
|
519
|
+
return 'untracked';
|
|
520
|
+
if (entry.kind === 'rename-or-copy')
|
|
521
|
+
return 'renamed';
|
|
522
|
+
if (entry.kind === 'unmerged')
|
|
523
|
+
return 'unmerged';
|
|
524
|
+
const xy = entry.xy;
|
|
525
|
+
if (entry.kind !== 'ordinary' || typeof xy !== 'string' || xy.length !== 2) {
|
|
526
|
+
return 'other';
|
|
527
|
+
}
|
|
528
|
+
if (xy[1] === 'D')
|
|
529
|
+
return 'deleted';
|
|
530
|
+
if (xy[1] === '.')
|
|
531
|
+
return xy[0] === '.' ? 'other' : 'staged';
|
|
532
|
+
return 'modified';
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* The pre-existing-changes block appended to an effect-authorized player
|
|
536
|
+
* prompt, or `undefined` for an empty baseline projection.
|
|
537
|
+
*/
|
|
538
|
+
function preExistingChangesBlock(baseline) {
|
|
539
|
+
const projection = baseline?.projection;
|
|
540
|
+
if (!isPlainObject(projection))
|
|
541
|
+
return undefined;
|
|
542
|
+
const grouped = new Map();
|
|
543
|
+
let total = 0;
|
|
544
|
+
for (const path of Object.keys(projection)) {
|
|
545
|
+
const group = preExistingChangeGroup(projection[path]);
|
|
546
|
+
const paths = grouped.get(group);
|
|
547
|
+
if (paths === undefined)
|
|
548
|
+
grouped.set(group, [path]);
|
|
549
|
+
else
|
|
550
|
+
paths.push(path);
|
|
551
|
+
total += 1;
|
|
552
|
+
}
|
|
553
|
+
if (total === 0)
|
|
554
|
+
return undefined;
|
|
555
|
+
const omitted = Math.max(0, total - PRE_EXISTING_CHANGE_PATH_LIMIT);
|
|
556
|
+
const lines = [];
|
|
557
|
+
let budget = PRE_EXISTING_CHANGE_PATH_LIMIT;
|
|
558
|
+
for (const group of PRE_EXISTING_CHANGE_GROUPS) {
|
|
559
|
+
const paths = grouped.get(group);
|
|
560
|
+
if (paths === undefined || budget <= 0)
|
|
561
|
+
continue;
|
|
562
|
+
const printed = paths.slice().sort().slice(0, budget);
|
|
563
|
+
budget -= printed.length;
|
|
564
|
+
lines.push(`> - ${group}: ${printed.join(', ')}`);
|
|
565
|
+
}
|
|
566
|
+
if (omitted > 0 && lines.length > 0) {
|
|
567
|
+
lines[lines.length - 1] += `, … and ${omitted} more`;
|
|
568
|
+
}
|
|
569
|
+
return [
|
|
570
|
+
'> Uncommitted changes present before this call, belonging to the Boss:',
|
|
571
|
+
...lines,
|
|
572
|
+
"> Leave them exactly as they are unless the task or the Boss's request requires building on them; never revert or delete them; when you commit any of them, name them in your final report.",
|
|
573
|
+
].join('\n');
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* The block for one governed call, or `undefined` where the call is not
|
|
577
|
+
* effect-authorized — a call whose declared outcomes carry no
|
|
578
|
+
* `one-descendant-commit` disposition may commit nothing, so it is told
|
|
579
|
+
* nothing.
|
|
580
|
+
*/
|
|
581
|
+
function effectAuthorizedPreExistingBlock(effectBoundary, baseline) {
|
|
582
|
+
return effectBoundary.dispositions.includes('one-descendant-commit')
|
|
583
|
+
? preExistingChangesBlock(baseline)
|
|
584
|
+
: undefined;
|
|
585
|
+
}
|
|
420
586
|
const PLACEHOLDER_PATTERN = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g;
|
|
421
587
|
function placeholderFieldName(token, fields) {
|
|
422
588
|
const explicit = fields[token];
|
|
@@ -830,7 +996,8 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
|
|
|
830
996
|
: false);
|
|
831
997
|
}
|
|
832
998
|
if (result.status !== 'ok') {
|
|
833
|
-
|
|
999
|
+
const failure = new Error(result.error ?? `captainBridge: callPlayer status "${result.status}"`);
|
|
1000
|
+
throw boundary?.markPlayerResultFailure?.(failure) ?? failure;
|
|
834
1001
|
}
|
|
835
1002
|
const finalText = result.finalText ?? '';
|
|
836
1003
|
if (isEmptyFinalText(finalText)) {
|
|
@@ -2019,6 +2186,15 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2019
2186
|
let deferInspectionEmissions = false;
|
|
2020
2187
|
let deferredInspectionEmissions = [];
|
|
2021
2188
|
let controlPlaneError;
|
|
2189
|
+
// DR-063 §2: the cause decided at the site the failure was decided, kept
|
|
2190
|
+
// for the failures whose Error the compiled artifact builds itself and the
|
|
2191
|
+
// runtime therefore cannot decorate. It is cleared at each Boss turn start
|
|
2192
|
+
// and at each successful player result, so it is never stale, and it is
|
|
2193
|
+
// only ever read as the cause of an error that carries none of its own.
|
|
2194
|
+
let lastFailureCause;
|
|
2195
|
+
const rememberFailureCause = (cause) => {
|
|
2196
|
+
lastFailureCause = cause;
|
|
2197
|
+
};
|
|
2022
2198
|
// Previous root-machine state for the inspect-driven telemetry /
|
|
2023
2199
|
// status emitter. undefined before the first inspect firing.
|
|
2024
2200
|
let priorState;
|
|
@@ -2388,6 +2564,13 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2388
2564
|
refreshRetainedEffectReconciliation(current);
|
|
2389
2565
|
syncDeferredReconciliationOverlay();
|
|
2390
2566
|
refreshUnresolvedSemanticReconciliation(current);
|
|
2567
|
+
return projectUnresolvedEffectEnvelopes(current);
|
|
2568
|
+
}
|
|
2569
|
+
/**
|
|
2570
|
+
* The unresolved envelopes of one already-refreshed ledger mirror. Pure:
|
|
2571
|
+
* `describe()` reads it through the control view without moving anything.
|
|
2572
|
+
*/
|
|
2573
|
+
function projectUnresolvedEffectEnvelopes(current) {
|
|
2391
2574
|
if (!hasUnresolvedReconciliation())
|
|
2392
2575
|
return [];
|
|
2393
2576
|
const boundaryIds = new Set(unresolvedSemanticBoundaryIds);
|
|
@@ -2919,16 +3102,44 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2919
3102
|
},
|
|
2920
3103
|
});
|
|
2921
3104
|
}
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
3105
|
+
// DR-063 §1: the cause of an unresolved governed settlement. A reconciled
|
|
3106
|
+
// mismatch supplies its own receipt-read cause; the reasons this function
|
|
3107
|
+
// owns map to adjudication, abort, and runtime-defect codes.
|
|
3108
|
+
function governedSettlementCause(reason, error, aborted, supplied) {
|
|
3109
|
+
if (supplied !== undefined)
|
|
3110
|
+
return supplied;
|
|
3111
|
+
if (aborted || reason.includes('aborted'))
|
|
3112
|
+
return ABORTED_FAILURE_CAUSE;
|
|
3113
|
+
if (GOVERNED_JUDGE_FAILURE_REASONS.includes(reason)) {
|
|
3114
|
+
const transport = failureErrorEvidence(error);
|
|
3115
|
+
return assertPlaybookFailureCause({
|
|
3116
|
+
code: 'judge-failed',
|
|
3117
|
+
evidence: {
|
|
3118
|
+
reason,
|
|
3119
|
+
...(transport === undefined ? {} : { error: transport }),
|
|
3120
|
+
},
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
return runtimeDefectCause(reason);
|
|
3124
|
+
}
|
|
3125
|
+
function unresolvedGovernedSettlement(reason, error, signal = activeSignal, cause) {
|
|
3126
|
+
const aborted = error !== undefined && signal?.aborted === true
|
|
3127
|
+
? Object.is(error, signal.reason)
|
|
3128
|
+
: false;
|
|
3129
|
+
const decided = governedSettlementCause(reason, error, aborted, cause);
|
|
3130
|
+
rememberFailureCause(decided);
|
|
3131
|
+
if (aborted) {
|
|
3132
|
+
return {
|
|
3133
|
+
status: 'unresolved',
|
|
3134
|
+
error: attachPlaybookFailureCause(error, decided),
|
|
3135
|
+
};
|
|
2925
3136
|
}
|
|
2926
3137
|
const failure = error instanceof Error
|
|
2927
3138
|
? error
|
|
2928
3139
|
: new Error(`${label} governed outcome remains unresolved: ${reason}`);
|
|
2929
3140
|
return {
|
|
2930
3141
|
status: 'unresolved',
|
|
2931
|
-
error: markFsmResultFailure(failure),
|
|
3142
|
+
error: attachPlaybookFailureCause(markFsmResultFailure(failure), decided),
|
|
2932
3143
|
};
|
|
2933
3144
|
}
|
|
2934
3145
|
async function spendSemanticCorrectionBudget(completedBoundary, receipt, finalText, semanticCandidate) {
|
|
@@ -3085,7 +3296,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3085
3296
|
}
|
|
3086
3297
|
const semanticCandidate = snapshotJsonValue(reconciliation.evidence.semanticCandidate, `${label} governed semantic candidate`);
|
|
3087
3298
|
if (reconciliation.status === 'unresolved') {
|
|
3088
|
-
governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement(reconciliation.reason));
|
|
3299
|
+
governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement(reconciliation.reason, undefined, undefined, reconciliation.cause));
|
|
3089
3300
|
return { finalText, semanticCandidate, unresolved: true };
|
|
3090
3301
|
}
|
|
3091
3302
|
const output = reconciliation.output;
|
|
@@ -3462,9 +3673,16 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3462
3673
|
}
|
|
3463
3674
|
activePlayerKeys.add(playerKey);
|
|
3464
3675
|
try {
|
|
3465
|
-
const runTracedPlayerCall = async (resume = selectedResume
|
|
3676
|
+
const runTracedPlayerCall = async (resume = selectedResume,
|
|
3677
|
+
// DR-062 §4: composed from this call's own baseline observation,
|
|
3678
|
+
// so the prompt the trace records is the prompt the player is sent.
|
|
3679
|
+
preExistingBlock) => {
|
|
3466
3680
|
const identity = callIdentity(resume);
|
|
3467
|
-
const
|
|
3681
|
+
const withPreExisting = (text) => preExistingBlock === undefined
|
|
3682
|
+
? text
|
|
3683
|
+
: `${text}\n\n${preExistingBlock}`;
|
|
3684
|
+
const callPrompt = withPreExisting(resume === false ? freshPrompt : prompt);
|
|
3685
|
+
const callFreshPrompt = withPreExisting(freshPrompt);
|
|
3468
3686
|
await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt: callPrompt }, position, signal);
|
|
3469
3687
|
let rawResult;
|
|
3470
3688
|
try {
|
|
@@ -3473,7 +3691,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3473
3691
|
// never start after abort, so settle the already-started pair
|
|
3474
3692
|
// as `aborted` through the catch below.
|
|
3475
3693
|
signal.throwIfAborted();
|
|
3476
|
-
rawResult = await requireHostPorts().callPlayer(roleId, callPrompt, signal, {
|
|
3694
|
+
rawResult = await requireHostPorts().callPlayer(roleId, callPrompt, signal, {
|
|
3695
|
+
resume,
|
|
3696
|
+
...(callPrompt === callFreshPrompt
|
|
3697
|
+
? {}
|
|
3698
|
+
: { freshPrompt: callFreshPrompt }),
|
|
3699
|
+
});
|
|
3477
3700
|
// A host promise is not required to honor cancellation. Do not
|
|
3478
3701
|
// let a late result mutate continuity or publish a successful
|
|
3479
3702
|
// finish.
|
|
@@ -3494,7 +3717,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3494
3717
|
}
|
|
3495
3718
|
// A thrown port call carries no authoritative result, so the
|
|
3496
3719
|
// prior token remains available for a later explicit resume.
|
|
3497
|
-
throw error;
|
|
3720
|
+
throw decidePlayerCallFailure(error, roleId, playerId, signal);
|
|
3498
3721
|
}
|
|
3499
3722
|
let result;
|
|
3500
3723
|
try {
|
|
@@ -3509,7 +3732,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3509
3732
|
catch {
|
|
3510
3733
|
// The malformed host result remains authoritative.
|
|
3511
3734
|
}
|
|
3512
|
-
throw error;
|
|
3735
|
+
throw decidePlayerCallFailure(error, roleId, playerId, signal);
|
|
3513
3736
|
}
|
|
3514
3737
|
try {
|
|
3515
3738
|
updatePlayerResume(roleId, playerId, result);
|
|
@@ -3538,6 +3761,24 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3538
3761
|
? { resumeToken: result.resumeToken }
|
|
3539
3762
|
: {}),
|
|
3540
3763
|
}, position);
|
|
3764
|
+
// DR-063 §2: the player bridge builds the failure Error for a
|
|
3765
|
+
// non-`ok` result, so the cause is decided here, where the role,
|
|
3766
|
+
// the player, and the reported error are known, and a successful
|
|
3767
|
+
// result clears it so no later failure inherits it.
|
|
3768
|
+
if (result.status === 'ok') {
|
|
3769
|
+
lastFailureCause = undefined;
|
|
3770
|
+
}
|
|
3771
|
+
else if (result.status === 'aborted') {
|
|
3772
|
+
rememberFailureCause(ABORTED_FAILURE_CAUSE);
|
|
3773
|
+
}
|
|
3774
|
+
else {
|
|
3775
|
+
rememberFailureCause(playerFailureCause({
|
|
3776
|
+
roleId,
|
|
3777
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
3778
|
+
error: result.error ??
|
|
3779
|
+
`callPlayer status ${JSON.stringify(result.status)}`,
|
|
3780
|
+
}));
|
|
3781
|
+
}
|
|
3541
3782
|
return result;
|
|
3542
3783
|
};
|
|
3543
3784
|
const effectBoundary = governedBoundarySeed(input, roleId, callId, turnId);
|
|
@@ -3560,7 +3801,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3560
3801
|
deferredContinuation.playerId = playerId;
|
|
3561
3802
|
deferredContinuation.signal = signal;
|
|
3562
3803
|
try {
|
|
3563
|
-
deferredContinuation.result = await runTracedPlayerCall(selectedResume);
|
|
3804
|
+
deferredContinuation.result = await runTracedPlayerCall(selectedResume, effectAuthorizedPreExistingBlock(effectBoundary, deferredContinuation.baseline));
|
|
3564
3805
|
}
|
|
3565
3806
|
catch (error) {
|
|
3566
3807
|
deferredContinuation.callError = error;
|
|
@@ -3582,7 +3823,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3582
3823
|
const exclusive = await repositoryCapability.runExclusive({
|
|
3583
3824
|
signal,
|
|
3584
3825
|
effectBoundary,
|
|
3585
|
-
|
|
3826
|
+
// DR-062 §4: the call's own baseline, supplied by the host that
|
|
3827
|
+
// captured it under this exclusive claim. A host that supplies
|
|
3828
|
+
// none states no pre-existing change, exactly as an empty
|
|
3829
|
+
// baseline projection does.
|
|
3830
|
+
operation: (context) => runTracedPlayerCall(selectedResume, effectAuthorizedPreExistingBlock(effectBoundary, context?.baseline)),
|
|
3586
3831
|
completeEffectBoundary: completionEvidenceFor(input, roleId, playerId, signal, undefined),
|
|
3587
3832
|
});
|
|
3588
3833
|
return acknowledgeGovernedPlayerResult(exclusive, effectBoundary.boundaryId);
|
|
@@ -3604,6 +3849,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3604
3849
|
activePlayerKeys.delete(playerKey);
|
|
3605
3850
|
}
|
|
3606
3851
|
},
|
|
3852
|
+
markPlayerResultFailure(error) {
|
|
3853
|
+
return lastFailureCause === undefined
|
|
3854
|
+
? error
|
|
3855
|
+
: attachPlaybookFailureCause(error, lastFailureCause);
|
|
3856
|
+
},
|
|
3607
3857
|
takeGovernedPlayerOutput(result) {
|
|
3608
3858
|
const settlement = governedPlayerSettlements.get(result);
|
|
3609
3859
|
if (settlement !== undefined)
|
|
@@ -4118,6 +4368,61 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4118
4368
|
function tracePositionForActiveTurn() {
|
|
4119
4369
|
return activeTurnId === undefined ? {} : { turnId: activeTurnId };
|
|
4120
4370
|
}
|
|
4371
|
+
// DR-063 §2: the parked failure's error as every public surface publishes
|
|
4372
|
+
// it — the FSM's own `lastError`, completed with the cause decided where
|
|
4373
|
+
// the failure was decided. An error that already carries a valid cause
|
|
4374
|
+
// keeps it; an artifact-built error takes the runtime's own slot; anything
|
|
4375
|
+
// else is a runtime defect carrying its message. There is no failure
|
|
4376
|
+
// without a cause.
|
|
4377
|
+
function failedStateError(context) {
|
|
4378
|
+
const normalized = normalizeErrorFull(context.lastError);
|
|
4379
|
+
if (normalized === undefined || normalized.cause !== undefined) {
|
|
4380
|
+
return normalized;
|
|
4381
|
+
}
|
|
4382
|
+
return {
|
|
4383
|
+
...normalized,
|
|
4384
|
+
cause: lastFailureCause ?? runtimeDefectCause(normalized.message),
|
|
4385
|
+
};
|
|
4386
|
+
}
|
|
4387
|
+
// DR-063 §2: a rejected player port or a result the runtime cannot read is
|
|
4388
|
+
// a `player-failed` failure named by role; an abort is `aborted`.
|
|
4389
|
+
function decidePlayerCallFailure(error, roleId, playerId, signal) {
|
|
4390
|
+
const cause = isAbortFailure(error, signal)
|
|
4391
|
+
? ABORTED_FAILURE_CAUSE
|
|
4392
|
+
: playerFailureCause({
|
|
4393
|
+
roleId,
|
|
4394
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
4395
|
+
error,
|
|
4396
|
+
});
|
|
4397
|
+
rememberFailureCause(cause);
|
|
4398
|
+
return attachPlaybookFailureCause(error, cause);
|
|
4399
|
+
}
|
|
4400
|
+
function completeFailedStatuses(statuses, state, context) {
|
|
4401
|
+
if (state.stateId !== 'failed')
|
|
4402
|
+
return statuses;
|
|
4403
|
+
const lastError = failedStateError(context);
|
|
4404
|
+
if (lastError?.cause === undefined)
|
|
4405
|
+
return statuses;
|
|
4406
|
+
const cause = lastError.cause;
|
|
4407
|
+
return statuses.map((status) => {
|
|
4408
|
+
const data = status.data;
|
|
4409
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
4410
|
+
return status;
|
|
4411
|
+
}
|
|
4412
|
+
const recorded = data;
|
|
4413
|
+
const recordedError = recorded.lastError;
|
|
4414
|
+
if (recordedError === undefined ||
|
|
4415
|
+
recordedError === null ||
|
|
4416
|
+
typeof recordedError !== 'object' ||
|
|
4417
|
+
Array.isArray(recordedError)) {
|
|
4418
|
+
return status;
|
|
4419
|
+
}
|
|
4420
|
+
return {
|
|
4421
|
+
...status,
|
|
4422
|
+
data: snapshotJsonValue({ ...recorded, lastError: { ...recordedError, cause } }, 'failed status data'),
|
|
4423
|
+
};
|
|
4424
|
+
});
|
|
4425
|
+
}
|
|
4121
4426
|
function structuredStateTelemetryPayload(previousState, state, event, context) {
|
|
4122
4427
|
const payload = {
|
|
4123
4428
|
from: previousState?.value ?? null,
|
|
@@ -4132,7 +4437,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4132
4437
|
payload.pendingBossQuestion = pendingBossQuestion;
|
|
4133
4438
|
}
|
|
4134
4439
|
if (state.stateId === 'failed') {
|
|
4135
|
-
const lastError =
|
|
4440
|
+
const lastError = failedStateError(context);
|
|
4136
4441
|
if (lastError !== undefined)
|
|
4137
4442
|
payload.lastError = lastError;
|
|
4138
4443
|
}
|
|
@@ -4302,7 +4607,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4302
4607
|
validateBoundQuestionProjection();
|
|
4303
4608
|
}
|
|
4304
4609
|
const payload = structuredStateTelemetryPayload(previousState, state, inspectionEvent.event, context);
|
|
4305
|
-
const stateStatuses = statusesForState(state, context, inspectionEvent.event);
|
|
4610
|
+
const stateStatuses = completeFailedStatuses(statusesForState(state, context, inspectionEvent.event), state, context);
|
|
4306
4611
|
const outcomeStatuses = usesDefaultStatuses
|
|
4307
4612
|
? acceptedOutcomes.map(({ acceptedOutcome }) => ({
|
|
4308
4613
|
message: `→ ${acceptedOutcome}`,
|
|
@@ -4382,10 +4687,17 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4382
4687
|
: outcome === 'aborted'
|
|
4383
4688
|
? activeSignal?.reason
|
|
4384
4689
|
: undefined);
|
|
4690
|
+
if (failure === undefined)
|
|
4691
|
+
return { outcome, state };
|
|
4692
|
+
// DR-063 §2: a `failed` settlement publishes the same cause-completed
|
|
4693
|
+
// error the failed state's status line and telemetry publish.
|
|
4694
|
+
const normalized = outcome === 'failed'
|
|
4695
|
+
? failedStateError({ lastError: failure })
|
|
4696
|
+
: normalizeError(failure);
|
|
4385
4697
|
return {
|
|
4386
4698
|
outcome,
|
|
4387
4699
|
state,
|
|
4388
|
-
...(
|
|
4700
|
+
...(normalized === undefined ? {} : { error: normalized }),
|
|
4389
4701
|
};
|
|
4390
4702
|
}
|
|
4391
4703
|
function settledOutcome(signal) {
|
|
@@ -4594,10 +4906,36 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4594
4906
|
action: {
|
|
4595
4907
|
id: `retry:${retryEvent.type}`,
|
|
4596
4908
|
label: `Retry: ${description}`,
|
|
4909
|
+
// DR-063 §3: retrying a failed call is `ready` — the runtime cannot
|
|
4910
|
+
// see whether the Boss changed the outside world since it failed.
|
|
4911
|
+
standing: 'ready',
|
|
4597
4912
|
},
|
|
4598
4913
|
event: retryEvent,
|
|
4599
4914
|
};
|
|
4600
4915
|
}
|
|
4916
|
+
/**
|
|
4917
|
+
* DR-063 §3: reconciliation only re-reads the host's ledger, which the
|
|
4918
|
+
* control view has just done. Where no checkpoint restoration is eligible
|
|
4919
|
+
* and every unresolved envelope's boundary already holds a complete
|
|
4920
|
+
* receipt, re-reading can resolve nothing, so the action is a no-op.
|
|
4921
|
+
*/
|
|
4922
|
+
function reconciliationStanding(deferredRestoreOperationId) {
|
|
4923
|
+
if (deferredRestoreOperationId !== undefined)
|
|
4924
|
+
return { standing: 'ready' };
|
|
4925
|
+
const current = effectLedgerMirror;
|
|
4926
|
+
const envelopes = projectUnresolvedEffectEnvelopes(current);
|
|
4927
|
+
if (envelopes.length === 0)
|
|
4928
|
+
return { standing: 'ready' };
|
|
4929
|
+
const boundaryIds = envelopes.flatMap((envelope) => envelope.kind === 'boundary'
|
|
4930
|
+
? [envelope.boundaryId]
|
|
4931
|
+
: (current.logicalOperations.find(({ operationId }) => operationId === envelope.operationId)?.boundaryIds ?? []));
|
|
4932
|
+
if (boundaryIds.length === 0)
|
|
4933
|
+
return { standing: 'ready' };
|
|
4934
|
+
const complete = boundaryIds.every((boundaryId) => current.boundaries.find((candidate) => candidate.boundaryId === boundaryId)?.physicalReceipt !== undefined);
|
|
4935
|
+
return complete
|
|
4936
|
+
? { standing: 'no-op', reason: 'receipt-complete' }
|
|
4937
|
+
: { standing: 'ready' };
|
|
4938
|
+
}
|
|
4601
4939
|
function deriveControlActions(snapshot) {
|
|
4602
4940
|
// Actions derive only at the safe point the parked snapshot also
|
|
4603
4941
|
// uses — quiescent actor with status `active` and no pending nested
|
|
@@ -4629,6 +4967,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4629
4967
|
action: {
|
|
4630
4968
|
id: UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID,
|
|
4631
4969
|
label: 'Retry unresolved effect reconciliation',
|
|
4970
|
+
...reconciliationStanding(deferredRestoreOperationId),
|
|
4632
4971
|
},
|
|
4633
4972
|
unresolvedEffectAction: 'reconcile',
|
|
4634
4973
|
...(deferredRestoreOperationId === undefined
|
|
@@ -4638,6 +4977,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4638
4977
|
action: {
|
|
4639
4978
|
id: UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID,
|
|
4640
4979
|
label: 'Abandon unresolved workflow attempt',
|
|
4980
|
+
// DR-063 §3: abandonment always changes the session's standing.
|
|
4981
|
+
standing: 'ready',
|
|
4641
4982
|
},
|
|
4642
4983
|
unresolvedEffectAction: 'abandon',
|
|
4643
4984
|
});
|
|
@@ -4664,6 +5005,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4664
5005
|
action: {
|
|
4665
5006
|
id: `jump:${targetId}`,
|
|
4666
5007
|
label: `Resume from: ${description}`,
|
|
5008
|
+
// DR-063 §3: a jump always moves the machine.
|
|
5009
|
+
standing: 'ready',
|
|
4667
5010
|
},
|
|
4668
5011
|
event,
|
|
4669
5012
|
});
|
|
@@ -5050,8 +5393,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5050
5393
|
signal,
|
|
5051
5394
|
operationId: operation.operationId,
|
|
5052
5395
|
effectBoundary,
|
|
5053
|
-
operation: async (
|
|
5396
|
+
operation: async (context) => {
|
|
5397
|
+
const { playerContinuation } = context ?? {};
|
|
5054
5398
|
validateBinding(playerContinuation);
|
|
5399
|
+
// DR-062 §4: the continuation's own baseline, captured before the
|
|
5400
|
+
// FSM re-enters the bound player state that reads it.
|
|
5401
|
+
continuation.baseline = context?.baseline;
|
|
5055
5402
|
continuation.playerContinuation = selectPlayerResume(effectBoundary.roleId, boundPlayerId);
|
|
5056
5403
|
continuationStarted = true;
|
|
5057
5404
|
actor.send(event);
|
|
@@ -5440,7 +5787,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5440
5787
|
const pending = !hasUnresolvedReconciliation()
|
|
5441
5788
|
? pendingBossQuestionForState(state, context)
|
|
5442
5789
|
: undefined;
|
|
5443
|
-
const lastError =
|
|
5790
|
+
const lastError = state.stateId === 'failed'
|
|
5791
|
+
? failedStateError(context)
|
|
5792
|
+
: normalizeErrorFull(context.lastError);
|
|
5444
5793
|
const projectedContext = projectControlContext(context);
|
|
5445
5794
|
const stateDescription = !hasUnresolvedReconciliation()
|
|
5446
5795
|
? stateDescriptionFor(state)
|
|
@@ -5779,6 +6128,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5779
6128
|
activeAborts = abortReasonClassifier(signal);
|
|
5780
6129
|
activeAbortEmission = undefined;
|
|
5781
6130
|
controlPlaneError = undefined;
|
|
6131
|
+
// DR-063 §2: each turn decides its own failures, so the slot never
|
|
6132
|
+
// carries a previous turn's cause into this one.
|
|
6133
|
+
lastFailureCause = undefined;
|
|
5782
6134
|
let result;
|
|
5783
6135
|
let operationError;
|
|
5784
6136
|
// The boundary sentinel releases on every exit: a settlement defect
|