@sublang/playbook 9.0.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/cli.md +109 -21
- package/docs/configuration.md +89 -40
- package/docs/embedding.md +126 -12
- package/package.json +14 -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 +140 -10
- package/reference/sdlc/captain.playbook/captain.playbook.ts +188 -16
- package/reference/sdlc/code.md +35 -16
- package/reference/sdlc/code.playbook/bin/interactive-session.js +228 -23
- package/reference/sdlc/code.playbook/bin/launch-config.js +611 -221
- package/reference/sdlc/code.playbook/bin/playbook.js +304 -178
- package/reference/sdlc/code.playbook/bin/replay-observer.js +221 -0
- package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
- package/reference/sdlc/code.playbook/bin/run.js +669 -215
- package/reference/sdlc/code.playbook/bin/session-store.js +4546 -502
- package/reference/sdlc/code.playbook/code.fsm.d.ts +7 -0
- package/reference/sdlc/code.playbook/code.fsm.js +74 -25
- package/reference/sdlc/code.playbook/code.fsm.ts +83 -29
- 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 +54 -2
- package/reference/sdlc/code.playbook/code.playbook.ts +75 -6
- 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 +103 -8
- package/reference/sdlc/code.playbook/playbook-captain.js +1871 -75
- package/reference/sdlc/code.playbook/playbook-captain.ts +2801 -102
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +14 -10
- package/reference/sdlc/code.playbook/session-store.d.ts +82 -0
- package/reference/sdlc/code.playbook/session-store.js +113 -0
- package/reference/sdlc/decide.md +24 -16
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +7 -0
- 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 +13 -5
- package/reference/sdlc/decide.playbook/decide.playbook.js +1712 -91
- package/reference/sdlc/decide.playbook/decide.playbook.ts +2677 -136
- 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.md +36 -18
- 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 +65 -2
- package/reference/sdlc/review.playbook/review.playbook.ts +83 -6
- 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 +6 -5
- package/slc/link.md +544 -41
- 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 +164 -3
- package/src/runtime.ts +213 -2
- package/src/xstate-playbook-runtime.d.ts +149 -10
- package/src/xstate-playbook-runtime.js +2569 -270
- package/src/xstate-playbook-runtime.ts +4133 -490
- package/src/xstate-runtime.d.ts +59 -1
- package/src/xstate-runtime.js +866 -7
- package/src/xstate-runtime.ts +1397 -7
|
@@ -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)
|
|
@@ -405,11 +431,45 @@ function forwardedToolOptions(requested, captainAdapter) {
|
|
|
405
431
|
return { allowedTools: requested };
|
|
406
432
|
}
|
|
407
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
|
+
}
|
|
408
468
|
// CAPTAIN-20: the result-phase block the shell supplies inside the closing
|
|
409
469
|
// reply call's envelope — the settlement's outcome-report facts verbatim, the
|
|
410
470
|
// exact counts, and the saved-counts line only when counted activity is
|
|
411
471
|
// nonzero.
|
|
412
|
-
function outcomeReportBlock(report) {
|
|
472
|
+
function outcomeReportBlock(report, unresolvedEffects) {
|
|
413
473
|
const lines = [
|
|
414
474
|
`Settlement status: ${report.status}`,
|
|
415
475
|
...(report.playbookId === undefined
|
|
@@ -433,6 +493,12 @@ function outcomeReportBlock(report) {
|
|
|
433
493
|
if (report.leafStateSummary !== undefined) {
|
|
434
494
|
lines.push(`Resulting leaf state: ${report.leafStateSummary}`);
|
|
435
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
|
+
}
|
|
436
502
|
lines.push(`Progress counts: ${report.progressPhrase}`);
|
|
437
503
|
lines.push(`Counts: ${JSON.stringify({
|
|
438
504
|
...report.counts,
|
|
@@ -467,10 +533,150 @@ function summaryProgressPhrase(stateCounts) {
|
|
|
467
533
|
function summaryProgressRoundCount(stateCounts) {
|
|
468
534
|
return [...stateCounts.values()].reduce((total, count) => total + count, 0);
|
|
469
535
|
}
|
|
470
|
-
function
|
|
471
|
-
|
|
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;
|
|
654
|
+
}
|
|
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;
|
|
472
678
|
}
|
|
473
|
-
function isValidRegistryEntry(value) {
|
|
679
|
+
function isValidRegistryEntry(value, artifactSchema) {
|
|
474
680
|
if (typeof value !== 'object' || value === null)
|
|
475
681
|
return false;
|
|
476
682
|
const e = value;
|
|
@@ -495,7 +701,7 @@ function isValidRegistryEntry(value) {
|
|
|
495
701
|
return (typeof e.id === 'string' &&
|
|
496
702
|
typeof e.command === 'string' &&
|
|
497
703
|
typeof e.intent === 'string' &&
|
|
498
|
-
|
|
704
|
+
artifactSchema === 3 &&
|
|
499
705
|
typeof e.validateOptions === 'function' &&
|
|
500
706
|
typeof e.createRuntime === 'function');
|
|
501
707
|
}
|
|
@@ -503,6 +709,7 @@ const SNAPSHOT_ACTIONS = new Set([
|
|
|
503
709
|
'respond',
|
|
504
710
|
'start',
|
|
505
711
|
'switch',
|
|
712
|
+
'resume',
|
|
506
713
|
'dismiss',
|
|
507
714
|
'deliver',
|
|
508
715
|
'runtime',
|
|
@@ -519,6 +726,68 @@ const SNAPSHOT_JOURNAL_KINDS = new Set([
|
|
|
519
726
|
'action',
|
|
520
727
|
'outcome',
|
|
521
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
|
+
}
|
|
522
791
|
function snapshotRecord(value, path) {
|
|
523
792
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
524
793
|
throw new TypeError(`${path} must be an object`);
|
|
@@ -706,9 +975,16 @@ function normalizeHostPlayerResult(value, expectedPlayerId) {
|
|
|
706
975
|
export function assertPlaybookCaptainShellSnapshot(value) {
|
|
707
976
|
const detached = snapshotJsonValue(value, 'Captain shell snapshot');
|
|
708
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
|
+
}
|
|
709
984
|
const mode = snapshot.mode;
|
|
710
985
|
const commonKeys = [
|
|
711
986
|
'schemaVersion',
|
|
987
|
+
'effectLedger',
|
|
712
988
|
'captain',
|
|
713
989
|
'playerSessions',
|
|
714
990
|
'issuedSessionIds',
|
|
@@ -725,6 +1001,7 @@ export function assertPlaybookCaptainShellSnapshot(value) {
|
|
|
725
1001
|
rejectSnapshotKeys(snapshot, [
|
|
726
1002
|
...commonKeys,
|
|
727
1003
|
'frames',
|
|
1004
|
+
'retainedEffectReconciliation',
|
|
728
1005
|
'pendingBossQuestions',
|
|
729
1006
|
'lastError',
|
|
730
1007
|
], 'Captain shell snapshot');
|
|
@@ -732,13 +1009,28 @@ export function assertPlaybookCaptainShellSnapshot(value) {
|
|
|
732
1009
|
else {
|
|
733
1010
|
throw new TypeError('Captain shell snapshot.mode must be "chat" or "engaged.parked"');
|
|
734
1011
|
}
|
|
735
|
-
if (snapshot.schemaVersion !== 3) {
|
|
736
|
-
throw new TypeError(`Captain shell snapshot.schemaVersion ${String(snapshot.schemaVersion)} is not supported (expected 3)`);
|
|
737
|
-
}
|
|
738
1012
|
const captain = snapshotRecord(snapshot.captain, 'Captain shell snapshot.captain');
|
|
739
1013
|
rejectSnapshotKeys(captain, ['sessionId', 'runtime', 'agent', 'conversation'], 'Captain shell snapshot.captain');
|
|
740
1014
|
const captainSessionId = snapshotUuid(captain.sessionId, 'Captain shell snapshot.captain.sessionId');
|
|
741
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
|
+
}
|
|
742
1034
|
const captainAgent = snapshotFixedAgent(captain.agent, 'Captain shell snapshot.captain.agent');
|
|
743
1035
|
const conversation = snapshotRecord(captain.conversation, 'Captain shell snapshot.captain.conversation');
|
|
744
1036
|
let normalizedConversation;
|
|
@@ -855,7 +1147,8 @@ export function assertPlaybookCaptainShellSnapshot(value) {
|
|
|
855
1147
|
}
|
|
856
1148
|
const playerSessions = snapshotPlayerSessions(snapshot.playerSessions, 'Captain shell snapshot.playerSessions');
|
|
857
1149
|
const common = {
|
|
858
|
-
schemaVersion:
|
|
1150
|
+
schemaVersion: 4,
|
|
1151
|
+
effectLedger,
|
|
859
1152
|
captain: {
|
|
860
1153
|
sessionId: captainSessionId,
|
|
861
1154
|
runtime: captainRuntime,
|
|
@@ -941,6 +1234,32 @@ export function assertPlaybookCaptainShellSnapshot(value) {
|
|
|
941
1234
|
const issuedIds = new Set(issued);
|
|
942
1235
|
const rootSessionId = normalizedFrames[0].sessionId;
|
|
943
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
|
+
}
|
|
944
1263
|
if (activePlaybooks.has(frame.playbookId)) {
|
|
945
1264
|
throw new TypeError('Captain shell snapshot engagement path must not contain a playbook cycle');
|
|
946
1265
|
}
|
|
@@ -1003,13 +1322,21 @@ export function assertPlaybookCaptainShellSnapshot(value) {
|
|
|
1003
1322
|
!leafRuntime.state.tags.includes('playbook.parked')) {
|
|
1004
1323
|
throw new TypeError('Captain shell snapshot leaf runtime must be parked without a dangling suspended child call');
|
|
1005
1324
|
}
|
|
1006
|
-
if (
|
|
1007
|
-
|
|
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');
|
|
1008
1332
|
}
|
|
1009
1333
|
return snapshotJsonValue({
|
|
1010
1334
|
...common,
|
|
1011
1335
|
mode,
|
|
1012
1336
|
frames: normalizedFrames,
|
|
1337
|
+
...(retainedEffectReconciliation === undefined
|
|
1338
|
+
? {}
|
|
1339
|
+
: { retainedEffectReconciliation }),
|
|
1013
1340
|
...(snapshot.pendingBossQuestions === undefined
|
|
1014
1341
|
? {}
|
|
1015
1342
|
: { pendingBossQuestions: snapshot.pendingBossQuestions }),
|
|
@@ -1052,10 +1379,16 @@ function snapshotEffortSelection(value, path) {
|
|
|
1052
1379
|
}
|
|
1053
1380
|
return selection;
|
|
1054
1381
|
}
|
|
1382
|
+
function snapshotFastMode(value, path) {
|
|
1383
|
+
if (value !== undefined && typeof value !== 'boolean') {
|
|
1384
|
+
throw new TypeError(`${path} must be a boolean`);
|
|
1385
|
+
}
|
|
1386
|
+
return value;
|
|
1387
|
+
}
|
|
1055
1388
|
function snapshotSessionAgent(value, path) {
|
|
1056
1389
|
const agent = snapshotRecord(value, path);
|
|
1057
|
-
rejectSnapshotKeys(agent, ['adapter', 'model', 'effort', 'instruction', 'permissions'], path);
|
|
1058
|
-
const fixed = snapshotFixedAgent(Object.fromEntries(Object.entries(agent).filter(([key]) => key !== 'model' && key !== 'effort')), path);
|
|
1390
|
+
rejectSnapshotKeys(agent, ['adapter', 'model', 'effort', 'fastMode', 'instruction', 'permissions'], path);
|
|
1391
|
+
const fixed = snapshotFixedAgent(Object.fromEntries(Object.entries(agent).filter(([key]) => key !== 'model' && key !== 'effort' && key !== 'fastMode')), path);
|
|
1059
1392
|
return {
|
|
1060
1393
|
adapter: fixed.adapter,
|
|
1061
1394
|
...(fixed.instruction === undefined
|
|
@@ -1066,6 +1399,9 @@ function snapshotSessionAgent(value, path) {
|
|
|
1066
1399
|
: { permissions: livePermissions(fixed.permissions) }),
|
|
1067
1400
|
model: snapshotTuningSelection(agent.model, `${path}.model`),
|
|
1068
1401
|
effort: snapshotEffortSelection(agent.effort, `${path}.effort`),
|
|
1402
|
+
...(agent.fastMode === undefined
|
|
1403
|
+
? {}
|
|
1404
|
+
: { fastMode: snapshotFastMode(agent.fastMode, `${path}.fastMode`) }),
|
|
1069
1405
|
};
|
|
1070
1406
|
}
|
|
1071
1407
|
function fixedAgent(agent) {
|
|
@@ -1076,9 +1412,13 @@ function fixedAgent(agent) {
|
|
|
1076
1412
|
};
|
|
1077
1413
|
}
|
|
1078
1414
|
function callSettings(agent, tuning = agent) {
|
|
1415
|
+
// cligent treats supplied call settings as a complete replacement, so an
|
|
1416
|
+
// omitted fastMode here is a request for the provider default, never an
|
|
1417
|
+
// inheritance of whatever the previous call left behind.
|
|
1079
1418
|
return {
|
|
1080
1419
|
model: tuning.model,
|
|
1081
1420
|
effort: tuning.effort,
|
|
1421
|
+
...(tuning.fastMode === undefined ? {} : { fastMode: tuning.fastMode }),
|
|
1082
1422
|
...(agent.instruction === undefined ? {} : { instruction: agent.instruction }),
|
|
1083
1423
|
...(agent.permissions === undefined ? {} : { permissions: agent.permissions }),
|
|
1084
1424
|
};
|
|
@@ -1088,14 +1428,25 @@ function promptIdentity(binding) {
|
|
|
1088
1428
|
? binding.model.value
|
|
1089
1429
|
: binding.agent.adapter;
|
|
1090
1430
|
}
|
|
1431
|
+
function rejectConfiguredHostCapabilities(value, path) {
|
|
1432
|
+
if (value !== null &&
|
|
1433
|
+
typeof value === 'object' &&
|
|
1434
|
+
!Array.isArray(value) &&
|
|
1435
|
+
Object.prototype.hasOwnProperty.call(value, HOST_CAPABILITIES_OPTION_KEY)) {
|
|
1436
|
+
throw new Error(`${path}.${HOST_CAPABILITIES_OPTION_KEY} is host-owned and cannot be configured`);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1091
1439
|
// Resolve the active registry at init from exact normalized role and session
|
|
1092
1440
|
// agent projections (CAPTAIN-16). No role, ancestor, or generated-name fallback
|
|
1093
1441
|
// exists at this boundary.
|
|
1094
|
-
async function buildEnablements(options, loadModule) {
|
|
1442
|
+
async function buildEnablements(options, loadModule, hostCapabilities) {
|
|
1095
1443
|
const entries = [];
|
|
1096
1444
|
const byCommand = new Map();
|
|
1097
1445
|
const byId = new Map();
|
|
1098
1446
|
const enablementById = new Map();
|
|
1447
|
+
const hostCapabilitiesById = new Map();
|
|
1448
|
+
const suppliedHostCapabilities = captureHostCapabilityRecord(hostCapabilities);
|
|
1449
|
+
const expectedHostCapabilityIds = [];
|
|
1099
1450
|
const detached = snapshotJsonValue(options, 'captain.options');
|
|
1100
1451
|
const top = snapshotRecord(detached, 'captain.options');
|
|
1101
1452
|
rejectSnapshotKeys(top, ['playbooks', 'sessionAgents', 'captainAdapter'], 'captain.options');
|
|
@@ -1135,6 +1486,7 @@ async function buildEnablements(options, loadModule) {
|
|
|
1135
1486
|
}
|
|
1136
1487
|
const record = block;
|
|
1137
1488
|
rejectSnapshotKeys(record, ['from', 'command', 'roles', 'options'], `captain.options.playbooks.${id}`);
|
|
1489
|
+
rejectConfiguredHostCapabilities(record.options, `captain.options.playbooks.${id}.options`);
|
|
1138
1490
|
const from = record.from;
|
|
1139
1491
|
if (typeof from !== 'string' || from.length === 0) {
|
|
1140
1492
|
throw new Error(`captain.options.playbooks.${id}.from must be a module specifier`);
|
|
@@ -1146,10 +1498,27 @@ async function buildEnablements(options, loadModule) {
|
|
|
1146
1498
|
catch (cause) {
|
|
1147
1499
|
throw new Error(`captain.options.playbooks.${id}.from "${from}" failed to import: ${String(cause?.message ?? cause)}`);
|
|
1148
1500
|
}
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1501
|
+
const capturedEntry = captureRegistryEntry(mod?.default);
|
|
1502
|
+
const artifactSchema = capturedEntry
|
|
1503
|
+
?.artifactSchema;
|
|
1504
|
+
const runtimeProfile = validateRuntimeProfile(capturedEntry?.runtimeProfile);
|
|
1505
|
+
if (artifactSchema !== 3 ||
|
|
1506
|
+
runtimeProfile === undefined ||
|
|
1507
|
+
!isValidRegistryEntry(capturedEntry, artifactSchema)) {
|
|
1151
1508
|
throw new Error(`captain.options.playbooks.${id}.from "${from}" exposes no valid registry entry`);
|
|
1152
1509
|
}
|
|
1510
|
+
const entry = Object.freeze({
|
|
1511
|
+
...capturedEntry,
|
|
1512
|
+
requiredRoleIds: Object.freeze([...capturedEntry.requiredRoleIds]),
|
|
1513
|
+
concurrentRoleSets: Object.freeze(capturedEntry.concurrentRoleSets.map((roles) => Object.freeze([...roles]))),
|
|
1514
|
+
});
|
|
1515
|
+
if (runtimeProfile.artifactSchema !== artifactSchema) {
|
|
1516
|
+
const implementation = runtimeProfile.kind === 'shared-factory'
|
|
1517
|
+
? 'shared factory'
|
|
1518
|
+
: 'bespoke runtime';
|
|
1519
|
+
throw new Error(`captain.options.playbooks.${id}.from "${from}" advertises artifact schema ${artifactSchema} ` +
|
|
1520
|
+
`but its ${implementation} implements schema ${runtimeProfile.artifactSchema}`);
|
|
1521
|
+
}
|
|
1153
1522
|
if (entry.id !== id) {
|
|
1154
1523
|
throw new Error(`captain.options.playbooks.${id} key must equal the module manifest id "${entry.id}"`);
|
|
1155
1524
|
}
|
|
@@ -1177,7 +1546,7 @@ async function buildEnablements(options, loadModule) {
|
|
|
1177
1546
|
for (const role of entry.requiredRoleIds) {
|
|
1178
1547
|
const path = `captain.options.playbooks.${id}.roles.${role}`;
|
|
1179
1548
|
const rawBinding = snapshotRecord(roleRecord[role], path);
|
|
1180
|
-
rejectSnapshotKeys(rawBinding, ['playerId', 'model', 'effort'], path);
|
|
1549
|
+
rejectSnapshotKeys(rawBinding, ['playerId', 'model', 'effort', 'fastMode'], path);
|
|
1181
1550
|
const playerId = snapshotString(rawBinding.playerId, `${path}.playerId`);
|
|
1182
1551
|
if (!PLAYER_ID_PATTERN.test(playerId) || playerId === INTERNAL_CAPTAIN_ID) {
|
|
1183
1552
|
throw new Error(`${path}.playerId is not a canonical player id`);
|
|
@@ -1190,6 +1559,11 @@ async function buildEnablements(options, loadModule) {
|
|
|
1190
1559
|
playerId,
|
|
1191
1560
|
model: snapshotTuningSelection(rawBinding.model, `${path}.model`),
|
|
1192
1561
|
effort: snapshotEffortSelection(rawBinding.effort, `${path}.effort`),
|
|
1562
|
+
...(rawBinding.fastMode === undefined
|
|
1563
|
+
? {}
|
|
1564
|
+
: {
|
|
1565
|
+
fastMode: snapshotFastMode(rawBinding.fastMode, `${path}.fastMode`),
|
|
1566
|
+
}),
|
|
1193
1567
|
agent,
|
|
1194
1568
|
});
|
|
1195
1569
|
}
|
|
@@ -1200,16 +1574,26 @@ async function buildEnablements(options, loadModule) {
|
|
|
1200
1574
|
}
|
|
1201
1575
|
}
|
|
1202
1576
|
const validatedOptions = snapshotJsonValue(entry.validateOptions(record.options), `captain.options.playbooks.${id}.options`);
|
|
1577
|
+
rejectConfiguredHostCapabilities(validatedOptions, `captain.options.playbooks.${id}.options`);
|
|
1203
1578
|
entries.push(entry);
|
|
1204
1579
|
byId.set(entry.id, entry);
|
|
1205
1580
|
byCommand.set(command, entry);
|
|
1581
|
+
const hostCapability = validateHostCapabilities(suppliedHostCapabilities[entry.id], entry, command);
|
|
1582
|
+
expectedHostCapabilityIds.push(entry.id);
|
|
1583
|
+
hostCapabilitiesById.set(entry.id, hostCapability);
|
|
1206
1584
|
enablementById.set(entry.id, {
|
|
1207
1585
|
entry,
|
|
1586
|
+
artifactSchema,
|
|
1208
1587
|
command,
|
|
1209
1588
|
options: validatedOptions,
|
|
1210
1589
|
roleBindings,
|
|
1211
1590
|
});
|
|
1212
1591
|
}
|
|
1592
|
+
const suppliedHostCapabilityIds = Object.keys(suppliedHostCapabilities).sort();
|
|
1593
|
+
expectedHostCapabilityIds.sort();
|
|
1594
|
+
if (!isDeepStrictEqual(suppliedHostCapabilityIds, expectedHostCapabilityIds)) {
|
|
1595
|
+
throw new Error('current-host construction capabilities must exactly cover schema-3 playbooks');
|
|
1596
|
+
}
|
|
1213
1597
|
const referenced = new Set([...enablementById.values()].flatMap((enablement) => [...enablement.roleBindings.values()].map((binding) => binding.playerId)));
|
|
1214
1598
|
const unreferenced = [...playerAgents.keys()].find((id) => !referenced.has(id));
|
|
1215
1599
|
if (unreferenced !== undefined) {
|
|
@@ -1220,6 +1604,7 @@ async function buildEnablements(options, loadModule) {
|
|
|
1220
1604
|
byCommand,
|
|
1221
1605
|
byId,
|
|
1222
1606
|
enablementById,
|
|
1607
|
+
hostCapabilitiesById,
|
|
1223
1608
|
captainAgent,
|
|
1224
1609
|
playerAgents,
|
|
1225
1610
|
};
|
|
@@ -1228,6 +1613,17 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1228
1613
|
const loadModule = deps.loadModule ?? ((specifier) => import(specifier));
|
|
1229
1614
|
const createSessionId = deps.createSessionId ?? randomUUID;
|
|
1230
1615
|
const createCaptainRuntime = deps.createCaptainRuntime ?? createDefaultCaptainRuntime;
|
|
1616
|
+
const unresolvedEffectSettlement = deps.unresolvedEffectSettlement;
|
|
1617
|
+
let pendingHostCapabilities = deps.hostCapabilities;
|
|
1618
|
+
let currentEffectLedger = () => emptyPlaybookEffectLedger();
|
|
1619
|
+
// The returned shell must not retain the caller's aggregate dependency
|
|
1620
|
+
// object after its one live capability input has moved to a clearable slot.
|
|
1621
|
+
deps = {};
|
|
1622
|
+
const buildCurrentEnablements = async () => {
|
|
1623
|
+
const hostCapabilities = pendingHostCapabilities;
|
|
1624
|
+
pendingHostCapabilities = undefined;
|
|
1625
|
+
return buildEnablements(options, loadModule, hostCapabilities);
|
|
1626
|
+
};
|
|
1231
1627
|
let captainAgent;
|
|
1232
1628
|
let captainAdapter;
|
|
1233
1629
|
let playerAgents = new Map();
|
|
@@ -1237,6 +1633,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1237
1633
|
let byCommand = new Map();
|
|
1238
1634
|
let byId = new Map();
|
|
1239
1635
|
let enablementById = new Map();
|
|
1636
|
+
let hostCapabilitiesById = new Map();
|
|
1240
1637
|
let session;
|
|
1241
1638
|
let sessionEmissionsOpen = false;
|
|
1242
1639
|
let closedGateAttempted = false;
|
|
@@ -1245,6 +1642,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1245
1642
|
let activeContext;
|
|
1246
1643
|
const frames = [];
|
|
1247
1644
|
let mode = 'chat';
|
|
1645
|
+
let retainedEffectReconciliation;
|
|
1248
1646
|
let pendingBossQuestions;
|
|
1249
1647
|
let lastError;
|
|
1250
1648
|
let activeTurnSummary;
|
|
@@ -1314,6 +1712,19 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1314
1712
|
let decisionCall;
|
|
1315
1713
|
let lastAction;
|
|
1316
1714
|
let lastSettlementStatus;
|
|
1715
|
+
const retainedGenerationCandidates = new Map();
|
|
1716
|
+
const pendingRetentionUpdates = new Map();
|
|
1717
|
+
const retainedGenerations = new Map();
|
|
1718
|
+
const retainedGenerationOffers = new Map();
|
|
1719
|
+
const ineligibleRetainedGenerations = new Set();
|
|
1720
|
+
const retainedGenerationRootClears = new Set();
|
|
1721
|
+
const retiredRetainedRuntimes = [];
|
|
1722
|
+
let retainedGenerationsInstalled = false;
|
|
1723
|
+
let retainedGenerationInstallationInProgress = false;
|
|
1724
|
+
let retainedGenerationInstallationClosed = false;
|
|
1725
|
+
let retentionSettlementReady = false;
|
|
1726
|
+
let abandonmentSettlementUnsafe = false;
|
|
1727
|
+
let settledTurnUnresolvedEffects;
|
|
1317
1728
|
// DR-029: a run that lands in the runtime's own failure state
|
|
1318
1729
|
// is an outcome the report must name. `processFrameResult` records it here
|
|
1319
1730
|
// and the settling selection folds it into its facts, so the grounding the
|
|
@@ -1322,6 +1733,568 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1322
1733
|
const rootFrame = () => frames[0];
|
|
1323
1734
|
const leafFrame = () => frames.at(-1);
|
|
1324
1735
|
const frameLabel = (frame) => `/${frame.enablement.command}`;
|
|
1736
|
+
const capturedUnresolvedEnvelopeReferences = (frame) => {
|
|
1737
|
+
let advertisesUnresolved = false;
|
|
1738
|
+
try {
|
|
1739
|
+
advertisesUnresolved =
|
|
1740
|
+
frame.runtime
|
|
1741
|
+
.describe?.()
|
|
1742
|
+
.actions.some(({ id }) => id === UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID ||
|
|
1743
|
+
id === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID) === true;
|
|
1744
|
+
}
|
|
1745
|
+
catch {
|
|
1746
|
+
advertisesUnresolved = true;
|
|
1747
|
+
}
|
|
1748
|
+
if (typeof frame.runtime.unresolvedEffectEnvelopes !== 'function') {
|
|
1749
|
+
if (advertisesUnresolved) {
|
|
1750
|
+
throw new Error(`${frameLabel(frame)} unresolved-effect runtime exposes no envelope identities`);
|
|
1751
|
+
}
|
|
1752
|
+
return [];
|
|
1753
|
+
}
|
|
1754
|
+
const detached = snapshotJsonValue(frame.runtime.unresolvedEffectEnvelopes(), `${frameLabel(frame)} unresolved effect envelope identities`);
|
|
1755
|
+
if (!Array.isArray(detached)) {
|
|
1756
|
+
throw new TypeError(`${frameLabel(frame)} unresolved effect envelope identities must be an array`);
|
|
1757
|
+
}
|
|
1758
|
+
return detached.map((raw, index) => {
|
|
1759
|
+
const path = `${frameLabel(frame)} unresolved effect envelope identities[${index}]`;
|
|
1760
|
+
const record = snapshotRecord(raw, path);
|
|
1761
|
+
if (record.kind === 'boundary') {
|
|
1762
|
+
rejectSnapshotKeys(record, ['kind', 'boundaryId'], path);
|
|
1763
|
+
return {
|
|
1764
|
+
kind: 'boundary',
|
|
1765
|
+
boundaryId: snapshotString(record.boundaryId, `${path}.boundaryId`),
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
if (record.kind === 'logical-operation') {
|
|
1769
|
+
rejectSnapshotKeys(record, ['kind', 'operationId'], path);
|
|
1770
|
+
return {
|
|
1771
|
+
kind: 'logical-operation',
|
|
1772
|
+
operationId: snapshotString(record.operationId, `${path}.operationId`),
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
throw new TypeError(`${path}.kind is not supported`);
|
|
1776
|
+
});
|
|
1777
|
+
};
|
|
1778
|
+
const unresolvedEffectFromReceipt = (receipt, baselineHead = receipt.baseline.head) => {
|
|
1779
|
+
if (receipt.classification === 'unchanged')
|
|
1780
|
+
return undefined;
|
|
1781
|
+
return {
|
|
1782
|
+
classification: receipt.classification,
|
|
1783
|
+
baselineHead,
|
|
1784
|
+
...(receipt.after === undefined ? {} : { afterHead: receipt.after.head }),
|
|
1785
|
+
...(receipt.commitOid === undefined ? {} : { commitOid: receipt.commitOid }),
|
|
1786
|
+
};
|
|
1787
|
+
};
|
|
1788
|
+
const cumulativeOpenLogicalEffect = (operation, boundaries) => {
|
|
1789
|
+
const original = operation.originalBaseline;
|
|
1790
|
+
const latest = boundaries.at(-1);
|
|
1791
|
+
const after = latest.after ?? operation.checkpoint;
|
|
1792
|
+
const receipt = latest.physicalReceipt;
|
|
1793
|
+
if (receipt === undefined) {
|
|
1794
|
+
return {
|
|
1795
|
+
classification: 'incomplete',
|
|
1796
|
+
baselineHead: original.head,
|
|
1797
|
+
...(after === undefined ? {} : { afterHead: after.head }),
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
if (after === undefined) {
|
|
1801
|
+
return unresolvedEffectFromReceipt(receipt, original.head);
|
|
1802
|
+
}
|
|
1803
|
+
// An open deferred chain has no authoritative cumulative receipt. Reuse
|
|
1804
|
+
// physical ancestry only while every preceding checkpoint is one of the
|
|
1805
|
+
// same-HEAD dispositions that can lawfully keep a deferred operation
|
|
1806
|
+
// open. Any other history is bounded but not cumulatively attributable.
|
|
1807
|
+
const checkpointChainIsSafe = boundaries
|
|
1808
|
+
.slice(0, -1)
|
|
1809
|
+
.every((boundary) => boundary.after?.head === original.head &&
|
|
1810
|
+
(boundary.physicalReceipt?.classification === 'unchanged' ||
|
|
1811
|
+
boundary.physicalReceipt?.classification ===
|
|
1812
|
+
'worktree-only-change'));
|
|
1813
|
+
if (!checkpointChainIsSafe || latest.baseline.head !== original.head) {
|
|
1814
|
+
return {
|
|
1815
|
+
classification: 'observation-ambiguous',
|
|
1816
|
+
baselineHead: original.head,
|
|
1817
|
+
afterHead: after.head,
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
if (receipt.classification !== 'unchanged' &&
|
|
1821
|
+
receipt.classification !== 'worktree-only-change' &&
|
|
1822
|
+
receipt.classification !== 'one-descendant-commit') {
|
|
1823
|
+
return unresolvedEffectFromReceipt(receipt, original.head);
|
|
1824
|
+
}
|
|
1825
|
+
const sameProjection = isDeepStrictEqual(original.projection, after.projection);
|
|
1826
|
+
if (after.head === original.head) {
|
|
1827
|
+
if (receipt.classification !== 'unchanged' &&
|
|
1828
|
+
receipt.classification !== 'worktree-only-change') {
|
|
1829
|
+
return {
|
|
1830
|
+
classification: 'observation-ambiguous',
|
|
1831
|
+
baselineHead: original.head,
|
|
1832
|
+
afterHead: after.head,
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
if (sameProjection)
|
|
1836
|
+
return undefined;
|
|
1837
|
+
const preservesOriginal = Object.entries(original.projection).every(([path, entry]) => Object.hasOwn(after.projection, path) &&
|
|
1838
|
+
isDeepStrictEqual(entry, after.projection[path]));
|
|
1839
|
+
return {
|
|
1840
|
+
classification: preservesOriginal
|
|
1841
|
+
? 'worktree-only-change'
|
|
1842
|
+
: 'observation-ambiguous',
|
|
1843
|
+
baselineHead: original.head,
|
|
1844
|
+
afterHead: after.head,
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
if (receipt.classification === 'one-descendant-commit' &&
|
|
1848
|
+
sameProjection) {
|
|
1849
|
+
return {
|
|
1850
|
+
classification: 'one-descendant-commit',
|
|
1851
|
+
baselineHead: original.head,
|
|
1852
|
+
afterHead: after.head,
|
|
1853
|
+
commitOid: after.head,
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
return {
|
|
1857
|
+
classification: 'observation-ambiguous',
|
|
1858
|
+
baselineHead: original.head,
|
|
1859
|
+
afterHead: after.head,
|
|
1860
|
+
};
|
|
1861
|
+
};
|
|
1862
|
+
const projectUnresolvedEffects = (ledger, references) => {
|
|
1863
|
+
const pendingReferences = [...references];
|
|
1864
|
+
const projected = [];
|
|
1865
|
+
const seenBoundaries = new Set();
|
|
1866
|
+
const seenOperations = new Set();
|
|
1867
|
+
for (let index = 0; index < pendingReferences.length; index += 1) {
|
|
1868
|
+
const reference = pendingReferences[index];
|
|
1869
|
+
if (reference.kind === 'boundary') {
|
|
1870
|
+
if (seenBoundaries.has(reference.boundaryId))
|
|
1871
|
+
continue;
|
|
1872
|
+
seenBoundaries.add(reference.boundaryId);
|
|
1873
|
+
const boundary = ledger.boundaries.find(({ boundaryId }) => boundaryId === reference.boundaryId);
|
|
1874
|
+
if (boundary === undefined) {
|
|
1875
|
+
throw new Error(`unresolved effect boundary ${JSON.stringify(reference.boundaryId)} is absent from the authoritative ledger`);
|
|
1876
|
+
}
|
|
1877
|
+
if (boundary.logicalOperationId !== undefined) {
|
|
1878
|
+
if (!seenOperations.has(boundary.logicalOperationId)) {
|
|
1879
|
+
pendingReferences.push({
|
|
1880
|
+
kind: 'logical-operation',
|
|
1881
|
+
operationId: boundary.logicalOperationId,
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
continue;
|
|
1885
|
+
}
|
|
1886
|
+
const effect = boundary.physicalReceipt === undefined
|
|
1887
|
+
? {
|
|
1888
|
+
classification: 'incomplete',
|
|
1889
|
+
baselineHead: boundary.baseline.head,
|
|
1890
|
+
...(boundary.after === undefined
|
|
1891
|
+
? {}
|
|
1892
|
+
: { afterHead: boundary.after.head }),
|
|
1893
|
+
}
|
|
1894
|
+
: unresolvedEffectFromReceipt(boundary.physicalReceipt);
|
|
1895
|
+
if (effect !== undefined) {
|
|
1896
|
+
projected.push({ order: boundary.sequence, effect });
|
|
1897
|
+
}
|
|
1898
|
+
continue;
|
|
1899
|
+
}
|
|
1900
|
+
if (seenOperations.has(reference.operationId))
|
|
1901
|
+
continue;
|
|
1902
|
+
seenOperations.add(reference.operationId);
|
|
1903
|
+
const operation = ledger.logicalOperations.find(({ operationId }) => operationId === reference.operationId);
|
|
1904
|
+
if (operation === undefined) {
|
|
1905
|
+
throw new Error(`unresolved logical operation ${JSON.stringify(reference.operationId)} is absent from the authoritative ledger`);
|
|
1906
|
+
}
|
|
1907
|
+
const boundaries = operation.boundaryIds.map((boundaryId) => {
|
|
1908
|
+
const boundary = ledger.boundaries.find((candidate) => candidate.boundaryId === boundaryId);
|
|
1909
|
+
if (boundary === undefined) {
|
|
1910
|
+
throw new Error(`unresolved logical operation ${JSON.stringify(reference.operationId)} names an absent boundary`);
|
|
1911
|
+
}
|
|
1912
|
+
seenBoundaries.add(boundaryId);
|
|
1913
|
+
return boundary;
|
|
1914
|
+
});
|
|
1915
|
+
let effect;
|
|
1916
|
+
if (operation.logicalReceipt !== undefined) {
|
|
1917
|
+
effect = unresolvedEffectFromReceipt(operation.logicalReceipt, operation.originalBaseline.head);
|
|
1918
|
+
}
|
|
1919
|
+
else {
|
|
1920
|
+
effect = cumulativeOpenLogicalEffect(operation, boundaries);
|
|
1921
|
+
}
|
|
1922
|
+
if (effect !== undefined) {
|
|
1923
|
+
projected.push({ order: boundaries[0].sequence, effect });
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
return assertPlaybookCaptainUnresolvedEffects(projected
|
|
1927
|
+
.sort((left, right) => left.order - right.order)
|
|
1928
|
+
.map(({ effect }) => effect));
|
|
1929
|
+
};
|
|
1930
|
+
const currentUnresolvedEffects = () => {
|
|
1931
|
+
const ledger = assertPlaybookEffectLedger(currentEffectLedger());
|
|
1932
|
+
const references = [];
|
|
1933
|
+
for (const frame of frames) {
|
|
1934
|
+
references.push(...capturedUnresolvedEnvelopeReferences(frame));
|
|
1935
|
+
}
|
|
1936
|
+
if (retainedEffectReconciliation !== undefined) {
|
|
1937
|
+
for (const boundary of ledger.boundaries.slice(retainedEffectReconciliation.checkpoint.boundaries.length)) {
|
|
1938
|
+
if (boundary.physicalReceipt?.classification === 'unchanged') {
|
|
1939
|
+
continue;
|
|
1940
|
+
}
|
|
1941
|
+
references.push(boundary.logicalOperationId === undefined
|
|
1942
|
+
? { kind: 'boundary', boundaryId: boundary.boundaryId }
|
|
1943
|
+
: {
|
|
1944
|
+
kind: 'logical-operation',
|
|
1945
|
+
operationId: boundary.logicalOperationId,
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
return projectUnresolvedEffects(ledger, references);
|
|
1950
|
+
};
|
|
1951
|
+
const freezeTurnUnresolvedEffects = () => {
|
|
1952
|
+
const turn = activeTurn;
|
|
1953
|
+
if (turn?.unresolvedEffects !== undefined)
|
|
1954
|
+
return turn.unresolvedEffects;
|
|
1955
|
+
const frozen = currentUnresolvedEffects();
|
|
1956
|
+
if (turn !== undefined)
|
|
1957
|
+
turn.unresolvedEffects = frozen;
|
|
1958
|
+
settledTurnUnresolvedEffects = frozen;
|
|
1959
|
+
return frozen;
|
|
1960
|
+
};
|
|
1961
|
+
const normalizeInstalledRetainedGenerations = (value) => {
|
|
1962
|
+
const path = 'Captain retained generations';
|
|
1963
|
+
const detached = snapshotJsonValue(value, path);
|
|
1964
|
+
const record = snapshotRecord(detached, path);
|
|
1965
|
+
const authoritativeEffectLedger = assertPlaybookEffectLedger(currentEffectLedger(), `${path} current host effect ledger`);
|
|
1966
|
+
const sourceSessionIds = new Set();
|
|
1967
|
+
const normalized = new Map();
|
|
1968
|
+
for (const [rootPlaybookId, rawGeneration] of Object.entries(record)) {
|
|
1969
|
+
const generationPath = `${path}[${JSON.stringify(rootPlaybookId)}]`;
|
|
1970
|
+
const enablement = enablementById.get(rootPlaybookId);
|
|
1971
|
+
if (enablement === undefined) {
|
|
1972
|
+
throw new TypeError(`${generationPath} names a disabled root playbook`);
|
|
1973
|
+
}
|
|
1974
|
+
const generation = snapshotRecord(rawGeneration, generationPath);
|
|
1975
|
+
rejectSnapshotKeys(generation, [
|
|
1976
|
+
'effectLedger',
|
|
1977
|
+
'frames',
|
|
1978
|
+
'retainedEffectReconciliation',
|
|
1979
|
+
'rootStateDescription',
|
|
1980
|
+
], generationPath);
|
|
1981
|
+
const generationEffectLedger = assertPlaybookEffectLedger(generation.effectLedger, `${generationPath}.effectLedger`);
|
|
1982
|
+
if (generationEffectLedger.boundaries.some(({ physicalReceipt }) => physicalReceipt === undefined)) {
|
|
1983
|
+
throw new TypeError(`${generationPath}.effectLedger contains an incomplete physical boundary`);
|
|
1984
|
+
}
|
|
1985
|
+
if (!isPlaybookEffectLedgerMonotonicExtension(generationEffectLedger, authoritativeEffectLedger)) {
|
|
1986
|
+
throw new TypeError(`${generationPath}.effectLedger is not a monotonic prefix of the current host mirror`);
|
|
1987
|
+
}
|
|
1988
|
+
const generationReconciliation = generation.retainedEffectReconciliation === undefined
|
|
1989
|
+
? undefined
|
|
1990
|
+
: snapshotRecord(generation.retainedEffectReconciliation, `${generationPath}.retainedEffectReconciliation`);
|
|
1991
|
+
if (generationReconciliation !== undefined) {
|
|
1992
|
+
rejectSnapshotKeys(generationReconciliation, ['sourceGenerationId'], `${generationPath}.retainedEffectReconciliation`);
|
|
1993
|
+
}
|
|
1994
|
+
const sourceGenerationId = generationReconciliation === undefined
|
|
1995
|
+
? undefined
|
|
1996
|
+
: snapshotUuid(generationReconciliation.sourceGenerationId, `${generationPath}.retainedEffectReconciliation.sourceGenerationId`);
|
|
1997
|
+
if (!Array.isArray(generation.frames) || generation.frames.length === 0) {
|
|
1998
|
+
throw new TypeError(`${generationPath}.frames must be non-empty`);
|
|
1999
|
+
}
|
|
2000
|
+
const rootStateDescription = generation.rootStateDescription === undefined
|
|
2001
|
+
? undefined
|
|
2002
|
+
: snapshotString(generation.rootStateDescription, `${generationPath}.rootStateDescription`);
|
|
2003
|
+
const normalizedFrames = [];
|
|
2004
|
+
const playbookIds = new Set();
|
|
2005
|
+
let markedCaptureEffectLedger;
|
|
2006
|
+
for (const [index, rawFrame] of generation.frames.entries()) {
|
|
2007
|
+
const framePath = `${generationPath}.frames[${index}]`;
|
|
2008
|
+
const frame = snapshotRecord(rawFrame, framePath);
|
|
2009
|
+
rejectSnapshotKeys(frame, [
|
|
2010
|
+
'playbookId',
|
|
2011
|
+
'sessionId',
|
|
2012
|
+
'rootSessionId',
|
|
2013
|
+
'depth',
|
|
2014
|
+
'parentSessionId',
|
|
2015
|
+
'parentCallId',
|
|
2016
|
+
'options',
|
|
2017
|
+
'roleBindings',
|
|
2018
|
+
'runtime',
|
|
2019
|
+
], framePath);
|
|
2020
|
+
const playbookId = snapshotString(frame.playbookId, `${framePath}.playbookId`);
|
|
2021
|
+
const frameEnablement = enablementById.get(playbookId);
|
|
2022
|
+
if (frameEnablement === undefined) {
|
|
2023
|
+
throw new TypeError(`${framePath} names a disabled playbook`);
|
|
2024
|
+
}
|
|
2025
|
+
if (playbookIds.has(playbookId)) {
|
|
2026
|
+
throw new TypeError(`${generationPath}.frames must not contain a playbook cycle`);
|
|
2027
|
+
}
|
|
2028
|
+
playbookIds.add(playbookId);
|
|
2029
|
+
const sessionId = snapshotUuid(frame.sessionId, `${framePath}.sessionId`);
|
|
2030
|
+
if (sourceSessionIds.has(sessionId)) {
|
|
2031
|
+
throw new TypeError(`${path} frame session ids must be unique across generations`);
|
|
2032
|
+
}
|
|
2033
|
+
sourceSessionIds.add(sessionId);
|
|
2034
|
+
const rootSessionId = snapshotUuid(frame.rootSessionId, `${framePath}.rootSessionId`);
|
|
2035
|
+
const depth = snapshotInteger(frame.depth, `${framePath}.depth`);
|
|
2036
|
+
const parentSessionId = frame.parentSessionId === undefined
|
|
2037
|
+
? undefined
|
|
2038
|
+
: snapshotUuid(frame.parentSessionId, `${framePath}.parentSessionId`);
|
|
2039
|
+
const parentCallId = frame.parentCallId === undefined
|
|
2040
|
+
? undefined
|
|
2041
|
+
: snapshotString(frame.parentCallId, `${framePath}.parentCallId`);
|
|
2042
|
+
const options = frame.options;
|
|
2043
|
+
if (index === 0 &&
|
|
2044
|
+
!isDeepStrictEqual(options, frameEnablement.options)) {
|
|
2045
|
+
throw new TypeError(`${framePath}.options changed`);
|
|
2046
|
+
}
|
|
2047
|
+
const roleBindings = snapshotFrameRoleBindings(frame.roleBindings, `${framePath}.roleBindings`);
|
|
2048
|
+
if (!isDeepStrictEqual(Object.keys(roleBindings).sort(), [...frameEnablement.entry.requiredRoleIds].sort())) {
|
|
2049
|
+
throw new TypeError(`${framePath}.roleBindings do not cover the current role set`);
|
|
2050
|
+
}
|
|
2051
|
+
const runtime = assertPlaybookRuntimeSnapshot(frame.runtime, playbookId, { allowSuspendedCall: true });
|
|
2052
|
+
const retainedReconciliation = runtime.retainedEffectReconciliation;
|
|
2053
|
+
if (retainedReconciliation === undefined) {
|
|
2054
|
+
if (!isDeepStrictEqual(runtime.effectLedger, generationEffectLedger)) {
|
|
2055
|
+
throw new TypeError(`${framePath}.runtime effect ledger differs from the retained checkpoint`);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
else {
|
|
2059
|
+
if (!isDeepStrictEqual(retainedReconciliation.checkpoint, generationEffectLedger) ||
|
|
2060
|
+
isDeepStrictEqual(runtime.effectLedger, generationEffectLedger) ||
|
|
2061
|
+
!isPlaybookEffectLedgerMonotonicExtension(runtime.effectLedger, authoritativeEffectLedger)) {
|
|
2062
|
+
throw new TypeError(`${framePath}.runtime retained-effect evidence is inconsistent`);
|
|
2063
|
+
}
|
|
2064
|
+
if (markedCaptureEffectLedger === undefined) {
|
|
2065
|
+
markedCaptureEffectLedger = runtime.effectLedger;
|
|
2066
|
+
}
|
|
2067
|
+
else if (!isDeepStrictEqual(runtime.effectLedger, markedCaptureEffectLedger)) {
|
|
2068
|
+
throw new TypeError(`${framePath}.runtime effect ledger differs from the marked generation capture mirror`);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
if (runtime.state.status !== 'active' ||
|
|
2072
|
+
!runtime.state.quiescent ||
|
|
2073
|
+
typeof runtime.state.stateId !== 'string' ||
|
|
2074
|
+
runtime.state.stateId.trim().length === 0) {
|
|
2075
|
+
throw new TypeError(`${framePath}.runtime must be active, quiescent, and state-identified`);
|
|
2076
|
+
}
|
|
2077
|
+
for (const question of runtime.pendingBossQuestions) {
|
|
2078
|
+
if (question.asker.kind === 'role' &&
|
|
2079
|
+
roleBindings[question.asker.roleId] === undefined) {
|
|
2080
|
+
throw new TypeError(`${framePath}.runtime pending question names an unbound role`);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
for (const roleId of Object.keys(runtime.roleResumeTokens)) {
|
|
2084
|
+
if (roleBindings[roleId] === undefined) {
|
|
2085
|
+
throw new TypeError(`${framePath}.runtime role-resume token names an unbound role`);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
normalizedFrames.push({
|
|
2089
|
+
playbookId,
|
|
2090
|
+
sessionId,
|
|
2091
|
+
rootSessionId,
|
|
2092
|
+
depth,
|
|
2093
|
+
...(parentSessionId === undefined ? {} : { parentSessionId }),
|
|
2094
|
+
...(parentCallId === undefined ? {} : { parentCallId }),
|
|
2095
|
+
options,
|
|
2096
|
+
roleBindings,
|
|
2097
|
+
runtime,
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
const sourceRootSessionId = normalizedFrames[0].sessionId;
|
|
2101
|
+
for (const [index, frame] of normalizedFrames.entries()) {
|
|
2102
|
+
if (frame.depth !== index || frame.rootSessionId !== sourceRootSessionId) {
|
|
2103
|
+
throw new TypeError(`${generationPath}.frames have inconsistent depth or root identity`);
|
|
2104
|
+
}
|
|
2105
|
+
if (index === 0) {
|
|
2106
|
+
if (frame.playbookId !== rootPlaybookId ||
|
|
2107
|
+
frame.sessionId !== frame.rootSessionId ||
|
|
2108
|
+
frame.parentSessionId !== undefined ||
|
|
2109
|
+
frame.parentCallId !== undefined) {
|
|
2110
|
+
throw new TypeError(`${generationPath}.frames[0] is not the named root`);
|
|
2111
|
+
}
|
|
2112
|
+
continue;
|
|
2113
|
+
}
|
|
2114
|
+
const parent = normalizedFrames[index - 1];
|
|
2115
|
+
const suspended = parent.runtime.suspendedCall;
|
|
2116
|
+
if (frame.parentSessionId !== parent.sessionId ||
|
|
2117
|
+
frame.parentCallId === undefined ||
|
|
2118
|
+
suspended === undefined ||
|
|
2119
|
+
suspended.callId !== frame.parentCallId ||
|
|
2120
|
+
suspended.playbookId !== frame.playbookId ||
|
|
2121
|
+
suspended.childSessionId !== frame.sessionId) {
|
|
2122
|
+
throw new TypeError(`${generationPath}.frames[${index}] does not match its suspended parent edge`);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
const leaf = normalizedFrames.at(-1);
|
|
2126
|
+
if (leaf.runtime.suspendedCall !== undefined ||
|
|
2127
|
+
!leaf.runtime.state.tags.includes('playbook.parked')) {
|
|
2128
|
+
throw new TypeError(`${generationPath} leaf must be parked without a suspended child`);
|
|
2129
|
+
}
|
|
2130
|
+
const markedFrames = normalizedFrames.filter(({ runtime }) => runtime.retainedEffectReconciliation !== undefined);
|
|
2131
|
+
if ((sourceGenerationId === undefined && markedFrames.length !== 0) ||
|
|
2132
|
+
(sourceGenerationId !== undefined &&
|
|
2133
|
+
markedFrames.length !== normalizedFrames.length) ||
|
|
2134
|
+
(sourceGenerationId !== undefined &&
|
|
2135
|
+
normalizedFrames[0].runtime.retainedEffectReconciliation
|
|
2136
|
+
?.sourceSessionId !== sourceGenerationId)) {
|
|
2137
|
+
throw new TypeError(`${generationPath} retained-effect source marker is inconsistent`);
|
|
2138
|
+
}
|
|
2139
|
+
normalized.set(rootPlaybookId, snapshotJsonValue({
|
|
2140
|
+
effectLedger: generationEffectLedger,
|
|
2141
|
+
frames: normalizedFrames,
|
|
2142
|
+
...(sourceGenerationId === undefined
|
|
2143
|
+
? {}
|
|
2144
|
+
: {
|
|
2145
|
+
retainedEffectReconciliation: { sourceGenerationId },
|
|
2146
|
+
}),
|
|
2147
|
+
...(rootStateDescription === undefined
|
|
2148
|
+
? {}
|
|
2149
|
+
: { rootStateDescription }),
|
|
2150
|
+
}, generationPath));
|
|
2151
|
+
}
|
|
2152
|
+
return normalized;
|
|
2153
|
+
};
|
|
2154
|
+
const runtimeRetainsGenerations = (runtime) => {
|
|
2155
|
+
const metadata = runtime.retainedGenerationMetadata;
|
|
2156
|
+
return (typeof runtime.exportSnapshot === 'function' &&
|
|
2157
|
+
typeof runtime.restore === 'function' &&
|
|
2158
|
+
typeof runtime.adopt === 'function' &&
|
|
2159
|
+
metadata !== undefined &&
|
|
2160
|
+
Array.isArray(metadata.unfinishedFinalStateIds) &&
|
|
2161
|
+
metadata.unfinishedFinalStateIds.every((stateId) => typeof stateId === 'string'));
|
|
2162
|
+
};
|
|
2163
|
+
class RetainedRuntimeCleanupError extends AggregateError {
|
|
2164
|
+
failedRuntimes;
|
|
2165
|
+
constructor(failures, message, failedRuntimes = []) {
|
|
2166
|
+
super(failures, message);
|
|
2167
|
+
this.name = 'RetainedRuntimeCleanupError';
|
|
2168
|
+
this.failedRuntimes = failedRuntimes;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
const disposeRetainedRuntimeSet = async (runtimes, message) => {
|
|
2172
|
+
const failures = [];
|
|
2173
|
+
const failedRuntimes = [];
|
|
2174
|
+
for (const runtime of [...runtimes].reverse()) {
|
|
2175
|
+
try {
|
|
2176
|
+
await runtime.dispose();
|
|
2177
|
+
}
|
|
2178
|
+
catch (error) {
|
|
2179
|
+
failures.push(error);
|
|
2180
|
+
failedRuntimes.unshift(runtime);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
if (failures.length > 0) {
|
|
2184
|
+
throw new RetainedRuntimeCleanupError(failures, message, failedRuntimes);
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
const retireRetainedOffer = (rootPlaybookId) => {
|
|
2188
|
+
const offer = retainedGenerationOffers.get(rootPlaybookId);
|
|
2189
|
+
if (offer === undefined)
|
|
2190
|
+
return;
|
|
2191
|
+
retainedGenerationOffers.delete(rootPlaybookId);
|
|
2192
|
+
retiredRetainedRuntimes.push(...offer.runtimes);
|
|
2193
|
+
};
|
|
2194
|
+
const applyRetentionUpdateToCatalog = (update) => {
|
|
2195
|
+
if (update.kind === 'clear') {
|
|
2196
|
+
retireRetainedOffer(update.rootPlaybookId);
|
|
2197
|
+
retainedGenerations.delete(update.rootPlaybookId);
|
|
2198
|
+
ineligibleRetainedGenerations.delete(update.rootPlaybookId);
|
|
2199
|
+
retainedGenerationRootClears.delete(update.rootPlaybookId);
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
const prior = retainedGenerations.get(update.rootPlaybookId);
|
|
2203
|
+
if (isDeepStrictEqual(prior, update.generation))
|
|
2204
|
+
return;
|
|
2205
|
+
retireRetainedOffer(update.rootPlaybookId);
|
|
2206
|
+
retainedGenerations.set(update.rootPlaybookId, update.generation);
|
|
2207
|
+
ineligibleRetainedGenerations.delete(update.rootPlaybookId);
|
|
2208
|
+
retainedGenerationRootClears.delete(update.rootPlaybookId);
|
|
2209
|
+
};
|
|
2210
|
+
const drainRetiredRetainedRuntimes = async () => {
|
|
2211
|
+
if (retiredRetainedRuntimes.length === 0)
|
|
2212
|
+
return;
|
|
2213
|
+
const runtimes = retiredRetainedRuntimes.splice(0);
|
|
2214
|
+
try {
|
|
2215
|
+
await disposeRetainedRuntimeSet(runtimes, 'retired retained-generation runtime cleanup failed');
|
|
2216
|
+
}
|
|
2217
|
+
catch (error) {
|
|
2218
|
+
if (error instanceof RetainedRuntimeCleanupError) {
|
|
2219
|
+
retiredRetainedRuntimes.unshift(...error.failedRuntimes);
|
|
2220
|
+
}
|
|
2221
|
+
terminallyDisposed = true;
|
|
2222
|
+
lifecycle = 'closed';
|
|
2223
|
+
throw error;
|
|
2224
|
+
}
|
|
2225
|
+
};
|
|
2226
|
+
const takeRetainedOfferRuntimes = () => {
|
|
2227
|
+
const runtimes = [
|
|
2228
|
+
...[...retainedGenerationOffers.values()].flatMap((offer) => [
|
|
2229
|
+
...offer.runtimes,
|
|
2230
|
+
]),
|
|
2231
|
+
...retiredRetainedRuntimes.splice(0),
|
|
2232
|
+
];
|
|
2233
|
+
retainedGenerationOffers.clear();
|
|
2234
|
+
return runtimes;
|
|
2235
|
+
};
|
|
2236
|
+
const prepareRetainedGenerationOffers = async () => {
|
|
2237
|
+
if (rootFrame() !== undefined)
|
|
2238
|
+
return;
|
|
2239
|
+
for (const [rootPlaybookId, generation] of [...retainedGenerations].sort(([left], [right]) => left.localeCompare(right))) {
|
|
2240
|
+
if (retainedGenerationOffers.has(rootPlaybookId) ||
|
|
2241
|
+
ineligibleRetainedGenerations.has(rootPlaybookId)) {
|
|
2242
|
+
continue;
|
|
2243
|
+
}
|
|
2244
|
+
const runtimes = [];
|
|
2245
|
+
try {
|
|
2246
|
+
for (const sourceFrame of generation.frames) {
|
|
2247
|
+
const enablement = enablementById.get(sourceFrame.playbookId);
|
|
2248
|
+
runtimes.push(createRuntimeForEnablement(enablement, hostCapabilitiesById));
|
|
2249
|
+
}
|
|
2250
|
+
if (runtimes.some((runtime) => !runtimeRetainsGenerations(runtime))) {
|
|
2251
|
+
const rootRetainsGenerations = runtimeRetainsGenerations(runtimes[0]);
|
|
2252
|
+
await disposeRetainedRuntimeSet(runtimes, `/${enablementById.get(rootPlaybookId).command} retained-generation capability cleanup failed`);
|
|
2253
|
+
runtimes.splice(0);
|
|
2254
|
+
ineligibleRetainedGenerations.add(rootPlaybookId);
|
|
2255
|
+
if (!rootRetainsGenerations) {
|
|
2256
|
+
retainedGenerationRootClears.add(rootPlaybookId);
|
|
2257
|
+
}
|
|
2258
|
+
continue;
|
|
2259
|
+
}
|
|
2260
|
+
retainedGenerationOffers.set(rootPlaybookId, {
|
|
2261
|
+
generation,
|
|
2262
|
+
requiresEffectReconciliation: generation.retainedEffectReconciliation !== undefined ||
|
|
2263
|
+
generation.frames.some(({ runtime }) => runtime.retainedEffectReconciliation !== undefined) ||
|
|
2264
|
+
!retainedEffectLedgerCanRebase(generation.effectLedger, assertPlaybookEffectLedger(currentEffectLedger())),
|
|
2265
|
+
runtimes,
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
catch (error) {
|
|
2269
|
+
if (error instanceof RetainedRuntimeCleanupError) {
|
|
2270
|
+
retiredRetainedRuntimes.push(...error.failedRuntimes);
|
|
2271
|
+
terminallyDisposed = true;
|
|
2272
|
+
lifecycle = 'closed';
|
|
2273
|
+
throw error;
|
|
2274
|
+
}
|
|
2275
|
+
let cleanupError;
|
|
2276
|
+
if (runtimes.length > 0) {
|
|
2277
|
+
try {
|
|
2278
|
+
await disposeRetainedRuntimeSet(runtimes, 'retained-generation preparation cleanup failed');
|
|
2279
|
+
}
|
|
2280
|
+
catch (caught) {
|
|
2281
|
+
cleanupError = caught;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
if (cleanupError !== undefined) {
|
|
2285
|
+
if (cleanupError instanceof RetainedRuntimeCleanupError) {
|
|
2286
|
+
retiredRetainedRuntimes.push(...cleanupError.failedRuntimes);
|
|
2287
|
+
}
|
|
2288
|
+
terminallyDisposed = true;
|
|
2289
|
+
lifecycle = 'closed';
|
|
2290
|
+
throw new RetainedRuntimeCleanupError([error, cleanupError], 'retained-generation preparation and cleanup failed', cleanupError instanceof RetainedRuntimeCleanupError
|
|
2291
|
+
? cleanupError.failedRuntimes
|
|
2292
|
+
: []);
|
|
2293
|
+
}
|
|
2294
|
+
ineligibleRetainedGenerations.add(rootPlaybookId);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
};
|
|
1325
2298
|
const bindingFor = (frame, localRole) => {
|
|
1326
2299
|
const binding = frame.playerBindings.get(localRole);
|
|
1327
2300
|
if (!binding) {
|
|
@@ -1364,7 +2337,10 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1364
2337
|
...(leafFrame()?.state
|
|
1365
2338
|
? { latestSubRuntimeState: leafFrame().state }
|
|
1366
2339
|
: {}),
|
|
1367
|
-
...(
|
|
2340
|
+
...(retainedEffectReconciliation === undefined &&
|
|
2341
|
+
pendingBossQuestions !== undefined
|
|
2342
|
+
? { pendingBossQuestions }
|
|
2343
|
+
: {}),
|
|
1368
2344
|
...(lastError ? { lastError } : {}),
|
|
1369
2345
|
...(captainSessionId ? { captainSessionId } : {}),
|
|
1370
2346
|
// Presence only: the pinned token value never reaches telemetry
|
|
@@ -1526,6 +2502,59 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1526
2502
|
}
|
|
1527
2503
|
}
|
|
1528
2504
|
};
|
|
2505
|
+
const observeSummaryTrace = (frame, payload) => {
|
|
2506
|
+
const trace = payloadRecord(payload);
|
|
2507
|
+
const turn = activeTurn;
|
|
2508
|
+
const summary = activeTurnSummary;
|
|
2509
|
+
const expectedParentSessionId = frame.parent?.frame.sessionId;
|
|
2510
|
+
const expectedParentCallId = frame.parent?.callId;
|
|
2511
|
+
if (trace?.schemaVersion !== 4 ||
|
|
2512
|
+
trace.sessionId !== frame.sessionId ||
|
|
2513
|
+
trace.playbookId !== frame.entry.id ||
|
|
2514
|
+
trace.rootSessionId !== frame.rootSessionId ||
|
|
2515
|
+
(expectedParentSessionId === undefined
|
|
2516
|
+
? Object.hasOwn(trace, 'parentSessionId')
|
|
2517
|
+
: !Object.hasOwn(trace, 'parentSessionId') ||
|
|
2518
|
+
trace.parentSessionId !== expectedParentSessionId) ||
|
|
2519
|
+
(expectedParentCallId === undefined
|
|
2520
|
+
? Object.hasOwn(trace, 'parentCallId')
|
|
2521
|
+
: !Object.hasOwn(trace, 'parentCallId') ||
|
|
2522
|
+
trace.parentCallId !== expectedParentCallId) ||
|
|
2523
|
+
trace.depth !== frame.depth ||
|
|
2524
|
+
turn === undefined ||
|
|
2525
|
+
!Number.isSafeInteger(trace.turnId) ||
|
|
2526
|
+
trace.turnId <= 0 ||
|
|
2527
|
+
!Number.isSafeInteger(trace.sequence) ||
|
|
2528
|
+
trace.sequence <= 0 ||
|
|
2529
|
+
summary === undefined ||
|
|
2530
|
+
!summaryIncludes(frame)) {
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
if (trace.type !== 'outcome.accepted')
|
|
2534
|
+
return;
|
|
2535
|
+
const receipt = exactOwnDataRecord(trace.payload, [
|
|
2536
|
+
'source',
|
|
2537
|
+
'target',
|
|
2538
|
+
'acceptedOutcome',
|
|
2539
|
+
]);
|
|
2540
|
+
if (receipt === undefined ||
|
|
2541
|
+
typeof receipt.source !== 'string' ||
|
|
2542
|
+
receipt.source.trim().length === 0 ||
|
|
2543
|
+
typeof receipt.target !== 'string' ||
|
|
2544
|
+
receipt.target.trim().length === 0 ||
|
|
2545
|
+
typeof receipt.acceptedOutcome !== 'string' ||
|
|
2546
|
+
receipt.acceptedOutcome.trim().length === 0) {
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
const traceKey = `${frame.sessionId}:${trace.sequence}`;
|
|
2550
|
+
if (summary.acceptedOutcomeTraceKeys.has(traceKey))
|
|
2551
|
+
return;
|
|
2552
|
+
summary.acceptedOutcomeTraceKeys.add(traceKey);
|
|
2553
|
+
summary.counts.interruptions++;
|
|
2554
|
+
if (frame.entry.summaryPolicy?.copyPasteGuardNames.includes(receipt.acceptedOutcome)) {
|
|
2555
|
+
summary.counts.copyPastes++;
|
|
2556
|
+
}
|
|
2557
|
+
};
|
|
1529
2558
|
let callNestedPlaybook;
|
|
1530
2559
|
const createPorts = (frame) => ({
|
|
1531
2560
|
callPlayer: async (roleId, prompt, signal, options) => {
|
|
@@ -1687,14 +2716,6 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1687
2716
|
if (result.finalText === undefined) {
|
|
1688
2717
|
throw new Error('callCaptain returned status=ok with no finalText');
|
|
1689
2718
|
}
|
|
1690
|
-
const guard = guardFromJudgeReply(result.finalText);
|
|
1691
|
-
const summary = activeTurnSummary;
|
|
1692
|
-
if (guard &&
|
|
1693
|
-
summary &&
|
|
1694
|
-
summaryIncludes(frame) &&
|
|
1695
|
-
frame.entry.summaryPolicy?.copyPasteGuardNames.includes(guard)) {
|
|
1696
|
-
summary.counts.copyPastes++;
|
|
1697
|
-
}
|
|
1698
2719
|
return result.finalText;
|
|
1699
2720
|
},
|
|
1700
2721
|
callPlaybook: (request, signal) => {
|
|
@@ -1727,6 +2748,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1727
2748
|
await mirrorSubRuntimeTelemetry(frame, event.payload);
|
|
1728
2749
|
}
|
|
1729
2750
|
await requireSession().emitTelemetry(event);
|
|
2751
|
+
if (event.topic === 'playbook.trace') {
|
|
2752
|
+
observeSummaryTrace(frame, event.payload);
|
|
2753
|
+
}
|
|
1730
2754
|
})();
|
|
1731
2755
|
return trackHostCall(frame, emission);
|
|
1732
2756
|
},
|
|
@@ -1748,17 +2772,42 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1748
2772
|
throw new VisibilityControlError(error);
|
|
1749
2773
|
}
|
|
1750
2774
|
};
|
|
1751
|
-
const
|
|
2775
|
+
const generatedSessionId = () => {
|
|
1752
2776
|
const sessionId = createSessionId();
|
|
1753
2777
|
if (!UUID_PATTERN.test(sessionId)) {
|
|
1754
2778
|
throw new Error(`playbook session id generator returned a non-UUID value: ${JSON.stringify(sessionId)}`);
|
|
1755
2779
|
}
|
|
2780
|
+
return sessionId;
|
|
2781
|
+
};
|
|
2782
|
+
const allocateSessionId = () => {
|
|
2783
|
+
const sessionId = generatedSessionId();
|
|
1756
2784
|
if (issuedSessionIds.has(sessionId)) {
|
|
1757
2785
|
throw new Error(`playbook session id collision: ${sessionId}`);
|
|
1758
2786
|
}
|
|
1759
2787
|
issuedSessionIds.add(sessionId);
|
|
1760
2788
|
return sessionId;
|
|
1761
2789
|
};
|
|
2790
|
+
const allocateAdoptionSessionIds = (count, sourceSessionIds) => {
|
|
2791
|
+
const candidates = [];
|
|
2792
|
+
const rejectedSourceIds = new Set();
|
|
2793
|
+
while (candidates.length < count) {
|
|
2794
|
+
const candidate = generatedSessionId();
|
|
2795
|
+
if (sourceSessionIds.has(candidate)) {
|
|
2796
|
+
if (rejectedSourceIds.has(candidate)) {
|
|
2797
|
+
throw new Error(`playbook source session id collision: ${candidate}`);
|
|
2798
|
+
}
|
|
2799
|
+
rejectedSourceIds.add(candidate);
|
|
2800
|
+
continue;
|
|
2801
|
+
}
|
|
2802
|
+
if (issuedSessionIds.has(candidate) || candidates.includes(candidate)) {
|
|
2803
|
+
throw new Error(`playbook session id collision: ${candidate}`);
|
|
2804
|
+
}
|
|
2805
|
+
candidates.push(candidate);
|
|
2806
|
+
}
|
|
2807
|
+
for (const candidate of candidates)
|
|
2808
|
+
issuedSessionIds.add(candidate);
|
|
2809
|
+
return candidates;
|
|
2810
|
+
};
|
|
1762
2811
|
const normalizeErrorFull = (value) => {
|
|
1763
2812
|
const compact = normalizeErrorCompact(value) ?? {
|
|
1764
2813
|
name: 'Error',
|
|
@@ -1778,7 +2827,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1778
2827
|
const entry = enablement.entry;
|
|
1779
2828
|
const sessionId = allocateSessionId();
|
|
1780
2829
|
const playerBindings = makePlayerBindings(enablement);
|
|
1781
|
-
const runtime =
|
|
2830
|
+
const runtime = createRuntimeForEnablement(enablement, hostCapabilitiesById);
|
|
1782
2831
|
return {
|
|
1783
2832
|
entry,
|
|
1784
2833
|
enablement,
|
|
@@ -1794,7 +2843,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1794
2843
|
const makeRestoredFrame = (enablement, snapshot, parent) => {
|
|
1795
2844
|
const entry = enablement.entry;
|
|
1796
2845
|
const playerBindings = makePlayerBindings(enablement);
|
|
1797
|
-
const runtime =
|
|
2846
|
+
const runtime = createRuntimeForEnablement(enablement, hostCapabilitiesById);
|
|
1798
2847
|
return {
|
|
1799
2848
|
entry,
|
|
1800
2849
|
enablement,
|
|
@@ -1844,14 +2893,6 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1844
2893
|
delete ledger.resumeToken;
|
|
1845
2894
|
else
|
|
1846
2895
|
ledger.resumeToken = resumeToken;
|
|
1847
|
-
// CAPTAIN-20: a result counts only after the runtime validated it and
|
|
1848
|
-
// atomically published its authorized continuation transition.
|
|
1849
|
-
const summary = activeTurnSummary;
|
|
1850
|
-
if (pending.status === 'ok' &&
|
|
1851
|
-
summary &&
|
|
1852
|
-
summaryIncludes(frame)) {
|
|
1853
|
-
summary.counts.interruptions++;
|
|
1854
|
-
}
|
|
1855
2896
|
}
|
|
1856
2897
|
finally {
|
|
1857
2898
|
playerTransactions.delete(binding.playerId);
|
|
@@ -2101,6 +3142,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2101
3142
|
}
|
|
2102
3143
|
}
|
|
2103
3144
|
clearLeafLedger();
|
|
3145
|
+
if (frames.length === 0)
|
|
3146
|
+
retainedEffectReconciliation = undefined;
|
|
2104
3147
|
if (failures.length === 1)
|
|
2105
3148
|
throw failures[0];
|
|
2106
3149
|
if (failures.length > 1) {
|
|
@@ -2205,7 +3248,46 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2205
3248
|
throw new AggregateError(failures, 'playbook stack disposal failed');
|
|
2206
3249
|
}
|
|
2207
3250
|
};
|
|
3251
|
+
/**
|
|
3252
|
+
* DR-040 task 11: abandonment is a host settlement, not an authored FSM
|
|
3253
|
+
* result. Freeze the bounded evidence while the complete stack and its
|
|
3254
|
+
* runtime-owned envelope identities still exist, durably fence recovery,
|
|
3255
|
+
* dispose leaf-to-root without resuming a parent, then publish the matching
|
|
3256
|
+
* root clear and evidence as one durable completion before the controller
|
|
3257
|
+
* may return an executed receipt to its result phase.
|
|
3258
|
+
*/
|
|
3259
|
+
const settleUnresolvedEffectAbandonment = async (unresolvedLeaf) => {
|
|
3260
|
+
if (leafFrame() !== unresolvedLeaf) {
|
|
3261
|
+
throw new Error('unresolved-effect abandonment requires the active leaf');
|
|
3262
|
+
}
|
|
3263
|
+
const root = rootFrame();
|
|
3264
|
+
if (root === undefined) {
|
|
3265
|
+
throw new Error('unresolved-effect abandonment requires an active root');
|
|
3266
|
+
}
|
|
3267
|
+
const unresolvedEffects = freezeTurnUnresolvedEffects();
|
|
3268
|
+
if (unresolvedEffects.length === 0) {
|
|
3269
|
+
throw new Error('unresolved-effect abandonment requires nonempty effect evidence');
|
|
3270
|
+
}
|
|
3271
|
+
if (unresolvedEffectSettlement === undefined) {
|
|
3272
|
+
throw new Error('unresolved-effect abandonment requires durable host settlement');
|
|
3273
|
+
}
|
|
3274
|
+
const rootPlaybookId = root.entry.id;
|
|
3275
|
+
const settlement = Object.freeze({
|
|
3276
|
+
rootPlaybookId,
|
|
3277
|
+
unresolvedEffects,
|
|
3278
|
+
});
|
|
3279
|
+
await runEffect(() => unresolvedEffectSettlement.begin(settlement));
|
|
3280
|
+
await runEffect(() => disposeStack('unresolved-effect'));
|
|
3281
|
+
pendingRetentionUpdates.set(rootPlaybookId, {
|
|
3282
|
+
kind: 'clear',
|
|
3283
|
+
rootPlaybookId,
|
|
3284
|
+
});
|
|
3285
|
+
await runEffect(() => unresolvedEffectSettlement.complete(settlement));
|
|
3286
|
+
};
|
|
2208
3287
|
const callResultFor = (frame, result) => {
|
|
3288
|
+
if (result.outcome === 'unresolved-effect') {
|
|
3289
|
+
throw new Error(`playbook ${frame.entry.id} unresolved-effect result cannot resume a parent`);
|
|
3290
|
+
}
|
|
2209
3291
|
if (result.outcome === 'terminal') {
|
|
2210
3292
|
return {
|
|
2211
3293
|
status: 'ok',
|
|
@@ -2240,6 +3322,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2240
3322
|
if (leafFrame() !== frame) {
|
|
2241
3323
|
throw new Error('only the active leaf may receive Boss input');
|
|
2242
3324
|
}
|
|
3325
|
+
if (retainedEffectReconciliation !== undefined) {
|
|
3326
|
+
throw new Error('retained repository-effect reconciliation is required before Boss input');
|
|
3327
|
+
}
|
|
2243
3328
|
// CAPTAIN-35: the leaf check, the visibility request, and the mode change
|
|
2244
3329
|
// are shell control work performed on the way to the runtime, not the
|
|
2245
3330
|
// effect. Only the call below is the effect, so only it is inside the
|
|
@@ -2257,6 +3342,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2257
3342
|
if (!parentLink)
|
|
2258
3343
|
throw new Error('root playbook has no caller');
|
|
2259
3344
|
const parent = parentLink.frame;
|
|
3345
|
+
if (retainedEffectReconciliation !== undefined) {
|
|
3346
|
+
throw new Error('retained repository-effect reconciliation is required before parent resumption');
|
|
3347
|
+
}
|
|
2260
3348
|
const invocationSignal = child.invocationSignal;
|
|
2261
3349
|
let effectiveResult = callResult;
|
|
2262
3350
|
let ownsReturn = false;
|
|
@@ -2325,6 +3413,16 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2325
3413
|
}, context);
|
|
2326
3414
|
}
|
|
2327
3415
|
async function processFrameResult(frame, result, context) {
|
|
3416
|
+
if (result.outcome === 'unresolved-effect') {
|
|
3417
|
+
// Task 10 exposes the runtime-owned abandonment signal without
|
|
3418
|
+
// translating it into a nested result or claiming workflow completion.
|
|
3419
|
+
// Task 11 owns the durable host settlement and complete-stack disposal.
|
|
3420
|
+
assertRetainableResult(frame, result);
|
|
3421
|
+
if (leafFrame() === frame) {
|
|
3422
|
+
await setMode('engaged.parked', 'turn:unresolved-effect');
|
|
3423
|
+
}
|
|
3424
|
+
return;
|
|
3425
|
+
}
|
|
2328
3426
|
if (result.outcome === 'terminal') {
|
|
2329
3427
|
if (frame.parent) {
|
|
2330
3428
|
await resumeParent(frame, callResultFor(frame, result), context);
|
|
@@ -2336,6 +3434,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2336
3434
|
// output remains runtime-to-runtime data and never becomes Captain
|
|
2337
3435
|
// evidence (CAPPLAY-10).
|
|
2338
3436
|
activeTurn?.settlementFacts.push(rootCompletionFact(frame, result));
|
|
3437
|
+
recordTerminalRetention(frame, result);
|
|
2339
3438
|
await runEffect(() => disposeStack('final'));
|
|
2340
3439
|
}
|
|
2341
3440
|
return;
|
|
@@ -2515,7 +3614,14 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2515
3614
|
const policy = frame.entry.summaryPolicy;
|
|
2516
3615
|
const counts = { interruptions: 0, copyPastes: 0 };
|
|
2517
3616
|
const stateCounts = new Map();
|
|
2518
|
-
activeTurnSummary = policy
|
|
3617
|
+
activeTurnSummary = policy
|
|
3618
|
+
? {
|
|
3619
|
+
owner: frame,
|
|
3620
|
+
counts,
|
|
3621
|
+
stateCounts,
|
|
3622
|
+
acceptedOutcomeTraceKeys: new Set(),
|
|
3623
|
+
}
|
|
3624
|
+
: undefined;
|
|
2519
3625
|
let result;
|
|
2520
3626
|
let error;
|
|
2521
3627
|
try {
|
|
@@ -2580,12 +3686,58 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2580
3686
|
...entries.map(([key, value]) => digestLine `- ${key}: ${JSON.stringify(value)}`),
|
|
2581
3687
|
];
|
|
2582
3688
|
};
|
|
3689
|
+
const retainedResumptionDigest = () => {
|
|
3690
|
+
if (rootFrame() !== undefined) {
|
|
3691
|
+
return 'Retained resumptions: unavailable while a playbook is engaged.';
|
|
3692
|
+
}
|
|
3693
|
+
const offers = [...retainedGenerationOffers].sort(([left], [right]) => left.localeCompare(right));
|
|
3694
|
+
if (offers.length === 0)
|
|
3695
|
+
return 'Retained resumptions: none.';
|
|
3696
|
+
const lines = ['Retained resumptions:'];
|
|
3697
|
+
for (const [rootPlaybookId, offer] of offers) {
|
|
3698
|
+
const enablement = enablementById.get(rootPlaybookId);
|
|
3699
|
+
lines.push(digestLine `- ${rootPlaybookId} (/${enablement.command}): ${offer.generation.rootStateDescription ??
|
|
3700
|
+
'(no published root-state description was retained)'}`);
|
|
3701
|
+
}
|
|
3702
|
+
return lines.join('\n');
|
|
3703
|
+
};
|
|
2583
3704
|
const controlViewDigest = () => {
|
|
2584
3705
|
const leaf = leafFrame();
|
|
2585
3706
|
const lines = [digestLine `Active path: ${activePathDigest()}`];
|
|
2586
3707
|
if (!leaf) {
|
|
2587
3708
|
lines.push('The shell is idle: no leaf state, no pending question.');
|
|
2588
3709
|
lines.push('Advertised actions: none.');
|
|
3710
|
+
lines.push(retainedResumptionDigest());
|
|
3711
|
+
return lines.join('\n');
|
|
3712
|
+
}
|
|
3713
|
+
refreshRetainedEffectFence();
|
|
3714
|
+
if (retainedEffectReconciliation !== undefined) {
|
|
3715
|
+
let reconciliationActions = [];
|
|
3716
|
+
if (typeof leaf.runtime.describe === 'function' &&
|
|
3717
|
+
typeof leaf.runtime.apply === 'function') {
|
|
3718
|
+
try {
|
|
3719
|
+
reconciliationActions = leaf.runtime
|
|
3720
|
+
.describe()
|
|
3721
|
+
.actions.filter(({ id }) => id === UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID ||
|
|
3722
|
+
id === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID);
|
|
3723
|
+
}
|
|
3724
|
+
catch {
|
|
3725
|
+
// An unreadable control surface cannot open a fail-closed fence.
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
for (const action of reconciliationActions) {
|
|
3729
|
+
recordSuppliedIdentifier(action.id);
|
|
3730
|
+
}
|
|
3731
|
+
lines.push(`Leaf ${frameLabel(leaf)} is parked for repository-effect reconciliation.`);
|
|
3732
|
+
lines.push('Pending Boss questions: withheld until reconciliation.');
|
|
3733
|
+
lines.push(reconciliationActions.length === 0
|
|
3734
|
+
? 'Advertised actions: none.'
|
|
3735
|
+
: [
|
|
3736
|
+
'Advertised actions:',
|
|
3737
|
+
...reconciliationActions.map((action) => digestLine `- ${action.id}: ${action.label}`),
|
|
3738
|
+
].join('\n'));
|
|
3739
|
+
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.');
|
|
3740
|
+
lines.push(retainedResumptionDigest());
|
|
2589
3741
|
return lines.join('\n');
|
|
2590
3742
|
}
|
|
2591
3743
|
let view;
|
|
@@ -2634,6 +3786,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2634
3786
|
lines.push(describeFailure === undefined
|
|
2635
3787
|
? '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.'
|
|
2636
3788
|
: '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.');
|
|
3789
|
+
lines.push(retainedResumptionDigest());
|
|
2637
3790
|
return lines.join('\n');
|
|
2638
3791
|
}
|
|
2639
3792
|
// CAPTAIN-9: the guarded set is what the digest supplies *for selection* —
|
|
@@ -2672,6 +3825,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2672
3825
|
'Advertised actions:',
|
|
2673
3826
|
...view.actions.map((action) => digestLine `- ${action.id}: ${action.label}`),
|
|
2674
3827
|
].join('\n'));
|
|
3828
|
+
lines.push(retainedResumptionDigest());
|
|
2675
3829
|
return lines.join('\n');
|
|
2676
3830
|
};
|
|
2677
3831
|
// The catalog is registry-authored, not shell-authored: an id, a command,
|
|
@@ -2747,9 +3901,13 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2747
3901
|
// all of this prose. Preserve the exact attempt before crossing the
|
|
2748
3902
|
// boundary; the uncertainty record below keeps recovery from pretending
|
|
2749
3903
|
// delivery was confirmed while still understanding a Boss follow-up.
|
|
2750
|
-
|
|
3904
|
+
const suffix = turn?.mandatoryPresentationSuffix;
|
|
3905
|
+
const visibleText = suffix === undefined || settlement.text.includes(suffix)
|
|
3906
|
+
? settlement.text
|
|
3907
|
+
: `${settlement.text.trimEnd()}\n\n${suffix}`;
|
|
3908
|
+
appendJournal('reply', visibleText);
|
|
2751
3909
|
try {
|
|
2752
|
-
await trackTurnCall(settlement.context.emitReply(
|
|
3910
|
+
await trackTurnCall(settlement.context.emitReply(visibleText));
|
|
2753
3911
|
}
|
|
2754
3912
|
catch (error) {
|
|
2755
3913
|
conversation = { kind: 'needsSeeding' };
|
|
@@ -2889,8 +4047,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2889
4047
|
}
|
|
2890
4048
|
};
|
|
2891
4049
|
/**
|
|
2892
|
-
* CAPTAIN-35: the one wrapper an effect runs through — a runtime driven
|
|
2893
|
-
* engagement constructed, a stack disposed, an advertised
|
|
4050
|
+
* CAPTAIN-35: the one wrapper an effect runs through — a runtime driven or
|
|
4051
|
+
* adopted, an engagement constructed, a stack disposed, an advertised
|
|
4052
|
+
* action applied.
|
|
2894
4053
|
* Attribution is recorded here, at the operation that threw, and nowhere
|
|
2895
4054
|
* else: an error acquires the mark by escaping this call, so no later
|
|
2896
4055
|
* failure can inherit it.
|
|
@@ -2903,7 +4062,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2903
4062
|
* instead of settling, so a misfiling costs the Boss their only settlement.
|
|
2904
4063
|
*
|
|
2905
4064
|
* Neither can a boundary drawn around a *region* of the turn. `operation` is
|
|
2906
|
-
* therefore always one call expression naming one of those
|
|
4065
|
+
* therefore always one call expression naming one of those five operations,
|
|
2907
4066
|
* never a closure that also performs the shell work leading to it: the leaf
|
|
2908
4067
|
* check, the visibility request, the mode change, and the processing of what
|
|
2909
4068
|
* the runtime returned are all shell control work, and a boundary wide
|
|
@@ -3103,7 +4262,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3103
4262
|
...(kind === 'closingReply' && turn?.report
|
|
3104
4263
|
? [
|
|
3105
4264
|
labeledBlock('ControlView digest', controlViewDigest()),
|
|
3106
|
-
outcomeReportBlock(turn.report),
|
|
4265
|
+
outcomeReportBlock(turn.report, turn.unresolvedEffects ?? []),
|
|
3107
4266
|
]
|
|
3108
4267
|
: []),
|
|
3109
4268
|
...(options.proseRejection === undefined
|
|
@@ -3259,6 +4418,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3259
4418
|
const leaf = leafFrame();
|
|
3260
4419
|
if (!leaf)
|
|
3261
4420
|
return 'idle: no playbook is engaged';
|
|
4421
|
+
if (retainedEffectReconciliation !== undefined) {
|
|
4422
|
+
return `${frameLabel(leaf)} parked for repository-effect reconciliation`;
|
|
4423
|
+
}
|
|
3262
4424
|
if (!leaf.state)
|
|
3263
4425
|
return `${frameLabel(leaf)} engaged`;
|
|
3264
4426
|
return `${frameLabel(leaf)} at ${stateDigestLine(leaf.state, leafStateDescription(leaf))}`;
|
|
@@ -3305,6 +4467,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3305
4467
|
if (!root)
|
|
3306
4468
|
return false;
|
|
3307
4469
|
const label = frameLabel(root);
|
|
4470
|
+
// Dismissal leaves the procedure unfinished. Persist the latest safe
|
|
4471
|
+
// generation captured for this turn before disposal erases the frames.
|
|
4472
|
+
retainOrClearDisposedRoot(root);
|
|
3308
4473
|
try {
|
|
3309
4474
|
await runEffect(() => disposeStack('dismiss'));
|
|
3310
4475
|
facts.push(`Dismissed the ${label} engagement.`);
|
|
@@ -3351,6 +4516,133 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3351
4516
|
}
|
|
3352
4517
|
return { frame, report: outcome.report, failed: false };
|
|
3353
4518
|
};
|
|
4519
|
+
const adoptRetainedGeneration = async (rootPlaybookId, offer) => {
|
|
4520
|
+
const generation = offer.generation;
|
|
4521
|
+
const currentLedger = assertPlaybookEffectLedger(currentEffectLedger());
|
|
4522
|
+
const requiresEffectReconciliation = offer.requiresEffectReconciliation ||
|
|
4523
|
+
!retainedEffectLedgerCanRebase(generation.effectLedger, currentLedger);
|
|
4524
|
+
const sourceSessionIds = new Set(generation.frames.map((frame) => frame.sessionId));
|
|
4525
|
+
const targetSessionIds = allocateAdoptionSessionIds(generation.frames.length, sourceSessionIds);
|
|
4526
|
+
const targetRootSessionId = targetSessionIds[0];
|
|
4527
|
+
const adoptedFrames = [];
|
|
4528
|
+
for (const [index, sourceFrame] of generation.frames.entries()) {
|
|
4529
|
+
const enablement = enablementById.get(sourceFrame.playbookId);
|
|
4530
|
+
const parent = adoptedFrames.at(-1);
|
|
4531
|
+
adoptedFrames.push({
|
|
4532
|
+
entry: enablement.entry,
|
|
4533
|
+
enablement,
|
|
4534
|
+
runtime: offer.runtimes[index],
|
|
4535
|
+
sessionId: targetSessionIds[index],
|
|
4536
|
+
rootSessionId: targetRootSessionId,
|
|
4537
|
+
depth: index,
|
|
4538
|
+
playerBindings: makePlayerBindings(enablement),
|
|
4539
|
+
...(parent
|
|
4540
|
+
? { parent: { frame: parent, callId: 'playbook-1' } }
|
|
4541
|
+
: {}),
|
|
4542
|
+
state: sourceFrame.runtime.state,
|
|
4543
|
+
inFlightHostCalls: new Set(),
|
|
4544
|
+
});
|
|
4545
|
+
}
|
|
4546
|
+
retainedGenerationOffers.delete(rootPlaybookId);
|
|
4547
|
+
let installed = false;
|
|
4548
|
+
try {
|
|
4549
|
+
for (const [index, frame] of adoptedFrames.entries()) {
|
|
4550
|
+
const sourceFrame = generation.frames[index];
|
|
4551
|
+
const targetChild = adoptedFrames[index + 1];
|
|
4552
|
+
await runEffect(() => frame.runtime.adopt(frameSession(frame), sourceFrame.runtime, {
|
|
4553
|
+
sourceSessionId: sourceFrame.sessionId,
|
|
4554
|
+
sourceGenerationId: generation.frames[0].rootSessionId,
|
|
4555
|
+
...(targetChild === undefined
|
|
4556
|
+
? {}
|
|
4557
|
+
: { targetChildSessionId: targetChild.sessionId }),
|
|
4558
|
+
}));
|
|
4559
|
+
}
|
|
4560
|
+
frames.push(...adoptedFrames);
|
|
4561
|
+
installed = true;
|
|
4562
|
+
retainedEffectReconciliation = requiresEffectReconciliation
|
|
4563
|
+
? {
|
|
4564
|
+
sourceGenerationId: generation.retainedEffectReconciliation?.sourceGenerationId ??
|
|
4565
|
+
generation.frames[0].runtime.retainedEffectSourceSessionId ??
|
|
4566
|
+
generation.frames[0].rootSessionId,
|
|
4567
|
+
checkpoint: generation.effectLedger,
|
|
4568
|
+
}
|
|
4569
|
+
: undefined;
|
|
4570
|
+
for (const parent of adoptedFrames.slice(0, -1)) {
|
|
4571
|
+
pendingChildParents.add(parent);
|
|
4572
|
+
}
|
|
4573
|
+
const retainedQuestions = generation.frames.at(-1).runtime
|
|
4574
|
+
.pendingBossQuestions;
|
|
4575
|
+
pendingBossQuestions =
|
|
4576
|
+
requiresEffectReconciliation || retainedQuestions.length === 0
|
|
4577
|
+
? undefined
|
|
4578
|
+
: mirroredBossQuestions(retainedQuestions);
|
|
4579
|
+
lastError = undefined;
|
|
4580
|
+
retainedGenerations.delete(rootPlaybookId);
|
|
4581
|
+
ineligibleRetainedGenerations.delete(rootPlaybookId);
|
|
4582
|
+
await setMode('engaged.parked', 'resume', rootPlaybookId, targetRootSessionId);
|
|
4583
|
+
if (requiresEffectReconciliation) {
|
|
4584
|
+
await requireSession().setVisiblePlayers([]);
|
|
4585
|
+
}
|
|
4586
|
+
else {
|
|
4587
|
+
await requestVisibility(adoptedFrames.at(-1));
|
|
4588
|
+
}
|
|
4589
|
+
await requireSession().emitStatus(`◇ /${enablementById.get(rootPlaybookId).command} resumed`);
|
|
4590
|
+
if (activeTurn) {
|
|
4591
|
+
appendMandatoryPresentationSuffix(activeTurn, requiresEffectReconciliation
|
|
4592
|
+
? 'The retained work remains parked until its repository-effect evidence is reconciled.'
|
|
4593
|
+
: RESUMPTION_DUPLICATE_EFFECT_WARNING);
|
|
4594
|
+
}
|
|
4595
|
+
return [
|
|
4596
|
+
generation.rootStateDescription === undefined
|
|
4597
|
+
? `Resumed /${enablementById.get(rootPlaybookId).command} from its retained state; no published root-state description was retained.`
|
|
4598
|
+
: `Resumed /${enablementById.get(rootPlaybookId).command} from the retained state described as ${quoteEvidence(compactEvidence(generation.rootStateDescription))}.`,
|
|
4599
|
+
requiresEffectReconciliation
|
|
4600
|
+
? 'The retained work remains parked until its repository-effect evidence is reconciled; no ordinary action was resumed.'
|
|
4601
|
+
: RESUMPTION_DUPLICATE_EFFECT_WARNING,
|
|
4602
|
+
];
|
|
4603
|
+
}
|
|
4604
|
+
catch (error) {
|
|
4605
|
+
if (installed) {
|
|
4606
|
+
frames.splice(0);
|
|
4607
|
+
pendingChildParents.clear();
|
|
4608
|
+
retainedEffectReconciliation = undefined;
|
|
4609
|
+
clearLeafLedger();
|
|
4610
|
+
}
|
|
4611
|
+
const cleanupFailures = [];
|
|
4612
|
+
const failedCleanupRuntimes = [];
|
|
4613
|
+
for (const frame of [...adoptedFrames].reverse()) {
|
|
4614
|
+
try {
|
|
4615
|
+
await disposeFrame(frame);
|
|
4616
|
+
}
|
|
4617
|
+
catch (cleanupError) {
|
|
4618
|
+
cleanupFailures.push(cleanupError);
|
|
4619
|
+
failedCleanupRuntimes.push(frame.runtime);
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
const rollbackFailures = [];
|
|
4623
|
+
if (installed) {
|
|
4624
|
+
try {
|
|
4625
|
+
await setMode('chat', 'resume.failed');
|
|
4626
|
+
}
|
|
4627
|
+
catch (rollbackError) {
|
|
4628
|
+
rollbackFailures.push(rollbackError);
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
else {
|
|
4632
|
+
mode = 'chat';
|
|
4633
|
+
}
|
|
4634
|
+
if (cleanupFailures.length > 0 || rollbackFailures.length > 0) {
|
|
4635
|
+
retiredRetainedRuntimes.push(...failedCleanupRuntimes);
|
|
4636
|
+
ineligibleRetainedGenerations.add(rootPlaybookId);
|
|
4637
|
+
terminallyDisposed = true;
|
|
4638
|
+
lifecycle = 'closed';
|
|
4639
|
+
throw new AggregateError([error, ...cleanupFailures, ...rollbackFailures], 'retained-generation adoption and rollback failed');
|
|
4640
|
+
}
|
|
4641
|
+
retainedGenerations.set(rootPlaybookId, generation);
|
|
4642
|
+
ineligibleRetainedGenerations.delete(rootPlaybookId);
|
|
4643
|
+
throw error;
|
|
4644
|
+
}
|
|
4645
|
+
};
|
|
3354
4646
|
const driveAndProcess = async (frame, text, context, onDriven) => {
|
|
3355
4647
|
try {
|
|
3356
4648
|
// CAPTAIN-35: no boundary here. `driveFrame` marks the runtime call and
|
|
@@ -3377,11 +4669,36 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3377
4669
|
}
|
|
3378
4670
|
}
|
|
3379
4671
|
};
|
|
4672
|
+
const containsEffectThrow = (turn, error) => {
|
|
4673
|
+
if (turn.effectThrows.has(error))
|
|
4674
|
+
return true;
|
|
4675
|
+
return (error instanceof AggregateError &&
|
|
4676
|
+
error.errors.some((nested) => containsEffectThrow(turn, nested)));
|
|
4677
|
+
};
|
|
3380
4678
|
const settleSelection = async (selection, signal) => {
|
|
3381
4679
|
const turn = activeTurn;
|
|
3382
4680
|
runFailureFacts = [];
|
|
4681
|
+
let frozenUnresolvedEffects;
|
|
4682
|
+
const freezeControllerEvidence = () => {
|
|
4683
|
+
frozenUnresolvedEffects ??= freezeTurnUnresolvedEffects();
|
|
4684
|
+
const report = unresolvedEffectBossReport(frozenUnresolvedEffects);
|
|
4685
|
+
if (turn !== undefined && report !== undefined) {
|
|
4686
|
+
appendMandatoryPresentationSuffix(turn, report);
|
|
4687
|
+
}
|
|
4688
|
+
return frozenUnresolvedEffects;
|
|
4689
|
+
};
|
|
4690
|
+
const finalizeSettlement = (settlement) => Object.freeze({
|
|
4691
|
+
...settlement,
|
|
4692
|
+
unresolvedEffects: freezeControllerEvidence(),
|
|
4693
|
+
});
|
|
3383
4694
|
try {
|
|
3384
|
-
|
|
4695
|
+
// `respond` has no result phase: freeze its no-effect projection before
|
|
4696
|
+
// its decision-call prose crosses the presentation boundary. Acting
|
|
4697
|
+
// selections freeze after their work and before reporting begins.
|
|
4698
|
+
if (selection.action === 'respond')
|
|
4699
|
+
freezeControllerEvidence();
|
|
4700
|
+
const settlement = await executeSelection(selection, signal);
|
|
4701
|
+
return finalizeSettlement(settlement);
|
|
3385
4702
|
}
|
|
3386
4703
|
catch (error) {
|
|
3387
4704
|
if (turn?.presentationError === error)
|
|
@@ -3405,7 +4722,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3405
4722
|
turn.settlementFacts.push(...runFailureFacts.splice(0));
|
|
3406
4723
|
}
|
|
3407
4724
|
const mayHaveApplied = selection.action !== 'respond' &&
|
|
3408
|
-
turn
|
|
4725
|
+
containsEffectThrow(turn, error);
|
|
3409
4726
|
turn.settlementFacts.push(mayHaveApplied
|
|
3410
4727
|
? `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.`
|
|
3411
4728
|
: `The ${selection.action} action failed before its complete outcome could be confirmed: ${normalized.name}: ${compactEvidence(normalized.message)}. It was not repeated automatically.`);
|
|
@@ -3440,7 +4757,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3440
4757
|
};
|
|
3441
4758
|
turn.settled = true;
|
|
3442
4759
|
lastSettlementStatus = 'failed';
|
|
3443
|
-
|
|
4760
|
+
const settlement = {
|
|
3444
4761
|
status: 'failed',
|
|
3445
4762
|
facts: [...turn.settlementFacts],
|
|
3446
4763
|
...(turn.report.receipt === undefined
|
|
@@ -3448,6 +4765,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3448
4765
|
: { receipt: turn.report.receipt }),
|
|
3449
4766
|
...(summary === undefined ? {} : { leafStateSummary: summary }),
|
|
3450
4767
|
};
|
|
4768
|
+
return finalizeSettlement(settlement);
|
|
3451
4769
|
}
|
|
3452
4770
|
finally {
|
|
3453
4771
|
runFailureFacts = undefined;
|
|
@@ -3505,6 +4823,46 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3505
4823
|
: { leafStateSummary: leafStateSummary() }),
|
|
3506
4824
|
};
|
|
3507
4825
|
}
|
|
4826
|
+
refreshRetainedEffectFence();
|
|
4827
|
+
const fencedLeaf = leafFrame();
|
|
4828
|
+
const routesRetainedReconciliation = selection.action === 'runtime' &&
|
|
4829
|
+
(selection.actionId === UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID ||
|
|
4830
|
+
selection.actionId === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID) &&
|
|
4831
|
+
fencedLeaf !== undefined;
|
|
4832
|
+
if (retainedEffectReconciliation !== undefined &&
|
|
4833
|
+
!routesRetainedReconciliation) {
|
|
4834
|
+
return rejectSelection(selection, 'retained work must reconcile its repository-effect evidence before an ordinary action can run');
|
|
4835
|
+
}
|
|
4836
|
+
if (selection.action === 'resume') {
|
|
4837
|
+
const entry = byId.get(selection.playbookId);
|
|
4838
|
+
if (entry === undefined) {
|
|
4839
|
+
return rejectSelection(selection, `"${selection.playbookId}" is not an enabled playbook`);
|
|
4840
|
+
}
|
|
4841
|
+
if (rootFrame() !== undefined) {
|
|
4842
|
+
return rejectSelection(selection, 'a playbook is already engaged; its live actions take precedence');
|
|
4843
|
+
}
|
|
4844
|
+
const offer = retainedGenerationOffers.get(entry.id);
|
|
4845
|
+
if (offer === undefined) {
|
|
4846
|
+
return rejectSelection(selection, `/${enablementById.get(entry.id).command} has no resumable retained generation`);
|
|
4847
|
+
}
|
|
4848
|
+
turn.settled = true;
|
|
4849
|
+
journalAction({ action: 'resume', playbookId: entry.id });
|
|
4850
|
+
facts.push(...(await adoptRetainedGeneration(entry.id, offer)));
|
|
4851
|
+
const summary = leafStateSummary();
|
|
4852
|
+
turn.report = {
|
|
4853
|
+
...emptyReport(),
|
|
4854
|
+
facts,
|
|
4855
|
+
status: 'ok',
|
|
4856
|
+
...(summary === undefined ? {} : { leafStateSummary: summary }),
|
|
4857
|
+
};
|
|
4858
|
+
journalOutcome([...facts]);
|
|
4859
|
+
lastSettlementStatus = 'ok';
|
|
4860
|
+
return {
|
|
4861
|
+
status: 'ok',
|
|
4862
|
+
facts: [...facts],
|
|
4863
|
+
...(summary === undefined ? {} : { leafStateSummary: summary }),
|
|
4864
|
+
};
|
|
4865
|
+
}
|
|
3508
4866
|
if (selection.action === 'start' || selection.action === 'switch') {
|
|
3509
4867
|
const entry = byId.get(selection.playbookId);
|
|
3510
4868
|
if (!entry) {
|
|
@@ -3726,6 +5084,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3726
5084
|
if (outcome.error !== undefined)
|
|
3727
5085
|
throw outcome.error;
|
|
3728
5086
|
const receipt = outcome.result;
|
|
5087
|
+
refreshRetainedEffectFence();
|
|
3729
5088
|
let status = receipt.disposition === 'executed'
|
|
3730
5089
|
? 'ok'
|
|
3731
5090
|
: receipt.disposition === 'rejected'
|
|
@@ -3745,9 +5104,47 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3745
5104
|
}
|
|
3746
5105
|
: {}),
|
|
3747
5106
|
};
|
|
5107
|
+
const unresolvedAbandonment = receipt.disposition === 'executed' &&
|
|
5108
|
+
actionId === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID;
|
|
3748
5109
|
if (receipt.disposition === 'executed') {
|
|
3749
|
-
|
|
5110
|
+
if (unresolvedAbandonment &&
|
|
5111
|
+
receipt.run?.outcome !== 'unresolved-effect') {
|
|
5112
|
+
throw new Error('unresolved-effect abandonment returned no unresolved-effect result');
|
|
5113
|
+
}
|
|
3750
5114
|
const establishedSummary = leafStateSummary();
|
|
5115
|
+
if (unresolvedAbandonment) {
|
|
5116
|
+
try {
|
|
5117
|
+
await settleUnresolvedEffectAbandonment(leaf);
|
|
5118
|
+
}
|
|
5119
|
+
catch (error) {
|
|
5120
|
+
abandonmentSettlementUnsafe = true;
|
|
5121
|
+
const normalized = normalizeErrorCompact(error) ?? {
|
|
5122
|
+
name: 'Error',
|
|
5123
|
+
message: String(error),
|
|
5124
|
+
};
|
|
5125
|
+
// The runtime action was accepted, but its distinct host-level
|
|
5126
|
+
// settlement did not complete. Do not expose an `executed` control
|
|
5127
|
+
// receipt until both disposal and durable publication have
|
|
5128
|
+
// succeeded.
|
|
5129
|
+
turn.report = {
|
|
5130
|
+
...outcome.report,
|
|
5131
|
+
facts: [...facts],
|
|
5132
|
+
bossFacts: facts.map((fact) => fact
|
|
5133
|
+
.split(`"${actionId}"`)
|
|
5134
|
+
.join(`"${compactEvidence(actionLabel)}"`)),
|
|
5135
|
+
status: 'failed',
|
|
5136
|
+
receipt: {
|
|
5137
|
+
disposition: 'failed',
|
|
5138
|
+
error: normalized,
|
|
5139
|
+
},
|
|
5140
|
+
...(establishedSummary === undefined
|
|
5141
|
+
? {}
|
|
5142
|
+
: { leafStateSummary: establishedSummary }),
|
|
5143
|
+
};
|
|
5144
|
+
throw error;
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5147
|
+
facts.push(`Applied "${actionId}" on ${frameLabel(leaf)}.`);
|
|
3751
5148
|
// Execution is now proven. Preserve that receipt and the counts already
|
|
3752
5149
|
// collected before processing the returned run, because disposal,
|
|
3753
5150
|
// telemetry, or parent resumption can still fail afterward.
|
|
@@ -3763,7 +5160,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3763
5160
|
? {}
|
|
3764
5161
|
: { leafStateSummary: establishedSummary }),
|
|
3765
5162
|
};
|
|
3766
|
-
if (
|
|
5163
|
+
if (!unresolvedAbandonment &&
|
|
5164
|
+
receipt.run !== undefined &&
|
|
5165
|
+
retainedEffectReconciliation === undefined) {
|
|
3767
5166
|
// The same rule as the drive path: processing the run the receipt
|
|
3768
5167
|
// carried is not itself an effect, and the resume or disposal it may
|
|
3769
5168
|
// perform is marked where it happens (CAPTAIN-35).
|
|
@@ -3959,6 +5358,25 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3959
5358
|
if (!isDeepStrictEqual(frame.roleBindings, configuredBindings)) {
|
|
3960
5359
|
throw new TypeError(`Captain shell snapshot frame ${JSON.stringify(frame.playbookId)} role bindings changed`);
|
|
3961
5360
|
}
|
|
5361
|
+
if (!isDeepStrictEqual(frame.runtime.effectLedger, snapshot.effectLedger)) {
|
|
5362
|
+
throw new TypeError(`Captain shell snapshot frame ${JSON.stringify(frame.playbookId)} effect ledger does not match its artifact schema`);
|
|
5363
|
+
}
|
|
5364
|
+
const runtimeReconciliation = frame.runtime.retainedEffectReconciliation;
|
|
5365
|
+
if (snapshot.retainedEffectReconciliation === undefined) {
|
|
5366
|
+
if (runtimeReconciliation !== undefined) {
|
|
5367
|
+
throw new TypeError(`Captain shell snapshot frame ${JSON.stringify(frame.playbookId)} carries an unmirrored retained-effect fence`);
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
else if (runtimeReconciliation === undefined ||
|
|
5371
|
+
!isDeepStrictEqual(runtimeReconciliation.checkpoint, snapshot.retainedEffectReconciliation.checkpoint)) {
|
|
5372
|
+
throw new TypeError(`Captain shell snapshot frame ${JSON.stringify(frame.playbookId)} does not mirror the root retained-effect fence`);
|
|
5373
|
+
}
|
|
5374
|
+
if (frame.depth === 0 &&
|
|
5375
|
+
snapshot.retainedEffectReconciliation !== undefined &&
|
|
5376
|
+
runtimeReconciliation?.sourceSessionId !==
|
|
5377
|
+
snapshot.retainedEffectReconciliation.sourceGenerationId) {
|
|
5378
|
+
throw new TypeError('Captain shell snapshot retained-effect root source identity differs from its generation');
|
|
5379
|
+
}
|
|
3962
5380
|
}
|
|
3963
5381
|
};
|
|
3964
5382
|
const safeCapturePoint = () => {
|
|
@@ -4013,33 +5431,33 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4013
5431
|
frame.abortListener !== undefined)));
|
|
4014
5432
|
});
|
|
4015
5433
|
};
|
|
4016
|
-
const
|
|
4017
|
-
|
|
4018
|
-
!captainRuntime ||
|
|
4019
|
-
!captainSessionId ||
|
|
4020
|
-
!captainAgent) {
|
|
4021
|
-
return undefined;
|
|
4022
|
-
}
|
|
5434
|
+
const captureFrameSnapshots = (requireRecordedState) => {
|
|
5435
|
+
const captured = [];
|
|
4023
5436
|
try {
|
|
4024
|
-
|
|
4025
|
-
typeof captainRuntime.restore !== 'function') {
|
|
4026
|
-
return undefined;
|
|
4027
|
-
}
|
|
4028
|
-
const captainSnapshot = captainRuntime.exportSnapshot();
|
|
4029
|
-
if (captainSnapshot === undefined)
|
|
4030
|
-
return undefined;
|
|
4031
|
-
const frameSnapshots = [];
|
|
4032
|
-
for (const frame of frames) {
|
|
5437
|
+
for (const [index, frame] of frames.entries()) {
|
|
4033
5438
|
if (typeof frame.runtime.exportSnapshot !== 'function' ||
|
|
4034
5439
|
typeof frame.runtime.restore !== 'function') {
|
|
4035
5440
|
return undefined;
|
|
4036
5441
|
}
|
|
4037
|
-
const
|
|
4038
|
-
if (
|
|
4039
|
-
|
|
5442
|
+
const exported = frame.runtime.exportSnapshot();
|
|
5443
|
+
if (exported === undefined)
|
|
5444
|
+
return undefined;
|
|
5445
|
+
const runtime = assertPlaybookRuntimeSnapshot(exported, frame.entry.id, { allowSuspendedCall: true });
|
|
5446
|
+
if (runtime.state.status !== 'active' ||
|
|
5447
|
+
!runtime.state.quiescent ||
|
|
5448
|
+
(requireRecordedState && frame.state === undefined) ||
|
|
5449
|
+
(frame.state !== undefined &&
|
|
5450
|
+
!isDeepStrictEqual(frame.state, runtime.state))) {
|
|
5451
|
+
return undefined;
|
|
5452
|
+
}
|
|
5453
|
+
const isLeaf = index === frames.length - 1;
|
|
5454
|
+
if ((isLeaf &&
|
|
5455
|
+
(runtime.suspendedCall !== undefined ||
|
|
5456
|
+
!runtime.state.tags.includes('playbook.parked'))) ||
|
|
5457
|
+
(!isLeaf && runtime.suspendedCall === undefined)) {
|
|
4040
5458
|
return undefined;
|
|
4041
5459
|
}
|
|
4042
|
-
|
|
5460
|
+
captured.push({
|
|
4043
5461
|
playbookId: frame.entry.id,
|
|
4044
5462
|
sessionId: frame.sessionId,
|
|
4045
5463
|
rootSessionId: frame.rootSessionId,
|
|
@@ -4058,8 +5476,198 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4058
5476
|
runtime,
|
|
4059
5477
|
});
|
|
4060
5478
|
}
|
|
5479
|
+
for (let index = 1; index < captured.length; index += 1) {
|
|
5480
|
+
const parent = captured[index - 1];
|
|
5481
|
+
const child = captured[index];
|
|
5482
|
+
if (child.parentSessionId !== parent.sessionId ||
|
|
5483
|
+
child.parentCallId === undefined ||
|
|
5484
|
+
parent.runtime.suspendedCall?.callId !== child.parentCallId ||
|
|
5485
|
+
parent.runtime.suspendedCall.playbookId !== child.playbookId ||
|
|
5486
|
+
parent.runtime.suspendedCall.childSessionId !== child.sessionId) {
|
|
5487
|
+
return undefined;
|
|
5488
|
+
}
|
|
5489
|
+
}
|
|
5490
|
+
return captured;
|
|
5491
|
+
}
|
|
5492
|
+
catch {
|
|
5493
|
+
return undefined;
|
|
5494
|
+
}
|
|
5495
|
+
};
|
|
5496
|
+
const refreshRetainedEffectFence = (capturedFrames, capturedLedger) => {
|
|
5497
|
+
const fence = retainedEffectReconciliation;
|
|
5498
|
+
if (fence === undefined)
|
|
5499
|
+
return;
|
|
5500
|
+
try {
|
|
5501
|
+
const ledger = capturedLedger ?? assertPlaybookEffectLedger(currentEffectLedger());
|
|
5502
|
+
if (!retainedEffectLedgerCanRebase(fence.checkpoint, ledger)) {
|
|
5503
|
+
return;
|
|
5504
|
+
}
|
|
5505
|
+
const snapshots = capturedFrames ?? captureFrameSnapshots(true);
|
|
5506
|
+
if (snapshots === undefined || snapshots.length !== frames.length)
|
|
5507
|
+
return;
|
|
5508
|
+
for (const frameSnapshot of snapshots) {
|
|
5509
|
+
const runtime = frameSnapshot.runtime;
|
|
5510
|
+
if (!isDeepStrictEqual(runtime.effectLedger, ledger) ||
|
|
5511
|
+
runtime.retainedEffectSourceSessionId === undefined ||
|
|
5512
|
+
runtime.retainedEffectReconciliation !== undefined) {
|
|
5513
|
+
return;
|
|
5514
|
+
}
|
|
5515
|
+
}
|
|
5516
|
+
const retainedQuestions = snapshots.at(-1).runtime.pendingBossQuestions;
|
|
5517
|
+
const restoredQuestions = retainedQuestions.length === 0
|
|
5518
|
+
? undefined
|
|
5519
|
+
: mirroredBossQuestions(retainedQuestions);
|
|
5520
|
+
pendingBossQuestions = restoredQuestions;
|
|
5521
|
+
retainedEffectReconciliation = undefined;
|
|
5522
|
+
}
|
|
5523
|
+
catch {
|
|
5524
|
+
// Any host-ledger or runtime-snapshot defect leaves the root fence shut.
|
|
5525
|
+
}
|
|
5526
|
+
};
|
|
5527
|
+
const rootStateDescriptionForRetention = (root, retainedState) => {
|
|
5528
|
+
if (typeof root.runtime.describe !== 'function')
|
|
5529
|
+
return undefined;
|
|
5530
|
+
try {
|
|
5531
|
+
const view = root.runtime.describe();
|
|
5532
|
+
if (!isDeepStrictEqual(view.state, retainedState))
|
|
5533
|
+
return undefined;
|
|
5534
|
+
return typeof view.stateDescription === 'string' &&
|
|
5535
|
+
view.stateDescription.trim().length > 0
|
|
5536
|
+
? view.stateDescription
|
|
5537
|
+
: undefined;
|
|
5538
|
+
}
|
|
5539
|
+
catch {
|
|
5540
|
+
return undefined;
|
|
5541
|
+
}
|
|
5542
|
+
};
|
|
5543
|
+
const retainedGenerationFromFrames = (root, frameSnapshots) => {
|
|
5544
|
+
const rootStateDescription = rootStateDescriptionForRetention(root, frameSnapshots[0].runtime.state);
|
|
5545
|
+
const checkpoint = retainedEffectReconciliation?.checkpoint ??
|
|
5546
|
+
assertPlaybookEffectLedger(currentEffectLedger());
|
|
5547
|
+
if (checkpoint.boundaries.some(({ physicalReceipt }) => physicalReceipt === undefined)) {
|
|
5548
|
+
throw new TypeError('Captain retained-generation checkpoint contains an incomplete physical boundary');
|
|
5549
|
+
}
|
|
5550
|
+
return snapshotJsonValue({
|
|
5551
|
+
effectLedger: checkpoint,
|
|
5552
|
+
frames: frameSnapshots,
|
|
5553
|
+
...(retainedEffectReconciliation === undefined
|
|
5554
|
+
? {}
|
|
5555
|
+
: {
|
|
5556
|
+
retainedEffectReconciliation: {
|
|
5557
|
+
sourceGenerationId: retainedEffectReconciliation.sourceGenerationId,
|
|
5558
|
+
},
|
|
5559
|
+
}),
|
|
5560
|
+
...(rootStateDescription === undefined
|
|
5561
|
+
? {}
|
|
5562
|
+
: { rootStateDescription }),
|
|
5563
|
+
}, 'Captain retained generation');
|
|
5564
|
+
};
|
|
5565
|
+
const captureRetainedGeneration = () => {
|
|
5566
|
+
const root = rootFrame();
|
|
5567
|
+
if (!root ||
|
|
5568
|
+
frames.some((frame) => !runtimeRetainsGenerations(frame.runtime))) {
|
|
5569
|
+
return undefined;
|
|
5570
|
+
}
|
|
5571
|
+
const frameSnapshots = captureFrameSnapshots(true);
|
|
5572
|
+
if (frameSnapshots === undefined || frameSnapshots.length === 0) {
|
|
5573
|
+
return undefined;
|
|
5574
|
+
}
|
|
5575
|
+
return retainedGenerationFromFrames(root, frameSnapshots);
|
|
5576
|
+
};
|
|
5577
|
+
const rememberRetainedGeneration = () => {
|
|
5578
|
+
const root = rootFrame();
|
|
5579
|
+
if (!root)
|
|
5580
|
+
return;
|
|
5581
|
+
if (frames.some((frame) => !runtimeRetainsGenerations(frame.runtime))) {
|
|
5582
|
+
retainedGenerationCandidates.set(root.entry.id, {
|
|
5583
|
+
status: 'incapable',
|
|
5584
|
+
});
|
|
5585
|
+
return;
|
|
5586
|
+
}
|
|
5587
|
+
const generation = captureRetainedGeneration();
|
|
5588
|
+
retainedGenerationCandidates.set(root.entry.id, generation === undefined
|
|
5589
|
+
? { status: 'unsafe' }
|
|
5590
|
+
: { status: 'captured', generation });
|
|
5591
|
+
};
|
|
5592
|
+
const retentionUpdateForPriorGeneration = (root) => {
|
|
5593
|
+
const rootPlaybookId = root.entry.id;
|
|
5594
|
+
if (!runtimeRetainsGenerations(root.runtime)) {
|
|
5595
|
+
return {
|
|
5596
|
+
kind: 'clear',
|
|
5597
|
+
rootPlaybookId,
|
|
5598
|
+
};
|
|
5599
|
+
}
|
|
5600
|
+
const candidate = retainedGenerationCandidates.get(rootPlaybookId);
|
|
5601
|
+
if (candidate?.status === 'incapable') {
|
|
5602
|
+
return undefined;
|
|
5603
|
+
}
|
|
5604
|
+
if (candidate?.status !== 'captured') {
|
|
5605
|
+
throw new Error(`${frameLabel(root)} could not capture its pre-terminal retained generation`);
|
|
5606
|
+
}
|
|
5607
|
+
return {
|
|
5608
|
+
kind: 'retain',
|
|
5609
|
+
rootPlaybookId,
|
|
5610
|
+
generation: candidate.generation,
|
|
5611
|
+
};
|
|
5612
|
+
};
|
|
5613
|
+
const retainOrClearDisposedRoot = (root) => {
|
|
5614
|
+
const update = retentionUpdateForPriorGeneration(root);
|
|
5615
|
+
if (update !== undefined) {
|
|
5616
|
+
pendingRetentionUpdates.set(update.rootPlaybookId, update);
|
|
5617
|
+
}
|
|
5618
|
+
};
|
|
5619
|
+
const recordTerminalRetention = (root, result) => {
|
|
5620
|
+
const rootPlaybookId = root.entry.id;
|
|
5621
|
+
if (!runtimeRetainsGenerations(root.runtime)) {
|
|
5622
|
+
pendingRetentionUpdates.set(rootPlaybookId, {
|
|
5623
|
+
kind: 'clear',
|
|
5624
|
+
rootPlaybookId,
|
|
5625
|
+
});
|
|
5626
|
+
return;
|
|
5627
|
+
}
|
|
5628
|
+
const terminalStateId = result.state.stateId;
|
|
5629
|
+
if (typeof terminalStateId !== 'string' ||
|
|
5630
|
+
terminalStateId.trim().length === 0) {
|
|
5631
|
+
throw new Error(`${frameLabel(root)} terminal result has no stable state id for retention`);
|
|
5632
|
+
}
|
|
5633
|
+
const unfinished = root.runtime.retainedGenerationMetadata.unfinishedFinalStateIds.includes(terminalStateId);
|
|
5634
|
+
if (unfinished) {
|
|
5635
|
+
// A root opened and terminated within this turn has no pre-turn,
|
|
5636
|
+
// work-bearing generation. Leave any earlier store entry untouched.
|
|
5637
|
+
if (!retainedGenerationCandidates.has(rootPlaybookId))
|
|
5638
|
+
return;
|
|
5639
|
+
retainOrClearDisposedRoot(root);
|
|
5640
|
+
}
|
|
5641
|
+
else {
|
|
5642
|
+
pendingRetentionUpdates.set(rootPlaybookId, {
|
|
5643
|
+
kind: 'clear',
|
|
5644
|
+
rootPlaybookId,
|
|
5645
|
+
});
|
|
5646
|
+
}
|
|
5647
|
+
};
|
|
5648
|
+
const exportShellSnapshot = () => {
|
|
5649
|
+
if (!safeCapturePoint() ||
|
|
5650
|
+
!captainRuntime ||
|
|
5651
|
+
!captainSessionId ||
|
|
5652
|
+
!captainAgent) {
|
|
5653
|
+
return undefined;
|
|
5654
|
+
}
|
|
5655
|
+
try {
|
|
5656
|
+
if (typeof captainRuntime.exportSnapshot !== 'function' ||
|
|
5657
|
+
typeof captainRuntime.restore !== 'function') {
|
|
5658
|
+
return undefined;
|
|
5659
|
+
}
|
|
5660
|
+
const captainSnapshot = captainRuntime.exportSnapshot();
|
|
5661
|
+
if (captainSnapshot === undefined)
|
|
5662
|
+
return undefined;
|
|
5663
|
+
const frameSnapshots = captureFrameSnapshots(true);
|
|
5664
|
+
if (frameSnapshots === undefined)
|
|
5665
|
+
return undefined;
|
|
5666
|
+
const effectLedger = assertPlaybookEffectLedger(currentEffectLedger());
|
|
5667
|
+
refreshRetainedEffectFence(frameSnapshots, effectLedger);
|
|
4061
5668
|
const common = {
|
|
4062
|
-
schemaVersion:
|
|
5669
|
+
schemaVersion: 4,
|
|
5670
|
+
effectLedger,
|
|
4063
5671
|
captain: {
|
|
4064
5672
|
sessionId: captainSessionId,
|
|
4065
5673
|
runtime: captainSnapshot,
|
|
@@ -4081,6 +5689,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4081
5689
|
...common,
|
|
4082
5690
|
mode: 'engaged.parked',
|
|
4083
5691
|
frames: frameSnapshots,
|
|
5692
|
+
...(retainedEffectReconciliation === undefined
|
|
5693
|
+
? {}
|
|
5694
|
+
: { retainedEffectReconciliation }),
|
|
4084
5695
|
...(pendingBossQuestions === undefined
|
|
4085
5696
|
? {}
|
|
4086
5697
|
: { pendingBossQuestions: pendingBossQuestions }),
|
|
@@ -4094,6 +5705,64 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4094
5705
|
return undefined;
|
|
4095
5706
|
}
|
|
4096
5707
|
};
|
|
5708
|
+
const exportSettlement = () => {
|
|
5709
|
+
if (!retentionSettlementReady || abandonmentSettlementUnsafe) {
|
|
5710
|
+
return undefined;
|
|
5711
|
+
}
|
|
5712
|
+
const snapshot = exportShellSnapshot();
|
|
5713
|
+
if (snapshot === undefined)
|
|
5714
|
+
return undefined;
|
|
5715
|
+
let unresolvedEffects;
|
|
5716
|
+
try {
|
|
5717
|
+
unresolvedEffects =
|
|
5718
|
+
settledTurnUnresolvedEffects ?? currentUnresolvedEffects();
|
|
5719
|
+
}
|
|
5720
|
+
catch {
|
|
5721
|
+
return undefined;
|
|
5722
|
+
}
|
|
5723
|
+
const updates = new Map(pendingRetentionUpdates);
|
|
5724
|
+
for (const rootPlaybookId of retainedGenerationRootClears) {
|
|
5725
|
+
updates.set(rootPlaybookId, { kind: 'clear', rootPlaybookId });
|
|
5726
|
+
}
|
|
5727
|
+
const root = rootFrame();
|
|
5728
|
+
if (root !== undefined) {
|
|
5729
|
+
const rootPlaybookId = root.entry.id;
|
|
5730
|
+
if (frames.every((frame) => runtimeRetainsGenerations(frame.runtime))) {
|
|
5731
|
+
const generation = snapshot.mode === 'engaged.parked'
|
|
5732
|
+
? retainedGenerationFromFrames(root, snapshot.frames)
|
|
5733
|
+
: undefined;
|
|
5734
|
+
if (generation !== undefined) {
|
|
5735
|
+
updates.set(rootPlaybookId, {
|
|
5736
|
+
kind: 'retain',
|
|
5737
|
+
rootPlaybookId,
|
|
5738
|
+
generation,
|
|
5739
|
+
});
|
|
5740
|
+
}
|
|
5741
|
+
}
|
|
5742
|
+
else if (!runtimeRetainsGenerations(root.runtime)) {
|
|
5743
|
+
updates.set(rootPlaybookId, { kind: 'clear', rootPlaybookId });
|
|
5744
|
+
}
|
|
5745
|
+
else if (retainedGenerationCandidates.has(rootPlaybookId)) {
|
|
5746
|
+
try {
|
|
5747
|
+
const update = retentionUpdateForPriorGeneration(root);
|
|
5748
|
+
if (update !== undefined) {
|
|
5749
|
+
updates.set(rootPlaybookId, update);
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5752
|
+
catch {
|
|
5753
|
+
return undefined;
|
|
5754
|
+
}
|
|
5755
|
+
}
|
|
5756
|
+
}
|
|
5757
|
+
for (const update of updates.values()) {
|
|
5758
|
+
applyRetentionUpdateToCatalog(update);
|
|
5759
|
+
}
|
|
5760
|
+
return snapshotJsonValue({
|
|
5761
|
+
snapshot,
|
|
5762
|
+
retentionUpdates: [...updates.values()].sort((left, right) => left.rootPlaybookId.localeCompare(right.rootPlaybookId)),
|
|
5763
|
+
unresolvedEffects,
|
|
5764
|
+
}, 'Captain settlement');
|
|
5765
|
+
};
|
|
4097
5766
|
const verifyRestoredRuntime = (runtime, expected, playbookId, allowSuspendedCall) => {
|
|
4098
5767
|
const actual = runtime.exportSnapshot?.();
|
|
4099
5768
|
if (actual === undefined) {
|
|
@@ -4106,6 +5775,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4106
5775
|
'sequences',
|
|
4107
5776
|
'pendingBossQuestions',
|
|
4108
5777
|
'suspendedCall',
|
|
5778
|
+
'effectLedger',
|
|
5779
|
+
'retainedEffectSourceSessionId',
|
|
5780
|
+
'retainedEffectReconciliation',
|
|
4109
5781
|
]) {
|
|
4110
5782
|
if (!isDeepStrictEqual(normalized[key], expected[key])) {
|
|
4111
5783
|
throw new Error(`restored ${playbookId} runtime changed snapshot field ${key}`);
|
|
@@ -4123,6 +5795,15 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4123
5795
|
cleanupFailures.push(error);
|
|
4124
5796
|
}
|
|
4125
5797
|
}
|
|
5798
|
+
const retainedRuntimes = takeRetainedOfferRuntimes();
|
|
5799
|
+
if (retainedRuntimes.length > 0) {
|
|
5800
|
+
try {
|
|
5801
|
+
await disposeRetainedRuntimeSet(retainedRuntimes, 'retained-generation restore cleanup failed');
|
|
5802
|
+
}
|
|
5803
|
+
catch (error) {
|
|
5804
|
+
cleanupFailures.push(error);
|
|
5805
|
+
}
|
|
5806
|
+
}
|
|
4126
5807
|
if (captainRuntime) {
|
|
4127
5808
|
shuttingDown = true;
|
|
4128
5809
|
try {
|
|
@@ -4140,11 +5821,20 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4140
5821
|
byCommand = new Map();
|
|
4141
5822
|
byId = new Map();
|
|
4142
5823
|
enablementById = new Map();
|
|
5824
|
+
hostCapabilitiesById = new Map();
|
|
5825
|
+
pendingHostCapabilities = undefined;
|
|
5826
|
+
currentEffectLedger = () => emptyPlaybookEffectLedger();
|
|
4143
5827
|
captainAgent = undefined;
|
|
4144
5828
|
captainAdapter = undefined;
|
|
4145
5829
|
playerAgents = new Map();
|
|
4146
5830
|
playerLedger.clear();
|
|
4147
5831
|
playerTransactions.clear();
|
|
5832
|
+
retainedGenerations.clear();
|
|
5833
|
+
ineligibleRetainedGenerations.clear();
|
|
5834
|
+
retainedGenerationRootClears.clear();
|
|
5835
|
+
retainedGenerationsInstalled = false;
|
|
5836
|
+
retainedGenerationInstallationInProgress = false;
|
|
5837
|
+
retainedGenerationInstallationClosed = false;
|
|
4148
5838
|
session = undefined;
|
|
4149
5839
|
sessionEmissionsOpen = false;
|
|
4150
5840
|
closedGateAttempted = false;
|
|
@@ -4152,6 +5842,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4152
5842
|
captainSessionId = undefined;
|
|
4153
5843
|
conversation = { kind: 'unopened' };
|
|
4154
5844
|
mode = 'chat';
|
|
5845
|
+
retainedEffectReconciliation = undefined;
|
|
4155
5846
|
pendingBossQuestions = undefined;
|
|
4156
5847
|
lastError = undefined;
|
|
4157
5848
|
journalSeq = 0;
|
|
@@ -4178,7 +5869,13 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4178
5869
|
lifecycle = 'restoring';
|
|
4179
5870
|
try {
|
|
4180
5871
|
const snapshot = assertPlaybookCaptainShellSnapshot(untrusted);
|
|
4181
|
-
const built = await
|
|
5872
|
+
const built = await buildCurrentEnablements();
|
|
5873
|
+
const builtHostCapabilities = new Map(built.hostCapabilitiesById);
|
|
5874
|
+
const readEffectLedger = () => effectLedgerMirrorFromCapabilities(builtHostCapabilities);
|
|
5875
|
+
const hostLedger = readEffectLedger();
|
|
5876
|
+
if (!isDeepStrictEqual(snapshot.effectLedger, hostLedger)) {
|
|
5877
|
+
throw new Error('Captain shell restore effect ledger does not match current-host authority');
|
|
5878
|
+
}
|
|
4182
5879
|
captainAgent = built.captainAgent;
|
|
4183
5880
|
captainAdapter = captainAgent.adapter;
|
|
4184
5881
|
playerAgents = built.playerAgents;
|
|
@@ -4188,6 +5885,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4188
5885
|
byCommand = built.byCommand;
|
|
4189
5886
|
byId = built.byId;
|
|
4190
5887
|
enablementById = built.enablementById;
|
|
5888
|
+
hostCapabilitiesById = builtHostCapabilities;
|
|
5889
|
+
currentEffectLedger = readEffectLedger;
|
|
4191
5890
|
for (const [playerId, saved] of Object.entries(snapshot.playerSessions)) {
|
|
4192
5891
|
playerLedger.set(playerId, {
|
|
4193
5892
|
adapter: saved.adapter,
|
|
@@ -4267,6 +5966,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4267
5966
|
lastSettlementStatus = snapshot.lastSettlementStatus;
|
|
4268
5967
|
mode = snapshot.mode;
|
|
4269
5968
|
if (snapshot.mode === 'engaged.parked') {
|
|
5969
|
+
retainedEffectReconciliation =
|
|
5970
|
+
snapshot.retainedEffectReconciliation;
|
|
4270
5971
|
pendingBossQuestions = snapshot.pendingBossQuestions;
|
|
4271
5972
|
lastError = snapshot.lastError;
|
|
4272
5973
|
}
|
|
@@ -4282,6 +5983,57 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4282
5983
|
throw error;
|
|
4283
5984
|
}
|
|
4284
5985
|
};
|
|
5986
|
+
const installRetainedGenerations = async (generations) => {
|
|
5987
|
+
if (lifecycle !== 'ready' || terminallyDisposed) {
|
|
5988
|
+
throw new Error('retained generations require an initialized or restored Captain shell');
|
|
5989
|
+
}
|
|
5990
|
+
if (retainedGenerationsInstalled ||
|
|
5991
|
+
retainedGenerationInstallationInProgress ||
|
|
5992
|
+
retainedGenerationInstallationClosed ||
|
|
5993
|
+
activeTurnHostCalls !== undefined) {
|
|
5994
|
+
throw new Error('retained generations may be installed exactly once before the first nonempty Boss turn');
|
|
5995
|
+
}
|
|
5996
|
+
const normalized = normalizeInstalledRetainedGenerations(generations);
|
|
5997
|
+
retainedGenerationInstallationInProgress = true;
|
|
5998
|
+
try {
|
|
5999
|
+
retainedGenerations.clear();
|
|
6000
|
+
retainedGenerationOffers.clear();
|
|
6001
|
+
ineligibleRetainedGenerations.clear();
|
|
6002
|
+
retainedGenerationRootClears.clear();
|
|
6003
|
+
for (const [rootPlaybookId, generation] of normalized) {
|
|
6004
|
+
retainedGenerations.set(rootPlaybookId, generation);
|
|
6005
|
+
}
|
|
6006
|
+
await prepareRetainedGenerationOffers();
|
|
6007
|
+
retainedGenerationsInstalled = true;
|
|
6008
|
+
}
|
|
6009
|
+
catch (error) {
|
|
6010
|
+
const preparationCleanupFailed = error instanceof RetainedRuntimeCleanupError;
|
|
6011
|
+
const runtimes = [...retainedGenerationOffers.values()].flatMap((offer) => [...offer.runtimes]);
|
|
6012
|
+
retainedGenerationOffers.clear();
|
|
6013
|
+
retainedGenerations.clear();
|
|
6014
|
+
ineligibleRetainedGenerations.clear();
|
|
6015
|
+
retainedGenerationRootClears.clear();
|
|
6016
|
+
try {
|
|
6017
|
+
await disposeRetainedRuntimeSet(runtimes, 'retained-generation installation cleanup failed');
|
|
6018
|
+
}
|
|
6019
|
+
catch (cleanupError) {
|
|
6020
|
+
if (cleanupError instanceof RetainedRuntimeCleanupError) {
|
|
6021
|
+
retiredRetainedRuntimes.push(...cleanupError.failedRuntimes);
|
|
6022
|
+
}
|
|
6023
|
+
terminallyDisposed = true;
|
|
6024
|
+
lifecycle = 'closed';
|
|
6025
|
+
throw new AggregateError([error, cleanupError], 'retained-generation installation and cleanup failed');
|
|
6026
|
+
}
|
|
6027
|
+
if (preparationCleanupFailed) {
|
|
6028
|
+
terminallyDisposed = true;
|
|
6029
|
+
lifecycle = 'closed';
|
|
6030
|
+
}
|
|
6031
|
+
throw error;
|
|
6032
|
+
}
|
|
6033
|
+
finally {
|
|
6034
|
+
retainedGenerationInstallationInProgress = false;
|
|
6035
|
+
}
|
|
6036
|
+
};
|
|
4285
6037
|
return {
|
|
4286
6038
|
async init(initSession) {
|
|
4287
6039
|
if (lifecycle !== 'fresh' || terminallyDisposed) {
|
|
@@ -4293,11 +6045,16 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4293
6045
|
lifecycle = 'initializing';
|
|
4294
6046
|
try {
|
|
4295
6047
|
installSession(initSession, true);
|
|
4296
|
-
const built = await
|
|
6048
|
+
const built = await buildCurrentEnablements();
|
|
6049
|
+
const builtHostCapabilities = new Map(built.hostCapabilitiesById);
|
|
6050
|
+
const readEffectLedger = () => effectLedgerMirrorFromCapabilities(builtHostCapabilities);
|
|
6051
|
+
readEffectLedger();
|
|
4297
6052
|
entries = built.entries;
|
|
4298
6053
|
byCommand = built.byCommand;
|
|
4299
6054
|
byId = built.byId;
|
|
4300
6055
|
enablementById = built.enablementById;
|
|
6056
|
+
hostCapabilitiesById = builtHostCapabilities;
|
|
6057
|
+
currentEffectLedger = readEffectLedger;
|
|
4301
6058
|
captainAgent = built.captainAgent;
|
|
4302
6059
|
captainAdapter = captainAgent.adapter;
|
|
4303
6060
|
playerAgents = built.playerAgents;
|
|
@@ -4322,7 +6079,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4322
6079
|
}
|
|
4323
6080
|
},
|
|
4324
6081
|
exportSnapshot: exportShellSnapshot,
|
|
6082
|
+
exportSettlement,
|
|
4325
6083
|
restore: restoreShellSnapshot,
|
|
6084
|
+
installRetainedGenerations,
|
|
4326
6085
|
async handleBossTurn(turn, context) {
|
|
4327
6086
|
if (lifecycle !== 'ready' || terminallyDisposed) {
|
|
4328
6087
|
throw new Error('init must be called first, or restore must complete before handling a Boss turn');
|
|
@@ -4334,10 +6093,25 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4334
6093
|
if (activeTurnHostCalls !== undefined) {
|
|
4335
6094
|
throw new Error('cannot handle concurrent Boss turns');
|
|
4336
6095
|
}
|
|
6096
|
+
if (retainedGenerationInstallationInProgress) {
|
|
6097
|
+
throw new Error('cannot handle a Boss turn while retained generations are installing');
|
|
6098
|
+
}
|
|
4337
6099
|
// Empty or whitespace-only input allocates no call, session, or
|
|
4338
6100
|
// telemetry (CAPTAIN-7).
|
|
6101
|
+
retentionSettlementReady = false;
|
|
6102
|
+
abandonmentSettlementUnsafe = false;
|
|
4339
6103
|
if (turn.prompt.trim().length === 0)
|
|
4340
6104
|
return;
|
|
6105
|
+
settledTurnUnresolvedEffects = undefined;
|
|
6106
|
+
retainedGenerationInstallationClosed = true;
|
|
6107
|
+
for (const update of pendingRetentionUpdates.values()) {
|
|
6108
|
+
applyRetentionUpdateToCatalog(update);
|
|
6109
|
+
}
|
|
6110
|
+
retainedGenerationCandidates.clear();
|
|
6111
|
+
pendingRetentionUpdates.clear();
|
|
6112
|
+
// A terminal or dismissal can remove the whole stack during this turn;
|
|
6113
|
+
// take the latest already-settled generation before controller work.
|
|
6114
|
+
rememberRetainedGeneration();
|
|
4341
6115
|
const turnHostCalls = new Set();
|
|
4342
6116
|
activeTurnHostCalls = turnHostCalls;
|
|
4343
6117
|
activeContext = context;
|
|
@@ -4360,6 +6134,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4360
6134
|
decisionCall = undefined;
|
|
4361
6135
|
appendJournal('boss', turn.prompt);
|
|
4362
6136
|
try {
|
|
6137
|
+
await drainRetiredRetainedRuntimes();
|
|
6138
|
+
await prepareRetainedGenerationOffers();
|
|
4363
6139
|
const result = await captainRuntime.handleBossInput({
|
|
4364
6140
|
text: turn.prompt,
|
|
4365
6141
|
signal: context.signal,
|
|
@@ -4422,17 +6198,22 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4422
6198
|
activeTurnHostCalls = undefined;
|
|
4423
6199
|
}
|
|
4424
6200
|
activeContext = undefined;
|
|
6201
|
+
retentionSettlementReady = true;
|
|
4425
6202
|
}
|
|
4426
6203
|
},
|
|
4427
6204
|
async prepareDispose() {
|
|
4428
|
-
if (lifecycle === 'initializing' ||
|
|
6205
|
+
if (lifecycle === 'initializing' ||
|
|
6206
|
+
lifecycle === 'restoring' ||
|
|
6207
|
+
retainedGenerationInstallationInProgress) {
|
|
4429
6208
|
throw new Error('cannot dispose while Captain shell setup is in progress');
|
|
4430
6209
|
}
|
|
4431
6210
|
activeContext = undefined;
|
|
4432
6211
|
await teardown();
|
|
4433
6212
|
},
|
|
4434
6213
|
async dispose() {
|
|
4435
|
-
if (lifecycle === 'initializing' ||
|
|
6214
|
+
if (lifecycle === 'initializing' ||
|
|
6215
|
+
lifecycle === 'restoring' ||
|
|
6216
|
+
retainedGenerationInstallationInProgress) {
|
|
4436
6217
|
throw new Error('cannot dispose while Captain shell setup is in progress');
|
|
4437
6218
|
}
|
|
4438
6219
|
activeContext = undefined;
|
|
@@ -4451,6 +6232,18 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4451
6232
|
catch (error) {
|
|
4452
6233
|
failure = error;
|
|
4453
6234
|
}
|
|
6235
|
+
const retainedRuntimes = takeRetainedOfferRuntimes();
|
|
6236
|
+
if (retainedRuntimes.length > 0) {
|
|
6237
|
+
try {
|
|
6238
|
+
await disposeRetainedRuntimeSet(retainedRuntimes, 'retained-generation shell cleanup failed');
|
|
6239
|
+
}
|
|
6240
|
+
catch (error) {
|
|
6241
|
+
failure ??= error;
|
|
6242
|
+
}
|
|
6243
|
+
}
|
|
6244
|
+
retainedGenerations.clear();
|
|
6245
|
+
ineligibleRetainedGenerations.clear();
|
|
6246
|
+
retainedGenerationRootClears.clear();
|
|
4454
6247
|
const runtime = captainRuntime;
|
|
4455
6248
|
captainRuntime = undefined;
|
|
4456
6249
|
if (runtime) {
|
|
@@ -4465,6 +6258,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4465
6258
|
// Quarantine is session-wide by design. Only terminal teardown may drop
|
|
4466
6259
|
// its ownership after every frame host call and the Captain are drained.
|
|
4467
6260
|
playerTransactions.clear();
|
|
6261
|
+
hostCapabilitiesById = new Map();
|
|
6262
|
+
pendingHostCapabilities = undefined;
|
|
6263
|
+
currentEffectLedger = () => emptyPlaybookEffectLedger();
|
|
4468
6264
|
lifecycle = 'closed';
|
|
4469
6265
|
if (failure !== undefined)
|
|
4470
6266
|
throw failure;
|