@sublang/playbook 8.0.0 → 10.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 +3 -3
- package/docs/cli.md +66 -23
- package/docs/configuration.md +13 -8
- package/docs/embedding.md +45 -14
- package/package.json +7 -3
- package/reference/sdlc/captain.md +14 -10
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +33 -13
- package/reference/sdlc/captain.playbook/captain.fsm.js +80 -9
- package/reference/sdlc/captain.playbook/captain.fsm.ts +137 -18
- package/reference/sdlc/captain.playbook/captain.gears.md +10 -6
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +5 -1
- package/reference/sdlc/captain.playbook/captain.playbook.js +151 -10
- package/reference/sdlc/captain.playbook/captain.playbook.ts +200 -14
- package/reference/sdlc/code.md +0 -1
- package/reference/sdlc/code.playbook/bin/interactive-session.js +170 -17
- package/reference/sdlc/code.playbook/bin/launch-config.js +136 -4
- package/reference/sdlc/code.playbook/bin/playbook.js +81 -4
- package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
- package/reference/sdlc/code.playbook/bin/run.js +365 -63
- package/reference/sdlc/code.playbook/bin/session-store.js +2877 -209
- package/reference/sdlc/code.playbook/code.fsm.d.ts +11 -1
- package/reference/sdlc/code.playbook/code.fsm.js +85 -29
- package/reference/sdlc/code.playbook/code.fsm.ts +95 -33
- package/reference/sdlc/code.playbook/code.gears.md +0 -2
- package/reference/sdlc/code.playbook/code.playbook.d.ts +5 -2
- package/reference/sdlc/code.playbook/code.playbook.js +67 -4
- package/reference/sdlc/code.playbook/code.playbook.ts +87 -8
- package/reference/sdlc/code.playbook/code.registry.d.ts +10 -3
- package/reference/sdlc/code.playbook/code.registry.js +10 -3
- package/reference/sdlc/code.playbook/code.registry.ts +23 -5
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +99 -7
- package/reference/sdlc/code.playbook/playbook-captain.js +1894 -82
- package/reference/sdlc/code.playbook/playbook-captain.ts +2809 -109
- package/reference/sdlc/decide.md +0 -1
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +8 -1
- package/reference/sdlc/decide.playbook/decide.fsm.js +80 -29
- package/reference/sdlc/decide.playbook/decide.fsm.ts +89 -31
- package/reference/sdlc/decide.playbook/decide.gears.md +0 -1
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +15 -5
- package/reference/sdlc/decide.playbook/decide.playbook.js +1994 -191
- package/reference/sdlc/decide.playbook/decide.playbook.ts +3209 -404
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +7 -3
- package/reference/sdlc/decide.playbook/decide.registry.js +10 -3
- package/reference/sdlc/decide.playbook/decide.registry.ts +20 -5
- package/reference/sdlc/review.playbook/review.fsm.d.ts +7 -0
- package/reference/sdlc/review.playbook/review.fsm.js +133 -12
- package/reference/sdlc/review.playbook/review.fsm.ts +140 -12
- package/reference/sdlc/review.playbook/review.playbook.d.ts +5 -2
- package/reference/sdlc/review.playbook/review.playbook.js +78 -4
- package/reference/sdlc/review.playbook/review.playbook.ts +95 -8
- package/reference/sdlc/review.playbook/review.registry.d.ts +10 -3
- package/reference/sdlc/review.playbook/review.registry.js +10 -3
- package/reference/sdlc/review.playbook/review.registry.ts +23 -5
- package/slc/gears2fsm.md +25 -7
- package/slc/link.md +727 -82
- package/src/accepted-outcome.d.ts +18 -0
- package/src/accepted-outcome.js +94 -0
- package/src/accepted-outcome.ts +140 -0
- package/src/runtime.d.ts +165 -3
- package/src/runtime.ts +214 -2
- package/src/xstate-playbook-runtime.d.ts +162 -13
- package/src/xstate-playbook-runtime.js +3344 -564
- package/src/xstate-playbook-runtime.ts +4873 -637
- package/src/xstate-runtime.d.ts +76 -8
- package/src/xstate-runtime.js +1001 -64
- package/src/xstate-runtime.ts +1640 -91
|
@@ -4,8 +4,30 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { isDeepStrictEqual } from 'node:util';
|
|
5
5
|
import PQueue from 'p-queue';
|
|
6
6
|
import { isAgentCallSettingsError, } from '@sublang/cligent/tmux-play';
|
|
7
|
-
import { assertPlaybookRuntimeSnapshot, hiddenControlEnvelope, registerPlaybookAbortCleanup, snapshotJsonValue, validatePlayerResult, } from '../../../src/xstate-runtime.js';
|
|
7
|
+
import { assertPlaybookRuntimeSnapshot, assertPlaybookEffectLedger, emptyPlaybookEffectLedger, hiddenControlEnvelope, isPlaybookEffectLedgerMonotonicExtension, registerPlaybookAbortCleanup, snapshotJsonValue, validatePlayerResult, } from '../../../src/xstate-runtime.js';
|
|
8
8
|
import createDefaultCaptainRuntime from '../captain.playbook/captain.playbook.js';
|
|
9
|
+
function retainedEffectLedgerCanRebase(checkpoint, current) {
|
|
10
|
+
if (checkpoint.boundaries.some(({ physicalReceipt }) => physicalReceipt === undefined)) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (!isPlaybookEffectLedgerMonotonicExtension(checkpoint, current)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (!isDeepStrictEqual(current.boundaries.slice(0, checkpoint.boundaries.length), checkpoint.boundaries) ||
|
|
17
|
+
!isDeepStrictEqual(current.logicalOperations, checkpoint.logicalOperations)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return current.boundaries
|
|
21
|
+
.slice(checkpoint.boundaries.length)
|
|
22
|
+
.every(({ physicalReceipt }) => physicalReceipt?.classification === 'unchanged');
|
|
23
|
+
}
|
|
24
|
+
function createRuntimeForEnablement(enablement, hostCapabilitiesById) {
|
|
25
|
+
const hostCapabilities = hostCapabilitiesById.get(enablement.entry.id);
|
|
26
|
+
if (hostCapabilities === undefined) {
|
|
27
|
+
throw new Error(`/${enablement.command} schema-3 runtime requires current-host construction capabilities`);
|
|
28
|
+
}
|
|
29
|
+
return enablement.entry.createRuntime(enablement.options, hostCapabilities);
|
|
30
|
+
}
|
|
9
31
|
class VisibilityControlError extends Error {
|
|
10
32
|
constructor(cause) {
|
|
11
33
|
super(`playbook visibility request failed: ${String(cause?.message ?? cause)}`, { cause });
|
|
@@ -39,6 +61,10 @@ const INTERNAL_CAPTAIN_ID = 'captain';
|
|
|
39
61
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
40
62
|
const PLAYER_ID_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)*$/;
|
|
41
63
|
const ROLE_ID_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
64
|
+
const HOST_CAPABILITIES_OPTION_KEY = 'hostCapabilities';
|
|
65
|
+
const UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID = 'reconcile:unresolved-effect';
|
|
66
|
+
const UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID = 'abandon:unresolved-effect';
|
|
67
|
+
const RESUMPTION_DUPLICATE_EFFECT_WARNING = 'Warning: resumption may duplicate external effects attempted after the retained boundary; verify the current world before continuing.';
|
|
42
68
|
function parseRegisteredCommand(prompt) {
|
|
43
69
|
const match = /^\/([A-Za-z][A-Za-z0-9_-]*)(?:\s+([\s\S]*))?$/.exec(prompt.trim());
|
|
44
70
|
if (!match)
|
|
@@ -126,6 +152,34 @@ function stateDigestLine(state, description) {
|
|
|
126
152
|
digestLine `status ${state.status}`,
|
|
127
153
|
].join('; ');
|
|
128
154
|
}
|
|
155
|
+
// CAPTAIN-5's mirrored ledger member holds the runtime snapshot's
|
|
156
|
+
// pending-question projection — entries of `{ questionId, asker, question,
|
|
157
|
+
// sourceItem }` — never a raw telemetry payload: the linked runtime's state
|
|
158
|
+
// telemetry carries the singular full-context question, whose extra
|
|
159
|
+
// runtime-internal fields (`resumeStateId`) fail the durable snapshot's
|
|
160
|
+
// leaf-projection equality (CAPTAIN-41) and with it headless settlement.
|
|
161
|
+
function mirroredBossQuestions(value) {
|
|
162
|
+
if (value === undefined || value === null)
|
|
163
|
+
return undefined;
|
|
164
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
165
|
+
return entries.map((entry) => {
|
|
166
|
+
if (typeof entry !== 'object' || entry === null)
|
|
167
|
+
return entry;
|
|
168
|
+
const record = entry;
|
|
169
|
+
const projected = {};
|
|
170
|
+
if (record.questionId !== undefined) {
|
|
171
|
+
projected.questionId = record.questionId;
|
|
172
|
+
}
|
|
173
|
+
if (record.asker !== undefined)
|
|
174
|
+
projected.asker = record.asker;
|
|
175
|
+
if (record.question !== undefined)
|
|
176
|
+
projected.question = record.question;
|
|
177
|
+
if (record.sourceItem !== undefined) {
|
|
178
|
+
projected.sourceItem = record.sourceItem;
|
|
179
|
+
}
|
|
180
|
+
return projected;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
129
183
|
function pendingQuestionLines(pending) {
|
|
130
184
|
const list = Array.isArray(pending)
|
|
131
185
|
? pending
|
|
@@ -377,11 +431,45 @@ function forwardedToolOptions(requested, captainAdapter) {
|
|
|
377
431
|
return { allowedTools: requested };
|
|
378
432
|
}
|
|
379
433
|
const hiddenJudgeEnvelope = hiddenControlEnvelope;
|
|
434
|
+
function unresolvedEffectReportLines(unresolvedEffects) {
|
|
435
|
+
return unresolvedEffects.map((effect, index) => {
|
|
436
|
+
const merelyPossible = effect.classification === 'observation-ambiguous' ||
|
|
437
|
+
effect.classification === 'incomplete';
|
|
438
|
+
return [
|
|
439
|
+
`${index + 1}. ${merelyPossible ? 'Possible repository effect; a change could not be excluded' : 'Observed repository change'} (${effect.classification})`,
|
|
440
|
+
`baseline HEAD ${effect.baselineHead}`,
|
|
441
|
+
effect.afterHead === undefined
|
|
442
|
+
? 'after HEAD was not available'
|
|
443
|
+
: `after HEAD ${effect.afterHead}`,
|
|
444
|
+
...(effect.commitOid === undefined
|
|
445
|
+
? []
|
|
446
|
+
: [`proven commit OID ${effect.commitOid}`]),
|
|
447
|
+
].join('; ') + '.';
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
function unresolvedEffectBossReport(unresolvedEffects) {
|
|
451
|
+
if (unresolvedEffects.length === 0)
|
|
452
|
+
return undefined;
|
|
453
|
+
return [
|
|
454
|
+
'Repository-effect evidence:',
|
|
455
|
+
...unresolvedEffectReportLines(unresolvedEffects).map((line) => `- ${line}`),
|
|
456
|
+
'This evidence does not establish workflow completion or attribute any repository change or commit to this workflow.',
|
|
457
|
+
].join('\n');
|
|
458
|
+
}
|
|
459
|
+
function appendMandatoryPresentationSuffix(turn, suffix) {
|
|
460
|
+
const current = turn.mandatoryPresentationSuffix;
|
|
461
|
+
if (current === undefined) {
|
|
462
|
+
turn.mandatoryPresentationSuffix = suffix;
|
|
463
|
+
}
|
|
464
|
+
else if (!current.includes(suffix)) {
|
|
465
|
+
turn.mandatoryPresentationSuffix = `${current}\n\n${suffix}`;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
380
468
|
// CAPTAIN-20: the result-phase block the shell supplies inside the closing
|
|
381
469
|
// reply call's envelope — the settlement's outcome-report facts verbatim, the
|
|
382
470
|
// exact counts, and the saved-counts line only when counted activity is
|
|
383
471
|
// nonzero.
|
|
384
|
-
function outcomeReportBlock(report) {
|
|
472
|
+
function outcomeReportBlock(report, unresolvedEffects) {
|
|
385
473
|
const lines = [
|
|
386
474
|
`Settlement status: ${report.status}`,
|
|
387
475
|
...(report.playbookId === undefined
|
|
@@ -405,6 +493,12 @@ function outcomeReportBlock(report) {
|
|
|
405
493
|
if (report.leafStateSummary !== undefined) {
|
|
406
494
|
lines.push(`Resulting leaf state: ${report.leafStateSummary}`);
|
|
407
495
|
}
|
|
496
|
+
const effectLines = unresolvedEffectReportLines(unresolvedEffects);
|
|
497
|
+
if (effectLines.length > 0) {
|
|
498
|
+
lines.push('Repository-effect evidence (canonical, in ledger order):');
|
|
499
|
+
lines.push(...effectLines.map((line) => `- ${line}`));
|
|
500
|
+
lines.push('Report this evidence without claiming workflow completion or attributing any repository change or commit to the workflow.');
|
|
501
|
+
}
|
|
408
502
|
lines.push(`Progress counts: ${report.progressPhrase}`);
|
|
409
503
|
lines.push(`Counts: ${JSON.stringify({
|
|
410
504
|
...report.counts,
|
|
@@ -439,10 +533,150 @@ function summaryProgressPhrase(stateCounts) {
|
|
|
439
533
|
function summaryProgressRoundCount(stateCounts) {
|
|
440
534
|
return [...stateCounts.values()].reduce((total, count) => total + count, 0);
|
|
441
535
|
}
|
|
442
|
-
function
|
|
443
|
-
|
|
536
|
+
function captureRegistryEntry(value) {
|
|
537
|
+
if (value === null || typeof value !== 'object')
|
|
538
|
+
return value;
|
|
539
|
+
const source = value;
|
|
540
|
+
return {
|
|
541
|
+
id: source.id,
|
|
542
|
+
command: source.command,
|
|
543
|
+
intent: source.intent,
|
|
544
|
+
artifactSchema: source.artifactSchema,
|
|
545
|
+
runtimeProfile: source.runtimeProfile,
|
|
546
|
+
requiredRoleIds: source.requiredRoleIds,
|
|
547
|
+
concurrentRoleSets: source.concurrentRoleSets,
|
|
548
|
+
summaryPolicy: source.summaryPolicy,
|
|
549
|
+
validateOptions: source.validateOptions,
|
|
550
|
+
createRuntime: source.createRuntime,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function exactOwnDataRecord(value, keys) {
|
|
554
|
+
if (value === null ||
|
|
555
|
+
typeof value !== 'object' ||
|
|
556
|
+
Array.isArray(value) ||
|
|
557
|
+
(Object.getPrototypeOf(value) !== Object.prototype &&
|
|
558
|
+
Object.getPrototypeOf(value) !== null)) {
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
562
|
+
if (Reflect.ownKeys(descriptors).length !== keys.length ||
|
|
563
|
+
keys.some((key) => !Object.hasOwn(descriptors, key) ||
|
|
564
|
+
!Object.hasOwn(descriptors[key], 'value') ||
|
|
565
|
+
descriptors[key].enumerable !== true)) {
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
568
|
+
return Object.fromEntries(keys.map((key) => [key, descriptors[key].value]));
|
|
569
|
+
}
|
|
570
|
+
function captureHostCapabilityRecord(value) {
|
|
571
|
+
if (value === undefined)
|
|
572
|
+
return Object.freeze({});
|
|
573
|
+
const captured = exactOwnDataRecord(value, Object.keys(value));
|
|
574
|
+
if (captured === undefined) {
|
|
575
|
+
throw new TypeError('current-host construction capabilities must be an exact data-property record');
|
|
576
|
+
}
|
|
577
|
+
return captured;
|
|
578
|
+
}
|
|
579
|
+
function validateHostCapabilities(value, entry, command) {
|
|
580
|
+
if (value === undefined) {
|
|
581
|
+
throw new Error(`/${command} schema-3 runtime requires current-host construction capabilities`);
|
|
582
|
+
}
|
|
583
|
+
const capability = exactOwnDataRecord(value, [
|
|
584
|
+
'authority',
|
|
585
|
+
'repository',
|
|
586
|
+
'effectLedger',
|
|
587
|
+
]);
|
|
588
|
+
const authority = exactOwnDataRecord(capability?.authority, [
|
|
589
|
+
'playbookId',
|
|
590
|
+
'artifactSchema',
|
|
591
|
+
'cwd',
|
|
592
|
+
'sessionId',
|
|
593
|
+
'leaseOwnerToken',
|
|
594
|
+
'canonicalWorktree',
|
|
595
|
+
'requiredRoleIds',
|
|
596
|
+
'concurrentRoleSets',
|
|
597
|
+
]);
|
|
598
|
+
const canonicalWorktree = exactOwnDataRecord(authority?.canonicalWorktree, ['worktree', 'gitDir']);
|
|
599
|
+
const repository = exactOwnDataRecord(capability?.repository, [
|
|
600
|
+
'identity',
|
|
601
|
+
'observe',
|
|
602
|
+
'acquire',
|
|
603
|
+
'runExclusive',
|
|
604
|
+
'runCohort',
|
|
605
|
+
'runDeferred',
|
|
606
|
+
]);
|
|
607
|
+
const identity = exactOwnDataRecord(repository?.identity, [
|
|
608
|
+
'worktree',
|
|
609
|
+
'gitDir',
|
|
610
|
+
]);
|
|
611
|
+
const effectLedger = exactOwnDataRecord(capability?.effectLedger, [
|
|
612
|
+
'snapshot',
|
|
613
|
+
'writeAhead',
|
|
614
|
+
]);
|
|
615
|
+
if (authority?.playbookId !== entry.id ||
|
|
616
|
+
authority.artifactSchema !== 3 ||
|
|
617
|
+
typeof authority.cwd !== 'string' ||
|
|
618
|
+
authority.cwd.length === 0 ||
|
|
619
|
+
typeof authority.sessionId !== 'string' ||
|
|
620
|
+
authority.sessionId.length === 0 ||
|
|
621
|
+
typeof authority.leaseOwnerToken !== 'string' ||
|
|
622
|
+
authority.leaseOwnerToken.length === 0 ||
|
|
623
|
+
canonicalWorktree === undefined ||
|
|
624
|
+
typeof canonicalWorktree.worktree !== 'string' ||
|
|
625
|
+
canonicalWorktree.worktree.length === 0 ||
|
|
626
|
+
typeof canonicalWorktree.gitDir !== 'string' ||
|
|
627
|
+
canonicalWorktree.gitDir.length === 0 ||
|
|
628
|
+
!isDeepStrictEqual(authority.requiredRoleIds, entry.requiredRoleIds) ||
|
|
629
|
+
!isDeepStrictEqual(authority.concurrentRoleSets, entry.concurrentRoleSets) ||
|
|
630
|
+
identity === undefined ||
|
|
631
|
+
!isDeepStrictEqual(identity, canonicalWorktree) ||
|
|
632
|
+
typeof repository?.observe !== 'function' ||
|
|
633
|
+
typeof repository.acquire !== 'function' ||
|
|
634
|
+
typeof repository.runExclusive !== 'function' ||
|
|
635
|
+
typeof repository.runCohort !== 'function' ||
|
|
636
|
+
typeof repository.runDeferred !== 'function' ||
|
|
637
|
+
typeof effectLedger?.snapshot !== 'function' ||
|
|
638
|
+
typeof effectLedger.writeAhead !== 'function') {
|
|
639
|
+
throw new Error(`/${command} schema-3 current-host capability authority does not match its imported artifact`);
|
|
640
|
+
}
|
|
641
|
+
return value;
|
|
642
|
+
}
|
|
643
|
+
function effectLedgerMirrorFromCapabilities(capabilities) {
|
|
644
|
+
const values = [...capabilities.values()];
|
|
645
|
+
if (values.length === 0)
|
|
646
|
+
return emptyPlaybookEffectLedger();
|
|
647
|
+
const mirror = assertPlaybookEffectLedger(values[0].effectLedger.snapshot());
|
|
648
|
+
for (const capability of values.slice(1)) {
|
|
649
|
+
if (!isDeepStrictEqual(assertPlaybookEffectLedger(capability.effectLedger.snapshot()), mirror)) {
|
|
650
|
+
throw new Error('schema-3 current-host capabilities disagree on their effect ledger');
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return mirror;
|
|
444
654
|
}
|
|
445
|
-
function
|
|
655
|
+
function validateRuntimeProfile(value) {
|
|
656
|
+
const shared = exactOwnDataRecord(value, ['kind', 'compat']);
|
|
657
|
+
if (shared?.kind === 'shared-factory') {
|
|
658
|
+
const compat = exactOwnDataRecord(shared.compat, [
|
|
659
|
+
'artifactSchema',
|
|
660
|
+
'runtimeAbi',
|
|
661
|
+
]);
|
|
662
|
+
if (compat?.artifactSchema === 3 &&
|
|
663
|
+
typeof compat.runtimeAbi === 'number' &&
|
|
664
|
+
Number.isSafeInteger(compat.runtimeAbi)) {
|
|
665
|
+
return {
|
|
666
|
+
kind: 'shared-factory',
|
|
667
|
+
artifactSchema: compat.artifactSchema,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
return undefined;
|
|
671
|
+
}
|
|
672
|
+
const bespoke = exactOwnDataRecord(value, ['kind', 'artifactSchema']);
|
|
673
|
+
if (bespoke?.kind === 'bespoke' &&
|
|
674
|
+
bespoke.artifactSchema === 3) {
|
|
675
|
+
return { kind: 'bespoke', artifactSchema: bespoke.artifactSchema };
|
|
676
|
+
}
|
|
677
|
+
return undefined;
|
|
678
|
+
}
|
|
679
|
+
function isValidRegistryEntry(value, artifactSchema) {
|
|
446
680
|
if (typeof value !== 'object' || value === null)
|
|
447
681
|
return false;
|
|
448
682
|
const e = value;
|
|
@@ -467,7 +701,7 @@ function isValidRegistryEntry(value) {
|
|
|
467
701
|
return (typeof e.id === 'string' &&
|
|
468
702
|
typeof e.command === 'string' &&
|
|
469
703
|
typeof e.intent === 'string' &&
|
|
470
|
-
|
|
704
|
+
artifactSchema === 3 &&
|
|
471
705
|
typeof e.validateOptions === 'function' &&
|
|
472
706
|
typeof e.createRuntime === 'function');
|
|
473
707
|
}
|
|
@@ -475,6 +709,7 @@ const SNAPSHOT_ACTIONS = new Set([
|
|
|
475
709
|
'respond',
|
|
476
710
|
'start',
|
|
477
711
|
'switch',
|
|
712
|
+
'resume',
|
|
478
713
|
'dismiss',
|
|
479
714
|
'deliver',
|
|
480
715
|
'runtime',
|
|
@@ -491,6 +726,68 @@ const SNAPSHOT_JOURNAL_KINDS = new Set([
|
|
|
491
726
|
'action',
|
|
492
727
|
'outcome',
|
|
493
728
|
]);
|
|
729
|
+
const UNRESOLVED_EFFECT_CLASSIFICATIONS = new Set([
|
|
730
|
+
'one-descendant-commit',
|
|
731
|
+
'multiple-commits',
|
|
732
|
+
'rewritten-or-non-descendant',
|
|
733
|
+
'worktree-only-change',
|
|
734
|
+
'concurrent-or-foreign-change',
|
|
735
|
+
'observation-ambiguous',
|
|
736
|
+
'incomplete',
|
|
737
|
+
]);
|
|
738
|
+
const GIT_OID_PATTERN = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
739
|
+
export function assertPlaybookCaptainUnresolvedEffects(value) {
|
|
740
|
+
const detached = snapshotJsonValue(value, 'Captain unresolved effects');
|
|
741
|
+
if (!Array.isArray(detached)) {
|
|
742
|
+
throw new TypeError('Captain unresolved effects must be an array');
|
|
743
|
+
}
|
|
744
|
+
for (const [index, raw] of detached.entries()) {
|
|
745
|
+
const path = `Captain unresolved effects[${index}]`;
|
|
746
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
747
|
+
throw new TypeError(`${path} must be an object`);
|
|
748
|
+
}
|
|
749
|
+
const entry = raw;
|
|
750
|
+
const allowed = new Set([
|
|
751
|
+
'classification',
|
|
752
|
+
'baselineHead',
|
|
753
|
+
'afterHead',
|
|
754
|
+
'commitOid',
|
|
755
|
+
]);
|
|
756
|
+
const unknown = Object.keys(entry).find((key) => !allowed.has(key));
|
|
757
|
+
if (unknown !== undefined) {
|
|
758
|
+
throw new TypeError(`${path} has unknown field ${JSON.stringify(unknown)}`);
|
|
759
|
+
}
|
|
760
|
+
if (typeof entry.classification !== 'string' ||
|
|
761
|
+
!UNRESOLVED_EFFECT_CLASSIFICATIONS.has(entry.classification)) {
|
|
762
|
+
throw new TypeError(`${path}.classification is not supported`);
|
|
763
|
+
}
|
|
764
|
+
if (typeof entry.baselineHead !== 'string' ||
|
|
765
|
+
!GIT_OID_PATTERN.test(entry.baselineHead)) {
|
|
766
|
+
throw new TypeError(`${path}.baselineHead must be a Git OID`);
|
|
767
|
+
}
|
|
768
|
+
if (entry.afterHead !== undefined &&
|
|
769
|
+
(typeof entry.afterHead !== 'string' ||
|
|
770
|
+
!GIT_OID_PATTERN.test(entry.afterHead))) {
|
|
771
|
+
throw new TypeError(`${path}.afterHead must be a Git OID`);
|
|
772
|
+
}
|
|
773
|
+
if (entry.classification !== 'observation-ambiguous' &&
|
|
774
|
+
entry.classification !== 'incomplete' &&
|
|
775
|
+
entry.afterHead === undefined) {
|
|
776
|
+
throw new TypeError(`${path}.afterHead is required for ${entry.classification}`);
|
|
777
|
+
}
|
|
778
|
+
if (entry.classification === 'one-descendant-commit') {
|
|
779
|
+
if (typeof entry.commitOid !== 'string' ||
|
|
780
|
+
!GIT_OID_PATTERN.test(entry.commitOid) ||
|
|
781
|
+
entry.commitOid !== entry.afterHead) {
|
|
782
|
+
throw new TypeError(`${path}.commitOid must equal afterHead for one-descendant-commit`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
else if (entry.commitOid !== undefined) {
|
|
786
|
+
throw new TypeError(`${path}.commitOid is permitted only for one-descendant-commit`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return detached;
|
|
790
|
+
}
|
|
494
791
|
function snapshotRecord(value, path) {
|
|
495
792
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
496
793
|
throw new TypeError(`${path} must be an object`);
|
|
@@ -678,9 +975,16 @@ function normalizeHostPlayerResult(value, expectedPlayerId) {
|
|
|
678
975
|
export function assertPlaybookCaptainShellSnapshot(value) {
|
|
679
976
|
const detached = snapshotJsonValue(value, 'Captain shell snapshot');
|
|
680
977
|
const snapshot = snapshotRecord(detached, 'Captain shell snapshot');
|
|
978
|
+
if (snapshot.schemaVersion !== 4) {
|
|
979
|
+
if (snapshot.schemaVersion === 1) {
|
|
980
|
+
throw new TypeError('Captain shell snapshot schemaVersion 1 has incompatible player identity; schema 4 is required');
|
|
981
|
+
}
|
|
982
|
+
throw new TypeError(`Captain shell snapshot.schemaVersion ${String(snapshot.schemaVersion)} is not supported (expected 4)`);
|
|
983
|
+
}
|
|
681
984
|
const mode = snapshot.mode;
|
|
682
985
|
const commonKeys = [
|
|
683
986
|
'schemaVersion',
|
|
987
|
+
'effectLedger',
|
|
684
988
|
'captain',
|
|
685
989
|
'playerSessions',
|
|
686
990
|
'issuedSessionIds',
|
|
@@ -697,6 +1001,7 @@ export function assertPlaybookCaptainShellSnapshot(value) {
|
|
|
697
1001
|
rejectSnapshotKeys(snapshot, [
|
|
698
1002
|
...commonKeys,
|
|
699
1003
|
'frames',
|
|
1004
|
+
'retainedEffectReconciliation',
|
|
700
1005
|
'pendingBossQuestions',
|
|
701
1006
|
'lastError',
|
|
702
1007
|
], 'Captain shell snapshot');
|
|
@@ -704,13 +1009,28 @@ export function assertPlaybookCaptainShellSnapshot(value) {
|
|
|
704
1009
|
else {
|
|
705
1010
|
throw new TypeError('Captain shell snapshot.mode must be "chat" or "engaged.parked"');
|
|
706
1011
|
}
|
|
707
|
-
if (snapshot.schemaVersion !== 3) {
|
|
708
|
-
throw new TypeError(`Captain shell snapshot.schemaVersion ${String(snapshot.schemaVersion)} is not supported (expected 3)`);
|
|
709
|
-
}
|
|
710
1012
|
const captain = snapshotRecord(snapshot.captain, 'Captain shell snapshot.captain');
|
|
711
1013
|
rejectSnapshotKeys(captain, ['sessionId', 'runtime', 'agent', 'conversation'], 'Captain shell snapshot.captain');
|
|
712
1014
|
const captainSessionId = snapshotUuid(captain.sessionId, 'Captain shell snapshot.captain.sessionId');
|
|
713
1015
|
const captainRuntime = assertPlaybookRuntimeSnapshot(captain.runtime, INTERNAL_CAPTAIN_ID);
|
|
1016
|
+
const effectLedger = assertPlaybookEffectLedger(snapshot.effectLedger);
|
|
1017
|
+
let retainedEffectReconciliation;
|
|
1018
|
+
if (snapshot.retainedEffectReconciliation !== undefined) {
|
|
1019
|
+
const reconciliation = snapshotRecord(snapshot.retainedEffectReconciliation, 'Captain shell snapshot.retainedEffectReconciliation');
|
|
1020
|
+
rejectSnapshotKeys(reconciliation, ['sourceGenerationId', 'checkpoint'], 'Captain shell snapshot.retainedEffectReconciliation');
|
|
1021
|
+
const checkpoint = assertPlaybookEffectLedger(reconciliation.checkpoint, 'Captain shell snapshot retained-effect checkpoint');
|
|
1022
|
+
if (isDeepStrictEqual(checkpoint, effectLedger) ||
|
|
1023
|
+
!isPlaybookEffectLedgerMonotonicExtension(checkpoint, effectLedger)) {
|
|
1024
|
+
throw new TypeError('Captain shell retained-effect checkpoint must be a strict monotonic prefix of its current mirror');
|
|
1025
|
+
}
|
|
1026
|
+
retainedEffectReconciliation = {
|
|
1027
|
+
sourceGenerationId: snapshotUuid(reconciliation.sourceGenerationId, 'Captain shell snapshot.retainedEffectReconciliation.sourceGenerationId'),
|
|
1028
|
+
checkpoint,
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
if (!isDeepStrictEqual(captainRuntime.effectLedger, emptyPlaybookEffectLedger())) {
|
|
1032
|
+
throw new TypeError('Captain shell snapshot internal Captain runtime effect ledger must be empty');
|
|
1033
|
+
}
|
|
714
1034
|
const captainAgent = snapshotFixedAgent(captain.agent, 'Captain shell snapshot.captain.agent');
|
|
715
1035
|
const conversation = snapshotRecord(captain.conversation, 'Captain shell snapshot.captain.conversation');
|
|
716
1036
|
let normalizedConversation;
|
|
@@ -827,7 +1147,8 @@ export function assertPlaybookCaptainShellSnapshot(value) {
|
|
|
827
1147
|
}
|
|
828
1148
|
const playerSessions = snapshotPlayerSessions(snapshot.playerSessions, 'Captain shell snapshot.playerSessions');
|
|
829
1149
|
const common = {
|
|
830
|
-
schemaVersion:
|
|
1150
|
+
schemaVersion: 4,
|
|
1151
|
+
effectLedger,
|
|
831
1152
|
captain: {
|
|
832
1153
|
sessionId: captainSessionId,
|
|
833
1154
|
runtime: captainRuntime,
|
|
@@ -913,6 +1234,32 @@ export function assertPlaybookCaptainShellSnapshot(value) {
|
|
|
913
1234
|
const issuedIds = new Set(issued);
|
|
914
1235
|
const rootSessionId = normalizedFrames[0].sessionId;
|
|
915
1236
|
for (const [index, frame] of normalizedFrames.entries()) {
|
|
1237
|
+
const frameLedger = frame.runtime.effectLedger;
|
|
1238
|
+
if (!isDeepStrictEqual(frameLedger, emptyPlaybookEffectLedger()) &&
|
|
1239
|
+
!isDeepStrictEqual(frameLedger, effectLedger)) {
|
|
1240
|
+
throw new TypeError(`Captain shell snapshot frame ${JSON.stringify(frame.playbookId)} effect ledger is neither empty nor the shell mirror`);
|
|
1241
|
+
}
|
|
1242
|
+
const frameReconciliation = frame.runtime.retainedEffectReconciliation;
|
|
1243
|
+
if (retainedEffectReconciliation === undefined) {
|
|
1244
|
+
if (frameReconciliation !== undefined) {
|
|
1245
|
+
throw new TypeError(`Captain shell snapshot frame ${JSON.stringify(frame.playbookId)} carries an unmirrored retained-effect fence`);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
else if (isDeepStrictEqual(frameLedger, effectLedger)) {
|
|
1249
|
+
if (frameReconciliation === undefined ||
|
|
1250
|
+
!isDeepStrictEqual(frameReconciliation.checkpoint, retainedEffectReconciliation.checkpoint)) {
|
|
1251
|
+
throw new TypeError(`Captain shell snapshot frame ${JSON.stringify(frame.playbookId)} does not mirror the root retained-effect fence`);
|
|
1252
|
+
}
|
|
1253
|
+
if (index === 0 &&
|
|
1254
|
+
frameReconciliation.sourceSessionId !==
|
|
1255
|
+
retainedEffectReconciliation.sourceGenerationId) {
|
|
1256
|
+
throw new TypeError('Captain shell snapshot retained-effect root source identity differs from its generation');
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
else if (frameReconciliation !== undefined ||
|
|
1260
|
+
frame.runtime.retainedEffectSourceSessionId !== undefined) {
|
|
1261
|
+
throw new TypeError(`Captain shell snapshot empty-ledger frame ${JSON.stringify(frame.playbookId)} carries retained-effect adoption state`);
|
|
1262
|
+
}
|
|
916
1263
|
if (activePlaybooks.has(frame.playbookId)) {
|
|
917
1264
|
throw new TypeError('Captain shell snapshot engagement path must not contain a playbook cycle');
|
|
918
1265
|
}
|
|
@@ -975,13 +1322,21 @@ export function assertPlaybookCaptainShellSnapshot(value) {
|
|
|
975
1322
|
!leafRuntime.state.tags.includes('playbook.parked')) {
|
|
976
1323
|
throw new TypeError('Captain shell snapshot leaf runtime must be parked without a dangling suspended child call');
|
|
977
1324
|
}
|
|
978
|
-
if (
|
|
979
|
-
|
|
1325
|
+
if (retainedEffectReconciliation === undefined) {
|
|
1326
|
+
if (!isDeepStrictEqual(snapshot.pendingBossQuestions ?? [], leafRuntime.pendingBossQuestions)) {
|
|
1327
|
+
throw new TypeError('Captain shell snapshot pending Boss questions must equal the leaf runtime projection');
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
else if (snapshot.pendingBossQuestions !== undefined) {
|
|
1331
|
+
throw new TypeError('Captain shell snapshot must withhold pending Boss questions behind retained-effect reconciliation');
|
|
980
1332
|
}
|
|
981
1333
|
return snapshotJsonValue({
|
|
982
1334
|
...common,
|
|
983
1335
|
mode,
|
|
984
1336
|
frames: normalizedFrames,
|
|
1337
|
+
...(retainedEffectReconciliation === undefined
|
|
1338
|
+
? {}
|
|
1339
|
+
: { retainedEffectReconciliation }),
|
|
985
1340
|
...(snapshot.pendingBossQuestions === undefined
|
|
986
1341
|
? {}
|
|
987
1342
|
: { pendingBossQuestions: snapshot.pendingBossQuestions }),
|
|
@@ -1060,14 +1415,25 @@ function promptIdentity(binding) {
|
|
|
1060
1415
|
? binding.model.value
|
|
1061
1416
|
: binding.agent.adapter;
|
|
1062
1417
|
}
|
|
1418
|
+
function rejectConfiguredHostCapabilities(value, path) {
|
|
1419
|
+
if (value !== null &&
|
|
1420
|
+
typeof value === 'object' &&
|
|
1421
|
+
!Array.isArray(value) &&
|
|
1422
|
+
Object.prototype.hasOwnProperty.call(value, HOST_CAPABILITIES_OPTION_KEY)) {
|
|
1423
|
+
throw new Error(`${path}.${HOST_CAPABILITIES_OPTION_KEY} is host-owned and cannot be configured`);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1063
1426
|
// Resolve the active registry at init from exact normalized role and session
|
|
1064
1427
|
// agent projections (CAPTAIN-16). No role, ancestor, or generated-name fallback
|
|
1065
1428
|
// exists at this boundary.
|
|
1066
|
-
async function buildEnablements(options, loadModule) {
|
|
1429
|
+
async function buildEnablements(options, loadModule, hostCapabilities) {
|
|
1067
1430
|
const entries = [];
|
|
1068
1431
|
const byCommand = new Map();
|
|
1069
1432
|
const byId = new Map();
|
|
1070
1433
|
const enablementById = new Map();
|
|
1434
|
+
const hostCapabilitiesById = new Map();
|
|
1435
|
+
const suppliedHostCapabilities = captureHostCapabilityRecord(hostCapabilities);
|
|
1436
|
+
const expectedHostCapabilityIds = [];
|
|
1071
1437
|
const detached = snapshotJsonValue(options, 'captain.options');
|
|
1072
1438
|
const top = snapshotRecord(detached, 'captain.options');
|
|
1073
1439
|
rejectSnapshotKeys(top, ['playbooks', 'sessionAgents', 'captainAdapter'], 'captain.options');
|
|
@@ -1107,6 +1473,7 @@ async function buildEnablements(options, loadModule) {
|
|
|
1107
1473
|
}
|
|
1108
1474
|
const record = block;
|
|
1109
1475
|
rejectSnapshotKeys(record, ['from', 'command', 'roles', 'options'], `captain.options.playbooks.${id}`);
|
|
1476
|
+
rejectConfiguredHostCapabilities(record.options, `captain.options.playbooks.${id}.options`);
|
|
1110
1477
|
const from = record.from;
|
|
1111
1478
|
if (typeof from !== 'string' || from.length === 0) {
|
|
1112
1479
|
throw new Error(`captain.options.playbooks.${id}.from must be a module specifier`);
|
|
@@ -1118,10 +1485,27 @@ async function buildEnablements(options, loadModule) {
|
|
|
1118
1485
|
catch (cause) {
|
|
1119
1486
|
throw new Error(`captain.options.playbooks.${id}.from "${from}" failed to import: ${String(cause?.message ?? cause)}`);
|
|
1120
1487
|
}
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1488
|
+
const capturedEntry = captureRegistryEntry(mod?.default);
|
|
1489
|
+
const artifactSchema = capturedEntry
|
|
1490
|
+
?.artifactSchema;
|
|
1491
|
+
const runtimeProfile = validateRuntimeProfile(capturedEntry?.runtimeProfile);
|
|
1492
|
+
if (artifactSchema !== 3 ||
|
|
1493
|
+
runtimeProfile === undefined ||
|
|
1494
|
+
!isValidRegistryEntry(capturedEntry, artifactSchema)) {
|
|
1123
1495
|
throw new Error(`captain.options.playbooks.${id}.from "${from}" exposes no valid registry entry`);
|
|
1124
1496
|
}
|
|
1497
|
+
const entry = Object.freeze({
|
|
1498
|
+
...capturedEntry,
|
|
1499
|
+
requiredRoleIds: Object.freeze([...capturedEntry.requiredRoleIds]),
|
|
1500
|
+
concurrentRoleSets: Object.freeze(capturedEntry.concurrentRoleSets.map((roles) => Object.freeze([...roles]))),
|
|
1501
|
+
});
|
|
1502
|
+
if (runtimeProfile.artifactSchema !== artifactSchema) {
|
|
1503
|
+
const implementation = runtimeProfile.kind === 'shared-factory'
|
|
1504
|
+
? 'shared factory'
|
|
1505
|
+
: 'bespoke runtime';
|
|
1506
|
+
throw new Error(`captain.options.playbooks.${id}.from "${from}" advertises artifact schema ${artifactSchema} ` +
|
|
1507
|
+
`but its ${implementation} implements schema ${runtimeProfile.artifactSchema}`);
|
|
1508
|
+
}
|
|
1125
1509
|
if (entry.id !== id) {
|
|
1126
1510
|
throw new Error(`captain.options.playbooks.${id} key must equal the module manifest id "${entry.id}"`);
|
|
1127
1511
|
}
|
|
@@ -1172,16 +1556,26 @@ async function buildEnablements(options, loadModule) {
|
|
|
1172
1556
|
}
|
|
1173
1557
|
}
|
|
1174
1558
|
const validatedOptions = snapshotJsonValue(entry.validateOptions(record.options), `captain.options.playbooks.${id}.options`);
|
|
1559
|
+
rejectConfiguredHostCapabilities(validatedOptions, `captain.options.playbooks.${id}.options`);
|
|
1175
1560
|
entries.push(entry);
|
|
1176
1561
|
byId.set(entry.id, entry);
|
|
1177
1562
|
byCommand.set(command, entry);
|
|
1563
|
+
const hostCapability = validateHostCapabilities(suppliedHostCapabilities[entry.id], entry, command);
|
|
1564
|
+
expectedHostCapabilityIds.push(entry.id);
|
|
1565
|
+
hostCapabilitiesById.set(entry.id, hostCapability);
|
|
1178
1566
|
enablementById.set(entry.id, {
|
|
1179
1567
|
entry,
|
|
1568
|
+
artifactSchema,
|
|
1180
1569
|
command,
|
|
1181
1570
|
options: validatedOptions,
|
|
1182
1571
|
roleBindings,
|
|
1183
1572
|
});
|
|
1184
1573
|
}
|
|
1574
|
+
const suppliedHostCapabilityIds = Object.keys(suppliedHostCapabilities).sort();
|
|
1575
|
+
expectedHostCapabilityIds.sort();
|
|
1576
|
+
if (!isDeepStrictEqual(suppliedHostCapabilityIds, expectedHostCapabilityIds)) {
|
|
1577
|
+
throw new Error('current-host construction capabilities must exactly cover schema-3 playbooks');
|
|
1578
|
+
}
|
|
1185
1579
|
const referenced = new Set([...enablementById.values()].flatMap((enablement) => [...enablement.roleBindings.values()].map((binding) => binding.playerId)));
|
|
1186
1580
|
const unreferenced = [...playerAgents.keys()].find((id) => !referenced.has(id));
|
|
1187
1581
|
if (unreferenced !== undefined) {
|
|
@@ -1192,6 +1586,7 @@ async function buildEnablements(options, loadModule) {
|
|
|
1192
1586
|
byCommand,
|
|
1193
1587
|
byId,
|
|
1194
1588
|
enablementById,
|
|
1589
|
+
hostCapabilitiesById,
|
|
1195
1590
|
captainAgent,
|
|
1196
1591
|
playerAgents,
|
|
1197
1592
|
};
|
|
@@ -1200,6 +1595,17 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1200
1595
|
const loadModule = deps.loadModule ?? ((specifier) => import(specifier));
|
|
1201
1596
|
const createSessionId = deps.createSessionId ?? randomUUID;
|
|
1202
1597
|
const createCaptainRuntime = deps.createCaptainRuntime ?? createDefaultCaptainRuntime;
|
|
1598
|
+
const unresolvedEffectSettlement = deps.unresolvedEffectSettlement;
|
|
1599
|
+
let pendingHostCapabilities = deps.hostCapabilities;
|
|
1600
|
+
let currentEffectLedger = () => emptyPlaybookEffectLedger();
|
|
1601
|
+
// The returned shell must not retain the caller's aggregate dependency
|
|
1602
|
+
// object after its one live capability input has moved to a clearable slot.
|
|
1603
|
+
deps = {};
|
|
1604
|
+
const buildCurrentEnablements = async () => {
|
|
1605
|
+
const hostCapabilities = pendingHostCapabilities;
|
|
1606
|
+
pendingHostCapabilities = undefined;
|
|
1607
|
+
return buildEnablements(options, loadModule, hostCapabilities);
|
|
1608
|
+
};
|
|
1203
1609
|
let captainAgent;
|
|
1204
1610
|
let captainAdapter;
|
|
1205
1611
|
let playerAgents = new Map();
|
|
@@ -1209,6 +1615,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1209
1615
|
let byCommand = new Map();
|
|
1210
1616
|
let byId = new Map();
|
|
1211
1617
|
let enablementById = new Map();
|
|
1618
|
+
let hostCapabilitiesById = new Map();
|
|
1212
1619
|
let session;
|
|
1213
1620
|
let sessionEmissionsOpen = false;
|
|
1214
1621
|
let closedGateAttempted = false;
|
|
@@ -1217,6 +1624,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1217
1624
|
let activeContext;
|
|
1218
1625
|
const frames = [];
|
|
1219
1626
|
let mode = 'chat';
|
|
1627
|
+
let retainedEffectReconciliation;
|
|
1220
1628
|
let pendingBossQuestions;
|
|
1221
1629
|
let lastError;
|
|
1222
1630
|
let activeTurnSummary;
|
|
@@ -1286,6 +1694,19 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1286
1694
|
let decisionCall;
|
|
1287
1695
|
let lastAction;
|
|
1288
1696
|
let lastSettlementStatus;
|
|
1697
|
+
const retainedGenerationCandidates = new Map();
|
|
1698
|
+
const pendingRetentionUpdates = new Map();
|
|
1699
|
+
const retainedGenerations = new Map();
|
|
1700
|
+
const retainedGenerationOffers = new Map();
|
|
1701
|
+
const ineligibleRetainedGenerations = new Set();
|
|
1702
|
+
const retainedGenerationRootClears = new Set();
|
|
1703
|
+
const retiredRetainedRuntimes = [];
|
|
1704
|
+
let retainedGenerationsInstalled = false;
|
|
1705
|
+
let retainedGenerationInstallationInProgress = false;
|
|
1706
|
+
let retainedGenerationInstallationClosed = false;
|
|
1707
|
+
let retentionSettlementReady = false;
|
|
1708
|
+
let abandonmentSettlementUnsafe = false;
|
|
1709
|
+
let settledTurnUnresolvedEffects;
|
|
1289
1710
|
// DR-029: a run that lands in the runtime's own failure state
|
|
1290
1711
|
// is an outcome the report must name. `processFrameResult` records it here
|
|
1291
1712
|
// and the settling selection folds it into its facts, so the grounding the
|
|
@@ -1294,6 +1715,568 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1294
1715
|
const rootFrame = () => frames[0];
|
|
1295
1716
|
const leafFrame = () => frames.at(-1);
|
|
1296
1717
|
const frameLabel = (frame) => `/${frame.enablement.command}`;
|
|
1718
|
+
const capturedUnresolvedEnvelopeReferences = (frame) => {
|
|
1719
|
+
let advertisesUnresolved = false;
|
|
1720
|
+
try {
|
|
1721
|
+
advertisesUnresolved =
|
|
1722
|
+
frame.runtime
|
|
1723
|
+
.describe?.()
|
|
1724
|
+
.actions.some(({ id }) => id === UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID ||
|
|
1725
|
+
id === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID) === true;
|
|
1726
|
+
}
|
|
1727
|
+
catch {
|
|
1728
|
+
advertisesUnresolved = true;
|
|
1729
|
+
}
|
|
1730
|
+
if (typeof frame.runtime.unresolvedEffectEnvelopes !== 'function') {
|
|
1731
|
+
if (advertisesUnresolved) {
|
|
1732
|
+
throw new Error(`${frameLabel(frame)} unresolved-effect runtime exposes no envelope identities`);
|
|
1733
|
+
}
|
|
1734
|
+
return [];
|
|
1735
|
+
}
|
|
1736
|
+
const detached = snapshotJsonValue(frame.runtime.unresolvedEffectEnvelopes(), `${frameLabel(frame)} unresolved effect envelope identities`);
|
|
1737
|
+
if (!Array.isArray(detached)) {
|
|
1738
|
+
throw new TypeError(`${frameLabel(frame)} unresolved effect envelope identities must be an array`);
|
|
1739
|
+
}
|
|
1740
|
+
return detached.map((raw, index) => {
|
|
1741
|
+
const path = `${frameLabel(frame)} unresolved effect envelope identities[${index}]`;
|
|
1742
|
+
const record = snapshotRecord(raw, path);
|
|
1743
|
+
if (record.kind === 'boundary') {
|
|
1744
|
+
rejectSnapshotKeys(record, ['kind', 'boundaryId'], path);
|
|
1745
|
+
return {
|
|
1746
|
+
kind: 'boundary',
|
|
1747
|
+
boundaryId: snapshotString(record.boundaryId, `${path}.boundaryId`),
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
if (record.kind === 'logical-operation') {
|
|
1751
|
+
rejectSnapshotKeys(record, ['kind', 'operationId'], path);
|
|
1752
|
+
return {
|
|
1753
|
+
kind: 'logical-operation',
|
|
1754
|
+
operationId: snapshotString(record.operationId, `${path}.operationId`),
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
throw new TypeError(`${path}.kind is not supported`);
|
|
1758
|
+
});
|
|
1759
|
+
};
|
|
1760
|
+
const unresolvedEffectFromReceipt = (receipt, baselineHead = receipt.baseline.head) => {
|
|
1761
|
+
if (receipt.classification === 'unchanged')
|
|
1762
|
+
return undefined;
|
|
1763
|
+
return {
|
|
1764
|
+
classification: receipt.classification,
|
|
1765
|
+
baselineHead,
|
|
1766
|
+
...(receipt.after === undefined ? {} : { afterHead: receipt.after.head }),
|
|
1767
|
+
...(receipt.commitOid === undefined ? {} : { commitOid: receipt.commitOid }),
|
|
1768
|
+
};
|
|
1769
|
+
};
|
|
1770
|
+
const cumulativeOpenLogicalEffect = (operation, boundaries) => {
|
|
1771
|
+
const original = operation.originalBaseline;
|
|
1772
|
+
const latest = boundaries.at(-1);
|
|
1773
|
+
const after = latest.after ?? operation.checkpoint;
|
|
1774
|
+
const receipt = latest.physicalReceipt;
|
|
1775
|
+
if (receipt === undefined) {
|
|
1776
|
+
return {
|
|
1777
|
+
classification: 'incomplete',
|
|
1778
|
+
baselineHead: original.head,
|
|
1779
|
+
...(after === undefined ? {} : { afterHead: after.head }),
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
if (after === undefined) {
|
|
1783
|
+
return unresolvedEffectFromReceipt(receipt, original.head);
|
|
1784
|
+
}
|
|
1785
|
+
// An open deferred chain has no authoritative cumulative receipt. Reuse
|
|
1786
|
+
// physical ancestry only while every preceding checkpoint is one of the
|
|
1787
|
+
// same-HEAD dispositions that can lawfully keep a deferred operation
|
|
1788
|
+
// open. Any other history is bounded but not cumulatively attributable.
|
|
1789
|
+
const checkpointChainIsSafe = boundaries
|
|
1790
|
+
.slice(0, -1)
|
|
1791
|
+
.every((boundary) => boundary.after?.head === original.head &&
|
|
1792
|
+
(boundary.physicalReceipt?.classification === 'unchanged' ||
|
|
1793
|
+
boundary.physicalReceipt?.classification ===
|
|
1794
|
+
'worktree-only-change'));
|
|
1795
|
+
if (!checkpointChainIsSafe || latest.baseline.head !== original.head) {
|
|
1796
|
+
return {
|
|
1797
|
+
classification: 'observation-ambiguous',
|
|
1798
|
+
baselineHead: original.head,
|
|
1799
|
+
afterHead: after.head,
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
if (receipt.classification !== 'unchanged' &&
|
|
1803
|
+
receipt.classification !== 'worktree-only-change' &&
|
|
1804
|
+
receipt.classification !== 'one-descendant-commit') {
|
|
1805
|
+
return unresolvedEffectFromReceipt(receipt, original.head);
|
|
1806
|
+
}
|
|
1807
|
+
const sameProjection = isDeepStrictEqual(original.projection, after.projection);
|
|
1808
|
+
if (after.head === original.head) {
|
|
1809
|
+
if (receipt.classification !== 'unchanged' &&
|
|
1810
|
+
receipt.classification !== 'worktree-only-change') {
|
|
1811
|
+
return {
|
|
1812
|
+
classification: 'observation-ambiguous',
|
|
1813
|
+
baselineHead: original.head,
|
|
1814
|
+
afterHead: after.head,
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
if (sameProjection)
|
|
1818
|
+
return undefined;
|
|
1819
|
+
const preservesOriginal = Object.entries(original.projection).every(([path, entry]) => Object.hasOwn(after.projection, path) &&
|
|
1820
|
+
isDeepStrictEqual(entry, after.projection[path]));
|
|
1821
|
+
return {
|
|
1822
|
+
classification: preservesOriginal
|
|
1823
|
+
? 'worktree-only-change'
|
|
1824
|
+
: 'observation-ambiguous',
|
|
1825
|
+
baselineHead: original.head,
|
|
1826
|
+
afterHead: after.head,
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
if (receipt.classification === 'one-descendant-commit' &&
|
|
1830
|
+
sameProjection) {
|
|
1831
|
+
return {
|
|
1832
|
+
classification: 'one-descendant-commit',
|
|
1833
|
+
baselineHead: original.head,
|
|
1834
|
+
afterHead: after.head,
|
|
1835
|
+
commitOid: after.head,
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
return {
|
|
1839
|
+
classification: 'observation-ambiguous',
|
|
1840
|
+
baselineHead: original.head,
|
|
1841
|
+
afterHead: after.head,
|
|
1842
|
+
};
|
|
1843
|
+
};
|
|
1844
|
+
const projectUnresolvedEffects = (ledger, references) => {
|
|
1845
|
+
const pendingReferences = [...references];
|
|
1846
|
+
const projected = [];
|
|
1847
|
+
const seenBoundaries = new Set();
|
|
1848
|
+
const seenOperations = new Set();
|
|
1849
|
+
for (let index = 0; index < pendingReferences.length; index += 1) {
|
|
1850
|
+
const reference = pendingReferences[index];
|
|
1851
|
+
if (reference.kind === 'boundary') {
|
|
1852
|
+
if (seenBoundaries.has(reference.boundaryId))
|
|
1853
|
+
continue;
|
|
1854
|
+
seenBoundaries.add(reference.boundaryId);
|
|
1855
|
+
const boundary = ledger.boundaries.find(({ boundaryId }) => boundaryId === reference.boundaryId);
|
|
1856
|
+
if (boundary === undefined) {
|
|
1857
|
+
throw new Error(`unresolved effect boundary ${JSON.stringify(reference.boundaryId)} is absent from the authoritative ledger`);
|
|
1858
|
+
}
|
|
1859
|
+
if (boundary.logicalOperationId !== undefined) {
|
|
1860
|
+
if (!seenOperations.has(boundary.logicalOperationId)) {
|
|
1861
|
+
pendingReferences.push({
|
|
1862
|
+
kind: 'logical-operation',
|
|
1863
|
+
operationId: boundary.logicalOperationId,
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
continue;
|
|
1867
|
+
}
|
|
1868
|
+
const effect = boundary.physicalReceipt === undefined
|
|
1869
|
+
? {
|
|
1870
|
+
classification: 'incomplete',
|
|
1871
|
+
baselineHead: boundary.baseline.head,
|
|
1872
|
+
...(boundary.after === undefined
|
|
1873
|
+
? {}
|
|
1874
|
+
: { afterHead: boundary.after.head }),
|
|
1875
|
+
}
|
|
1876
|
+
: unresolvedEffectFromReceipt(boundary.physicalReceipt);
|
|
1877
|
+
if (effect !== undefined) {
|
|
1878
|
+
projected.push({ order: boundary.sequence, effect });
|
|
1879
|
+
}
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
if (seenOperations.has(reference.operationId))
|
|
1883
|
+
continue;
|
|
1884
|
+
seenOperations.add(reference.operationId);
|
|
1885
|
+
const operation = ledger.logicalOperations.find(({ operationId }) => operationId === reference.operationId);
|
|
1886
|
+
if (operation === undefined) {
|
|
1887
|
+
throw new Error(`unresolved logical operation ${JSON.stringify(reference.operationId)} is absent from the authoritative ledger`);
|
|
1888
|
+
}
|
|
1889
|
+
const boundaries = operation.boundaryIds.map((boundaryId) => {
|
|
1890
|
+
const boundary = ledger.boundaries.find((candidate) => candidate.boundaryId === boundaryId);
|
|
1891
|
+
if (boundary === undefined) {
|
|
1892
|
+
throw new Error(`unresolved logical operation ${JSON.stringify(reference.operationId)} names an absent boundary`);
|
|
1893
|
+
}
|
|
1894
|
+
seenBoundaries.add(boundaryId);
|
|
1895
|
+
return boundary;
|
|
1896
|
+
});
|
|
1897
|
+
let effect;
|
|
1898
|
+
if (operation.logicalReceipt !== undefined) {
|
|
1899
|
+
effect = unresolvedEffectFromReceipt(operation.logicalReceipt, operation.originalBaseline.head);
|
|
1900
|
+
}
|
|
1901
|
+
else {
|
|
1902
|
+
effect = cumulativeOpenLogicalEffect(operation, boundaries);
|
|
1903
|
+
}
|
|
1904
|
+
if (effect !== undefined) {
|
|
1905
|
+
projected.push({ order: boundaries[0].sequence, effect });
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
return assertPlaybookCaptainUnresolvedEffects(projected
|
|
1909
|
+
.sort((left, right) => left.order - right.order)
|
|
1910
|
+
.map(({ effect }) => effect));
|
|
1911
|
+
};
|
|
1912
|
+
const currentUnresolvedEffects = () => {
|
|
1913
|
+
const ledger = assertPlaybookEffectLedger(currentEffectLedger());
|
|
1914
|
+
const references = [];
|
|
1915
|
+
for (const frame of frames) {
|
|
1916
|
+
references.push(...capturedUnresolvedEnvelopeReferences(frame));
|
|
1917
|
+
}
|
|
1918
|
+
if (retainedEffectReconciliation !== undefined) {
|
|
1919
|
+
for (const boundary of ledger.boundaries.slice(retainedEffectReconciliation.checkpoint.boundaries.length)) {
|
|
1920
|
+
if (boundary.physicalReceipt?.classification === 'unchanged') {
|
|
1921
|
+
continue;
|
|
1922
|
+
}
|
|
1923
|
+
references.push(boundary.logicalOperationId === undefined
|
|
1924
|
+
? { kind: 'boundary', boundaryId: boundary.boundaryId }
|
|
1925
|
+
: {
|
|
1926
|
+
kind: 'logical-operation',
|
|
1927
|
+
operationId: boundary.logicalOperationId,
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
return projectUnresolvedEffects(ledger, references);
|
|
1932
|
+
};
|
|
1933
|
+
const freezeTurnUnresolvedEffects = () => {
|
|
1934
|
+
const turn = activeTurn;
|
|
1935
|
+
if (turn?.unresolvedEffects !== undefined)
|
|
1936
|
+
return turn.unresolvedEffects;
|
|
1937
|
+
const frozen = currentUnresolvedEffects();
|
|
1938
|
+
if (turn !== undefined)
|
|
1939
|
+
turn.unresolvedEffects = frozen;
|
|
1940
|
+
settledTurnUnresolvedEffects = frozen;
|
|
1941
|
+
return frozen;
|
|
1942
|
+
};
|
|
1943
|
+
const normalizeInstalledRetainedGenerations = (value) => {
|
|
1944
|
+
const path = 'Captain retained generations';
|
|
1945
|
+
const detached = snapshotJsonValue(value, path);
|
|
1946
|
+
const record = snapshotRecord(detached, path);
|
|
1947
|
+
const authoritativeEffectLedger = assertPlaybookEffectLedger(currentEffectLedger(), `${path} current host effect ledger`);
|
|
1948
|
+
const sourceSessionIds = new Set();
|
|
1949
|
+
const normalized = new Map();
|
|
1950
|
+
for (const [rootPlaybookId, rawGeneration] of Object.entries(record)) {
|
|
1951
|
+
const generationPath = `${path}[${JSON.stringify(rootPlaybookId)}]`;
|
|
1952
|
+
const enablement = enablementById.get(rootPlaybookId);
|
|
1953
|
+
if (enablement === undefined) {
|
|
1954
|
+
throw new TypeError(`${generationPath} names a disabled root playbook`);
|
|
1955
|
+
}
|
|
1956
|
+
const generation = snapshotRecord(rawGeneration, generationPath);
|
|
1957
|
+
rejectSnapshotKeys(generation, [
|
|
1958
|
+
'effectLedger',
|
|
1959
|
+
'frames',
|
|
1960
|
+
'retainedEffectReconciliation',
|
|
1961
|
+
'rootStateDescription',
|
|
1962
|
+
], generationPath);
|
|
1963
|
+
const generationEffectLedger = assertPlaybookEffectLedger(generation.effectLedger, `${generationPath}.effectLedger`);
|
|
1964
|
+
if (generationEffectLedger.boundaries.some(({ physicalReceipt }) => physicalReceipt === undefined)) {
|
|
1965
|
+
throw new TypeError(`${generationPath}.effectLedger contains an incomplete physical boundary`);
|
|
1966
|
+
}
|
|
1967
|
+
if (!isPlaybookEffectLedgerMonotonicExtension(generationEffectLedger, authoritativeEffectLedger)) {
|
|
1968
|
+
throw new TypeError(`${generationPath}.effectLedger is not a monotonic prefix of the current host mirror`);
|
|
1969
|
+
}
|
|
1970
|
+
const generationReconciliation = generation.retainedEffectReconciliation === undefined
|
|
1971
|
+
? undefined
|
|
1972
|
+
: snapshotRecord(generation.retainedEffectReconciliation, `${generationPath}.retainedEffectReconciliation`);
|
|
1973
|
+
if (generationReconciliation !== undefined) {
|
|
1974
|
+
rejectSnapshotKeys(generationReconciliation, ['sourceGenerationId'], `${generationPath}.retainedEffectReconciliation`);
|
|
1975
|
+
}
|
|
1976
|
+
const sourceGenerationId = generationReconciliation === undefined
|
|
1977
|
+
? undefined
|
|
1978
|
+
: snapshotUuid(generationReconciliation.sourceGenerationId, `${generationPath}.retainedEffectReconciliation.sourceGenerationId`);
|
|
1979
|
+
if (!Array.isArray(generation.frames) || generation.frames.length === 0) {
|
|
1980
|
+
throw new TypeError(`${generationPath}.frames must be non-empty`);
|
|
1981
|
+
}
|
|
1982
|
+
const rootStateDescription = generation.rootStateDescription === undefined
|
|
1983
|
+
? undefined
|
|
1984
|
+
: snapshotString(generation.rootStateDescription, `${generationPath}.rootStateDescription`);
|
|
1985
|
+
const normalizedFrames = [];
|
|
1986
|
+
const playbookIds = new Set();
|
|
1987
|
+
let markedCaptureEffectLedger;
|
|
1988
|
+
for (const [index, rawFrame] of generation.frames.entries()) {
|
|
1989
|
+
const framePath = `${generationPath}.frames[${index}]`;
|
|
1990
|
+
const frame = snapshotRecord(rawFrame, framePath);
|
|
1991
|
+
rejectSnapshotKeys(frame, [
|
|
1992
|
+
'playbookId',
|
|
1993
|
+
'sessionId',
|
|
1994
|
+
'rootSessionId',
|
|
1995
|
+
'depth',
|
|
1996
|
+
'parentSessionId',
|
|
1997
|
+
'parentCallId',
|
|
1998
|
+
'options',
|
|
1999
|
+
'roleBindings',
|
|
2000
|
+
'runtime',
|
|
2001
|
+
], framePath);
|
|
2002
|
+
const playbookId = snapshotString(frame.playbookId, `${framePath}.playbookId`);
|
|
2003
|
+
const frameEnablement = enablementById.get(playbookId);
|
|
2004
|
+
if (frameEnablement === undefined) {
|
|
2005
|
+
throw new TypeError(`${framePath} names a disabled playbook`);
|
|
2006
|
+
}
|
|
2007
|
+
if (playbookIds.has(playbookId)) {
|
|
2008
|
+
throw new TypeError(`${generationPath}.frames must not contain a playbook cycle`);
|
|
2009
|
+
}
|
|
2010
|
+
playbookIds.add(playbookId);
|
|
2011
|
+
const sessionId = snapshotUuid(frame.sessionId, `${framePath}.sessionId`);
|
|
2012
|
+
if (sourceSessionIds.has(sessionId)) {
|
|
2013
|
+
throw new TypeError(`${path} frame session ids must be unique across generations`);
|
|
2014
|
+
}
|
|
2015
|
+
sourceSessionIds.add(sessionId);
|
|
2016
|
+
const rootSessionId = snapshotUuid(frame.rootSessionId, `${framePath}.rootSessionId`);
|
|
2017
|
+
const depth = snapshotInteger(frame.depth, `${framePath}.depth`);
|
|
2018
|
+
const parentSessionId = frame.parentSessionId === undefined
|
|
2019
|
+
? undefined
|
|
2020
|
+
: snapshotUuid(frame.parentSessionId, `${framePath}.parentSessionId`);
|
|
2021
|
+
const parentCallId = frame.parentCallId === undefined
|
|
2022
|
+
? undefined
|
|
2023
|
+
: snapshotString(frame.parentCallId, `${framePath}.parentCallId`);
|
|
2024
|
+
const options = frame.options;
|
|
2025
|
+
if (index === 0 &&
|
|
2026
|
+
!isDeepStrictEqual(options, frameEnablement.options)) {
|
|
2027
|
+
throw new TypeError(`${framePath}.options changed`);
|
|
2028
|
+
}
|
|
2029
|
+
const roleBindings = snapshotFrameRoleBindings(frame.roleBindings, `${framePath}.roleBindings`);
|
|
2030
|
+
if (!isDeepStrictEqual(Object.keys(roleBindings).sort(), [...frameEnablement.entry.requiredRoleIds].sort())) {
|
|
2031
|
+
throw new TypeError(`${framePath}.roleBindings do not cover the current role set`);
|
|
2032
|
+
}
|
|
2033
|
+
const runtime = assertPlaybookRuntimeSnapshot(frame.runtime, playbookId, { allowSuspendedCall: true });
|
|
2034
|
+
const retainedReconciliation = runtime.retainedEffectReconciliation;
|
|
2035
|
+
if (retainedReconciliation === undefined) {
|
|
2036
|
+
if (!isDeepStrictEqual(runtime.effectLedger, generationEffectLedger)) {
|
|
2037
|
+
throw new TypeError(`${framePath}.runtime effect ledger differs from the retained checkpoint`);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
else {
|
|
2041
|
+
if (!isDeepStrictEqual(retainedReconciliation.checkpoint, generationEffectLedger) ||
|
|
2042
|
+
isDeepStrictEqual(runtime.effectLedger, generationEffectLedger) ||
|
|
2043
|
+
!isPlaybookEffectLedgerMonotonicExtension(runtime.effectLedger, authoritativeEffectLedger)) {
|
|
2044
|
+
throw new TypeError(`${framePath}.runtime retained-effect evidence is inconsistent`);
|
|
2045
|
+
}
|
|
2046
|
+
if (markedCaptureEffectLedger === undefined) {
|
|
2047
|
+
markedCaptureEffectLedger = runtime.effectLedger;
|
|
2048
|
+
}
|
|
2049
|
+
else if (!isDeepStrictEqual(runtime.effectLedger, markedCaptureEffectLedger)) {
|
|
2050
|
+
throw new TypeError(`${framePath}.runtime effect ledger differs from the marked generation capture mirror`);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
if (runtime.state.status !== 'active' ||
|
|
2054
|
+
!runtime.state.quiescent ||
|
|
2055
|
+
typeof runtime.state.stateId !== 'string' ||
|
|
2056
|
+
runtime.state.stateId.trim().length === 0) {
|
|
2057
|
+
throw new TypeError(`${framePath}.runtime must be active, quiescent, and state-identified`);
|
|
2058
|
+
}
|
|
2059
|
+
for (const question of runtime.pendingBossQuestions) {
|
|
2060
|
+
if (question.asker.kind === 'role' &&
|
|
2061
|
+
roleBindings[question.asker.roleId] === undefined) {
|
|
2062
|
+
throw new TypeError(`${framePath}.runtime pending question names an unbound role`);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
for (const roleId of Object.keys(runtime.roleResumeTokens)) {
|
|
2066
|
+
if (roleBindings[roleId] === undefined) {
|
|
2067
|
+
throw new TypeError(`${framePath}.runtime role-resume token names an unbound role`);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
normalizedFrames.push({
|
|
2071
|
+
playbookId,
|
|
2072
|
+
sessionId,
|
|
2073
|
+
rootSessionId,
|
|
2074
|
+
depth,
|
|
2075
|
+
...(parentSessionId === undefined ? {} : { parentSessionId }),
|
|
2076
|
+
...(parentCallId === undefined ? {} : { parentCallId }),
|
|
2077
|
+
options,
|
|
2078
|
+
roleBindings,
|
|
2079
|
+
runtime,
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
const sourceRootSessionId = normalizedFrames[0].sessionId;
|
|
2083
|
+
for (const [index, frame] of normalizedFrames.entries()) {
|
|
2084
|
+
if (frame.depth !== index || frame.rootSessionId !== sourceRootSessionId) {
|
|
2085
|
+
throw new TypeError(`${generationPath}.frames have inconsistent depth or root identity`);
|
|
2086
|
+
}
|
|
2087
|
+
if (index === 0) {
|
|
2088
|
+
if (frame.playbookId !== rootPlaybookId ||
|
|
2089
|
+
frame.sessionId !== frame.rootSessionId ||
|
|
2090
|
+
frame.parentSessionId !== undefined ||
|
|
2091
|
+
frame.parentCallId !== undefined) {
|
|
2092
|
+
throw new TypeError(`${generationPath}.frames[0] is not the named root`);
|
|
2093
|
+
}
|
|
2094
|
+
continue;
|
|
2095
|
+
}
|
|
2096
|
+
const parent = normalizedFrames[index - 1];
|
|
2097
|
+
const suspended = parent.runtime.suspendedCall;
|
|
2098
|
+
if (frame.parentSessionId !== parent.sessionId ||
|
|
2099
|
+
frame.parentCallId === undefined ||
|
|
2100
|
+
suspended === undefined ||
|
|
2101
|
+
suspended.callId !== frame.parentCallId ||
|
|
2102
|
+
suspended.playbookId !== frame.playbookId ||
|
|
2103
|
+
suspended.childSessionId !== frame.sessionId) {
|
|
2104
|
+
throw new TypeError(`${generationPath}.frames[${index}] does not match its suspended parent edge`);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
const leaf = normalizedFrames.at(-1);
|
|
2108
|
+
if (leaf.runtime.suspendedCall !== undefined ||
|
|
2109
|
+
!leaf.runtime.state.tags.includes('playbook.parked')) {
|
|
2110
|
+
throw new TypeError(`${generationPath} leaf must be parked without a suspended child`);
|
|
2111
|
+
}
|
|
2112
|
+
const markedFrames = normalizedFrames.filter(({ runtime }) => runtime.retainedEffectReconciliation !== undefined);
|
|
2113
|
+
if ((sourceGenerationId === undefined && markedFrames.length !== 0) ||
|
|
2114
|
+
(sourceGenerationId !== undefined &&
|
|
2115
|
+
markedFrames.length !== normalizedFrames.length) ||
|
|
2116
|
+
(sourceGenerationId !== undefined &&
|
|
2117
|
+
normalizedFrames[0].runtime.retainedEffectReconciliation
|
|
2118
|
+
?.sourceSessionId !== sourceGenerationId)) {
|
|
2119
|
+
throw new TypeError(`${generationPath} retained-effect source marker is inconsistent`);
|
|
2120
|
+
}
|
|
2121
|
+
normalized.set(rootPlaybookId, snapshotJsonValue({
|
|
2122
|
+
effectLedger: generationEffectLedger,
|
|
2123
|
+
frames: normalizedFrames,
|
|
2124
|
+
...(sourceGenerationId === undefined
|
|
2125
|
+
? {}
|
|
2126
|
+
: {
|
|
2127
|
+
retainedEffectReconciliation: { sourceGenerationId },
|
|
2128
|
+
}),
|
|
2129
|
+
...(rootStateDescription === undefined
|
|
2130
|
+
? {}
|
|
2131
|
+
: { rootStateDescription }),
|
|
2132
|
+
}, generationPath));
|
|
2133
|
+
}
|
|
2134
|
+
return normalized;
|
|
2135
|
+
};
|
|
2136
|
+
const runtimeRetainsGenerations = (runtime) => {
|
|
2137
|
+
const metadata = runtime.retainedGenerationMetadata;
|
|
2138
|
+
return (typeof runtime.exportSnapshot === 'function' &&
|
|
2139
|
+
typeof runtime.restore === 'function' &&
|
|
2140
|
+
typeof runtime.adopt === 'function' &&
|
|
2141
|
+
metadata !== undefined &&
|
|
2142
|
+
Array.isArray(metadata.unfinishedFinalStateIds) &&
|
|
2143
|
+
metadata.unfinishedFinalStateIds.every((stateId) => typeof stateId === 'string'));
|
|
2144
|
+
};
|
|
2145
|
+
class RetainedRuntimeCleanupError extends AggregateError {
|
|
2146
|
+
failedRuntimes;
|
|
2147
|
+
constructor(failures, message, failedRuntimes = []) {
|
|
2148
|
+
super(failures, message);
|
|
2149
|
+
this.name = 'RetainedRuntimeCleanupError';
|
|
2150
|
+
this.failedRuntimes = failedRuntimes;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
const disposeRetainedRuntimeSet = async (runtimes, message) => {
|
|
2154
|
+
const failures = [];
|
|
2155
|
+
const failedRuntimes = [];
|
|
2156
|
+
for (const runtime of [...runtimes].reverse()) {
|
|
2157
|
+
try {
|
|
2158
|
+
await runtime.dispose();
|
|
2159
|
+
}
|
|
2160
|
+
catch (error) {
|
|
2161
|
+
failures.push(error);
|
|
2162
|
+
failedRuntimes.unshift(runtime);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
if (failures.length > 0) {
|
|
2166
|
+
throw new RetainedRuntimeCleanupError(failures, message, failedRuntimes);
|
|
2167
|
+
}
|
|
2168
|
+
};
|
|
2169
|
+
const retireRetainedOffer = (rootPlaybookId) => {
|
|
2170
|
+
const offer = retainedGenerationOffers.get(rootPlaybookId);
|
|
2171
|
+
if (offer === undefined)
|
|
2172
|
+
return;
|
|
2173
|
+
retainedGenerationOffers.delete(rootPlaybookId);
|
|
2174
|
+
retiredRetainedRuntimes.push(...offer.runtimes);
|
|
2175
|
+
};
|
|
2176
|
+
const applyRetentionUpdateToCatalog = (update) => {
|
|
2177
|
+
if (update.kind === 'clear') {
|
|
2178
|
+
retireRetainedOffer(update.rootPlaybookId);
|
|
2179
|
+
retainedGenerations.delete(update.rootPlaybookId);
|
|
2180
|
+
ineligibleRetainedGenerations.delete(update.rootPlaybookId);
|
|
2181
|
+
retainedGenerationRootClears.delete(update.rootPlaybookId);
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
const prior = retainedGenerations.get(update.rootPlaybookId);
|
|
2185
|
+
if (isDeepStrictEqual(prior, update.generation))
|
|
2186
|
+
return;
|
|
2187
|
+
retireRetainedOffer(update.rootPlaybookId);
|
|
2188
|
+
retainedGenerations.set(update.rootPlaybookId, update.generation);
|
|
2189
|
+
ineligibleRetainedGenerations.delete(update.rootPlaybookId);
|
|
2190
|
+
retainedGenerationRootClears.delete(update.rootPlaybookId);
|
|
2191
|
+
};
|
|
2192
|
+
const drainRetiredRetainedRuntimes = async () => {
|
|
2193
|
+
if (retiredRetainedRuntimes.length === 0)
|
|
2194
|
+
return;
|
|
2195
|
+
const runtimes = retiredRetainedRuntimes.splice(0);
|
|
2196
|
+
try {
|
|
2197
|
+
await disposeRetainedRuntimeSet(runtimes, 'retired retained-generation runtime cleanup failed');
|
|
2198
|
+
}
|
|
2199
|
+
catch (error) {
|
|
2200
|
+
if (error instanceof RetainedRuntimeCleanupError) {
|
|
2201
|
+
retiredRetainedRuntimes.unshift(...error.failedRuntimes);
|
|
2202
|
+
}
|
|
2203
|
+
terminallyDisposed = true;
|
|
2204
|
+
lifecycle = 'closed';
|
|
2205
|
+
throw error;
|
|
2206
|
+
}
|
|
2207
|
+
};
|
|
2208
|
+
const takeRetainedOfferRuntimes = () => {
|
|
2209
|
+
const runtimes = [
|
|
2210
|
+
...[...retainedGenerationOffers.values()].flatMap((offer) => [
|
|
2211
|
+
...offer.runtimes,
|
|
2212
|
+
]),
|
|
2213
|
+
...retiredRetainedRuntimes.splice(0),
|
|
2214
|
+
];
|
|
2215
|
+
retainedGenerationOffers.clear();
|
|
2216
|
+
return runtimes;
|
|
2217
|
+
};
|
|
2218
|
+
const prepareRetainedGenerationOffers = async () => {
|
|
2219
|
+
if (rootFrame() !== undefined)
|
|
2220
|
+
return;
|
|
2221
|
+
for (const [rootPlaybookId, generation] of [...retainedGenerations].sort(([left], [right]) => left.localeCompare(right))) {
|
|
2222
|
+
if (retainedGenerationOffers.has(rootPlaybookId) ||
|
|
2223
|
+
ineligibleRetainedGenerations.has(rootPlaybookId)) {
|
|
2224
|
+
continue;
|
|
2225
|
+
}
|
|
2226
|
+
const runtimes = [];
|
|
2227
|
+
try {
|
|
2228
|
+
for (const sourceFrame of generation.frames) {
|
|
2229
|
+
const enablement = enablementById.get(sourceFrame.playbookId);
|
|
2230
|
+
runtimes.push(createRuntimeForEnablement(enablement, hostCapabilitiesById));
|
|
2231
|
+
}
|
|
2232
|
+
if (runtimes.some((runtime) => !runtimeRetainsGenerations(runtime))) {
|
|
2233
|
+
const rootRetainsGenerations = runtimeRetainsGenerations(runtimes[0]);
|
|
2234
|
+
await disposeRetainedRuntimeSet(runtimes, `/${enablementById.get(rootPlaybookId).command} retained-generation capability cleanup failed`);
|
|
2235
|
+
runtimes.splice(0);
|
|
2236
|
+
ineligibleRetainedGenerations.add(rootPlaybookId);
|
|
2237
|
+
if (!rootRetainsGenerations) {
|
|
2238
|
+
retainedGenerationRootClears.add(rootPlaybookId);
|
|
2239
|
+
}
|
|
2240
|
+
continue;
|
|
2241
|
+
}
|
|
2242
|
+
retainedGenerationOffers.set(rootPlaybookId, {
|
|
2243
|
+
generation,
|
|
2244
|
+
requiresEffectReconciliation: generation.retainedEffectReconciliation !== undefined ||
|
|
2245
|
+
generation.frames.some(({ runtime }) => runtime.retainedEffectReconciliation !== undefined) ||
|
|
2246
|
+
!retainedEffectLedgerCanRebase(generation.effectLedger, assertPlaybookEffectLedger(currentEffectLedger())),
|
|
2247
|
+
runtimes,
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
catch (error) {
|
|
2251
|
+
if (error instanceof RetainedRuntimeCleanupError) {
|
|
2252
|
+
retiredRetainedRuntimes.push(...error.failedRuntimes);
|
|
2253
|
+
terminallyDisposed = true;
|
|
2254
|
+
lifecycle = 'closed';
|
|
2255
|
+
throw error;
|
|
2256
|
+
}
|
|
2257
|
+
let cleanupError;
|
|
2258
|
+
if (runtimes.length > 0) {
|
|
2259
|
+
try {
|
|
2260
|
+
await disposeRetainedRuntimeSet(runtimes, 'retained-generation preparation cleanup failed');
|
|
2261
|
+
}
|
|
2262
|
+
catch (caught) {
|
|
2263
|
+
cleanupError = caught;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
if (cleanupError !== undefined) {
|
|
2267
|
+
if (cleanupError instanceof RetainedRuntimeCleanupError) {
|
|
2268
|
+
retiredRetainedRuntimes.push(...cleanupError.failedRuntimes);
|
|
2269
|
+
}
|
|
2270
|
+
terminallyDisposed = true;
|
|
2271
|
+
lifecycle = 'closed';
|
|
2272
|
+
throw new RetainedRuntimeCleanupError([error, cleanupError], 'retained-generation preparation and cleanup failed', cleanupError instanceof RetainedRuntimeCleanupError
|
|
2273
|
+
? cleanupError.failedRuntimes
|
|
2274
|
+
: []);
|
|
2275
|
+
}
|
|
2276
|
+
ineligibleRetainedGenerations.add(rootPlaybookId);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
1297
2280
|
const bindingFor = (frame, localRole) => {
|
|
1298
2281
|
const binding = frame.playerBindings.get(localRole);
|
|
1299
2282
|
if (!binding) {
|
|
@@ -1336,7 +2319,10 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1336
2319
|
...(leafFrame()?.state
|
|
1337
2320
|
? { latestSubRuntimeState: leafFrame().state }
|
|
1338
2321
|
: {}),
|
|
1339
|
-
...(
|
|
2322
|
+
...(retainedEffectReconciliation === undefined &&
|
|
2323
|
+
pendingBossQuestions !== undefined
|
|
2324
|
+
? { pendingBossQuestions }
|
|
2325
|
+
: {}),
|
|
1340
2326
|
...(lastError ? { lastError } : {}),
|
|
1341
2327
|
...(captainSessionId ? { captainSessionId } : {}),
|
|
1342
2328
|
// Presence only: the pinned token value never reaches telemetry
|
|
@@ -1491,14 +2477,66 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1491
2477
|
}
|
|
1492
2478
|
}
|
|
1493
2479
|
if (leafFrame() === frame) {
|
|
1494
|
-
pendingBossQuestions =
|
|
1495
|
-
record.pendingBossQuestions ?? record.pendingBossQuestion;
|
|
2480
|
+
pendingBossQuestions = mirroredBossQuestions(record.pendingBossQuestions ?? record.pendingBossQuestion);
|
|
1496
2481
|
lastError = normalizeErrorCompact(record.lastError);
|
|
1497
2482
|
if (state.quiescent && state.tags.includes('playbook.parked')) {
|
|
1498
2483
|
await setMode('engaged.parked', `sub-runtime:${state.stateId ?? 'structured'}`);
|
|
1499
2484
|
}
|
|
1500
2485
|
}
|
|
1501
2486
|
};
|
|
2487
|
+
const observeSummaryTrace = (frame, payload) => {
|
|
2488
|
+
const trace = payloadRecord(payload);
|
|
2489
|
+
const turn = activeTurn;
|
|
2490
|
+
const summary = activeTurnSummary;
|
|
2491
|
+
const expectedParentSessionId = frame.parent?.frame.sessionId;
|
|
2492
|
+
const expectedParentCallId = frame.parent?.callId;
|
|
2493
|
+
if (trace?.schemaVersion !== 4 ||
|
|
2494
|
+
trace.sessionId !== frame.sessionId ||
|
|
2495
|
+
trace.playbookId !== frame.entry.id ||
|
|
2496
|
+
trace.rootSessionId !== frame.rootSessionId ||
|
|
2497
|
+
(expectedParentSessionId === undefined
|
|
2498
|
+
? Object.hasOwn(trace, 'parentSessionId')
|
|
2499
|
+
: !Object.hasOwn(trace, 'parentSessionId') ||
|
|
2500
|
+
trace.parentSessionId !== expectedParentSessionId) ||
|
|
2501
|
+
(expectedParentCallId === undefined
|
|
2502
|
+
? Object.hasOwn(trace, 'parentCallId')
|
|
2503
|
+
: !Object.hasOwn(trace, 'parentCallId') ||
|
|
2504
|
+
trace.parentCallId !== expectedParentCallId) ||
|
|
2505
|
+
trace.depth !== frame.depth ||
|
|
2506
|
+
turn === undefined ||
|
|
2507
|
+
!Number.isSafeInteger(trace.turnId) ||
|
|
2508
|
+
trace.turnId <= 0 ||
|
|
2509
|
+
!Number.isSafeInteger(trace.sequence) ||
|
|
2510
|
+
trace.sequence <= 0 ||
|
|
2511
|
+
summary === undefined ||
|
|
2512
|
+
!summaryIncludes(frame)) {
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
if (trace.type !== 'outcome.accepted')
|
|
2516
|
+
return;
|
|
2517
|
+
const receipt = exactOwnDataRecord(trace.payload, [
|
|
2518
|
+
'source',
|
|
2519
|
+
'target',
|
|
2520
|
+
'acceptedOutcome',
|
|
2521
|
+
]);
|
|
2522
|
+
if (receipt === undefined ||
|
|
2523
|
+
typeof receipt.source !== 'string' ||
|
|
2524
|
+
receipt.source.trim().length === 0 ||
|
|
2525
|
+
typeof receipt.target !== 'string' ||
|
|
2526
|
+
receipt.target.trim().length === 0 ||
|
|
2527
|
+
typeof receipt.acceptedOutcome !== 'string' ||
|
|
2528
|
+
receipt.acceptedOutcome.trim().length === 0) {
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
const traceKey = `${frame.sessionId}:${trace.sequence}`;
|
|
2532
|
+
if (summary.acceptedOutcomeTraceKeys.has(traceKey))
|
|
2533
|
+
return;
|
|
2534
|
+
summary.acceptedOutcomeTraceKeys.add(traceKey);
|
|
2535
|
+
summary.counts.interruptions++;
|
|
2536
|
+
if (frame.entry.summaryPolicy?.copyPasteGuardNames.includes(receipt.acceptedOutcome)) {
|
|
2537
|
+
summary.counts.copyPastes++;
|
|
2538
|
+
}
|
|
2539
|
+
};
|
|
1502
2540
|
let callNestedPlaybook;
|
|
1503
2541
|
const createPorts = (frame) => ({
|
|
1504
2542
|
callPlayer: async (roleId, prompt, signal, options) => {
|
|
@@ -1660,14 +2698,6 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1660
2698
|
if (result.finalText === undefined) {
|
|
1661
2699
|
throw new Error('callCaptain returned status=ok with no finalText');
|
|
1662
2700
|
}
|
|
1663
|
-
const guard = guardFromJudgeReply(result.finalText);
|
|
1664
|
-
const summary = activeTurnSummary;
|
|
1665
|
-
if (guard &&
|
|
1666
|
-
summary &&
|
|
1667
|
-
summaryIncludes(frame) &&
|
|
1668
|
-
frame.entry.summaryPolicy?.copyPasteGuardNames.includes(guard)) {
|
|
1669
|
-
summary.counts.copyPastes++;
|
|
1670
|
-
}
|
|
1671
2701
|
return result.finalText;
|
|
1672
2702
|
},
|
|
1673
2703
|
callPlaybook: (request, signal) => {
|
|
@@ -1700,6 +2730,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1700
2730
|
await mirrorSubRuntimeTelemetry(frame, event.payload);
|
|
1701
2731
|
}
|
|
1702
2732
|
await requireSession().emitTelemetry(event);
|
|
2733
|
+
if (event.topic === 'playbook.trace') {
|
|
2734
|
+
observeSummaryTrace(frame, event.payload);
|
|
2735
|
+
}
|
|
1703
2736
|
})();
|
|
1704
2737
|
return trackHostCall(frame, emission);
|
|
1705
2738
|
},
|
|
@@ -1721,17 +2754,42 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1721
2754
|
throw new VisibilityControlError(error);
|
|
1722
2755
|
}
|
|
1723
2756
|
};
|
|
1724
|
-
const
|
|
2757
|
+
const generatedSessionId = () => {
|
|
1725
2758
|
const sessionId = createSessionId();
|
|
1726
2759
|
if (!UUID_PATTERN.test(sessionId)) {
|
|
1727
2760
|
throw new Error(`playbook session id generator returned a non-UUID value: ${JSON.stringify(sessionId)}`);
|
|
1728
2761
|
}
|
|
2762
|
+
return sessionId;
|
|
2763
|
+
};
|
|
2764
|
+
const allocateSessionId = () => {
|
|
2765
|
+
const sessionId = generatedSessionId();
|
|
1729
2766
|
if (issuedSessionIds.has(sessionId)) {
|
|
1730
2767
|
throw new Error(`playbook session id collision: ${sessionId}`);
|
|
1731
2768
|
}
|
|
1732
2769
|
issuedSessionIds.add(sessionId);
|
|
1733
2770
|
return sessionId;
|
|
1734
2771
|
};
|
|
2772
|
+
const allocateAdoptionSessionIds = (count, sourceSessionIds) => {
|
|
2773
|
+
const candidates = [];
|
|
2774
|
+
const rejectedSourceIds = new Set();
|
|
2775
|
+
while (candidates.length < count) {
|
|
2776
|
+
const candidate = generatedSessionId();
|
|
2777
|
+
if (sourceSessionIds.has(candidate)) {
|
|
2778
|
+
if (rejectedSourceIds.has(candidate)) {
|
|
2779
|
+
throw new Error(`playbook source session id collision: ${candidate}`);
|
|
2780
|
+
}
|
|
2781
|
+
rejectedSourceIds.add(candidate);
|
|
2782
|
+
continue;
|
|
2783
|
+
}
|
|
2784
|
+
if (issuedSessionIds.has(candidate) || candidates.includes(candidate)) {
|
|
2785
|
+
throw new Error(`playbook session id collision: ${candidate}`);
|
|
2786
|
+
}
|
|
2787
|
+
candidates.push(candidate);
|
|
2788
|
+
}
|
|
2789
|
+
for (const candidate of candidates)
|
|
2790
|
+
issuedSessionIds.add(candidate);
|
|
2791
|
+
return candidates;
|
|
2792
|
+
};
|
|
1735
2793
|
const normalizeErrorFull = (value) => {
|
|
1736
2794
|
const compact = normalizeErrorCompact(value) ?? {
|
|
1737
2795
|
name: 'Error',
|
|
@@ -1751,7 +2809,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1751
2809
|
const entry = enablement.entry;
|
|
1752
2810
|
const sessionId = allocateSessionId();
|
|
1753
2811
|
const playerBindings = makePlayerBindings(enablement);
|
|
1754
|
-
const runtime =
|
|
2812
|
+
const runtime = createRuntimeForEnablement(enablement, hostCapabilitiesById);
|
|
1755
2813
|
return {
|
|
1756
2814
|
entry,
|
|
1757
2815
|
enablement,
|
|
@@ -1767,7 +2825,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1767
2825
|
const makeRestoredFrame = (enablement, snapshot, parent) => {
|
|
1768
2826
|
const entry = enablement.entry;
|
|
1769
2827
|
const playerBindings = makePlayerBindings(enablement);
|
|
1770
|
-
const runtime =
|
|
2828
|
+
const runtime = createRuntimeForEnablement(enablement, hostCapabilitiesById);
|
|
1771
2829
|
return {
|
|
1772
2830
|
entry,
|
|
1773
2831
|
enablement,
|
|
@@ -1817,14 +2875,6 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1817
2875
|
delete ledger.resumeToken;
|
|
1818
2876
|
else
|
|
1819
2877
|
ledger.resumeToken = resumeToken;
|
|
1820
|
-
// CAPTAIN-20: a result counts only after the runtime validated it and
|
|
1821
|
-
// atomically published its authorized continuation transition.
|
|
1822
|
-
const summary = activeTurnSummary;
|
|
1823
|
-
if (pending.status === 'ok' &&
|
|
1824
|
-
summary &&
|
|
1825
|
-
summaryIncludes(frame)) {
|
|
1826
|
-
summary.counts.interruptions++;
|
|
1827
|
-
}
|
|
1828
2878
|
}
|
|
1829
2879
|
finally {
|
|
1830
2880
|
playerTransactions.delete(binding.playerId);
|
|
@@ -2074,6 +3124,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2074
3124
|
}
|
|
2075
3125
|
}
|
|
2076
3126
|
clearLeafLedger();
|
|
3127
|
+
if (frames.length === 0)
|
|
3128
|
+
retainedEffectReconciliation = undefined;
|
|
2077
3129
|
if (failures.length === 1)
|
|
2078
3130
|
throw failures[0];
|
|
2079
3131
|
if (failures.length > 1) {
|
|
@@ -2178,7 +3230,46 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2178
3230
|
throw new AggregateError(failures, 'playbook stack disposal failed');
|
|
2179
3231
|
}
|
|
2180
3232
|
};
|
|
3233
|
+
/**
|
|
3234
|
+
* DR-040 task 11: abandonment is a host settlement, not an authored FSM
|
|
3235
|
+
* result. Freeze the bounded evidence while the complete stack and its
|
|
3236
|
+
* runtime-owned envelope identities still exist, durably fence recovery,
|
|
3237
|
+
* dispose leaf-to-root without resuming a parent, then publish the matching
|
|
3238
|
+
* root clear and evidence as one durable completion before the controller
|
|
3239
|
+
* may return an executed receipt to its result phase.
|
|
3240
|
+
*/
|
|
3241
|
+
const settleUnresolvedEffectAbandonment = async (unresolvedLeaf) => {
|
|
3242
|
+
if (leafFrame() !== unresolvedLeaf) {
|
|
3243
|
+
throw new Error('unresolved-effect abandonment requires the active leaf');
|
|
3244
|
+
}
|
|
3245
|
+
const root = rootFrame();
|
|
3246
|
+
if (root === undefined) {
|
|
3247
|
+
throw new Error('unresolved-effect abandonment requires an active root');
|
|
3248
|
+
}
|
|
3249
|
+
const unresolvedEffects = freezeTurnUnresolvedEffects();
|
|
3250
|
+
if (unresolvedEffects.length === 0) {
|
|
3251
|
+
throw new Error('unresolved-effect abandonment requires nonempty effect evidence');
|
|
3252
|
+
}
|
|
3253
|
+
if (unresolvedEffectSettlement === undefined) {
|
|
3254
|
+
throw new Error('unresolved-effect abandonment requires durable host settlement');
|
|
3255
|
+
}
|
|
3256
|
+
const rootPlaybookId = root.entry.id;
|
|
3257
|
+
const settlement = Object.freeze({
|
|
3258
|
+
rootPlaybookId,
|
|
3259
|
+
unresolvedEffects,
|
|
3260
|
+
});
|
|
3261
|
+
await runEffect(() => unresolvedEffectSettlement.begin(settlement));
|
|
3262
|
+
await runEffect(() => disposeStack('unresolved-effect'));
|
|
3263
|
+
pendingRetentionUpdates.set(rootPlaybookId, {
|
|
3264
|
+
kind: 'clear',
|
|
3265
|
+
rootPlaybookId,
|
|
3266
|
+
});
|
|
3267
|
+
await runEffect(() => unresolvedEffectSettlement.complete(settlement));
|
|
3268
|
+
};
|
|
2181
3269
|
const callResultFor = (frame, result) => {
|
|
3270
|
+
if (result.outcome === 'unresolved-effect') {
|
|
3271
|
+
throw new Error(`playbook ${frame.entry.id} unresolved-effect result cannot resume a parent`);
|
|
3272
|
+
}
|
|
2182
3273
|
if (result.outcome === 'terminal') {
|
|
2183
3274
|
return {
|
|
2184
3275
|
status: 'ok',
|
|
@@ -2213,6 +3304,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2213
3304
|
if (leafFrame() !== frame) {
|
|
2214
3305
|
throw new Error('only the active leaf may receive Boss input');
|
|
2215
3306
|
}
|
|
3307
|
+
if (retainedEffectReconciliation !== undefined) {
|
|
3308
|
+
throw new Error('retained repository-effect reconciliation is required before Boss input');
|
|
3309
|
+
}
|
|
2216
3310
|
// CAPTAIN-35: the leaf check, the visibility request, and the mode change
|
|
2217
3311
|
// are shell control work performed on the way to the runtime, not the
|
|
2218
3312
|
// effect. Only the call below is the effect, so only it is inside the
|
|
@@ -2230,6 +3324,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2230
3324
|
if (!parentLink)
|
|
2231
3325
|
throw new Error('root playbook has no caller');
|
|
2232
3326
|
const parent = parentLink.frame;
|
|
3327
|
+
if (retainedEffectReconciliation !== undefined) {
|
|
3328
|
+
throw new Error('retained repository-effect reconciliation is required before parent resumption');
|
|
3329
|
+
}
|
|
2233
3330
|
const invocationSignal = child.invocationSignal;
|
|
2234
3331
|
let effectiveResult = callResult;
|
|
2235
3332
|
let ownsReturn = false;
|
|
@@ -2298,6 +3395,16 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2298
3395
|
}, context);
|
|
2299
3396
|
}
|
|
2300
3397
|
async function processFrameResult(frame, result, context) {
|
|
3398
|
+
if (result.outcome === 'unresolved-effect') {
|
|
3399
|
+
// Task 10 exposes the runtime-owned abandonment signal without
|
|
3400
|
+
// translating it into a nested result or claiming workflow completion.
|
|
3401
|
+
// Task 11 owns the durable host settlement and complete-stack disposal.
|
|
3402
|
+
assertRetainableResult(frame, result);
|
|
3403
|
+
if (leafFrame() === frame) {
|
|
3404
|
+
await setMode('engaged.parked', 'turn:unresolved-effect');
|
|
3405
|
+
}
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
2301
3408
|
if (result.outcome === 'terminal') {
|
|
2302
3409
|
if (frame.parent) {
|
|
2303
3410
|
await resumeParent(frame, callResultFor(frame, result), context);
|
|
@@ -2308,7 +3415,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2308
3415
|
// runtime publishes before disposal removes the frame. The opaque run
|
|
2309
3416
|
// output remains runtime-to-runtime data and never becomes Captain
|
|
2310
3417
|
// evidence (CAPPLAY-10).
|
|
2311
|
-
activeTurn?.settlementFacts.push(rootCompletionFact(frame));
|
|
3418
|
+
activeTurn?.settlementFacts.push(rootCompletionFact(frame, result));
|
|
3419
|
+
recordTerminalRetention(frame, result);
|
|
2312
3420
|
await runEffect(() => disposeStack('final'));
|
|
2313
3421
|
}
|
|
2314
3422
|
return;
|
|
@@ -2488,7 +3596,14 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2488
3596
|
const policy = frame.entry.summaryPolicy;
|
|
2489
3597
|
const counts = { interruptions: 0, copyPastes: 0 };
|
|
2490
3598
|
const stateCounts = new Map();
|
|
2491
|
-
activeTurnSummary = policy
|
|
3599
|
+
activeTurnSummary = policy
|
|
3600
|
+
? {
|
|
3601
|
+
owner: frame,
|
|
3602
|
+
counts,
|
|
3603
|
+
stateCounts,
|
|
3604
|
+
acceptedOutcomeTraceKeys: new Set(),
|
|
3605
|
+
}
|
|
3606
|
+
: undefined;
|
|
2492
3607
|
let result;
|
|
2493
3608
|
let error;
|
|
2494
3609
|
try {
|
|
@@ -2553,12 +3668,58 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2553
3668
|
...entries.map(([key, value]) => digestLine `- ${key}: ${JSON.stringify(value)}`),
|
|
2554
3669
|
];
|
|
2555
3670
|
};
|
|
3671
|
+
const retainedResumptionDigest = () => {
|
|
3672
|
+
if (rootFrame() !== undefined) {
|
|
3673
|
+
return 'Retained resumptions: unavailable while a playbook is engaged.';
|
|
3674
|
+
}
|
|
3675
|
+
const offers = [...retainedGenerationOffers].sort(([left], [right]) => left.localeCompare(right));
|
|
3676
|
+
if (offers.length === 0)
|
|
3677
|
+
return 'Retained resumptions: none.';
|
|
3678
|
+
const lines = ['Retained resumptions:'];
|
|
3679
|
+
for (const [rootPlaybookId, offer] of offers) {
|
|
3680
|
+
const enablement = enablementById.get(rootPlaybookId);
|
|
3681
|
+
lines.push(digestLine `- ${rootPlaybookId} (/${enablement.command}): ${offer.generation.rootStateDescription ??
|
|
3682
|
+
'(no published root-state description was retained)'}`);
|
|
3683
|
+
}
|
|
3684
|
+
return lines.join('\n');
|
|
3685
|
+
};
|
|
2556
3686
|
const controlViewDigest = () => {
|
|
2557
3687
|
const leaf = leafFrame();
|
|
2558
3688
|
const lines = [digestLine `Active path: ${activePathDigest()}`];
|
|
2559
3689
|
if (!leaf) {
|
|
2560
3690
|
lines.push('The shell is idle: no leaf state, no pending question.');
|
|
2561
3691
|
lines.push('Advertised actions: none.');
|
|
3692
|
+
lines.push(retainedResumptionDigest());
|
|
3693
|
+
return lines.join('\n');
|
|
3694
|
+
}
|
|
3695
|
+
refreshRetainedEffectFence();
|
|
3696
|
+
if (retainedEffectReconciliation !== undefined) {
|
|
3697
|
+
let reconciliationActions = [];
|
|
3698
|
+
if (typeof leaf.runtime.describe === 'function' &&
|
|
3699
|
+
typeof leaf.runtime.apply === 'function') {
|
|
3700
|
+
try {
|
|
3701
|
+
reconciliationActions = leaf.runtime
|
|
3702
|
+
.describe()
|
|
3703
|
+
.actions.filter(({ id }) => id === UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID ||
|
|
3704
|
+
id === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID);
|
|
3705
|
+
}
|
|
3706
|
+
catch {
|
|
3707
|
+
// An unreadable control surface cannot open a fail-closed fence.
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
for (const action of reconciliationActions) {
|
|
3711
|
+
recordSuppliedIdentifier(action.id);
|
|
3712
|
+
}
|
|
3713
|
+
lines.push(`Leaf ${frameLabel(leaf)} is parked for repository-effect reconciliation.`);
|
|
3714
|
+
lines.push('Pending Boss questions: withheld until reconciliation.');
|
|
3715
|
+
lines.push(reconciliationActions.length === 0
|
|
3716
|
+
? 'Advertised actions: none.'
|
|
3717
|
+
: [
|
|
3718
|
+
'Advertised actions:',
|
|
3719
|
+
...reconciliationActions.map((action) => digestLine `- ${action.id}: ${action.label}`),
|
|
3720
|
+
].join('\n'));
|
|
3721
|
+
lines.push('Ordinary delivery, switching, dismissal, and runtime actions are unavailable while retained effect evidence is unresolved. Only the advertised unresolved-effect controls may run. Conversation is unaffected: `respond` stays valid for any turn.');
|
|
3722
|
+
lines.push(retainedResumptionDigest());
|
|
2562
3723
|
return lines.join('\n');
|
|
2563
3724
|
}
|
|
2564
3725
|
let view;
|
|
@@ -2607,6 +3768,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2607
3768
|
lines.push(describeFailure === undefined
|
|
2608
3769
|
? 'This leaf advertises no runtime action, so plain text delivery is the only machine verb against it and a `runtime` selection is invalid. Conversation is unaffected: `respond` stays valid for any turn.'
|
|
2609
3770
|
: 'No runtime action can be validated while the control view is unreadable, so plain text delivery is the only machine verb against it this turn and a `runtime` selection is invalid. Conversation is unaffected: `respond` stays valid for any turn.');
|
|
3771
|
+
lines.push(retainedResumptionDigest());
|
|
2610
3772
|
return lines.join('\n');
|
|
2611
3773
|
}
|
|
2612
3774
|
// CAPTAIN-9: the guarded set is what the digest supplies *for selection* —
|
|
@@ -2645,6 +3807,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2645
3807
|
'Advertised actions:',
|
|
2646
3808
|
...view.actions.map((action) => digestLine `- ${action.id}: ${action.label}`),
|
|
2647
3809
|
].join('\n'));
|
|
3810
|
+
lines.push(retainedResumptionDigest());
|
|
2648
3811
|
return lines.join('\n');
|
|
2649
3812
|
};
|
|
2650
3813
|
// The catalog is registry-authored, not shell-authored: an id, a command,
|
|
@@ -2720,9 +3883,13 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2720
3883
|
// all of this prose. Preserve the exact attempt before crossing the
|
|
2721
3884
|
// boundary; the uncertainty record below keeps recovery from pretending
|
|
2722
3885
|
// delivery was confirmed while still understanding a Boss follow-up.
|
|
2723
|
-
|
|
3886
|
+
const suffix = turn?.mandatoryPresentationSuffix;
|
|
3887
|
+
const visibleText = suffix === undefined || settlement.text.includes(suffix)
|
|
3888
|
+
? settlement.text
|
|
3889
|
+
: `${settlement.text.trimEnd()}\n\n${suffix}`;
|
|
3890
|
+
appendJournal('reply', visibleText);
|
|
2724
3891
|
try {
|
|
2725
|
-
await trackTurnCall(settlement.context.emitReply(
|
|
3892
|
+
await trackTurnCall(settlement.context.emitReply(visibleText));
|
|
2726
3893
|
}
|
|
2727
3894
|
catch (error) {
|
|
2728
3895
|
conversation = { kind: 'needsSeeding' };
|
|
@@ -2862,8 +4029,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2862
4029
|
}
|
|
2863
4030
|
};
|
|
2864
4031
|
/**
|
|
2865
|
-
* CAPTAIN-35: the one wrapper an effect runs through — a runtime driven
|
|
2866
|
-
* engagement constructed, a stack disposed, an advertised
|
|
4032
|
+
* CAPTAIN-35: the one wrapper an effect runs through — a runtime driven or
|
|
4033
|
+
* adopted, an engagement constructed, a stack disposed, an advertised
|
|
4034
|
+
* action applied.
|
|
2867
4035
|
* Attribution is recorded here, at the operation that threw, and nowhere
|
|
2868
4036
|
* else: an error acquires the mark by escaping this call, so no later
|
|
2869
4037
|
* failure can inherit it.
|
|
@@ -2876,7 +4044,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2876
4044
|
* instead of settling, so a misfiling costs the Boss their only settlement.
|
|
2877
4045
|
*
|
|
2878
4046
|
* Neither can a boundary drawn around a *region* of the turn. `operation` is
|
|
2879
|
-
* therefore always one call expression naming one of those
|
|
4047
|
+
* therefore always one call expression naming one of those five operations,
|
|
2880
4048
|
* never a closure that also performs the shell work leading to it: the leaf
|
|
2881
4049
|
* check, the visibility request, the mode change, and the processing of what
|
|
2882
4050
|
* the runtime returned are all shell control work, and a boundary wide
|
|
@@ -3076,7 +4244,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3076
4244
|
...(kind === 'closingReply' && turn?.report
|
|
3077
4245
|
? [
|
|
3078
4246
|
labeledBlock('ControlView digest', controlViewDigest()),
|
|
3079
|
-
outcomeReportBlock(turn.report),
|
|
4247
|
+
outcomeReportBlock(turn.report, turn.unresolvedEffects ?? []),
|
|
3080
4248
|
]
|
|
3081
4249
|
: []),
|
|
3082
4250
|
...(options.proseRejection === undefined
|
|
@@ -3200,10 +4368,10 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3200
4368
|
// -------------------------------------------------------------------------
|
|
3201
4369
|
// The controller port (DR-029): host validation is the sole effector.
|
|
3202
4370
|
// -------------------------------------------------------------------------
|
|
3203
|
-
// The
|
|
3204
|
-
// same way the digest reads it.
|
|
3205
|
-
//
|
|
3206
|
-
//
|
|
4371
|
+
// The legacy state-description channel, read from the live control view the
|
|
4372
|
+
// same way the digest reads it. DR-037 makes the terminal result authoritative
|
|
4373
|
+
// for completion; this remains only for an older runtime that omits the new
|
|
4374
|
+
// optional member.
|
|
3207
4375
|
const leafStateDescription = (frame) => {
|
|
3208
4376
|
if (typeof frame.runtime.describe !== 'function')
|
|
3209
4377
|
return undefined;
|
|
@@ -3214,9 +4382,16 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3214
4382
|
return undefined;
|
|
3215
4383
|
}
|
|
3216
4384
|
};
|
|
3217
|
-
const rootCompletionFact = (frame) => {
|
|
3218
|
-
const
|
|
3219
|
-
|
|
4385
|
+
const rootCompletionFact = (frame, result) => {
|
|
4386
|
+
const returned = result.stateDescription === undefined
|
|
4387
|
+
? ''
|
|
4388
|
+
: compactEvidence(result.stateDescription);
|
|
4389
|
+
const legacy = returned === '' ? leafStateDescription(frame) : undefined;
|
|
4390
|
+
const description = returned !== ''
|
|
4391
|
+
? returned
|
|
4392
|
+
: legacy === undefined
|
|
4393
|
+
? ''
|
|
4394
|
+
: compactEvidence(legacy);
|
|
3220
4395
|
return description === ''
|
|
3221
4396
|
? `${frameLabel(frame)} completed; its runtime published no result description.`
|
|
3222
4397
|
: `${frameLabel(frame)} completed; its runtime-published result meaning was ${quoteEvidence(description)}.`;
|
|
@@ -3225,6 +4400,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3225
4400
|
const leaf = leafFrame();
|
|
3226
4401
|
if (!leaf)
|
|
3227
4402
|
return 'idle: no playbook is engaged';
|
|
4403
|
+
if (retainedEffectReconciliation !== undefined) {
|
|
4404
|
+
return `${frameLabel(leaf)} parked for repository-effect reconciliation`;
|
|
4405
|
+
}
|
|
3228
4406
|
if (!leaf.state)
|
|
3229
4407
|
return `${frameLabel(leaf)} engaged`;
|
|
3230
4408
|
return `${frameLabel(leaf)} at ${stateDigestLine(leaf.state, leafStateDescription(leaf))}`;
|
|
@@ -3271,6 +4449,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3271
4449
|
if (!root)
|
|
3272
4450
|
return false;
|
|
3273
4451
|
const label = frameLabel(root);
|
|
4452
|
+
// Dismissal leaves the procedure unfinished. Persist the latest safe
|
|
4453
|
+
// generation captured for this turn before disposal erases the frames.
|
|
4454
|
+
retainOrClearDisposedRoot(root);
|
|
3274
4455
|
try {
|
|
3275
4456
|
await runEffect(() => disposeStack('dismiss'));
|
|
3276
4457
|
facts.push(`Dismissed the ${label} engagement.`);
|
|
@@ -3317,6 +4498,133 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3317
4498
|
}
|
|
3318
4499
|
return { frame, report: outcome.report, failed: false };
|
|
3319
4500
|
};
|
|
4501
|
+
const adoptRetainedGeneration = async (rootPlaybookId, offer) => {
|
|
4502
|
+
const generation = offer.generation;
|
|
4503
|
+
const currentLedger = assertPlaybookEffectLedger(currentEffectLedger());
|
|
4504
|
+
const requiresEffectReconciliation = offer.requiresEffectReconciliation ||
|
|
4505
|
+
!retainedEffectLedgerCanRebase(generation.effectLedger, currentLedger);
|
|
4506
|
+
const sourceSessionIds = new Set(generation.frames.map((frame) => frame.sessionId));
|
|
4507
|
+
const targetSessionIds = allocateAdoptionSessionIds(generation.frames.length, sourceSessionIds);
|
|
4508
|
+
const targetRootSessionId = targetSessionIds[0];
|
|
4509
|
+
const adoptedFrames = [];
|
|
4510
|
+
for (const [index, sourceFrame] of generation.frames.entries()) {
|
|
4511
|
+
const enablement = enablementById.get(sourceFrame.playbookId);
|
|
4512
|
+
const parent = adoptedFrames.at(-1);
|
|
4513
|
+
adoptedFrames.push({
|
|
4514
|
+
entry: enablement.entry,
|
|
4515
|
+
enablement,
|
|
4516
|
+
runtime: offer.runtimes[index],
|
|
4517
|
+
sessionId: targetSessionIds[index],
|
|
4518
|
+
rootSessionId: targetRootSessionId,
|
|
4519
|
+
depth: index,
|
|
4520
|
+
playerBindings: makePlayerBindings(enablement),
|
|
4521
|
+
...(parent
|
|
4522
|
+
? { parent: { frame: parent, callId: 'playbook-1' } }
|
|
4523
|
+
: {}),
|
|
4524
|
+
state: sourceFrame.runtime.state,
|
|
4525
|
+
inFlightHostCalls: new Set(),
|
|
4526
|
+
});
|
|
4527
|
+
}
|
|
4528
|
+
retainedGenerationOffers.delete(rootPlaybookId);
|
|
4529
|
+
let installed = false;
|
|
4530
|
+
try {
|
|
4531
|
+
for (const [index, frame] of adoptedFrames.entries()) {
|
|
4532
|
+
const sourceFrame = generation.frames[index];
|
|
4533
|
+
const targetChild = adoptedFrames[index + 1];
|
|
4534
|
+
await runEffect(() => frame.runtime.adopt(frameSession(frame), sourceFrame.runtime, {
|
|
4535
|
+
sourceSessionId: sourceFrame.sessionId,
|
|
4536
|
+
sourceGenerationId: generation.frames[0].rootSessionId,
|
|
4537
|
+
...(targetChild === undefined
|
|
4538
|
+
? {}
|
|
4539
|
+
: { targetChildSessionId: targetChild.sessionId }),
|
|
4540
|
+
}));
|
|
4541
|
+
}
|
|
4542
|
+
frames.push(...adoptedFrames);
|
|
4543
|
+
installed = true;
|
|
4544
|
+
retainedEffectReconciliation = requiresEffectReconciliation
|
|
4545
|
+
? {
|
|
4546
|
+
sourceGenerationId: generation.retainedEffectReconciliation?.sourceGenerationId ??
|
|
4547
|
+
generation.frames[0].runtime.retainedEffectSourceSessionId ??
|
|
4548
|
+
generation.frames[0].rootSessionId,
|
|
4549
|
+
checkpoint: generation.effectLedger,
|
|
4550
|
+
}
|
|
4551
|
+
: undefined;
|
|
4552
|
+
for (const parent of adoptedFrames.slice(0, -1)) {
|
|
4553
|
+
pendingChildParents.add(parent);
|
|
4554
|
+
}
|
|
4555
|
+
const retainedQuestions = generation.frames.at(-1).runtime
|
|
4556
|
+
.pendingBossQuestions;
|
|
4557
|
+
pendingBossQuestions =
|
|
4558
|
+
requiresEffectReconciliation || retainedQuestions.length === 0
|
|
4559
|
+
? undefined
|
|
4560
|
+
: mirroredBossQuestions(retainedQuestions);
|
|
4561
|
+
lastError = undefined;
|
|
4562
|
+
retainedGenerations.delete(rootPlaybookId);
|
|
4563
|
+
ineligibleRetainedGenerations.delete(rootPlaybookId);
|
|
4564
|
+
await setMode('engaged.parked', 'resume', rootPlaybookId, targetRootSessionId);
|
|
4565
|
+
if (requiresEffectReconciliation) {
|
|
4566
|
+
await requireSession().setVisiblePlayers([]);
|
|
4567
|
+
}
|
|
4568
|
+
else {
|
|
4569
|
+
await requestVisibility(adoptedFrames.at(-1));
|
|
4570
|
+
}
|
|
4571
|
+
await requireSession().emitStatus(`◇ /${enablementById.get(rootPlaybookId).command} resumed`);
|
|
4572
|
+
if (activeTurn) {
|
|
4573
|
+
appendMandatoryPresentationSuffix(activeTurn, requiresEffectReconciliation
|
|
4574
|
+
? 'The retained work remains parked until its repository-effect evidence is reconciled.'
|
|
4575
|
+
: RESUMPTION_DUPLICATE_EFFECT_WARNING);
|
|
4576
|
+
}
|
|
4577
|
+
return [
|
|
4578
|
+
generation.rootStateDescription === undefined
|
|
4579
|
+
? `Resumed /${enablementById.get(rootPlaybookId).command} from its retained state; no published root-state description was retained.`
|
|
4580
|
+
: `Resumed /${enablementById.get(rootPlaybookId).command} from the retained state described as ${quoteEvidence(compactEvidence(generation.rootStateDescription))}.`,
|
|
4581
|
+
requiresEffectReconciliation
|
|
4582
|
+
? 'The retained work remains parked until its repository-effect evidence is reconciled; no ordinary action was resumed.'
|
|
4583
|
+
: RESUMPTION_DUPLICATE_EFFECT_WARNING,
|
|
4584
|
+
];
|
|
4585
|
+
}
|
|
4586
|
+
catch (error) {
|
|
4587
|
+
if (installed) {
|
|
4588
|
+
frames.splice(0);
|
|
4589
|
+
pendingChildParents.clear();
|
|
4590
|
+
retainedEffectReconciliation = undefined;
|
|
4591
|
+
clearLeafLedger();
|
|
4592
|
+
}
|
|
4593
|
+
const cleanupFailures = [];
|
|
4594
|
+
const failedCleanupRuntimes = [];
|
|
4595
|
+
for (const frame of [...adoptedFrames].reverse()) {
|
|
4596
|
+
try {
|
|
4597
|
+
await disposeFrame(frame);
|
|
4598
|
+
}
|
|
4599
|
+
catch (cleanupError) {
|
|
4600
|
+
cleanupFailures.push(cleanupError);
|
|
4601
|
+
failedCleanupRuntimes.push(frame.runtime);
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4604
|
+
const rollbackFailures = [];
|
|
4605
|
+
if (installed) {
|
|
4606
|
+
try {
|
|
4607
|
+
await setMode('chat', 'resume.failed');
|
|
4608
|
+
}
|
|
4609
|
+
catch (rollbackError) {
|
|
4610
|
+
rollbackFailures.push(rollbackError);
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
else {
|
|
4614
|
+
mode = 'chat';
|
|
4615
|
+
}
|
|
4616
|
+
if (cleanupFailures.length > 0 || rollbackFailures.length > 0) {
|
|
4617
|
+
retiredRetainedRuntimes.push(...failedCleanupRuntimes);
|
|
4618
|
+
ineligibleRetainedGenerations.add(rootPlaybookId);
|
|
4619
|
+
terminallyDisposed = true;
|
|
4620
|
+
lifecycle = 'closed';
|
|
4621
|
+
throw new AggregateError([error, ...cleanupFailures, ...rollbackFailures], 'retained-generation adoption and rollback failed');
|
|
4622
|
+
}
|
|
4623
|
+
retainedGenerations.set(rootPlaybookId, generation);
|
|
4624
|
+
ineligibleRetainedGenerations.delete(rootPlaybookId);
|
|
4625
|
+
throw error;
|
|
4626
|
+
}
|
|
4627
|
+
};
|
|
3320
4628
|
const driveAndProcess = async (frame, text, context, onDriven) => {
|
|
3321
4629
|
try {
|
|
3322
4630
|
// CAPTAIN-35: no boundary here. `driveFrame` marks the runtime call and
|
|
@@ -3343,11 +4651,36 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3343
4651
|
}
|
|
3344
4652
|
}
|
|
3345
4653
|
};
|
|
4654
|
+
const containsEffectThrow = (turn, error) => {
|
|
4655
|
+
if (turn.effectThrows.has(error))
|
|
4656
|
+
return true;
|
|
4657
|
+
return (error instanceof AggregateError &&
|
|
4658
|
+
error.errors.some((nested) => containsEffectThrow(turn, nested)));
|
|
4659
|
+
};
|
|
3346
4660
|
const settleSelection = async (selection, signal) => {
|
|
3347
4661
|
const turn = activeTurn;
|
|
3348
4662
|
runFailureFacts = [];
|
|
4663
|
+
let frozenUnresolvedEffects;
|
|
4664
|
+
const freezeControllerEvidence = () => {
|
|
4665
|
+
frozenUnresolvedEffects ??= freezeTurnUnresolvedEffects();
|
|
4666
|
+
const report = unresolvedEffectBossReport(frozenUnresolvedEffects);
|
|
4667
|
+
if (turn !== undefined && report !== undefined) {
|
|
4668
|
+
appendMandatoryPresentationSuffix(turn, report);
|
|
4669
|
+
}
|
|
4670
|
+
return frozenUnresolvedEffects;
|
|
4671
|
+
};
|
|
4672
|
+
const finalizeSettlement = (settlement) => Object.freeze({
|
|
4673
|
+
...settlement,
|
|
4674
|
+
unresolvedEffects: freezeControllerEvidence(),
|
|
4675
|
+
});
|
|
3349
4676
|
try {
|
|
3350
|
-
|
|
4677
|
+
// `respond` has no result phase: freeze its no-effect projection before
|
|
4678
|
+
// its decision-call prose crosses the presentation boundary. Acting
|
|
4679
|
+
// selections freeze after their work and before reporting begins.
|
|
4680
|
+
if (selection.action === 'respond')
|
|
4681
|
+
freezeControllerEvidence();
|
|
4682
|
+
const settlement = await executeSelection(selection, signal);
|
|
4683
|
+
return finalizeSettlement(settlement);
|
|
3351
4684
|
}
|
|
3352
4685
|
catch (error) {
|
|
3353
4686
|
if (turn?.presentationError === error)
|
|
@@ -3371,7 +4704,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3371
4704
|
turn.settlementFacts.push(...runFailureFacts.splice(0));
|
|
3372
4705
|
}
|
|
3373
4706
|
const mayHaveApplied = selection.action !== 'respond' &&
|
|
3374
|
-
turn
|
|
4707
|
+
containsEffectThrow(turn, error);
|
|
3375
4708
|
turn.settlementFacts.push(mayHaveApplied
|
|
3376
4709
|
? `The ${selection.action} action failed before its complete outcome could be confirmed and may have changed the session: ${normalized.name}: ${compactEvidence(normalized.message)}. It was not repeated automatically.`
|
|
3377
4710
|
: `The ${selection.action} action failed before its complete outcome could be confirmed: ${normalized.name}: ${compactEvidence(normalized.message)}. It was not repeated automatically.`);
|
|
@@ -3406,7 +4739,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3406
4739
|
};
|
|
3407
4740
|
turn.settled = true;
|
|
3408
4741
|
lastSettlementStatus = 'failed';
|
|
3409
|
-
|
|
4742
|
+
const settlement = {
|
|
3410
4743
|
status: 'failed',
|
|
3411
4744
|
facts: [...turn.settlementFacts],
|
|
3412
4745
|
...(turn.report.receipt === undefined
|
|
@@ -3414,6 +4747,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3414
4747
|
: { receipt: turn.report.receipt }),
|
|
3415
4748
|
...(summary === undefined ? {} : { leafStateSummary: summary }),
|
|
3416
4749
|
};
|
|
4750
|
+
return finalizeSettlement(settlement);
|
|
3417
4751
|
}
|
|
3418
4752
|
finally {
|
|
3419
4753
|
runFailureFacts = undefined;
|
|
@@ -3471,6 +4805,46 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3471
4805
|
: { leafStateSummary: leafStateSummary() }),
|
|
3472
4806
|
};
|
|
3473
4807
|
}
|
|
4808
|
+
refreshRetainedEffectFence();
|
|
4809
|
+
const fencedLeaf = leafFrame();
|
|
4810
|
+
const routesRetainedReconciliation = selection.action === 'runtime' &&
|
|
4811
|
+
(selection.actionId === UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID ||
|
|
4812
|
+
selection.actionId === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID) &&
|
|
4813
|
+
fencedLeaf !== undefined;
|
|
4814
|
+
if (retainedEffectReconciliation !== undefined &&
|
|
4815
|
+
!routesRetainedReconciliation) {
|
|
4816
|
+
return rejectSelection(selection, 'retained work must reconcile its repository-effect evidence before an ordinary action can run');
|
|
4817
|
+
}
|
|
4818
|
+
if (selection.action === 'resume') {
|
|
4819
|
+
const entry = byId.get(selection.playbookId);
|
|
4820
|
+
if (entry === undefined) {
|
|
4821
|
+
return rejectSelection(selection, `"${selection.playbookId}" is not an enabled playbook`);
|
|
4822
|
+
}
|
|
4823
|
+
if (rootFrame() !== undefined) {
|
|
4824
|
+
return rejectSelection(selection, 'a playbook is already engaged; its live actions take precedence');
|
|
4825
|
+
}
|
|
4826
|
+
const offer = retainedGenerationOffers.get(entry.id);
|
|
4827
|
+
if (offer === undefined) {
|
|
4828
|
+
return rejectSelection(selection, `/${enablementById.get(entry.id).command} has no resumable retained generation`);
|
|
4829
|
+
}
|
|
4830
|
+
turn.settled = true;
|
|
4831
|
+
journalAction({ action: 'resume', playbookId: entry.id });
|
|
4832
|
+
facts.push(...(await adoptRetainedGeneration(entry.id, offer)));
|
|
4833
|
+
const summary = leafStateSummary();
|
|
4834
|
+
turn.report = {
|
|
4835
|
+
...emptyReport(),
|
|
4836
|
+
facts,
|
|
4837
|
+
status: 'ok',
|
|
4838
|
+
...(summary === undefined ? {} : { leafStateSummary: summary }),
|
|
4839
|
+
};
|
|
4840
|
+
journalOutcome([...facts]);
|
|
4841
|
+
lastSettlementStatus = 'ok';
|
|
4842
|
+
return {
|
|
4843
|
+
status: 'ok',
|
|
4844
|
+
facts: [...facts],
|
|
4845
|
+
...(summary === undefined ? {} : { leafStateSummary: summary }),
|
|
4846
|
+
};
|
|
4847
|
+
}
|
|
3474
4848
|
if (selection.action === 'start' || selection.action === 'switch') {
|
|
3475
4849
|
const entry = byId.get(selection.playbookId);
|
|
3476
4850
|
if (!entry) {
|
|
@@ -3692,6 +5066,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3692
5066
|
if (outcome.error !== undefined)
|
|
3693
5067
|
throw outcome.error;
|
|
3694
5068
|
const receipt = outcome.result;
|
|
5069
|
+
refreshRetainedEffectFence();
|
|
3695
5070
|
let status = receipt.disposition === 'executed'
|
|
3696
5071
|
? 'ok'
|
|
3697
5072
|
: receipt.disposition === 'rejected'
|
|
@@ -3711,9 +5086,47 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3711
5086
|
}
|
|
3712
5087
|
: {}),
|
|
3713
5088
|
};
|
|
5089
|
+
const unresolvedAbandonment = receipt.disposition === 'executed' &&
|
|
5090
|
+
actionId === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID;
|
|
3714
5091
|
if (receipt.disposition === 'executed') {
|
|
3715
|
-
|
|
5092
|
+
if (unresolvedAbandonment &&
|
|
5093
|
+
receipt.run?.outcome !== 'unresolved-effect') {
|
|
5094
|
+
throw new Error('unresolved-effect abandonment returned no unresolved-effect result');
|
|
5095
|
+
}
|
|
3716
5096
|
const establishedSummary = leafStateSummary();
|
|
5097
|
+
if (unresolvedAbandonment) {
|
|
5098
|
+
try {
|
|
5099
|
+
await settleUnresolvedEffectAbandonment(leaf);
|
|
5100
|
+
}
|
|
5101
|
+
catch (error) {
|
|
5102
|
+
abandonmentSettlementUnsafe = true;
|
|
5103
|
+
const normalized = normalizeErrorCompact(error) ?? {
|
|
5104
|
+
name: 'Error',
|
|
5105
|
+
message: String(error),
|
|
5106
|
+
};
|
|
5107
|
+
// The runtime action was accepted, but its distinct host-level
|
|
5108
|
+
// settlement did not complete. Do not expose an `executed` control
|
|
5109
|
+
// receipt until both disposal and durable publication have
|
|
5110
|
+
// succeeded.
|
|
5111
|
+
turn.report = {
|
|
5112
|
+
...outcome.report,
|
|
5113
|
+
facts: [...facts],
|
|
5114
|
+
bossFacts: facts.map((fact) => fact
|
|
5115
|
+
.split(`"${actionId}"`)
|
|
5116
|
+
.join(`"${compactEvidence(actionLabel)}"`)),
|
|
5117
|
+
status: 'failed',
|
|
5118
|
+
receipt: {
|
|
5119
|
+
disposition: 'failed',
|
|
5120
|
+
error: normalized,
|
|
5121
|
+
},
|
|
5122
|
+
...(establishedSummary === undefined
|
|
5123
|
+
? {}
|
|
5124
|
+
: { leafStateSummary: establishedSummary }),
|
|
5125
|
+
};
|
|
5126
|
+
throw error;
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
facts.push(`Applied "${actionId}" on ${frameLabel(leaf)}.`);
|
|
3717
5130
|
// Execution is now proven. Preserve that receipt and the counts already
|
|
3718
5131
|
// collected before processing the returned run, because disposal,
|
|
3719
5132
|
// telemetry, or parent resumption can still fail afterward.
|
|
@@ -3729,7 +5142,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3729
5142
|
? {}
|
|
3730
5143
|
: { leafStateSummary: establishedSummary }),
|
|
3731
5144
|
};
|
|
3732
|
-
if (
|
|
5145
|
+
if (!unresolvedAbandonment &&
|
|
5146
|
+
receipt.run !== undefined &&
|
|
5147
|
+
retainedEffectReconciliation === undefined) {
|
|
3733
5148
|
// The same rule as the drive path: processing the run the receipt
|
|
3734
5149
|
// carried is not itself an effect, and the resume or disposal it may
|
|
3735
5150
|
// perform is marked where it happens (CAPTAIN-35).
|
|
@@ -3925,6 +5340,25 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3925
5340
|
if (!isDeepStrictEqual(frame.roleBindings, configuredBindings)) {
|
|
3926
5341
|
throw new TypeError(`Captain shell snapshot frame ${JSON.stringify(frame.playbookId)} role bindings changed`);
|
|
3927
5342
|
}
|
|
5343
|
+
if (!isDeepStrictEqual(frame.runtime.effectLedger, snapshot.effectLedger)) {
|
|
5344
|
+
throw new TypeError(`Captain shell snapshot frame ${JSON.stringify(frame.playbookId)} effect ledger does not match its artifact schema`);
|
|
5345
|
+
}
|
|
5346
|
+
const runtimeReconciliation = frame.runtime.retainedEffectReconciliation;
|
|
5347
|
+
if (snapshot.retainedEffectReconciliation === undefined) {
|
|
5348
|
+
if (runtimeReconciliation !== undefined) {
|
|
5349
|
+
throw new TypeError(`Captain shell snapshot frame ${JSON.stringify(frame.playbookId)} carries an unmirrored retained-effect fence`);
|
|
5350
|
+
}
|
|
5351
|
+
}
|
|
5352
|
+
else if (runtimeReconciliation === undefined ||
|
|
5353
|
+
!isDeepStrictEqual(runtimeReconciliation.checkpoint, snapshot.retainedEffectReconciliation.checkpoint)) {
|
|
5354
|
+
throw new TypeError(`Captain shell snapshot frame ${JSON.stringify(frame.playbookId)} does not mirror the root retained-effect fence`);
|
|
5355
|
+
}
|
|
5356
|
+
if (frame.depth === 0 &&
|
|
5357
|
+
snapshot.retainedEffectReconciliation !== undefined &&
|
|
5358
|
+
runtimeReconciliation?.sourceSessionId !==
|
|
5359
|
+
snapshot.retainedEffectReconciliation.sourceGenerationId) {
|
|
5360
|
+
throw new TypeError('Captain shell snapshot retained-effect root source identity differs from its generation');
|
|
5361
|
+
}
|
|
3928
5362
|
}
|
|
3929
5363
|
};
|
|
3930
5364
|
const safeCapturePoint = () => {
|
|
@@ -3979,33 +5413,33 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3979
5413
|
frame.abortListener !== undefined)));
|
|
3980
5414
|
});
|
|
3981
5415
|
};
|
|
3982
|
-
const
|
|
3983
|
-
|
|
3984
|
-
!captainRuntime ||
|
|
3985
|
-
!captainSessionId ||
|
|
3986
|
-
!captainAgent) {
|
|
3987
|
-
return undefined;
|
|
3988
|
-
}
|
|
5416
|
+
const captureFrameSnapshots = (requireRecordedState) => {
|
|
5417
|
+
const captured = [];
|
|
3989
5418
|
try {
|
|
3990
|
-
|
|
3991
|
-
typeof captainRuntime.restore !== 'function') {
|
|
3992
|
-
return undefined;
|
|
3993
|
-
}
|
|
3994
|
-
const captainSnapshot = captainRuntime.exportSnapshot();
|
|
3995
|
-
if (captainSnapshot === undefined)
|
|
3996
|
-
return undefined;
|
|
3997
|
-
const frameSnapshots = [];
|
|
3998
|
-
for (const frame of frames) {
|
|
5419
|
+
for (const [index, frame] of frames.entries()) {
|
|
3999
5420
|
if (typeof frame.runtime.exportSnapshot !== 'function' ||
|
|
4000
5421
|
typeof frame.runtime.restore !== 'function') {
|
|
4001
5422
|
return undefined;
|
|
4002
5423
|
}
|
|
4003
|
-
const
|
|
4004
|
-
if (
|
|
4005
|
-
|
|
5424
|
+
const exported = frame.runtime.exportSnapshot();
|
|
5425
|
+
if (exported === undefined)
|
|
5426
|
+
return undefined;
|
|
5427
|
+
const runtime = assertPlaybookRuntimeSnapshot(exported, frame.entry.id, { allowSuspendedCall: true });
|
|
5428
|
+
if (runtime.state.status !== 'active' ||
|
|
5429
|
+
!runtime.state.quiescent ||
|
|
5430
|
+
(requireRecordedState && frame.state === undefined) ||
|
|
5431
|
+
(frame.state !== undefined &&
|
|
5432
|
+
!isDeepStrictEqual(frame.state, runtime.state))) {
|
|
5433
|
+
return undefined;
|
|
5434
|
+
}
|
|
5435
|
+
const isLeaf = index === frames.length - 1;
|
|
5436
|
+
if ((isLeaf &&
|
|
5437
|
+
(runtime.suspendedCall !== undefined ||
|
|
5438
|
+
!runtime.state.tags.includes('playbook.parked'))) ||
|
|
5439
|
+
(!isLeaf && runtime.suspendedCall === undefined)) {
|
|
4006
5440
|
return undefined;
|
|
4007
5441
|
}
|
|
4008
|
-
|
|
5442
|
+
captured.push({
|
|
4009
5443
|
playbookId: frame.entry.id,
|
|
4010
5444
|
sessionId: frame.sessionId,
|
|
4011
5445
|
rootSessionId: frame.rootSessionId,
|
|
@@ -4024,8 +5458,198 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4024
5458
|
runtime,
|
|
4025
5459
|
});
|
|
4026
5460
|
}
|
|
5461
|
+
for (let index = 1; index < captured.length; index += 1) {
|
|
5462
|
+
const parent = captured[index - 1];
|
|
5463
|
+
const child = captured[index];
|
|
5464
|
+
if (child.parentSessionId !== parent.sessionId ||
|
|
5465
|
+
child.parentCallId === undefined ||
|
|
5466
|
+
parent.runtime.suspendedCall?.callId !== child.parentCallId ||
|
|
5467
|
+
parent.runtime.suspendedCall.playbookId !== child.playbookId ||
|
|
5468
|
+
parent.runtime.suspendedCall.childSessionId !== child.sessionId) {
|
|
5469
|
+
return undefined;
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5472
|
+
return captured;
|
|
5473
|
+
}
|
|
5474
|
+
catch {
|
|
5475
|
+
return undefined;
|
|
5476
|
+
}
|
|
5477
|
+
};
|
|
5478
|
+
const refreshRetainedEffectFence = (capturedFrames, capturedLedger) => {
|
|
5479
|
+
const fence = retainedEffectReconciliation;
|
|
5480
|
+
if (fence === undefined)
|
|
5481
|
+
return;
|
|
5482
|
+
try {
|
|
5483
|
+
const ledger = capturedLedger ?? assertPlaybookEffectLedger(currentEffectLedger());
|
|
5484
|
+
if (!retainedEffectLedgerCanRebase(fence.checkpoint, ledger)) {
|
|
5485
|
+
return;
|
|
5486
|
+
}
|
|
5487
|
+
const snapshots = capturedFrames ?? captureFrameSnapshots(true);
|
|
5488
|
+
if (snapshots === undefined || snapshots.length !== frames.length)
|
|
5489
|
+
return;
|
|
5490
|
+
for (const frameSnapshot of snapshots) {
|
|
5491
|
+
const runtime = frameSnapshot.runtime;
|
|
5492
|
+
if (!isDeepStrictEqual(runtime.effectLedger, ledger) ||
|
|
5493
|
+
runtime.retainedEffectSourceSessionId === undefined ||
|
|
5494
|
+
runtime.retainedEffectReconciliation !== undefined) {
|
|
5495
|
+
return;
|
|
5496
|
+
}
|
|
5497
|
+
}
|
|
5498
|
+
const retainedQuestions = snapshots.at(-1).runtime.pendingBossQuestions;
|
|
5499
|
+
const restoredQuestions = retainedQuestions.length === 0
|
|
5500
|
+
? undefined
|
|
5501
|
+
: mirroredBossQuestions(retainedQuestions);
|
|
5502
|
+
pendingBossQuestions = restoredQuestions;
|
|
5503
|
+
retainedEffectReconciliation = undefined;
|
|
5504
|
+
}
|
|
5505
|
+
catch {
|
|
5506
|
+
// Any host-ledger or runtime-snapshot defect leaves the root fence shut.
|
|
5507
|
+
}
|
|
5508
|
+
};
|
|
5509
|
+
const rootStateDescriptionForRetention = (root, retainedState) => {
|
|
5510
|
+
if (typeof root.runtime.describe !== 'function')
|
|
5511
|
+
return undefined;
|
|
5512
|
+
try {
|
|
5513
|
+
const view = root.runtime.describe();
|
|
5514
|
+
if (!isDeepStrictEqual(view.state, retainedState))
|
|
5515
|
+
return undefined;
|
|
5516
|
+
return typeof view.stateDescription === 'string' &&
|
|
5517
|
+
view.stateDescription.trim().length > 0
|
|
5518
|
+
? view.stateDescription
|
|
5519
|
+
: undefined;
|
|
5520
|
+
}
|
|
5521
|
+
catch {
|
|
5522
|
+
return undefined;
|
|
5523
|
+
}
|
|
5524
|
+
};
|
|
5525
|
+
const retainedGenerationFromFrames = (root, frameSnapshots) => {
|
|
5526
|
+
const rootStateDescription = rootStateDescriptionForRetention(root, frameSnapshots[0].runtime.state);
|
|
5527
|
+
const checkpoint = retainedEffectReconciliation?.checkpoint ??
|
|
5528
|
+
assertPlaybookEffectLedger(currentEffectLedger());
|
|
5529
|
+
if (checkpoint.boundaries.some(({ physicalReceipt }) => physicalReceipt === undefined)) {
|
|
5530
|
+
throw new TypeError('Captain retained-generation checkpoint contains an incomplete physical boundary');
|
|
5531
|
+
}
|
|
5532
|
+
return snapshotJsonValue({
|
|
5533
|
+
effectLedger: checkpoint,
|
|
5534
|
+
frames: frameSnapshots,
|
|
5535
|
+
...(retainedEffectReconciliation === undefined
|
|
5536
|
+
? {}
|
|
5537
|
+
: {
|
|
5538
|
+
retainedEffectReconciliation: {
|
|
5539
|
+
sourceGenerationId: retainedEffectReconciliation.sourceGenerationId,
|
|
5540
|
+
},
|
|
5541
|
+
}),
|
|
5542
|
+
...(rootStateDescription === undefined
|
|
5543
|
+
? {}
|
|
5544
|
+
: { rootStateDescription }),
|
|
5545
|
+
}, 'Captain retained generation');
|
|
5546
|
+
};
|
|
5547
|
+
const captureRetainedGeneration = () => {
|
|
5548
|
+
const root = rootFrame();
|
|
5549
|
+
if (!root ||
|
|
5550
|
+
frames.some((frame) => !runtimeRetainsGenerations(frame.runtime))) {
|
|
5551
|
+
return undefined;
|
|
5552
|
+
}
|
|
5553
|
+
const frameSnapshots = captureFrameSnapshots(true);
|
|
5554
|
+
if (frameSnapshots === undefined || frameSnapshots.length === 0) {
|
|
5555
|
+
return undefined;
|
|
5556
|
+
}
|
|
5557
|
+
return retainedGenerationFromFrames(root, frameSnapshots);
|
|
5558
|
+
};
|
|
5559
|
+
const rememberRetainedGeneration = () => {
|
|
5560
|
+
const root = rootFrame();
|
|
5561
|
+
if (!root)
|
|
5562
|
+
return;
|
|
5563
|
+
if (frames.some((frame) => !runtimeRetainsGenerations(frame.runtime))) {
|
|
5564
|
+
retainedGenerationCandidates.set(root.entry.id, {
|
|
5565
|
+
status: 'incapable',
|
|
5566
|
+
});
|
|
5567
|
+
return;
|
|
5568
|
+
}
|
|
5569
|
+
const generation = captureRetainedGeneration();
|
|
5570
|
+
retainedGenerationCandidates.set(root.entry.id, generation === undefined
|
|
5571
|
+
? { status: 'unsafe' }
|
|
5572
|
+
: { status: 'captured', generation });
|
|
5573
|
+
};
|
|
5574
|
+
const retentionUpdateForPriorGeneration = (root) => {
|
|
5575
|
+
const rootPlaybookId = root.entry.id;
|
|
5576
|
+
if (!runtimeRetainsGenerations(root.runtime)) {
|
|
5577
|
+
return {
|
|
5578
|
+
kind: 'clear',
|
|
5579
|
+
rootPlaybookId,
|
|
5580
|
+
};
|
|
5581
|
+
}
|
|
5582
|
+
const candidate = retainedGenerationCandidates.get(rootPlaybookId);
|
|
5583
|
+
if (candidate?.status === 'incapable') {
|
|
5584
|
+
return undefined;
|
|
5585
|
+
}
|
|
5586
|
+
if (candidate?.status !== 'captured') {
|
|
5587
|
+
throw new Error(`${frameLabel(root)} could not capture its pre-terminal retained generation`);
|
|
5588
|
+
}
|
|
5589
|
+
return {
|
|
5590
|
+
kind: 'retain',
|
|
5591
|
+
rootPlaybookId,
|
|
5592
|
+
generation: candidate.generation,
|
|
5593
|
+
};
|
|
5594
|
+
};
|
|
5595
|
+
const retainOrClearDisposedRoot = (root) => {
|
|
5596
|
+
const update = retentionUpdateForPriorGeneration(root);
|
|
5597
|
+
if (update !== undefined) {
|
|
5598
|
+
pendingRetentionUpdates.set(update.rootPlaybookId, update);
|
|
5599
|
+
}
|
|
5600
|
+
};
|
|
5601
|
+
const recordTerminalRetention = (root, result) => {
|
|
5602
|
+
const rootPlaybookId = root.entry.id;
|
|
5603
|
+
if (!runtimeRetainsGenerations(root.runtime)) {
|
|
5604
|
+
pendingRetentionUpdates.set(rootPlaybookId, {
|
|
5605
|
+
kind: 'clear',
|
|
5606
|
+
rootPlaybookId,
|
|
5607
|
+
});
|
|
5608
|
+
return;
|
|
5609
|
+
}
|
|
5610
|
+
const terminalStateId = result.state.stateId;
|
|
5611
|
+
if (typeof terminalStateId !== 'string' ||
|
|
5612
|
+
terminalStateId.trim().length === 0) {
|
|
5613
|
+
throw new Error(`${frameLabel(root)} terminal result has no stable state id for retention`);
|
|
5614
|
+
}
|
|
5615
|
+
const unfinished = root.runtime.retainedGenerationMetadata.unfinishedFinalStateIds.includes(terminalStateId);
|
|
5616
|
+
if (unfinished) {
|
|
5617
|
+
// A root opened and terminated within this turn has no pre-turn,
|
|
5618
|
+
// work-bearing generation. Leave any earlier store entry untouched.
|
|
5619
|
+
if (!retainedGenerationCandidates.has(rootPlaybookId))
|
|
5620
|
+
return;
|
|
5621
|
+
retainOrClearDisposedRoot(root);
|
|
5622
|
+
}
|
|
5623
|
+
else {
|
|
5624
|
+
pendingRetentionUpdates.set(rootPlaybookId, {
|
|
5625
|
+
kind: 'clear',
|
|
5626
|
+
rootPlaybookId,
|
|
5627
|
+
});
|
|
5628
|
+
}
|
|
5629
|
+
};
|
|
5630
|
+
const exportShellSnapshot = () => {
|
|
5631
|
+
if (!safeCapturePoint() ||
|
|
5632
|
+
!captainRuntime ||
|
|
5633
|
+
!captainSessionId ||
|
|
5634
|
+
!captainAgent) {
|
|
5635
|
+
return undefined;
|
|
5636
|
+
}
|
|
5637
|
+
try {
|
|
5638
|
+
if (typeof captainRuntime.exportSnapshot !== 'function' ||
|
|
5639
|
+
typeof captainRuntime.restore !== 'function') {
|
|
5640
|
+
return undefined;
|
|
5641
|
+
}
|
|
5642
|
+
const captainSnapshot = captainRuntime.exportSnapshot();
|
|
5643
|
+
if (captainSnapshot === undefined)
|
|
5644
|
+
return undefined;
|
|
5645
|
+
const frameSnapshots = captureFrameSnapshots(true);
|
|
5646
|
+
if (frameSnapshots === undefined)
|
|
5647
|
+
return undefined;
|
|
5648
|
+
const effectLedger = assertPlaybookEffectLedger(currentEffectLedger());
|
|
5649
|
+
refreshRetainedEffectFence(frameSnapshots, effectLedger);
|
|
4027
5650
|
const common = {
|
|
4028
|
-
schemaVersion:
|
|
5651
|
+
schemaVersion: 4,
|
|
5652
|
+
effectLedger,
|
|
4029
5653
|
captain: {
|
|
4030
5654
|
sessionId: captainSessionId,
|
|
4031
5655
|
runtime: captainSnapshot,
|
|
@@ -4047,6 +5671,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4047
5671
|
...common,
|
|
4048
5672
|
mode: 'engaged.parked',
|
|
4049
5673
|
frames: frameSnapshots,
|
|
5674
|
+
...(retainedEffectReconciliation === undefined
|
|
5675
|
+
? {}
|
|
5676
|
+
: { retainedEffectReconciliation }),
|
|
4050
5677
|
...(pendingBossQuestions === undefined
|
|
4051
5678
|
? {}
|
|
4052
5679
|
: { pendingBossQuestions: pendingBossQuestions }),
|
|
@@ -4060,6 +5687,64 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4060
5687
|
return undefined;
|
|
4061
5688
|
}
|
|
4062
5689
|
};
|
|
5690
|
+
const exportSettlement = () => {
|
|
5691
|
+
if (!retentionSettlementReady || abandonmentSettlementUnsafe) {
|
|
5692
|
+
return undefined;
|
|
5693
|
+
}
|
|
5694
|
+
const snapshot = exportShellSnapshot();
|
|
5695
|
+
if (snapshot === undefined)
|
|
5696
|
+
return undefined;
|
|
5697
|
+
let unresolvedEffects;
|
|
5698
|
+
try {
|
|
5699
|
+
unresolvedEffects =
|
|
5700
|
+
settledTurnUnresolvedEffects ?? currentUnresolvedEffects();
|
|
5701
|
+
}
|
|
5702
|
+
catch {
|
|
5703
|
+
return undefined;
|
|
5704
|
+
}
|
|
5705
|
+
const updates = new Map(pendingRetentionUpdates);
|
|
5706
|
+
for (const rootPlaybookId of retainedGenerationRootClears) {
|
|
5707
|
+
updates.set(rootPlaybookId, { kind: 'clear', rootPlaybookId });
|
|
5708
|
+
}
|
|
5709
|
+
const root = rootFrame();
|
|
5710
|
+
if (root !== undefined) {
|
|
5711
|
+
const rootPlaybookId = root.entry.id;
|
|
5712
|
+
if (frames.every((frame) => runtimeRetainsGenerations(frame.runtime))) {
|
|
5713
|
+
const generation = snapshot.mode === 'engaged.parked'
|
|
5714
|
+
? retainedGenerationFromFrames(root, snapshot.frames)
|
|
5715
|
+
: undefined;
|
|
5716
|
+
if (generation !== undefined) {
|
|
5717
|
+
updates.set(rootPlaybookId, {
|
|
5718
|
+
kind: 'retain',
|
|
5719
|
+
rootPlaybookId,
|
|
5720
|
+
generation,
|
|
5721
|
+
});
|
|
5722
|
+
}
|
|
5723
|
+
}
|
|
5724
|
+
else if (!runtimeRetainsGenerations(root.runtime)) {
|
|
5725
|
+
updates.set(rootPlaybookId, { kind: 'clear', rootPlaybookId });
|
|
5726
|
+
}
|
|
5727
|
+
else if (retainedGenerationCandidates.has(rootPlaybookId)) {
|
|
5728
|
+
try {
|
|
5729
|
+
const update = retentionUpdateForPriorGeneration(root);
|
|
5730
|
+
if (update !== undefined) {
|
|
5731
|
+
updates.set(rootPlaybookId, update);
|
|
5732
|
+
}
|
|
5733
|
+
}
|
|
5734
|
+
catch {
|
|
5735
|
+
return undefined;
|
|
5736
|
+
}
|
|
5737
|
+
}
|
|
5738
|
+
}
|
|
5739
|
+
for (const update of updates.values()) {
|
|
5740
|
+
applyRetentionUpdateToCatalog(update);
|
|
5741
|
+
}
|
|
5742
|
+
return snapshotJsonValue({
|
|
5743
|
+
snapshot,
|
|
5744
|
+
retentionUpdates: [...updates.values()].sort((left, right) => left.rootPlaybookId.localeCompare(right.rootPlaybookId)),
|
|
5745
|
+
unresolvedEffects,
|
|
5746
|
+
}, 'Captain settlement');
|
|
5747
|
+
};
|
|
4063
5748
|
const verifyRestoredRuntime = (runtime, expected, playbookId, allowSuspendedCall) => {
|
|
4064
5749
|
const actual = runtime.exportSnapshot?.();
|
|
4065
5750
|
if (actual === undefined) {
|
|
@@ -4072,6 +5757,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4072
5757
|
'sequences',
|
|
4073
5758
|
'pendingBossQuestions',
|
|
4074
5759
|
'suspendedCall',
|
|
5760
|
+
'effectLedger',
|
|
5761
|
+
'retainedEffectSourceSessionId',
|
|
5762
|
+
'retainedEffectReconciliation',
|
|
4075
5763
|
]) {
|
|
4076
5764
|
if (!isDeepStrictEqual(normalized[key], expected[key])) {
|
|
4077
5765
|
throw new Error(`restored ${playbookId} runtime changed snapshot field ${key}`);
|
|
@@ -4089,6 +5777,15 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4089
5777
|
cleanupFailures.push(error);
|
|
4090
5778
|
}
|
|
4091
5779
|
}
|
|
5780
|
+
const retainedRuntimes = takeRetainedOfferRuntimes();
|
|
5781
|
+
if (retainedRuntimes.length > 0) {
|
|
5782
|
+
try {
|
|
5783
|
+
await disposeRetainedRuntimeSet(retainedRuntimes, 'retained-generation restore cleanup failed');
|
|
5784
|
+
}
|
|
5785
|
+
catch (error) {
|
|
5786
|
+
cleanupFailures.push(error);
|
|
5787
|
+
}
|
|
5788
|
+
}
|
|
4092
5789
|
if (captainRuntime) {
|
|
4093
5790
|
shuttingDown = true;
|
|
4094
5791
|
try {
|
|
@@ -4106,11 +5803,20 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4106
5803
|
byCommand = new Map();
|
|
4107
5804
|
byId = new Map();
|
|
4108
5805
|
enablementById = new Map();
|
|
5806
|
+
hostCapabilitiesById = new Map();
|
|
5807
|
+
pendingHostCapabilities = undefined;
|
|
5808
|
+
currentEffectLedger = () => emptyPlaybookEffectLedger();
|
|
4109
5809
|
captainAgent = undefined;
|
|
4110
5810
|
captainAdapter = undefined;
|
|
4111
5811
|
playerAgents = new Map();
|
|
4112
5812
|
playerLedger.clear();
|
|
4113
5813
|
playerTransactions.clear();
|
|
5814
|
+
retainedGenerations.clear();
|
|
5815
|
+
ineligibleRetainedGenerations.clear();
|
|
5816
|
+
retainedGenerationRootClears.clear();
|
|
5817
|
+
retainedGenerationsInstalled = false;
|
|
5818
|
+
retainedGenerationInstallationInProgress = false;
|
|
5819
|
+
retainedGenerationInstallationClosed = false;
|
|
4114
5820
|
session = undefined;
|
|
4115
5821
|
sessionEmissionsOpen = false;
|
|
4116
5822
|
closedGateAttempted = false;
|
|
@@ -4118,6 +5824,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4118
5824
|
captainSessionId = undefined;
|
|
4119
5825
|
conversation = { kind: 'unopened' };
|
|
4120
5826
|
mode = 'chat';
|
|
5827
|
+
retainedEffectReconciliation = undefined;
|
|
4121
5828
|
pendingBossQuestions = undefined;
|
|
4122
5829
|
lastError = undefined;
|
|
4123
5830
|
journalSeq = 0;
|
|
@@ -4144,7 +5851,13 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4144
5851
|
lifecycle = 'restoring';
|
|
4145
5852
|
try {
|
|
4146
5853
|
const snapshot = assertPlaybookCaptainShellSnapshot(untrusted);
|
|
4147
|
-
const built = await
|
|
5854
|
+
const built = await buildCurrentEnablements();
|
|
5855
|
+
const builtHostCapabilities = new Map(built.hostCapabilitiesById);
|
|
5856
|
+
const readEffectLedger = () => effectLedgerMirrorFromCapabilities(builtHostCapabilities);
|
|
5857
|
+
const hostLedger = readEffectLedger();
|
|
5858
|
+
if (!isDeepStrictEqual(snapshot.effectLedger, hostLedger)) {
|
|
5859
|
+
throw new Error('Captain shell restore effect ledger does not match current-host authority');
|
|
5860
|
+
}
|
|
4148
5861
|
captainAgent = built.captainAgent;
|
|
4149
5862
|
captainAdapter = captainAgent.adapter;
|
|
4150
5863
|
playerAgents = built.playerAgents;
|
|
@@ -4154,6 +5867,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4154
5867
|
byCommand = built.byCommand;
|
|
4155
5868
|
byId = built.byId;
|
|
4156
5869
|
enablementById = built.enablementById;
|
|
5870
|
+
hostCapabilitiesById = builtHostCapabilities;
|
|
5871
|
+
currentEffectLedger = readEffectLedger;
|
|
4157
5872
|
for (const [playerId, saved] of Object.entries(snapshot.playerSessions)) {
|
|
4158
5873
|
playerLedger.set(playerId, {
|
|
4159
5874
|
adapter: saved.adapter,
|
|
@@ -4233,6 +5948,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4233
5948
|
lastSettlementStatus = snapshot.lastSettlementStatus;
|
|
4234
5949
|
mode = snapshot.mode;
|
|
4235
5950
|
if (snapshot.mode === 'engaged.parked') {
|
|
5951
|
+
retainedEffectReconciliation =
|
|
5952
|
+
snapshot.retainedEffectReconciliation;
|
|
4236
5953
|
pendingBossQuestions = snapshot.pendingBossQuestions;
|
|
4237
5954
|
lastError = snapshot.lastError;
|
|
4238
5955
|
}
|
|
@@ -4248,6 +5965,57 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4248
5965
|
throw error;
|
|
4249
5966
|
}
|
|
4250
5967
|
};
|
|
5968
|
+
const installRetainedGenerations = async (generations) => {
|
|
5969
|
+
if (lifecycle !== 'ready' || terminallyDisposed) {
|
|
5970
|
+
throw new Error('retained generations require an initialized or restored Captain shell');
|
|
5971
|
+
}
|
|
5972
|
+
if (retainedGenerationsInstalled ||
|
|
5973
|
+
retainedGenerationInstallationInProgress ||
|
|
5974
|
+
retainedGenerationInstallationClosed ||
|
|
5975
|
+
activeTurnHostCalls !== undefined) {
|
|
5976
|
+
throw new Error('retained generations may be installed exactly once before the first nonempty Boss turn');
|
|
5977
|
+
}
|
|
5978
|
+
const normalized = normalizeInstalledRetainedGenerations(generations);
|
|
5979
|
+
retainedGenerationInstallationInProgress = true;
|
|
5980
|
+
try {
|
|
5981
|
+
retainedGenerations.clear();
|
|
5982
|
+
retainedGenerationOffers.clear();
|
|
5983
|
+
ineligibleRetainedGenerations.clear();
|
|
5984
|
+
retainedGenerationRootClears.clear();
|
|
5985
|
+
for (const [rootPlaybookId, generation] of normalized) {
|
|
5986
|
+
retainedGenerations.set(rootPlaybookId, generation);
|
|
5987
|
+
}
|
|
5988
|
+
await prepareRetainedGenerationOffers();
|
|
5989
|
+
retainedGenerationsInstalled = true;
|
|
5990
|
+
}
|
|
5991
|
+
catch (error) {
|
|
5992
|
+
const preparationCleanupFailed = error instanceof RetainedRuntimeCleanupError;
|
|
5993
|
+
const runtimes = [...retainedGenerationOffers.values()].flatMap((offer) => [...offer.runtimes]);
|
|
5994
|
+
retainedGenerationOffers.clear();
|
|
5995
|
+
retainedGenerations.clear();
|
|
5996
|
+
ineligibleRetainedGenerations.clear();
|
|
5997
|
+
retainedGenerationRootClears.clear();
|
|
5998
|
+
try {
|
|
5999
|
+
await disposeRetainedRuntimeSet(runtimes, 'retained-generation installation cleanup failed');
|
|
6000
|
+
}
|
|
6001
|
+
catch (cleanupError) {
|
|
6002
|
+
if (cleanupError instanceof RetainedRuntimeCleanupError) {
|
|
6003
|
+
retiredRetainedRuntimes.push(...cleanupError.failedRuntimes);
|
|
6004
|
+
}
|
|
6005
|
+
terminallyDisposed = true;
|
|
6006
|
+
lifecycle = 'closed';
|
|
6007
|
+
throw new AggregateError([error, cleanupError], 'retained-generation installation and cleanup failed');
|
|
6008
|
+
}
|
|
6009
|
+
if (preparationCleanupFailed) {
|
|
6010
|
+
terminallyDisposed = true;
|
|
6011
|
+
lifecycle = 'closed';
|
|
6012
|
+
}
|
|
6013
|
+
throw error;
|
|
6014
|
+
}
|
|
6015
|
+
finally {
|
|
6016
|
+
retainedGenerationInstallationInProgress = false;
|
|
6017
|
+
}
|
|
6018
|
+
};
|
|
4251
6019
|
return {
|
|
4252
6020
|
async init(initSession) {
|
|
4253
6021
|
if (lifecycle !== 'fresh' || terminallyDisposed) {
|
|
@@ -4259,11 +6027,16 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4259
6027
|
lifecycle = 'initializing';
|
|
4260
6028
|
try {
|
|
4261
6029
|
installSession(initSession, true);
|
|
4262
|
-
const built = await
|
|
6030
|
+
const built = await buildCurrentEnablements();
|
|
6031
|
+
const builtHostCapabilities = new Map(built.hostCapabilitiesById);
|
|
6032
|
+
const readEffectLedger = () => effectLedgerMirrorFromCapabilities(builtHostCapabilities);
|
|
6033
|
+
readEffectLedger();
|
|
4263
6034
|
entries = built.entries;
|
|
4264
6035
|
byCommand = built.byCommand;
|
|
4265
6036
|
byId = built.byId;
|
|
4266
6037
|
enablementById = built.enablementById;
|
|
6038
|
+
hostCapabilitiesById = builtHostCapabilities;
|
|
6039
|
+
currentEffectLedger = readEffectLedger;
|
|
4267
6040
|
captainAgent = built.captainAgent;
|
|
4268
6041
|
captainAdapter = captainAgent.adapter;
|
|
4269
6042
|
playerAgents = built.playerAgents;
|
|
@@ -4288,7 +6061,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4288
6061
|
}
|
|
4289
6062
|
},
|
|
4290
6063
|
exportSnapshot: exportShellSnapshot,
|
|
6064
|
+
exportSettlement,
|
|
4291
6065
|
restore: restoreShellSnapshot,
|
|
6066
|
+
installRetainedGenerations,
|
|
4292
6067
|
async handleBossTurn(turn, context) {
|
|
4293
6068
|
if (lifecycle !== 'ready' || terminallyDisposed) {
|
|
4294
6069
|
throw new Error('init must be called first, or restore must complete before handling a Boss turn');
|
|
@@ -4300,10 +6075,25 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4300
6075
|
if (activeTurnHostCalls !== undefined) {
|
|
4301
6076
|
throw new Error('cannot handle concurrent Boss turns');
|
|
4302
6077
|
}
|
|
6078
|
+
if (retainedGenerationInstallationInProgress) {
|
|
6079
|
+
throw new Error('cannot handle a Boss turn while retained generations are installing');
|
|
6080
|
+
}
|
|
4303
6081
|
// Empty or whitespace-only input allocates no call, session, or
|
|
4304
6082
|
// telemetry (CAPTAIN-7).
|
|
6083
|
+
retentionSettlementReady = false;
|
|
6084
|
+
abandonmentSettlementUnsafe = false;
|
|
4305
6085
|
if (turn.prompt.trim().length === 0)
|
|
4306
6086
|
return;
|
|
6087
|
+
settledTurnUnresolvedEffects = undefined;
|
|
6088
|
+
retainedGenerationInstallationClosed = true;
|
|
6089
|
+
for (const update of pendingRetentionUpdates.values()) {
|
|
6090
|
+
applyRetentionUpdateToCatalog(update);
|
|
6091
|
+
}
|
|
6092
|
+
retainedGenerationCandidates.clear();
|
|
6093
|
+
pendingRetentionUpdates.clear();
|
|
6094
|
+
// A terminal or dismissal can remove the whole stack during this turn;
|
|
6095
|
+
// take the latest already-settled generation before controller work.
|
|
6096
|
+
rememberRetainedGeneration();
|
|
4307
6097
|
const turnHostCalls = new Set();
|
|
4308
6098
|
activeTurnHostCalls = turnHostCalls;
|
|
4309
6099
|
activeContext = context;
|
|
@@ -4326,6 +6116,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4326
6116
|
decisionCall = undefined;
|
|
4327
6117
|
appendJournal('boss', turn.prompt);
|
|
4328
6118
|
try {
|
|
6119
|
+
await drainRetiredRetainedRuntimes();
|
|
6120
|
+
await prepareRetainedGenerationOffers();
|
|
4329
6121
|
const result = await captainRuntime.handleBossInput({
|
|
4330
6122
|
text: turn.prompt,
|
|
4331
6123
|
signal: context.signal,
|
|
@@ -4388,17 +6180,22 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4388
6180
|
activeTurnHostCalls = undefined;
|
|
4389
6181
|
}
|
|
4390
6182
|
activeContext = undefined;
|
|
6183
|
+
retentionSettlementReady = true;
|
|
4391
6184
|
}
|
|
4392
6185
|
},
|
|
4393
6186
|
async prepareDispose() {
|
|
4394
|
-
if (lifecycle === 'initializing' ||
|
|
6187
|
+
if (lifecycle === 'initializing' ||
|
|
6188
|
+
lifecycle === 'restoring' ||
|
|
6189
|
+
retainedGenerationInstallationInProgress) {
|
|
4395
6190
|
throw new Error('cannot dispose while Captain shell setup is in progress');
|
|
4396
6191
|
}
|
|
4397
6192
|
activeContext = undefined;
|
|
4398
6193
|
await teardown();
|
|
4399
6194
|
},
|
|
4400
6195
|
async dispose() {
|
|
4401
|
-
if (lifecycle === 'initializing' ||
|
|
6196
|
+
if (lifecycle === 'initializing' ||
|
|
6197
|
+
lifecycle === 'restoring' ||
|
|
6198
|
+
retainedGenerationInstallationInProgress) {
|
|
4402
6199
|
throw new Error('cannot dispose while Captain shell setup is in progress');
|
|
4403
6200
|
}
|
|
4404
6201
|
activeContext = undefined;
|
|
@@ -4417,6 +6214,18 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4417
6214
|
catch (error) {
|
|
4418
6215
|
failure = error;
|
|
4419
6216
|
}
|
|
6217
|
+
const retainedRuntimes = takeRetainedOfferRuntimes();
|
|
6218
|
+
if (retainedRuntimes.length > 0) {
|
|
6219
|
+
try {
|
|
6220
|
+
await disposeRetainedRuntimeSet(retainedRuntimes, 'retained-generation shell cleanup failed');
|
|
6221
|
+
}
|
|
6222
|
+
catch (error) {
|
|
6223
|
+
failure ??= error;
|
|
6224
|
+
}
|
|
6225
|
+
}
|
|
6226
|
+
retainedGenerations.clear();
|
|
6227
|
+
ineligibleRetainedGenerations.clear();
|
|
6228
|
+
retainedGenerationRootClears.clear();
|
|
4420
6229
|
const runtime = captainRuntime;
|
|
4421
6230
|
captainRuntime = undefined;
|
|
4422
6231
|
if (runtime) {
|
|
@@ -4431,6 +6240,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4431
6240
|
// Quarantine is session-wide by design. Only terminal teardown may drop
|
|
4432
6241
|
// its ownership after every frame host call and the Captain are drained.
|
|
4433
6242
|
playerTransactions.clear();
|
|
6243
|
+
hostCapabilitiesById = new Map();
|
|
6244
|
+
pendingHostCapabilities = undefined;
|
|
6245
|
+
currentEffectLedger = () => emptyPlaybookEffectLedger();
|
|
4434
6246
|
lifecycle = 'closed';
|
|
4435
6247
|
if (failure !== undefined)
|
|
4436
6248
|
throw failure;
|