@sublang/playbook 13.3.0 → 14.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 +5 -5
- package/docs/cli.md +14 -14
- package/docs/configuration.md +10 -2
- package/docs/embedding.md +20 -1
- package/package.json +6 -1
- package/reference/sdlc/branch.playbook/branch.fsm.d.ts +1 -1
- package/reference/sdlc/code.playbook/bin/launch-config.js +88 -5
- package/reference/sdlc/code.playbook/bin/playbook.js +4 -0
- package/reference/sdlc/code.playbook/bin/repository-effects.js +198 -17
- package/reference/sdlc/code.playbook/bin/run.js +4 -0
- package/reference/sdlc/code.playbook/bin/session-store.js +2 -2
- package/reference/sdlc/code.playbook/code.fsm.d.ts +1 -1
- 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 +374 -20
- package/reference/sdlc/code.playbook/playbook-captain.ts +478 -38
- package/reference/sdlc/code.playbook/session-host.d.ts +6 -3
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +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.d.ts +3 -10
- package/reference/sdlc/dev.playbook/dev.playbook.js +6 -6
- package/reference/sdlc/dev.playbook/dev.playbook.ts +8 -9
- package/reference/sdlc/dev.playbook/dev.registry.js +1 -1
- package/reference/sdlc/dev.playbook/dev.registry.ts +1 -1
- package/reference/sdlc/pr.md +17 -4
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +18 -4
- package/reference/sdlc/pr.playbook/pr.fsm.js +49 -18
- package/reference/sdlc/pr.playbook/pr.fsm.ts +63 -20
- package/reference/sdlc/pr.playbook/pr.gears.md +20 -7
- package/reference/sdlc/pr.playbook/pr.playbook.js +2 -1
- package/reference/sdlc/pr.playbook/pr.playbook.ts +2 -1
- package/reference/sdlc/review.playbook/review.fsm.d.ts +1 -1
- package/slc/experiments/fsm-scaffold-guidance.md +31 -0
- package/slc/gears2fsm.md +145 -58
- package/slc/link.md +190 -7
- package/slc/materialize-link.mjs +460 -0
- package/slc/optimize.md +46 -6
- package/slc/scaffold-fsm.mjs +225 -0
- package/slc/slc.pin-inputs.json +13 -0
- package/slc/text2gears.md +38 -3
- package/slc/workflow-contracts.json +371 -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 +407 -21
- package/src/xstate-playbook-runtime.ts +514 -20
- package/src/xstate-runtime.d.ts +9 -1
- package/src/xstate-runtime.js +299 -19
- package/src/xstate-runtime.ts +362 -21
|
@@ -31,9 +31,11 @@ import {
|
|
|
31
31
|
createAcceptedOutcomeConsumer,
|
|
32
32
|
type AcceptedOutcomeReceipt,
|
|
33
33
|
} from './accepted-outcome.js';
|
|
34
|
+
import { assertPlaybookFailureCause } from './runtime.js';
|
|
34
35
|
import {
|
|
35
36
|
assertPlaybookRuntimeSnapshot,
|
|
36
37
|
assertPlaybookEffectLedger,
|
|
38
|
+
attachPlaybookFailureCause,
|
|
37
39
|
combineAbortSignals,
|
|
38
40
|
createNestedPlaybookBridge,
|
|
39
41
|
detachPersistedMachineSnapshot,
|
|
@@ -60,7 +62,9 @@ import type {
|
|
|
60
62
|
PlaybookEffectBoundaryStart,
|
|
61
63
|
PlaybookEffectLedger,
|
|
62
64
|
PlaybookEffectLedgerCapability,
|
|
65
|
+
NormalizedError,
|
|
63
66
|
PlaybookEffectLogicalOperation,
|
|
67
|
+
PlaybookFailureCause,
|
|
64
68
|
PlaybookPendingBossQuestion,
|
|
65
69
|
PlaybookPorts,
|
|
66
70
|
PlaybookRepositoryReceipt,
|
|
@@ -149,6 +153,12 @@ export interface RuntimeBoundaryCalls {
|
|
|
149
153
|
result: PlayerResult,
|
|
150
154
|
output: PlaybookActorOutput,
|
|
151
155
|
): void;
|
|
156
|
+
/**
|
|
157
|
+
* DR-063 §2: decorate the failure the bridge builds for a non-`ok` result
|
|
158
|
+
* with the cause the boundary decided at the call itself, where the role,
|
|
159
|
+
* the resolved player, and the reported error are known.
|
|
160
|
+
*/
|
|
161
|
+
markPlayerResultFailure?(error: Error): Error;
|
|
152
162
|
callJudge(
|
|
153
163
|
purpose: JudgePurpose,
|
|
154
164
|
stateId: string | undefined,
|
|
@@ -236,6 +246,18 @@ const fsmResultFailures = new WeakSet<object>();
|
|
|
236
246
|
|
|
237
247
|
function markFsmResultFailure(error: Error): Error {
|
|
238
248
|
fsmResultFailures.add(error);
|
|
249
|
+
// DR-063 §2: there is no failure without a cause. A failure the runtime
|
|
250
|
+
// marks with nothing more specific is a runtime defect carrying its own
|
|
251
|
+
// message; a decision site that knows better attaches its cause afterwards.
|
|
252
|
+
attachPlaybookFailureCause(error, {
|
|
253
|
+
code: 'runtime-defect',
|
|
254
|
+
evidence: {
|
|
255
|
+
reason:
|
|
256
|
+
typeof error.message === 'string' && error.message.length > 0
|
|
257
|
+
? error.message
|
|
258
|
+
: 'unknown runtime defect',
|
|
259
|
+
},
|
|
260
|
+
});
|
|
239
261
|
return error;
|
|
240
262
|
}
|
|
241
263
|
|
|
@@ -247,6 +269,74 @@ function isFsmResultFailure(error: unknown): boolean {
|
|
|
247
269
|
);
|
|
248
270
|
}
|
|
249
271
|
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// DR-063 §1/§2: there is no failure without a cause. Each decision site builds
|
|
274
|
+
// the cause its own evidence states, the runtime attaches it to the error it
|
|
275
|
+
// marks as the FSM failure, and anything else the runtime marks becomes
|
|
276
|
+
// `runtime-defect` carrying its message as `reason`.
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
/** Adjudication reasons this runtime decides itself, and their codes. */
|
|
280
|
+
const GOVERNED_JUDGE_FAILURE_REASONS: readonly string[] = [
|
|
281
|
+
'judge transport failed',
|
|
282
|
+
'corrective judge failed',
|
|
283
|
+
'corrective semantic candidate is invalid',
|
|
284
|
+
'semantic correction budget is unavailable',
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
const ABORTED_FAILURE_CAUSE: PlaybookFailureCause = assertPlaybookFailureCause({
|
|
288
|
+
code: 'aborted',
|
|
289
|
+
evidence: {},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
function runtimeDefectCause(reason: string): PlaybookFailureCause {
|
|
293
|
+
return assertPlaybookFailureCause({
|
|
294
|
+
code: 'runtime-defect',
|
|
295
|
+
evidence: { reason: reason.length === 0 ? 'unknown runtime defect' : reason },
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function failureErrorEvidence(
|
|
300
|
+
error: unknown,
|
|
301
|
+
): { readonly name: string; readonly message: string } | undefined {
|
|
302
|
+
if (error === undefined || error === null) return undefined;
|
|
303
|
+
const { name, message } = normalizeError(error);
|
|
304
|
+
return { name, message };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** A string `code` the rejecting port reported, when it reported one. */
|
|
308
|
+
function failureErrorCode(error: unknown): string | undefined {
|
|
309
|
+
if (typeof error !== 'object' || error === null) return undefined;
|
|
310
|
+
try {
|
|
311
|
+
const code = (error as { code?: unknown }).code;
|
|
312
|
+
return typeof code === 'string' && code.length > 0 ? code : undefined;
|
|
313
|
+
} catch {
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function playerFailureCause(input: {
|
|
319
|
+
readonly roleId: string;
|
|
320
|
+
readonly playerId?: string;
|
|
321
|
+
readonly error: unknown;
|
|
322
|
+
}): PlaybookFailureCause {
|
|
323
|
+
return assertPlaybookFailureCause({
|
|
324
|
+
code: 'player-failed',
|
|
325
|
+
evidence: {
|
|
326
|
+
roleId: input.roleId,
|
|
327
|
+
...(input.playerId === undefined ? {} : { playerId: input.playerId }),
|
|
328
|
+
error: failureErrorEvidence(input.error) ?? {
|
|
329
|
+
name: 'Error',
|
|
330
|
+
message: 'Unknown error',
|
|
331
|
+
},
|
|
332
|
+
...(failureErrorCode(input.error) === undefined
|
|
333
|
+
? {}
|
|
334
|
+
: { errorCode: failureErrorCode(input.error)! }),
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
|
|
250
340
|
// ---------------------------------------------------------------------------
|
|
251
341
|
// DR-028: both call boundaries treat an `ok` result whose `finalText` is
|
|
252
342
|
// missing, empty, or whitespace-only under one empty predicate, and that
|
|
@@ -946,15 +1036,21 @@ export function parseJudgeJson(raw: string): unknown {
|
|
|
946
1036
|
|
|
947
1037
|
export function normalizeErrorCompact(
|
|
948
1038
|
err: unknown,
|
|
949
|
-
):
|
|
1039
|
+
): Omit<NormalizedError, 'stack'> | undefined {
|
|
950
1040
|
if (err === undefined || err === null) return undefined;
|
|
951
1041
|
const normalized = normalizeError(err);
|
|
952
|
-
return {
|
|
1042
|
+
return {
|
|
1043
|
+
name: normalized.name,
|
|
1044
|
+
message: normalized.message,
|
|
1045
|
+
// DR-063 §2: on a failure the cause is why the compact form exists, so it
|
|
1046
|
+
// survives where the stack does not.
|
|
1047
|
+
...(normalized.cause === undefined ? {} : { cause: normalized.cause }),
|
|
1048
|
+
};
|
|
953
1049
|
}
|
|
954
1050
|
|
|
955
1051
|
export function normalizeErrorFull(
|
|
956
1052
|
err: unknown,
|
|
957
|
-
):
|
|
1053
|
+
): NormalizedError | undefined {
|
|
958
1054
|
if (err === undefined || err === null) return undefined;
|
|
959
1055
|
return normalizeError(err);
|
|
960
1056
|
}
|
|
@@ -1090,6 +1186,98 @@ export function composePlayerContinuation(
|
|
|
1090
1186
|
return [...continuationBlocks(input, resuming), body].join('\n\n');
|
|
1091
1187
|
}
|
|
1092
1188
|
|
|
1189
|
+
// DR-062 §4: the Coder is told which uncommitted changes were already in the
|
|
1190
|
+
// worktree when its own call started. The block is composed from the call's
|
|
1191
|
+
// own baseline observation, names paths only, and is bounded.
|
|
1192
|
+
const PRE_EXISTING_CHANGE_GROUPS = [
|
|
1193
|
+
'staged',
|
|
1194
|
+
'modified',
|
|
1195
|
+
'deleted',
|
|
1196
|
+
'untracked',
|
|
1197
|
+
'renamed',
|
|
1198
|
+
'unmerged',
|
|
1199
|
+
'other',
|
|
1200
|
+
] as const;
|
|
1201
|
+
|
|
1202
|
+
type PreExistingChangeGroup = (typeof PRE_EXISTING_CHANGE_GROUPS)[number];
|
|
1203
|
+
|
|
1204
|
+
const PRE_EXISTING_CHANGE_PATH_LIMIT = 40;
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* The group one baseline projection entry belongs to. The shapes are the
|
|
1208
|
+
* porcelain-v2 records the repository observer projects: an `untracked`,
|
|
1209
|
+
* `rename-or-copy`, or `unmerged` kind, and otherwise an `ordinary` record
|
|
1210
|
+
* whose two-character `xy` carries the index state first and the worktree
|
|
1211
|
+
* state second.
|
|
1212
|
+
*/
|
|
1213
|
+
function preExistingChangeGroup(entry: unknown): PreExistingChangeGroup {
|
|
1214
|
+
if (!isPlainObject(entry)) return 'other';
|
|
1215
|
+
if (entry.kind === 'untracked') return 'untracked';
|
|
1216
|
+
if (entry.kind === 'rename-or-copy') return 'renamed';
|
|
1217
|
+
if (entry.kind === 'unmerged') return 'unmerged';
|
|
1218
|
+
const xy = entry.xy;
|
|
1219
|
+
if (entry.kind !== 'ordinary' || typeof xy !== 'string' || xy.length !== 2) {
|
|
1220
|
+
return 'other';
|
|
1221
|
+
}
|
|
1222
|
+
if (xy[1] === 'D') return 'deleted';
|
|
1223
|
+
if (xy[1] === '.') return xy[0] === '.' ? 'other' : 'staged';
|
|
1224
|
+
return 'modified';
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* The pre-existing-changes block appended to an effect-authorized player
|
|
1229
|
+
* prompt, or `undefined` for an empty baseline projection.
|
|
1230
|
+
*/
|
|
1231
|
+
function preExistingChangesBlock(
|
|
1232
|
+
baseline: PlaybookRepositoryReceipt['baseline'] | undefined,
|
|
1233
|
+
): string | undefined {
|
|
1234
|
+
const projection = baseline?.projection;
|
|
1235
|
+
if (!isPlainObject(projection)) return undefined;
|
|
1236
|
+
const grouped = new Map<PreExistingChangeGroup, string[]>();
|
|
1237
|
+
let total = 0;
|
|
1238
|
+
for (const path of Object.keys(projection)) {
|
|
1239
|
+
const group = preExistingChangeGroup(projection[path]);
|
|
1240
|
+
const paths = grouped.get(group);
|
|
1241
|
+
if (paths === undefined) grouped.set(group, [path]);
|
|
1242
|
+
else paths.push(path);
|
|
1243
|
+
total += 1;
|
|
1244
|
+
}
|
|
1245
|
+
if (total === 0) return undefined;
|
|
1246
|
+
const omitted = Math.max(0, total - PRE_EXISTING_CHANGE_PATH_LIMIT);
|
|
1247
|
+
const lines: string[] = [];
|
|
1248
|
+
let budget = PRE_EXISTING_CHANGE_PATH_LIMIT;
|
|
1249
|
+
for (const group of PRE_EXISTING_CHANGE_GROUPS) {
|
|
1250
|
+
const paths = grouped.get(group);
|
|
1251
|
+
if (paths === undefined || budget <= 0) continue;
|
|
1252
|
+
const printed = paths.slice().sort().slice(0, budget);
|
|
1253
|
+
budget -= printed.length;
|
|
1254
|
+
lines.push(`> - ${group}: ${printed.join(', ')}`);
|
|
1255
|
+
}
|
|
1256
|
+
if (omitted > 0 && lines.length > 0) {
|
|
1257
|
+
lines[lines.length - 1] += `, … and ${omitted} more`;
|
|
1258
|
+
}
|
|
1259
|
+
return [
|
|
1260
|
+
'> Uncommitted changes present before this call, belonging to the Boss:',
|
|
1261
|
+
...lines,
|
|
1262
|
+
"> 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.",
|
|
1263
|
+
].join('\n');
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* The block for one governed call, or `undefined` where the call is not
|
|
1268
|
+
* effect-authorized — a call whose declared outcomes carry no
|
|
1269
|
+
* `one-descendant-commit` disposition may commit nothing, so it is told
|
|
1270
|
+
* nothing.
|
|
1271
|
+
*/
|
|
1272
|
+
function effectAuthorizedPreExistingBlock(
|
|
1273
|
+
effectBoundary: Pick<XStateEffectBoundarySeed, 'dispositions'>,
|
|
1274
|
+
baseline: PlaybookRepositoryReceipt['baseline'] | undefined,
|
|
1275
|
+
): string | undefined {
|
|
1276
|
+
return effectBoundary.dispositions.includes('one-descendant-commit')
|
|
1277
|
+
? preExistingChangesBlock(baseline)
|
|
1278
|
+
: undefined;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1093
1281
|
const PLACEHOLDER_PATTERN = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g;
|
|
1094
1282
|
|
|
1095
1283
|
function placeholderFieldName(
|
|
@@ -1692,9 +1880,10 @@ export function createPlayerBridge(
|
|
|
1692
1880
|
);
|
|
1693
1881
|
}
|
|
1694
1882
|
if (result.status !== 'ok') {
|
|
1695
|
-
|
|
1883
|
+
const failure = new Error(
|
|
1696
1884
|
result.error ?? `captainBridge: callPlayer status "${result.status}"`,
|
|
1697
1885
|
);
|
|
1886
|
+
throw boundary?.markPlayerResultFailure?.(failure) ?? failure;
|
|
1698
1887
|
}
|
|
1699
1888
|
const finalText = result.finalText ?? '';
|
|
1700
1889
|
if (isEmptyFinalText(finalText)) {
|
|
@@ -3371,6 +3560,8 @@ export function createXStatePlaybookRuntime<
|
|
|
3371
3560
|
| {
|
|
3372
3561
|
readonly operationId: string;
|
|
3373
3562
|
readonly effectBoundary: XStateEffectBoundarySeed;
|
|
3563
|
+
/** The continuation's own baseline observation (DR-062 §4). */
|
|
3564
|
+
baseline?: PlaybookRepositoryReceipt['baseline'];
|
|
3374
3565
|
playerContinuation?: string | false;
|
|
3375
3566
|
input?: PlaybookPlayerInput;
|
|
3376
3567
|
roleId?: string;
|
|
@@ -3386,6 +3577,15 @@ export function createXStatePlaybookRuntime<
|
|
|
3386
3577
|
let deferInspectionEmissions = false;
|
|
3387
3578
|
let deferredInspectionEmissions: Array<() => void> = [];
|
|
3388
3579
|
let controlPlaneError: unknown;
|
|
3580
|
+
// DR-063 §2: the cause decided at the site the failure was decided, kept
|
|
3581
|
+
// for the failures whose Error the compiled artifact builds itself and the
|
|
3582
|
+
// runtime therefore cannot decorate. It is cleared at each Boss turn start
|
|
3583
|
+
// and at each successful player result, so it is never stale, and it is
|
|
3584
|
+
// only ever read as the cause of an error that carries none of its own.
|
|
3585
|
+
let lastFailureCause: PlaybookFailureCause | undefined;
|
|
3586
|
+
const rememberFailureCause = (cause: PlaybookFailureCause): void => {
|
|
3587
|
+
lastFailureCause = cause;
|
|
3588
|
+
};
|
|
3389
3589
|
// Previous root-machine state for the inspect-driven telemetry /
|
|
3390
3590
|
// status emitter. undefined before the first inspect firing.
|
|
3391
3591
|
let priorState: PlaybookState | undefined;
|
|
@@ -3881,6 +4081,19 @@ export function createXStatePlaybookRuntime<
|
|
|
3881
4081
|
refreshRetainedEffectReconciliation(current);
|
|
3882
4082
|
syncDeferredReconciliationOverlay();
|
|
3883
4083
|
refreshUnresolvedSemanticReconciliation(current);
|
|
4084
|
+
return projectUnresolvedEffectEnvelopes(current);
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
/**
|
|
4088
|
+
* The unresolved envelopes of one already-refreshed ledger mirror. Pure:
|
|
4089
|
+
* `describe()` reads it through the control view without moving anything.
|
|
4090
|
+
*/
|
|
4091
|
+
function projectUnresolvedEffectEnvelopes(
|
|
4092
|
+
current: PlaybookEffectLedger,
|
|
4093
|
+
): readonly (
|
|
4094
|
+
| { readonly kind: 'boundary'; readonly boundaryId: string }
|
|
4095
|
+
| { readonly kind: 'logical-operation'; readonly operationId: string }
|
|
4096
|
+
)[] {
|
|
3884
4097
|
if (!hasUnresolvedReconciliation()) return [];
|
|
3885
4098
|
|
|
3886
4099
|
const boundaryIds = new Set(unresolvedSemanticBoundaryIds);
|
|
@@ -3987,6 +4200,58 @@ export function createXStatePlaybookRuntime<
|
|
|
3987
4200
|
}
|
|
3988
4201
|
}
|
|
3989
4202
|
|
|
4203
|
+
function pendingHasExactUnchangedOrigin(
|
|
4204
|
+
pending: PlaybookPendingBossQuestionContext,
|
|
4205
|
+
): boolean {
|
|
4206
|
+
// The outcome menu is not the selected outcome: a source-authored
|
|
4207
|
+
// unchanged question may share its state with needsBossReply: deferred.
|
|
4208
|
+
// An open operation nevertheless always retains its checkpoint and
|
|
4209
|
+
// cumulative receipt authority, even if a later boundary looks alike.
|
|
4210
|
+
if (
|
|
4211
|
+
runtimeLogicalOperations().some(
|
|
4212
|
+
(operation) => operation.logicalReceipt === undefined,
|
|
4213
|
+
)
|
|
4214
|
+
) {
|
|
4215
|
+
return false;
|
|
4216
|
+
}
|
|
4217
|
+
const latest = effectLedgerMirror.boundaries
|
|
4218
|
+
.filter(runtimeBoundaryIsOwned)
|
|
4219
|
+
.at(-1);
|
|
4220
|
+
if (
|
|
4221
|
+
latest === undefined ||
|
|
4222
|
+
latest.sourceStateId !== pending.resumeStateId ||
|
|
4223
|
+
pending.asker.kind !== 'role' ||
|
|
4224
|
+
latest.roleId !== pending.asker.roleId ||
|
|
4225
|
+
latest.logicalOperationId !== undefined ||
|
|
4226
|
+
latest.physicalReceipt?.classification !== 'unchanged'
|
|
4227
|
+
) {
|
|
4228
|
+
return false;
|
|
4229
|
+
}
|
|
4230
|
+
// Reuse the persisted authority/receipt check rather than infer an arm
|
|
4231
|
+
// from question prose or search older evidence for a convenient match.
|
|
4232
|
+
const persisted = persistedBoundaryReconciliation(
|
|
4233
|
+
latest,
|
|
4234
|
+
effectLedgerMirror,
|
|
4235
|
+
);
|
|
4236
|
+
if (
|
|
4237
|
+
persisted === undefined ||
|
|
4238
|
+
persisted.historicalDeferred ||
|
|
4239
|
+
persisted.reconciliation.status !== 'resolved'
|
|
4240
|
+
) {
|
|
4241
|
+
return false;
|
|
4242
|
+
}
|
|
4243
|
+
const output = persisted.reconciliation.output;
|
|
4244
|
+
const declaration =
|
|
4245
|
+
outcomeAuthority.governedPlayerStates[latest.sourceStateId]?.[
|
|
4246
|
+
output.guard
|
|
4247
|
+
];
|
|
4248
|
+
return (
|
|
4249
|
+
declaration?.repositoryDisposition === 'unchanged' &&
|
|
4250
|
+
typeof output.question === 'string' &&
|
|
4251
|
+
output.question === pending.question
|
|
4252
|
+
);
|
|
4253
|
+
}
|
|
4254
|
+
|
|
3990
4255
|
function currentBoundDeferredOperation(
|
|
3991
4256
|
pending: PlaybookPendingBossQuestionContext,
|
|
3992
4257
|
): PlaybookEffectLogicalOperation | undefined {
|
|
@@ -4600,13 +4865,47 @@ export function createXStatePlaybookRuntime<
|
|
|
4600
4865
|
});
|
|
4601
4866
|
}
|
|
4602
4867
|
|
|
4868
|
+
// DR-063 §1: the cause of an unresolved governed settlement. A reconciled
|
|
4869
|
+
// mismatch supplies its own receipt-read cause; the reasons this function
|
|
4870
|
+
// owns map to adjudication, abort, and runtime-defect codes.
|
|
4871
|
+
function governedSettlementCause(
|
|
4872
|
+
reason: string,
|
|
4873
|
+
error: unknown,
|
|
4874
|
+
aborted: boolean,
|
|
4875
|
+
supplied: PlaybookFailureCause | undefined,
|
|
4876
|
+
): PlaybookFailureCause {
|
|
4877
|
+
if (supplied !== undefined) return supplied;
|
|
4878
|
+
if (aborted || reason.includes('aborted')) return ABORTED_FAILURE_CAUSE;
|
|
4879
|
+
if (GOVERNED_JUDGE_FAILURE_REASONS.includes(reason)) {
|
|
4880
|
+
const transport = failureErrorEvidence(error);
|
|
4881
|
+
return assertPlaybookFailureCause({
|
|
4882
|
+
code: 'judge-failed',
|
|
4883
|
+
evidence: {
|
|
4884
|
+
reason,
|
|
4885
|
+
...(transport === undefined ? {} : { error: transport }),
|
|
4886
|
+
},
|
|
4887
|
+
});
|
|
4888
|
+
}
|
|
4889
|
+
return runtimeDefectCause(reason);
|
|
4890
|
+
}
|
|
4891
|
+
|
|
4603
4892
|
function unresolvedGovernedSettlement(
|
|
4604
4893
|
reason: string,
|
|
4605
4894
|
error?: unknown,
|
|
4606
4895
|
signal: AbortSignal | undefined = activeSignal,
|
|
4896
|
+
cause?: PlaybookFailureCause,
|
|
4607
4897
|
): GovernedPlayerSettlement {
|
|
4608
|
-
|
|
4609
|
-
|
|
4898
|
+
const aborted =
|
|
4899
|
+
error !== undefined && signal?.aborted === true
|
|
4900
|
+
? Object.is(error, signal.reason)
|
|
4901
|
+
: false;
|
|
4902
|
+
const decided = governedSettlementCause(reason, error, aborted, cause);
|
|
4903
|
+
rememberFailureCause(decided);
|
|
4904
|
+
if (aborted) {
|
|
4905
|
+
return {
|
|
4906
|
+
status: 'unresolved',
|
|
4907
|
+
error: attachPlaybookFailureCause(error, decided),
|
|
4908
|
+
};
|
|
4610
4909
|
}
|
|
4611
4910
|
const failure =
|
|
4612
4911
|
error instanceof Error
|
|
@@ -4614,7 +4913,10 @@ export function createXStatePlaybookRuntime<
|
|
|
4614
4913
|
: new Error(`${label} governed outcome remains unresolved: ${reason}`);
|
|
4615
4914
|
return {
|
|
4616
4915
|
status: 'unresolved',
|
|
4617
|
-
error:
|
|
4916
|
+
error: attachPlaybookFailureCause(
|
|
4917
|
+
markFsmResultFailure(failure),
|
|
4918
|
+
decided,
|
|
4919
|
+
),
|
|
4618
4920
|
};
|
|
4619
4921
|
}
|
|
4620
4922
|
|
|
@@ -4862,7 +5164,12 @@ export function createXStatePlaybookRuntime<
|
|
|
4862
5164
|
if (reconciliation.status === 'unresolved') {
|
|
4863
5165
|
governedSettlementsByBoundaryId.set(
|
|
4864
5166
|
completion.boundary.boundaryId,
|
|
4865
|
-
unresolvedGovernedSettlement(
|
|
5167
|
+
unresolvedGovernedSettlement(
|
|
5168
|
+
reconciliation.reason,
|
|
5169
|
+
undefined,
|
|
5170
|
+
undefined,
|
|
5171
|
+
reconciliation.cause,
|
|
5172
|
+
),
|
|
4866
5173
|
);
|
|
4867
5174
|
return { finalText, semanticCandidate, unresolved: true };
|
|
4868
5175
|
}
|
|
@@ -5410,9 +5717,19 @@ export function createXStatePlaybookRuntime<
|
|
|
5410
5717
|
try {
|
|
5411
5718
|
const runTracedPlayerCall = async (
|
|
5412
5719
|
resume: string | false = selectedResume,
|
|
5720
|
+
// DR-062 §4: composed from this call's own baseline observation,
|
|
5721
|
+
// so the prompt the trace records is the prompt the player is sent.
|
|
5722
|
+
preExistingBlock?: string,
|
|
5413
5723
|
): Promise<PlayerResult> => {
|
|
5414
5724
|
const identity = callIdentity(resume);
|
|
5415
|
-
const
|
|
5725
|
+
const withPreExisting = (text: string): string =>
|
|
5726
|
+
preExistingBlock === undefined
|
|
5727
|
+
? text
|
|
5728
|
+
: `${text}\n\n${preExistingBlock}`;
|
|
5729
|
+
const callPrompt = withPreExisting(
|
|
5730
|
+
resume === false ? freshPrompt : prompt,
|
|
5731
|
+
);
|
|
5732
|
+
const callFreshPrompt = withPreExisting(freshPrompt);
|
|
5416
5733
|
await emitCallStarted(
|
|
5417
5734
|
'player.call.started',
|
|
5418
5735
|
'player.call.finished',
|
|
@@ -5432,7 +5749,12 @@ export function createXStatePlaybookRuntime<
|
|
|
5432
5749
|
roleId,
|
|
5433
5750
|
callPrompt,
|
|
5434
5751
|
signal,
|
|
5435
|
-
{
|
|
5752
|
+
{
|
|
5753
|
+
resume,
|
|
5754
|
+
...(callPrompt === callFreshPrompt
|
|
5755
|
+
? {}
|
|
5756
|
+
: { freshPrompt: callFreshPrompt }),
|
|
5757
|
+
},
|
|
5436
5758
|
);
|
|
5437
5759
|
// A host promise is not required to honor cancellation. Do not
|
|
5438
5760
|
// let a late result mutate continuity or publish a successful
|
|
@@ -5455,7 +5777,7 @@ export function createXStatePlaybookRuntime<
|
|
|
5455
5777
|
}
|
|
5456
5778
|
// A thrown port call carries no authoritative result, so the
|
|
5457
5779
|
// prior token remains available for a later explicit resume.
|
|
5458
|
-
throw error;
|
|
5780
|
+
throw decidePlayerCallFailure(error, roleId, playerId, signal);
|
|
5459
5781
|
}
|
|
5460
5782
|
|
|
5461
5783
|
let result: PlayerResult;
|
|
@@ -5472,7 +5794,7 @@ export function createXStatePlaybookRuntime<
|
|
|
5472
5794
|
} catch {
|
|
5473
5795
|
// The malformed host result remains authoritative.
|
|
5474
5796
|
}
|
|
5475
|
-
throw error;
|
|
5797
|
+
throw decidePlayerCallFailure(error, roleId, playerId, signal);
|
|
5476
5798
|
}
|
|
5477
5799
|
|
|
5478
5800
|
try {
|
|
@@ -5508,6 +5830,25 @@ export function createXStatePlaybookRuntime<
|
|
|
5508
5830
|
},
|
|
5509
5831
|
position,
|
|
5510
5832
|
);
|
|
5833
|
+
// DR-063 §2: the player bridge builds the failure Error for a
|
|
5834
|
+
// non-`ok` result, so the cause is decided here, where the role,
|
|
5835
|
+
// the player, and the reported error are known, and a successful
|
|
5836
|
+
// result clears it so no later failure inherits it.
|
|
5837
|
+
if (result.status === 'ok') {
|
|
5838
|
+
lastFailureCause = undefined;
|
|
5839
|
+
} else if (result.status === 'aborted') {
|
|
5840
|
+
rememberFailureCause(ABORTED_FAILURE_CAUSE);
|
|
5841
|
+
} else {
|
|
5842
|
+
rememberFailureCause(
|
|
5843
|
+
playerFailureCause({
|
|
5844
|
+
roleId,
|
|
5845
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
5846
|
+
error:
|
|
5847
|
+
result.error ??
|
|
5848
|
+
`callPlayer status ${JSON.stringify(result.status)}`,
|
|
5849
|
+
}),
|
|
5850
|
+
);
|
|
5851
|
+
}
|
|
5511
5852
|
return result;
|
|
5512
5853
|
};
|
|
5513
5854
|
|
|
@@ -5548,6 +5889,10 @@ export function createXStatePlaybookRuntime<
|
|
|
5548
5889
|
try {
|
|
5549
5890
|
deferredContinuation.result = await runTracedPlayerCall(
|
|
5550
5891
|
selectedResume,
|
|
5892
|
+
effectAuthorizedPreExistingBlock(
|
|
5893
|
+
effectBoundary,
|
|
5894
|
+
deferredContinuation.baseline,
|
|
5895
|
+
),
|
|
5551
5896
|
);
|
|
5552
5897
|
} catch (error) {
|
|
5553
5898
|
deferredContinuation.callError = error;
|
|
@@ -5569,7 +5914,18 @@ export function createXStatePlaybookRuntime<
|
|
|
5569
5914
|
const exclusive = await repositoryCapability.runExclusive({
|
|
5570
5915
|
signal,
|
|
5571
5916
|
effectBoundary,
|
|
5572
|
-
|
|
5917
|
+
// DR-062 §4: the call's own baseline, supplied by the host that
|
|
5918
|
+
// captured it under this exclusive claim. A host that supplies
|
|
5919
|
+
// none states no pre-existing change, exactly as an empty
|
|
5920
|
+
// baseline projection does.
|
|
5921
|
+
operation: (context) =>
|
|
5922
|
+
runTracedPlayerCall(
|
|
5923
|
+
selectedResume,
|
|
5924
|
+
effectAuthorizedPreExistingBlock(
|
|
5925
|
+
effectBoundary,
|
|
5926
|
+
context?.baseline,
|
|
5927
|
+
),
|
|
5928
|
+
),
|
|
5573
5929
|
completeEffectBoundary: completionEvidenceFor(
|
|
5574
5930
|
input,
|
|
5575
5931
|
roleId,
|
|
@@ -5600,6 +5956,12 @@ export function createXStatePlaybookRuntime<
|
|
|
5600
5956
|
}
|
|
5601
5957
|
},
|
|
5602
5958
|
|
|
5959
|
+
markPlayerResultFailure(error): Error {
|
|
5960
|
+
return lastFailureCause === undefined
|
|
5961
|
+
? error
|
|
5962
|
+
: attachPlaybookFailureCause(error, lastFailureCause);
|
|
5963
|
+
},
|
|
5964
|
+
|
|
5603
5965
|
takeGovernedPlayerOutput(result): GovernedPlayerSettlement | undefined {
|
|
5604
5966
|
const settlement = governedPlayerSettlements.get(result);
|
|
5605
5967
|
if (settlement !== undefined) governedPlayerSettlements.delete(result);
|
|
@@ -6240,6 +6602,80 @@ export function createXStatePlaybookRuntime<
|
|
|
6240
6602
|
return activeTurnId === undefined ? {} : { turnId: activeTurnId };
|
|
6241
6603
|
}
|
|
6242
6604
|
|
|
6605
|
+
// DR-063 §2: the parked failure's error as every public surface publishes
|
|
6606
|
+
// it — the FSM's own `lastError`, completed with the cause decided where
|
|
6607
|
+
// the failure was decided. An error that already carries a valid cause
|
|
6608
|
+
// keeps it; an artifact-built error takes the runtime's own slot; anything
|
|
6609
|
+
// else is a runtime defect carrying its message. There is no failure
|
|
6610
|
+
// without a cause.
|
|
6611
|
+
function failedStateError(
|
|
6612
|
+
context: Record<string, unknown>,
|
|
6613
|
+
): NormalizedError | undefined {
|
|
6614
|
+
const normalized: NormalizedError | undefined = normalizeErrorFull(
|
|
6615
|
+
context.lastError,
|
|
6616
|
+
);
|
|
6617
|
+
if (normalized === undefined || normalized.cause !== undefined) {
|
|
6618
|
+
return normalized;
|
|
6619
|
+
}
|
|
6620
|
+
return {
|
|
6621
|
+
...normalized,
|
|
6622
|
+
cause: lastFailureCause ?? runtimeDefectCause(normalized.message),
|
|
6623
|
+
};
|
|
6624
|
+
}
|
|
6625
|
+
|
|
6626
|
+
// DR-063 §2: a rejected player port or a result the runtime cannot read is
|
|
6627
|
+
// a `player-failed` failure named by role; an abort is `aborted`.
|
|
6628
|
+
function decidePlayerCallFailure(
|
|
6629
|
+
error: unknown,
|
|
6630
|
+
roleId: string,
|
|
6631
|
+
playerId: string | undefined,
|
|
6632
|
+
signal: AbortSignal,
|
|
6633
|
+
): unknown {
|
|
6634
|
+
const cause = isAbortFailure(error, signal)
|
|
6635
|
+
? ABORTED_FAILURE_CAUSE
|
|
6636
|
+
: playerFailureCause({
|
|
6637
|
+
roleId,
|
|
6638
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
6639
|
+
error,
|
|
6640
|
+
});
|
|
6641
|
+
rememberFailureCause(cause);
|
|
6642
|
+
return attachPlaybookFailureCause(error, cause);
|
|
6643
|
+
}
|
|
6644
|
+
|
|
6645
|
+
function completeFailedStatuses(
|
|
6646
|
+
statuses: readonly ScheduledStatus[],
|
|
6647
|
+
state: PlaybookState,
|
|
6648
|
+
context: Record<string, unknown>,
|
|
6649
|
+
): readonly ScheduledStatus[] {
|
|
6650
|
+
if (state.stateId !== 'failed') return statuses;
|
|
6651
|
+
const lastError = failedStateError(context);
|
|
6652
|
+
if (lastError?.cause === undefined) return statuses;
|
|
6653
|
+
const cause = lastError.cause as unknown as JsonValue;
|
|
6654
|
+
return statuses.map((status) => {
|
|
6655
|
+
const data = status.data;
|
|
6656
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
6657
|
+
return status;
|
|
6658
|
+
}
|
|
6659
|
+
const recorded = data as { readonly [key: string]: JsonValue };
|
|
6660
|
+
const recordedError = recorded.lastError;
|
|
6661
|
+
if (
|
|
6662
|
+
recordedError === undefined ||
|
|
6663
|
+
recordedError === null ||
|
|
6664
|
+
typeof recordedError !== 'object' ||
|
|
6665
|
+
Array.isArray(recordedError)
|
|
6666
|
+
) {
|
|
6667
|
+
return status;
|
|
6668
|
+
}
|
|
6669
|
+
return {
|
|
6670
|
+
...status,
|
|
6671
|
+
data: snapshotJsonValue(
|
|
6672
|
+
{ ...recorded, lastError: { ...recordedError, cause } },
|
|
6673
|
+
'failed status data',
|
|
6674
|
+
),
|
|
6675
|
+
};
|
|
6676
|
+
});
|
|
6677
|
+
}
|
|
6678
|
+
|
|
6243
6679
|
function structuredStateTelemetryPayload(
|
|
6244
6680
|
previousState: PlaybookState | undefined,
|
|
6245
6681
|
state: PlaybookState,
|
|
@@ -6261,7 +6697,7 @@ export function createXStatePlaybookRuntime<
|
|
|
6261
6697
|
payload.pendingBossQuestion = pendingBossQuestion;
|
|
6262
6698
|
}
|
|
6263
6699
|
if (state.stateId === 'failed') {
|
|
6264
|
-
const lastError =
|
|
6700
|
+
const lastError = failedStateError(context);
|
|
6265
6701
|
if (lastError !== undefined) payload.lastError = lastError;
|
|
6266
6702
|
}
|
|
6267
6703
|
return snapshotJsonValue(payload, 'FSM telemetry payload');
|
|
@@ -6466,10 +6902,10 @@ export function createXStatePlaybookRuntime<
|
|
|
6466
6902
|
inspectionEvent.event,
|
|
6467
6903
|
context,
|
|
6468
6904
|
);
|
|
6469
|
-
const stateStatuses =
|
|
6905
|
+
const stateStatuses = completeFailedStatuses(
|
|
6906
|
+
statusesForState(state, context, inspectionEvent.event),
|
|
6470
6907
|
state,
|
|
6471
6908
|
context,
|
|
6472
|
-
inspectionEvent.event,
|
|
6473
6909
|
);
|
|
6474
6910
|
const outcomeStatuses = usesDefaultStatuses
|
|
6475
6911
|
? acceptedOutcomes.map(({ acceptedOutcome }) => ({
|
|
@@ -6569,10 +7005,17 @@ export function createXStatePlaybookRuntime<
|
|
|
6569
7005
|
: outcome === 'aborted'
|
|
6570
7006
|
? activeSignal?.reason
|
|
6571
7007
|
: undefined);
|
|
7008
|
+
if (failure === undefined) return { outcome, state };
|
|
7009
|
+
// DR-063 §2: a `failed` settlement publishes the same cause-completed
|
|
7010
|
+
// error the failed state's status line and telemetry publish.
|
|
7011
|
+
const normalized =
|
|
7012
|
+
outcome === 'failed'
|
|
7013
|
+
? failedStateError({ lastError: failure })
|
|
7014
|
+
: normalizeError(failure);
|
|
6572
7015
|
return {
|
|
6573
7016
|
outcome,
|
|
6574
7017
|
state,
|
|
6575
|
-
...(
|
|
7018
|
+
...(normalized === undefined ? {} : { error: normalized }),
|
|
6576
7019
|
};
|
|
6577
7020
|
}
|
|
6578
7021
|
|
|
@@ -6801,11 +7244,46 @@ export function createXStatePlaybookRuntime<
|
|
|
6801
7244
|
action: {
|
|
6802
7245
|
id: `retry:${retryEvent.type}`,
|
|
6803
7246
|
label: `Retry: ${description}`,
|
|
7247
|
+
// DR-063 §3: retrying a failed call is `ready` — the runtime cannot
|
|
7248
|
+
// see whether the Boss changed the outside world since it failed.
|
|
7249
|
+
standing: 'ready',
|
|
6804
7250
|
},
|
|
6805
7251
|
event: retryEvent,
|
|
6806
7252
|
};
|
|
6807
7253
|
}
|
|
6808
7254
|
|
|
7255
|
+
/**
|
|
7256
|
+
* DR-063 §3: reconciliation only re-reads the host's ledger, which the
|
|
7257
|
+
* control view has just done. Where no checkpoint restoration is eligible
|
|
7258
|
+
* and every unresolved envelope's boundary already holds a complete
|
|
7259
|
+
* receipt, re-reading can resolve nothing, so the action is a no-op.
|
|
7260
|
+
*/
|
|
7261
|
+
function reconciliationStanding(
|
|
7262
|
+
deferredRestoreOperationId: string | undefined,
|
|
7263
|
+
): Pick<PlaybookControlAction, 'standing' | 'reason'> {
|
|
7264
|
+
if (deferredRestoreOperationId !== undefined) return { standing: 'ready' };
|
|
7265
|
+
const current = effectLedgerMirror;
|
|
7266
|
+
const envelopes = projectUnresolvedEffectEnvelopes(current);
|
|
7267
|
+
if (envelopes.length === 0) return { standing: 'ready' };
|
|
7268
|
+
const boundaryIds = envelopes.flatMap((envelope) =>
|
|
7269
|
+
envelope.kind === 'boundary'
|
|
7270
|
+
? [envelope.boundaryId]
|
|
7271
|
+
: (current.logicalOperations.find(
|
|
7272
|
+
({ operationId }) => operationId === envelope.operationId,
|
|
7273
|
+
)?.boundaryIds ?? []),
|
|
7274
|
+
);
|
|
7275
|
+
if (boundaryIds.length === 0) return { standing: 'ready' };
|
|
7276
|
+
const complete = boundaryIds.every(
|
|
7277
|
+
(boundaryId) =>
|
|
7278
|
+
current.boundaries.find(
|
|
7279
|
+
(candidate) => candidate.boundaryId === boundaryId,
|
|
7280
|
+
)?.physicalReceipt !== undefined,
|
|
7281
|
+
);
|
|
7282
|
+
return complete
|
|
7283
|
+
? { standing: 'no-op', reason: 'receipt-complete' }
|
|
7284
|
+
: { standing: 'ready' };
|
|
7285
|
+
}
|
|
7286
|
+
|
|
6809
7287
|
function deriveControlActions(snapshot: unknown): DerivedControlAction[] {
|
|
6810
7288
|
// Actions derive only at the safe point the parked snapshot also
|
|
6811
7289
|
// uses — quiescent actor with status `active` and no pending nested
|
|
@@ -6844,6 +7322,7 @@ export function createXStatePlaybookRuntime<
|
|
|
6844
7322
|
action: {
|
|
6845
7323
|
id: UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID,
|
|
6846
7324
|
label: 'Retry unresolved effect reconciliation',
|
|
7325
|
+
...reconciliationStanding(deferredRestoreOperationId),
|
|
6847
7326
|
},
|
|
6848
7327
|
unresolvedEffectAction: 'reconcile',
|
|
6849
7328
|
...(deferredRestoreOperationId === undefined
|
|
@@ -6854,6 +7333,8 @@ export function createXStatePlaybookRuntime<
|
|
|
6854
7333
|
action: {
|
|
6855
7334
|
id: UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID,
|
|
6856
7335
|
label: 'Abandon unresolved workflow attempt',
|
|
7336
|
+
// DR-063 §3: abandonment always changes the session's standing.
|
|
7337
|
+
standing: 'ready',
|
|
6857
7338
|
},
|
|
6858
7339
|
unresolvedEffectAction: 'abandon',
|
|
6859
7340
|
},
|
|
@@ -6878,6 +7359,8 @@ export function createXStatePlaybookRuntime<
|
|
|
6878
7359
|
action: {
|
|
6879
7360
|
id: `jump:${targetId}`,
|
|
6880
7361
|
label: `Resume from: ${description}`,
|
|
7362
|
+
// DR-063 §3: a jump always moves the machine.
|
|
7363
|
+
standing: 'ready',
|
|
6881
7364
|
},
|
|
6882
7365
|
event,
|
|
6883
7366
|
});
|
|
@@ -7369,8 +7852,12 @@ export function createXStatePlaybookRuntime<
|
|
|
7369
7852
|
signal,
|
|
7370
7853
|
operationId: operation.operationId,
|
|
7371
7854
|
effectBoundary,
|
|
7372
|
-
operation: async (
|
|
7855
|
+
operation: async (context) => {
|
|
7856
|
+
const { playerContinuation } = context ?? {};
|
|
7373
7857
|
validateBinding(playerContinuation);
|
|
7858
|
+
// DR-062 §4: the continuation's own baseline, captured before the
|
|
7859
|
+
// FSM re-enters the bound player state that reads it.
|
|
7860
|
+
continuation.baseline = context?.baseline;
|
|
7374
7861
|
continuation.playerContinuation = selectPlayerResume(
|
|
7375
7862
|
effectBoundary.roleId, boundPlayerId,
|
|
7376
7863
|
);
|
|
@@ -7874,7 +8361,10 @@ export function createXStatePlaybookRuntime<
|
|
|
7874
8361
|
!hasUnresolvedReconciliation()
|
|
7875
8362
|
? pendingBossQuestionForState(state, context)
|
|
7876
8363
|
: undefined;
|
|
7877
|
-
const lastError =
|
|
8364
|
+
const lastError =
|
|
8365
|
+
state.stateId === 'failed'
|
|
8366
|
+
? failedStateError(context)
|
|
8367
|
+
: normalizeErrorFull(context.lastError);
|
|
7878
8368
|
const projectedContext = projectControlContext(context);
|
|
7879
8369
|
const stateDescription =
|
|
7880
8370
|
!hasUnresolvedReconciliation()
|
|
@@ -8273,6 +8763,9 @@ export function createXStatePlaybookRuntime<
|
|
|
8273
8763
|
activeAborts = abortReasonClassifier(signal);
|
|
8274
8764
|
activeAbortEmission = undefined;
|
|
8275
8765
|
controlPlaneError = undefined;
|
|
8766
|
+
// DR-063 §2: each turn decides its own failures, so the slot never
|
|
8767
|
+
// carries a previous turn's cause into this one.
|
|
8768
|
+
lastFailureCause = undefined;
|
|
8276
8769
|
let result: PlaybookRunResult | undefined;
|
|
8277
8770
|
let operationError: unknown;
|
|
8278
8771
|
// The boundary sentinel releases on every exit: a settlement defect
|
|
@@ -8313,7 +8806,8 @@ export function createXStatePlaybookRuntime<
|
|
|
8313
8806
|
deferredPending !== undefined &&
|
|
8314
8807
|
outcomeAuthority?.governedPlayerStates[
|
|
8315
8808
|
deferredPending.resumeStateId
|
|
8316
|
-
]?.needsBossReply?.repositoryDisposition === 'deferred'
|
|
8809
|
+
]?.needsBossReply?.repositoryDisposition === 'deferred' &&
|
|
8810
|
+
!pendingHasExactUnchangedOrigin(deferredPending);
|
|
8317
8811
|
deferredOperation =
|
|
8318
8812
|
!pendingRequiresDeferredBinding
|
|
8319
8813
|
? undefined
|