@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
|
@@ -19,6 +19,8 @@ import type { Effort, PermissionPolicy } from '@sublang/cligent';
|
|
|
19
19
|
import type {
|
|
20
20
|
JsonValue,
|
|
21
21
|
NormalizedError,
|
|
22
|
+
PlaybookEffectLedger,
|
|
23
|
+
PlaybookEffectLedgerCommandBatch,
|
|
22
24
|
PlaybookCallRequest,
|
|
23
25
|
PlaybookCallResult,
|
|
24
26
|
PlaybookCallStart,
|
|
@@ -33,7 +35,10 @@ import type {
|
|
|
33
35
|
} from '@sublang/playbook/runtime';
|
|
34
36
|
import {
|
|
35
37
|
assertPlaybookRuntimeSnapshot,
|
|
38
|
+
assertPlaybookEffectLedger,
|
|
39
|
+
emptyPlaybookEffectLedger,
|
|
36
40
|
hiddenControlEnvelope,
|
|
41
|
+
isPlaybookEffectLedgerMonotonicExtension,
|
|
37
42
|
registerPlaybookAbortCleanup,
|
|
38
43
|
snapshotJsonValue,
|
|
39
44
|
validatePlayerResult,
|
|
@@ -50,6 +55,9 @@ interface SessionAgent {
|
|
|
50
55
|
readonly adapter: string;
|
|
51
56
|
readonly model: TuningSelection;
|
|
52
57
|
readonly effort: TuningSelection<Effort>;
|
|
58
|
+
/** Adapter-scoped fast mode. Absence is the provider default; `false` is a
|
|
59
|
+
* literal request, so this carries no provider-default sentinel. */
|
|
60
|
+
readonly fastMode?: boolean;
|
|
53
61
|
readonly instruction?: string;
|
|
54
62
|
readonly permissions?: PermissionPolicy;
|
|
55
63
|
}
|
|
@@ -69,8 +77,27 @@ type DeepReadonly<T> = T extends (...args: never[]) => unknown
|
|
|
69
77
|
? { readonly [Key in keyof T]: DeepReadonly<T[Key]> }
|
|
70
78
|
: T;
|
|
71
79
|
|
|
80
|
+
export interface PlaybookCaptainUnresolvedEffect {
|
|
81
|
+
readonly classification:
|
|
82
|
+
| 'one-descendant-commit'
|
|
83
|
+
| 'multiple-commits'
|
|
84
|
+
| 'rewritten-or-non-descendant'
|
|
85
|
+
| 'worktree-only-change'
|
|
86
|
+
| 'concurrent-or-foreign-change'
|
|
87
|
+
| 'observation-ambiguous'
|
|
88
|
+
| 'incomplete';
|
|
89
|
+
readonly baselineHead: string;
|
|
90
|
+
readonly afterHead?: string;
|
|
91
|
+
readonly commitOid?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface PlaybookCaptainUnresolvedEffectSettlementInput {
|
|
95
|
+
readonly rootPlaybookId: string;
|
|
96
|
+
readonly unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[];
|
|
97
|
+
}
|
|
98
|
+
|
|
72
99
|
type SnapshotAgentEnvelope = DeepReadonly<
|
|
73
|
-
Omit<SessionAgent, 'model' | 'effort'>
|
|
100
|
+
Omit<SessionAgent, 'model' | 'effort' | 'fastMode'>
|
|
74
101
|
>;
|
|
75
102
|
|
|
76
103
|
type PlayerLedgerSnapshotEntry = DeepReadonly<PlayerLedgerEntry>;
|
|
@@ -78,6 +105,9 @@ type PlayerLedgerSnapshotEntry = DeepReadonly<PlayerLedgerEntry>;
|
|
|
78
105
|
export interface PlaybookCaptainDeps {
|
|
79
106
|
loadModule?: (specifier: string) => Promise<unknown>;
|
|
80
107
|
createSessionId?: () => string;
|
|
108
|
+
hostCapabilities?: Readonly<
|
|
109
|
+
Record<string, PlaybookHostConstructionCapabilities>
|
|
110
|
+
>;
|
|
81
111
|
createCaptainRuntime?: (options: {
|
|
82
112
|
readonly enabledPlaybooks: readonly {
|
|
83
113
|
readonly id: string;
|
|
@@ -86,20 +116,81 @@ export interface PlaybookCaptainDeps {
|
|
|
86
116
|
}[];
|
|
87
117
|
readonly controller: CaptainControllerPort;
|
|
88
118
|
}) => PlaybookRuntime;
|
|
119
|
+
unresolvedEffectSettlement?: {
|
|
120
|
+
begin(input: PlaybookCaptainUnresolvedEffectSettlementInput): Promise<void>;
|
|
121
|
+
complete(input: PlaybookCaptainUnresolvedEffectSettlementInput): Promise<void>;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Live, artifact-typed host facilities supplied outside configured options. */
|
|
126
|
+
export interface PlaybookHostConstructionCapabilities {
|
|
127
|
+
readonly authority: {
|
|
128
|
+
readonly playbookId: string;
|
|
129
|
+
readonly artifactSchema: 3;
|
|
130
|
+
readonly cwd: string;
|
|
131
|
+
readonly sessionId: string;
|
|
132
|
+
readonly leaseOwnerToken: string;
|
|
133
|
+
readonly canonicalWorktree: {
|
|
134
|
+
readonly worktree: string;
|
|
135
|
+
readonly gitDir: string;
|
|
136
|
+
};
|
|
137
|
+
readonly requiredRoleIds: readonly string[];
|
|
138
|
+
readonly concurrentRoleSets: readonly (readonly string[])[];
|
|
139
|
+
};
|
|
140
|
+
readonly repository: {
|
|
141
|
+
readonly identity: {
|
|
142
|
+
readonly worktree: string;
|
|
143
|
+
readonly gitDir: string;
|
|
144
|
+
};
|
|
145
|
+
readonly observe: (options?: unknown) => Promise<unknown>;
|
|
146
|
+
readonly acquire: (options?: unknown) => Promise<unknown>;
|
|
147
|
+
readonly runExclusive: (options: unknown) => Promise<unknown>;
|
|
148
|
+
readonly runCohort: (options: unknown) => Promise<unknown>;
|
|
149
|
+
readonly runDeferred: (options: unknown) => Promise<unknown>;
|
|
150
|
+
};
|
|
151
|
+
readonly effectLedger: {
|
|
152
|
+
readonly snapshot: () => PlaybookEffectLedger;
|
|
153
|
+
readonly writeAhead: (
|
|
154
|
+
commands: PlaybookEffectLedgerCommandBatch,
|
|
155
|
+
) => Promise<PlaybookEffectLedger>;
|
|
156
|
+
};
|
|
89
157
|
}
|
|
90
158
|
|
|
91
|
-
export
|
|
159
|
+
export type PlaybookCaptainRuntimeProfile =
|
|
160
|
+
| {
|
|
161
|
+
readonly kind: 'shared-factory';
|
|
162
|
+
readonly compat: {
|
|
163
|
+
readonly artifactSchema: 3;
|
|
164
|
+
readonly runtimeAbi: number;
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
| {
|
|
168
|
+
readonly kind: 'bespoke';
|
|
169
|
+
readonly artifactSchema: 3;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
interface PlaybookCaptainRegistryEntryBase {
|
|
92
173
|
id: string;
|
|
93
174
|
command: string;
|
|
94
175
|
intent: string;
|
|
95
|
-
|
|
176
|
+
runtimeProfile: PlaybookCaptainRuntimeProfile;
|
|
96
177
|
requiredRoleIds: readonly string[];
|
|
97
178
|
concurrentRoleSets: readonly (readonly string[])[];
|
|
98
179
|
summaryPolicy?: PlaybookSummaryPolicy;
|
|
99
180
|
validateOptions(optionSlice: unknown): unknown;
|
|
100
|
-
createRuntime(options: unknown): PlaybookRuntime;
|
|
101
181
|
}
|
|
102
182
|
|
|
183
|
+
export interface PlaybookCaptainRegistryEntryV3
|
|
184
|
+
extends PlaybookCaptainRegistryEntryBase {
|
|
185
|
+
artifactSchema: 3;
|
|
186
|
+
createRuntime(
|
|
187
|
+
configuredOptions: unknown,
|
|
188
|
+
hostCapabilities: PlaybookHostConstructionCapabilities,
|
|
189
|
+
): PlaybookRuntime;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export type PlaybookCaptainRegistryEntry = PlaybookCaptainRegistryEntryV3;
|
|
193
|
+
|
|
103
194
|
type PlaybookCaptainConversationSnapshot =
|
|
104
195
|
| { readonly kind: 'unopened' }
|
|
105
196
|
| { readonly kind: 'pinned'; readonly token: string }
|
|
@@ -117,7 +208,7 @@ interface PlaybookCaptainJournalRecord {
|
|
|
117
208
|
readonly payload: JsonValue;
|
|
118
209
|
}
|
|
119
210
|
|
|
120
|
-
interface PlaybookCaptainFrameSnapshot {
|
|
211
|
+
export interface PlaybookCaptainFrameSnapshot {
|
|
121
212
|
readonly playbookId: string;
|
|
122
213
|
readonly sessionId: string;
|
|
123
214
|
readonly rootSessionId: string;
|
|
@@ -130,7 +221,8 @@ interface PlaybookCaptainFrameSnapshot {
|
|
|
130
221
|
}
|
|
131
222
|
|
|
132
223
|
interface PlaybookCaptainShellSnapshotFields {
|
|
133
|
-
readonly schemaVersion:
|
|
224
|
+
readonly schemaVersion: 4;
|
|
225
|
+
readonly effectLedger: DeepReadonly<PlaybookEffectLedger>;
|
|
134
226
|
readonly captain: {
|
|
135
227
|
readonly sessionId: string;
|
|
136
228
|
readonly runtime: DeepReadonly<PlaybookRuntimeSnapshot>;
|
|
@@ -149,6 +241,7 @@ interface PlaybookCaptainShellSnapshotFields {
|
|
|
149
241
|
| 'respond'
|
|
150
242
|
| 'start'
|
|
151
243
|
| 'switch'
|
|
244
|
+
| 'resume'
|
|
152
245
|
| 'dismiss'
|
|
153
246
|
| 'deliver'
|
|
154
247
|
| 'runtime';
|
|
@@ -172,6 +265,7 @@ type PlaybookCaptainShellSnapshotValue =
|
|
|
172
265
|
readonly mode: 'engaged.parked';
|
|
173
266
|
/** Root-to-leaf engagement order. */
|
|
174
267
|
readonly frames: readonly PlaybookCaptainFrameSnapshot[];
|
|
268
|
+
readonly retainedEffectReconciliation?: PlaybookCaptainRetainedEffectReconciliation;
|
|
175
269
|
readonly pendingBossQuestions?: JsonValue;
|
|
176
270
|
readonly lastError?: { readonly name: string; readonly message: string };
|
|
177
271
|
}
|
|
@@ -180,9 +274,76 @@ type PlaybookCaptainShellSnapshotValue =
|
|
|
180
274
|
export type PlaybookCaptainShellSnapshot =
|
|
181
275
|
DeepReadonly<PlaybookCaptainShellSnapshotValue>;
|
|
182
276
|
|
|
277
|
+
export interface PlaybookCaptainRetainedGeneration {
|
|
278
|
+
/** Repository-effect checkpoint reflected by the retained machine state. */
|
|
279
|
+
readonly effectLedger: DeepReadonly<PlaybookEffectLedger>;
|
|
280
|
+
readonly frames: readonly PlaybookCaptainFrameSnapshot[];
|
|
281
|
+
readonly retainedEffectReconciliation?: {
|
|
282
|
+
readonly sourceGenerationId: string;
|
|
283
|
+
};
|
|
284
|
+
/** Boss-facing description published for the retained root state, if any. */
|
|
285
|
+
readonly rootStateDescription?: string;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
interface PlaybookCaptainRetainedEffectReconciliation {
|
|
289
|
+
readonly sourceGenerationId: string;
|
|
290
|
+
readonly checkpoint: DeepReadonly<PlaybookEffectLedger>;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function retainedEffectLedgerCanRebase(
|
|
294
|
+
checkpoint: PlaybookEffectLedger,
|
|
295
|
+
current: PlaybookEffectLedger,
|
|
296
|
+
): boolean {
|
|
297
|
+
if (
|
|
298
|
+
checkpoint.boundaries.some(
|
|
299
|
+
({ physicalReceipt }) => physicalReceipt === undefined,
|
|
300
|
+
)
|
|
301
|
+
) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
if (!isPlaybookEffectLedgerMonotonicExtension(checkpoint, current)) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
if (
|
|
308
|
+
!isDeepStrictEqual(
|
|
309
|
+
current.boundaries.slice(0, checkpoint.boundaries.length),
|
|
310
|
+
checkpoint.boundaries,
|
|
311
|
+
) ||
|
|
312
|
+
!isDeepStrictEqual(current.logicalOperations, checkpoint.logicalOperations)
|
|
313
|
+
) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
return current.boundaries
|
|
317
|
+
.slice(checkpoint.boundaries.length)
|
|
318
|
+
.every(
|
|
319
|
+
({ physicalReceipt }) =>
|
|
320
|
+
physicalReceipt?.classification === 'unchanged',
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export type PlaybookCaptainRetentionUpdate =
|
|
325
|
+
| {
|
|
326
|
+
readonly kind: 'retain';
|
|
327
|
+
readonly rootPlaybookId: string;
|
|
328
|
+
readonly generation: PlaybookCaptainRetainedGeneration;
|
|
329
|
+
}
|
|
330
|
+
| { readonly kind: 'clear'; readonly rootPlaybookId: string };
|
|
331
|
+
|
|
332
|
+
export interface PlaybookCaptainSettlement {
|
|
333
|
+
readonly snapshot: PlaybookCaptainShellSnapshot;
|
|
334
|
+
readonly retentionUpdates: readonly PlaybookCaptainRetentionUpdate[];
|
|
335
|
+
readonly unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[];
|
|
336
|
+
}
|
|
337
|
+
|
|
183
338
|
/** tmux and headless front ends share this one durable Captain shell API. */
|
|
184
339
|
export interface PlaybookCaptainShell extends Captain {
|
|
340
|
+
installRetainedGenerations(
|
|
341
|
+
generations: Readonly<
|
|
342
|
+
Record<string, PlaybookCaptainRetainedGeneration>
|
|
343
|
+
>,
|
|
344
|
+
): Promise<void>;
|
|
185
345
|
exportSnapshot(): PlaybookCaptainShellSnapshot | undefined;
|
|
346
|
+
exportSettlement(): PlaybookCaptainSettlement | undefined;
|
|
186
347
|
restore(
|
|
187
348
|
session: CaptainSession,
|
|
188
349
|
snapshot: PlaybookCaptainShellSnapshot,
|
|
@@ -193,15 +354,33 @@ export interface PlaybookCaptainShell extends Captain {
|
|
|
193
354
|
// normalized `captain.options.playbooks.<id>` role map.
|
|
194
355
|
interface Enablement {
|
|
195
356
|
entry: PlaybookCaptainRegistryEntry;
|
|
357
|
+
artifactSchema: 3;
|
|
196
358
|
command: string;
|
|
197
359
|
options: JsonValue;
|
|
198
360
|
roleBindings: ReadonlyMap<string, EffectivePlayerBinding>;
|
|
199
361
|
}
|
|
200
362
|
|
|
363
|
+
function createRuntimeForEnablement(
|
|
364
|
+
enablement: Enablement,
|
|
365
|
+
hostCapabilitiesById: ReadonlyMap<
|
|
366
|
+
string,
|
|
367
|
+
PlaybookHostConstructionCapabilities
|
|
368
|
+
>,
|
|
369
|
+
): PlaybookRuntime {
|
|
370
|
+
const hostCapabilities = hostCapabilitiesById.get(enablement.entry.id);
|
|
371
|
+
if (hostCapabilities === undefined) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`/${enablement.command} schema-3 runtime requires current-host construction capabilities`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
return enablement.entry.createRuntime(enablement.options, hostCapabilities);
|
|
377
|
+
}
|
|
378
|
+
|
|
201
379
|
interface EffectivePlayerBinding {
|
|
202
380
|
readonly playerId: string;
|
|
203
381
|
readonly model: TuningSelection;
|
|
204
382
|
readonly effort: TuningSelection<Effort>;
|
|
383
|
+
readonly fastMode?: boolean;
|
|
205
384
|
readonly agent: SessionAgent;
|
|
206
385
|
}
|
|
207
386
|
|
|
@@ -289,7 +468,12 @@ async function classifySettingsCall<T>(call: () => Promise<T>): Promise<T> {
|
|
|
289
468
|
}
|
|
290
469
|
}
|
|
291
470
|
|
|
292
|
-
type DisposalReason =
|
|
471
|
+
type DisposalReason =
|
|
472
|
+
| 'dismiss'
|
|
473
|
+
| 'final'
|
|
474
|
+
| 'dispose'
|
|
475
|
+
| 'failure'
|
|
476
|
+
| 'unresolved-effect';
|
|
293
477
|
|
|
294
478
|
interface ControlLedger {
|
|
295
479
|
activePlaybookId?: string;
|
|
@@ -366,6 +550,12 @@ const UUID_PATTERN =
|
|
|
366
550
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
367
551
|
const PLAYER_ID_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)*$/;
|
|
368
552
|
const ROLE_ID_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
553
|
+
const HOST_CAPABILITIES_OPTION_KEY = 'hostCapabilities';
|
|
554
|
+
const UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID =
|
|
555
|
+
'reconcile:unresolved-effect';
|
|
556
|
+
const UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID = 'abandon:unresolved-effect';
|
|
557
|
+
const RESUMPTION_DUPLICATE_EFFECT_WARNING =
|
|
558
|
+
'Warning: resumption may duplicate external effects attempted after the retained boundary; verify the current world before continuing.';
|
|
369
559
|
|
|
370
560
|
interface TurnSummaryCounts {
|
|
371
561
|
interruptions: number;
|
|
@@ -376,6 +566,7 @@ interface ActiveTurnSummary {
|
|
|
376
566
|
owner: EngagementFrame;
|
|
377
567
|
counts: TurnSummaryCounts;
|
|
378
568
|
stateCounts: Map<string, number>;
|
|
569
|
+
acceptedOutcomeTraceKeys: Set<string>;
|
|
379
570
|
}
|
|
380
571
|
|
|
381
572
|
/** The shell state of one Boss turn (DR-029). */
|
|
@@ -402,6 +593,8 @@ interface ActiveTurn {
|
|
|
402
593
|
presentationAttempted: boolean;
|
|
403
594
|
/** The exact rejected presentation boundary, propagated without retry. */
|
|
404
595
|
presentationError?: unknown;
|
|
596
|
+
/** Shell-authored safety text that must accompany the one visible reply. */
|
|
597
|
+
mandatoryPresentationSuffix?: string;
|
|
405
598
|
/** Facts accumulated while the selected action runs, including partial work. */
|
|
406
599
|
readonly settlementFacts: string[];
|
|
407
600
|
report?: OutcomeReport;
|
|
@@ -416,8 +609,8 @@ interface ActiveTurn {
|
|
|
416
609
|
readonly settingsPreflightFailures: Set<unknown>;
|
|
417
610
|
/**
|
|
418
611
|
* Every value that escaped an effect invocation this turn — a runtime
|
|
419
|
-
* driven, an engagement constructed, a stack disposed, an
|
|
420
|
-
* applied — recorded by `runEffect` at the throw itself.
|
|
612
|
+
* driven or adopted, an engagement constructed, a stack disposed, an
|
|
613
|
+
* advertised action applied — recorded by `runEffect` at the throw itself.
|
|
421
614
|
*
|
|
422
615
|
* Attribution follows the operation that threw rather than a latch set
|
|
423
616
|
* before it. A latch is turn-scoped, so once any effect has been attempted
|
|
@@ -446,6 +639,8 @@ interface ActiveTurn {
|
|
|
446
639
|
outcomePending?: boolean;
|
|
447
640
|
/** Whether this turn already has a closing journal outcome. */
|
|
448
641
|
outcomeRecorded: boolean;
|
|
642
|
+
/** Canonical host evidence frozen before the controller result phase. */
|
|
643
|
+
unresolvedEffects?: readonly PlaybookCaptainUnresolvedEffect[];
|
|
449
644
|
}
|
|
450
645
|
|
|
451
646
|
function parseRegisteredCommand(
|
|
@@ -905,11 +1100,62 @@ interface OutcomeReport {
|
|
|
905
1100
|
savedLine?: string;
|
|
906
1101
|
}
|
|
907
1102
|
|
|
1103
|
+
type ControllerSettlementDraft = Omit<
|
|
1104
|
+
SettlementEvidence,
|
|
1105
|
+
'unresolvedEffects'
|
|
1106
|
+
>;
|
|
1107
|
+
|
|
1108
|
+
function unresolvedEffectReportLines(
|
|
1109
|
+
unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[],
|
|
1110
|
+
): readonly string[] {
|
|
1111
|
+
return unresolvedEffects.map((effect, index) => {
|
|
1112
|
+
const merelyPossible =
|
|
1113
|
+
effect.classification === 'observation-ambiguous' ||
|
|
1114
|
+
effect.classification === 'incomplete';
|
|
1115
|
+
return [
|
|
1116
|
+
`${index + 1}. ${merelyPossible ? 'Possible repository effect; a change could not be excluded' : 'Observed repository change'} (${effect.classification})`,
|
|
1117
|
+
`baseline HEAD ${effect.baselineHead}`,
|
|
1118
|
+
effect.afterHead === undefined
|
|
1119
|
+
? 'after HEAD was not available'
|
|
1120
|
+
: `after HEAD ${effect.afterHead}`,
|
|
1121
|
+
...(effect.commitOid === undefined
|
|
1122
|
+
? []
|
|
1123
|
+
: [`proven commit OID ${effect.commitOid}`]),
|
|
1124
|
+
].join('; ') + '.';
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function unresolvedEffectBossReport(
|
|
1129
|
+
unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[],
|
|
1130
|
+
): string | undefined {
|
|
1131
|
+
if (unresolvedEffects.length === 0) return undefined;
|
|
1132
|
+
return [
|
|
1133
|
+
'Repository-effect evidence:',
|
|
1134
|
+
...unresolvedEffectReportLines(unresolvedEffects).map((line) => `- ${line}`),
|
|
1135
|
+
'This evidence does not establish workflow completion or attribute any repository change or commit to this workflow.',
|
|
1136
|
+
].join('\n');
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function appendMandatoryPresentationSuffix(
|
|
1140
|
+
turn: ActiveTurn,
|
|
1141
|
+
suffix: string,
|
|
1142
|
+
): void {
|
|
1143
|
+
const current = turn.mandatoryPresentationSuffix;
|
|
1144
|
+
if (current === undefined) {
|
|
1145
|
+
turn.mandatoryPresentationSuffix = suffix;
|
|
1146
|
+
} else if (!current.includes(suffix)) {
|
|
1147
|
+
turn.mandatoryPresentationSuffix = `${current}\n\n${suffix}`;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
908
1151
|
// CAPTAIN-20: the result-phase block the shell supplies inside the closing
|
|
909
1152
|
// reply call's envelope — the settlement's outcome-report facts verbatim, the
|
|
910
1153
|
// exact counts, and the saved-counts line only when counted activity is
|
|
911
1154
|
// nonzero.
|
|
912
|
-
function outcomeReportBlock(
|
|
1155
|
+
function outcomeReportBlock(
|
|
1156
|
+
report: OutcomeReport,
|
|
1157
|
+
unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[],
|
|
1158
|
+
): string {
|
|
913
1159
|
const lines: string[] = [
|
|
914
1160
|
`Settlement status: ${report.status}`,
|
|
915
1161
|
...(report.playbookId === undefined
|
|
@@ -935,6 +1181,14 @@ function outcomeReportBlock(report: OutcomeReport): string {
|
|
|
935
1181
|
if (report.leafStateSummary !== undefined) {
|
|
936
1182
|
lines.push(`Resulting leaf state: ${report.leafStateSummary}`);
|
|
937
1183
|
}
|
|
1184
|
+
const effectLines = unresolvedEffectReportLines(unresolvedEffects);
|
|
1185
|
+
if (effectLines.length > 0) {
|
|
1186
|
+
lines.push('Repository-effect evidence (canonical, in ledger order):');
|
|
1187
|
+
lines.push(...effectLines.map((line) => `- ${line}`));
|
|
1188
|
+
lines.push(
|
|
1189
|
+
'Report this evidence without claiming workflow completion or attributing any repository change or commit to the workflow.',
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
938
1192
|
lines.push(`Progress counts: ${report.progressPhrase}`);
|
|
939
1193
|
lines.push(
|
|
940
1194
|
`Counts: ${JSON.stringify({
|
|
@@ -984,12 +1238,209 @@ function summaryProgressRoundCount(
|
|
|
984
1238
|
return [...stateCounts.values()].reduce((total, count) => total + count, 0);
|
|
985
1239
|
}
|
|
986
1240
|
|
|
987
|
-
|
|
988
|
-
|
|
1241
|
+
interface ValidatedRuntimeProfile {
|
|
1242
|
+
readonly kind: 'shared-factory' | 'bespoke';
|
|
1243
|
+
readonly artifactSchema: 3;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function captureRegistryEntry(value: unknown): unknown {
|
|
1247
|
+
if (value === null || typeof value !== 'object') return value;
|
|
1248
|
+
const source = value as Record<string, unknown>;
|
|
1249
|
+
return {
|
|
1250
|
+
id: source.id,
|
|
1251
|
+
command: source.command,
|
|
1252
|
+
intent: source.intent,
|
|
1253
|
+
artifactSchema: source.artifactSchema,
|
|
1254
|
+
runtimeProfile: source.runtimeProfile,
|
|
1255
|
+
requiredRoleIds: source.requiredRoleIds,
|
|
1256
|
+
concurrentRoleSets: source.concurrentRoleSets,
|
|
1257
|
+
summaryPolicy: source.summaryPolicy,
|
|
1258
|
+
validateOptions: source.validateOptions,
|
|
1259
|
+
createRuntime: source.createRuntime,
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function exactOwnDataRecord(
|
|
1264
|
+
value: unknown,
|
|
1265
|
+
keys: readonly string[],
|
|
1266
|
+
): Readonly<Record<string, unknown>> | undefined {
|
|
1267
|
+
if (
|
|
1268
|
+
value === null ||
|
|
1269
|
+
typeof value !== 'object' ||
|
|
1270
|
+
Array.isArray(value) ||
|
|
1271
|
+
(Object.getPrototypeOf(value) !== Object.prototype &&
|
|
1272
|
+
Object.getPrototypeOf(value) !== null)
|
|
1273
|
+
) {
|
|
1274
|
+
return undefined;
|
|
1275
|
+
}
|
|
1276
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1277
|
+
if (
|
|
1278
|
+
Reflect.ownKeys(descriptors).length !== keys.length ||
|
|
1279
|
+
keys.some(
|
|
1280
|
+
(key) =>
|
|
1281
|
+
!Object.hasOwn(descriptors, key) ||
|
|
1282
|
+
!Object.hasOwn(descriptors[key]!, 'value') ||
|
|
1283
|
+
descriptors[key]!.enumerable !== true,
|
|
1284
|
+
)
|
|
1285
|
+
) {
|
|
1286
|
+
return undefined;
|
|
1287
|
+
}
|
|
1288
|
+
return Object.fromEntries(
|
|
1289
|
+
keys.map((key) => [key, descriptors[key]!.value]),
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function captureHostCapabilityRecord(
|
|
1294
|
+
value: PlaybookCaptainDeps['hostCapabilities'],
|
|
1295
|
+
): Readonly<Record<string, PlaybookHostConstructionCapabilities>> {
|
|
1296
|
+
if (value === undefined) return Object.freeze({});
|
|
1297
|
+
const captured = exactOwnDataRecord(value, Object.keys(value));
|
|
1298
|
+
if (captured === undefined) {
|
|
1299
|
+
throw new TypeError(
|
|
1300
|
+
'current-host construction capabilities must be an exact data-property record',
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
return captured as Readonly<
|
|
1304
|
+
Record<string, PlaybookHostConstructionCapabilities>
|
|
1305
|
+
>;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function validateHostCapabilities(
|
|
1309
|
+
value: unknown,
|
|
1310
|
+
entry: PlaybookCaptainRegistryEntryV3,
|
|
1311
|
+
command: string,
|
|
1312
|
+
): PlaybookHostConstructionCapabilities {
|
|
1313
|
+
if (value === undefined) {
|
|
1314
|
+
throw new Error(
|
|
1315
|
+
`/${command} schema-3 runtime requires current-host construction capabilities`,
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
const capability = exactOwnDataRecord(value, [
|
|
1319
|
+
'authority',
|
|
1320
|
+
'repository',
|
|
1321
|
+
'effectLedger',
|
|
1322
|
+
]);
|
|
1323
|
+
const authority = exactOwnDataRecord(capability?.authority, [
|
|
1324
|
+
'playbookId',
|
|
1325
|
+
'artifactSchema',
|
|
1326
|
+
'cwd',
|
|
1327
|
+
'sessionId',
|
|
1328
|
+
'leaseOwnerToken',
|
|
1329
|
+
'canonicalWorktree',
|
|
1330
|
+
'requiredRoleIds',
|
|
1331
|
+
'concurrentRoleSets',
|
|
1332
|
+
]);
|
|
1333
|
+
const canonicalWorktree = exactOwnDataRecord(
|
|
1334
|
+
authority?.canonicalWorktree,
|
|
1335
|
+
['worktree', 'gitDir'],
|
|
1336
|
+
);
|
|
1337
|
+
const repository = exactOwnDataRecord(capability?.repository, [
|
|
1338
|
+
'identity',
|
|
1339
|
+
'observe',
|
|
1340
|
+
'acquire',
|
|
1341
|
+
'runExclusive',
|
|
1342
|
+
'runCohort',
|
|
1343
|
+
'runDeferred',
|
|
1344
|
+
]);
|
|
1345
|
+
const identity = exactOwnDataRecord(repository?.identity, [
|
|
1346
|
+
'worktree',
|
|
1347
|
+
'gitDir',
|
|
1348
|
+
]);
|
|
1349
|
+
const effectLedger = exactOwnDataRecord(capability?.effectLedger, [
|
|
1350
|
+
'snapshot',
|
|
1351
|
+
'writeAhead',
|
|
1352
|
+
]);
|
|
1353
|
+
if (
|
|
1354
|
+
authority?.playbookId !== entry.id ||
|
|
1355
|
+
authority.artifactSchema !== 3 ||
|
|
1356
|
+
typeof authority.cwd !== 'string' ||
|
|
1357
|
+
authority.cwd.length === 0 ||
|
|
1358
|
+
typeof authority.sessionId !== 'string' ||
|
|
1359
|
+
authority.sessionId.length === 0 ||
|
|
1360
|
+
typeof authority.leaseOwnerToken !== 'string' ||
|
|
1361
|
+
authority.leaseOwnerToken.length === 0 ||
|
|
1362
|
+
canonicalWorktree === undefined ||
|
|
1363
|
+
typeof canonicalWorktree.worktree !== 'string' ||
|
|
1364
|
+
canonicalWorktree.worktree.length === 0 ||
|
|
1365
|
+
typeof canonicalWorktree.gitDir !== 'string' ||
|
|
1366
|
+
canonicalWorktree.gitDir.length === 0 ||
|
|
1367
|
+
!isDeepStrictEqual(authority.requiredRoleIds, entry.requiredRoleIds) ||
|
|
1368
|
+
!isDeepStrictEqual(
|
|
1369
|
+
authority.concurrentRoleSets,
|
|
1370
|
+
entry.concurrentRoleSets,
|
|
1371
|
+
) ||
|
|
1372
|
+
identity === undefined ||
|
|
1373
|
+
!isDeepStrictEqual(identity, canonicalWorktree) ||
|
|
1374
|
+
typeof repository?.observe !== 'function' ||
|
|
1375
|
+
typeof repository.acquire !== 'function' ||
|
|
1376
|
+
typeof repository.runExclusive !== 'function' ||
|
|
1377
|
+
typeof repository.runCohort !== 'function' ||
|
|
1378
|
+
typeof repository.runDeferred !== 'function' ||
|
|
1379
|
+
typeof effectLedger?.snapshot !== 'function' ||
|
|
1380
|
+
typeof effectLedger.writeAhead !== 'function'
|
|
1381
|
+
) {
|
|
1382
|
+
throw new Error(
|
|
1383
|
+
`/${command} schema-3 current-host capability authority does not match its imported artifact`,
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
return value as PlaybookHostConstructionCapabilities;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function effectLedgerMirrorFromCapabilities(
|
|
1390
|
+
capabilities: ReadonlyMap<string, PlaybookHostConstructionCapabilities>,
|
|
1391
|
+
): PlaybookEffectLedger {
|
|
1392
|
+
const values = [...capabilities.values()];
|
|
1393
|
+
if (values.length === 0) return emptyPlaybookEffectLedger();
|
|
1394
|
+
const mirror = assertPlaybookEffectLedger(values[0]!.effectLedger.snapshot());
|
|
1395
|
+
for (const capability of values.slice(1)) {
|
|
1396
|
+
if (
|
|
1397
|
+
!isDeepStrictEqual(
|
|
1398
|
+
assertPlaybookEffectLedger(capability.effectLedger.snapshot()),
|
|
1399
|
+
mirror,
|
|
1400
|
+
)
|
|
1401
|
+
) {
|
|
1402
|
+
throw new Error(
|
|
1403
|
+
'schema-3 current-host capabilities disagree on their effect ledger',
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
return mirror;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function validateRuntimeProfile(
|
|
1411
|
+
value: unknown,
|
|
1412
|
+
): ValidatedRuntimeProfile | undefined {
|
|
1413
|
+
const shared = exactOwnDataRecord(value, ['kind', 'compat']);
|
|
1414
|
+
if (shared?.kind === 'shared-factory') {
|
|
1415
|
+
const compat = exactOwnDataRecord(shared.compat, [
|
|
1416
|
+
'artifactSchema',
|
|
1417
|
+
'runtimeAbi',
|
|
1418
|
+
]);
|
|
1419
|
+
if (
|
|
1420
|
+
compat?.artifactSchema === 3 &&
|
|
1421
|
+
typeof compat.runtimeAbi === 'number' &&
|
|
1422
|
+
Number.isSafeInteger(compat.runtimeAbi)
|
|
1423
|
+
) {
|
|
1424
|
+
return {
|
|
1425
|
+
kind: 'shared-factory',
|
|
1426
|
+
artifactSchema: compat.artifactSchema,
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
return undefined;
|
|
1430
|
+
}
|
|
1431
|
+
const bespoke = exactOwnDataRecord(value, ['kind', 'artifactSchema']);
|
|
1432
|
+
if (
|
|
1433
|
+
bespoke?.kind === 'bespoke' &&
|
|
1434
|
+
bespoke.artifactSchema === 3
|
|
1435
|
+
) {
|
|
1436
|
+
return { kind: 'bespoke', artifactSchema: bespoke.artifactSchema };
|
|
1437
|
+
}
|
|
1438
|
+
return undefined;
|
|
989
1439
|
}
|
|
990
1440
|
|
|
991
1441
|
function isValidRegistryEntry(
|
|
992
1442
|
value: unknown,
|
|
1443
|
+
artifactSchema: unknown,
|
|
993
1444
|
): value is PlaybookCaptainRegistryEntry {
|
|
994
1445
|
if (typeof value !== 'object' || value === null) return false;
|
|
995
1446
|
const e = value as Record<string, unknown>;
|
|
@@ -1025,7 +1476,7 @@ function isValidRegistryEntry(
|
|
|
1025
1476
|
typeof e.id === 'string' &&
|
|
1026
1477
|
typeof e.command === 'string' &&
|
|
1027
1478
|
typeof e.intent === 'string' &&
|
|
1028
|
-
|
|
1479
|
+
artifactSchema === 3 &&
|
|
1029
1480
|
typeof e.validateOptions === 'function' &&
|
|
1030
1481
|
typeof e.createRuntime === 'function'
|
|
1031
1482
|
);
|
|
@@ -1035,6 +1486,7 @@ const SNAPSHOT_ACTIONS = new Set([
|
|
|
1035
1486
|
'respond',
|
|
1036
1487
|
'start',
|
|
1037
1488
|
'switch',
|
|
1489
|
+
'resume',
|
|
1038
1490
|
'dismiss',
|
|
1039
1491
|
'deliver',
|
|
1040
1492
|
'runtime',
|
|
@@ -1051,6 +1503,93 @@ const SNAPSHOT_JOURNAL_KINDS = new Set([
|
|
|
1051
1503
|
'action',
|
|
1052
1504
|
'outcome',
|
|
1053
1505
|
] as const);
|
|
1506
|
+
const UNRESOLVED_EFFECT_CLASSIFICATIONS = new Set<
|
|
1507
|
+
PlaybookCaptainUnresolvedEffect['classification']
|
|
1508
|
+
>([
|
|
1509
|
+
'one-descendant-commit',
|
|
1510
|
+
'multiple-commits',
|
|
1511
|
+
'rewritten-or-non-descendant',
|
|
1512
|
+
'worktree-only-change',
|
|
1513
|
+
'concurrent-or-foreign-change',
|
|
1514
|
+
'observation-ambiguous',
|
|
1515
|
+
'incomplete',
|
|
1516
|
+
]);
|
|
1517
|
+
const GIT_OID_PATTERN = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
1518
|
+
|
|
1519
|
+
export function assertPlaybookCaptainUnresolvedEffects(
|
|
1520
|
+
value: unknown,
|
|
1521
|
+
): readonly PlaybookCaptainUnresolvedEffect[] {
|
|
1522
|
+
const detached = snapshotJsonValue(
|
|
1523
|
+
value,
|
|
1524
|
+
'Captain unresolved effects',
|
|
1525
|
+
);
|
|
1526
|
+
if (!Array.isArray(detached)) {
|
|
1527
|
+
throw new TypeError('Captain unresolved effects must be an array');
|
|
1528
|
+
}
|
|
1529
|
+
for (const [index, raw] of detached.entries()) {
|
|
1530
|
+
const path = `Captain unresolved effects[${index}]`;
|
|
1531
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
1532
|
+
throw new TypeError(`${path} must be an object`);
|
|
1533
|
+
}
|
|
1534
|
+
const entry = raw as Record<string, JsonValue>;
|
|
1535
|
+
const allowed = new Set([
|
|
1536
|
+
'classification',
|
|
1537
|
+
'baselineHead',
|
|
1538
|
+
'afterHead',
|
|
1539
|
+
'commitOid',
|
|
1540
|
+
]);
|
|
1541
|
+
const unknown = Object.keys(entry).find((key) => !allowed.has(key));
|
|
1542
|
+
if (unknown !== undefined) {
|
|
1543
|
+
throw new TypeError(`${path} has unknown field ${JSON.stringify(unknown)}`);
|
|
1544
|
+
}
|
|
1545
|
+
if (
|
|
1546
|
+
typeof entry.classification !== 'string' ||
|
|
1547
|
+
!UNRESOLVED_EFFECT_CLASSIFICATIONS.has(
|
|
1548
|
+
entry.classification as PlaybookCaptainUnresolvedEffect['classification'],
|
|
1549
|
+
)
|
|
1550
|
+
) {
|
|
1551
|
+
throw new TypeError(`${path}.classification is not supported`);
|
|
1552
|
+
}
|
|
1553
|
+
if (
|
|
1554
|
+
typeof entry.baselineHead !== 'string' ||
|
|
1555
|
+
!GIT_OID_PATTERN.test(entry.baselineHead)
|
|
1556
|
+
) {
|
|
1557
|
+
throw new TypeError(`${path}.baselineHead must be a Git OID`);
|
|
1558
|
+
}
|
|
1559
|
+
if (
|
|
1560
|
+
entry.afterHead !== undefined &&
|
|
1561
|
+
(typeof entry.afterHead !== 'string' ||
|
|
1562
|
+
!GIT_OID_PATTERN.test(entry.afterHead))
|
|
1563
|
+
) {
|
|
1564
|
+
throw new TypeError(`${path}.afterHead must be a Git OID`);
|
|
1565
|
+
}
|
|
1566
|
+
if (
|
|
1567
|
+
entry.classification !== 'observation-ambiguous' &&
|
|
1568
|
+
entry.classification !== 'incomplete' &&
|
|
1569
|
+
entry.afterHead === undefined
|
|
1570
|
+
) {
|
|
1571
|
+
throw new TypeError(
|
|
1572
|
+
`${path}.afterHead is required for ${entry.classification}`,
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
if (entry.classification === 'one-descendant-commit') {
|
|
1576
|
+
if (
|
|
1577
|
+
typeof entry.commitOid !== 'string' ||
|
|
1578
|
+
!GIT_OID_PATTERN.test(entry.commitOid) ||
|
|
1579
|
+
entry.commitOid !== entry.afterHead
|
|
1580
|
+
) {
|
|
1581
|
+
throw new TypeError(
|
|
1582
|
+
`${path}.commitOid must equal afterHead for one-descendant-commit`,
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
} else if (entry.commitOid !== undefined) {
|
|
1586
|
+
throw new TypeError(
|
|
1587
|
+
`${path}.commitOid is permitted only for one-descendant-commit`,
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
return detached as unknown as readonly PlaybookCaptainUnresolvedEffect[];
|
|
1592
|
+
}
|
|
1054
1593
|
|
|
1055
1594
|
function snapshotRecord(
|
|
1056
1595
|
value: JsonValue | undefined,
|
|
@@ -1312,9 +1851,20 @@ export function assertPlaybookCaptainShellSnapshot(
|
|
|
1312
1851
|
): PlaybookCaptainShellSnapshot {
|
|
1313
1852
|
const detached = snapshotJsonValue(value, 'Captain shell snapshot');
|
|
1314
1853
|
const snapshot = snapshotRecord(detached, 'Captain shell snapshot');
|
|
1854
|
+
if (snapshot.schemaVersion !== 4) {
|
|
1855
|
+
if (snapshot.schemaVersion === 1) {
|
|
1856
|
+
throw new TypeError(
|
|
1857
|
+
'Captain shell snapshot schemaVersion 1 has incompatible player identity; schema 4 is required',
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
throw new TypeError(
|
|
1861
|
+
`Captain shell snapshot.schemaVersion ${String(snapshot.schemaVersion)} is not supported (expected 4)`,
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1315
1864
|
const mode = snapshot.mode;
|
|
1316
1865
|
const commonKeys = [
|
|
1317
1866
|
'schemaVersion',
|
|
1867
|
+
'effectLedger',
|
|
1318
1868
|
'captain',
|
|
1319
1869
|
'playerSessions',
|
|
1320
1870
|
'issuedSessionIds',
|
|
@@ -1332,6 +1882,7 @@ export function assertPlaybookCaptainShellSnapshot(
|
|
|
1332
1882
|
[
|
|
1333
1883
|
...commonKeys,
|
|
1334
1884
|
'frames',
|
|
1885
|
+
'retainedEffectReconciliation',
|
|
1335
1886
|
'pendingBossQuestions',
|
|
1336
1887
|
'lastError',
|
|
1337
1888
|
],
|
|
@@ -1342,12 +1893,6 @@ export function assertPlaybookCaptainShellSnapshot(
|
|
|
1342
1893
|
'Captain shell snapshot.mode must be "chat" or "engaged.parked"',
|
|
1343
1894
|
);
|
|
1344
1895
|
}
|
|
1345
|
-
if (snapshot.schemaVersion !== 3) {
|
|
1346
|
-
throw new TypeError(
|
|
1347
|
-
`Captain shell snapshot.schemaVersion ${String(snapshot.schemaVersion)} is not supported (expected 3)`,
|
|
1348
|
-
);
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
1896
|
const captain = snapshotRecord(
|
|
1352
1897
|
snapshot.captain,
|
|
1353
1898
|
'Captain shell snapshot.captain',
|
|
@@ -1365,6 +1910,50 @@ export function assertPlaybookCaptainShellSnapshot(
|
|
|
1365
1910
|
captain.runtime,
|
|
1366
1911
|
INTERNAL_CAPTAIN_ID,
|
|
1367
1912
|
);
|
|
1913
|
+
const effectLedger = assertPlaybookEffectLedger(snapshot.effectLedger);
|
|
1914
|
+
let retainedEffectReconciliation:
|
|
1915
|
+
| PlaybookCaptainRetainedEffectReconciliation
|
|
1916
|
+
| undefined;
|
|
1917
|
+
if (snapshot.retainedEffectReconciliation !== undefined) {
|
|
1918
|
+
const reconciliation = snapshotRecord(
|
|
1919
|
+
snapshot.retainedEffectReconciliation,
|
|
1920
|
+
'Captain shell snapshot.retainedEffectReconciliation',
|
|
1921
|
+
);
|
|
1922
|
+
rejectSnapshotKeys(
|
|
1923
|
+
reconciliation,
|
|
1924
|
+
['sourceGenerationId', 'checkpoint'],
|
|
1925
|
+
'Captain shell snapshot.retainedEffectReconciliation',
|
|
1926
|
+
);
|
|
1927
|
+
const checkpoint = assertPlaybookEffectLedger(
|
|
1928
|
+
reconciliation.checkpoint,
|
|
1929
|
+
'Captain shell snapshot retained-effect checkpoint',
|
|
1930
|
+
);
|
|
1931
|
+
if (
|
|
1932
|
+
isDeepStrictEqual(checkpoint, effectLedger) ||
|
|
1933
|
+
!isPlaybookEffectLedgerMonotonicExtension(checkpoint, effectLedger)
|
|
1934
|
+
) {
|
|
1935
|
+
throw new TypeError(
|
|
1936
|
+
'Captain shell retained-effect checkpoint must be a strict monotonic prefix of its current mirror',
|
|
1937
|
+
);
|
|
1938
|
+
}
|
|
1939
|
+
retainedEffectReconciliation = {
|
|
1940
|
+
sourceGenerationId: snapshotUuid(
|
|
1941
|
+
reconciliation.sourceGenerationId,
|
|
1942
|
+
'Captain shell snapshot.retainedEffectReconciliation.sourceGenerationId',
|
|
1943
|
+
),
|
|
1944
|
+
checkpoint,
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
if (
|
|
1948
|
+
!isDeepStrictEqual(
|
|
1949
|
+
captainRuntime.effectLedger,
|
|
1950
|
+
emptyPlaybookEffectLedger(),
|
|
1951
|
+
)
|
|
1952
|
+
) {
|
|
1953
|
+
throw new TypeError(
|
|
1954
|
+
'Captain shell snapshot internal Captain runtime effect ledger must be empty',
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1368
1957
|
const captainAgent = snapshotFixedAgent(
|
|
1369
1958
|
captain.agent,
|
|
1370
1959
|
'Captain shell snapshot.captain.agent',
|
|
@@ -1594,7 +2183,8 @@ export function assertPlaybookCaptainShellSnapshot(
|
|
|
1594
2183
|
'Captain shell snapshot.playerSessions',
|
|
1595
2184
|
);
|
|
1596
2185
|
const common: PlaybookCaptainShellSnapshotFields = {
|
|
1597
|
-
schemaVersion:
|
|
2186
|
+
schemaVersion: 4,
|
|
2187
|
+
effectLedger,
|
|
1598
2188
|
captain: {
|
|
1599
2189
|
sessionId: captainSessionId,
|
|
1600
2190
|
runtime: captainRuntime,
|
|
@@ -1743,6 +2333,51 @@ export function assertPlaybookCaptainShellSnapshot(
|
|
|
1743
2333
|
const issuedIds = new Set(issued);
|
|
1744
2334
|
const rootSessionId = normalizedFrames[0]!.sessionId;
|
|
1745
2335
|
for (const [index, frame] of normalizedFrames.entries()) {
|
|
2336
|
+
const frameLedger = frame.runtime.effectLedger;
|
|
2337
|
+
if (
|
|
2338
|
+
!isDeepStrictEqual(frameLedger, emptyPlaybookEffectLedger()) &&
|
|
2339
|
+
!isDeepStrictEqual(frameLedger, effectLedger)
|
|
2340
|
+
) {
|
|
2341
|
+
throw new TypeError(
|
|
2342
|
+
`Captain shell snapshot frame ${JSON.stringify(frame.playbookId)} effect ledger is neither empty nor the shell mirror`,
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
const frameReconciliation = frame.runtime.retainedEffectReconciliation;
|
|
2346
|
+
if (retainedEffectReconciliation === undefined) {
|
|
2347
|
+
if (frameReconciliation !== undefined) {
|
|
2348
|
+
throw new TypeError(
|
|
2349
|
+
`Captain shell snapshot frame ${JSON.stringify(frame.playbookId)} carries an unmirrored retained-effect fence`,
|
|
2350
|
+
);
|
|
2351
|
+
}
|
|
2352
|
+
} else if (isDeepStrictEqual(frameLedger, effectLedger)) {
|
|
2353
|
+
if (
|
|
2354
|
+
frameReconciliation === undefined ||
|
|
2355
|
+
!isDeepStrictEqual(
|
|
2356
|
+
frameReconciliation.checkpoint,
|
|
2357
|
+
retainedEffectReconciliation.checkpoint,
|
|
2358
|
+
)
|
|
2359
|
+
) {
|
|
2360
|
+
throw new TypeError(
|
|
2361
|
+
`Captain shell snapshot frame ${JSON.stringify(frame.playbookId)} does not mirror the root retained-effect fence`,
|
|
2362
|
+
);
|
|
2363
|
+
}
|
|
2364
|
+
if (
|
|
2365
|
+
index === 0 &&
|
|
2366
|
+
frameReconciliation.sourceSessionId !==
|
|
2367
|
+
retainedEffectReconciliation.sourceGenerationId
|
|
2368
|
+
) {
|
|
2369
|
+
throw new TypeError(
|
|
2370
|
+
'Captain shell snapshot retained-effect root source identity differs from its generation',
|
|
2371
|
+
);
|
|
2372
|
+
}
|
|
2373
|
+
} else if (
|
|
2374
|
+
frameReconciliation !== undefined ||
|
|
2375
|
+
frame.runtime.retainedEffectSourceSessionId !== undefined
|
|
2376
|
+
) {
|
|
2377
|
+
throw new TypeError(
|
|
2378
|
+
`Captain shell snapshot empty-ledger frame ${JSON.stringify(frame.playbookId)} carries retained-effect adoption state`,
|
|
2379
|
+
);
|
|
2380
|
+
}
|
|
1746
2381
|
if (activePlaybooks.has(frame.playbookId)) {
|
|
1747
2382
|
throw new TypeError(
|
|
1748
2383
|
'Captain shell snapshot engagement path must not contain a playbook cycle',
|
|
@@ -1840,14 +2475,20 @@ export function assertPlaybookCaptainShellSnapshot(
|
|
|
1840
2475
|
'Captain shell snapshot leaf runtime must be parked without a dangling suspended child call',
|
|
1841
2476
|
);
|
|
1842
2477
|
}
|
|
1843
|
-
if (
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
2478
|
+
if (retainedEffectReconciliation === undefined) {
|
|
2479
|
+
if (
|
|
2480
|
+
!isDeepStrictEqual(
|
|
2481
|
+
snapshot.pendingBossQuestions ?? [],
|
|
2482
|
+
leafRuntime.pendingBossQuestions,
|
|
2483
|
+
)
|
|
2484
|
+
) {
|
|
2485
|
+
throw new TypeError(
|
|
2486
|
+
'Captain shell snapshot pending Boss questions must equal the leaf runtime projection',
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2489
|
+
} else if (snapshot.pendingBossQuestions !== undefined) {
|
|
1849
2490
|
throw new TypeError(
|
|
1850
|
-
'Captain shell snapshot pending Boss questions
|
|
2491
|
+
'Captain shell snapshot must withhold pending Boss questions behind retained-effect reconciliation',
|
|
1851
2492
|
);
|
|
1852
2493
|
}
|
|
1853
2494
|
return snapshotJsonValue(
|
|
@@ -1855,6 +2496,9 @@ export function assertPlaybookCaptainShellSnapshot(
|
|
|
1855
2496
|
...common,
|
|
1856
2497
|
mode,
|
|
1857
2498
|
frames: normalizedFrames,
|
|
2499
|
+
...(retainedEffectReconciliation === undefined
|
|
2500
|
+
? {}
|
|
2501
|
+
: { retainedEffectReconciliation }),
|
|
1858
2502
|
...(snapshot.pendingBossQuestions === undefined
|
|
1859
2503
|
? {}
|
|
1860
2504
|
: { pendingBossQuestions: snapshot.pendingBossQuestions }),
|
|
@@ -1871,6 +2515,10 @@ interface BuiltRegistry {
|
|
|
1871
2515
|
byCommand: Map<string, PlaybookCaptainRegistryEntry>;
|
|
1872
2516
|
byId: Map<string, PlaybookCaptainRegistryEntry>;
|
|
1873
2517
|
enablementById: Map<string, Enablement>;
|
|
2518
|
+
hostCapabilitiesById: ReadonlyMap<
|
|
2519
|
+
string,
|
|
2520
|
+
PlaybookHostConstructionCapabilities
|
|
2521
|
+
>;
|
|
1874
2522
|
captainAgent: SessionAgent;
|
|
1875
2523
|
playerAgents: Map<string, SessionAgent>;
|
|
1876
2524
|
}
|
|
@@ -1918,6 +2566,16 @@ function snapshotEffortSelection(
|
|
|
1918
2566
|
return selection as TuningSelection<Effort>;
|
|
1919
2567
|
}
|
|
1920
2568
|
|
|
2569
|
+
function snapshotFastMode(
|
|
2570
|
+
value: JsonValue | undefined,
|
|
2571
|
+
path: string,
|
|
2572
|
+
): boolean | undefined {
|
|
2573
|
+
if (value !== undefined && typeof value !== 'boolean') {
|
|
2574
|
+
throw new TypeError(`${path} must be a boolean`);
|
|
2575
|
+
}
|
|
2576
|
+
return value;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
1921
2579
|
function snapshotSessionAgent(
|
|
1922
2580
|
value: JsonValue | undefined,
|
|
1923
2581
|
path: string,
|
|
@@ -1925,13 +2583,14 @@ function snapshotSessionAgent(
|
|
|
1925
2583
|
const agent = snapshotRecord(value, path);
|
|
1926
2584
|
rejectSnapshotKeys(
|
|
1927
2585
|
agent,
|
|
1928
|
-
['adapter', 'model', 'effort', 'instruction', 'permissions'],
|
|
2586
|
+
['adapter', 'model', 'effort', 'fastMode', 'instruction', 'permissions'],
|
|
1929
2587
|
path,
|
|
1930
2588
|
);
|
|
1931
2589
|
const fixed = snapshotFixedAgent(
|
|
1932
2590
|
Object.fromEntries(
|
|
1933
2591
|
Object.entries(agent).filter(
|
|
1934
|
-
([key]) =>
|
|
2592
|
+
([key]) =>
|
|
2593
|
+
key !== 'model' && key !== 'effort' && key !== 'fastMode',
|
|
1935
2594
|
),
|
|
1936
2595
|
) as JsonValue,
|
|
1937
2596
|
path,
|
|
@@ -1946,10 +2605,15 @@ function snapshotSessionAgent(
|
|
|
1946
2605
|
: { permissions: livePermissions(fixed.permissions) }),
|
|
1947
2606
|
model: snapshotTuningSelection(agent.model, `${path}.model`),
|
|
1948
2607
|
effort: snapshotEffortSelection(agent.effort, `${path}.effort`),
|
|
2608
|
+
...(agent.fastMode === undefined
|
|
2609
|
+
? {}
|
|
2610
|
+
: { fastMode: snapshotFastMode(agent.fastMode, `${path}.fastMode`) }),
|
|
1949
2611
|
};
|
|
1950
2612
|
}
|
|
1951
2613
|
|
|
1952
|
-
function fixedAgent(
|
|
2614
|
+
function fixedAgent(
|
|
2615
|
+
agent: SessionAgent,
|
|
2616
|
+
): Omit<SessionAgent, 'model' | 'effort' | 'fastMode'> {
|
|
1953
2617
|
return {
|
|
1954
2618
|
adapter: agent.adapter,
|
|
1955
2619
|
...(agent.instruction === undefined ? {} : { instruction: agent.instruction }),
|
|
@@ -1959,11 +2623,15 @@ function fixedAgent(agent: SessionAgent): Omit<SessionAgent, 'model' | 'effort'>
|
|
|
1959
2623
|
|
|
1960
2624
|
function callSettings(
|
|
1961
2625
|
agent: SessionAgent,
|
|
1962
|
-
tuning: Pick<SessionAgent, 'model' | 'effort'> = agent,
|
|
2626
|
+
tuning: Pick<SessionAgent, 'model' | 'effort' | 'fastMode'> = agent,
|
|
1963
2627
|
): AgentCallSettings {
|
|
2628
|
+
// cligent treats supplied call settings as a complete replacement, so an
|
|
2629
|
+
// omitted fastMode here is a request for the provider default, never an
|
|
2630
|
+
// inheritance of whatever the previous call left behind.
|
|
1964
2631
|
return {
|
|
1965
2632
|
model: tuning.model,
|
|
1966
2633
|
effort: tuning.effort,
|
|
2634
|
+
...(tuning.fastMode === undefined ? {} : { fastMode: tuning.fastMode }),
|
|
1967
2635
|
...(agent.instruction === undefined ? {} : { instruction: agent.instruction }),
|
|
1968
2636
|
...(agent.permissions === undefined ? {} : { permissions: agent.permissions }),
|
|
1969
2637
|
};
|
|
@@ -1975,17 +2643,38 @@ function promptIdentity(binding: EffectivePlayerBinding): string {
|
|
|
1975
2643
|
: binding.agent.adapter;
|
|
1976
2644
|
}
|
|
1977
2645
|
|
|
2646
|
+
function rejectConfiguredHostCapabilities(value: JsonValue | undefined, path: string): void {
|
|
2647
|
+
if (
|
|
2648
|
+
value !== null &&
|
|
2649
|
+
typeof value === 'object' &&
|
|
2650
|
+
!Array.isArray(value) &&
|
|
2651
|
+
Object.prototype.hasOwnProperty.call(value, HOST_CAPABILITIES_OPTION_KEY)
|
|
2652
|
+
) {
|
|
2653
|
+
throw new Error(
|
|
2654
|
+
`${path}.${HOST_CAPABILITIES_OPTION_KEY} is host-owned and cannot be configured`,
|
|
2655
|
+
);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
|
|
1978
2659
|
// Resolve the active registry at init from exact normalized role and session
|
|
1979
2660
|
// agent projections (CAPTAIN-16). No role, ancestor, or generated-name fallback
|
|
1980
2661
|
// exists at this boundary.
|
|
1981
2662
|
async function buildEnablements(
|
|
1982
2663
|
options: unknown,
|
|
1983
2664
|
loadModule: (specifier: string) => Promise<unknown>,
|
|
2665
|
+
hostCapabilities: PlaybookCaptainDeps['hostCapabilities'],
|
|
1984
2666
|
): Promise<BuiltRegistry> {
|
|
1985
2667
|
const entries: PlaybookCaptainRegistryEntry[] = [];
|
|
1986
2668
|
const byCommand = new Map<string, PlaybookCaptainRegistryEntry>();
|
|
1987
2669
|
const byId = new Map<string, PlaybookCaptainRegistryEntry>();
|
|
1988
2670
|
const enablementById = new Map<string, Enablement>();
|
|
2671
|
+
const hostCapabilitiesById = new Map<
|
|
2672
|
+
string,
|
|
2673
|
+
PlaybookHostConstructionCapabilities
|
|
2674
|
+
>();
|
|
2675
|
+
const suppliedHostCapabilities =
|
|
2676
|
+
captureHostCapabilityRecord(hostCapabilities);
|
|
2677
|
+
const expectedHostCapabilityIds: string[] = [];
|
|
1989
2678
|
|
|
1990
2679
|
const detached = snapshotJsonValue(options, 'captain.options');
|
|
1991
2680
|
const top = snapshotRecord(detached, 'captain.options');
|
|
@@ -2067,6 +2756,10 @@ async function buildEnablements(
|
|
|
2067
2756
|
['from', 'command', 'roles', 'options'],
|
|
2068
2757
|
`captain.options.playbooks.${id}`,
|
|
2069
2758
|
);
|
|
2759
|
+
rejectConfiguredHostCapabilities(
|
|
2760
|
+
record.options,
|
|
2761
|
+
`captain.options.playbooks.${id}.options`,
|
|
2762
|
+
);
|
|
2070
2763
|
const from = record.from;
|
|
2071
2764
|
if (typeof from !== 'string' || from.length === 0) {
|
|
2072
2765
|
throw new Error(
|
|
@@ -2083,13 +2776,45 @@ async function buildEnablements(
|
|
|
2083
2776
|
)}`,
|
|
2084
2777
|
);
|
|
2085
2778
|
}
|
|
2086
|
-
const
|
|
2087
|
-
|
|
2779
|
+
const capturedEntry = captureRegistryEntry(
|
|
2780
|
+
(mod as { default?: unknown })?.default,
|
|
2781
|
+
);
|
|
2782
|
+
const artifactSchema = (
|
|
2783
|
+
capturedEntry as { artifactSchema?: unknown } | null
|
|
2784
|
+
)
|
|
2785
|
+
?.artifactSchema;
|
|
2786
|
+
const runtimeProfile = validateRuntimeProfile(
|
|
2787
|
+
(capturedEntry as { runtimeProfile?: unknown } | null)?.runtimeProfile,
|
|
2788
|
+
);
|
|
2789
|
+
if (
|
|
2790
|
+
artifactSchema !== 3 ||
|
|
2791
|
+
runtimeProfile === undefined ||
|
|
2792
|
+
!isValidRegistryEntry(capturedEntry, artifactSchema)
|
|
2793
|
+
) {
|
|
2088
2794
|
throw new Error(
|
|
2089
2795
|
`captain.options.playbooks.${id}.from "${from}" exposes no valid registry entry`,
|
|
2090
2796
|
);
|
|
2091
2797
|
}
|
|
2092
|
-
|
|
2798
|
+
const entry = Object.freeze({
|
|
2799
|
+
...capturedEntry,
|
|
2800
|
+
requiredRoleIds: Object.freeze([...capturedEntry.requiredRoleIds]),
|
|
2801
|
+
concurrentRoleSets: Object.freeze(
|
|
2802
|
+
capturedEntry.concurrentRoleSets.map((roles) =>
|
|
2803
|
+
Object.freeze([...roles]),
|
|
2804
|
+
),
|
|
2805
|
+
),
|
|
2806
|
+
}) as PlaybookCaptainRegistryEntry;
|
|
2807
|
+
if (runtimeProfile.artifactSchema !== artifactSchema) {
|
|
2808
|
+
const implementation =
|
|
2809
|
+
runtimeProfile.kind === 'shared-factory'
|
|
2810
|
+
? 'shared factory'
|
|
2811
|
+
: 'bespoke runtime';
|
|
2812
|
+
throw new Error(
|
|
2813
|
+
`captain.options.playbooks.${id}.from "${from}" advertises artifact schema ${artifactSchema} ` +
|
|
2814
|
+
`but its ${implementation} implements schema ${runtimeProfile.artifactSchema}`,
|
|
2815
|
+
);
|
|
2816
|
+
}
|
|
2817
|
+
if (entry.id !== id) {
|
|
2093
2818
|
throw new Error(
|
|
2094
2819
|
`captain.options.playbooks.${id} key must equal the module manifest id "${entry.id}"`,
|
|
2095
2820
|
);
|
|
@@ -2132,7 +2857,11 @@ async function buildEnablements(
|
|
|
2132
2857
|
for (const role of entry.requiredRoleIds) {
|
|
2133
2858
|
const path = `captain.options.playbooks.${id}.roles.${role}`;
|
|
2134
2859
|
const rawBinding = snapshotRecord(roleRecord[role], path);
|
|
2135
|
-
rejectSnapshotKeys(
|
|
2860
|
+
rejectSnapshotKeys(
|
|
2861
|
+
rawBinding,
|
|
2862
|
+
['playerId', 'model', 'effort', 'fastMode'],
|
|
2863
|
+
path,
|
|
2864
|
+
);
|
|
2136
2865
|
const playerId = snapshotString(rawBinding.playerId, `${path}.playerId`);
|
|
2137
2866
|
if (!PLAYER_ID_PATTERN.test(playerId) || playerId === INTERNAL_CAPTAIN_ID) {
|
|
2138
2867
|
throw new Error(`${path}.playerId is not a canonical player id`);
|
|
@@ -2147,6 +2876,14 @@ async function buildEnablements(
|
|
|
2147
2876
|
playerId,
|
|
2148
2877
|
model: snapshotTuningSelection(rawBinding.model, `${path}.model`),
|
|
2149
2878
|
effort: snapshotEffortSelection(rawBinding.effort, `${path}.effort`),
|
|
2879
|
+
...(rawBinding.fastMode === undefined
|
|
2880
|
+
? {}
|
|
2881
|
+
: {
|
|
2882
|
+
fastMode: snapshotFastMode(
|
|
2883
|
+
rawBinding.fastMode,
|
|
2884
|
+
`${path}.fastMode`,
|
|
2885
|
+
),
|
|
2886
|
+
}),
|
|
2150
2887
|
agent,
|
|
2151
2888
|
});
|
|
2152
2889
|
}
|
|
@@ -2164,16 +2901,42 @@ async function buildEnablements(
|
|
|
2164
2901
|
entry.validateOptions(record.options),
|
|
2165
2902
|
`captain.options.playbooks.${id}.options`,
|
|
2166
2903
|
);
|
|
2904
|
+
rejectConfiguredHostCapabilities(
|
|
2905
|
+
validatedOptions,
|
|
2906
|
+
`captain.options.playbooks.${id}.options`,
|
|
2907
|
+
);
|
|
2167
2908
|
entries.push(entry);
|
|
2168
2909
|
byId.set(entry.id, entry);
|
|
2169
2910
|
byCommand.set(command, entry);
|
|
2911
|
+
const hostCapability = validateHostCapabilities(
|
|
2912
|
+
suppliedHostCapabilities[entry.id],
|
|
2913
|
+
entry,
|
|
2914
|
+
command,
|
|
2915
|
+
);
|
|
2916
|
+
expectedHostCapabilityIds.push(entry.id);
|
|
2917
|
+
hostCapabilitiesById.set(entry.id, hostCapability);
|
|
2170
2918
|
enablementById.set(entry.id, {
|
|
2171
2919
|
entry,
|
|
2920
|
+
artifactSchema,
|
|
2172
2921
|
command,
|
|
2173
2922
|
options: validatedOptions,
|
|
2174
2923
|
roleBindings,
|
|
2175
2924
|
});
|
|
2176
2925
|
}
|
|
2926
|
+
const suppliedHostCapabilityIds = Object.keys(
|
|
2927
|
+
suppliedHostCapabilities,
|
|
2928
|
+
).sort();
|
|
2929
|
+
expectedHostCapabilityIds.sort();
|
|
2930
|
+
if (
|
|
2931
|
+
!isDeepStrictEqual(
|
|
2932
|
+
suppliedHostCapabilityIds,
|
|
2933
|
+
expectedHostCapabilityIds,
|
|
2934
|
+
)
|
|
2935
|
+
) {
|
|
2936
|
+
throw new Error(
|
|
2937
|
+
'current-host construction capabilities must exactly cover schema-3 playbooks',
|
|
2938
|
+
);
|
|
2939
|
+
}
|
|
2177
2940
|
const referenced = new Set(
|
|
2178
2941
|
[...enablementById.values()].flatMap((enablement) =>
|
|
2179
2942
|
[...enablement.roleBindings.values()].map((binding) => binding.playerId),
|
|
@@ -2190,6 +2953,7 @@ async function buildEnablements(
|
|
|
2190
2953
|
byCommand,
|
|
2191
2954
|
byId,
|
|
2192
2955
|
enablementById,
|
|
2956
|
+
hostCapabilitiesById,
|
|
2193
2957
|
captainAgent,
|
|
2194
2958
|
playerAgents,
|
|
2195
2959
|
};
|
|
@@ -2205,6 +2969,17 @@ export function createPlaybookCaptainShell(
|
|
|
2205
2969
|
const createCaptainRuntime: NonNullable<
|
|
2206
2970
|
PlaybookCaptainDeps['createCaptainRuntime']
|
|
2207
2971
|
> = deps.createCaptainRuntime ?? createDefaultCaptainRuntime;
|
|
2972
|
+
const unresolvedEffectSettlement = deps.unresolvedEffectSettlement;
|
|
2973
|
+
let pendingHostCapabilities = deps.hostCapabilities;
|
|
2974
|
+
let currentEffectLedger = () => emptyPlaybookEffectLedger();
|
|
2975
|
+
// The returned shell must not retain the caller's aggregate dependency
|
|
2976
|
+
// object after its one live capability input has moved to a clearable slot.
|
|
2977
|
+
deps = {};
|
|
2978
|
+
const buildCurrentEnablements = async (): Promise<BuiltRegistry> => {
|
|
2979
|
+
const hostCapabilities = pendingHostCapabilities;
|
|
2980
|
+
pendingHostCapabilities = undefined;
|
|
2981
|
+
return buildEnablements(options, loadModule, hostCapabilities);
|
|
2982
|
+
};
|
|
2208
2983
|
let captainAgent: SessionAgent | undefined;
|
|
2209
2984
|
let captainAdapter: string | undefined;
|
|
2210
2985
|
let playerAgents = new Map<string, SessionAgent>();
|
|
@@ -2214,6 +2989,10 @@ export function createPlaybookCaptainShell(
|
|
|
2214
2989
|
let byCommand = new Map<string, PlaybookCaptainRegistryEntry>();
|
|
2215
2990
|
let byId = new Map<string, PlaybookCaptainRegistryEntry>();
|
|
2216
2991
|
let enablementById = new Map<string, Enablement>();
|
|
2992
|
+
let hostCapabilitiesById = new Map<
|
|
2993
|
+
string,
|
|
2994
|
+
PlaybookHostConstructionCapabilities
|
|
2995
|
+
>();
|
|
2217
2996
|
let session: CaptainSession | undefined;
|
|
2218
2997
|
let sessionEmissionsOpen = false;
|
|
2219
2998
|
let closedGateAttempted = false;
|
|
@@ -2228,6 +3007,9 @@ export function createPlaybookCaptainShell(
|
|
|
2228
3007
|
let activeContext: CaptainContext | undefined;
|
|
2229
3008
|
const frames: EngagementFrame[] = [];
|
|
2230
3009
|
let mode: ShellMode = 'chat';
|
|
3010
|
+
let retainedEffectReconciliation:
|
|
3011
|
+
| PlaybookCaptainRetainedEffectReconciliation
|
|
3012
|
+
| undefined;
|
|
2231
3013
|
let pendingBossQuestions: unknown;
|
|
2232
3014
|
let lastError: { name: string; message: string } | undefined;
|
|
2233
3015
|
let activeTurnSummary: ActiveTurnSummary | undefined;
|
|
@@ -2308,6 +3090,50 @@ export function createPlaybookCaptainShell(
|
|
|
2308
3090
|
| undefined;
|
|
2309
3091
|
let lastAction: ControllerAction | undefined;
|
|
2310
3092
|
let lastSettlementStatus: SettlementEvidence['status'] | undefined;
|
|
3093
|
+
// DR-038 §2: a turn can dispose its root before the durable caller asks
|
|
3094
|
+
// for settlement. Keep only that turn's latest safe pre-terminal generation
|
|
3095
|
+
// and its per-root retain/clear decisions until `exportSettlement()` pairs
|
|
3096
|
+
// them with the complete shell snapshot.
|
|
3097
|
+
type RetainedGenerationCandidate =
|
|
3098
|
+
| {
|
|
3099
|
+
readonly status: 'captured';
|
|
3100
|
+
readonly generation: PlaybookCaptainRetainedGeneration;
|
|
3101
|
+
}
|
|
3102
|
+
| { readonly status: 'incapable' }
|
|
3103
|
+
| { readonly status: 'unsafe' };
|
|
3104
|
+
const retainedGenerationCandidates = new Map<
|
|
3105
|
+
string,
|
|
3106
|
+
RetainedGenerationCandidate
|
|
3107
|
+
>();
|
|
3108
|
+
const pendingRetentionUpdates = new Map<
|
|
3109
|
+
string,
|
|
3110
|
+
PlaybookCaptainRetentionUpdate
|
|
3111
|
+
>();
|
|
3112
|
+
interface RetainedGenerationOffer {
|
|
3113
|
+
readonly generation: PlaybookCaptainRetainedGeneration;
|
|
3114
|
+
readonly requiresEffectReconciliation: boolean;
|
|
3115
|
+
/** Fresh, uninitialized runtimes reserved for one adoption attempt. */
|
|
3116
|
+
readonly runtimes: readonly PlaybookRuntime[];
|
|
3117
|
+
}
|
|
3118
|
+
const retainedGenerations = new Map<
|
|
3119
|
+
string,
|
|
3120
|
+
PlaybookCaptainRetainedGeneration
|
|
3121
|
+
>();
|
|
3122
|
+
const retainedGenerationOffers = new Map<
|
|
3123
|
+
string,
|
|
3124
|
+
RetainedGenerationOffer
|
|
3125
|
+
>();
|
|
3126
|
+
const ineligibleRetainedGenerations = new Set<string>();
|
|
3127
|
+
const retainedGenerationRootClears = new Set<string>();
|
|
3128
|
+
const retiredRetainedRuntimes: PlaybookRuntime[] = [];
|
|
3129
|
+
let retainedGenerationsInstalled = false;
|
|
3130
|
+
let retainedGenerationInstallationInProgress = false;
|
|
3131
|
+
let retainedGenerationInstallationClosed = false;
|
|
3132
|
+
let retentionSettlementReady = false;
|
|
3133
|
+
let abandonmentSettlementUnsafe = false;
|
|
3134
|
+
let settledTurnUnresolvedEffects:
|
|
3135
|
+
| readonly PlaybookCaptainUnresolvedEffect[]
|
|
3136
|
+
| undefined;
|
|
2311
3137
|
// DR-029: a run that lands in the runtime's own failure state
|
|
2312
3138
|
// is an outcome the report must name. `processFrameResult` records it here
|
|
2313
3139
|
// and the settling selection folds it into its facts, so the grounding the
|
|
@@ -2319,6 +3145,826 @@ export function createPlaybookCaptainShell(
|
|
|
2319
3145
|
const frameLabel = (frame: EngagementFrame): string =>
|
|
2320
3146
|
`/${frame.enablement.command}`;
|
|
2321
3147
|
|
|
3148
|
+
type UnresolvedEnvelopeReference =
|
|
3149
|
+
| { readonly kind: 'boundary'; readonly boundaryId: string }
|
|
3150
|
+
| { readonly kind: 'logical-operation'; readonly operationId: string };
|
|
3151
|
+
|
|
3152
|
+
const capturedUnresolvedEnvelopeReferences = (
|
|
3153
|
+
frame: EngagementFrame,
|
|
3154
|
+
): readonly UnresolvedEnvelopeReference[] => {
|
|
3155
|
+
let advertisesUnresolved = false;
|
|
3156
|
+
try {
|
|
3157
|
+
advertisesUnresolved =
|
|
3158
|
+
frame.runtime
|
|
3159
|
+
.describe?.()
|
|
3160
|
+
.actions.some(
|
|
3161
|
+
({ id }) =>
|
|
3162
|
+
id === UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID ||
|
|
3163
|
+
id === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID,
|
|
3164
|
+
) === true;
|
|
3165
|
+
} catch {
|
|
3166
|
+
advertisesUnresolved = true;
|
|
3167
|
+
}
|
|
3168
|
+
if (typeof frame.runtime.unresolvedEffectEnvelopes !== 'function') {
|
|
3169
|
+
if (advertisesUnresolved) {
|
|
3170
|
+
throw new Error(
|
|
3171
|
+
`${frameLabel(frame)} unresolved-effect runtime exposes no envelope identities`,
|
|
3172
|
+
);
|
|
3173
|
+
}
|
|
3174
|
+
return [];
|
|
3175
|
+
}
|
|
3176
|
+
const detached = snapshotJsonValue(
|
|
3177
|
+
frame.runtime.unresolvedEffectEnvelopes(),
|
|
3178
|
+
`${frameLabel(frame)} unresolved effect envelope identities`,
|
|
3179
|
+
);
|
|
3180
|
+
if (!Array.isArray(detached)) {
|
|
3181
|
+
throw new TypeError(
|
|
3182
|
+
`${frameLabel(frame)} unresolved effect envelope identities must be an array`,
|
|
3183
|
+
);
|
|
3184
|
+
}
|
|
3185
|
+
return detached.map((raw, index) => {
|
|
3186
|
+
const path = `${frameLabel(frame)} unresolved effect envelope identities[${index}]`;
|
|
3187
|
+
const record = snapshotRecord(raw, path);
|
|
3188
|
+
if (record.kind === 'boundary') {
|
|
3189
|
+
rejectSnapshotKeys(record, ['kind', 'boundaryId'], path);
|
|
3190
|
+
return {
|
|
3191
|
+
kind: 'boundary' as const,
|
|
3192
|
+
boundaryId: snapshotString(record.boundaryId, `${path}.boundaryId`),
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
if (record.kind === 'logical-operation') {
|
|
3196
|
+
rejectSnapshotKeys(record, ['kind', 'operationId'], path);
|
|
3197
|
+
return {
|
|
3198
|
+
kind: 'logical-operation' as const,
|
|
3199
|
+
operationId: snapshotString(record.operationId, `${path}.operationId`),
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
throw new TypeError(`${path}.kind is not supported`);
|
|
3203
|
+
});
|
|
3204
|
+
};
|
|
3205
|
+
|
|
3206
|
+
const unresolvedEffectFromReceipt = (
|
|
3207
|
+
receipt: NonNullable<
|
|
3208
|
+
PlaybookEffectLedger['boundaries'][number]['physicalReceipt']
|
|
3209
|
+
>,
|
|
3210
|
+
baselineHead = receipt.baseline.head,
|
|
3211
|
+
): PlaybookCaptainUnresolvedEffect | undefined => {
|
|
3212
|
+
if (receipt.classification === 'unchanged') return undefined;
|
|
3213
|
+
return {
|
|
3214
|
+
classification: receipt.classification,
|
|
3215
|
+
baselineHead,
|
|
3216
|
+
...(receipt.after === undefined ? {} : { afterHead: receipt.after.head }),
|
|
3217
|
+
...(receipt.commitOid === undefined ? {} : { commitOid: receipt.commitOid }),
|
|
3218
|
+
};
|
|
3219
|
+
};
|
|
3220
|
+
|
|
3221
|
+
const cumulativeOpenLogicalEffect = (
|
|
3222
|
+
operation: PlaybookEffectLedger['logicalOperations'][number],
|
|
3223
|
+
boundaries: readonly PlaybookEffectLedger['boundaries'][number][],
|
|
3224
|
+
): PlaybookCaptainUnresolvedEffect | undefined => {
|
|
3225
|
+
const original = operation.originalBaseline;
|
|
3226
|
+
const latest = boundaries.at(-1)!;
|
|
3227
|
+
const after = latest.after ?? operation.checkpoint;
|
|
3228
|
+
const receipt = latest.physicalReceipt;
|
|
3229
|
+
if (receipt === undefined) {
|
|
3230
|
+
return {
|
|
3231
|
+
classification: 'incomplete',
|
|
3232
|
+
baselineHead: original.head,
|
|
3233
|
+
...(after === undefined ? {} : { afterHead: after.head }),
|
|
3234
|
+
};
|
|
3235
|
+
}
|
|
3236
|
+
if (after === undefined) {
|
|
3237
|
+
return unresolvedEffectFromReceipt(receipt, original.head);
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
// An open deferred chain has no authoritative cumulative receipt. Reuse
|
|
3241
|
+
// physical ancestry only while every preceding checkpoint is one of the
|
|
3242
|
+
// same-HEAD dispositions that can lawfully keep a deferred operation
|
|
3243
|
+
// open. Any other history is bounded but not cumulatively attributable.
|
|
3244
|
+
const checkpointChainIsSafe = boundaries
|
|
3245
|
+
.slice(0, -1)
|
|
3246
|
+
.every(
|
|
3247
|
+
(boundary) =>
|
|
3248
|
+
boundary.after?.head === original.head &&
|
|
3249
|
+
(boundary.physicalReceipt?.classification === 'unchanged' ||
|
|
3250
|
+
boundary.physicalReceipt?.classification ===
|
|
3251
|
+
'worktree-only-change'),
|
|
3252
|
+
);
|
|
3253
|
+
if (!checkpointChainIsSafe || latest.baseline.head !== original.head) {
|
|
3254
|
+
return {
|
|
3255
|
+
classification: 'observation-ambiguous',
|
|
3256
|
+
baselineHead: original.head,
|
|
3257
|
+
afterHead: after.head,
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
if (
|
|
3262
|
+
receipt.classification !== 'unchanged' &&
|
|
3263
|
+
receipt.classification !== 'worktree-only-change' &&
|
|
3264
|
+
receipt.classification !== 'one-descendant-commit'
|
|
3265
|
+
) {
|
|
3266
|
+
return unresolvedEffectFromReceipt(receipt, original.head);
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
const sameProjection = isDeepStrictEqual(
|
|
3270
|
+
original.projection,
|
|
3271
|
+
after.projection,
|
|
3272
|
+
);
|
|
3273
|
+
if (after.head === original.head) {
|
|
3274
|
+
if (
|
|
3275
|
+
receipt.classification !== 'unchanged' &&
|
|
3276
|
+
receipt.classification !== 'worktree-only-change'
|
|
3277
|
+
) {
|
|
3278
|
+
return {
|
|
3279
|
+
classification: 'observation-ambiguous',
|
|
3280
|
+
baselineHead: original.head,
|
|
3281
|
+
afterHead: after.head,
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
if (sameProjection) return undefined;
|
|
3285
|
+
const preservesOriginal = Object.entries(original.projection).every(
|
|
3286
|
+
([path, entry]) =>
|
|
3287
|
+
Object.hasOwn(after.projection, path) &&
|
|
3288
|
+
isDeepStrictEqual(entry, after.projection[path]),
|
|
3289
|
+
);
|
|
3290
|
+
return {
|
|
3291
|
+
classification: preservesOriginal
|
|
3292
|
+
? 'worktree-only-change'
|
|
3293
|
+
: 'observation-ambiguous',
|
|
3294
|
+
baselineHead: original.head,
|
|
3295
|
+
afterHead: after.head,
|
|
3296
|
+
};
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
if (
|
|
3300
|
+
receipt.classification === 'one-descendant-commit' &&
|
|
3301
|
+
sameProjection
|
|
3302
|
+
) {
|
|
3303
|
+
return {
|
|
3304
|
+
classification: 'one-descendant-commit',
|
|
3305
|
+
baselineHead: original.head,
|
|
3306
|
+
afterHead: after.head,
|
|
3307
|
+
commitOid: after.head,
|
|
3308
|
+
};
|
|
3309
|
+
}
|
|
3310
|
+
return {
|
|
3311
|
+
classification: 'observation-ambiguous',
|
|
3312
|
+
baselineHead: original.head,
|
|
3313
|
+
afterHead: after.head,
|
|
3314
|
+
};
|
|
3315
|
+
};
|
|
3316
|
+
|
|
3317
|
+
const projectUnresolvedEffects = (
|
|
3318
|
+
ledger: PlaybookEffectLedger,
|
|
3319
|
+
references: readonly UnresolvedEnvelopeReference[],
|
|
3320
|
+
): readonly PlaybookCaptainUnresolvedEffect[] => {
|
|
3321
|
+
const pendingReferences = [...references];
|
|
3322
|
+
const projected: Array<{
|
|
3323
|
+
readonly order: number;
|
|
3324
|
+
readonly effect: PlaybookCaptainUnresolvedEffect;
|
|
3325
|
+
}> = [];
|
|
3326
|
+
const seenBoundaries = new Set<string>();
|
|
3327
|
+
const seenOperations = new Set<string>();
|
|
3328
|
+
for (let index = 0; index < pendingReferences.length; index += 1) {
|
|
3329
|
+
const reference = pendingReferences[index]!;
|
|
3330
|
+
if (reference.kind === 'boundary') {
|
|
3331
|
+
if (seenBoundaries.has(reference.boundaryId)) continue;
|
|
3332
|
+
seenBoundaries.add(reference.boundaryId);
|
|
3333
|
+
const boundary = ledger.boundaries.find(
|
|
3334
|
+
({ boundaryId }) => boundaryId === reference.boundaryId,
|
|
3335
|
+
);
|
|
3336
|
+
if (boundary === undefined) {
|
|
3337
|
+
throw new Error(
|
|
3338
|
+
`unresolved effect boundary ${JSON.stringify(reference.boundaryId)} is absent from the authoritative ledger`,
|
|
3339
|
+
);
|
|
3340
|
+
}
|
|
3341
|
+
if (boundary.logicalOperationId !== undefined) {
|
|
3342
|
+
if (!seenOperations.has(boundary.logicalOperationId)) {
|
|
3343
|
+
pendingReferences.push({
|
|
3344
|
+
kind: 'logical-operation',
|
|
3345
|
+
operationId: boundary.logicalOperationId,
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
continue;
|
|
3349
|
+
}
|
|
3350
|
+
const effect =
|
|
3351
|
+
boundary.physicalReceipt === undefined
|
|
3352
|
+
? {
|
|
3353
|
+
classification: 'incomplete' as const,
|
|
3354
|
+
baselineHead: boundary.baseline.head,
|
|
3355
|
+
...(boundary.after === undefined
|
|
3356
|
+
? {}
|
|
3357
|
+
: { afterHead: boundary.after.head }),
|
|
3358
|
+
}
|
|
3359
|
+
: unresolvedEffectFromReceipt(boundary.physicalReceipt);
|
|
3360
|
+
if (effect !== undefined) {
|
|
3361
|
+
projected.push({ order: boundary.sequence, effect });
|
|
3362
|
+
}
|
|
3363
|
+
continue;
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
if (seenOperations.has(reference.operationId)) continue;
|
|
3367
|
+
seenOperations.add(reference.operationId);
|
|
3368
|
+
const operation = ledger.logicalOperations.find(
|
|
3369
|
+
({ operationId }) => operationId === reference.operationId,
|
|
3370
|
+
);
|
|
3371
|
+
if (operation === undefined) {
|
|
3372
|
+
throw new Error(
|
|
3373
|
+
`unresolved logical operation ${JSON.stringify(reference.operationId)} is absent from the authoritative ledger`,
|
|
3374
|
+
);
|
|
3375
|
+
}
|
|
3376
|
+
const boundaries = operation.boundaryIds.map((boundaryId) => {
|
|
3377
|
+
const boundary = ledger.boundaries.find(
|
|
3378
|
+
(candidate) => candidate.boundaryId === boundaryId,
|
|
3379
|
+
);
|
|
3380
|
+
if (boundary === undefined) {
|
|
3381
|
+
throw new Error(
|
|
3382
|
+
`unresolved logical operation ${JSON.stringify(reference.operationId)} names an absent boundary`,
|
|
3383
|
+
);
|
|
3384
|
+
}
|
|
3385
|
+
seenBoundaries.add(boundaryId);
|
|
3386
|
+
return boundary;
|
|
3387
|
+
});
|
|
3388
|
+
let effect: PlaybookCaptainUnresolvedEffect | undefined;
|
|
3389
|
+
if (operation.logicalReceipt !== undefined) {
|
|
3390
|
+
effect = unresolvedEffectFromReceipt(
|
|
3391
|
+
operation.logicalReceipt,
|
|
3392
|
+
operation.originalBaseline.head,
|
|
3393
|
+
);
|
|
3394
|
+
} else {
|
|
3395
|
+
effect = cumulativeOpenLogicalEffect(operation, boundaries);
|
|
3396
|
+
}
|
|
3397
|
+
if (effect !== undefined) {
|
|
3398
|
+
projected.push({ order: boundaries[0]!.sequence, effect });
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
return assertPlaybookCaptainUnresolvedEffects(
|
|
3402
|
+
projected
|
|
3403
|
+
.sort((left, right) => left.order - right.order)
|
|
3404
|
+
.map(({ effect }) => effect),
|
|
3405
|
+
);
|
|
3406
|
+
};
|
|
3407
|
+
|
|
3408
|
+
const currentUnresolvedEffects =
|
|
3409
|
+
(): readonly PlaybookCaptainUnresolvedEffect[] => {
|
|
3410
|
+
const ledger = assertPlaybookEffectLedger(currentEffectLedger());
|
|
3411
|
+
const references: UnresolvedEnvelopeReference[] = [];
|
|
3412
|
+
for (const frame of frames) {
|
|
3413
|
+
references.push(...capturedUnresolvedEnvelopeReferences(frame));
|
|
3414
|
+
}
|
|
3415
|
+
if (retainedEffectReconciliation !== undefined) {
|
|
3416
|
+
for (const boundary of ledger.boundaries.slice(
|
|
3417
|
+
retainedEffectReconciliation.checkpoint.boundaries.length,
|
|
3418
|
+
)) {
|
|
3419
|
+
if (boundary.physicalReceipt?.classification === 'unchanged') {
|
|
3420
|
+
continue;
|
|
3421
|
+
}
|
|
3422
|
+
references.push(
|
|
3423
|
+
boundary.logicalOperationId === undefined
|
|
3424
|
+
? { kind: 'boundary', boundaryId: boundary.boundaryId }
|
|
3425
|
+
: {
|
|
3426
|
+
kind: 'logical-operation',
|
|
3427
|
+
operationId: boundary.logicalOperationId,
|
|
3428
|
+
},
|
|
3429
|
+
);
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
return projectUnresolvedEffects(ledger, references);
|
|
3433
|
+
};
|
|
3434
|
+
|
|
3435
|
+
const freezeTurnUnresolvedEffects =
|
|
3436
|
+
(): readonly PlaybookCaptainUnresolvedEffect[] => {
|
|
3437
|
+
const turn = activeTurn;
|
|
3438
|
+
if (turn?.unresolvedEffects !== undefined) return turn.unresolvedEffects;
|
|
3439
|
+
const frozen = currentUnresolvedEffects();
|
|
3440
|
+
if (turn !== undefined) turn.unresolvedEffects = frozen;
|
|
3441
|
+
settledTurnUnresolvedEffects = frozen;
|
|
3442
|
+
return frozen;
|
|
3443
|
+
};
|
|
3444
|
+
|
|
3445
|
+
const normalizeInstalledRetainedGenerations = (
|
|
3446
|
+
value: Readonly<Record<string, PlaybookCaptainRetainedGeneration>>,
|
|
3447
|
+
): Map<string, PlaybookCaptainRetainedGeneration> => {
|
|
3448
|
+
const path = 'Captain retained generations';
|
|
3449
|
+
const detached = snapshotJsonValue(value, path);
|
|
3450
|
+
const record = snapshotRecord(detached, path);
|
|
3451
|
+
const authoritativeEffectLedger = assertPlaybookEffectLedger(
|
|
3452
|
+
currentEffectLedger(),
|
|
3453
|
+
`${path} current host effect ledger`,
|
|
3454
|
+
);
|
|
3455
|
+
const sourceSessionIds = new Set<string>();
|
|
3456
|
+
const normalized = new Map<string, PlaybookCaptainRetainedGeneration>();
|
|
3457
|
+
for (const [rootPlaybookId, rawGeneration] of Object.entries(record)) {
|
|
3458
|
+
const generationPath = `${path}[${JSON.stringify(rootPlaybookId)}]`;
|
|
3459
|
+
const enablement = enablementById.get(rootPlaybookId);
|
|
3460
|
+
if (enablement === undefined) {
|
|
3461
|
+
throw new TypeError(
|
|
3462
|
+
`${generationPath} names a disabled root playbook`,
|
|
3463
|
+
);
|
|
3464
|
+
}
|
|
3465
|
+
const generation = snapshotRecord(rawGeneration, generationPath);
|
|
3466
|
+
rejectSnapshotKeys(
|
|
3467
|
+
generation,
|
|
3468
|
+
[
|
|
3469
|
+
'effectLedger',
|
|
3470
|
+
'frames',
|
|
3471
|
+
'retainedEffectReconciliation',
|
|
3472
|
+
'rootStateDescription',
|
|
3473
|
+
],
|
|
3474
|
+
generationPath,
|
|
3475
|
+
);
|
|
3476
|
+
const generationEffectLedger = assertPlaybookEffectLedger(
|
|
3477
|
+
generation.effectLedger,
|
|
3478
|
+
`${generationPath}.effectLedger`,
|
|
3479
|
+
);
|
|
3480
|
+
if (
|
|
3481
|
+
generationEffectLedger.boundaries.some(
|
|
3482
|
+
({ physicalReceipt }) => physicalReceipt === undefined,
|
|
3483
|
+
)
|
|
3484
|
+
) {
|
|
3485
|
+
throw new TypeError(
|
|
3486
|
+
`${generationPath}.effectLedger contains an incomplete physical boundary`,
|
|
3487
|
+
);
|
|
3488
|
+
}
|
|
3489
|
+
if (
|
|
3490
|
+
!isPlaybookEffectLedgerMonotonicExtension(
|
|
3491
|
+
generationEffectLedger,
|
|
3492
|
+
authoritativeEffectLedger,
|
|
3493
|
+
)
|
|
3494
|
+
) {
|
|
3495
|
+
throw new TypeError(
|
|
3496
|
+
`${generationPath}.effectLedger is not a monotonic prefix of the current host mirror`,
|
|
3497
|
+
);
|
|
3498
|
+
}
|
|
3499
|
+
const generationReconciliation =
|
|
3500
|
+
generation.retainedEffectReconciliation === undefined
|
|
3501
|
+
? undefined
|
|
3502
|
+
: snapshotRecord(
|
|
3503
|
+
generation.retainedEffectReconciliation,
|
|
3504
|
+
`${generationPath}.retainedEffectReconciliation`,
|
|
3505
|
+
);
|
|
3506
|
+
if (generationReconciliation !== undefined) {
|
|
3507
|
+
rejectSnapshotKeys(
|
|
3508
|
+
generationReconciliation,
|
|
3509
|
+
['sourceGenerationId'],
|
|
3510
|
+
`${generationPath}.retainedEffectReconciliation`,
|
|
3511
|
+
);
|
|
3512
|
+
}
|
|
3513
|
+
const sourceGenerationId =
|
|
3514
|
+
generationReconciliation === undefined
|
|
3515
|
+
? undefined
|
|
3516
|
+
: snapshotUuid(
|
|
3517
|
+
generationReconciliation.sourceGenerationId,
|
|
3518
|
+
`${generationPath}.retainedEffectReconciliation.sourceGenerationId`,
|
|
3519
|
+
);
|
|
3520
|
+
if (!Array.isArray(generation.frames) || generation.frames.length === 0) {
|
|
3521
|
+
throw new TypeError(`${generationPath}.frames must be non-empty`);
|
|
3522
|
+
}
|
|
3523
|
+
const rootStateDescription =
|
|
3524
|
+
generation.rootStateDescription === undefined
|
|
3525
|
+
? undefined
|
|
3526
|
+
: snapshotString(
|
|
3527
|
+
generation.rootStateDescription,
|
|
3528
|
+
`${generationPath}.rootStateDescription`,
|
|
3529
|
+
);
|
|
3530
|
+
const normalizedFrames: PlaybookCaptainFrameSnapshot[] = [];
|
|
3531
|
+
const playbookIds = new Set<string>();
|
|
3532
|
+
let markedCaptureEffectLedger:
|
|
3533
|
+
| DeepReadonly<PlaybookEffectLedger>
|
|
3534
|
+
| undefined;
|
|
3535
|
+
for (const [index, rawFrame] of generation.frames.entries()) {
|
|
3536
|
+
const framePath = `${generationPath}.frames[${index}]`;
|
|
3537
|
+
const frame = snapshotRecord(rawFrame, framePath);
|
|
3538
|
+
rejectSnapshotKeys(
|
|
3539
|
+
frame,
|
|
3540
|
+
[
|
|
3541
|
+
'playbookId',
|
|
3542
|
+
'sessionId',
|
|
3543
|
+
'rootSessionId',
|
|
3544
|
+
'depth',
|
|
3545
|
+
'parentSessionId',
|
|
3546
|
+
'parentCallId',
|
|
3547
|
+
'options',
|
|
3548
|
+
'roleBindings',
|
|
3549
|
+
'runtime',
|
|
3550
|
+
],
|
|
3551
|
+
framePath,
|
|
3552
|
+
);
|
|
3553
|
+
const playbookId = snapshotString(
|
|
3554
|
+
frame.playbookId,
|
|
3555
|
+
`${framePath}.playbookId`,
|
|
3556
|
+
);
|
|
3557
|
+
const frameEnablement = enablementById.get(playbookId);
|
|
3558
|
+
if (frameEnablement === undefined) {
|
|
3559
|
+
throw new TypeError(`${framePath} names a disabled playbook`);
|
|
3560
|
+
}
|
|
3561
|
+
if (playbookIds.has(playbookId)) {
|
|
3562
|
+
throw new TypeError(
|
|
3563
|
+
`${generationPath}.frames must not contain a playbook cycle`,
|
|
3564
|
+
);
|
|
3565
|
+
}
|
|
3566
|
+
playbookIds.add(playbookId);
|
|
3567
|
+
const sessionId = snapshotUuid(
|
|
3568
|
+
frame.sessionId,
|
|
3569
|
+
`${framePath}.sessionId`,
|
|
3570
|
+
);
|
|
3571
|
+
if (sourceSessionIds.has(sessionId)) {
|
|
3572
|
+
throw new TypeError(
|
|
3573
|
+
`${path} frame session ids must be unique across generations`,
|
|
3574
|
+
);
|
|
3575
|
+
}
|
|
3576
|
+
sourceSessionIds.add(sessionId);
|
|
3577
|
+
const rootSessionId = snapshotUuid(
|
|
3578
|
+
frame.rootSessionId,
|
|
3579
|
+
`${framePath}.rootSessionId`,
|
|
3580
|
+
);
|
|
3581
|
+
const depth = snapshotInteger(frame.depth, `${framePath}.depth`);
|
|
3582
|
+
const parentSessionId =
|
|
3583
|
+
frame.parentSessionId === undefined
|
|
3584
|
+
? undefined
|
|
3585
|
+
: snapshotUuid(
|
|
3586
|
+
frame.parentSessionId,
|
|
3587
|
+
`${framePath}.parentSessionId`,
|
|
3588
|
+
);
|
|
3589
|
+
const parentCallId =
|
|
3590
|
+
frame.parentCallId === undefined
|
|
3591
|
+
? undefined
|
|
3592
|
+
: snapshotString(
|
|
3593
|
+
frame.parentCallId,
|
|
3594
|
+
`${framePath}.parentCallId`,
|
|
3595
|
+
);
|
|
3596
|
+
const options = frame.options as JsonValue;
|
|
3597
|
+
if (
|
|
3598
|
+
index === 0 &&
|
|
3599
|
+
!isDeepStrictEqual(options, frameEnablement.options)
|
|
3600
|
+
) {
|
|
3601
|
+
throw new TypeError(`${framePath}.options changed`);
|
|
3602
|
+
}
|
|
3603
|
+
const roleBindings = snapshotFrameRoleBindings(
|
|
3604
|
+
frame.roleBindings,
|
|
3605
|
+
`${framePath}.roleBindings`,
|
|
3606
|
+
);
|
|
3607
|
+
if (
|
|
3608
|
+
!isDeepStrictEqual(
|
|
3609
|
+
Object.keys(roleBindings).sort(),
|
|
3610
|
+
[...frameEnablement.entry.requiredRoleIds].sort(),
|
|
3611
|
+
)
|
|
3612
|
+
) {
|
|
3613
|
+
throw new TypeError(
|
|
3614
|
+
`${framePath}.roleBindings do not cover the current role set`,
|
|
3615
|
+
);
|
|
3616
|
+
}
|
|
3617
|
+
const runtime = assertPlaybookRuntimeSnapshot(
|
|
3618
|
+
frame.runtime,
|
|
3619
|
+
playbookId,
|
|
3620
|
+
{ allowSuspendedCall: true },
|
|
3621
|
+
);
|
|
3622
|
+
const retainedReconciliation = runtime.retainedEffectReconciliation;
|
|
3623
|
+
if (retainedReconciliation === undefined) {
|
|
3624
|
+
if (!isDeepStrictEqual(runtime.effectLedger, generationEffectLedger)) {
|
|
3625
|
+
throw new TypeError(
|
|
3626
|
+
`${framePath}.runtime effect ledger differs from the retained checkpoint`,
|
|
3627
|
+
);
|
|
3628
|
+
}
|
|
3629
|
+
} else {
|
|
3630
|
+
if (
|
|
3631
|
+
!isDeepStrictEqual(
|
|
3632
|
+
retainedReconciliation.checkpoint,
|
|
3633
|
+
generationEffectLedger,
|
|
3634
|
+
) ||
|
|
3635
|
+
isDeepStrictEqual(runtime.effectLedger, generationEffectLedger) ||
|
|
3636
|
+
!isPlaybookEffectLedgerMonotonicExtension(
|
|
3637
|
+
runtime.effectLedger,
|
|
3638
|
+
authoritativeEffectLedger,
|
|
3639
|
+
)
|
|
3640
|
+
) {
|
|
3641
|
+
throw new TypeError(
|
|
3642
|
+
`${framePath}.runtime retained-effect evidence is inconsistent`,
|
|
3643
|
+
);
|
|
3644
|
+
}
|
|
3645
|
+
if (markedCaptureEffectLedger === undefined) {
|
|
3646
|
+
markedCaptureEffectLedger = runtime.effectLedger;
|
|
3647
|
+
} else if (
|
|
3648
|
+
!isDeepStrictEqual(
|
|
3649
|
+
runtime.effectLedger,
|
|
3650
|
+
markedCaptureEffectLedger,
|
|
3651
|
+
)
|
|
3652
|
+
) {
|
|
3653
|
+
throw new TypeError(
|
|
3654
|
+
`${framePath}.runtime effect ledger differs from the marked generation capture mirror`,
|
|
3655
|
+
);
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
if (
|
|
3659
|
+
runtime.state.status !== 'active' ||
|
|
3660
|
+
!runtime.state.quiescent ||
|
|
3661
|
+
typeof runtime.state.stateId !== 'string' ||
|
|
3662
|
+
runtime.state.stateId.trim().length === 0
|
|
3663
|
+
) {
|
|
3664
|
+
throw new TypeError(
|
|
3665
|
+
`${framePath}.runtime must be active, quiescent, and state-identified`,
|
|
3666
|
+
);
|
|
3667
|
+
}
|
|
3668
|
+
for (const question of runtime.pendingBossQuestions) {
|
|
3669
|
+
if (
|
|
3670
|
+
question.asker.kind === 'role' &&
|
|
3671
|
+
roleBindings[question.asker.roleId] === undefined
|
|
3672
|
+
) {
|
|
3673
|
+
throw new TypeError(
|
|
3674
|
+
`${framePath}.runtime pending question names an unbound role`,
|
|
3675
|
+
);
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
for (const roleId of Object.keys(runtime.roleResumeTokens)) {
|
|
3679
|
+
if (roleBindings[roleId] === undefined) {
|
|
3680
|
+
throw new TypeError(
|
|
3681
|
+
`${framePath}.runtime role-resume token names an unbound role`,
|
|
3682
|
+
);
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
normalizedFrames.push({
|
|
3686
|
+
playbookId,
|
|
3687
|
+
sessionId,
|
|
3688
|
+
rootSessionId,
|
|
3689
|
+
depth,
|
|
3690
|
+
...(parentSessionId === undefined ? {} : { parentSessionId }),
|
|
3691
|
+
...(parentCallId === undefined ? {} : { parentCallId }),
|
|
3692
|
+
options,
|
|
3693
|
+
roleBindings,
|
|
3694
|
+
runtime,
|
|
3695
|
+
});
|
|
3696
|
+
}
|
|
3697
|
+
const sourceRootSessionId = normalizedFrames[0]!.sessionId;
|
|
3698
|
+
for (const [index, frame] of normalizedFrames.entries()) {
|
|
3699
|
+
if (frame.depth !== index || frame.rootSessionId !== sourceRootSessionId) {
|
|
3700
|
+
throw new TypeError(
|
|
3701
|
+
`${generationPath}.frames have inconsistent depth or root identity`,
|
|
3702
|
+
);
|
|
3703
|
+
}
|
|
3704
|
+
if (index === 0) {
|
|
3705
|
+
if (
|
|
3706
|
+
frame.playbookId !== rootPlaybookId ||
|
|
3707
|
+
frame.sessionId !== frame.rootSessionId ||
|
|
3708
|
+
frame.parentSessionId !== undefined ||
|
|
3709
|
+
frame.parentCallId !== undefined
|
|
3710
|
+
) {
|
|
3711
|
+
throw new TypeError(
|
|
3712
|
+
`${generationPath}.frames[0] is not the named root`,
|
|
3713
|
+
);
|
|
3714
|
+
}
|
|
3715
|
+
continue;
|
|
3716
|
+
}
|
|
3717
|
+
const parent = normalizedFrames[index - 1]!;
|
|
3718
|
+
const suspended = parent.runtime.suspendedCall;
|
|
3719
|
+
if (
|
|
3720
|
+
frame.parentSessionId !== parent.sessionId ||
|
|
3721
|
+
frame.parentCallId === undefined ||
|
|
3722
|
+
suspended === undefined ||
|
|
3723
|
+
suspended.callId !== frame.parentCallId ||
|
|
3724
|
+
suspended.playbookId !== frame.playbookId ||
|
|
3725
|
+
suspended.childSessionId !== frame.sessionId
|
|
3726
|
+
) {
|
|
3727
|
+
throw new TypeError(
|
|
3728
|
+
`${generationPath}.frames[${index}] does not match its suspended parent edge`,
|
|
3729
|
+
);
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
const leaf = normalizedFrames.at(-1)!;
|
|
3733
|
+
if (
|
|
3734
|
+
leaf.runtime.suspendedCall !== undefined ||
|
|
3735
|
+
!leaf.runtime.state.tags.includes('playbook.parked')
|
|
3736
|
+
) {
|
|
3737
|
+
throw new TypeError(
|
|
3738
|
+
`${generationPath} leaf must be parked without a suspended child`,
|
|
3739
|
+
);
|
|
3740
|
+
}
|
|
3741
|
+
const markedFrames = normalizedFrames.filter(
|
|
3742
|
+
({ runtime }) => runtime.retainedEffectReconciliation !== undefined,
|
|
3743
|
+
);
|
|
3744
|
+
if (
|
|
3745
|
+
(sourceGenerationId === undefined && markedFrames.length !== 0) ||
|
|
3746
|
+
(sourceGenerationId !== undefined &&
|
|
3747
|
+
markedFrames.length !== normalizedFrames.length) ||
|
|
3748
|
+
(sourceGenerationId !== undefined &&
|
|
3749
|
+
normalizedFrames[0]!.runtime.retainedEffectReconciliation
|
|
3750
|
+
?.sourceSessionId !== sourceGenerationId)
|
|
3751
|
+
) {
|
|
3752
|
+
throw new TypeError(
|
|
3753
|
+
`${generationPath} retained-effect source marker is inconsistent`,
|
|
3754
|
+
);
|
|
3755
|
+
}
|
|
3756
|
+
normalized.set(
|
|
3757
|
+
rootPlaybookId,
|
|
3758
|
+
snapshotJsonValue(
|
|
3759
|
+
{
|
|
3760
|
+
effectLedger: generationEffectLedger,
|
|
3761
|
+
frames: normalizedFrames,
|
|
3762
|
+
...(sourceGenerationId === undefined
|
|
3763
|
+
? {}
|
|
3764
|
+
: {
|
|
3765
|
+
retainedEffectReconciliation: { sourceGenerationId },
|
|
3766
|
+
}),
|
|
3767
|
+
...(rootStateDescription === undefined
|
|
3768
|
+
? {}
|
|
3769
|
+
: { rootStateDescription }),
|
|
3770
|
+
},
|
|
3771
|
+
generationPath,
|
|
3772
|
+
) as unknown as PlaybookCaptainRetainedGeneration,
|
|
3773
|
+
);
|
|
3774
|
+
}
|
|
3775
|
+
return normalized;
|
|
3776
|
+
};
|
|
3777
|
+
|
|
3778
|
+
const runtimeRetainsGenerations = (runtime: PlaybookRuntime): boolean => {
|
|
3779
|
+
const metadata = runtime.retainedGenerationMetadata;
|
|
3780
|
+
return (
|
|
3781
|
+
typeof runtime.exportSnapshot === 'function' &&
|
|
3782
|
+
typeof runtime.restore === 'function' &&
|
|
3783
|
+
typeof runtime.adopt === 'function' &&
|
|
3784
|
+
metadata !== undefined &&
|
|
3785
|
+
Array.isArray(metadata.unfinishedFinalStateIds) &&
|
|
3786
|
+
metadata.unfinishedFinalStateIds.every(
|
|
3787
|
+
(stateId) => typeof stateId === 'string',
|
|
3788
|
+
)
|
|
3789
|
+
);
|
|
3790
|
+
};
|
|
3791
|
+
|
|
3792
|
+
class RetainedRuntimeCleanupError extends AggregateError {
|
|
3793
|
+
readonly failedRuntimes: readonly PlaybookRuntime[];
|
|
3794
|
+
|
|
3795
|
+
constructor(
|
|
3796
|
+
failures: readonly unknown[],
|
|
3797
|
+
message: string,
|
|
3798
|
+
failedRuntimes: readonly PlaybookRuntime[] = [],
|
|
3799
|
+
) {
|
|
3800
|
+
super(failures, message);
|
|
3801
|
+
this.name = 'RetainedRuntimeCleanupError';
|
|
3802
|
+
this.failedRuntimes = failedRuntimes;
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
const disposeRetainedRuntimeSet = async (
|
|
3807
|
+
runtimes: readonly PlaybookRuntime[],
|
|
3808
|
+
message: string,
|
|
3809
|
+
): Promise<void> => {
|
|
3810
|
+
const failures: unknown[] = [];
|
|
3811
|
+
const failedRuntimes: PlaybookRuntime[] = [];
|
|
3812
|
+
for (const runtime of [...runtimes].reverse()) {
|
|
3813
|
+
try {
|
|
3814
|
+
await runtime.dispose();
|
|
3815
|
+
} catch (error) {
|
|
3816
|
+
failures.push(error);
|
|
3817
|
+
failedRuntimes.unshift(runtime);
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
if (failures.length > 0) {
|
|
3821
|
+
throw new RetainedRuntimeCleanupError(
|
|
3822
|
+
failures,
|
|
3823
|
+
message,
|
|
3824
|
+
failedRuntimes,
|
|
3825
|
+
);
|
|
3826
|
+
}
|
|
3827
|
+
};
|
|
3828
|
+
|
|
3829
|
+
const retireRetainedOffer = (rootPlaybookId: string): void => {
|
|
3830
|
+
const offer = retainedGenerationOffers.get(rootPlaybookId);
|
|
3831
|
+
if (offer === undefined) return;
|
|
3832
|
+
retainedGenerationOffers.delete(rootPlaybookId);
|
|
3833
|
+
retiredRetainedRuntimes.push(...offer.runtimes);
|
|
3834
|
+
};
|
|
3835
|
+
|
|
3836
|
+
const applyRetentionUpdateToCatalog = (
|
|
3837
|
+
update: PlaybookCaptainRetentionUpdate,
|
|
3838
|
+
): void => {
|
|
3839
|
+
if (update.kind === 'clear') {
|
|
3840
|
+
retireRetainedOffer(update.rootPlaybookId);
|
|
3841
|
+
retainedGenerations.delete(update.rootPlaybookId);
|
|
3842
|
+
ineligibleRetainedGenerations.delete(update.rootPlaybookId);
|
|
3843
|
+
retainedGenerationRootClears.delete(update.rootPlaybookId);
|
|
3844
|
+
return;
|
|
3845
|
+
}
|
|
3846
|
+
const prior = retainedGenerations.get(update.rootPlaybookId);
|
|
3847
|
+
if (isDeepStrictEqual(prior, update.generation)) return;
|
|
3848
|
+
retireRetainedOffer(update.rootPlaybookId);
|
|
3849
|
+
retainedGenerations.set(update.rootPlaybookId, update.generation);
|
|
3850
|
+
ineligibleRetainedGenerations.delete(update.rootPlaybookId);
|
|
3851
|
+
retainedGenerationRootClears.delete(update.rootPlaybookId);
|
|
3852
|
+
};
|
|
3853
|
+
|
|
3854
|
+
const drainRetiredRetainedRuntimes = async (): Promise<void> => {
|
|
3855
|
+
if (retiredRetainedRuntimes.length === 0) return;
|
|
3856
|
+
const runtimes = retiredRetainedRuntimes.splice(0);
|
|
3857
|
+
try {
|
|
3858
|
+
await disposeRetainedRuntimeSet(
|
|
3859
|
+
runtimes,
|
|
3860
|
+
'retired retained-generation runtime cleanup failed',
|
|
3861
|
+
);
|
|
3862
|
+
} catch (error) {
|
|
3863
|
+
if (error instanceof RetainedRuntimeCleanupError) {
|
|
3864
|
+
retiredRetainedRuntimes.unshift(...error.failedRuntimes);
|
|
3865
|
+
}
|
|
3866
|
+
terminallyDisposed = true;
|
|
3867
|
+
lifecycle = 'closed';
|
|
3868
|
+
throw error;
|
|
3869
|
+
}
|
|
3870
|
+
};
|
|
3871
|
+
|
|
3872
|
+
const takeRetainedOfferRuntimes = (): readonly PlaybookRuntime[] => {
|
|
3873
|
+
const runtimes = [
|
|
3874
|
+
...[...retainedGenerationOffers.values()].flatMap((offer) => [
|
|
3875
|
+
...offer.runtimes,
|
|
3876
|
+
]),
|
|
3877
|
+
...retiredRetainedRuntimes.splice(0),
|
|
3878
|
+
];
|
|
3879
|
+
retainedGenerationOffers.clear();
|
|
3880
|
+
return runtimes;
|
|
3881
|
+
};
|
|
3882
|
+
|
|
3883
|
+
const prepareRetainedGenerationOffers = async (): Promise<void> => {
|
|
3884
|
+
if (rootFrame() !== undefined) return;
|
|
3885
|
+
for (const [rootPlaybookId, generation] of [...retainedGenerations].sort(
|
|
3886
|
+
([left], [right]) => left.localeCompare(right),
|
|
3887
|
+
)) {
|
|
3888
|
+
if (
|
|
3889
|
+
retainedGenerationOffers.has(rootPlaybookId) ||
|
|
3890
|
+
ineligibleRetainedGenerations.has(rootPlaybookId)
|
|
3891
|
+
) {
|
|
3892
|
+
continue;
|
|
3893
|
+
}
|
|
3894
|
+
const runtimes: PlaybookRuntime[] = [];
|
|
3895
|
+
try {
|
|
3896
|
+
for (const sourceFrame of generation.frames) {
|
|
3897
|
+
const enablement = enablementById.get(sourceFrame.playbookId)!;
|
|
3898
|
+
runtimes.push(
|
|
3899
|
+
createRuntimeForEnablement(enablement, hostCapabilitiesById),
|
|
3900
|
+
);
|
|
3901
|
+
}
|
|
3902
|
+
if (runtimes.some((runtime) => !runtimeRetainsGenerations(runtime))) {
|
|
3903
|
+
const rootRetainsGenerations = runtimeRetainsGenerations(runtimes[0]!);
|
|
3904
|
+
await disposeRetainedRuntimeSet(
|
|
3905
|
+
runtimes,
|
|
3906
|
+
`/${enablementById.get(rootPlaybookId)!.command} retained-generation capability cleanup failed`,
|
|
3907
|
+
);
|
|
3908
|
+
runtimes.splice(0);
|
|
3909
|
+
ineligibleRetainedGenerations.add(rootPlaybookId);
|
|
3910
|
+
if (!rootRetainsGenerations) {
|
|
3911
|
+
retainedGenerationRootClears.add(rootPlaybookId);
|
|
3912
|
+
}
|
|
3913
|
+
continue;
|
|
3914
|
+
}
|
|
3915
|
+
retainedGenerationOffers.set(rootPlaybookId, {
|
|
3916
|
+
generation,
|
|
3917
|
+
requiresEffectReconciliation:
|
|
3918
|
+
generation.retainedEffectReconciliation !== undefined ||
|
|
3919
|
+
generation.frames.some(
|
|
3920
|
+
({ runtime }) =>
|
|
3921
|
+
runtime.retainedEffectReconciliation !== undefined,
|
|
3922
|
+
) ||
|
|
3923
|
+
!retainedEffectLedgerCanRebase(
|
|
3924
|
+
generation.effectLedger as PlaybookEffectLedger,
|
|
3925
|
+
assertPlaybookEffectLedger(currentEffectLedger()),
|
|
3926
|
+
),
|
|
3927
|
+
runtimes,
|
|
3928
|
+
});
|
|
3929
|
+
} catch (error) {
|
|
3930
|
+
if (error instanceof RetainedRuntimeCleanupError) {
|
|
3931
|
+
retiredRetainedRuntimes.push(...error.failedRuntimes);
|
|
3932
|
+
terminallyDisposed = true;
|
|
3933
|
+
lifecycle = 'closed';
|
|
3934
|
+
throw error;
|
|
3935
|
+
}
|
|
3936
|
+
let cleanupError: unknown;
|
|
3937
|
+
if (runtimes.length > 0) {
|
|
3938
|
+
try {
|
|
3939
|
+
await disposeRetainedRuntimeSet(
|
|
3940
|
+
runtimes,
|
|
3941
|
+
'retained-generation preparation cleanup failed',
|
|
3942
|
+
);
|
|
3943
|
+
} catch (caught) {
|
|
3944
|
+
cleanupError = caught;
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
if (cleanupError !== undefined) {
|
|
3948
|
+
if (cleanupError instanceof RetainedRuntimeCleanupError) {
|
|
3949
|
+
retiredRetainedRuntimes.push(
|
|
3950
|
+
...cleanupError.failedRuntimes,
|
|
3951
|
+
);
|
|
3952
|
+
}
|
|
3953
|
+
terminallyDisposed = true;
|
|
3954
|
+
lifecycle = 'closed';
|
|
3955
|
+
throw new RetainedRuntimeCleanupError(
|
|
3956
|
+
[error, cleanupError],
|
|
3957
|
+
'retained-generation preparation and cleanup failed',
|
|
3958
|
+
cleanupError instanceof RetainedRuntimeCleanupError
|
|
3959
|
+
? cleanupError.failedRuntimes
|
|
3960
|
+
: [],
|
|
3961
|
+
);
|
|
3962
|
+
}
|
|
3963
|
+
ineligibleRetainedGenerations.add(rootPlaybookId);
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
};
|
|
3967
|
+
|
|
2322
3968
|
const bindingFor = (
|
|
2323
3969
|
frame: EngagementFrame,
|
|
2324
3970
|
localRole: string,
|
|
@@ -2370,7 +4016,10 @@ export function createPlaybookCaptainShell(
|
|
|
2370
4016
|
...(leafFrame()?.state
|
|
2371
4017
|
? { latestSubRuntimeState: leafFrame()!.state }
|
|
2372
4018
|
: {}),
|
|
2373
|
-
...(
|
|
4019
|
+
...(retainedEffectReconciliation === undefined &&
|
|
4020
|
+
pendingBossQuestions !== undefined
|
|
4021
|
+
? { pendingBossQuestions }
|
|
4022
|
+
: {}),
|
|
2374
4023
|
...(lastError ? { lastError } : {}),
|
|
2375
4024
|
...(captainSessionId ? { captainSessionId } : {}),
|
|
2376
4025
|
// Presence only: the pinned token value never reaches telemetry
|
|
@@ -2591,6 +4240,69 @@ export function createPlaybookCaptainShell(
|
|
|
2591
4240
|
}
|
|
2592
4241
|
};
|
|
2593
4242
|
|
|
4243
|
+
const observeSummaryTrace = (
|
|
4244
|
+
frame: EngagementFrame,
|
|
4245
|
+
payload: unknown,
|
|
4246
|
+
): void => {
|
|
4247
|
+
const trace = payloadRecord(payload);
|
|
4248
|
+
const turn = activeTurn;
|
|
4249
|
+
const summary = activeTurnSummary;
|
|
4250
|
+
const expectedParentSessionId = frame.parent?.frame.sessionId;
|
|
4251
|
+
const expectedParentCallId = frame.parent?.callId;
|
|
4252
|
+
if (
|
|
4253
|
+
trace?.schemaVersion !== 4 ||
|
|
4254
|
+
trace.sessionId !== frame.sessionId ||
|
|
4255
|
+
trace.playbookId !== frame.entry.id ||
|
|
4256
|
+
trace.rootSessionId !== frame.rootSessionId ||
|
|
4257
|
+
(expectedParentSessionId === undefined
|
|
4258
|
+
? Object.hasOwn(trace, 'parentSessionId')
|
|
4259
|
+
: !Object.hasOwn(trace, 'parentSessionId') ||
|
|
4260
|
+
trace.parentSessionId !== expectedParentSessionId) ||
|
|
4261
|
+
(expectedParentCallId === undefined
|
|
4262
|
+
? Object.hasOwn(trace, 'parentCallId')
|
|
4263
|
+
: !Object.hasOwn(trace, 'parentCallId') ||
|
|
4264
|
+
trace.parentCallId !== expectedParentCallId) ||
|
|
4265
|
+
trace.depth !== frame.depth ||
|
|
4266
|
+
turn === undefined ||
|
|
4267
|
+
!Number.isSafeInteger(trace.turnId) ||
|
|
4268
|
+
(trace.turnId as number) <= 0 ||
|
|
4269
|
+
!Number.isSafeInteger(trace.sequence) ||
|
|
4270
|
+
(trace.sequence as number) <= 0 ||
|
|
4271
|
+
summary === undefined ||
|
|
4272
|
+
!summaryIncludes(frame)
|
|
4273
|
+
) {
|
|
4274
|
+
return;
|
|
4275
|
+
}
|
|
4276
|
+
if (trace.type !== 'outcome.accepted') return;
|
|
4277
|
+
const receipt = exactOwnDataRecord(trace.payload, [
|
|
4278
|
+
'source',
|
|
4279
|
+
'target',
|
|
4280
|
+
'acceptedOutcome',
|
|
4281
|
+
]);
|
|
4282
|
+
if (
|
|
4283
|
+
receipt === undefined ||
|
|
4284
|
+
typeof receipt.source !== 'string' ||
|
|
4285
|
+
receipt.source.trim().length === 0 ||
|
|
4286
|
+
typeof receipt.target !== 'string' ||
|
|
4287
|
+
receipt.target.trim().length === 0 ||
|
|
4288
|
+
typeof receipt.acceptedOutcome !== 'string' ||
|
|
4289
|
+
receipt.acceptedOutcome.trim().length === 0
|
|
4290
|
+
) {
|
|
4291
|
+
return;
|
|
4292
|
+
}
|
|
4293
|
+
const traceKey = `${frame.sessionId}:${trace.sequence}`;
|
|
4294
|
+
if (summary.acceptedOutcomeTraceKeys.has(traceKey)) return;
|
|
4295
|
+
summary.acceptedOutcomeTraceKeys.add(traceKey);
|
|
4296
|
+
summary.counts.interruptions++;
|
|
4297
|
+
if (
|
|
4298
|
+
frame.entry.summaryPolicy?.copyPasteGuardNames.includes(
|
|
4299
|
+
receipt.acceptedOutcome,
|
|
4300
|
+
)
|
|
4301
|
+
) {
|
|
4302
|
+
summary.counts.copyPastes++;
|
|
4303
|
+
}
|
|
4304
|
+
};
|
|
4305
|
+
|
|
2594
4306
|
let callNestedPlaybook: (
|
|
2595
4307
|
frame: EngagementFrame,
|
|
2596
4308
|
request: PlaybookCallRequest,
|
|
@@ -2787,16 +4499,6 @@ export function createPlaybookCaptainShell(
|
|
|
2787
4499
|
if (result.finalText === undefined) {
|
|
2788
4500
|
throw new Error('callCaptain returned status=ok with no finalText');
|
|
2789
4501
|
}
|
|
2790
|
-
const guard = guardFromJudgeReply(result.finalText);
|
|
2791
|
-
const summary = activeTurnSummary;
|
|
2792
|
-
if (
|
|
2793
|
-
guard &&
|
|
2794
|
-
summary &&
|
|
2795
|
-
summaryIncludes(frame) &&
|
|
2796
|
-
frame.entry.summaryPolicy?.copyPasteGuardNames.includes(guard)
|
|
2797
|
-
) {
|
|
2798
|
-
summary.counts.copyPastes++;
|
|
2799
|
-
}
|
|
2800
4502
|
return result.finalText;
|
|
2801
4503
|
},
|
|
2802
4504
|
callPlaybook: (request, signal) => {
|
|
@@ -2832,6 +4534,9 @@ export function createPlaybookCaptainShell(
|
|
|
2832
4534
|
await mirrorSubRuntimeTelemetry(frame, event.payload);
|
|
2833
4535
|
}
|
|
2834
4536
|
await requireSession().emitTelemetry(event);
|
|
4537
|
+
if (event.topic === 'playbook.trace') {
|
|
4538
|
+
observeSummaryTrace(frame, event.payload);
|
|
4539
|
+
}
|
|
2835
4540
|
})();
|
|
2836
4541
|
return trackHostCall(frame, emission);
|
|
2837
4542
|
},
|
|
@@ -2855,7 +4560,7 @@ export function createPlaybookCaptainShell(
|
|
|
2855
4560
|
}
|
|
2856
4561
|
};
|
|
2857
4562
|
|
|
2858
|
-
const
|
|
4563
|
+
const generatedSessionId = (): string => {
|
|
2859
4564
|
const sessionId = createSessionId();
|
|
2860
4565
|
if (!UUID_PATTERN.test(sessionId)) {
|
|
2861
4566
|
throw new Error(
|
|
@@ -2864,6 +4569,11 @@ export function createPlaybookCaptainShell(
|
|
|
2864
4569
|
)}`,
|
|
2865
4570
|
);
|
|
2866
4571
|
}
|
|
4572
|
+
return sessionId;
|
|
4573
|
+
};
|
|
4574
|
+
|
|
4575
|
+
const allocateSessionId = (): string => {
|
|
4576
|
+
const sessionId = generatedSessionId();
|
|
2867
4577
|
if (issuedSessionIds.has(sessionId)) {
|
|
2868
4578
|
throw new Error(`playbook session id collision: ${sessionId}`);
|
|
2869
4579
|
}
|
|
@@ -2871,6 +4581,30 @@ export function createPlaybookCaptainShell(
|
|
|
2871
4581
|
return sessionId;
|
|
2872
4582
|
};
|
|
2873
4583
|
|
|
4584
|
+
const allocateAdoptionSessionIds = (
|
|
4585
|
+
count: number,
|
|
4586
|
+
sourceSessionIds: ReadonlySet<string>,
|
|
4587
|
+
): readonly string[] => {
|
|
4588
|
+
const candidates: string[] = [];
|
|
4589
|
+
const rejectedSourceIds = new Set<string>();
|
|
4590
|
+
while (candidates.length < count) {
|
|
4591
|
+
const candidate = generatedSessionId();
|
|
4592
|
+
if (sourceSessionIds.has(candidate)) {
|
|
4593
|
+
if (rejectedSourceIds.has(candidate)) {
|
|
4594
|
+
throw new Error(`playbook source session id collision: ${candidate}`);
|
|
4595
|
+
}
|
|
4596
|
+
rejectedSourceIds.add(candidate);
|
|
4597
|
+
continue;
|
|
4598
|
+
}
|
|
4599
|
+
if (issuedSessionIds.has(candidate) || candidates.includes(candidate)) {
|
|
4600
|
+
throw new Error(`playbook session id collision: ${candidate}`);
|
|
4601
|
+
}
|
|
4602
|
+
candidates.push(candidate);
|
|
4603
|
+
}
|
|
4604
|
+
for (const candidate of candidates) issuedSessionIds.add(candidate);
|
|
4605
|
+
return candidates;
|
|
4606
|
+
};
|
|
4607
|
+
|
|
2874
4608
|
const normalizeErrorFull = (value: unknown): NormalizedError => {
|
|
2875
4609
|
const compact = normalizeErrorCompact(value) ?? {
|
|
2876
4610
|
name: 'Error',
|
|
@@ -2898,7 +4632,10 @@ export function createPlaybookCaptainShell(
|
|
|
2898
4632
|
const entry = enablement.entry;
|
|
2899
4633
|
const sessionId = allocateSessionId();
|
|
2900
4634
|
const playerBindings = makePlayerBindings(enablement);
|
|
2901
|
-
const runtime =
|
|
4635
|
+
const runtime = createRuntimeForEnablement(
|
|
4636
|
+
enablement,
|
|
4637
|
+
hostCapabilitiesById,
|
|
4638
|
+
);
|
|
2902
4639
|
return {
|
|
2903
4640
|
entry,
|
|
2904
4641
|
enablement,
|
|
@@ -2919,7 +4656,10 @@ export function createPlaybookCaptainShell(
|
|
|
2919
4656
|
): EngagementFrame => {
|
|
2920
4657
|
const entry = enablement.entry;
|
|
2921
4658
|
const playerBindings = makePlayerBindings(enablement);
|
|
2922
|
-
const runtime =
|
|
4659
|
+
const runtime = createRuntimeForEnablement(
|
|
4660
|
+
enablement,
|
|
4661
|
+
hostCapabilitiesById,
|
|
4662
|
+
);
|
|
2923
4663
|
return {
|
|
2924
4664
|
entry,
|
|
2925
4665
|
enablement,
|
|
@@ -2976,16 +4716,6 @@ export function createPlaybookCaptainShell(
|
|
|
2976
4716
|
try {
|
|
2977
4717
|
if (resumeToken === undefined) delete ledger.resumeToken;
|
|
2978
4718
|
else ledger.resumeToken = resumeToken;
|
|
2979
|
-
// CAPTAIN-20: a result counts only after the runtime validated it and
|
|
2980
|
-
// atomically published its authorized continuation transition.
|
|
2981
|
-
const summary = activeTurnSummary;
|
|
2982
|
-
if (
|
|
2983
|
-
pending.status === 'ok' &&
|
|
2984
|
-
summary &&
|
|
2985
|
-
summaryIncludes(frame)
|
|
2986
|
-
) {
|
|
2987
|
-
summary.counts.interruptions++;
|
|
2988
|
-
}
|
|
2989
4719
|
} finally {
|
|
2990
4720
|
playerTransactions.delete(binding.playerId);
|
|
2991
4721
|
}
|
|
@@ -3265,6 +4995,7 @@ export function createPlaybookCaptainShell(
|
|
|
3265
4995
|
}
|
|
3266
4996
|
}
|
|
3267
4997
|
clearLeafLedger();
|
|
4998
|
+
if (frames.length === 0) retainedEffectReconciliation = undefined;
|
|
3268
4999
|
if (failures.length === 1) throw failures[0];
|
|
3269
5000
|
if (failures.length > 1) {
|
|
3270
5001
|
throw new AggregateError(
|
|
@@ -3374,10 +5105,62 @@ export function createPlaybookCaptainShell(
|
|
|
3374
5105
|
}
|
|
3375
5106
|
};
|
|
3376
5107
|
|
|
5108
|
+
/**
|
|
5109
|
+
* DR-040 task 11: abandonment is a host settlement, not an authored FSM
|
|
5110
|
+
* result. Freeze the bounded evidence while the complete stack and its
|
|
5111
|
+
* runtime-owned envelope identities still exist, durably fence recovery,
|
|
5112
|
+
* dispose leaf-to-root without resuming a parent, then publish the matching
|
|
5113
|
+
* root clear and evidence as one durable completion before the controller
|
|
5114
|
+
* may return an executed receipt to its result phase.
|
|
5115
|
+
*/
|
|
5116
|
+
const settleUnresolvedEffectAbandonment = async (
|
|
5117
|
+
unresolvedLeaf: EngagementFrame,
|
|
5118
|
+
): Promise<void> => {
|
|
5119
|
+
if (leafFrame() !== unresolvedLeaf) {
|
|
5120
|
+
throw new Error(
|
|
5121
|
+
'unresolved-effect abandonment requires the active leaf',
|
|
5122
|
+
);
|
|
5123
|
+
}
|
|
5124
|
+
const root = rootFrame();
|
|
5125
|
+
if (root === undefined) {
|
|
5126
|
+
throw new Error(
|
|
5127
|
+
'unresolved-effect abandonment requires an active root',
|
|
5128
|
+
);
|
|
5129
|
+
}
|
|
5130
|
+
const unresolvedEffects = freezeTurnUnresolvedEffects();
|
|
5131
|
+
if (unresolvedEffects.length === 0) {
|
|
5132
|
+
throw new Error(
|
|
5133
|
+
'unresolved-effect abandonment requires nonempty effect evidence',
|
|
5134
|
+
);
|
|
5135
|
+
}
|
|
5136
|
+
if (unresolvedEffectSettlement === undefined) {
|
|
5137
|
+
throw new Error(
|
|
5138
|
+
'unresolved-effect abandonment requires durable host settlement',
|
|
5139
|
+
);
|
|
5140
|
+
}
|
|
5141
|
+
const rootPlaybookId = root.entry.id;
|
|
5142
|
+
const settlement = Object.freeze({
|
|
5143
|
+
rootPlaybookId,
|
|
5144
|
+
unresolvedEffects,
|
|
5145
|
+
});
|
|
5146
|
+
await runEffect(() => unresolvedEffectSettlement.begin(settlement));
|
|
5147
|
+
await runEffect(() => disposeStack('unresolved-effect'));
|
|
5148
|
+
pendingRetentionUpdates.set(rootPlaybookId, {
|
|
5149
|
+
kind: 'clear',
|
|
5150
|
+
rootPlaybookId,
|
|
5151
|
+
});
|
|
5152
|
+
await runEffect(() => unresolvedEffectSettlement.complete(settlement));
|
|
5153
|
+
};
|
|
5154
|
+
|
|
3377
5155
|
const callResultFor = (
|
|
3378
5156
|
frame: EngagementFrame,
|
|
3379
5157
|
result: PlaybookRunResult,
|
|
3380
5158
|
): PlaybookCallResult => {
|
|
5159
|
+
if (result.outcome === 'unresolved-effect') {
|
|
5160
|
+
throw new Error(
|
|
5161
|
+
`playbook ${frame.entry.id} unresolved-effect result cannot resume a parent`,
|
|
5162
|
+
);
|
|
5163
|
+
}
|
|
3381
5164
|
if (result.outcome === 'terminal') {
|
|
3382
5165
|
return {
|
|
3383
5166
|
status: 'ok',
|
|
@@ -3425,6 +5208,11 @@ export function createPlaybookCaptainShell(
|
|
|
3425
5208
|
if (leafFrame() !== frame) {
|
|
3426
5209
|
throw new Error('only the active leaf may receive Boss input');
|
|
3427
5210
|
}
|
|
5211
|
+
if (retainedEffectReconciliation !== undefined) {
|
|
5212
|
+
throw new Error(
|
|
5213
|
+
'retained repository-effect reconciliation is required before Boss input',
|
|
5214
|
+
);
|
|
5215
|
+
}
|
|
3428
5216
|
// CAPTAIN-35: the leaf check, the visibility request, and the mode change
|
|
3429
5217
|
// are shell control work performed on the way to the runtime, not the
|
|
3430
5218
|
// effect. Only the call below is the effect, so only it is inside the
|
|
@@ -3451,6 +5239,11 @@ export function createPlaybookCaptainShell(
|
|
|
3451
5239
|
const parentLink = child.parent;
|
|
3452
5240
|
if (!parentLink) throw new Error('root playbook has no caller');
|
|
3453
5241
|
const parent = parentLink.frame;
|
|
5242
|
+
if (retainedEffectReconciliation !== undefined) {
|
|
5243
|
+
throw new Error(
|
|
5244
|
+
'retained repository-effect reconciliation is required before parent resumption',
|
|
5245
|
+
);
|
|
5246
|
+
}
|
|
3454
5247
|
const invocationSignal = child.invocationSignal;
|
|
3455
5248
|
let effectiveResult = callResult;
|
|
3456
5249
|
let ownsReturn = false;
|
|
@@ -3535,6 +5328,16 @@ export function createPlaybookCaptainShell(
|
|
|
3535
5328
|
result: PlaybookRunResult,
|
|
3536
5329
|
context: CaptainContext,
|
|
3537
5330
|
): Promise<void> {
|
|
5331
|
+
if (result.outcome === 'unresolved-effect') {
|
|
5332
|
+
// Task 10 exposes the runtime-owned abandonment signal without
|
|
5333
|
+
// translating it into a nested result or claiming workflow completion.
|
|
5334
|
+
// Task 11 owns the durable host settlement and complete-stack disposal.
|
|
5335
|
+
assertRetainableResult(frame, result);
|
|
5336
|
+
if (leafFrame() === frame) {
|
|
5337
|
+
await setMode('engaged.parked', 'turn:unresolved-effect');
|
|
5338
|
+
}
|
|
5339
|
+
return;
|
|
5340
|
+
}
|
|
3538
5341
|
if (result.outcome === 'terminal') {
|
|
3539
5342
|
if (frame.parent) {
|
|
3540
5343
|
await resumeParent(frame, callResultFor(frame, result), context);
|
|
@@ -3545,6 +5348,7 @@ export function createPlaybookCaptainShell(
|
|
|
3545
5348
|
// output remains runtime-to-runtime data and never becomes Captain
|
|
3546
5349
|
// evidence (CAPPLAY-10).
|
|
3547
5350
|
activeTurn?.settlementFacts.push(rootCompletionFact(frame, result));
|
|
5351
|
+
recordTerminalRetention(frame, result);
|
|
3548
5352
|
await runEffect(() => disposeStack('final'));
|
|
3549
5353
|
}
|
|
3550
5354
|
return;
|
|
@@ -3761,7 +5565,14 @@ export function createPlaybookCaptainShell(
|
|
|
3761
5565
|
const policy = frame.entry.summaryPolicy;
|
|
3762
5566
|
const counts: TurnSummaryCounts = { interruptions: 0, copyPastes: 0 };
|
|
3763
5567
|
const stateCounts = new Map<string, number>();
|
|
3764
|
-
activeTurnSummary = policy
|
|
5568
|
+
activeTurnSummary = policy
|
|
5569
|
+
? {
|
|
5570
|
+
owner: frame,
|
|
5571
|
+
counts,
|
|
5572
|
+
stateCounts,
|
|
5573
|
+
acceptedOutcomeTraceKeys: new Set(),
|
|
5574
|
+
}
|
|
5575
|
+
: undefined;
|
|
3765
5576
|
let result: T | undefined;
|
|
3766
5577
|
let error: unknown;
|
|
3767
5578
|
try {
|
|
@@ -3834,12 +5645,79 @@ export function createPlaybookCaptainShell(
|
|
|
3834
5645
|
];
|
|
3835
5646
|
};
|
|
3836
5647
|
|
|
5648
|
+
const retainedResumptionDigest = (): string => {
|
|
5649
|
+
if (rootFrame() !== undefined) {
|
|
5650
|
+
return 'Retained resumptions: unavailable while a playbook is engaged.';
|
|
5651
|
+
}
|
|
5652
|
+
const offers = [...retainedGenerationOffers].sort(([left], [right]) =>
|
|
5653
|
+
left.localeCompare(right),
|
|
5654
|
+
);
|
|
5655
|
+
if (offers.length === 0) return 'Retained resumptions: none.';
|
|
5656
|
+
const lines = ['Retained resumptions:'];
|
|
5657
|
+
for (const [rootPlaybookId, offer] of offers) {
|
|
5658
|
+
const enablement = enablementById.get(rootPlaybookId)!;
|
|
5659
|
+
lines.push(
|
|
5660
|
+
digestLine`- ${rootPlaybookId} (/${enablement.command}): ${
|
|
5661
|
+
offer.generation.rootStateDescription ??
|
|
5662
|
+
'(no published root-state description was retained)'
|
|
5663
|
+
}`,
|
|
5664
|
+
);
|
|
5665
|
+
}
|
|
5666
|
+
return lines.join('\n');
|
|
5667
|
+
};
|
|
5668
|
+
|
|
3837
5669
|
const controlViewDigest = (): string => {
|
|
3838
5670
|
const leaf = leafFrame();
|
|
3839
5671
|
const lines: string[] = [digestLine`Active path: ${activePathDigest()}`];
|
|
3840
5672
|
if (!leaf) {
|
|
3841
5673
|
lines.push('The shell is idle: no leaf state, no pending question.');
|
|
3842
5674
|
lines.push('Advertised actions: none.');
|
|
5675
|
+
lines.push(retainedResumptionDigest());
|
|
5676
|
+
return lines.join('\n');
|
|
5677
|
+
}
|
|
5678
|
+
refreshRetainedEffectFence();
|
|
5679
|
+
if (retainedEffectReconciliation !== undefined) {
|
|
5680
|
+
let reconciliationActions: readonly {
|
|
5681
|
+
readonly id: string;
|
|
5682
|
+
readonly label: string;
|
|
5683
|
+
}[] = [];
|
|
5684
|
+
if (
|
|
5685
|
+
typeof leaf.runtime.describe === 'function' &&
|
|
5686
|
+
typeof leaf.runtime.apply === 'function'
|
|
5687
|
+
) {
|
|
5688
|
+
try {
|
|
5689
|
+
reconciliationActions = leaf.runtime
|
|
5690
|
+
.describe()
|
|
5691
|
+
.actions.filter(
|
|
5692
|
+
({ id }) =>
|
|
5693
|
+
id === UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID ||
|
|
5694
|
+
id === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID,
|
|
5695
|
+
);
|
|
5696
|
+
} catch {
|
|
5697
|
+
// An unreadable control surface cannot open a fail-closed fence.
|
|
5698
|
+
}
|
|
5699
|
+
}
|
|
5700
|
+
for (const action of reconciliationActions) {
|
|
5701
|
+
recordSuppliedIdentifier(action.id);
|
|
5702
|
+
}
|
|
5703
|
+
lines.push(
|
|
5704
|
+
`Leaf ${frameLabel(leaf)} is parked for repository-effect reconciliation.`,
|
|
5705
|
+
);
|
|
5706
|
+
lines.push('Pending Boss questions: withheld until reconciliation.');
|
|
5707
|
+
lines.push(
|
|
5708
|
+
reconciliationActions.length === 0
|
|
5709
|
+
? 'Advertised actions: none.'
|
|
5710
|
+
: [
|
|
5711
|
+
'Advertised actions:',
|
|
5712
|
+
...reconciliationActions.map(
|
|
5713
|
+
(action) => digestLine`- ${action.id}: ${action.label}`,
|
|
5714
|
+
),
|
|
5715
|
+
].join('\n'),
|
|
5716
|
+
);
|
|
5717
|
+
lines.push(
|
|
5718
|
+
'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.',
|
|
5719
|
+
);
|
|
5720
|
+
lines.push(retainedResumptionDigest());
|
|
3843
5721
|
return lines.join('\n');
|
|
3844
5722
|
}
|
|
3845
5723
|
let view: PlaybookControlView | undefined;
|
|
@@ -3897,6 +5775,7 @@ export function createPlaybookCaptainShell(
|
|
|
3897
5775
|
? '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.'
|
|
3898
5776
|
: '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.',
|
|
3899
5777
|
);
|
|
5778
|
+
lines.push(retainedResumptionDigest());
|
|
3900
5779
|
return lines.join('\n');
|
|
3901
5780
|
}
|
|
3902
5781
|
// CAPTAIN-9: the guarded set is what the digest supplies *for selection* —
|
|
@@ -3949,6 +5828,7 @@ export function createPlaybookCaptainShell(
|
|
|
3949
5828
|
),
|
|
3950
5829
|
].join('\n'),
|
|
3951
5830
|
);
|
|
5831
|
+
lines.push(retainedResumptionDigest());
|
|
3952
5832
|
return lines.join('\n');
|
|
3953
5833
|
};
|
|
3954
5834
|
|
|
@@ -4043,9 +5923,14 @@ export function createPlaybookCaptainShell(
|
|
|
4043
5923
|
// all of this prose. Preserve the exact attempt before crossing the
|
|
4044
5924
|
// boundary; the uncertainty record below keeps recovery from pretending
|
|
4045
5925
|
// delivery was confirmed while still understanding a Boss follow-up.
|
|
4046
|
-
|
|
5926
|
+
const suffix = turn?.mandatoryPresentationSuffix;
|
|
5927
|
+
const visibleText =
|
|
5928
|
+
suffix === undefined || settlement.text.includes(suffix)
|
|
5929
|
+
? settlement.text
|
|
5930
|
+
: `${settlement.text.trimEnd()}\n\n${suffix}`;
|
|
5931
|
+
appendJournal('reply', visibleText);
|
|
4047
5932
|
try {
|
|
4048
|
-
await trackTurnCall(settlement.context.emitReply(
|
|
5933
|
+
await trackTurnCall(settlement.context.emitReply(visibleText));
|
|
4049
5934
|
} catch (error) {
|
|
4050
5935
|
conversation = { kind: 'needsSeeding' };
|
|
4051
5936
|
const normalized = normalizeErrorCompact(error) ?? {
|
|
@@ -4211,8 +6096,9 @@ export function createPlaybookCaptainShell(
|
|
|
4211
6096
|
};
|
|
4212
6097
|
|
|
4213
6098
|
/**
|
|
4214
|
-
* CAPTAIN-35: the one wrapper an effect runs through — a runtime driven
|
|
4215
|
-
* engagement constructed, a stack disposed, an advertised
|
|
6099
|
+
* CAPTAIN-35: the one wrapper an effect runs through — a runtime driven or
|
|
6100
|
+
* adopted, an engagement constructed, a stack disposed, an advertised
|
|
6101
|
+
* action applied.
|
|
4216
6102
|
* Attribution is recorded here, at the operation that threw, and nowhere
|
|
4217
6103
|
* else: an error acquires the mark by escaping this call, so no later
|
|
4218
6104
|
* failure can inherit it.
|
|
@@ -4225,7 +6111,7 @@ export function createPlaybookCaptainShell(
|
|
|
4225
6111
|
* instead of settling, so a misfiling costs the Boss their only settlement.
|
|
4226
6112
|
*
|
|
4227
6113
|
* Neither can a boundary drawn around a *region* of the turn. `operation` is
|
|
4228
|
-
* therefore always one call expression naming one of those
|
|
6114
|
+
* therefore always one call expression naming one of those five operations,
|
|
4229
6115
|
* never a closure that also performs the shell work leading to it: the leaf
|
|
4230
6116
|
* check, the visibility request, the mode change, and the processing of what
|
|
4231
6117
|
* the runtime returned are all shell control work, and a boundary wide
|
|
@@ -4493,7 +6379,10 @@ export function createPlaybookCaptainShell(
|
|
|
4493
6379
|
...(kind === 'closingReply' && turn?.report
|
|
4494
6380
|
? [
|
|
4495
6381
|
labeledBlock('ControlView digest', controlViewDigest()),
|
|
4496
|
-
outcomeReportBlock(
|
|
6382
|
+
outcomeReportBlock(
|
|
6383
|
+
turn.report,
|
|
6384
|
+
turn.unresolvedEffects ?? [],
|
|
6385
|
+
),
|
|
4497
6386
|
]
|
|
4498
6387
|
: []),
|
|
4499
6388
|
...(options.proseRejection === undefined
|
|
@@ -4670,6 +6559,9 @@ export function createPlaybookCaptainShell(
|
|
|
4670
6559
|
const leafStateSummary = (): string | undefined => {
|
|
4671
6560
|
const leaf = leafFrame();
|
|
4672
6561
|
if (!leaf) return 'idle: no playbook is engaged';
|
|
6562
|
+
if (retainedEffectReconciliation !== undefined) {
|
|
6563
|
+
return `${frameLabel(leaf)} parked for repository-effect reconciliation`;
|
|
6564
|
+
}
|
|
4673
6565
|
if (!leaf.state) return `${frameLabel(leaf)} engaged`;
|
|
4674
6566
|
return `${frameLabel(leaf)} at ${stateDigestLine(
|
|
4675
6567
|
leaf.state,
|
|
@@ -4681,9 +6573,9 @@ export function createPlaybookCaptainShell(
|
|
|
4681
6573
|
selection: CaptainControllerSelection | undefined,
|
|
4682
6574
|
reason: string,
|
|
4683
6575
|
options: { silent?: boolean } = {},
|
|
4684
|
-
): Promise<
|
|
6576
|
+
): Promise<ControllerSettlementDraft> => {
|
|
4685
6577
|
const summary = leafStateSummary();
|
|
4686
|
-
const settlement:
|
|
6578
|
+
const settlement: ControllerSettlementDraft = {
|
|
4687
6579
|
status: 'rejected',
|
|
4688
6580
|
facts: [`Rejected: ${reason}.`],
|
|
4689
6581
|
reason,
|
|
@@ -4729,6 +6621,9 @@ export function createPlaybookCaptainShell(
|
|
|
4729
6621
|
const root = rootFrame();
|
|
4730
6622
|
if (!root) return false;
|
|
4731
6623
|
const label = frameLabel(root);
|
|
6624
|
+
// Dismissal leaves the procedure unfinished. Persist the latest safe
|
|
6625
|
+
// generation captured for this turn before disposal erases the frames.
|
|
6626
|
+
retainOrClearDisposedRoot(root);
|
|
4732
6627
|
try {
|
|
4733
6628
|
await runEffect(() => disposeStack('dismiss'));
|
|
4734
6629
|
facts.push(`Dismissed the ${label} engagement.`);
|
|
@@ -4785,6 +6680,163 @@ export function createPlaybookCaptainShell(
|
|
|
4785
6680
|
return { frame, report: outcome.report, failed: false };
|
|
4786
6681
|
};
|
|
4787
6682
|
|
|
6683
|
+
const adoptRetainedGeneration = async (
|
|
6684
|
+
rootPlaybookId: string,
|
|
6685
|
+
offer: RetainedGenerationOffer,
|
|
6686
|
+
): Promise<readonly string[]> => {
|
|
6687
|
+
const generation = offer.generation;
|
|
6688
|
+
const currentLedger = assertPlaybookEffectLedger(currentEffectLedger());
|
|
6689
|
+
const requiresEffectReconciliation =
|
|
6690
|
+
offer.requiresEffectReconciliation ||
|
|
6691
|
+
!retainedEffectLedgerCanRebase(
|
|
6692
|
+
generation.effectLedger as PlaybookEffectLedger,
|
|
6693
|
+
currentLedger,
|
|
6694
|
+
);
|
|
6695
|
+
const sourceSessionIds = new Set(
|
|
6696
|
+
generation.frames.map((frame) => frame.sessionId),
|
|
6697
|
+
);
|
|
6698
|
+
const targetSessionIds = allocateAdoptionSessionIds(
|
|
6699
|
+
generation.frames.length,
|
|
6700
|
+
sourceSessionIds,
|
|
6701
|
+
);
|
|
6702
|
+
const targetRootSessionId = targetSessionIds[0]!;
|
|
6703
|
+
const adoptedFrames: EngagementFrame[] = [];
|
|
6704
|
+
for (const [index, sourceFrame] of generation.frames.entries()) {
|
|
6705
|
+
const enablement = enablementById.get(sourceFrame.playbookId)!;
|
|
6706
|
+
const parent = adoptedFrames.at(-1);
|
|
6707
|
+
adoptedFrames.push({
|
|
6708
|
+
entry: enablement.entry,
|
|
6709
|
+
enablement,
|
|
6710
|
+
runtime: offer.runtimes[index]!,
|
|
6711
|
+
sessionId: targetSessionIds[index]!,
|
|
6712
|
+
rootSessionId: targetRootSessionId,
|
|
6713
|
+
depth: index,
|
|
6714
|
+
playerBindings: makePlayerBindings(enablement),
|
|
6715
|
+
...(parent
|
|
6716
|
+
? { parent: { frame: parent, callId: 'playbook-1' } }
|
|
6717
|
+
: {}),
|
|
6718
|
+
state: sourceFrame.runtime.state,
|
|
6719
|
+
inFlightHostCalls: new Set(),
|
|
6720
|
+
});
|
|
6721
|
+
}
|
|
6722
|
+
|
|
6723
|
+
retainedGenerationOffers.delete(rootPlaybookId);
|
|
6724
|
+
let installed = false;
|
|
6725
|
+
try {
|
|
6726
|
+
for (const [index, frame] of adoptedFrames.entries()) {
|
|
6727
|
+
const sourceFrame = generation.frames[index]!;
|
|
6728
|
+
const targetChild = adoptedFrames[index + 1];
|
|
6729
|
+
await runEffect(() =>
|
|
6730
|
+
frame.runtime.adopt!(
|
|
6731
|
+
frameSession(frame),
|
|
6732
|
+
sourceFrame.runtime,
|
|
6733
|
+
{
|
|
6734
|
+
sourceSessionId: sourceFrame.sessionId,
|
|
6735
|
+
sourceGenerationId: generation.frames[0]!.rootSessionId,
|
|
6736
|
+
...(targetChild === undefined
|
|
6737
|
+
? {}
|
|
6738
|
+
: { targetChildSessionId: targetChild.sessionId }),
|
|
6739
|
+
},
|
|
6740
|
+
),
|
|
6741
|
+
);
|
|
6742
|
+
}
|
|
6743
|
+
|
|
6744
|
+
frames.push(...adoptedFrames);
|
|
6745
|
+
installed = true;
|
|
6746
|
+
retainedEffectReconciliation = requiresEffectReconciliation
|
|
6747
|
+
? {
|
|
6748
|
+
sourceGenerationId:
|
|
6749
|
+
generation.retainedEffectReconciliation?.sourceGenerationId ??
|
|
6750
|
+
generation.frames[0]!.runtime.retainedEffectSourceSessionId ??
|
|
6751
|
+
generation.frames[0]!.rootSessionId,
|
|
6752
|
+
checkpoint: generation.effectLedger,
|
|
6753
|
+
}
|
|
6754
|
+
: undefined;
|
|
6755
|
+
for (const parent of adoptedFrames.slice(0, -1)) {
|
|
6756
|
+
pendingChildParents.add(parent);
|
|
6757
|
+
}
|
|
6758
|
+
const retainedQuestions = generation.frames.at(-1)!.runtime
|
|
6759
|
+
.pendingBossQuestions;
|
|
6760
|
+
pendingBossQuestions =
|
|
6761
|
+
requiresEffectReconciliation || retainedQuestions.length === 0
|
|
6762
|
+
? undefined
|
|
6763
|
+
: mirroredBossQuestions(retainedQuestions);
|
|
6764
|
+
lastError = undefined;
|
|
6765
|
+
retainedGenerations.delete(rootPlaybookId);
|
|
6766
|
+
ineligibleRetainedGenerations.delete(rootPlaybookId);
|
|
6767
|
+
await setMode(
|
|
6768
|
+
'engaged.parked',
|
|
6769
|
+
'resume',
|
|
6770
|
+
rootPlaybookId,
|
|
6771
|
+
targetRootSessionId,
|
|
6772
|
+
);
|
|
6773
|
+
if (requiresEffectReconciliation) {
|
|
6774
|
+
await requireSession().setVisiblePlayers([]);
|
|
6775
|
+
} else {
|
|
6776
|
+
await requestVisibility(adoptedFrames.at(-1)!);
|
|
6777
|
+
}
|
|
6778
|
+
await requireSession().emitStatus(
|
|
6779
|
+
`◇ /${enablementById.get(rootPlaybookId)!.command} resumed`,
|
|
6780
|
+
);
|
|
6781
|
+
if (activeTurn) {
|
|
6782
|
+
appendMandatoryPresentationSuffix(
|
|
6783
|
+
activeTurn,
|
|
6784
|
+
requiresEffectReconciliation
|
|
6785
|
+
? 'The retained work remains parked until its repository-effect evidence is reconciled.'
|
|
6786
|
+
: RESUMPTION_DUPLICATE_EFFECT_WARNING,
|
|
6787
|
+
);
|
|
6788
|
+
}
|
|
6789
|
+
return [
|
|
6790
|
+
generation.rootStateDescription === undefined
|
|
6791
|
+
? `Resumed /${enablementById.get(rootPlaybookId)!.command} from its retained state; no published root-state description was retained.`
|
|
6792
|
+
: `Resumed /${enablementById.get(rootPlaybookId)!.command} from the retained state described as ${quoteEvidence(compactEvidence(generation.rootStateDescription))}.`,
|
|
6793
|
+
requiresEffectReconciliation
|
|
6794
|
+
? 'The retained work remains parked until its repository-effect evidence is reconciled; no ordinary action was resumed.'
|
|
6795
|
+
: RESUMPTION_DUPLICATE_EFFECT_WARNING,
|
|
6796
|
+
];
|
|
6797
|
+
} catch (error) {
|
|
6798
|
+
if (installed) {
|
|
6799
|
+
frames.splice(0);
|
|
6800
|
+
pendingChildParents.clear();
|
|
6801
|
+
retainedEffectReconciliation = undefined;
|
|
6802
|
+
clearLeafLedger();
|
|
6803
|
+
}
|
|
6804
|
+
const cleanupFailures: unknown[] = [];
|
|
6805
|
+
const failedCleanupRuntimes: PlaybookRuntime[] = [];
|
|
6806
|
+
for (const frame of [...adoptedFrames].reverse()) {
|
|
6807
|
+
try {
|
|
6808
|
+
await disposeFrame(frame);
|
|
6809
|
+
} catch (cleanupError) {
|
|
6810
|
+
cleanupFailures.push(cleanupError);
|
|
6811
|
+
failedCleanupRuntimes.push(frame.runtime);
|
|
6812
|
+
}
|
|
6813
|
+
}
|
|
6814
|
+
const rollbackFailures: unknown[] = [];
|
|
6815
|
+
if (installed) {
|
|
6816
|
+
try {
|
|
6817
|
+
await setMode('chat', 'resume.failed');
|
|
6818
|
+
} catch (rollbackError) {
|
|
6819
|
+
rollbackFailures.push(rollbackError);
|
|
6820
|
+
}
|
|
6821
|
+
} else {
|
|
6822
|
+
mode = 'chat';
|
|
6823
|
+
}
|
|
6824
|
+
if (cleanupFailures.length > 0 || rollbackFailures.length > 0) {
|
|
6825
|
+
retiredRetainedRuntimes.push(...failedCleanupRuntimes);
|
|
6826
|
+
ineligibleRetainedGenerations.add(rootPlaybookId);
|
|
6827
|
+
terminallyDisposed = true;
|
|
6828
|
+
lifecycle = 'closed';
|
|
6829
|
+
throw new AggregateError(
|
|
6830
|
+
[error, ...cleanupFailures, ...rollbackFailures],
|
|
6831
|
+
'retained-generation adoption and rollback failed',
|
|
6832
|
+
);
|
|
6833
|
+
}
|
|
6834
|
+
retainedGenerations.set(rootPlaybookId, generation);
|
|
6835
|
+
ineligibleRetainedGenerations.delete(rootPlaybookId);
|
|
6836
|
+
throw error;
|
|
6837
|
+
}
|
|
6838
|
+
};
|
|
6839
|
+
|
|
4788
6840
|
const driveAndProcess = async (
|
|
4789
6841
|
frame: EngagementFrame,
|
|
4790
6842
|
text: string,
|
|
@@ -4815,14 +6867,46 @@ export function createPlaybookCaptainShell(
|
|
|
4815
6867
|
}
|
|
4816
6868
|
};
|
|
4817
6869
|
|
|
6870
|
+
const containsEffectThrow = (turn: ActiveTurn, error: unknown): boolean => {
|
|
6871
|
+
if (turn.effectThrows.has(error)) return true;
|
|
6872
|
+
return (
|
|
6873
|
+
error instanceof AggregateError &&
|
|
6874
|
+
error.errors.some((nested) => containsEffectThrow(turn, nested))
|
|
6875
|
+
);
|
|
6876
|
+
};
|
|
6877
|
+
|
|
4818
6878
|
const settleSelection = async (
|
|
4819
6879
|
selection: CaptainControllerSelection,
|
|
4820
6880
|
signal: AbortSignal,
|
|
4821
6881
|
): Promise<SettlementEvidence> => {
|
|
4822
6882
|
const turn = activeTurn;
|
|
4823
6883
|
runFailureFacts = [];
|
|
6884
|
+
let frozenUnresolvedEffects:
|
|
6885
|
+
| readonly PlaybookCaptainUnresolvedEffect[]
|
|
6886
|
+
| undefined;
|
|
6887
|
+
const freezeControllerEvidence =
|
|
6888
|
+
(): readonly PlaybookCaptainUnresolvedEffect[] => {
|
|
6889
|
+
frozenUnresolvedEffects ??= freezeTurnUnresolvedEffects();
|
|
6890
|
+
const report = unresolvedEffectBossReport(frozenUnresolvedEffects);
|
|
6891
|
+
if (turn !== undefined && report !== undefined) {
|
|
6892
|
+
appendMandatoryPresentationSuffix(turn, report);
|
|
6893
|
+
}
|
|
6894
|
+
return frozenUnresolvedEffects;
|
|
6895
|
+
};
|
|
6896
|
+
const finalizeSettlement = (
|
|
6897
|
+
settlement: ControllerSettlementDraft,
|
|
6898
|
+
): SettlementEvidence =>
|
|
6899
|
+
Object.freeze({
|
|
6900
|
+
...settlement,
|
|
6901
|
+
unresolvedEffects: freezeControllerEvidence(),
|
|
6902
|
+
});
|
|
4824
6903
|
try {
|
|
4825
|
-
|
|
6904
|
+
// `respond` has no result phase: freeze its no-effect projection before
|
|
6905
|
+
// its decision-call prose crosses the presentation boundary. Acting
|
|
6906
|
+
// selections freeze after their work and before reporting begins.
|
|
6907
|
+
if (selection.action === 'respond') freezeControllerEvidence();
|
|
6908
|
+
const settlement = await executeSelection(selection, signal);
|
|
6909
|
+
return finalizeSettlement(settlement);
|
|
4826
6910
|
} catch (error) {
|
|
4827
6911
|
if (turn?.presentationError === error) throw error;
|
|
4828
6912
|
const aborted = signal.aborted || activeContext?.signal.aborted === true;
|
|
@@ -4853,7 +6937,7 @@ export function createPlaybookCaptainShell(
|
|
|
4853
6937
|
}
|
|
4854
6938
|
const mayHaveApplied =
|
|
4855
6939
|
selection.action !== 'respond' &&
|
|
4856
|
-
turn
|
|
6940
|
+
containsEffectThrow(turn, error);
|
|
4857
6941
|
turn.settlementFacts.push(
|
|
4858
6942
|
mayHaveApplied
|
|
4859
6943
|
? `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.`
|
|
@@ -4896,7 +6980,7 @@ export function createPlaybookCaptainShell(
|
|
|
4896
6980
|
};
|
|
4897
6981
|
turn.settled = true;
|
|
4898
6982
|
lastSettlementStatus = 'failed';
|
|
4899
|
-
|
|
6983
|
+
const settlement: ControllerSettlementDraft = {
|
|
4900
6984
|
status: 'failed',
|
|
4901
6985
|
facts: [...turn.settlementFacts],
|
|
4902
6986
|
...(turn.report.receipt === undefined
|
|
@@ -4904,6 +6988,7 @@ export function createPlaybookCaptainShell(
|
|
|
4904
6988
|
: { receipt: turn.report.receipt }),
|
|
4905
6989
|
...(summary === undefined ? {} : { leafStateSummary: summary }),
|
|
4906
6990
|
};
|
|
6991
|
+
return finalizeSettlement(settlement);
|
|
4907
6992
|
} finally {
|
|
4908
6993
|
runFailureFacts = undefined;
|
|
4909
6994
|
}
|
|
@@ -4920,7 +7005,7 @@ export function createPlaybookCaptainShell(
|
|
|
4920
7005
|
const executeSelection = async (
|
|
4921
7006
|
selection: CaptainControllerSelection,
|
|
4922
7007
|
signal: AbortSignal,
|
|
4923
|
-
): Promise<
|
|
7008
|
+
): Promise<ControllerSettlementDraft> => {
|
|
4924
7009
|
const context = activeContext;
|
|
4925
7010
|
const turn = activeTurn;
|
|
4926
7011
|
if (!context || !turn) {
|
|
@@ -4973,6 +7058,63 @@ export function createPlaybookCaptainShell(
|
|
|
4973
7058
|
};
|
|
4974
7059
|
}
|
|
4975
7060
|
|
|
7061
|
+
refreshRetainedEffectFence();
|
|
7062
|
+
const fencedLeaf = leafFrame();
|
|
7063
|
+
const routesRetainedReconciliation =
|
|
7064
|
+
selection.action === 'runtime' &&
|
|
7065
|
+
(selection.actionId === UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID ||
|
|
7066
|
+
selection.actionId === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID) &&
|
|
7067
|
+
fencedLeaf !== undefined;
|
|
7068
|
+
if (
|
|
7069
|
+
retainedEffectReconciliation !== undefined &&
|
|
7070
|
+
!routesRetainedReconciliation
|
|
7071
|
+
) {
|
|
7072
|
+
return rejectSelection(
|
|
7073
|
+
selection,
|
|
7074
|
+
'retained work must reconcile its repository-effect evidence before an ordinary action can run',
|
|
7075
|
+
);
|
|
7076
|
+
}
|
|
7077
|
+
|
|
7078
|
+
if (selection.action === 'resume') {
|
|
7079
|
+
const entry = byId.get(selection.playbookId);
|
|
7080
|
+
if (entry === undefined) {
|
|
7081
|
+
return rejectSelection(
|
|
7082
|
+
selection,
|
|
7083
|
+
`"${selection.playbookId}" is not an enabled playbook`,
|
|
7084
|
+
);
|
|
7085
|
+
}
|
|
7086
|
+
if (rootFrame() !== undefined) {
|
|
7087
|
+
return rejectSelection(
|
|
7088
|
+
selection,
|
|
7089
|
+
'a playbook is already engaged; its live actions take precedence',
|
|
7090
|
+
);
|
|
7091
|
+
}
|
|
7092
|
+
const offer = retainedGenerationOffers.get(entry.id);
|
|
7093
|
+
if (offer === undefined) {
|
|
7094
|
+
return rejectSelection(
|
|
7095
|
+
selection,
|
|
7096
|
+
`/${enablementById.get(entry.id)!.command} has no resumable retained generation`,
|
|
7097
|
+
);
|
|
7098
|
+
}
|
|
7099
|
+
turn.settled = true;
|
|
7100
|
+
journalAction({ action: 'resume', playbookId: entry.id });
|
|
7101
|
+
facts.push(...(await adoptRetainedGeneration(entry.id, offer)));
|
|
7102
|
+
const summary = leafStateSummary();
|
|
7103
|
+
turn.report = {
|
|
7104
|
+
...emptyReport(),
|
|
7105
|
+
facts,
|
|
7106
|
+
status: 'ok',
|
|
7107
|
+
...(summary === undefined ? {} : { leafStateSummary: summary }),
|
|
7108
|
+
};
|
|
7109
|
+
journalOutcome([...facts]);
|
|
7110
|
+
lastSettlementStatus = 'ok';
|
|
7111
|
+
return {
|
|
7112
|
+
status: 'ok',
|
|
7113
|
+
facts: [...facts],
|
|
7114
|
+
...(summary === undefined ? {} : { leafStateSummary: summary }),
|
|
7115
|
+
};
|
|
7116
|
+
}
|
|
7117
|
+
|
|
4976
7118
|
if (selection.action === 'start' || selection.action === 'switch') {
|
|
4977
7119
|
const entry = byId.get(selection.playbookId);
|
|
4978
7120
|
if (!entry) {
|
|
@@ -5239,6 +7381,7 @@ export function createPlaybookCaptainShell(
|
|
|
5239
7381
|
);
|
|
5240
7382
|
if (outcome.error !== undefined) throw outcome.error;
|
|
5241
7383
|
const receipt = outcome.result!;
|
|
7384
|
+
refreshRetainedEffectFence();
|
|
5242
7385
|
let status: SettlementEvidence['status'] =
|
|
5243
7386
|
receipt.disposition === 'executed'
|
|
5244
7387
|
? 'ok'
|
|
@@ -5259,9 +7402,53 @@ export function createPlaybookCaptainShell(
|
|
|
5259
7402
|
}
|
|
5260
7403
|
: {}),
|
|
5261
7404
|
};
|
|
7405
|
+
const unresolvedAbandonment =
|
|
7406
|
+
receipt.disposition === 'executed' &&
|
|
7407
|
+
actionId === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID;
|
|
5262
7408
|
if (receipt.disposition === 'executed') {
|
|
5263
|
-
|
|
7409
|
+
if (
|
|
7410
|
+
unresolvedAbandonment &&
|
|
7411
|
+
receipt.run?.outcome !== 'unresolved-effect'
|
|
7412
|
+
) {
|
|
7413
|
+
throw new Error(
|
|
7414
|
+
'unresolved-effect abandonment returned no unresolved-effect result',
|
|
7415
|
+
);
|
|
7416
|
+
}
|
|
5264
7417
|
const establishedSummary = leafStateSummary();
|
|
7418
|
+
if (unresolvedAbandonment) {
|
|
7419
|
+
try {
|
|
7420
|
+
await settleUnresolvedEffectAbandonment(leaf);
|
|
7421
|
+
} catch (error) {
|
|
7422
|
+
abandonmentSettlementUnsafe = true;
|
|
7423
|
+
const normalized = normalizeErrorCompact(error) ?? {
|
|
7424
|
+
name: 'Error',
|
|
7425
|
+
message: String(error),
|
|
7426
|
+
};
|
|
7427
|
+
// The runtime action was accepted, but its distinct host-level
|
|
7428
|
+
// settlement did not complete. Do not expose an `executed` control
|
|
7429
|
+
// receipt until both disposal and durable publication have
|
|
7430
|
+
// succeeded.
|
|
7431
|
+
turn.report = {
|
|
7432
|
+
...outcome.report,
|
|
7433
|
+
facts: [...facts],
|
|
7434
|
+
bossFacts: facts.map((fact) =>
|
|
7435
|
+
fact
|
|
7436
|
+
.split(`"${actionId}"`)
|
|
7437
|
+
.join(`"${compactEvidence(actionLabel)}"`),
|
|
7438
|
+
),
|
|
7439
|
+
status: 'failed',
|
|
7440
|
+
receipt: {
|
|
7441
|
+
disposition: 'failed',
|
|
7442
|
+
error: normalized,
|
|
7443
|
+
},
|
|
7444
|
+
...(establishedSummary === undefined
|
|
7445
|
+
? {}
|
|
7446
|
+
: { leafStateSummary: establishedSummary }),
|
|
7447
|
+
};
|
|
7448
|
+
throw error;
|
|
7449
|
+
}
|
|
7450
|
+
}
|
|
7451
|
+
facts.push(`Applied "${actionId}" on ${frameLabel(leaf)}.`);
|
|
5265
7452
|
// Execution is now proven. Preserve that receipt and the counts already
|
|
5266
7453
|
// collected before processing the returned run, because disposal,
|
|
5267
7454
|
// telemetry, or parent resumption can still fail afterward.
|
|
@@ -5279,7 +7466,11 @@ export function createPlaybookCaptainShell(
|
|
|
5279
7466
|
? {}
|
|
5280
7467
|
: { leafStateSummary: establishedSummary }),
|
|
5281
7468
|
};
|
|
5282
|
-
if (
|
|
7469
|
+
if (
|
|
7470
|
+
!unresolvedAbandonment &&
|
|
7471
|
+
receipt.run !== undefined &&
|
|
7472
|
+
retainedEffectReconciliation === undefined
|
|
7473
|
+
) {
|
|
5283
7474
|
// The same rule as the drive path: processing the run the receipt
|
|
5284
7475
|
// carried is not itself an effect, and the resume or disposal it may
|
|
5285
7476
|
// perform is marked where it happens (CAPTAIN-35).
|
|
@@ -5520,6 +7711,42 @@ export function createPlaybookCaptainShell(
|
|
|
5520
7711
|
`Captain shell snapshot frame ${JSON.stringify(frame.playbookId)} role bindings changed`,
|
|
5521
7712
|
);
|
|
5522
7713
|
}
|
|
7714
|
+
if (
|
|
7715
|
+
!isDeepStrictEqual(frame.runtime.effectLedger, snapshot.effectLedger)
|
|
7716
|
+
) {
|
|
7717
|
+
throw new TypeError(
|
|
7718
|
+
`Captain shell snapshot frame ${JSON.stringify(frame.playbookId)} effect ledger does not match its artifact schema`,
|
|
7719
|
+
);
|
|
7720
|
+
}
|
|
7721
|
+
const runtimeReconciliation =
|
|
7722
|
+
frame.runtime.retainedEffectReconciliation;
|
|
7723
|
+
if (snapshot.retainedEffectReconciliation === undefined) {
|
|
7724
|
+
if (runtimeReconciliation !== undefined) {
|
|
7725
|
+
throw new TypeError(
|
|
7726
|
+
`Captain shell snapshot frame ${JSON.stringify(frame.playbookId)} carries an unmirrored retained-effect fence`,
|
|
7727
|
+
);
|
|
7728
|
+
}
|
|
7729
|
+
} else if (
|
|
7730
|
+
runtimeReconciliation === undefined ||
|
|
7731
|
+
!isDeepStrictEqual(
|
|
7732
|
+
runtimeReconciliation.checkpoint,
|
|
7733
|
+
snapshot.retainedEffectReconciliation.checkpoint,
|
|
7734
|
+
)
|
|
7735
|
+
) {
|
|
7736
|
+
throw new TypeError(
|
|
7737
|
+
`Captain shell snapshot frame ${JSON.stringify(frame.playbookId)} does not mirror the root retained-effect fence`,
|
|
7738
|
+
);
|
|
7739
|
+
}
|
|
7740
|
+
if (
|
|
7741
|
+
frame.depth === 0 &&
|
|
7742
|
+
snapshot.retainedEffectReconciliation !== undefined &&
|
|
7743
|
+
runtimeReconciliation?.sourceSessionId !==
|
|
7744
|
+
snapshot.retainedEffectReconciliation.sourceGenerationId
|
|
7745
|
+
) {
|
|
7746
|
+
throw new TypeError(
|
|
7747
|
+
'Captain shell snapshot retained-effect root source identity differs from its generation',
|
|
7748
|
+
);
|
|
7749
|
+
}
|
|
5523
7750
|
}
|
|
5524
7751
|
};
|
|
5525
7752
|
|
|
@@ -5585,40 +7812,44 @@ export function createPlaybookCaptainShell(
|
|
|
5585
7812
|
});
|
|
5586
7813
|
};
|
|
5587
7814
|
|
|
5588
|
-
const
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
!captainSessionId ||
|
|
5593
|
-
!captainAgent
|
|
5594
|
-
) {
|
|
5595
|
-
return undefined;
|
|
5596
|
-
}
|
|
7815
|
+
const captureFrameSnapshots = (
|
|
7816
|
+
requireRecordedState: boolean,
|
|
7817
|
+
): readonly PlaybookCaptainFrameSnapshot[] | undefined => {
|
|
7818
|
+
const captured: PlaybookCaptainFrameSnapshot[] = [];
|
|
5597
7819
|
try {
|
|
5598
|
-
|
|
5599
|
-
typeof captainRuntime.exportSnapshot !== 'function' ||
|
|
5600
|
-
typeof captainRuntime.restore !== 'function'
|
|
5601
|
-
) {
|
|
5602
|
-
return undefined;
|
|
5603
|
-
}
|
|
5604
|
-
const captainSnapshot = captainRuntime.exportSnapshot();
|
|
5605
|
-
if (captainSnapshot === undefined) return undefined;
|
|
5606
|
-
const frameSnapshots: PlaybookCaptainFrameSnapshot[] = [];
|
|
5607
|
-
for (const frame of frames) {
|
|
7820
|
+
for (const [index, frame] of frames.entries()) {
|
|
5608
7821
|
if (
|
|
5609
7822
|
typeof frame.runtime.exportSnapshot !== 'function' ||
|
|
5610
7823
|
typeof frame.runtime.restore !== 'function'
|
|
5611
7824
|
) {
|
|
5612
7825
|
return undefined;
|
|
5613
7826
|
}
|
|
5614
|
-
const
|
|
7827
|
+
const exported = frame.runtime.exportSnapshot();
|
|
7828
|
+
if (exported === undefined) return undefined;
|
|
7829
|
+
const runtime = assertPlaybookRuntimeSnapshot(
|
|
7830
|
+
exported,
|
|
7831
|
+
frame.entry.id,
|
|
7832
|
+
{ allowSuspendedCall: true },
|
|
7833
|
+
);
|
|
7834
|
+
if (
|
|
7835
|
+
runtime.state.status !== 'active' ||
|
|
7836
|
+
!runtime.state.quiescent ||
|
|
7837
|
+
(requireRecordedState && frame.state === undefined) ||
|
|
7838
|
+
(frame.state !== undefined &&
|
|
7839
|
+
!isDeepStrictEqual(frame.state, runtime.state))
|
|
7840
|
+
) {
|
|
7841
|
+
return undefined;
|
|
7842
|
+
}
|
|
7843
|
+
const isLeaf = index === frames.length - 1;
|
|
5615
7844
|
if (
|
|
5616
|
-
|
|
5617
|
-
|
|
7845
|
+
(isLeaf &&
|
|
7846
|
+
(runtime.suspendedCall !== undefined ||
|
|
7847
|
+
!runtime.state.tags.includes('playbook.parked'))) ||
|
|
7848
|
+
(!isLeaf && runtime.suspendedCall === undefined)
|
|
5618
7849
|
) {
|
|
5619
7850
|
return undefined;
|
|
5620
7851
|
}
|
|
5621
|
-
|
|
7852
|
+
captured.push({
|
|
5622
7853
|
playbookId: frame.entry.id,
|
|
5623
7854
|
sessionId: frame.sessionId,
|
|
5624
7855
|
rootSessionId: frame.rootSessionId,
|
|
@@ -5639,8 +7870,253 @@ export function createPlaybookCaptainShell(
|
|
|
5639
7870
|
runtime,
|
|
5640
7871
|
});
|
|
5641
7872
|
}
|
|
7873
|
+
for (let index = 1; index < captured.length; index += 1) {
|
|
7874
|
+
const parent = captured[index - 1]!;
|
|
7875
|
+
const child = captured[index]!;
|
|
7876
|
+
if (
|
|
7877
|
+
child.parentSessionId !== parent.sessionId ||
|
|
7878
|
+
child.parentCallId === undefined ||
|
|
7879
|
+
parent.runtime.suspendedCall?.callId !== child.parentCallId ||
|
|
7880
|
+
parent.runtime.suspendedCall.playbookId !== child.playbookId ||
|
|
7881
|
+
parent.runtime.suspendedCall.childSessionId !== child.sessionId
|
|
7882
|
+
) {
|
|
7883
|
+
return undefined;
|
|
7884
|
+
}
|
|
7885
|
+
}
|
|
7886
|
+
return captured;
|
|
7887
|
+
} catch {
|
|
7888
|
+
return undefined;
|
|
7889
|
+
}
|
|
7890
|
+
};
|
|
7891
|
+
|
|
7892
|
+
const refreshRetainedEffectFence = (
|
|
7893
|
+
capturedFrames?: readonly PlaybookCaptainFrameSnapshot[],
|
|
7894
|
+
capturedLedger?: PlaybookEffectLedger,
|
|
7895
|
+
): void => {
|
|
7896
|
+
const fence = retainedEffectReconciliation;
|
|
7897
|
+
if (fence === undefined) return;
|
|
7898
|
+
try {
|
|
7899
|
+
const ledger =
|
|
7900
|
+
capturedLedger ?? assertPlaybookEffectLedger(currentEffectLedger());
|
|
7901
|
+
if (
|
|
7902
|
+
!retainedEffectLedgerCanRebase(
|
|
7903
|
+
fence.checkpoint as PlaybookEffectLedger,
|
|
7904
|
+
ledger,
|
|
7905
|
+
)
|
|
7906
|
+
) {
|
|
7907
|
+
return;
|
|
7908
|
+
}
|
|
7909
|
+
const snapshots = capturedFrames ?? captureFrameSnapshots(true);
|
|
7910
|
+
if (snapshots === undefined || snapshots.length !== frames.length) return;
|
|
7911
|
+
for (const frameSnapshot of snapshots) {
|
|
7912
|
+
const runtime = frameSnapshot.runtime;
|
|
7913
|
+
if (
|
|
7914
|
+
!isDeepStrictEqual(runtime.effectLedger, ledger) ||
|
|
7915
|
+
runtime.retainedEffectSourceSessionId === undefined ||
|
|
7916
|
+
runtime.retainedEffectReconciliation !== undefined
|
|
7917
|
+
) {
|
|
7918
|
+
return;
|
|
7919
|
+
}
|
|
7920
|
+
}
|
|
7921
|
+
const retainedQuestions = snapshots.at(-1)!.runtime.pendingBossQuestions;
|
|
7922
|
+
const restoredQuestions =
|
|
7923
|
+
retainedQuestions.length === 0
|
|
7924
|
+
? undefined
|
|
7925
|
+
: mirroredBossQuestions(retainedQuestions);
|
|
7926
|
+
pendingBossQuestions = restoredQuestions;
|
|
7927
|
+
retainedEffectReconciliation = undefined;
|
|
7928
|
+
} catch {
|
|
7929
|
+
// Any host-ledger or runtime-snapshot defect leaves the root fence shut.
|
|
7930
|
+
}
|
|
7931
|
+
};
|
|
7932
|
+
|
|
7933
|
+
const rootStateDescriptionForRetention = (
|
|
7934
|
+
root: EngagementFrame,
|
|
7935
|
+
retainedState: PlaybookState,
|
|
7936
|
+
): string | undefined => {
|
|
7937
|
+
if (typeof root.runtime.describe !== 'function') return undefined;
|
|
7938
|
+
try {
|
|
7939
|
+
const view = root.runtime.describe();
|
|
7940
|
+
if (!isDeepStrictEqual(view.state, retainedState)) return undefined;
|
|
7941
|
+
return typeof view.stateDescription === 'string' &&
|
|
7942
|
+
view.stateDescription.trim().length > 0
|
|
7943
|
+
? view.stateDescription
|
|
7944
|
+
: undefined;
|
|
7945
|
+
} catch {
|
|
7946
|
+
return undefined;
|
|
7947
|
+
}
|
|
7948
|
+
};
|
|
7949
|
+
|
|
7950
|
+
const retainedGenerationFromFrames = (
|
|
7951
|
+
root: EngagementFrame,
|
|
7952
|
+
frameSnapshots: readonly PlaybookCaptainFrameSnapshot[],
|
|
7953
|
+
): PlaybookCaptainRetainedGeneration => {
|
|
7954
|
+
const rootStateDescription = rootStateDescriptionForRetention(
|
|
7955
|
+
root,
|
|
7956
|
+
frameSnapshots[0]!.runtime.state,
|
|
7957
|
+
);
|
|
7958
|
+
const checkpoint =
|
|
7959
|
+
retainedEffectReconciliation?.checkpoint ??
|
|
7960
|
+
assertPlaybookEffectLedger(currentEffectLedger());
|
|
7961
|
+
if (
|
|
7962
|
+
checkpoint.boundaries.some(
|
|
7963
|
+
({ physicalReceipt }) => physicalReceipt === undefined,
|
|
7964
|
+
)
|
|
7965
|
+
) {
|
|
7966
|
+
throw new TypeError(
|
|
7967
|
+
'Captain retained-generation checkpoint contains an incomplete physical boundary',
|
|
7968
|
+
);
|
|
7969
|
+
}
|
|
7970
|
+
return snapshotJsonValue(
|
|
7971
|
+
{
|
|
7972
|
+
effectLedger: checkpoint,
|
|
7973
|
+
frames: frameSnapshots,
|
|
7974
|
+
...(retainedEffectReconciliation === undefined
|
|
7975
|
+
? {}
|
|
7976
|
+
: {
|
|
7977
|
+
retainedEffectReconciliation: {
|
|
7978
|
+
sourceGenerationId:
|
|
7979
|
+
retainedEffectReconciliation.sourceGenerationId,
|
|
7980
|
+
},
|
|
7981
|
+
}),
|
|
7982
|
+
...(rootStateDescription === undefined
|
|
7983
|
+
? {}
|
|
7984
|
+
: { rootStateDescription }),
|
|
7985
|
+
},
|
|
7986
|
+
'Captain retained generation',
|
|
7987
|
+
) as unknown as PlaybookCaptainRetainedGeneration;
|
|
7988
|
+
};
|
|
7989
|
+
|
|
7990
|
+
const captureRetainedGeneration =
|
|
7991
|
+
(): PlaybookCaptainRetainedGeneration | undefined => {
|
|
7992
|
+
const root = rootFrame();
|
|
7993
|
+
if (
|
|
7994
|
+
!root ||
|
|
7995
|
+
frames.some((frame) => !runtimeRetainsGenerations(frame.runtime))
|
|
7996
|
+
) {
|
|
7997
|
+
return undefined;
|
|
7998
|
+
}
|
|
7999
|
+
const frameSnapshots = captureFrameSnapshots(true);
|
|
8000
|
+
if (frameSnapshots === undefined || frameSnapshots.length === 0) {
|
|
8001
|
+
return undefined;
|
|
8002
|
+
}
|
|
8003
|
+
return retainedGenerationFromFrames(root, frameSnapshots);
|
|
8004
|
+
};
|
|
8005
|
+
|
|
8006
|
+
const rememberRetainedGeneration = (): void => {
|
|
8007
|
+
const root = rootFrame();
|
|
8008
|
+
if (!root) return;
|
|
8009
|
+
if (frames.some((frame) => !runtimeRetainsGenerations(frame.runtime))) {
|
|
8010
|
+
retainedGenerationCandidates.set(root.entry.id, {
|
|
8011
|
+
status: 'incapable',
|
|
8012
|
+
});
|
|
8013
|
+
return;
|
|
8014
|
+
}
|
|
8015
|
+
const generation = captureRetainedGeneration();
|
|
8016
|
+
retainedGenerationCandidates.set(
|
|
8017
|
+
root.entry.id,
|
|
8018
|
+
generation === undefined
|
|
8019
|
+
? { status: 'unsafe' }
|
|
8020
|
+
: { status: 'captured', generation },
|
|
8021
|
+
);
|
|
8022
|
+
};
|
|
8023
|
+
|
|
8024
|
+
const retentionUpdateForPriorGeneration = (
|
|
8025
|
+
root: EngagementFrame,
|
|
8026
|
+
): PlaybookCaptainRetentionUpdate | undefined => {
|
|
8027
|
+
const rootPlaybookId = root.entry.id;
|
|
8028
|
+
if (!runtimeRetainsGenerations(root.runtime)) {
|
|
8029
|
+
return {
|
|
8030
|
+
kind: 'clear',
|
|
8031
|
+
rootPlaybookId,
|
|
8032
|
+
};
|
|
8033
|
+
}
|
|
8034
|
+
const candidate = retainedGenerationCandidates.get(rootPlaybookId);
|
|
8035
|
+
if (candidate?.status === 'incapable') {
|
|
8036
|
+
return undefined;
|
|
8037
|
+
}
|
|
8038
|
+
if (candidate?.status !== 'captured') {
|
|
8039
|
+
throw new Error(
|
|
8040
|
+
`${frameLabel(root)} could not capture its pre-terminal retained generation`,
|
|
8041
|
+
);
|
|
8042
|
+
}
|
|
8043
|
+
return {
|
|
8044
|
+
kind: 'retain',
|
|
8045
|
+
rootPlaybookId,
|
|
8046
|
+
generation: candidate.generation,
|
|
8047
|
+
};
|
|
8048
|
+
};
|
|
8049
|
+
|
|
8050
|
+
const retainOrClearDisposedRoot = (root: EngagementFrame): void => {
|
|
8051
|
+
const update = retentionUpdateForPriorGeneration(root);
|
|
8052
|
+
if (update !== undefined) {
|
|
8053
|
+
pendingRetentionUpdates.set(update.rootPlaybookId, update);
|
|
8054
|
+
}
|
|
8055
|
+
};
|
|
8056
|
+
|
|
8057
|
+
const recordTerminalRetention = (
|
|
8058
|
+
root: EngagementFrame,
|
|
8059
|
+
result: Extract<PlaybookRunResult, { outcome: 'terminal' }>,
|
|
8060
|
+
): void => {
|
|
8061
|
+
const rootPlaybookId = root.entry.id;
|
|
8062
|
+
if (!runtimeRetainsGenerations(root.runtime)) {
|
|
8063
|
+
pendingRetentionUpdates.set(rootPlaybookId, {
|
|
8064
|
+
kind: 'clear',
|
|
8065
|
+
rootPlaybookId,
|
|
8066
|
+
});
|
|
8067
|
+
return;
|
|
8068
|
+
}
|
|
8069
|
+
const terminalStateId = result.state.stateId;
|
|
8070
|
+
if (
|
|
8071
|
+
typeof terminalStateId !== 'string' ||
|
|
8072
|
+
terminalStateId.trim().length === 0
|
|
8073
|
+
) {
|
|
8074
|
+
throw new Error(
|
|
8075
|
+
`${frameLabel(root)} terminal result has no stable state id for retention`,
|
|
8076
|
+
);
|
|
8077
|
+
}
|
|
8078
|
+
const unfinished =
|
|
8079
|
+
root.runtime.retainedGenerationMetadata!.unfinishedFinalStateIds.includes(
|
|
8080
|
+
terminalStateId,
|
|
8081
|
+
);
|
|
8082
|
+
if (unfinished) {
|
|
8083
|
+
// A root opened and terminated within this turn has no pre-turn,
|
|
8084
|
+
// work-bearing generation. Leave any earlier store entry untouched.
|
|
8085
|
+
if (!retainedGenerationCandidates.has(rootPlaybookId)) return;
|
|
8086
|
+
retainOrClearDisposedRoot(root);
|
|
8087
|
+
} else {
|
|
8088
|
+
pendingRetentionUpdates.set(rootPlaybookId, {
|
|
8089
|
+
kind: 'clear',
|
|
8090
|
+
rootPlaybookId,
|
|
8091
|
+
});
|
|
8092
|
+
}
|
|
8093
|
+
};
|
|
8094
|
+
|
|
8095
|
+
const exportShellSnapshot = (): PlaybookCaptainShellSnapshot | undefined => {
|
|
8096
|
+
if (
|
|
8097
|
+
!safeCapturePoint() ||
|
|
8098
|
+
!captainRuntime ||
|
|
8099
|
+
!captainSessionId ||
|
|
8100
|
+
!captainAgent
|
|
8101
|
+
) {
|
|
8102
|
+
return undefined;
|
|
8103
|
+
}
|
|
8104
|
+
try {
|
|
8105
|
+
if (
|
|
8106
|
+
typeof captainRuntime.exportSnapshot !== 'function' ||
|
|
8107
|
+
typeof captainRuntime.restore !== 'function'
|
|
8108
|
+
) {
|
|
8109
|
+
return undefined;
|
|
8110
|
+
}
|
|
8111
|
+
const captainSnapshot = captainRuntime.exportSnapshot();
|
|
8112
|
+
if (captainSnapshot === undefined) return undefined;
|
|
8113
|
+
const frameSnapshots = captureFrameSnapshots(true);
|
|
8114
|
+
if (frameSnapshots === undefined) return undefined;
|
|
8115
|
+
const effectLedger = assertPlaybookEffectLedger(currentEffectLedger());
|
|
8116
|
+
refreshRetainedEffectFence(frameSnapshots, effectLedger);
|
|
5642
8117
|
const common = {
|
|
5643
|
-
schemaVersion:
|
|
8118
|
+
schemaVersion: 4 as const,
|
|
8119
|
+
effectLedger,
|
|
5644
8120
|
captain: {
|
|
5645
8121
|
sessionId: captainSessionId,
|
|
5646
8122
|
runtime: captainSnapshot,
|
|
@@ -5663,6 +8139,9 @@ export function createPlaybookCaptainShell(
|
|
|
5663
8139
|
...common,
|
|
5664
8140
|
mode: 'engaged.parked',
|
|
5665
8141
|
frames: frameSnapshots,
|
|
8142
|
+
...(retainedEffectReconciliation === undefined
|
|
8143
|
+
? {}
|
|
8144
|
+
: { retainedEffectReconciliation }),
|
|
5666
8145
|
...(pendingBossQuestions === undefined
|
|
5667
8146
|
? {}
|
|
5668
8147
|
: { pendingBossQuestions: pendingBossQuestions as JsonValue }),
|
|
@@ -5676,6 +8155,66 @@ export function createPlaybookCaptainShell(
|
|
|
5676
8155
|
}
|
|
5677
8156
|
};
|
|
5678
8157
|
|
|
8158
|
+
const exportSettlement = (): PlaybookCaptainSettlement | undefined => {
|
|
8159
|
+
if (!retentionSettlementReady || abandonmentSettlementUnsafe) {
|
|
8160
|
+
return undefined;
|
|
8161
|
+
}
|
|
8162
|
+
const snapshot = exportShellSnapshot();
|
|
8163
|
+
if (snapshot === undefined) return undefined;
|
|
8164
|
+
let unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[];
|
|
8165
|
+
try {
|
|
8166
|
+
unresolvedEffects =
|
|
8167
|
+
settledTurnUnresolvedEffects ?? currentUnresolvedEffects();
|
|
8168
|
+
} catch {
|
|
8169
|
+
return undefined;
|
|
8170
|
+
}
|
|
8171
|
+
const updates = new Map(pendingRetentionUpdates);
|
|
8172
|
+
for (const rootPlaybookId of retainedGenerationRootClears) {
|
|
8173
|
+
updates.set(rootPlaybookId, { kind: 'clear', rootPlaybookId });
|
|
8174
|
+
}
|
|
8175
|
+
const root = rootFrame();
|
|
8176
|
+
if (root !== undefined) {
|
|
8177
|
+
const rootPlaybookId = root.entry.id;
|
|
8178
|
+
if (frames.every((frame) => runtimeRetainsGenerations(frame.runtime))) {
|
|
8179
|
+
const generation =
|
|
8180
|
+
snapshot.mode === 'engaged.parked'
|
|
8181
|
+
? retainedGenerationFromFrames(root, snapshot.frames)
|
|
8182
|
+
: undefined;
|
|
8183
|
+
if (generation !== undefined) {
|
|
8184
|
+
updates.set(rootPlaybookId, {
|
|
8185
|
+
kind: 'retain',
|
|
8186
|
+
rootPlaybookId,
|
|
8187
|
+
generation,
|
|
8188
|
+
});
|
|
8189
|
+
}
|
|
8190
|
+
} else if (!runtimeRetainsGenerations(root.runtime)) {
|
|
8191
|
+
updates.set(rootPlaybookId, { kind: 'clear', rootPlaybookId });
|
|
8192
|
+
} else if (retainedGenerationCandidates.has(rootPlaybookId)) {
|
|
8193
|
+
try {
|
|
8194
|
+
const update = retentionUpdateForPriorGeneration(root);
|
|
8195
|
+
if (update !== undefined) {
|
|
8196
|
+
updates.set(rootPlaybookId, update);
|
|
8197
|
+
}
|
|
8198
|
+
} catch {
|
|
8199
|
+
return undefined;
|
|
8200
|
+
}
|
|
8201
|
+
}
|
|
8202
|
+
}
|
|
8203
|
+
for (const update of updates.values()) {
|
|
8204
|
+
applyRetentionUpdateToCatalog(update);
|
|
8205
|
+
}
|
|
8206
|
+
return snapshotJsonValue(
|
|
8207
|
+
{
|
|
8208
|
+
snapshot,
|
|
8209
|
+
retentionUpdates: [...updates.values()].sort((left, right) =>
|
|
8210
|
+
left.rootPlaybookId.localeCompare(right.rootPlaybookId),
|
|
8211
|
+
),
|
|
8212
|
+
unresolvedEffects,
|
|
8213
|
+
},
|
|
8214
|
+
'Captain settlement',
|
|
8215
|
+
) as unknown as PlaybookCaptainSettlement;
|
|
8216
|
+
};
|
|
8217
|
+
|
|
5679
8218
|
const verifyRestoredRuntime = (
|
|
5680
8219
|
runtime: PlaybookRuntime,
|
|
5681
8220
|
expected: PlaybookRuntimeSnapshot,
|
|
@@ -5699,6 +8238,9 @@ export function createPlaybookCaptainShell(
|
|
|
5699
8238
|
'sequences',
|
|
5700
8239
|
'pendingBossQuestions',
|
|
5701
8240
|
'suspendedCall',
|
|
8241
|
+
'effectLedger',
|
|
8242
|
+
'retainedEffectSourceSessionId',
|
|
8243
|
+
'retainedEffectReconciliation',
|
|
5702
8244
|
] as const) {
|
|
5703
8245
|
if (!isDeepStrictEqual(normalized[key], expected[key])) {
|
|
5704
8246
|
throw new Error(
|
|
@@ -5718,6 +8260,17 @@ export function createPlaybookCaptainShell(
|
|
|
5718
8260
|
cleanupFailures.push(error);
|
|
5719
8261
|
}
|
|
5720
8262
|
}
|
|
8263
|
+
const retainedRuntimes = takeRetainedOfferRuntimes();
|
|
8264
|
+
if (retainedRuntimes.length > 0) {
|
|
8265
|
+
try {
|
|
8266
|
+
await disposeRetainedRuntimeSet(
|
|
8267
|
+
retainedRuntimes,
|
|
8268
|
+
'retained-generation restore cleanup failed',
|
|
8269
|
+
);
|
|
8270
|
+
} catch (error) {
|
|
8271
|
+
cleanupFailures.push(error);
|
|
8272
|
+
}
|
|
8273
|
+
}
|
|
5721
8274
|
if (captainRuntime) {
|
|
5722
8275
|
shuttingDown = true;
|
|
5723
8276
|
try {
|
|
@@ -5734,11 +8287,20 @@ export function createPlaybookCaptainShell(
|
|
|
5734
8287
|
byCommand = new Map();
|
|
5735
8288
|
byId = new Map();
|
|
5736
8289
|
enablementById = new Map();
|
|
8290
|
+
hostCapabilitiesById = new Map();
|
|
8291
|
+
pendingHostCapabilities = undefined;
|
|
8292
|
+
currentEffectLedger = () => emptyPlaybookEffectLedger();
|
|
5737
8293
|
captainAgent = undefined;
|
|
5738
8294
|
captainAdapter = undefined;
|
|
5739
8295
|
playerAgents = new Map();
|
|
5740
8296
|
playerLedger.clear();
|
|
5741
8297
|
playerTransactions.clear();
|
|
8298
|
+
retainedGenerations.clear();
|
|
8299
|
+
ineligibleRetainedGenerations.clear();
|
|
8300
|
+
retainedGenerationRootClears.clear();
|
|
8301
|
+
retainedGenerationsInstalled = false;
|
|
8302
|
+
retainedGenerationInstallationInProgress = false;
|
|
8303
|
+
retainedGenerationInstallationClosed = false;
|
|
5742
8304
|
session = undefined;
|
|
5743
8305
|
sessionEmissionsOpen = false;
|
|
5744
8306
|
closedGateAttempted = false;
|
|
@@ -5746,6 +8308,7 @@ export function createPlaybookCaptainShell(
|
|
|
5746
8308
|
captainSessionId = undefined;
|
|
5747
8309
|
conversation = { kind: 'unopened' };
|
|
5748
8310
|
mode = 'chat';
|
|
8311
|
+
retainedEffectReconciliation = undefined;
|
|
5749
8312
|
pendingBossQuestions = undefined;
|
|
5750
8313
|
lastError = undefined;
|
|
5751
8314
|
journalSeq = 0;
|
|
@@ -5775,7 +8338,16 @@ export function createPlaybookCaptainShell(
|
|
|
5775
8338
|
lifecycle = 'restoring';
|
|
5776
8339
|
try {
|
|
5777
8340
|
const snapshot = assertPlaybookCaptainShellSnapshot(untrusted);
|
|
5778
|
-
const built = await
|
|
8341
|
+
const built = await buildCurrentEnablements();
|
|
8342
|
+
const builtHostCapabilities = new Map(built.hostCapabilitiesById);
|
|
8343
|
+
const readEffectLedger = () =>
|
|
8344
|
+
effectLedgerMirrorFromCapabilities(builtHostCapabilities);
|
|
8345
|
+
const hostLedger = readEffectLedger();
|
|
8346
|
+
if (!isDeepStrictEqual(snapshot.effectLedger, hostLedger)) {
|
|
8347
|
+
throw new Error(
|
|
8348
|
+
'Captain shell restore effect ledger does not match current-host authority',
|
|
8349
|
+
);
|
|
8350
|
+
}
|
|
5779
8351
|
captainAgent = built.captainAgent;
|
|
5780
8352
|
captainAdapter = captainAgent.adapter;
|
|
5781
8353
|
playerAgents = built.playerAgents;
|
|
@@ -5786,6 +8358,8 @@ export function createPlaybookCaptainShell(
|
|
|
5786
8358
|
byCommand = built.byCommand;
|
|
5787
8359
|
byId = built.byId;
|
|
5788
8360
|
enablementById = built.enablementById;
|
|
8361
|
+
hostCapabilitiesById = builtHostCapabilities;
|
|
8362
|
+
currentEffectLedger = readEffectLedger;
|
|
5789
8363
|
for (const [playerId, saved] of Object.entries(snapshot.playerSessions)) {
|
|
5790
8364
|
playerLedger.set(playerId, {
|
|
5791
8365
|
adapter: saved.adapter,
|
|
@@ -5891,6 +8465,8 @@ export function createPlaybookCaptainShell(
|
|
|
5891
8465
|
lastSettlementStatus = snapshot.lastSettlementStatus;
|
|
5892
8466
|
mode = snapshot.mode;
|
|
5893
8467
|
if (snapshot.mode === 'engaged.parked') {
|
|
8468
|
+
retainedEffectReconciliation =
|
|
8469
|
+
snapshot.retainedEffectReconciliation;
|
|
5894
8470
|
pendingBossQuestions = snapshot.pendingBossQuestions;
|
|
5895
8471
|
lastError = snapshot.lastError;
|
|
5896
8472
|
}
|
|
@@ -5909,6 +8485,74 @@ export function createPlaybookCaptainShell(
|
|
|
5909
8485
|
}
|
|
5910
8486
|
};
|
|
5911
8487
|
|
|
8488
|
+
const installRetainedGenerations = async (
|
|
8489
|
+
generations: Readonly<
|
|
8490
|
+
Record<string, PlaybookCaptainRetainedGeneration>
|
|
8491
|
+
>,
|
|
8492
|
+
): Promise<void> => {
|
|
8493
|
+
if (lifecycle !== 'ready' || terminallyDisposed) {
|
|
8494
|
+
throw new Error(
|
|
8495
|
+
'retained generations require an initialized or restored Captain shell',
|
|
8496
|
+
);
|
|
8497
|
+
}
|
|
8498
|
+
if (
|
|
8499
|
+
retainedGenerationsInstalled ||
|
|
8500
|
+
retainedGenerationInstallationInProgress ||
|
|
8501
|
+
retainedGenerationInstallationClosed ||
|
|
8502
|
+
activeTurnHostCalls !== undefined
|
|
8503
|
+
) {
|
|
8504
|
+
throw new Error(
|
|
8505
|
+
'retained generations may be installed exactly once before the first nonempty Boss turn',
|
|
8506
|
+
);
|
|
8507
|
+
}
|
|
8508
|
+
const normalized = normalizeInstalledRetainedGenerations(generations);
|
|
8509
|
+
retainedGenerationInstallationInProgress = true;
|
|
8510
|
+
try {
|
|
8511
|
+
retainedGenerations.clear();
|
|
8512
|
+
retainedGenerationOffers.clear();
|
|
8513
|
+
ineligibleRetainedGenerations.clear();
|
|
8514
|
+
retainedGenerationRootClears.clear();
|
|
8515
|
+
for (const [rootPlaybookId, generation] of normalized) {
|
|
8516
|
+
retainedGenerations.set(rootPlaybookId, generation);
|
|
8517
|
+
}
|
|
8518
|
+
await prepareRetainedGenerationOffers();
|
|
8519
|
+
retainedGenerationsInstalled = true;
|
|
8520
|
+
} catch (error) {
|
|
8521
|
+
const preparationCleanupFailed =
|
|
8522
|
+
error instanceof RetainedRuntimeCleanupError;
|
|
8523
|
+
const runtimes = [...retainedGenerationOffers.values()].flatMap(
|
|
8524
|
+
(offer) => [...offer.runtimes],
|
|
8525
|
+
);
|
|
8526
|
+
retainedGenerationOffers.clear();
|
|
8527
|
+
retainedGenerations.clear();
|
|
8528
|
+
ineligibleRetainedGenerations.clear();
|
|
8529
|
+
retainedGenerationRootClears.clear();
|
|
8530
|
+
try {
|
|
8531
|
+
await disposeRetainedRuntimeSet(
|
|
8532
|
+
runtimes,
|
|
8533
|
+
'retained-generation installation cleanup failed',
|
|
8534
|
+
);
|
|
8535
|
+
} catch (cleanupError) {
|
|
8536
|
+
if (cleanupError instanceof RetainedRuntimeCleanupError) {
|
|
8537
|
+
retiredRetainedRuntimes.push(...cleanupError.failedRuntimes);
|
|
8538
|
+
}
|
|
8539
|
+
terminallyDisposed = true;
|
|
8540
|
+
lifecycle = 'closed';
|
|
8541
|
+
throw new AggregateError(
|
|
8542
|
+
[error, cleanupError],
|
|
8543
|
+
'retained-generation installation and cleanup failed',
|
|
8544
|
+
);
|
|
8545
|
+
}
|
|
8546
|
+
if (preparationCleanupFailed) {
|
|
8547
|
+
terminallyDisposed = true;
|
|
8548
|
+
lifecycle = 'closed';
|
|
8549
|
+
}
|
|
8550
|
+
throw error;
|
|
8551
|
+
} finally {
|
|
8552
|
+
retainedGenerationInstallationInProgress = false;
|
|
8553
|
+
}
|
|
8554
|
+
};
|
|
8555
|
+
|
|
5912
8556
|
return {
|
|
5913
8557
|
async init(initSession: CaptainSession): Promise<void> {
|
|
5914
8558
|
if (lifecycle !== 'fresh' || terminallyDisposed) {
|
|
@@ -5920,11 +8564,17 @@ export function createPlaybookCaptainShell(
|
|
|
5920
8564
|
lifecycle = 'initializing';
|
|
5921
8565
|
try {
|
|
5922
8566
|
installSession(initSession, true);
|
|
5923
|
-
const built = await
|
|
8567
|
+
const built = await buildCurrentEnablements();
|
|
8568
|
+
const builtHostCapabilities = new Map(built.hostCapabilitiesById);
|
|
8569
|
+
const readEffectLedger = () =>
|
|
8570
|
+
effectLedgerMirrorFromCapabilities(builtHostCapabilities);
|
|
8571
|
+
readEffectLedger();
|
|
5924
8572
|
entries = built.entries;
|
|
5925
8573
|
byCommand = built.byCommand;
|
|
5926
8574
|
byId = built.byId;
|
|
5927
8575
|
enablementById = built.enablementById;
|
|
8576
|
+
hostCapabilitiesById = builtHostCapabilities;
|
|
8577
|
+
currentEffectLedger = readEffectLedger;
|
|
5928
8578
|
captainAgent = built.captainAgent;
|
|
5929
8579
|
captainAdapter = captainAgent.adapter;
|
|
5930
8580
|
playerAgents = built.playerAgents;
|
|
@@ -5950,8 +8600,12 @@ export function createPlaybookCaptainShell(
|
|
|
5950
8600
|
|
|
5951
8601
|
exportSnapshot: exportShellSnapshot,
|
|
5952
8602
|
|
|
8603
|
+
exportSettlement,
|
|
8604
|
+
|
|
5953
8605
|
restore: restoreShellSnapshot,
|
|
5954
8606
|
|
|
8607
|
+
installRetainedGenerations,
|
|
8608
|
+
|
|
5955
8609
|
async handleBossTurn(
|
|
5956
8610
|
turn: BossTurn,
|
|
5957
8611
|
context: CaptainContext,
|
|
@@ -5968,9 +8622,26 @@ export function createPlaybookCaptainShell(
|
|
|
5968
8622
|
if (activeTurnHostCalls !== undefined) {
|
|
5969
8623
|
throw new Error('cannot handle concurrent Boss turns');
|
|
5970
8624
|
}
|
|
8625
|
+
if (retainedGenerationInstallationInProgress) {
|
|
8626
|
+
throw new Error(
|
|
8627
|
+
'cannot handle a Boss turn while retained generations are installing',
|
|
8628
|
+
);
|
|
8629
|
+
}
|
|
5971
8630
|
// Empty or whitespace-only input allocates no call, session, or
|
|
5972
8631
|
// telemetry (CAPTAIN-7).
|
|
8632
|
+
retentionSettlementReady = false;
|
|
8633
|
+
abandonmentSettlementUnsafe = false;
|
|
5973
8634
|
if (turn.prompt.trim().length === 0) return;
|
|
8635
|
+
settledTurnUnresolvedEffects = undefined;
|
|
8636
|
+
retainedGenerationInstallationClosed = true;
|
|
8637
|
+
for (const update of pendingRetentionUpdates.values()) {
|
|
8638
|
+
applyRetentionUpdateToCatalog(update);
|
|
8639
|
+
}
|
|
8640
|
+
retainedGenerationCandidates.clear();
|
|
8641
|
+
pendingRetentionUpdates.clear();
|
|
8642
|
+
// A terminal or dismissal can remove the whole stack during this turn;
|
|
8643
|
+
// take the latest already-settled generation before controller work.
|
|
8644
|
+
rememberRetainedGeneration();
|
|
5974
8645
|
const turnHostCalls = new Set<Promise<unknown>>();
|
|
5975
8646
|
activeTurnHostCalls = turnHostCalls;
|
|
5976
8647
|
activeContext = context;
|
|
@@ -5993,6 +8664,8 @@ export function createPlaybookCaptainShell(
|
|
|
5993
8664
|
decisionCall = undefined;
|
|
5994
8665
|
appendJournal('boss', turn.prompt);
|
|
5995
8666
|
try {
|
|
8667
|
+
await drainRetiredRetainedRuntimes();
|
|
8668
|
+
await prepareRetainedGenerationOffers();
|
|
5996
8669
|
const result = await captainRuntime.handleBossInput({
|
|
5997
8670
|
text: turn.prompt,
|
|
5998
8671
|
signal: context.signal,
|
|
@@ -6064,11 +8737,16 @@ export function createPlaybookCaptainShell(
|
|
|
6064
8737
|
activeTurnHostCalls = undefined;
|
|
6065
8738
|
}
|
|
6066
8739
|
activeContext = undefined;
|
|
8740
|
+
retentionSettlementReady = true;
|
|
6067
8741
|
}
|
|
6068
8742
|
},
|
|
6069
8743
|
|
|
6070
8744
|
async prepareDispose(): Promise<void> {
|
|
6071
|
-
if (
|
|
8745
|
+
if (
|
|
8746
|
+
lifecycle === 'initializing' ||
|
|
8747
|
+
lifecycle === 'restoring' ||
|
|
8748
|
+
retainedGenerationInstallationInProgress
|
|
8749
|
+
) {
|
|
6072
8750
|
throw new Error('cannot dispose while Captain shell setup is in progress');
|
|
6073
8751
|
}
|
|
6074
8752
|
activeContext = undefined;
|
|
@@ -6076,7 +8754,11 @@ export function createPlaybookCaptainShell(
|
|
|
6076
8754
|
},
|
|
6077
8755
|
|
|
6078
8756
|
async dispose(): Promise<void> {
|
|
6079
|
-
if (
|
|
8757
|
+
if (
|
|
8758
|
+
lifecycle === 'initializing' ||
|
|
8759
|
+
lifecycle === 'restoring' ||
|
|
8760
|
+
retainedGenerationInstallationInProgress
|
|
8761
|
+
) {
|
|
6080
8762
|
throw new Error('cannot dispose while Captain shell setup is in progress');
|
|
6081
8763
|
}
|
|
6082
8764
|
activeContext = undefined;
|
|
@@ -6095,6 +8777,20 @@ export function createPlaybookCaptainShell(
|
|
|
6095
8777
|
} catch (error) {
|
|
6096
8778
|
failure = error;
|
|
6097
8779
|
}
|
|
8780
|
+
const retainedRuntimes = takeRetainedOfferRuntimes();
|
|
8781
|
+
if (retainedRuntimes.length > 0) {
|
|
8782
|
+
try {
|
|
8783
|
+
await disposeRetainedRuntimeSet(
|
|
8784
|
+
retainedRuntimes,
|
|
8785
|
+
'retained-generation shell cleanup failed',
|
|
8786
|
+
);
|
|
8787
|
+
} catch (error) {
|
|
8788
|
+
failure ??= error;
|
|
8789
|
+
}
|
|
8790
|
+
}
|
|
8791
|
+
retainedGenerations.clear();
|
|
8792
|
+
ineligibleRetainedGenerations.clear();
|
|
8793
|
+
retainedGenerationRootClears.clear();
|
|
6098
8794
|
const runtime = captainRuntime;
|
|
6099
8795
|
captainRuntime = undefined;
|
|
6100
8796
|
if (runtime) {
|
|
@@ -6108,6 +8804,9 @@ export function createPlaybookCaptainShell(
|
|
|
6108
8804
|
// Quarantine is session-wide by design. Only terminal teardown may drop
|
|
6109
8805
|
// its ownership after every frame host call and the Captain are drained.
|
|
6110
8806
|
playerTransactions.clear();
|
|
8807
|
+
hostCapabilitiesById = new Map();
|
|
8808
|
+
pendingHostCapabilities = undefined;
|
|
8809
|
+
currentEffectLedger = () => emptyPlaybookEffectLedger();
|
|
6111
8810
|
lifecycle = 'closed';
|
|
6112
8811
|
if (failure !== undefined) throw failure;
|
|
6113
8812
|
}
|