@sublang/playbook 9.0.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/cli.md +51 -8
- package/docs/embedding.md +38 -12
- package/package.json +7 -3
- package/reference/sdlc/captain.md +14 -10
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +33 -13
- package/reference/sdlc/captain.playbook/captain.fsm.js +80 -9
- package/reference/sdlc/captain.playbook/captain.fsm.ts +137 -18
- package/reference/sdlc/captain.playbook/captain.gears.md +10 -6
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +5 -1
- package/reference/sdlc/captain.playbook/captain.playbook.js +140 -10
- package/reference/sdlc/captain.playbook/captain.playbook.ts +188 -16
- package/reference/sdlc/code.md +0 -1
- package/reference/sdlc/code.playbook/bin/interactive-session.js +170 -17
- package/reference/sdlc/code.playbook/bin/launch-config.js +136 -4
- package/reference/sdlc/code.playbook/bin/playbook.js +81 -4
- package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
- package/reference/sdlc/code.playbook/bin/run.js +365 -63
- package/reference/sdlc/code.playbook/bin/session-store.js +2877 -209
- package/reference/sdlc/code.playbook/code.fsm.d.ts +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 +99 -7
- package/reference/sdlc/code.playbook/playbook-captain.js +1850 -72
- package/reference/sdlc/code.playbook/playbook-captain.ts +2759 -96
- package/reference/sdlc/decide.md +0 -1
- 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.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
|
@@ -16,24 +16,37 @@
|
|
|
16
16
|
// Output profile: bespoke parallel runtime with the shared nested-call
|
|
17
17
|
// bridge (slc/link.md §Output)
|
|
18
18
|
|
|
19
|
+
import { randomUUID } from 'node:crypto';
|
|
20
|
+
|
|
19
21
|
import PQueue from 'p-queue';
|
|
20
22
|
import { createActor, fromPromise } from 'xstate';
|
|
21
23
|
import type { InspectionEvent, SnapshotFrom } from 'xstate';
|
|
22
24
|
|
|
25
|
+
import {
|
|
26
|
+
createAcceptedOutcomeConsumer,
|
|
27
|
+
type AcceptedOutcomeReceipt,
|
|
28
|
+
} from '../../../src/accepted-outcome.js';
|
|
29
|
+
|
|
23
30
|
import {
|
|
24
31
|
assertJsonSafe,
|
|
32
|
+
assertPlaybookEffectLedger,
|
|
25
33
|
assertPlaybookRuntimeSnapshot,
|
|
26
34
|
combineAbortSignals,
|
|
27
35
|
createNestedPlaybookBridge,
|
|
28
36
|
detachPersistedMachineSnapshot,
|
|
29
37
|
normalizeError,
|
|
30
38
|
normalizePlaybookSnapshot,
|
|
39
|
+
PlaybookSemanticCandidateStructureError,
|
|
40
|
+
reconcilePlaybookSemanticEvidence,
|
|
31
41
|
snapshotJsonValue,
|
|
32
42
|
snapshotPlaybookSession,
|
|
33
43
|
validatePlayerResult,
|
|
34
44
|
waitForPlaybookQuiescence,
|
|
35
45
|
} from '../../../src/xstate-runtime.js';
|
|
36
|
-
import type {
|
|
46
|
+
import type {
|
|
47
|
+
NestedPlaybookBridge,
|
|
48
|
+
PlaybookSemanticOutcomeSpec,
|
|
49
|
+
} from '../../../src/xstate-runtime.js';
|
|
37
50
|
|
|
38
51
|
import decideMachine, {
|
|
39
52
|
type PlayerInput,
|
|
@@ -44,6 +57,8 @@ import decideMachine, {
|
|
|
44
57
|
type PlaybookInput,
|
|
45
58
|
} from './decide.fsm.js';
|
|
46
59
|
|
|
60
|
+
import type { PlaybookHostConstructionCapabilities } from '../code.playbook/playbook-captain.js';
|
|
61
|
+
|
|
47
62
|
import type {
|
|
48
63
|
CaptainCallOptions,
|
|
49
64
|
CaptainResult,
|
|
@@ -58,8 +73,14 @@ import type {
|
|
|
58
73
|
PlaybookControlAction,
|
|
59
74
|
PlaybookControlReceipt,
|
|
60
75
|
PlaybookControlView,
|
|
76
|
+
PlaybookEffectBoundary,
|
|
77
|
+
PlaybookEffectBoundaryStart,
|
|
78
|
+
PlaybookEffectLedger,
|
|
79
|
+
PlaybookEffectLedgerCapability,
|
|
80
|
+
PlaybookPendingBossQuestion,
|
|
61
81
|
PlaybookPendingCall,
|
|
62
82
|
PlaybookPorts,
|
|
83
|
+
PlaybookRepositoryReceipt,
|
|
63
84
|
PlaybookRunResult,
|
|
64
85
|
PlaybookRuntime,
|
|
65
86
|
PlaybookRuntimeFactory,
|
|
@@ -102,6 +123,14 @@ type RoleId = 'coder' | 'reviewer';
|
|
|
102
123
|
|
|
103
124
|
export type PlaybookRuntimeOptions = DecideInput;
|
|
104
125
|
|
|
126
|
+
export type DecidePlaybookHostCapabilities =
|
|
127
|
+
PlaybookHostConstructionCapabilities;
|
|
128
|
+
|
|
129
|
+
export interface DecidePlaybookRuntimeConstruction {
|
|
130
|
+
readonly configuredOptions: PlaybookRuntimeOptions;
|
|
131
|
+
readonly hostCapabilities: DecidePlaybookHostCapabilities;
|
|
132
|
+
}
|
|
133
|
+
|
|
105
134
|
function snapshotDecideRuntimeOptions(value: unknown): PlaybookRuntimeOptions {
|
|
106
135
|
const captured = snapshotJsonValue(value, 'DECIDE runtime options');
|
|
107
136
|
if (!isPlainObject(captured)) {
|
|
@@ -174,6 +203,61 @@ const ROLE_STATE_IDS: ReadonlySet<string> = new Set(
|
|
|
174
203
|
ROLE_STATES.map((state) => state.stateId),
|
|
175
204
|
);
|
|
176
205
|
|
|
206
|
+
const ACCEPTED_OUTCOME_DECLARATIONS: Readonly<
|
|
207
|
+
Record<string, ReadonlySet<string>>
|
|
208
|
+
> = Object.freeze({
|
|
209
|
+
askCoderProposal: new Set(['proposed', 'needsBossReply']),
|
|
210
|
+
askReviewerProposal: new Set(['proposed', 'needsBossReply']),
|
|
211
|
+
commitCoderProposal: new Set(['committed', 'needsBossReply']),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const DECIDE_OUTCOME_AUTHORITY = Object.freeze({
|
|
215
|
+
governedPlayerStates: Object.freeze({
|
|
216
|
+
askCoderProposal: Object.freeze({
|
|
217
|
+
proposed: Object.freeze({
|
|
218
|
+
fields: Object.freeze({ coderProposal: 'presentation' as const }),
|
|
219
|
+
repositoryDisposition: 'unchanged' as const,
|
|
220
|
+
}),
|
|
221
|
+
needsBossReply: Object.freeze({
|
|
222
|
+
fields: Object.freeze({ question: 'presentation' as const }),
|
|
223
|
+
repositoryDisposition: 'unchanged' as const,
|
|
224
|
+
}),
|
|
225
|
+
}),
|
|
226
|
+
askReviewerProposal: Object.freeze({
|
|
227
|
+
proposed: Object.freeze({
|
|
228
|
+
fields: Object.freeze({ reviewerProposal: 'presentation' as const }),
|
|
229
|
+
repositoryDisposition: 'unchanged' as const,
|
|
230
|
+
}),
|
|
231
|
+
needsBossReply: Object.freeze({
|
|
232
|
+
fields: Object.freeze({ question: 'presentation' as const }),
|
|
233
|
+
repositoryDisposition: 'unchanged' as const,
|
|
234
|
+
}),
|
|
235
|
+
}),
|
|
236
|
+
commitCoderProposal: Object.freeze({
|
|
237
|
+
committed: Object.freeze({
|
|
238
|
+
fields: Object.freeze({
|
|
239
|
+
coderOutput: 'presentation' as const,
|
|
240
|
+
latestCommit: 'effect' as const,
|
|
241
|
+
}),
|
|
242
|
+
repositoryDisposition: 'one-descendant-commit' as const,
|
|
243
|
+
}),
|
|
244
|
+
needsBossReply: Object.freeze({
|
|
245
|
+
fields: Object.freeze({ question: 'presentation' as const }),
|
|
246
|
+
repositoryDisposition: 'deferred' as const,
|
|
247
|
+
}),
|
|
248
|
+
}),
|
|
249
|
+
}),
|
|
250
|
+
}) satisfies {
|
|
251
|
+
readonly governedPlayerStates: Readonly<
|
|
252
|
+
Record<string, Readonly<Record<string, PlaybookSemanticOutcomeSpec>>>
|
|
253
|
+
>;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const PROPOSAL_STATE_BY_ROLE = Object.freeze({
|
|
257
|
+
coder: 'askCoderProposal',
|
|
258
|
+
reviewer: 'askReviewerProposal',
|
|
259
|
+
} as const);
|
|
260
|
+
|
|
177
261
|
const ROLE_IDS = ['coder', 'reviewer'] as const;
|
|
178
262
|
const ROLE_ID_SET: ReadonlySet<string> = new Set(ROLE_IDS);
|
|
179
263
|
|
|
@@ -185,9 +269,16 @@ const BOSS_INTERRUPT_TARGETS = ['independentProposals'] as const;
|
|
|
185
269
|
const BOSS_INTERRUPT_TARGET_IDS: ReadonlySet<string> = new Set(
|
|
186
270
|
BOSS_INTERRUPT_TARGETS,
|
|
187
271
|
);
|
|
272
|
+
const UNFINISHED_FINAL_STATE_IDS: ReadonlySet<string> = new Set([
|
|
273
|
+
'reportedReviewFailure',
|
|
274
|
+
]);
|
|
188
275
|
|
|
189
276
|
const TELEMETRY_TOPIC = 'playbook.fsm.state';
|
|
190
277
|
const TRACE_TOPIC = 'playbook.trace';
|
|
278
|
+
const UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID =
|
|
279
|
+
'reconcile:unresolved-effect';
|
|
280
|
+
const UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID =
|
|
281
|
+
'abandon:unresolved-effect';
|
|
191
282
|
|
|
192
283
|
const CONTINUATION_PREAMBLE =
|
|
193
284
|
'You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.';
|
|
@@ -261,6 +352,44 @@ function requiredFieldsFor(description: string): string[] {
|
|
|
261
352
|
return fields;
|
|
262
353
|
}
|
|
263
354
|
|
|
355
|
+
function governedOutcomesFor(
|
|
356
|
+
input: PlayerInput,
|
|
357
|
+
): Readonly<Record<string, PlaybookSemanticOutcomeSpec>> {
|
|
358
|
+
const outcomes = (
|
|
359
|
+
DECIDE_OUTCOME_AUTHORITY.governedPlayerStates as Readonly<
|
|
360
|
+
Record<string, Readonly<Record<string, PlaybookSemanticOutcomeSpec>>>
|
|
361
|
+
>
|
|
362
|
+
)[input.stateId];
|
|
363
|
+
if (outcomes === undefined) {
|
|
364
|
+
throw new TypeError(
|
|
365
|
+
`DECIDE governed player state ${JSON.stringify(input.stateId)} has no outcome authority`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
const declaredGuards = Object.keys(outcomes).sort();
|
|
369
|
+
const authoredGuards = Object.keys(input.result).sort();
|
|
370
|
+
if (
|
|
371
|
+
declaredGuards.length !== authoredGuards.length ||
|
|
372
|
+
declaredGuards.some((guard, index) => guard !== authoredGuards[index])
|
|
373
|
+
) {
|
|
374
|
+
throw new TypeError(
|
|
375
|
+
`DECIDE governed player state ${input.stateId} changed its authored outcome set`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
for (const guard of declaredGuards) {
|
|
379
|
+
const required = requiredFieldsFor(input.result[guard]!).sort();
|
|
380
|
+
const authoritative = Object.keys(outcomes[guard]!.fields).sort();
|
|
381
|
+
if (
|
|
382
|
+
required.length !== authoritative.length ||
|
|
383
|
+
required.some((field, index) => field !== authoritative[index])
|
|
384
|
+
) {
|
|
385
|
+
throw new TypeError(
|
|
386
|
+
`DECIDE governed outcome ${input.stateId}.${guard} changed its payload fields`,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return outcomes;
|
|
391
|
+
}
|
|
392
|
+
|
|
264
393
|
// LLM judges routinely wrap JSON in prose/fences or damage its tail. Match
|
|
265
394
|
// CODE's recovery contract: scan candidate starts in document order, prefer a
|
|
266
395
|
// strict balanced value at each position, then repair trailing commas and
|
|
@@ -329,6 +458,14 @@ function stableJson(value: unknown, path: string): string {
|
|
|
329
458
|
return JSON.stringify(sortJson(snapshotJsonValue(value, path)));
|
|
330
459
|
}
|
|
331
460
|
|
|
461
|
+
function deepFreeze<T>(value: T): T {
|
|
462
|
+
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
463
|
+
Object.freeze(value);
|
|
464
|
+
for (const member of Object.values(value)) deepFreeze(member);
|
|
465
|
+
}
|
|
466
|
+
return value;
|
|
467
|
+
}
|
|
468
|
+
|
|
332
469
|
function stripCodeFence(text: string): string {
|
|
333
470
|
const fence = text.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/i);
|
|
334
471
|
return fence ? fence[1].trim() : text;
|
|
@@ -492,7 +629,9 @@ function parseClassification(
|
|
|
492
629
|
function buildAdjudicatorPrompt(
|
|
493
630
|
input: PlayerInput,
|
|
494
631
|
playerOutput: string,
|
|
632
|
+
correction?: { readonly reply: string; readonly error: string },
|
|
495
633
|
): string {
|
|
634
|
+
const outcomes = governedOutcomesFor(input);
|
|
496
635
|
const lines: string[] = [];
|
|
497
636
|
lines.push('You are the guard adjudicator for a playbook state machine.');
|
|
498
637
|
lines.push(
|
|
@@ -514,72 +653,44 @@ function buildAdjudicatorPrompt(
|
|
|
514
653
|
'Guards (choose exactly one; the descriptions are authoritative and must be applied as written):',
|
|
515
654
|
);
|
|
516
655
|
for (const [guard, description] of Object.entries(input.result)) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
for (const description of Object.values(input.result)) {
|
|
521
|
-
for (const field of requiredFieldsFor(description)) {
|
|
522
|
-
if (VERBATIM_PAYLOAD_FIELDS.has(field)) runtimeOwnedFields.add(field);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
if (runtimeOwnedFields.size > 0) {
|
|
526
|
-
lines.push('');
|
|
656
|
+
const semanticFields = Object.entries(outcomes[guard]?.fields ?? {})
|
|
657
|
+
.filter(([, authority]) => authority === 'semantic')
|
|
658
|
+
.map(([field]) => field);
|
|
527
659
|
lines.push(
|
|
528
|
-
|
|
660
|
+
`- ${guard}: semantic fields: ${
|
|
661
|
+
semanticFields.length === 0 ? '(none)' : semanticFields.join(', ')
|
|
662
|
+
}; ${description}`,
|
|
529
663
|
);
|
|
530
664
|
}
|
|
531
665
|
lines.push('');
|
|
532
666
|
lines.push(
|
|
533
|
-
'Reply with
|
|
667
|
+
'Reply with exactly the chosen `guard` and every semantic-owned field for that guard, and no other field.',
|
|
668
|
+
);
|
|
669
|
+
lines.push(
|
|
670
|
+
'Do not include presentation-, effect-, or runtime-owned fields; the runtime supplies those from authoritative evidence.',
|
|
534
671
|
);
|
|
672
|
+
if (correction !== undefined) {
|
|
673
|
+
lines.push('');
|
|
674
|
+
lines.push('Your first reply was structurally invalid:');
|
|
675
|
+
lines.push('"""');
|
|
676
|
+
lines.push(correction.reply);
|
|
677
|
+
lines.push('"""');
|
|
678
|
+
lines.push(`Validation error: ${correction.error}`);
|
|
679
|
+
lines.push(
|
|
680
|
+
'Correct only that structure using the same player output and outcome schema.',
|
|
681
|
+
);
|
|
682
|
+
}
|
|
535
683
|
return lines.join('\n');
|
|
536
684
|
}
|
|
537
685
|
|
|
538
|
-
function
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if (!obj || typeof obj.guard !== 'string' || obj.guard.trim() === '') {
|
|
545
|
-
throw new Error('adjudicator returned empty or malformed JSON');
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
const guard = obj.guard;
|
|
549
|
-
if (!Object.prototype.hasOwnProperty.call(input.result, guard)) {
|
|
550
|
-
throw new Error(
|
|
551
|
-
`adjudicator returned undeclared guard "${guard}" for ${input.sourceItem}`,
|
|
686
|
+
function parseGovernedSemanticCandidate(raw: string): unknown {
|
|
687
|
+
try {
|
|
688
|
+
return parseJudgeJson(raw);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
throw new PlaybookSemanticCandidateStructureError(
|
|
691
|
+
error instanceof Error ? error.message : 'reply is not valid JSON',
|
|
552
692
|
);
|
|
553
693
|
}
|
|
554
|
-
|
|
555
|
-
const requiredFields = requiredFieldsFor(input.result[guard]);
|
|
556
|
-
const allowedFields = new Set(['guard', ...requiredFields]);
|
|
557
|
-
for (const key of Reflect.ownKeys(obj)) {
|
|
558
|
-
if (typeof key !== 'string' || !allowedFields.has(key)) {
|
|
559
|
-
throw new Error(
|
|
560
|
-
`adjudicator response for guard "${guard}" included undeclared field "${String(key)}"`,
|
|
561
|
-
);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
const output: Record<string, unknown> = { guard };
|
|
565
|
-
for (const field of requiredFields) {
|
|
566
|
-
if (VERBATIM_PAYLOAD_FIELDS.has(field)) {
|
|
567
|
-
output[field] = finalText;
|
|
568
|
-
continue;
|
|
569
|
-
}
|
|
570
|
-
const value = obj[field];
|
|
571
|
-
if (
|
|
572
|
-
value === undefined ||
|
|
573
|
-
value === null ||
|
|
574
|
-
(typeof value === 'string' && value.trim() === '')
|
|
575
|
-
) {
|
|
576
|
-
throw new Error(
|
|
577
|
-
`adjudicator response for guard "${guard}" missing required field "${field}"`,
|
|
578
|
-
);
|
|
579
|
-
}
|
|
580
|
-
output[field] = value;
|
|
581
|
-
}
|
|
582
|
-
return output as PlayerOutput;
|
|
583
694
|
}
|
|
584
695
|
|
|
585
696
|
function combineSignals(
|
|
@@ -611,6 +722,369 @@ function isEmptyFinalText(finalText: string | undefined): boolean {
|
|
|
611
722
|
return finalText === undefined || finalText.trim().length === 0;
|
|
612
723
|
}
|
|
613
724
|
|
|
725
|
+
interface AutomaticReplayPolicy {
|
|
726
|
+
allowsEmptyOkCorrection(runtimeSessionId: string, callId: string): boolean;
|
|
727
|
+
allowsFailureStateRetry(): boolean;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
interface Schema3AutomaticReplayEvidence {
|
|
731
|
+
readonly effectLedger: PlaybookEffectLedgerCapability;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
interface DecideRepositoryOperationSettlement<T> {
|
|
735
|
+
readonly status: 'fulfilled';
|
|
736
|
+
readonly value: T;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
interface DecideRepositoryOperationRejection {
|
|
740
|
+
readonly status: 'rejected';
|
|
741
|
+
readonly reason: unknown;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
interface DecideRepositoryCompletion<T> {
|
|
745
|
+
readonly boundary: PlaybookEffectBoundary;
|
|
746
|
+
readonly operation:
|
|
747
|
+
| DecideRepositoryOperationSettlement<T>
|
|
748
|
+
| DecideRepositoryOperationRejection;
|
|
749
|
+
readonly receipt: PlaybookRepositoryReceipt;
|
|
750
|
+
readonly outcomeReceipt: PlaybookRepositoryReceipt;
|
|
751
|
+
readonly roleId?: RoleId;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
interface DecideDeferredBinding {
|
|
755
|
+
readonly operationId: string;
|
|
756
|
+
readonly pendingQuestion: PlaybookPendingBossQuestion;
|
|
757
|
+
readonly playerContinuation: JsonValue;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
interface DecideRepositoryCompletionEvidence {
|
|
761
|
+
readonly finalText?: string;
|
|
762
|
+
readonly semanticCandidate?: JsonValue;
|
|
763
|
+
readonly deferred?: DecideDeferredBinding;
|
|
764
|
+
readonly unresolved?: true;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
interface DecideRepositoryExclusiveResult<T> {
|
|
768
|
+
readonly operation:
|
|
769
|
+
| DecideRepositoryOperationSettlement<T>
|
|
770
|
+
| DecideRepositoryOperationRejection;
|
|
771
|
+
readonly receipt: PlaybookRepositoryReceipt;
|
|
772
|
+
readonly effectLedger: PlaybookEffectLedger;
|
|
773
|
+
readonly deferredStatus?: 'bound' | 'unresolved';
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
interface DecideRepositoryCohortResult<T> {
|
|
777
|
+
readonly baseline: PlaybookRepositoryReceipt['baseline'];
|
|
778
|
+
readonly invocationId: string;
|
|
779
|
+
readonly operations: Readonly<
|
|
780
|
+
Record<
|
|
781
|
+
RoleId,
|
|
782
|
+
| DecideRepositoryOperationSettlement<T>
|
|
783
|
+
| DecideRepositoryOperationRejection
|
|
784
|
+
>
|
|
785
|
+
>;
|
|
786
|
+
readonly receipts: Readonly<Record<RoleId, PlaybookRepositoryReceipt>>;
|
|
787
|
+
readonly effectLedger: PlaybookEffectLedger;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
interface DecideRepositoryDeferredContinuationResult<T>
|
|
791
|
+
extends DecideRepositoryExclusiveResult<T> {
|
|
792
|
+
readonly status: 'continued';
|
|
793
|
+
readonly baseline: PlaybookRepositoryReceipt['baseline'];
|
|
794
|
+
readonly logicalReceipt?: PlaybookRepositoryReceipt;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
interface DecideRepositoryDeferredCheckpointMismatch {
|
|
798
|
+
readonly status: 'checkpoint-mismatch' | 'ineligible';
|
|
799
|
+
readonly effectLedger: PlaybookEffectLedger;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
interface DecideRepositoryDeferredParked {
|
|
803
|
+
readonly status: 'parked';
|
|
804
|
+
readonly effectLedger: PlaybookEffectLedger;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
interface DecideRepositoryDeferredRestoreResult {
|
|
808
|
+
readonly status: 'restored' | 'checkpoint-mismatch' | 'ineligible';
|
|
809
|
+
readonly effectLedger: PlaybookEffectLedger;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
type DecideEffectBoundarySeed = Omit<
|
|
813
|
+
PlaybookEffectBoundaryStart,
|
|
814
|
+
'playbookId' | 'canonicalWorktree' | 'baseline' | 'cohortId'
|
|
815
|
+
>;
|
|
816
|
+
|
|
817
|
+
interface DecideRepositoryCapability {
|
|
818
|
+
runExclusive<T>(options: {
|
|
819
|
+
readonly signal: AbortSignal;
|
|
820
|
+
readonly effectBoundary: DecideEffectBoundarySeed;
|
|
821
|
+
readonly operation: (context: {
|
|
822
|
+
readonly baseline: PlaybookRepositoryReceipt['baseline'];
|
|
823
|
+
readonly identity: unknown;
|
|
824
|
+
}) => Promise<T>;
|
|
825
|
+
readonly completeEffectBoundary: (
|
|
826
|
+
completion: DecideRepositoryCompletion<T>,
|
|
827
|
+
) =>
|
|
828
|
+
| DecideRepositoryCompletionEvidence
|
|
829
|
+
| Promise<DecideRepositoryCompletionEvidence>;
|
|
830
|
+
}): Promise<DecideRepositoryExclusiveResult<T>>;
|
|
831
|
+
runCohort<T>(options: {
|
|
832
|
+
readonly signal: AbortSignal;
|
|
833
|
+
readonly invocationId: string;
|
|
834
|
+
readonly roleIds: readonly [RoleId, RoleId];
|
|
835
|
+
readonly dispositionsByRole: Readonly<
|
|
836
|
+
Record<RoleId, readonly ['unchanged']>
|
|
837
|
+
>;
|
|
838
|
+
readonly effectBoundaries: Readonly<
|
|
839
|
+
Record<RoleId, DecideEffectBoundarySeed>
|
|
840
|
+
>;
|
|
841
|
+
readonly operations: Readonly<
|
|
842
|
+
Record<
|
|
843
|
+
RoleId,
|
|
844
|
+
(context: {
|
|
845
|
+
readonly baseline: PlaybookRepositoryReceipt['baseline'];
|
|
846
|
+
readonly identity: unknown;
|
|
847
|
+
readonly invocationId: string;
|
|
848
|
+
readonly roleId: RoleId;
|
|
849
|
+
}) => Promise<T>
|
|
850
|
+
>
|
|
851
|
+
>;
|
|
852
|
+
readonly completeEffectBoundary: (
|
|
853
|
+
completion: DecideRepositoryCompletion<T> & { readonly roleId: RoleId },
|
|
854
|
+
) =>
|
|
855
|
+
| DecideRepositoryCompletionEvidence
|
|
856
|
+
| Promise<DecideRepositoryCompletionEvidence>;
|
|
857
|
+
}): Promise<DecideRepositoryCohortResult<T>>;
|
|
858
|
+
runDeferred<T>(options: {
|
|
859
|
+
readonly mode: 'continue';
|
|
860
|
+
readonly signal: AbortSignal;
|
|
861
|
+
readonly operationId: string;
|
|
862
|
+
readonly effectBoundary: DecideEffectBoundarySeed;
|
|
863
|
+
readonly operation: (context: {
|
|
864
|
+
readonly baseline: PlaybookRepositoryReceipt['baseline'];
|
|
865
|
+
readonly identity: unknown;
|
|
866
|
+
readonly playerContinuation: JsonValue;
|
|
867
|
+
}) => Promise<T>;
|
|
868
|
+
readonly completeEffectBoundary: (
|
|
869
|
+
completion: DecideRepositoryCompletion<T>,
|
|
870
|
+
) =>
|
|
871
|
+
| DecideRepositoryCompletionEvidence
|
|
872
|
+
| Promise<DecideRepositoryCompletionEvidence>;
|
|
873
|
+
}): Promise<
|
|
874
|
+
| DecideRepositoryDeferredContinuationResult<T>
|
|
875
|
+
| DecideRepositoryDeferredCheckpointMismatch
|
|
876
|
+
>;
|
|
877
|
+
runDeferred(options: {
|
|
878
|
+
readonly mode: 'park' | 'restore';
|
|
879
|
+
readonly signal: AbortSignal;
|
|
880
|
+
readonly operationId: string;
|
|
881
|
+
}): Promise<
|
|
882
|
+
DecideRepositoryDeferredParked | DecideRepositoryDeferredRestoreResult
|
|
883
|
+
>;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
interface Schema3DeferredEffectEvidence extends Schema3AutomaticReplayEvidence {
|
|
887
|
+
readonly authority: DecidePlaybookHostCapabilities['authority'];
|
|
888
|
+
readonly repository: DecideRepositoryCapability;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function schema3Construction(
|
|
892
|
+
value: unknown,
|
|
893
|
+
): {
|
|
894
|
+
readonly configuredOptions: PlaybookRuntimeOptions;
|
|
895
|
+
readonly hostCapabilities: Schema3DeferredEffectEvidence;
|
|
896
|
+
} {
|
|
897
|
+
if (!isPlainObject(value)) {
|
|
898
|
+
throw new TypeError('DECIDE schema-3 factory input must be a plain object');
|
|
899
|
+
}
|
|
900
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
901
|
+
const keys = Reflect.ownKeys(value);
|
|
902
|
+
if (
|
|
903
|
+
keys.length !== 2 ||
|
|
904
|
+
!keys.includes('configuredOptions') ||
|
|
905
|
+
!keys.includes('hostCapabilities') ||
|
|
906
|
+
keys.some((key) => {
|
|
907
|
+
const descriptor = descriptors[key as keyof typeof descriptors];
|
|
908
|
+
return (
|
|
909
|
+
descriptor?.get !== undefined ||
|
|
910
|
+
descriptor?.set !== undefined ||
|
|
911
|
+
descriptor?.enumerable !== true ||
|
|
912
|
+
!Object.prototype.hasOwnProperty.call(descriptor, 'value')
|
|
913
|
+
);
|
|
914
|
+
})
|
|
915
|
+
) {
|
|
916
|
+
throw new TypeError(
|
|
917
|
+
'DECIDE schema-3 factory input must contain exactly configuredOptions and hostCapabilities data properties',
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
const configuredOptions = descriptors.configuredOptions!.value;
|
|
921
|
+
if (
|
|
922
|
+
configuredOptions !== null &&
|
|
923
|
+
typeof configuredOptions === 'object' &&
|
|
924
|
+
Object.prototype.hasOwnProperty.call(
|
|
925
|
+
configuredOptions,
|
|
926
|
+
'hostCapabilities',
|
|
927
|
+
)
|
|
928
|
+
) {
|
|
929
|
+
throw new TypeError(
|
|
930
|
+
'DECIDE configured options must not contain hostCapabilities',
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
const hostCapabilities = descriptors.hostCapabilities!.value;
|
|
934
|
+
if (
|
|
935
|
+
hostCapabilities === null ||
|
|
936
|
+
typeof hostCapabilities !== 'object' ||
|
|
937
|
+
Array.isArray(hostCapabilities)
|
|
938
|
+
) {
|
|
939
|
+
throw new TypeError(
|
|
940
|
+
'DECIDE schema-3 factory input hostCapabilities must be a live object',
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
const repositoryDescriptor = Object.getOwnPropertyDescriptor(
|
|
944
|
+
hostCapabilities,
|
|
945
|
+
'repository',
|
|
946
|
+
);
|
|
947
|
+
const authorityDescriptor = Object.getOwnPropertyDescriptor(
|
|
948
|
+
hostCapabilities,
|
|
949
|
+
'authority',
|
|
950
|
+
);
|
|
951
|
+
const effectLedgerDescriptor = Object.getOwnPropertyDescriptor(
|
|
952
|
+
hostCapabilities,
|
|
953
|
+
'effectLedger',
|
|
954
|
+
);
|
|
955
|
+
const repository = repositoryDescriptor?.value;
|
|
956
|
+
const authority = authorityDescriptor?.value;
|
|
957
|
+
const effectLedger = effectLedgerDescriptor?.value;
|
|
958
|
+
if (
|
|
959
|
+
authorityDescriptor === undefined ||
|
|
960
|
+
authorityDescriptor.get !== undefined ||
|
|
961
|
+
authorityDescriptor.set !== undefined ||
|
|
962
|
+
!Object.prototype.hasOwnProperty.call(authorityDescriptor, 'value') ||
|
|
963
|
+
!isPlainObject(authority) ||
|
|
964
|
+
authority.artifactSchema !== 3 ||
|
|
965
|
+
authority.playbookId !== 'decide' ||
|
|
966
|
+
typeof authority.sessionId !== 'string' ||
|
|
967
|
+
authority.sessionId.length === 0 ||
|
|
968
|
+
!Array.isArray(authority.requiredRoleIds) ||
|
|
969
|
+
stableJson(
|
|
970
|
+
[...authority.requiredRoleIds].sort(),
|
|
971
|
+
'DECIDE host authority required roles',
|
|
972
|
+
) !== stableJson([...ROLE_IDS].sort(), 'DECIDE required roles') ||
|
|
973
|
+
!Array.isArray(authority.concurrentRoleSets) ||
|
|
974
|
+
authority.concurrentRoleSets.length !== 1 ||
|
|
975
|
+
!Array.isArray(authority.concurrentRoleSets[0]) ||
|
|
976
|
+
stableJson(
|
|
977
|
+
[...authority.concurrentRoleSets[0]].sort(),
|
|
978
|
+
'DECIDE host authority concurrent roles',
|
|
979
|
+
) !== stableJson([...ROLE_IDS].sort(), 'DECIDE concurrent roles')
|
|
980
|
+
) {
|
|
981
|
+
throw new TypeError(
|
|
982
|
+
'DECIDE schema-3 factory input hostCapabilities.authority must identify one schema-3 playbook session',
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
if (
|
|
986
|
+
repositoryDescriptor === undefined ||
|
|
987
|
+
repositoryDescriptor.get !== undefined ||
|
|
988
|
+
repositoryDescriptor.set !== undefined ||
|
|
989
|
+
!Object.prototype.hasOwnProperty.call(repositoryDescriptor, 'value') ||
|
|
990
|
+
!isPlainObject(repository) ||
|
|
991
|
+
typeof repository.runExclusive !== 'function' ||
|
|
992
|
+
typeof repository.runCohort !== 'function' ||
|
|
993
|
+
typeof repository.runDeferred !== 'function'
|
|
994
|
+
) {
|
|
995
|
+
throw new TypeError(
|
|
996
|
+
'DECIDE schema-3 factory input hostCapabilities.repository must expose runExclusive, runCohort, and runDeferred functions',
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
if (
|
|
1000
|
+
effectLedgerDescriptor === undefined ||
|
|
1001
|
+
effectLedgerDescriptor.get !== undefined ||
|
|
1002
|
+
effectLedgerDescriptor.set !== undefined ||
|
|
1003
|
+
!Object.prototype.hasOwnProperty.call(effectLedgerDescriptor, 'value') ||
|
|
1004
|
+
!isPlainObject(effectLedger) ||
|
|
1005
|
+
typeof effectLedger.snapshot !== 'function' ||
|
|
1006
|
+
typeof effectLedger.writeAhead !== 'function'
|
|
1007
|
+
) {
|
|
1008
|
+
throw new TypeError(
|
|
1009
|
+
'DECIDE schema-3 factory input hostCapabilities.effectLedger must expose snapshot and writeAhead functions',
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
return {
|
|
1013
|
+
configuredOptions: configuredOptions as PlaybookRuntimeOptions,
|
|
1014
|
+
hostCapabilities: {
|
|
1015
|
+
authority:
|
|
1016
|
+
authority as unknown as DecidePlaybookHostCapabilities['authority'],
|
|
1017
|
+
repository: repository as unknown as DecideRepositoryCapability,
|
|
1018
|
+
effectLedger: effectLedger as unknown as PlaybookEffectLedgerCapability,
|
|
1019
|
+
},
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
interface DeferredValue<T> {
|
|
1024
|
+
readonly promise: Promise<T>;
|
|
1025
|
+
resolve(value: T): void;
|
|
1026
|
+
reject(reason: unknown): void;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function deferredValue<T>(): DeferredValue<T> {
|
|
1030
|
+
let resolve!: (value: T) => void;
|
|
1031
|
+
let reject!: (reason: unknown) => void;
|
|
1032
|
+
const promise = new Promise<T>((onResolve, onReject) => {
|
|
1033
|
+
resolve = onResolve;
|
|
1034
|
+
reject = onReject;
|
|
1035
|
+
});
|
|
1036
|
+
return { promise, resolve, reject };
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function hasCompleteUnchangedReceipt(
|
|
1040
|
+
boundary: PlaybookEffectBoundary,
|
|
1041
|
+
): boolean {
|
|
1042
|
+
return boundary.physicalReceipt?.classification === 'unchanged';
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// A corrective call is bound to the exact physical boundary it would repeat.
|
|
1046
|
+
// A failed-state restart replays the whole entry event. Cooperative host
|
|
1047
|
+
// attempts are serialized in ledger order, so the latest durable boundary
|
|
1048
|
+
// identifies the causal host attempt even when a nested or sibling runtime
|
|
1049
|
+
// wrote it; every boundary in that attempt must have a complete unchanged
|
|
1050
|
+
// receipt.
|
|
1051
|
+
// The durable ledger remains the authority in both cases; no process-local
|
|
1052
|
+
// player result or presentation text can make a replay safe.
|
|
1053
|
+
function createAutomaticReplayPolicy(
|
|
1054
|
+
evidence: Schema3AutomaticReplayEvidence,
|
|
1055
|
+
): AutomaticReplayPolicy {
|
|
1056
|
+
const readLedger = (): PlaybookEffectLedger =>
|
|
1057
|
+
assertPlaybookEffectLedger(
|
|
1058
|
+
evidence.effectLedger.snapshot(),
|
|
1059
|
+
'DECIDE automatic-replay effect ledger',
|
|
1060
|
+
);
|
|
1061
|
+
|
|
1062
|
+
return Object.freeze({
|
|
1063
|
+
allowsEmptyOkCorrection(runtimeSessionId: string, callId: string) {
|
|
1064
|
+
const matching = readLedger().boundaries.filter(
|
|
1065
|
+
(boundary) =>
|
|
1066
|
+
boundary.runtimeSessionId === runtimeSessionId &&
|
|
1067
|
+
boundary.callId === callId,
|
|
1068
|
+
);
|
|
1069
|
+
return (
|
|
1070
|
+
matching.length === 1 && hasCompleteUnchangedReceipt(matching[0]!)
|
|
1071
|
+
);
|
|
1072
|
+
},
|
|
1073
|
+
|
|
1074
|
+
allowsFailureStateRetry() {
|
|
1075
|
+
const ledger = readLedger();
|
|
1076
|
+
const latest = ledger.boundaries.at(-1);
|
|
1077
|
+
if (latest === undefined) return false;
|
|
1078
|
+
const attempt = ledger.boundaries.filter(
|
|
1079
|
+
(boundary) => boundary.attemptId === latest.attemptId,
|
|
1080
|
+
);
|
|
1081
|
+
return (
|
|
1082
|
+
attempt.length > 0 && attempt.every(hasCompleteUnchangedReceipt)
|
|
1083
|
+
);
|
|
1084
|
+
},
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
|
|
614
1088
|
function isAbortFailure(error: unknown, signal: AbortSignal): boolean {
|
|
615
1089
|
return signal.aborted && Object.is(error, signal.reason);
|
|
616
1090
|
}
|
|
@@ -766,8 +1240,12 @@ function telemetryPayload(
|
|
|
766
1240
|
state: PlaybookState,
|
|
767
1241
|
event: unknown,
|
|
768
1242
|
context: Record<string, unknown>,
|
|
1243
|
+
hiddenQuestionId?: string,
|
|
769
1244
|
): JsonValue {
|
|
770
|
-
const pendingBossQuestions = pendingQuestionsForState(
|
|
1245
|
+
const pendingBossQuestions = pendingQuestionsForState(
|
|
1246
|
+
state,
|
|
1247
|
+
context,
|
|
1248
|
+
).filter(({ questionId }) => questionId !== hiddenQuestionId);
|
|
771
1249
|
const prior = previousState ?? state;
|
|
772
1250
|
const payload = {
|
|
773
1251
|
from: prior.value,
|
|
@@ -784,10 +1262,56 @@ function telemetryPayload(
|
|
|
784
1262
|
return payload;
|
|
785
1263
|
}
|
|
786
1264
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1265
|
+
type DecidePlaybookRuntime = PlaybookRuntime & {
|
|
1266
|
+
_getNestedBridge(): NestedPlaybookBridge<PlaybookInput>;
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
type DeferredContinuationEnvelope = {
|
|
1270
|
+
readonly roleId: RoleId;
|
|
1271
|
+
readonly playerId?: string;
|
|
1272
|
+
readonly callId: string;
|
|
1273
|
+
readonly result: PlayerResult;
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
interface ActiveDeferredContinuation {
|
|
1277
|
+
readonly operationId: string;
|
|
1278
|
+
readonly effectBoundary: DecideEffectBoundarySeed;
|
|
1279
|
+
readonly result: DeferredValue<PlayerResult>;
|
|
1280
|
+
readonly acknowledged: DeferredValue<PlayerResult>;
|
|
1281
|
+
playerContinuation?: string | false;
|
|
1282
|
+
input?: PlayerInput;
|
|
1283
|
+
playerId?: string;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
interface PendingProposalCohortMember {
|
|
1287
|
+
readonly input: PlayerInput;
|
|
1288
|
+
readonly signal: AbortSignal;
|
|
1289
|
+
readonly callId: string;
|
|
1290
|
+
readonly effectBoundary: DecideEffectBoundarySeed;
|
|
1291
|
+
readonly result: DeferredValue<DeferredContinuationEnvelope>;
|
|
1292
|
+
envelope?: DeferredContinuationEnvelope;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function createDecidePlaybookRuntime(
|
|
1296
|
+
options: PlaybookRuntimeOptions,
|
|
1297
|
+
deferredEffects: Schema3DeferredEffectEvidence,
|
|
1298
|
+
): DecidePlaybookRuntime {
|
|
1299
|
+
const automaticReplayPolicy = createAutomaticReplayPolicy(deferredEffects);
|
|
790
1300
|
const fsmInput = snapshotDecideRuntimeOptions(options);
|
|
1301
|
+
const readEffectLedger = (): PlaybookEffectLedger =>
|
|
1302
|
+
assertPlaybookEffectLedger(
|
|
1303
|
+
deferredEffects.effectLedger.snapshot(),
|
|
1304
|
+
'DECIDE current host effect ledger',
|
|
1305
|
+
);
|
|
1306
|
+
let effectLedgerMirror = readEffectLedger();
|
|
1307
|
+
const acceptedOutcomeConsumer = createAcceptedOutcomeConsumer(
|
|
1308
|
+
(source, acceptedOutcome) =>
|
|
1309
|
+
Object.prototype.hasOwnProperty.call(
|
|
1310
|
+
ACCEPTED_OUTCOME_DECLARATIONS,
|
|
1311
|
+
source,
|
|
1312
|
+
) &&
|
|
1313
|
+
ACCEPTED_OUTCOME_DECLARATIONS[source]?.has(acceptedOutcome) === true,
|
|
1314
|
+
);
|
|
791
1315
|
|
|
792
1316
|
type SessionIdentity = Readonly<PlaybookSession>;
|
|
793
1317
|
|
|
@@ -807,6 +1331,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
807
1331
|
let judgeCallSequence = 0;
|
|
808
1332
|
let playerCallSequence = 0;
|
|
809
1333
|
let playbookCallSequence = 0;
|
|
1334
|
+
let applyCallSequence = 0;
|
|
810
1335
|
let lifecycleStarted = false;
|
|
811
1336
|
let initInFlight: Promise<void> | undefined;
|
|
812
1337
|
let disposed = false;
|
|
@@ -820,6 +1345,35 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
820
1345
|
const activeEmissionCalls = new Set<Promise<void>>();
|
|
821
1346
|
const emissionQueue = new PQueue({ concurrency: 1 });
|
|
822
1347
|
const judgeQueue = new PQueue({ concurrency: 1 });
|
|
1348
|
+
// A proposal cohort completes both semantic callbacks concurrently at the
|
|
1349
|
+
// repository seam. Serialize each whole adjudication/correction transaction
|
|
1350
|
+
// so their durable correction-budget compare-and-swaps cannot race.
|
|
1351
|
+
const semanticCompletionQueue = new PQueue({ concurrency: 1 });
|
|
1352
|
+
const governedOutputsByBoundaryId = new Map<string, PlayerOutput>();
|
|
1353
|
+
const governedFailuresByBoundaryId = new Map<string, Error>();
|
|
1354
|
+
const governedEvidenceByBoundaryId = new Map<
|
|
1355
|
+
string,
|
|
1356
|
+
{ readonly finalText: string; readonly semanticCandidate?: JsonValue }
|
|
1357
|
+
>();
|
|
1358
|
+
const governedReceiptsByBoundaryId = new Map<
|
|
1359
|
+
string,
|
|
1360
|
+
{
|
|
1361
|
+
readonly physicalReceipt: PlaybookRepositoryReceipt;
|
|
1362
|
+
readonly outcomeReceipt: PlaybookRepositoryReceipt;
|
|
1363
|
+
}
|
|
1364
|
+
>();
|
|
1365
|
+
const governedPlayerOutputs = new WeakMap<object, PlayerOutput>();
|
|
1366
|
+
const unresolvedSemanticBoundaryIds = new Set<string>();
|
|
1367
|
+
const appliedControlReceipts = new Map<string, PlaybookControlReceipt>();
|
|
1368
|
+
const pendingProposalCohort = new Map<
|
|
1369
|
+
RoleId,
|
|
1370
|
+
PendingProposalCohortMember
|
|
1371
|
+
>();
|
|
1372
|
+
let activeProposalCohort: Promise<void> | undefined;
|
|
1373
|
+
let completedProposalCohortTurnId: number | undefined;
|
|
1374
|
+
let deferredOperationId: string | undefined;
|
|
1375
|
+
let hiddenDeferredOperationId: string | undefined;
|
|
1376
|
+
let activeDeferredContinuation: ActiveDeferredContinuation | undefined;
|
|
823
1377
|
|
|
824
1378
|
const collectFailure = (failures: unknown[], error: unknown): void => {
|
|
825
1379
|
if (error instanceof AggregateError) {
|
|
@@ -922,6 +1476,11 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
922
1476
|
|
|
923
1477
|
const bindSession = (nextSession: PlaybookSession): SessionIdentity => {
|
|
924
1478
|
const bound = snapshotPlaybookSession(nextSession);
|
|
1479
|
+
if (bound.playbookId !== deferredEffects.authority.playbookId) {
|
|
1480
|
+
throw new TypeError(
|
|
1481
|
+
'DECIDE runtime playbook identity must match its bound schema-3 host authority',
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
925
1484
|
if (bound.roleBindings === undefined) return bound;
|
|
926
1485
|
const actual = Object.keys(bound.roleBindings).sort();
|
|
927
1486
|
const expected = [...ROLE_IDS].sort();
|
|
@@ -1102,6 +1661,273 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1102
1661
|
pendingCall,
|
|
1103
1662
|
});
|
|
1104
1663
|
};
|
|
1664
|
+
const visiblePendingQuestionsForState = (
|
|
1665
|
+
state: PlaybookState,
|
|
1666
|
+
context: Record<string, unknown>,
|
|
1667
|
+
): PendingBossQuestion[] =>
|
|
1668
|
+
pendingQuestionsForState(state, context).filter(
|
|
1669
|
+
({ questionId }) =>
|
|
1670
|
+
questionId !== 'commitCoderProposal' ||
|
|
1671
|
+
hiddenDeferredOperationId === undefined,
|
|
1672
|
+
);
|
|
1673
|
+
const openDeferredOperation = (
|
|
1674
|
+
ledger: PlaybookEffectLedger = effectLedgerMirror,
|
|
1675
|
+
) => {
|
|
1676
|
+
if (sessionIdentity === undefined) return undefined;
|
|
1677
|
+
const open = ledger.logicalOperations.filter(
|
|
1678
|
+
(operation) =>
|
|
1679
|
+
operation.playbookId === sessionIdentity!.playbookId &&
|
|
1680
|
+
operation.runtimeSessionId === sessionIdentity!.sessionId &&
|
|
1681
|
+
operation.logicalReceipt === undefined,
|
|
1682
|
+
);
|
|
1683
|
+
if (open.length > 1) {
|
|
1684
|
+
throw new TypeError(
|
|
1685
|
+
'DECIDE runtime has multiple open deferred logical operations',
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
return open[0];
|
|
1689
|
+
};
|
|
1690
|
+
const runtimeBoundaryIsOwned = (
|
|
1691
|
+
boundary: PlaybookEffectBoundary,
|
|
1692
|
+
): boolean =>
|
|
1693
|
+
sessionIdentity !== undefined &&
|
|
1694
|
+
boundary.playbookId === sessionIdentity.playbookId &&
|
|
1695
|
+
boundary.runtimeSessionId === sessionIdentity.sessionId;
|
|
1696
|
+
const governedOutcomesForBoundary = (
|
|
1697
|
+
boundary: PlaybookEffectBoundary,
|
|
1698
|
+
): Readonly<Record<string, PlaybookSemanticOutcomeSpec>> | undefined => {
|
|
1699
|
+
const outcomes = (
|
|
1700
|
+
DECIDE_OUTCOME_AUTHORITY.governedPlayerStates as Readonly<
|
|
1701
|
+
Record<string, Readonly<Record<string, PlaybookSemanticOutcomeSpec>>>
|
|
1702
|
+
>
|
|
1703
|
+
)[boundary.sourceStateId];
|
|
1704
|
+
if (
|
|
1705
|
+
outcomes === undefined ||
|
|
1706
|
+
!isPlainObject(boundary.sourceOutcomeSchema)
|
|
1707
|
+
) {
|
|
1708
|
+
return undefined;
|
|
1709
|
+
}
|
|
1710
|
+
const authoredGuards = Object.keys(boundary.sourceOutcomeSchema).sort();
|
|
1711
|
+
const governedGuards = Object.keys(outcomes).sort();
|
|
1712
|
+
if (
|
|
1713
|
+
authoredGuards.length !== governedGuards.length ||
|
|
1714
|
+
authoredGuards.some((guard, index) => guard !== governedGuards[index])
|
|
1715
|
+
) {
|
|
1716
|
+
return undefined;
|
|
1717
|
+
}
|
|
1718
|
+
for (const guard of governedGuards) {
|
|
1719
|
+
const description = boundary.sourceOutcomeSchema[guard];
|
|
1720
|
+
if (typeof description !== 'string') return undefined;
|
|
1721
|
+
const authoredFields = [
|
|
1722
|
+
...new Set(requiredFieldsFor(description)),
|
|
1723
|
+
].sort();
|
|
1724
|
+
const governedFields = Object.keys(outcomes[guard]!.fields).sort();
|
|
1725
|
+
if (
|
|
1726
|
+
authoredFields.length !== governedFields.length ||
|
|
1727
|
+
authoredFields.some(
|
|
1728
|
+
(field, index) => field !== governedFields[index],
|
|
1729
|
+
)
|
|
1730
|
+
) {
|
|
1731
|
+
return undefined;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
const dispositions = [
|
|
1735
|
+
...new Set(
|
|
1736
|
+
Object.values(outcomes).map(
|
|
1737
|
+
({ repositoryDisposition }) => repositoryDisposition,
|
|
1738
|
+
),
|
|
1739
|
+
),
|
|
1740
|
+
];
|
|
1741
|
+
const actualDispositions = new Set(boundary.dispositions);
|
|
1742
|
+
return actualDispositions.size === boundary.dispositions.length &&
|
|
1743
|
+
actualDispositions.size === dispositions.length &&
|
|
1744
|
+
dispositions.every((disposition) => actualDispositions.has(disposition))
|
|
1745
|
+
? outcomes
|
|
1746
|
+
: undefined;
|
|
1747
|
+
};
|
|
1748
|
+
const persistedBoundaryReconciliation = (
|
|
1749
|
+
boundary: PlaybookEffectBoundary,
|
|
1750
|
+
ledger: PlaybookEffectLedger,
|
|
1751
|
+
) => {
|
|
1752
|
+
const outcomes = governedOutcomesForBoundary(boundary);
|
|
1753
|
+
if (outcomes === undefined || boundary.semanticCandidate === undefined) {
|
|
1754
|
+
return undefined;
|
|
1755
|
+
}
|
|
1756
|
+
let receipt = boundary.physicalReceipt;
|
|
1757
|
+
let awaitingLogicalReceipt = false;
|
|
1758
|
+
let historicalDeferred = false;
|
|
1759
|
+
const logicalOperation =
|
|
1760
|
+
boundary.logicalOperationId === undefined
|
|
1761
|
+
? undefined
|
|
1762
|
+
: ledger.logicalOperations.find(
|
|
1763
|
+
({ operationId }) =>
|
|
1764
|
+
operationId === boundary.logicalOperationId,
|
|
1765
|
+
);
|
|
1766
|
+
if (
|
|
1767
|
+
boundary.logicalOperationId !== undefined &&
|
|
1768
|
+
logicalOperation === undefined
|
|
1769
|
+
) {
|
|
1770
|
+
return undefined;
|
|
1771
|
+
}
|
|
1772
|
+
if (logicalOperation !== undefined) {
|
|
1773
|
+
if (logicalOperation.boundaryIds.at(-1) !== boundary.boundaryId) {
|
|
1774
|
+
historicalDeferred = true;
|
|
1775
|
+
} else if (logicalOperation.logicalReceipt !== undefined) {
|
|
1776
|
+
receipt = logicalOperation.logicalReceipt;
|
|
1777
|
+
} else if (
|
|
1778
|
+
logicalOperation.pendingQuestion === undefined ||
|
|
1779
|
+
logicalOperation.checkpoint === undefined ||
|
|
1780
|
+
!Object.prototype.hasOwnProperty.call(
|
|
1781
|
+
logicalOperation,
|
|
1782
|
+
'playerContinuation',
|
|
1783
|
+
)
|
|
1784
|
+
) {
|
|
1785
|
+
return undefined;
|
|
1786
|
+
} else {
|
|
1787
|
+
awaitingLogicalReceipt = true;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
try {
|
|
1791
|
+
const reconciliation = reconcilePlaybookSemanticEvidence({
|
|
1792
|
+
outcomes,
|
|
1793
|
+
semanticCandidate: boundary.semanticCandidate,
|
|
1794
|
+
finalText: boundary.finalText,
|
|
1795
|
+
receipt,
|
|
1796
|
+
});
|
|
1797
|
+
if (awaitingLogicalReceipt && reconciliation.status !== 'deferred') {
|
|
1798
|
+
return undefined;
|
|
1799
|
+
}
|
|
1800
|
+
return { reconciliation, historicalDeferred };
|
|
1801
|
+
} catch {
|
|
1802
|
+
return undefined;
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
const boundaryNeedsSemanticReconciliation = (
|
|
1806
|
+
boundary: PlaybookEffectBoundary,
|
|
1807
|
+
ledger: PlaybookEffectLedger,
|
|
1808
|
+
): boolean => {
|
|
1809
|
+
if (!runtimeBoundaryIsOwned(boundary)) return false;
|
|
1810
|
+
if (governedOutcomesForBoundary(boundary) === undefined) return true;
|
|
1811
|
+
const persisted = persistedBoundaryReconciliation(boundary, ledger);
|
|
1812
|
+
if (persisted !== undefined) {
|
|
1813
|
+
if (persisted.reconciliation.status === 'unresolved') return true;
|
|
1814
|
+
if (persisted.historicalDeferred) {
|
|
1815
|
+
return persisted.reconciliation.status !== 'deferred';
|
|
1816
|
+
}
|
|
1817
|
+
if (
|
|
1818
|
+
persisted.reconciliation.status === 'deferred' &&
|
|
1819
|
+
boundary.logicalOperationId === undefined
|
|
1820
|
+
) {
|
|
1821
|
+
return true;
|
|
1822
|
+
}
|
|
1823
|
+
return false;
|
|
1824
|
+
}
|
|
1825
|
+
if (boundary.physicalReceipt === undefined) return true;
|
|
1826
|
+
if (
|
|
1827
|
+
typeof boundary.finalText === 'string' &&
|
|
1828
|
+
boundary.finalText.trim().length > 0
|
|
1829
|
+
) {
|
|
1830
|
+
return true;
|
|
1831
|
+
}
|
|
1832
|
+
return boundary.physicalReceipt.classification !== 'unchanged';
|
|
1833
|
+
};
|
|
1834
|
+
const refreshUnresolvedSemanticReconciliation = (
|
|
1835
|
+
ledger: PlaybookEffectLedger = effectLedgerMirror,
|
|
1836
|
+
): void => {
|
|
1837
|
+
unresolvedSemanticBoundaryIds.clear();
|
|
1838
|
+
for (const boundary of ledger.boundaries) {
|
|
1839
|
+
if (boundaryNeedsSemanticReconciliation(boundary, ledger)) {
|
|
1840
|
+
unresolvedSemanticBoundaryIds.add(boundary.boundaryId);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
const synchronizeDeferredProjection = (
|
|
1845
|
+
ledger: PlaybookEffectLedger = effectLedgerMirror,
|
|
1846
|
+
): void => {
|
|
1847
|
+
if (deferredEffects === undefined) return;
|
|
1848
|
+
effectLedgerMirror = ledger;
|
|
1849
|
+
refreshUnresolvedSemanticReconciliation(ledger);
|
|
1850
|
+
const operation = openDeferredOperation(ledger);
|
|
1851
|
+
if (operation === undefined) {
|
|
1852
|
+
deferredOperationId = undefined;
|
|
1853
|
+
hiddenDeferredOperationId = undefined;
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
deferredOperationId = operation.operationId;
|
|
1857
|
+
hiddenDeferredOperationId =
|
|
1858
|
+
operation.logicalReceipt === undefined &&
|
|
1859
|
+
(operation.checkpointRestorationEligible ||
|
|
1860
|
+
operation.pendingQuestion === undefined)
|
|
1861
|
+
? operation.operationId
|
|
1862
|
+
: undefined;
|
|
1863
|
+
};
|
|
1864
|
+
const hasUnresolvedReconciliation = (): boolean =>
|
|
1865
|
+
hiddenDeferredOperationId !== undefined ||
|
|
1866
|
+
unresolvedSemanticBoundaryIds.size > 0;
|
|
1867
|
+
const refreshReconciliationProjection = (): void => {
|
|
1868
|
+
synchronizeDeferredProjection(readEffectLedger());
|
|
1869
|
+
};
|
|
1870
|
+
const unresolvedEffectEnvelopeIdentities = (): readonly (
|
|
1871
|
+
| { readonly kind: 'boundary'; readonly boundaryId: string }
|
|
1872
|
+
| { readonly kind: 'logical-operation'; readonly operationId: string }
|
|
1873
|
+
)[] => {
|
|
1874
|
+
if (sessionIdentity === undefined) return [];
|
|
1875
|
+
refreshReconciliationProjection();
|
|
1876
|
+
if (!hasUnresolvedReconciliation()) return [];
|
|
1877
|
+
const boundaryIds = new Set(unresolvedSemanticBoundaryIds);
|
|
1878
|
+
const operationIds = new Set<string>();
|
|
1879
|
+
if (hiddenDeferredOperationId !== undefined) {
|
|
1880
|
+
operationIds.add(hiddenDeferredOperationId);
|
|
1881
|
+
}
|
|
1882
|
+
for (const boundaryId of [...boundaryIds]) {
|
|
1883
|
+
const boundary = effectLedgerMirror.boundaries.find(
|
|
1884
|
+
(candidate) => candidate.boundaryId === boundaryId,
|
|
1885
|
+
);
|
|
1886
|
+
const operation =
|
|
1887
|
+
boundary?.logicalOperationId === undefined
|
|
1888
|
+
? undefined
|
|
1889
|
+
: effectLedgerMirror.logicalOperations.find(
|
|
1890
|
+
({ operationId }) =>
|
|
1891
|
+
operationId === boundary.logicalOperationId,
|
|
1892
|
+
);
|
|
1893
|
+
if (operation === undefined) continue;
|
|
1894
|
+
operationIds.add(operation.operationId);
|
|
1895
|
+
for (const memberId of operation.boundaryIds) {
|
|
1896
|
+
boundaryIds.delete(memberId);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
const ordered = [
|
|
1900
|
+
...[...boundaryIds].map((boundaryId) => ({
|
|
1901
|
+
order:
|
|
1902
|
+
effectLedgerMirror.boundaries.find(
|
|
1903
|
+
(candidate) => candidate.boundaryId === boundaryId,
|
|
1904
|
+
)?.sequence ?? Number.MAX_SAFE_INTEGER,
|
|
1905
|
+
value: { kind: 'boundary' as const, boundaryId },
|
|
1906
|
+
})),
|
|
1907
|
+
...[...operationIds].map((operationId) => {
|
|
1908
|
+
const operation = effectLedgerMirror.logicalOperations.find(
|
|
1909
|
+
(candidate) => candidate.operationId === operationId,
|
|
1910
|
+
);
|
|
1911
|
+
return {
|
|
1912
|
+
order:
|
|
1913
|
+
effectLedgerMirror.boundaries.find(
|
|
1914
|
+
({ boundaryId }) =>
|
|
1915
|
+
boundaryId === operation?.boundaryIds[0],
|
|
1916
|
+
)?.sequence ?? Number.MAX_SAFE_INTEGER,
|
|
1917
|
+
value: { kind: 'logical-operation' as const, operationId },
|
|
1918
|
+
};
|
|
1919
|
+
}),
|
|
1920
|
+
].sort((left, right) => left.order - right.order);
|
|
1921
|
+
return deepFreeze(
|
|
1922
|
+
snapshotJsonValue(
|
|
1923
|
+
ordered.map(({ value }) => value),
|
|
1924
|
+
'DECIDE unresolved effect envelope identities',
|
|
1925
|
+
) as unknown as (
|
|
1926
|
+
| { readonly kind: 'boundary'; readonly boundaryId: string }
|
|
1927
|
+
| { readonly kind: 'logical-operation'; readonly operationId: string }
|
|
1928
|
+
)[],
|
|
1929
|
+
);
|
|
1930
|
+
};
|
|
1105
1931
|
const stateIdentity = (state: PlaybookState): { stateId?: string } => {
|
|
1106
1932
|
return state.stateId === undefined ? {} : { stateId: state.stateId };
|
|
1107
1933
|
};
|
|
@@ -1112,11 +1938,25 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1112
1938
|
describedEmission?: (runtimePorts: PlaybookPorts) => Promise<void>,
|
|
1113
1939
|
aborts?: AbortReasonClassifier,
|
|
1114
1940
|
): Promise<void> => {
|
|
1115
|
-
const
|
|
1941
|
+
const trace = createTraceEvent(type, payload, meta);
|
|
1942
|
+
return enqueue(
|
|
1943
|
+
async () => {
|
|
1944
|
+
const runtimePorts = requirePorts();
|
|
1945
|
+
await runtimePorts.emitTelemetry({ topic: TRACE_TOPIC, payload: trace });
|
|
1946
|
+
await describedEmission?.(runtimePorts);
|
|
1947
|
+
},
|
|
1948
|
+
aborts,
|
|
1949
|
+
);
|
|
1950
|
+
};
|
|
1951
|
+
const createTraceEvent = (
|
|
1952
|
+
type: PlaybookTraceType,
|
|
1953
|
+
payload: unknown,
|
|
1954
|
+
meta: { turnId?: number; callId?: string } = {},
|
|
1955
|
+
): PlaybookTraceEvent => {
|
|
1116
1956
|
const identity = requireSessionIdentity();
|
|
1117
1957
|
const jsonPayload = snapshotJsonValue(payload, `trace ${type} payload`);
|
|
1118
|
-
|
|
1119
|
-
schemaVersion:
|
|
1958
|
+
return Object.freeze({
|
|
1959
|
+
schemaVersion: 4,
|
|
1120
1960
|
sessionId: identity.sessionId,
|
|
1121
1961
|
playbookId: identity.playbookId,
|
|
1122
1962
|
rootSessionId: identity.rootSessionId,
|
|
@@ -1134,13 +1974,6 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1134
1974
|
...(meta.callId !== undefined ? { callId: meta.callId } : {}),
|
|
1135
1975
|
payload: jsonPayload,
|
|
1136
1976
|
});
|
|
1137
|
-
return enqueue(
|
|
1138
|
-
async () => {
|
|
1139
|
-
await runtimePorts.emitTelemetry({ topic: TRACE_TOPIC, payload: trace });
|
|
1140
|
-
await describedEmission?.(runtimePorts);
|
|
1141
|
-
},
|
|
1142
|
-
aborts,
|
|
1143
|
-
);
|
|
1144
1977
|
};
|
|
1145
1978
|
const emitTrace = (
|
|
1146
1979
|
type: PlaybookTraceType,
|
|
@@ -1148,19 +1981,51 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1148
1981
|
meta: { turnId?: number; callId?: string } = {},
|
|
1149
1982
|
aborts?: AbortReasonClassifier,
|
|
1150
1983
|
): Promise<void> => enqueueTracedEmission(type, payload, meta, undefined, aborts);
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1984
|
+
const enqueueAcceptedOutcomeEmission = (
|
|
1985
|
+
acceptedOutcome: AcceptedOutcomeReceipt,
|
|
1153
1986
|
state: PlaybookState,
|
|
1987
|
+
aborts?: AbortReasonClassifier,
|
|
1154
1988
|
): Promise<void> => {
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1989
|
+
const message = `→ ${acceptedOutcome.acceptedOutcome}`;
|
|
1990
|
+
const acceptedTrace = createTraceEvent(
|
|
1991
|
+
'outcome.accepted',
|
|
1992
|
+
acceptedOutcome,
|
|
1993
|
+
{ turnId: currentTurnId },
|
|
1157
1994
|
);
|
|
1158
|
-
|
|
1995
|
+
const statusTrace = createTraceEvent(
|
|
1159
1996
|
'status.emitted',
|
|
1160
|
-
{
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1997
|
+
{ stateId: acceptedOutcome.target, message, state },
|
|
1998
|
+
{ turnId: currentTurnId },
|
|
1999
|
+
);
|
|
2000
|
+
return enqueue(
|
|
2001
|
+
async () => {
|
|
2002
|
+
const runtimePorts = requirePorts();
|
|
2003
|
+
await runtimePorts.emitTelemetry({
|
|
2004
|
+
topic: TRACE_TOPIC,
|
|
2005
|
+
payload: acceptedTrace,
|
|
2006
|
+
});
|
|
2007
|
+
await runtimePorts.emitTelemetry({
|
|
2008
|
+
topic: TRACE_TOPIC,
|
|
2009
|
+
payload: statusTrace,
|
|
2010
|
+
});
|
|
2011
|
+
await runtimePorts.emitStatus(message);
|
|
2012
|
+
},
|
|
2013
|
+
aborts,
|
|
2014
|
+
);
|
|
2015
|
+
};
|
|
2016
|
+
const emitBoundaryStatus = async (
|
|
2017
|
+
message: string,
|
|
2018
|
+
state: PlaybookState,
|
|
2019
|
+
): Promise<void> => {
|
|
2020
|
+
const bossRelevantStateIds = state.activeStateIds.filter((stateId) =>
|
|
2021
|
+
STATUS_STATE_IDS.has(stateId),
|
|
2022
|
+
);
|
|
2023
|
+
await enqueueTracedEmission(
|
|
2024
|
+
'status.emitted',
|
|
2025
|
+
{
|
|
2026
|
+
...(bossRelevantStateIds.length === 1
|
|
2027
|
+
? { stateId: bossRelevantStateIds[0] }
|
|
2028
|
+
: {}),
|
|
1164
2029
|
message,
|
|
1165
2030
|
state,
|
|
1166
2031
|
},
|
|
@@ -1287,11 +2152,11 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1287
2152
|
const runPlayerCall = async (
|
|
1288
2153
|
input: PlayerInput,
|
|
1289
2154
|
signal: AbortSignal,
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
2155
|
+
continuation?: {
|
|
2156
|
+
readonly resume?: string | false;
|
|
2157
|
+
readonly callId: string;
|
|
2158
|
+
},
|
|
2159
|
+
): Promise<DeferredContinuationEnvelope> => {
|
|
1295
2160
|
const aborts = abortReasonClassifier(signal);
|
|
1296
2161
|
if (!ROLE_ID_SET.has(input.role)) {
|
|
1297
2162
|
throw new TypeError(
|
|
@@ -1305,12 +2170,17 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1305
2170
|
let resume: PlayerCallOptions['resume'];
|
|
1306
2171
|
try {
|
|
1307
2172
|
signal.throwIfAborted();
|
|
1308
|
-
resume =
|
|
2173
|
+
resume =
|
|
2174
|
+
continuation !== undefined &&
|
|
2175
|
+
Object.prototype.hasOwnProperty.call(continuation, 'resume')
|
|
2176
|
+
? continuation.resume!
|
|
2177
|
+
: selectPlayerResume(roleId, playerId);
|
|
1309
2178
|
} catch (error) {
|
|
1310
2179
|
latchControlPlaneError(error, signal);
|
|
1311
2180
|
throw error;
|
|
1312
2181
|
}
|
|
1313
|
-
const callId =
|
|
2182
|
+
const callId =
|
|
2183
|
+
continuation?.callId ?? `player-${++playerCallSequence}`;
|
|
1314
2184
|
const identity = {
|
|
1315
2185
|
stateId: input.stateId,
|
|
1316
2186
|
sourceItem: input.sourceItem,
|
|
@@ -1434,6 +2304,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1434
2304
|
return {
|
|
1435
2305
|
roleId,
|
|
1436
2306
|
...(playerId === undefined ? {} : { playerId }),
|
|
2307
|
+
callId,
|
|
1437
2308
|
result,
|
|
1438
2309
|
};
|
|
1439
2310
|
} finally {
|
|
@@ -1441,15 +2312,862 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1441
2312
|
}
|
|
1442
2313
|
};
|
|
1443
2314
|
|
|
2315
|
+
const governedBoundarySeed = (
|
|
2316
|
+
input: PlayerInput,
|
|
2317
|
+
callId: string,
|
|
2318
|
+
): DecideEffectBoundarySeed => {
|
|
2319
|
+
const outcomes = governedOutcomesFor(input);
|
|
2320
|
+
if (
|
|
2321
|
+
currentTurnId === undefined ||
|
|
2322
|
+
!Number.isSafeInteger(currentTurnId) ||
|
|
2323
|
+
currentTurnId <= 0
|
|
2324
|
+
) {
|
|
2325
|
+
throw new Error(
|
|
2326
|
+
'DECIDE governed player call requires an active positive turn id',
|
|
2327
|
+
);
|
|
2328
|
+
}
|
|
2329
|
+
return {
|
|
2330
|
+
boundaryId: randomUUID(),
|
|
2331
|
+
runtimeSessionId: requireSessionIdentity().sessionId,
|
|
2332
|
+
turnId: currentTurnId,
|
|
2333
|
+
callId,
|
|
2334
|
+
roleId: input.role,
|
|
2335
|
+
sourceStateId: input.stateId,
|
|
2336
|
+
sourceOutcomeSchema: snapshotJsonValue(
|
|
2337
|
+
input.result,
|
|
2338
|
+
'DECIDE governed player source outcome schema',
|
|
2339
|
+
),
|
|
2340
|
+
dispositions: [
|
|
2341
|
+
...new Set(
|
|
2342
|
+
Object.values(outcomes).map(
|
|
2343
|
+
({ repositoryDisposition }) => repositoryDisposition,
|
|
2344
|
+
),
|
|
2345
|
+
),
|
|
2346
|
+
],
|
|
2347
|
+
correctionBudget: { limit: 1, spent: false },
|
|
2348
|
+
};
|
|
2349
|
+
};
|
|
2350
|
+
|
|
2351
|
+
const unresolvedGovernedEvidence = (
|
|
2352
|
+
boundaryId: string,
|
|
2353
|
+
reason: string,
|
|
2354
|
+
error?: unknown,
|
|
2355
|
+
): void => {
|
|
2356
|
+
governedFailuresByBoundaryId.set(
|
|
2357
|
+
boundaryId,
|
|
2358
|
+
error instanceof Error
|
|
2359
|
+
? error
|
|
2360
|
+
: new Error(`DECIDE governed outcome remains unresolved: ${reason}`),
|
|
2361
|
+
);
|
|
2362
|
+
};
|
|
2363
|
+
|
|
2364
|
+
const spendSemanticCorrectionBudget = async (
|
|
2365
|
+
boundary: PlaybookEffectBoundary,
|
|
2366
|
+
receipt: PlaybookRepositoryReceipt,
|
|
2367
|
+
finalText: string,
|
|
2368
|
+
semanticCandidate: JsonValue | undefined,
|
|
2369
|
+
): Promise<boolean> => {
|
|
2370
|
+
const ledger = readEffectLedger();
|
|
2371
|
+
const current = ledger.boundaries.find(
|
|
2372
|
+
({ boundaryId }) => boundaryId === boundary.boundaryId,
|
|
2373
|
+
);
|
|
2374
|
+
if (
|
|
2375
|
+
current === undefined ||
|
|
2376
|
+
current.correctionBudget.limit !== 1 ||
|
|
2377
|
+
current.correctionBudget.spent
|
|
2378
|
+
) {
|
|
2379
|
+
return false;
|
|
2380
|
+
}
|
|
2381
|
+
if (current.finalText !== undefined && current.finalText !== finalText) {
|
|
2382
|
+
throw new TypeError(
|
|
2383
|
+
'DECIDE correction budget conflicts with retained finalText',
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
if (
|
|
2387
|
+
current.physicalReceipt !== undefined &&
|
|
2388
|
+
stableJson(current.physicalReceipt, 'DECIDE retained receipt') !==
|
|
2389
|
+
stableJson(receipt, 'DECIDE correction receipt')
|
|
2390
|
+
) {
|
|
2391
|
+
throw new TypeError(
|
|
2392
|
+
'DECIDE correction budget conflicts with retained repository receipt',
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
if (
|
|
2396
|
+
semanticCandidate !== undefined &&
|
|
2397
|
+
current.semanticCandidate !== undefined &&
|
|
2398
|
+
stableJson(current.semanticCandidate, 'DECIDE retained candidate') !==
|
|
2399
|
+
stableJson(semanticCandidate, 'DECIDE correction candidate')
|
|
2400
|
+
) {
|
|
2401
|
+
throw new TypeError(
|
|
2402
|
+
'DECIDE correction budget conflicts with retained semantic candidate',
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
const next: PlaybookEffectBoundary = {
|
|
2406
|
+
...current,
|
|
2407
|
+
...(receipt.after === undefined ? {} : { after: receipt.after }),
|
|
2408
|
+
physicalReceipt: receipt,
|
|
2409
|
+
finalText,
|
|
2410
|
+
...(semanticCandidate === undefined ? {} : { semanticCandidate }),
|
|
2411
|
+
correctionBudget: { limit: 1, spent: true },
|
|
2412
|
+
};
|
|
2413
|
+
const cohortMembers =
|
|
2414
|
+
current.cohortId === undefined
|
|
2415
|
+
? [current]
|
|
2416
|
+
: ledger.boundaries.filter(
|
|
2417
|
+
({ cohortId }) => cohortId === current.cohortId,
|
|
2418
|
+
);
|
|
2419
|
+
if (cohortMembers.length === 0) {
|
|
2420
|
+
throw new TypeError(
|
|
2421
|
+
'DECIDE correction boundary lost its repository cohort',
|
|
2422
|
+
);
|
|
2423
|
+
}
|
|
2424
|
+
const replacements = cohortMembers.map((member) => ({
|
|
2425
|
+
expected: member,
|
|
2426
|
+
next:
|
|
2427
|
+
member.boundaryId === current.boundaryId
|
|
2428
|
+
? next
|
|
2429
|
+
: {
|
|
2430
|
+
...member,
|
|
2431
|
+
...(receipt.after === undefined
|
|
2432
|
+
? {}
|
|
2433
|
+
: { after: receipt.after }),
|
|
2434
|
+
physicalReceipt: receipt,
|
|
2435
|
+
},
|
|
2436
|
+
})) as [
|
|
2437
|
+
{
|
|
2438
|
+
readonly expected: PlaybookEffectBoundary;
|
|
2439
|
+
readonly next: PlaybookEffectBoundary;
|
|
2440
|
+
},
|
|
2441
|
+
...{
|
|
2442
|
+
readonly expected: PlaybookEffectBoundary;
|
|
2443
|
+
readonly next: PlaybookEffectBoundary;
|
|
2444
|
+
}[],
|
|
2445
|
+
];
|
|
2446
|
+
const acknowledged = assertPlaybookEffectLedger(
|
|
2447
|
+
await deferredEffects.effectLedger.writeAhead([
|
|
2448
|
+
{
|
|
2449
|
+
kind: 'replace-boundaries',
|
|
2450
|
+
// A cohort's shared receipt must become visible for every member in
|
|
2451
|
+
// one ledger revision. Publishing only this member's correction
|
|
2452
|
+
// spend would transiently create an invalid half-complete cohort.
|
|
2453
|
+
replacements,
|
|
2454
|
+
},
|
|
2455
|
+
]),
|
|
2456
|
+
'DECIDE semantic correction budget acknowledgement',
|
|
2457
|
+
);
|
|
2458
|
+
synchronizeDeferredProjection(acknowledged);
|
|
2459
|
+
const spent = acknowledged.boundaries.find(
|
|
2460
|
+
({ boundaryId }) => boundaryId === boundary.boundaryId,
|
|
2461
|
+
);
|
|
2462
|
+
if (
|
|
2463
|
+
spent === undefined ||
|
|
2464
|
+
stableJson(spent, 'DECIDE acknowledged correction boundary') !==
|
|
2465
|
+
stableJson(next, 'DECIDE expected correction boundary')
|
|
2466
|
+
) {
|
|
2467
|
+
throw new TypeError(
|
|
2468
|
+
'DECIDE semantic correction budget spend was not acknowledged exactly',
|
|
2469
|
+
);
|
|
2470
|
+
}
|
|
2471
|
+
return true;
|
|
2472
|
+
};
|
|
2473
|
+
|
|
2474
|
+
const completionEvidenceFor = (
|
|
2475
|
+
input: PlayerInput,
|
|
2476
|
+
roleId: RoleId,
|
|
2477
|
+
playerId: string | undefined,
|
|
2478
|
+
signal: AbortSignal,
|
|
2479
|
+
operationId?: string,
|
|
2480
|
+
) =>
|
|
2481
|
+
async (
|
|
2482
|
+
completion: DecideRepositoryCompletion<PlayerResult>,
|
|
2483
|
+
): Promise<DecideRepositoryCompletionEvidence> => {
|
|
2484
|
+
const queued = await semanticCompletionQueue.add(async () => {
|
|
2485
|
+
const { boundary, operation } = completion;
|
|
2486
|
+
const outcomes = governedOutcomesFor(input);
|
|
2487
|
+
const expectedDispositions = [
|
|
2488
|
+
...new Set(
|
|
2489
|
+
Object.values(outcomes).map(
|
|
2490
|
+
({ repositoryDisposition }) => repositoryDisposition,
|
|
2491
|
+
),
|
|
2492
|
+
),
|
|
2493
|
+
];
|
|
2494
|
+
const session = requireSessionIdentity();
|
|
2495
|
+
if (
|
|
2496
|
+
boundary.playbookId !== session.playbookId ||
|
|
2497
|
+
boundary.runtimeSessionId !== session.sessionId ||
|
|
2498
|
+
boundary.turnId !== currentTurnId ||
|
|
2499
|
+
boundary.roleId !== roleId ||
|
|
2500
|
+
(completion.roleId !== undefined && completion.roleId !== roleId) ||
|
|
2501
|
+
boundary.sourceStateId !== input.stateId ||
|
|
2502
|
+
stableJson(
|
|
2503
|
+
boundary.dispositions,
|
|
2504
|
+
'DECIDE boundary dispositions',
|
|
2505
|
+
) !==
|
|
2506
|
+
stableJson(
|
|
2507
|
+
expectedDispositions,
|
|
2508
|
+
'DECIDE authority dispositions',
|
|
2509
|
+
) ||
|
|
2510
|
+
stableJson(
|
|
2511
|
+
boundary.sourceOutcomeSchema,
|
|
2512
|
+
'DECIDE boundary source schema',
|
|
2513
|
+
) !==
|
|
2514
|
+
stableJson(input.result, 'DECIDE authored source schema')
|
|
2515
|
+
) {
|
|
2516
|
+
throw new TypeError(
|
|
2517
|
+
'DECIDE governed semantic reconciliation source schema changed',
|
|
2518
|
+
);
|
|
2519
|
+
}
|
|
2520
|
+
governedReceiptsByBoundaryId.set(boundary.boundaryId, {
|
|
2521
|
+
physicalReceipt: completion.receipt,
|
|
2522
|
+
outcomeReceipt: completion.outcomeReceipt,
|
|
2523
|
+
});
|
|
2524
|
+
if (
|
|
2525
|
+
operation.status !== 'fulfilled' ||
|
|
2526
|
+
operation.value.status !== 'ok' ||
|
|
2527
|
+
isEmptyFinalText(operation.value.finalText)
|
|
2528
|
+
) {
|
|
2529
|
+
const incomplete =
|
|
2530
|
+
operation.status === 'fulfilled' &&
|
|
2531
|
+
operation.value.status === 'ok' &&
|
|
2532
|
+
operation.value.finalText !== undefined
|
|
2533
|
+
? { finalText: operation.value.finalText }
|
|
2534
|
+
: {};
|
|
2535
|
+
if (
|
|
2536
|
+
operation.status === 'fulfilled' &&
|
|
2537
|
+
operation.value.status === 'ok' &&
|
|
2538
|
+
operation.value.finalText !== undefined
|
|
2539
|
+
) {
|
|
2540
|
+
governedEvidenceByBoundaryId.set(boundary.boundaryId, {
|
|
2541
|
+
finalText: operation.value.finalText,
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2544
|
+
return operationId === undefined
|
|
2545
|
+
? incomplete
|
|
2546
|
+
: { ...incomplete, unresolved: true as const };
|
|
2547
|
+
}
|
|
2548
|
+
const finalText = operation.value.finalText!;
|
|
2549
|
+
let raw: string;
|
|
2550
|
+
try {
|
|
2551
|
+
raw = await callJudge(
|
|
2552
|
+
buildAdjudicatorPrompt(input, finalText),
|
|
2553
|
+
signal,
|
|
2554
|
+
'player-output-adjudication',
|
|
2555
|
+
input.stateId,
|
|
2556
|
+
);
|
|
2557
|
+
} catch (error) {
|
|
2558
|
+
unresolvedGovernedEvidence(
|
|
2559
|
+
boundary.boundaryId,
|
|
2560
|
+
'judge transport failed',
|
|
2561
|
+
error,
|
|
2562
|
+
);
|
|
2563
|
+
governedEvidenceByBoundaryId.set(boundary.boundaryId, { finalText });
|
|
2564
|
+
return { finalText, unresolved: true as const };
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
let candidate: unknown;
|
|
2568
|
+
let retainedCandidate: JsonValue | undefined;
|
|
2569
|
+
let reconciliation:
|
|
2570
|
+
| ReturnType<typeof reconcilePlaybookSemanticEvidence>
|
|
2571
|
+
| undefined;
|
|
2572
|
+
let structuralError:
|
|
2573
|
+
| PlaybookSemanticCandidateStructureError
|
|
2574
|
+
| undefined;
|
|
2575
|
+
const retainCandidate = (value: unknown): void => {
|
|
2576
|
+
try {
|
|
2577
|
+
retainedCandidate = snapshotJsonValue(
|
|
2578
|
+
value,
|
|
2579
|
+
'DECIDE recoverable governed semantic candidate',
|
|
2580
|
+
);
|
|
2581
|
+
} catch {
|
|
2582
|
+
// A non-detachable reply still retains presentation and receipt.
|
|
2583
|
+
}
|
|
2584
|
+
};
|
|
2585
|
+
try {
|
|
2586
|
+
candidate = parseGovernedSemanticCandidate(raw);
|
|
2587
|
+
retainCandidate(candidate);
|
|
2588
|
+
reconciliation = reconcilePlaybookSemanticEvidence({
|
|
2589
|
+
outcomes,
|
|
2590
|
+
semanticCandidate: candidate,
|
|
2591
|
+
finalText,
|
|
2592
|
+
receipt: completion.outcomeReceipt,
|
|
2593
|
+
});
|
|
2594
|
+
} catch (error) {
|
|
2595
|
+
if (!(error instanceof PlaybookSemanticCandidateStructureError)) {
|
|
2596
|
+
throw error;
|
|
2597
|
+
}
|
|
2598
|
+
structuralError = error;
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
if (structuralError !== undefined) {
|
|
2602
|
+
const spent = await spendSemanticCorrectionBudget(
|
|
2603
|
+
boundary,
|
|
2604
|
+
completion.receipt,
|
|
2605
|
+
finalText,
|
|
2606
|
+
retainedCandidate,
|
|
2607
|
+
);
|
|
2608
|
+
if (!spent || signal.aborted) {
|
|
2609
|
+
unresolvedGovernedEvidence(
|
|
2610
|
+
boundary.boundaryId,
|
|
2611
|
+
'semantic correction budget is unavailable',
|
|
2612
|
+
signal.aborted ? signal.reason : undefined,
|
|
2613
|
+
);
|
|
2614
|
+
governedEvidenceByBoundaryId.set(boundary.boundaryId, {
|
|
2615
|
+
finalText,
|
|
2616
|
+
...(retainedCandidate === undefined
|
|
2617
|
+
? {}
|
|
2618
|
+
: { semanticCandidate: retainedCandidate }),
|
|
2619
|
+
});
|
|
2620
|
+
return {
|
|
2621
|
+
finalText,
|
|
2622
|
+
...(retainedCandidate === undefined
|
|
2623
|
+
? {}
|
|
2624
|
+
: { semanticCandidate: retainedCandidate }),
|
|
2625
|
+
unresolved: true as const,
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
let correctiveRaw: string;
|
|
2629
|
+
try {
|
|
2630
|
+
correctiveRaw = await callJudge(
|
|
2631
|
+
buildAdjudicatorPrompt(input, finalText, {
|
|
2632
|
+
reply: raw,
|
|
2633
|
+
error: structuralError.message,
|
|
2634
|
+
}),
|
|
2635
|
+
signal,
|
|
2636
|
+
'player-output-adjudication',
|
|
2637
|
+
input.stateId,
|
|
2638
|
+
);
|
|
2639
|
+
} catch (error) {
|
|
2640
|
+
unresolvedGovernedEvidence(
|
|
2641
|
+
boundary.boundaryId,
|
|
2642
|
+
'corrective judge failed',
|
|
2643
|
+
error,
|
|
2644
|
+
);
|
|
2645
|
+
governedEvidenceByBoundaryId.set(boundary.boundaryId, {
|
|
2646
|
+
finalText,
|
|
2647
|
+
...(retainedCandidate === undefined
|
|
2648
|
+
? {}
|
|
2649
|
+
: { semanticCandidate: retainedCandidate }),
|
|
2650
|
+
});
|
|
2651
|
+
return {
|
|
2652
|
+
finalText,
|
|
2653
|
+
...(retainedCandidate === undefined
|
|
2654
|
+
? {}
|
|
2655
|
+
: { semanticCandidate: retainedCandidate }),
|
|
2656
|
+
unresolved: true as const,
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
try {
|
|
2660
|
+
candidate = parseGovernedSemanticCandidate(correctiveRaw);
|
|
2661
|
+
retainCandidate(candidate);
|
|
2662
|
+
reconciliation = reconcilePlaybookSemanticEvidence({
|
|
2663
|
+
outcomes,
|
|
2664
|
+
semanticCandidate: candidate,
|
|
2665
|
+
finalText,
|
|
2666
|
+
receipt: completion.outcomeReceipt,
|
|
2667
|
+
});
|
|
2668
|
+
} catch (error) {
|
|
2669
|
+
if (!(error instanceof PlaybookSemanticCandidateStructureError)) {
|
|
2670
|
+
throw error;
|
|
2671
|
+
}
|
|
2672
|
+
unresolvedGovernedEvidence(
|
|
2673
|
+
boundary.boundaryId,
|
|
2674
|
+
'corrective semantic candidate is invalid',
|
|
2675
|
+
);
|
|
2676
|
+
governedEvidenceByBoundaryId.set(boundary.boundaryId, {
|
|
2677
|
+
finalText,
|
|
2678
|
+
...(retainedCandidate === undefined
|
|
2679
|
+
? {}
|
|
2680
|
+
: { semanticCandidate: retainedCandidate }),
|
|
2681
|
+
});
|
|
2682
|
+
return {
|
|
2683
|
+
finalText,
|
|
2684
|
+
...(retainedCandidate === undefined
|
|
2685
|
+
? {}
|
|
2686
|
+
: { semanticCandidate: retainedCandidate }),
|
|
2687
|
+
unresolved: true as const,
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
if (reconciliation === undefined) {
|
|
2693
|
+
throw new Error(
|
|
2694
|
+
'DECIDE semantic reconciliation produced no decision',
|
|
2695
|
+
);
|
|
2696
|
+
}
|
|
2697
|
+
const semanticCandidate = snapshotJsonValue(
|
|
2698
|
+
reconciliation.evidence.semanticCandidate,
|
|
2699
|
+
'DECIDE governed semantic candidate',
|
|
2700
|
+
);
|
|
2701
|
+
governedEvidenceByBoundaryId.set(boundary.boundaryId, {
|
|
2702
|
+
finalText,
|
|
2703
|
+
semanticCandidate,
|
|
2704
|
+
});
|
|
2705
|
+
if (reconciliation.status === 'unresolved') {
|
|
2706
|
+
unresolvedGovernedEvidence(
|
|
2707
|
+
boundary.boundaryId,
|
|
2708
|
+
reconciliation.reason,
|
|
2709
|
+
);
|
|
2710
|
+
return { finalText, semanticCandidate, unresolved: true as const };
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
const output = reconciliation.output as unknown as PlayerOutput;
|
|
2714
|
+
governedOutputsByBoundaryId.set(boundary.boundaryId, output);
|
|
2715
|
+
if (reconciliation.status !== 'deferred') {
|
|
2716
|
+
return { finalText, semanticCandidate };
|
|
2717
|
+
}
|
|
2718
|
+
if (
|
|
2719
|
+
output.guard !== 'needsBossReply' ||
|
|
2720
|
+
typeof output.question !== 'string' ||
|
|
2721
|
+
output.question.trim() === ''
|
|
2722
|
+
) {
|
|
2723
|
+
throw new TypeError(
|
|
2724
|
+
'DECIDE deferred outcome must carry one exact Boss question',
|
|
2725
|
+
);
|
|
2726
|
+
}
|
|
2727
|
+
const pendingQuestion: PlaybookPendingBossQuestion = {
|
|
2728
|
+
questionId: input.stateId,
|
|
2729
|
+
asker: { kind: 'role', roleId },
|
|
2730
|
+
question: output.question,
|
|
2731
|
+
sourceItem: input.sourceItem,
|
|
2732
|
+
};
|
|
2733
|
+
return {
|
|
2734
|
+
finalText,
|
|
2735
|
+
semanticCandidate,
|
|
2736
|
+
deferred: {
|
|
2737
|
+
operationId: operationId ?? randomUUID(),
|
|
2738
|
+
pendingQuestion,
|
|
2739
|
+
playerContinuation: snapshotJsonValue(
|
|
2740
|
+
selectPlayerResume(roleId, playerId),
|
|
2741
|
+
'DECIDE deferred player continuation',
|
|
2742
|
+
),
|
|
2743
|
+
},
|
|
2744
|
+
};
|
|
2745
|
+
});
|
|
2746
|
+
if (queued === undefined) {
|
|
2747
|
+
throw new Error('DECIDE semantic completion produced no evidence');
|
|
2748
|
+
}
|
|
2749
|
+
return queued;
|
|
2750
|
+
};
|
|
2751
|
+
|
|
2752
|
+
const acknowledgeGovernedPlayerResult = (
|
|
2753
|
+
value: DecideRepositoryExclusiveResult<PlayerResult>,
|
|
2754
|
+
boundaryId: string,
|
|
2755
|
+
): PlayerResult => {
|
|
2756
|
+
const ledger = assertPlaybookEffectLedger(
|
|
2757
|
+
value.effectLedger,
|
|
2758
|
+
'DECIDE repository settlement effect ledger',
|
|
2759
|
+
);
|
|
2760
|
+
const completed = ledger.boundaries.find(
|
|
2761
|
+
(candidate) => candidate.boundaryId === boundaryId,
|
|
2762
|
+
);
|
|
2763
|
+
if (
|
|
2764
|
+
completed === undefined ||
|
|
2765
|
+
completed.physicalReceipt === undefined ||
|
|
2766
|
+
stableJson(completed.physicalReceipt, 'DECIDE completed receipt') !==
|
|
2767
|
+
stableJson(value.receipt, 'DECIDE acknowledged receipt')
|
|
2768
|
+
) {
|
|
2769
|
+
throw new TypeError(
|
|
2770
|
+
'DECIDE repository settlement did not acknowledge its completed boundary',
|
|
2771
|
+
);
|
|
2772
|
+
}
|
|
2773
|
+
const expectedEvidence = governedEvidenceByBoundaryId.get(boundaryId);
|
|
2774
|
+
const expectedReceipts = governedReceiptsByBoundaryId.get(boundaryId);
|
|
2775
|
+
if (
|
|
2776
|
+
expectedReceipts !== undefined &&
|
|
2777
|
+
stableJson(
|
|
2778
|
+
completed.physicalReceipt,
|
|
2779
|
+
'DECIDE completed physical receipt',
|
|
2780
|
+
) !==
|
|
2781
|
+
stableJson(
|
|
2782
|
+
expectedReceipts.physicalReceipt,
|
|
2783
|
+
'DECIDE reconciled physical receipt',
|
|
2784
|
+
)
|
|
2785
|
+
) {
|
|
2786
|
+
throw new TypeError(
|
|
2787
|
+
'DECIDE repository settlement changed the physical receipt used during reconciliation',
|
|
2788
|
+
);
|
|
2789
|
+
}
|
|
2790
|
+
if (
|
|
2791
|
+
expectedEvidence !== undefined &&
|
|
2792
|
+
(completed.finalText !== expectedEvidence.finalText ||
|
|
2793
|
+
(expectedEvidence.semanticCandidate === undefined
|
|
2794
|
+
? completed.semanticCandidate !== undefined
|
|
2795
|
+
: completed.semanticCandidate === undefined ||
|
|
2796
|
+
stableJson(
|
|
2797
|
+
completed.semanticCandidate,
|
|
2798
|
+
'DECIDE completed semantic candidate',
|
|
2799
|
+
) !==
|
|
2800
|
+
stableJson(
|
|
2801
|
+
expectedEvidence.semanticCandidate,
|
|
2802
|
+
'DECIDE expected semantic candidate',
|
|
2803
|
+
)))
|
|
2804
|
+
) {
|
|
2805
|
+
throw new TypeError(
|
|
2806
|
+
'DECIDE repository settlement did not acknowledge its exact governed evidence',
|
|
2807
|
+
);
|
|
2808
|
+
}
|
|
2809
|
+
governedEvidenceByBoundaryId.delete(boundaryId);
|
|
2810
|
+
governedReceiptsByBoundaryId.delete(boundaryId);
|
|
2811
|
+
synchronizeDeferredProjection(ledger);
|
|
2812
|
+
if (value.operation.status === 'rejected') {
|
|
2813
|
+
throw value.operation.reason;
|
|
2814
|
+
}
|
|
2815
|
+
const result = validatePlayerResult(value.operation.value);
|
|
2816
|
+
const output = governedOutputsByBoundaryId.get(boundaryId);
|
|
2817
|
+
const failure = governedFailuresByBoundaryId.get(boundaryId);
|
|
2818
|
+
if (expectedReceipts !== undefined) {
|
|
2819
|
+
const continued =
|
|
2820
|
+
'status' in value && value.status === 'continued'
|
|
2821
|
+
? (value as DecideRepositoryDeferredContinuationResult<PlayerResult>)
|
|
2822
|
+
: undefined;
|
|
2823
|
+
const acknowledgedOutcomeReceipt =
|
|
2824
|
+
continued === undefined ? value.receipt : continued.logicalReceipt;
|
|
2825
|
+
if (
|
|
2826
|
+
acknowledgedOutcomeReceipt !== undefined &&
|
|
2827
|
+
stableJson(
|
|
2828
|
+
acknowledgedOutcomeReceipt,
|
|
2829
|
+
'DECIDE acknowledged outcome receipt',
|
|
2830
|
+
) !==
|
|
2831
|
+
stableJson(
|
|
2832
|
+
expectedReceipts.outcomeReceipt,
|
|
2833
|
+
'DECIDE reconciled outcome receipt',
|
|
2834
|
+
)
|
|
2835
|
+
) {
|
|
2836
|
+
throw new TypeError(
|
|
2837
|
+
'DECIDE repository settlement changed the outcome receipt used during reconciliation',
|
|
2838
|
+
);
|
|
2839
|
+
}
|
|
2840
|
+
if (
|
|
2841
|
+
continued !== undefined &&
|
|
2842
|
+
output !== undefined &&
|
|
2843
|
+
output.guard !== 'needsBossReply' &&
|
|
2844
|
+
acknowledgedOutcomeReceipt === undefined
|
|
2845
|
+
) {
|
|
2846
|
+
throw new TypeError(
|
|
2847
|
+
'DECIDE completed deferred continuation omitted its reconciled logical receipt',
|
|
2848
|
+
);
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
governedOutputsByBoundaryId.delete(boundaryId);
|
|
2852
|
+
governedFailuresByBoundaryId.delete(boundaryId);
|
|
2853
|
+
const linkedOperationId = completed.logicalOperationId;
|
|
2854
|
+
if (value.deferredStatus === 'unresolved') {
|
|
2855
|
+
if (linkedOperationId === undefined) {
|
|
2856
|
+
throw new TypeError(
|
|
2857
|
+
'DECIDE unresolved deferred settlement omitted its logical operation',
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
deferredOperationId = linkedOperationId;
|
|
2861
|
+
hiddenDeferredOperationId = linkedOperationId;
|
|
2862
|
+
} else if (value.deferredStatus === 'bound') {
|
|
2863
|
+
if (linkedOperationId === undefined) {
|
|
2864
|
+
throw new TypeError(
|
|
2865
|
+
'DECIDE bound deferred settlement omitted its logical operation',
|
|
2866
|
+
);
|
|
2867
|
+
}
|
|
2868
|
+
deferredOperationId = linkedOperationId;
|
|
2869
|
+
hiddenDeferredOperationId = undefined;
|
|
2870
|
+
} else if (
|
|
2871
|
+
'status' in value &&
|
|
2872
|
+
value.status === 'continued' &&
|
|
2873
|
+
(value as { readonly logicalReceipt?: PlaybookRepositoryReceipt })
|
|
2874
|
+
.logicalReceipt === undefined &&
|
|
2875
|
+
linkedOperationId !== undefined
|
|
2876
|
+
) {
|
|
2877
|
+
deferredOperationId = linkedOperationId;
|
|
2878
|
+
hiddenDeferredOperationId = linkedOperationId;
|
|
2879
|
+
} else if (linkedOperationId !== undefined) {
|
|
2880
|
+
deferredOperationId = undefined;
|
|
2881
|
+
hiddenDeferredOperationId = undefined;
|
|
2882
|
+
}
|
|
2883
|
+
if (failure !== undefined) throw failure;
|
|
2884
|
+
if (value.deferredStatus === 'unresolved') {
|
|
2885
|
+
throw new Error(
|
|
2886
|
+
'DECIDE deferred repository settlement remains unresolved',
|
|
2887
|
+
);
|
|
2888
|
+
}
|
|
2889
|
+
if (output !== undefined) {
|
|
2890
|
+
governedPlayerOutputs.set(result, output);
|
|
2891
|
+
} else if (
|
|
2892
|
+
result.status === 'ok' &&
|
|
2893
|
+
!isEmptyFinalText(result.finalText)
|
|
2894
|
+
) {
|
|
2895
|
+
throw new Error(
|
|
2896
|
+
'DECIDE governed player result has no reconciled semantic output',
|
|
2897
|
+
);
|
|
2898
|
+
}
|
|
2899
|
+
return result;
|
|
2900
|
+
};
|
|
2901
|
+
|
|
2902
|
+
const shouldRunProposalCohort = (input: PlayerInput): boolean => {
|
|
2903
|
+
if (
|
|
2904
|
+
input.stateId !== PROPOSAL_STATE_BY_ROLE[input.role] ||
|
|
2905
|
+
completedProposalCohortTurnId === currentTurnId
|
|
2906
|
+
) {
|
|
2907
|
+
return false;
|
|
2908
|
+
}
|
|
2909
|
+
const state = currentState();
|
|
2910
|
+
return Object.values(PROPOSAL_STATE_BY_ROLE).every((stateId) =>
|
|
2911
|
+
state.activeStateIds.includes(stateId),
|
|
2912
|
+
);
|
|
2913
|
+
};
|
|
2914
|
+
|
|
2915
|
+
const settleProposalCohort = async (): Promise<void> => {
|
|
2916
|
+
const members = Object.fromEntries(
|
|
2917
|
+
ROLE_IDS.map((roleId) => [roleId, pendingProposalCohort.get(roleId)]),
|
|
2918
|
+
) as Record<RoleId, PendingProposalCohortMember | undefined>;
|
|
2919
|
+
const turnId = currentTurnId;
|
|
2920
|
+
const cohortSignal = currentSignal;
|
|
2921
|
+
const cohortOperations = new AbortController();
|
|
2922
|
+
const completionOrder: RoleId[] = [];
|
|
2923
|
+
try {
|
|
2924
|
+
if (
|
|
2925
|
+
turnId === undefined ||
|
|
2926
|
+
cohortSignal === undefined ||
|
|
2927
|
+
members.coder === undefined ||
|
|
2928
|
+
members.reviewer === undefined
|
|
2929
|
+
) {
|
|
2930
|
+
throw new Error(
|
|
2931
|
+
'DECIDE proposal cohort started without both governed members',
|
|
2932
|
+
);
|
|
2933
|
+
}
|
|
2934
|
+
for (const roleId of ROLE_IDS) members[roleId]!.signal.throwIfAborted();
|
|
2935
|
+
const operations = Object.fromEntries(
|
|
2936
|
+
ROLE_IDS.map((roleId) => [
|
|
2937
|
+
roleId,
|
|
2938
|
+
async () => {
|
|
2939
|
+
const member = members[roleId]!;
|
|
2940
|
+
try {
|
|
2941
|
+
const operationSignal = combineSignals(
|
|
2942
|
+
member.signal,
|
|
2943
|
+
cohortOperations.signal,
|
|
2944
|
+
);
|
|
2945
|
+
const envelope = await runPlayerCall(
|
|
2946
|
+
member.input,
|
|
2947
|
+
operationSignal,
|
|
2948
|
+
{ callId: member.callId },
|
|
2949
|
+
);
|
|
2950
|
+
member.envelope = envelope;
|
|
2951
|
+
if (
|
|
2952
|
+
envelope.result.status !== 'ok' &&
|
|
2953
|
+
!cohortOperations.signal.aborted
|
|
2954
|
+
) {
|
|
2955
|
+
cohortOperations.abort(
|
|
2956
|
+
new Error(
|
|
2957
|
+
`DECIDE proposal cohort ${roleId} returned status ${JSON.stringify(envelope.result.status)}`,
|
|
2958
|
+
),
|
|
2959
|
+
);
|
|
2960
|
+
}
|
|
2961
|
+
return envelope.result;
|
|
2962
|
+
} catch (error) {
|
|
2963
|
+
if (!cohortOperations.signal.aborted) {
|
|
2964
|
+
cohortOperations.abort(error);
|
|
2965
|
+
}
|
|
2966
|
+
throw error;
|
|
2967
|
+
} finally {
|
|
2968
|
+
completionOrder.push(roleId);
|
|
2969
|
+
}
|
|
2970
|
+
},
|
|
2971
|
+
]),
|
|
2972
|
+
) as unknown as Readonly<
|
|
2973
|
+
Record<RoleId, () => Promise<PlayerResult>>
|
|
2974
|
+
>;
|
|
2975
|
+
const invocationId = randomUUID();
|
|
2976
|
+
const settled = await deferredEffects.repository.runCohort({
|
|
2977
|
+
signal: cohortSignal,
|
|
2978
|
+
invocationId,
|
|
2979
|
+
roleIds: ROLE_IDS,
|
|
2980
|
+
dispositionsByRole: {
|
|
2981
|
+
coder: ['unchanged'],
|
|
2982
|
+
reviewer: ['unchanged'],
|
|
2983
|
+
},
|
|
2984
|
+
effectBoundaries: {
|
|
2985
|
+
coder: members.coder.effectBoundary,
|
|
2986
|
+
reviewer: members.reviewer.effectBoundary,
|
|
2987
|
+
},
|
|
2988
|
+
operations,
|
|
2989
|
+
completeEffectBoundary: (completion) => {
|
|
2990
|
+
const member = members[completion.roleId];
|
|
2991
|
+
if (member === undefined) {
|
|
2992
|
+
throw new TypeError(
|
|
2993
|
+
`DECIDE proposal cohort completed unknown role ${String(completion.roleId)}`,
|
|
2994
|
+
);
|
|
2995
|
+
}
|
|
2996
|
+
return completionEvidenceFor(
|
|
2997
|
+
member.input,
|
|
2998
|
+
completion.roleId,
|
|
2999
|
+
resolvedPlayerId(completion.roleId),
|
|
3000
|
+
member.signal,
|
|
3001
|
+
)(completion);
|
|
3002
|
+
},
|
|
3003
|
+
});
|
|
3004
|
+
if (settled.invocationId !== invocationId) {
|
|
3005
|
+
throw new TypeError(
|
|
3006
|
+
'DECIDE repository cohort changed its invocation identity',
|
|
3007
|
+
);
|
|
3008
|
+
}
|
|
3009
|
+
const releaseOrder = [
|
|
3010
|
+
...completionOrder,
|
|
3011
|
+
...ROLE_IDS.filter((roleId) => !completionOrder.includes(roleId)),
|
|
3012
|
+
];
|
|
3013
|
+
const acknowledgedResults = new Map<RoleId, PlayerResult>();
|
|
3014
|
+
const acknowledgementFailures: unknown[] = [];
|
|
3015
|
+
for (const roleId of releaseOrder) {
|
|
3016
|
+
const member = members[roleId]!;
|
|
3017
|
+
try {
|
|
3018
|
+
const result = acknowledgeGovernedPlayerResult(
|
|
3019
|
+
{
|
|
3020
|
+
operation: settled.operations[roleId],
|
|
3021
|
+
receipt: settled.receipts[roleId],
|
|
3022
|
+
effectLedger: settled.effectLedger,
|
|
3023
|
+
},
|
|
3024
|
+
member.effectBoundary.boundaryId,
|
|
3025
|
+
);
|
|
3026
|
+
if (member.envelope === undefined) {
|
|
3027
|
+
throw new TypeError(
|
|
3028
|
+
`DECIDE repository cohort omitted its ${roleId} invocation`,
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
3031
|
+
acknowledgedResults.set(roleId, result);
|
|
3032
|
+
} catch (error) {
|
|
3033
|
+
acknowledgementFailures.push(error);
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
if (acknowledgementFailures.length > 0) {
|
|
3037
|
+
for (const result of acknowledgedResults.values()) {
|
|
3038
|
+
governedPlayerOutputs.delete(result);
|
|
3039
|
+
}
|
|
3040
|
+
throw acknowledgementFailures.length === 1
|
|
3041
|
+
? acknowledgementFailures[0]
|
|
3042
|
+
: new AggregateError(
|
|
3043
|
+
acknowledgementFailures,
|
|
3044
|
+
'DECIDE proposal cohort reconciliation failed',
|
|
3045
|
+
);
|
|
3046
|
+
}
|
|
3047
|
+
for (const roleId of releaseOrder) {
|
|
3048
|
+
const member = members[roleId]!;
|
|
3049
|
+
const result = acknowledgedResults.get(roleId);
|
|
3050
|
+
if (member.envelope === undefined || result === undefined) {
|
|
3051
|
+
throw new Error(
|
|
3052
|
+
`DECIDE proposal cohort lost its ${roleId} acknowledgement`,
|
|
3053
|
+
);
|
|
3054
|
+
}
|
|
3055
|
+
member.result.resolve({ ...member.envelope, result });
|
|
3056
|
+
}
|
|
3057
|
+
} catch (error) {
|
|
3058
|
+
for (const member of Object.values(members)) member?.result.reject(error);
|
|
3059
|
+
} finally {
|
|
3060
|
+
completedProposalCohortTurnId = turnId;
|
|
3061
|
+
pendingProposalCohort.clear();
|
|
3062
|
+
activeProposalCohort = undefined;
|
|
3063
|
+
}
|
|
3064
|
+
};
|
|
3065
|
+
|
|
3066
|
+
const queueProposalCohortMember = (
|
|
3067
|
+
input: PlayerInput,
|
|
3068
|
+
signal: AbortSignal,
|
|
3069
|
+
): Promise<DeferredContinuationEnvelope> => {
|
|
3070
|
+
if (pendingProposalCohort.has(input.role)) {
|
|
3071
|
+
throw new Error(
|
|
3072
|
+
`DECIDE proposal cohort already registered role ${input.role}`,
|
|
3073
|
+
);
|
|
3074
|
+
}
|
|
3075
|
+
const callId = `player-${++playerCallSequence}`;
|
|
3076
|
+
const member: PendingProposalCohortMember = {
|
|
3077
|
+
input,
|
|
3078
|
+
signal,
|
|
3079
|
+
callId,
|
|
3080
|
+
effectBoundary: governedBoundarySeed(input, callId),
|
|
3081
|
+
result: deferredValue<DeferredContinuationEnvelope>(),
|
|
3082
|
+
};
|
|
3083
|
+
pendingProposalCohort.set(input.role, member);
|
|
3084
|
+
if (pendingProposalCohort.size === ROLE_IDS.length) {
|
|
3085
|
+
if (activeProposalCohort !== undefined) {
|
|
3086
|
+
throw new Error('DECIDE proposal cohort was started more than once');
|
|
3087
|
+
}
|
|
3088
|
+
activeProposalCohort = settleProposalCohort();
|
|
3089
|
+
}
|
|
3090
|
+
return member.result.promise;
|
|
3091
|
+
};
|
|
3092
|
+
|
|
1444
3093
|
const callPlayer = (
|
|
1445
3094
|
input: PlayerInput,
|
|
1446
3095
|
signal: AbortSignal,
|
|
1447
|
-
): Promise<{
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
3096
|
+
): Promise<DeferredContinuationEnvelope> => {
|
|
3097
|
+
const invocation = async (): Promise<DeferredContinuationEnvelope> => {
|
|
3098
|
+
const active = activeDeferredContinuation;
|
|
3099
|
+
if (active === undefined && hasUnresolvedReconciliation()) {
|
|
3100
|
+
throw new Error(
|
|
3101
|
+
'DECIDE governed semantic reconciliation remains unresolved',
|
|
3102
|
+
);
|
|
3103
|
+
}
|
|
3104
|
+
if (active !== undefined) {
|
|
3105
|
+
if (
|
|
3106
|
+
active.effectBoundary.runtimeSessionId !==
|
|
3107
|
+
requireSessionIdentity().sessionId ||
|
|
3108
|
+
active.effectBoundary.turnId !== currentTurnId ||
|
|
3109
|
+
active.effectBoundary.roleId !== input.role ||
|
|
3110
|
+
active.effectBoundary.sourceStateId !== input.stateId ||
|
|
3111
|
+
active.playerContinuation === undefined
|
|
3112
|
+
) {
|
|
3113
|
+
throw new TypeError(
|
|
3114
|
+
'DECIDE deferred continuation did not invoke its bound player boundary',
|
|
3115
|
+
);
|
|
3116
|
+
}
|
|
3117
|
+
active.input = input;
|
|
3118
|
+
active.playerId = resolvedPlayerId(input.role);
|
|
3119
|
+
try {
|
|
3120
|
+
const envelope = await runPlayerCall(input, signal, {
|
|
3121
|
+
callId: active.effectBoundary.callId,
|
|
3122
|
+
resume: active.playerContinuation,
|
|
3123
|
+
});
|
|
3124
|
+
active.result.resolve(envelope.result);
|
|
3125
|
+
const acknowledgedResult = await active.acknowledged.promise;
|
|
3126
|
+
return { ...envelope, result: acknowledgedResult };
|
|
3127
|
+
} catch (error) {
|
|
3128
|
+
active.result.reject(error);
|
|
3129
|
+
try {
|
|
3130
|
+
await active.acknowledged.promise;
|
|
3131
|
+
} catch (acknowledgementError) {
|
|
3132
|
+
throw acknowledgementError;
|
|
3133
|
+
}
|
|
3134
|
+
throw error;
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
if (shouldRunProposalCohort(input)) {
|
|
3139
|
+
return queueProposalCohortMember(input, signal);
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
const callId = `player-${++playerCallSequence}`;
|
|
3143
|
+
const effectBoundary = governedBoundarySeed(input, callId);
|
|
3144
|
+
let envelope: DeferredContinuationEnvelope | undefined;
|
|
3145
|
+
const settled = await deferredEffects.repository.runExclusive({
|
|
3146
|
+
signal,
|
|
3147
|
+
effectBoundary,
|
|
3148
|
+
operation: async () => {
|
|
3149
|
+
envelope = await runPlayerCall(input, signal, { callId });
|
|
3150
|
+
return envelope.result;
|
|
3151
|
+
},
|
|
3152
|
+
completeEffectBoundary: completionEvidenceFor(
|
|
3153
|
+
input,
|
|
3154
|
+
input.role,
|
|
3155
|
+
resolvedPlayerId(input.role),
|
|
3156
|
+
signal,
|
|
3157
|
+
),
|
|
3158
|
+
});
|
|
3159
|
+
const result = acknowledgeGovernedPlayerResult(
|
|
3160
|
+
settled,
|
|
3161
|
+
effectBoundary.boundaryId,
|
|
3162
|
+
);
|
|
3163
|
+
if (envelope === undefined) {
|
|
3164
|
+
throw new TypeError(
|
|
3165
|
+
'DECIDE repository settlement omitted its player invocation',
|
|
3166
|
+
);
|
|
3167
|
+
}
|
|
3168
|
+
return { ...envelope, result };
|
|
3169
|
+
};
|
|
3170
|
+
return trackBoundaryCall(invocation());
|
|
1453
3171
|
};
|
|
1454
3172
|
|
|
1455
3173
|
const player = fromPromise<PlayerOutput, PlayerInput>(
|
|
@@ -1469,8 +3187,18 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1469
3187
|
}
|
|
1470
3188
|
combined.throwIfAborted();
|
|
1471
3189
|
|
|
1472
|
-
let { roleId, playerId, result } = await callPlayer(
|
|
1473
|
-
|
|
3190
|
+
let { roleId, playerId, callId, result } = await callPlayer(
|
|
3191
|
+
input,
|
|
3192
|
+
combined,
|
|
3193
|
+
);
|
|
3194
|
+
if (
|
|
3195
|
+
result.status === 'ok' &&
|
|
3196
|
+
isEmptyFinalText(result.finalText) &&
|
|
3197
|
+
automaticReplayPolicy.allowsEmptyOkCorrection(
|
|
3198
|
+
requireSessionIdentity().sessionId,
|
|
3199
|
+
callId,
|
|
3200
|
+
)
|
|
3201
|
+
) {
|
|
1474
3202
|
// DR-028: an `ok` result whose finalText is missing, empty, or
|
|
1475
3203
|
// whitespace-only earns exactly one corrective re-ask — the same
|
|
1476
3204
|
// composed call repeated, traced by runPlayerCall as its own
|
|
@@ -1480,7 +3208,10 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1480
3208
|
// are never retried), and a rejecting finish emission rejects
|
|
1481
3209
|
// `callPlayer` itself, so it never reaches this branch (PBRT-47).
|
|
1482
3210
|
combined.throwIfAborted();
|
|
1483
|
-
({ roleId, playerId, result } = await callPlayer(
|
|
3211
|
+
({ roleId, playerId, callId, result } = await callPlayer(
|
|
3212
|
+
input,
|
|
3213
|
+
combined,
|
|
3214
|
+
));
|
|
1484
3215
|
}
|
|
1485
3216
|
if (result.status !== 'ok') {
|
|
1486
3217
|
throw new Error(
|
|
@@ -1501,22 +3232,14 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1501
3232
|
}
|
|
1502
3233
|
combined.throwIfAborted();
|
|
1503
3234
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
prompt,
|
|
1509
|
-
combined,
|
|
1510
|
-
'player-output-adjudication',
|
|
1511
|
-
input.stateId,
|
|
1512
|
-
),
|
|
1513
|
-
input,
|
|
1514
|
-
finalText,
|
|
1515
|
-
);
|
|
1516
|
-
} catch (error) {
|
|
1517
|
-
latchControlPlaneError(error, combined);
|
|
1518
|
-
throw error;
|
|
3235
|
+
const governedOutput = governedPlayerOutputs.get(result);
|
|
3236
|
+
if (governedOutput !== undefined) {
|
|
3237
|
+
governedPlayerOutputs.delete(result);
|
|
3238
|
+
return governedOutput;
|
|
1519
3239
|
}
|
|
3240
|
+
throw new Error(
|
|
3241
|
+
'DECIDE governed player result was not reconciled against repository evidence',
|
|
3242
|
+
);
|
|
1520
3243
|
} finally {
|
|
1521
3244
|
actorSettlementAborts.push(settlementAborts);
|
|
1522
3245
|
}
|
|
@@ -1611,16 +3334,38 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1611
3334
|
};
|
|
1612
3335
|
|
|
1613
3336
|
const inspect = (event: InspectionEvent): void => {
|
|
1614
|
-
if (event.type !== '@xstate.snapshot') return;
|
|
1615
3337
|
if (actor === undefined || event.actorRef !== actor) return;
|
|
1616
3338
|
if (suppressInspectionEmissions) return;
|
|
3339
|
+
if (event.type === '@xstate.action') {
|
|
3340
|
+
try {
|
|
3341
|
+
acceptedOutcomeConsumer.capture(event.action);
|
|
3342
|
+
} catch (error) {
|
|
3343
|
+
latchInspectionError(error);
|
|
3344
|
+
}
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
if (event.type !== '@xstate.snapshot') return;
|
|
1617
3348
|
const settlementAborts = consumeActorSettlementAborts(true);
|
|
1618
3349
|
try {
|
|
1619
3350
|
const snapshot = event.snapshot as SnapshotFrom<typeof decideMachine>;
|
|
1620
3351
|
const state = normalizePlaybookSnapshot(snapshot);
|
|
1621
3352
|
const prior = previousState ?? state;
|
|
3353
|
+
let acceptedOutcomes: readonly AcceptedOutcomeReceipt[] = [];
|
|
3354
|
+
try {
|
|
3355
|
+
acceptedOutcomes = acceptedOutcomeConsumer.confirm(previousState, state);
|
|
3356
|
+
} catch (error) {
|
|
3357
|
+
latchInspectionError(error);
|
|
3358
|
+
}
|
|
1622
3359
|
const context = snapshot.context as unknown as Record<string, unknown>;
|
|
1623
|
-
const fsmPayload = telemetryPayload(
|
|
3360
|
+
const fsmPayload = telemetryPayload(
|
|
3361
|
+
prior,
|
|
3362
|
+
state,
|
|
3363
|
+
event.event,
|
|
3364
|
+
context,
|
|
3365
|
+
hiddenDeferredOperationId === undefined
|
|
3366
|
+
? undefined
|
|
3367
|
+
: 'commitCoderProposal',
|
|
3368
|
+
);
|
|
1624
3369
|
const describedFsmPayload = snapshotJsonValue(
|
|
1625
3370
|
fsmPayload,
|
|
1626
3371
|
'described FSM telemetry',
|
|
@@ -1640,7 +3385,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1640
3385
|
|
|
1641
3386
|
const priorIds = new Set(previousState?.activeStateIds ?? []);
|
|
1642
3387
|
previousState = state;
|
|
1643
|
-
const pendingQuestions =
|
|
3388
|
+
const pendingQuestions = visiblePendingQuestionsForState(state, context);
|
|
1644
3389
|
const bossRelevantStateIds = state.activeStateIds.filter((stateId) =>
|
|
1645
3390
|
STATUS_STATE_IDS.has(stateId),
|
|
1646
3391
|
);
|
|
@@ -1665,6 +3410,14 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1665
3410
|
).catch(() => undefined);
|
|
1666
3411
|
};
|
|
1667
3412
|
|
|
3413
|
+
for (const acceptedOutcome of acceptedOutcomes) {
|
|
3414
|
+
void enqueueAcceptedOutcomeEmission(
|
|
3415
|
+
acceptedOutcome,
|
|
3416
|
+
state,
|
|
3417
|
+
settlementAborts,
|
|
3418
|
+
).catch(() => undefined);
|
|
3419
|
+
}
|
|
3420
|
+
|
|
1668
3421
|
for (const activeStateId of state.activeStateIds) {
|
|
1669
3422
|
if (
|
|
1670
3423
|
priorIds.has(activeStateId) ||
|
|
@@ -1707,12 +3460,14 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1707
3460
|
);
|
|
1708
3461
|
}
|
|
1709
3462
|
} catch (error) {
|
|
3463
|
+
acceptedOutcomeConsumer.reset();
|
|
1710
3464
|
latchInspectionError(error, settlementAborts);
|
|
1711
3465
|
}
|
|
1712
3466
|
};
|
|
1713
3467
|
|
|
1714
3468
|
const createRuntimeActor = (machineSnapshot?: JsonValue): void => {
|
|
1715
3469
|
previousState = undefined;
|
|
3470
|
+
acceptedOutcomeConsumer.reset();
|
|
1716
3471
|
// DR-014 §1: a restore rehydrates the persisted machine snapshot;
|
|
1717
3472
|
// XState derives context/value from it and ignores `input` then.
|
|
1718
3473
|
actor = createActor(providedMachine, {
|
|
@@ -1748,6 +3503,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1748
3503
|
const stopActor = (): void => {
|
|
1749
3504
|
if (!actor) return;
|
|
1750
3505
|
suppressInspectionEmissions = true;
|
|
3506
|
+
acceptedOutcomeConsumer.reset();
|
|
1751
3507
|
actor.stop();
|
|
1752
3508
|
};
|
|
1753
3509
|
|
|
@@ -1770,17 +3526,21 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1770
3526
|
): Promise<DecideEvent | { type: 'NO_ACTION' } | null> => {
|
|
1771
3527
|
const live = actor;
|
|
1772
3528
|
if (!live) throw new Error('decide runtime: actor is not initialized');
|
|
3529
|
+
refreshReconciliationProjection();
|
|
3530
|
+
if (hasUnresolvedReconciliation()) return null;
|
|
1773
3531
|
const snapshot = live.getSnapshot() as SnapshotFrom<typeof decideMachine>;
|
|
1774
3532
|
const context = snapshot.context as unknown as Record<string, unknown>;
|
|
1775
3533
|
const state = normalizePlaybookSnapshot(snapshot, {
|
|
1776
3534
|
pendingCall: nestedBridge.getPendingCall(),
|
|
1777
3535
|
});
|
|
1778
|
-
const pendingQuestions =
|
|
3536
|
+
const pendingQuestions = visiblePendingQuestionsForState(state, context);
|
|
3537
|
+
const failed = state.activeStateIds.includes('failed');
|
|
1779
3538
|
if (
|
|
1780
3539
|
pendingQuestions.length === 0 &&
|
|
1781
3540
|
(snapshot.status === 'done' ||
|
|
1782
3541
|
state.activeStateIds.includes('ready') ||
|
|
1783
|
-
|
|
3542
|
+
(failed &&
|
|
3543
|
+
automaticReplayPolicy.allowsFailureStateRetry()))
|
|
1784
3544
|
) {
|
|
1785
3545
|
return { type: 'START_DECIDE', callerTopic: text };
|
|
1786
3546
|
}
|
|
@@ -1802,6 +3562,640 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1802
3562
|
);
|
|
1803
3563
|
};
|
|
1804
3564
|
|
|
3565
|
+
const deferredBoundarySeed = (
|
|
3566
|
+
operationId: string,
|
|
3567
|
+
callId: string,
|
|
3568
|
+
): DecideEffectBoundarySeed => {
|
|
3569
|
+
const operation = effectLedgerMirror.logicalOperations.find(
|
|
3570
|
+
(candidate) => candidate.operationId === operationId,
|
|
3571
|
+
);
|
|
3572
|
+
const latestBoundaryId = operation?.boundaryIds.at(-1);
|
|
3573
|
+
const priorBoundary = effectLedgerMirror.boundaries.find(
|
|
3574
|
+
(candidate) => candidate.boundaryId === latestBoundaryId,
|
|
3575
|
+
);
|
|
3576
|
+
if (operation === undefined || priorBoundary === undefined) {
|
|
3577
|
+
throw new TypeError(
|
|
3578
|
+
'DECIDE deferred logical operation has no linked physical boundary',
|
|
3579
|
+
);
|
|
3580
|
+
}
|
|
3581
|
+
if (
|
|
3582
|
+
currentTurnId === undefined ||
|
|
3583
|
+
!Number.isSafeInteger(currentTurnId) ||
|
|
3584
|
+
currentTurnId <= 0
|
|
3585
|
+
) {
|
|
3586
|
+
throw new Error(
|
|
3587
|
+
'DECIDE deferred continuation requires an active positive turn id',
|
|
3588
|
+
);
|
|
3589
|
+
}
|
|
3590
|
+
return {
|
|
3591
|
+
boundaryId: randomUUID(),
|
|
3592
|
+
runtimeSessionId: requireSessionIdentity().sessionId,
|
|
3593
|
+
turnId: currentTurnId,
|
|
3594
|
+
callId,
|
|
3595
|
+
roleId: priorBoundary.roleId,
|
|
3596
|
+
sourceStateId: priorBoundary.sourceStateId,
|
|
3597
|
+
sourceOutcomeSchema: snapshotJsonValue(
|
|
3598
|
+
priorBoundary.sourceOutcomeSchema,
|
|
3599
|
+
'DECIDE deferred source outcome schema',
|
|
3600
|
+
),
|
|
3601
|
+
dispositions: [...priorBoundary.dispositions],
|
|
3602
|
+
correctionBudget: { limit: 1, spent: false },
|
|
3603
|
+
};
|
|
3604
|
+
};
|
|
3605
|
+
|
|
3606
|
+
type PreparedDeferredContinuation =
|
|
3607
|
+
| { readonly proceed: false }
|
|
3608
|
+
| { readonly proceed: true; readonly acknowledgement: Promise<void> };
|
|
3609
|
+
|
|
3610
|
+
const prepareDeferredContinuation = async (
|
|
3611
|
+
signal: AbortSignal,
|
|
3612
|
+
resumeEvent: Extract<DecideEvent, { readonly type: 'BOSS_REPLY' }>,
|
|
3613
|
+
): Promise<PreparedDeferredContinuation> => {
|
|
3614
|
+
if (deferredEffects === undefined || deferredOperationId === undefined) {
|
|
3615
|
+
throw new TypeError(
|
|
3616
|
+
'DECIDE deferred Boss reply has no host-bound logical operation',
|
|
3617
|
+
);
|
|
3618
|
+
}
|
|
3619
|
+
const operationId = deferredOperationId;
|
|
3620
|
+
const callId = `player-${++playerCallSequence}`;
|
|
3621
|
+
const effectBoundary = deferredBoundarySeed(operationId, callId);
|
|
3622
|
+
const active: ActiveDeferredContinuation = {
|
|
3623
|
+
operationId,
|
|
3624
|
+
effectBoundary,
|
|
3625
|
+
result: deferredValue<PlayerResult>(),
|
|
3626
|
+
acknowledged: deferredValue<PlayerResult>(),
|
|
3627
|
+
};
|
|
3628
|
+
type Readiness =
|
|
3629
|
+
| { readonly status: 'ready' }
|
|
3630
|
+
| {
|
|
3631
|
+
readonly status: 'settled';
|
|
3632
|
+
readonly value:
|
|
3633
|
+
| DecideRepositoryDeferredContinuationResult<PlayerResult>
|
|
3634
|
+
| DecideRepositoryDeferredCheckpointMismatch;
|
|
3635
|
+
}
|
|
3636
|
+
| { readonly status: 'rejected'; readonly reason: unknown };
|
|
3637
|
+
const readiness = deferredValue<Readiness>();
|
|
3638
|
+
const repositoryCall = deferredEffects.repository.runDeferred({
|
|
3639
|
+
mode: 'continue',
|
|
3640
|
+
signal,
|
|
3641
|
+
operationId,
|
|
3642
|
+
effectBoundary,
|
|
3643
|
+
operation: async ({ playerContinuation }) => {
|
|
3644
|
+
if (
|
|
3645
|
+
playerContinuation !== false &&
|
|
3646
|
+
(typeof playerContinuation !== 'string' ||
|
|
3647
|
+
playerContinuation.trim() === '')
|
|
3648
|
+
) {
|
|
3649
|
+
throw new TypeError(
|
|
3650
|
+
'DECIDE bound deferred player continuation is invalid',
|
|
3651
|
+
);
|
|
3652
|
+
}
|
|
3653
|
+
active.playerContinuation = playerContinuation;
|
|
3654
|
+
readiness.resolve({ status: 'ready' });
|
|
3655
|
+
return active.result.promise;
|
|
3656
|
+
},
|
|
3657
|
+
completeEffectBoundary: async (completion) => {
|
|
3658
|
+
if (active.input === undefined) {
|
|
3659
|
+
throw new TypeError(
|
|
3660
|
+
'DECIDE deferred continuation omitted its authored player input',
|
|
3661
|
+
);
|
|
3662
|
+
}
|
|
3663
|
+
return completionEvidenceFor(
|
|
3664
|
+
active.input,
|
|
3665
|
+
active.input.role,
|
|
3666
|
+
active.playerId,
|
|
3667
|
+
signal,
|
|
3668
|
+
operationId,
|
|
3669
|
+
)(completion);
|
|
3670
|
+
},
|
|
3671
|
+
});
|
|
3672
|
+
void repositoryCall.then(
|
|
3673
|
+
(value) => readiness.resolve({ status: 'settled', value }),
|
|
3674
|
+
(reason: unknown) => readiness.resolve({ status: 'rejected', reason }),
|
|
3675
|
+
);
|
|
3676
|
+
const prepared = await readiness.promise;
|
|
3677
|
+
if (prepared.status === 'rejected') throw prepared.reason;
|
|
3678
|
+
if (prepared.status === 'settled') {
|
|
3679
|
+
synchronizeDeferredProjection(
|
|
3680
|
+
assertPlaybookEffectLedger(
|
|
3681
|
+
prepared.value.effectLedger,
|
|
3682
|
+
'DECIDE deferred checkpoint-mismatch effect ledger',
|
|
3683
|
+
),
|
|
3684
|
+
);
|
|
3685
|
+
hiddenDeferredOperationId = operationId;
|
|
3686
|
+
return { proceed: false };
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
activeDeferredContinuation = active;
|
|
3690
|
+
const acknowledgement = repositoryCall.then(
|
|
3691
|
+
(value) => {
|
|
3692
|
+
if (value.status !== 'continued') {
|
|
3693
|
+
throw new TypeError(
|
|
3694
|
+
'DECIDE deferred repository changed status after starting its operation',
|
|
3695
|
+
);
|
|
3696
|
+
}
|
|
3697
|
+
const result = acknowledgeGovernedPlayerResult(
|
|
3698
|
+
value,
|
|
3699
|
+
effectBoundary.boundaryId,
|
|
3700
|
+
);
|
|
3701
|
+
if (hiddenDeferredOperationId === undefined) {
|
|
3702
|
+
suppressInspectionEmissions = false;
|
|
3703
|
+
const live = actor;
|
|
3704
|
+
if (live === undefined) {
|
|
3705
|
+
throw new Error(
|
|
3706
|
+
'DECIDE deferred acknowledgement lost its runtime actor',
|
|
3707
|
+
);
|
|
3708
|
+
}
|
|
3709
|
+
// Entry into the governed continuation was hidden until its durable
|
|
3710
|
+
// effect acknowledgement. Publish that exact current root snapshot
|
|
3711
|
+
// before releasing the actor output, so an accepted-outcome marker
|
|
3712
|
+
// is confirmed against commitCoderProposal rather than the earlier
|
|
3713
|
+
// awaitBossReply snapshot.
|
|
3714
|
+
inspect({
|
|
3715
|
+
type: '@xstate.snapshot',
|
|
3716
|
+
actorRef: live,
|
|
3717
|
+
event: resumeEvent,
|
|
3718
|
+
snapshot: live.getSnapshot(),
|
|
3719
|
+
} as unknown as InspectionEvent);
|
|
3720
|
+
}
|
|
3721
|
+
active.acknowledged.resolve(result);
|
|
3722
|
+
},
|
|
3723
|
+
(error: unknown) => {
|
|
3724
|
+
active.acknowledged.reject(error);
|
|
3725
|
+
throw error;
|
|
3726
|
+
},
|
|
3727
|
+
).catch((error: unknown) => {
|
|
3728
|
+
active.acknowledged.reject(error);
|
|
3729
|
+
throw error;
|
|
3730
|
+
});
|
|
3731
|
+
return { proceed: true, acknowledgement };
|
|
3732
|
+
};
|
|
3733
|
+
|
|
3734
|
+
const parkDeferredContinuation = async (
|
|
3735
|
+
signal: AbortSignal,
|
|
3736
|
+
): Promise<void> => {
|
|
3737
|
+
if (deferredEffects === undefined || deferredOperationId === undefined) {
|
|
3738
|
+
return;
|
|
3739
|
+
}
|
|
3740
|
+
const operationId = deferredOperationId;
|
|
3741
|
+
const parked = await deferredEffects.repository.runDeferred({
|
|
3742
|
+
mode: 'park',
|
|
3743
|
+
signal,
|
|
3744
|
+
operationId,
|
|
3745
|
+
});
|
|
3746
|
+
synchronizeDeferredProjection(
|
|
3747
|
+
assertPlaybookEffectLedger(
|
|
3748
|
+
parked.effectLedger,
|
|
3749
|
+
'DECIDE deferred park effect ledger',
|
|
3750
|
+
),
|
|
3751
|
+
);
|
|
3752
|
+
hiddenDeferredOperationId = operationId;
|
|
3753
|
+
};
|
|
3754
|
+
|
|
3755
|
+
const restoreDeferredReconciliation = async (
|
|
3756
|
+
operationId: string,
|
|
3757
|
+
signal: AbortSignal,
|
|
3758
|
+
publishQuestion: boolean,
|
|
3759
|
+
): Promise<'restored' | 'checkpoint-mismatch' | 'ineligible'> => {
|
|
3760
|
+
const restored = await deferredEffects.repository.runDeferred({
|
|
3761
|
+
mode: 'restore',
|
|
3762
|
+
signal,
|
|
3763
|
+
operationId,
|
|
3764
|
+
});
|
|
3765
|
+
synchronizeDeferredProjection(
|
|
3766
|
+
assertPlaybookEffectLedger(
|
|
3767
|
+
restored.effectLedger,
|
|
3768
|
+
'DECIDE deferred restoration effect ledger',
|
|
3769
|
+
),
|
|
3770
|
+
);
|
|
3771
|
+
if (restored.status === 'parked') {
|
|
3772
|
+
throw new TypeError(
|
|
3773
|
+
'DECIDE deferred restoration returned an invalid parked status',
|
|
3774
|
+
);
|
|
3775
|
+
}
|
|
3776
|
+
if (restored.status !== 'restored') {
|
|
3777
|
+
if (hiddenDeferredOperationId !== operationId) {
|
|
3778
|
+
throw new TypeError(
|
|
3779
|
+
'DECIDE unresolved deferred restoration lost its operation identity',
|
|
3780
|
+
);
|
|
3781
|
+
}
|
|
3782
|
+
return restored.status;
|
|
3783
|
+
}
|
|
3784
|
+
if (hiddenDeferredOperationId !== undefined) {
|
|
3785
|
+
throw new TypeError(
|
|
3786
|
+
'DECIDE restored deferred operation remained unresolved',
|
|
3787
|
+
);
|
|
3788
|
+
}
|
|
3789
|
+
if (openDeferredOperation()?.operationId !== operationId) {
|
|
3790
|
+
throw new TypeError(
|
|
3791
|
+
'DECIDE restored deferred operation is not the current bound wait',
|
|
3792
|
+
);
|
|
3793
|
+
}
|
|
3794
|
+
const live = actor;
|
|
3795
|
+
if (live === undefined) {
|
|
3796
|
+
throw new Error('decide runtime: init(session) must be called first');
|
|
3797
|
+
}
|
|
3798
|
+
const state = currentState();
|
|
3799
|
+
const context = (
|
|
3800
|
+
live.getSnapshot() as SnapshotFrom<typeof decideMachine>
|
|
3801
|
+
).context as unknown as Record<string, unknown>;
|
|
3802
|
+
const pending = questionForWaitState(
|
|
3803
|
+
'awaitBossReply',
|
|
3804
|
+
visiblePendingQuestionsForState(state, context),
|
|
3805
|
+
);
|
|
3806
|
+
const operation = openDeferredOperation();
|
|
3807
|
+
const projectedPending =
|
|
3808
|
+
pending === undefined
|
|
3809
|
+
? undefined
|
|
3810
|
+
: {
|
|
3811
|
+
questionId: pending.questionId,
|
|
3812
|
+
asker: pending.asker,
|
|
3813
|
+
question: pending.question,
|
|
3814
|
+
sourceItem: pending.sourceItem,
|
|
3815
|
+
};
|
|
3816
|
+
if (
|
|
3817
|
+
pending === undefined ||
|
|
3818
|
+
operation?.pendingQuestion === undefined ||
|
|
3819
|
+
stableJson(
|
|
3820
|
+
projectedPending,
|
|
3821
|
+
'DECIDE restored FSM pending question',
|
|
3822
|
+
) !==
|
|
3823
|
+
stableJson(
|
|
3824
|
+
operation.pendingQuestion,
|
|
3825
|
+
'DECIDE restored deferred pending question',
|
|
3826
|
+
)
|
|
3827
|
+
) {
|
|
3828
|
+
throw new TypeError(
|
|
3829
|
+
'DECIDE restored deferred wait does not equal its FSM question',
|
|
3830
|
+
);
|
|
3831
|
+
}
|
|
3832
|
+
if (publishQuestion) {
|
|
3833
|
+
await emitBoundaryStatus(
|
|
3834
|
+
`${pending.asker.roleId} asks: ${pending.question}`,
|
|
3835
|
+
state,
|
|
3836
|
+
);
|
|
3837
|
+
await emitBoundaryStatus(
|
|
3838
|
+
`◆ awaiting Boss reply · ${pending.resumeStateId} · ${pending.asker.roleId} · ${pending.sourceItem}`,
|
|
3839
|
+
state,
|
|
3840
|
+
);
|
|
3841
|
+
await flush();
|
|
3842
|
+
}
|
|
3843
|
+
return 'restored';
|
|
3844
|
+
};
|
|
3845
|
+
|
|
3846
|
+
type UnresolvedControlCandidate = {
|
|
3847
|
+
readonly action: PlaybookControlAction;
|
|
3848
|
+
readonly kind: 'reconcile' | 'abandon';
|
|
3849
|
+
readonly deferredRestoreOperationId?: string;
|
|
3850
|
+
};
|
|
3851
|
+
type UnresolvedControlCandidates = readonly UnresolvedControlCandidate[];
|
|
3852
|
+
|
|
3853
|
+
const unresolvedControlCandidates = (): UnresolvedControlCandidates => {
|
|
3854
|
+
if (!hasUnresolvedReconciliation()) return [];
|
|
3855
|
+
const operation =
|
|
3856
|
+
hiddenDeferredOperationId === undefined
|
|
3857
|
+
? undefined
|
|
3858
|
+
: effectLedgerMirror.logicalOperations.find(
|
|
3859
|
+
({ operationId }) =>
|
|
3860
|
+
operationId === hiddenDeferredOperationId,
|
|
3861
|
+
);
|
|
3862
|
+
return [
|
|
3863
|
+
{
|
|
3864
|
+
action: {
|
|
3865
|
+
id: UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID,
|
|
3866
|
+
label: 'Retry unresolved effect reconciliation',
|
|
3867
|
+
},
|
|
3868
|
+
kind: 'reconcile',
|
|
3869
|
+
...(operation?.checkpointRestorationEligible === true
|
|
3870
|
+
? { deferredRestoreOperationId: operation.operationId }
|
|
3871
|
+
: {}),
|
|
3872
|
+
},
|
|
3873
|
+
{
|
|
3874
|
+
action: {
|
|
3875
|
+
id: UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID,
|
|
3876
|
+
label: 'Abandon unresolved workflow attempt',
|
|
3877
|
+
},
|
|
3878
|
+
kind: 'abandon',
|
|
3879
|
+
},
|
|
3880
|
+
];
|
|
3881
|
+
};
|
|
3882
|
+
|
|
3883
|
+
const deriveControlCandidates = (): UnresolvedControlCandidates => {
|
|
3884
|
+
const state = currentState();
|
|
3885
|
+
if (
|
|
3886
|
+
state.status !== 'active' ||
|
|
3887
|
+
!state.quiescent ||
|
|
3888
|
+
nestedBridge.getPendingCall() !== undefined
|
|
3889
|
+
) {
|
|
3890
|
+
return [];
|
|
3891
|
+
}
|
|
3892
|
+
return unresolvedControlCandidates();
|
|
3893
|
+
};
|
|
3894
|
+
|
|
3895
|
+
const describeControlView = (): PlaybookControlView => {
|
|
3896
|
+
if (disposed || disposalPromise !== undefined) {
|
|
3897
|
+
throw new Error('decide runtime: runtime is disposing or disposed');
|
|
3898
|
+
}
|
|
3899
|
+
const live = actor;
|
|
3900
|
+
if (live === undefined || sessionIdentity === undefined) {
|
|
3901
|
+
throw new Error(
|
|
3902
|
+
'decide runtime: init(session) must be called before describe',
|
|
3903
|
+
);
|
|
3904
|
+
}
|
|
3905
|
+
if (currentSignal !== undefined || currentTurnId !== undefined) {
|
|
3906
|
+
throw new Error('decide runtime: another runtime turn is active');
|
|
3907
|
+
}
|
|
3908
|
+
refreshReconciliationProjection();
|
|
3909
|
+
const state = currentState();
|
|
3910
|
+
const context = (
|
|
3911
|
+
live.getSnapshot() as SnapshotFrom<typeof decideMachine>
|
|
3912
|
+
).context as unknown as Record<string, unknown>;
|
|
3913
|
+
const unresolved = hasUnresolvedReconciliation();
|
|
3914
|
+
const pendingQuestions = unresolved
|
|
3915
|
+
? []
|
|
3916
|
+
: visiblePendingQuestionsForState(state, context).map((pending) =>
|
|
3917
|
+
Object.freeze({
|
|
3918
|
+
questionId: pending.questionId,
|
|
3919
|
+
asker: pending.asker,
|
|
3920
|
+
question: pending.question,
|
|
3921
|
+
sourceItem: pending.sourceItem,
|
|
3922
|
+
}),
|
|
3923
|
+
);
|
|
3924
|
+
const actions = deriveControlCandidates().map(({ action }) => ({
|
|
3925
|
+
...action,
|
|
3926
|
+
}));
|
|
3927
|
+
const lastError = normalizeErrorFull(context.lastError);
|
|
3928
|
+
const stateDescription =
|
|
3929
|
+
unresolved || state.stateId === undefined
|
|
3930
|
+
? undefined
|
|
3931
|
+
: STATE_DESCRIPTIONS[state.stateId];
|
|
3932
|
+
return deepFreeze(
|
|
3933
|
+
snapshotJsonValue(
|
|
3934
|
+
{
|
|
3935
|
+
state,
|
|
3936
|
+
...(stateDescription === undefined ? {} : { stateDescription }),
|
|
3937
|
+
pendingQuestions,
|
|
3938
|
+
...(lastError === undefined ? {} : { lastError }),
|
|
3939
|
+
actions,
|
|
3940
|
+
},
|
|
3941
|
+
'DECIDE control view',
|
|
3942
|
+
) as unknown as PlaybookControlView,
|
|
3943
|
+
);
|
|
3944
|
+
};
|
|
3945
|
+
|
|
3946
|
+
const normalizedControlError = (error: unknown): NormalizedError =>
|
|
3947
|
+
normalizeErrorFull(error) ?? {
|
|
3948
|
+
name: error instanceof Error ? error.name : 'Error',
|
|
3949
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3950
|
+
};
|
|
3951
|
+
|
|
3952
|
+
const frozenControlReceipt = (
|
|
3953
|
+
receipt: PlaybookControlReceipt,
|
|
3954
|
+
): PlaybookControlReceipt =>
|
|
3955
|
+
deepFreeze(
|
|
3956
|
+
snapshotJsonValue(
|
|
3957
|
+
receipt,
|
|
3958
|
+
'DECIDE apply receipt',
|
|
3959
|
+
) as unknown as PlaybookControlReceipt,
|
|
3960
|
+
);
|
|
3961
|
+
|
|
3962
|
+
const applyControlAction = async (input: {
|
|
3963
|
+
actionId: string;
|
|
3964
|
+
key: string;
|
|
3965
|
+
signal: AbortSignal;
|
|
3966
|
+
}): Promise<PlaybookControlReceipt> => {
|
|
3967
|
+
if (input === null || typeof input !== 'object') {
|
|
3968
|
+
throw new TypeError('decide runtime: apply input must be an object');
|
|
3969
|
+
}
|
|
3970
|
+
const { actionId, key, signal } = input;
|
|
3971
|
+
if (typeof actionId !== 'string' || actionId.length === 0) {
|
|
3972
|
+
throw new TypeError(
|
|
3973
|
+
'decide runtime: apply actionId must be a non-empty string',
|
|
3974
|
+
);
|
|
3975
|
+
}
|
|
3976
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
3977
|
+
throw new TypeError(
|
|
3978
|
+
'decide runtime: apply key must be a non-empty string',
|
|
3979
|
+
);
|
|
3980
|
+
}
|
|
3981
|
+
if (!(signal instanceof AbortSignal)) {
|
|
3982
|
+
throw new TypeError(
|
|
3983
|
+
'decide runtime: apply signal must be an AbortSignal',
|
|
3984
|
+
);
|
|
3985
|
+
}
|
|
3986
|
+
if (disposed || disposalPromise !== undefined) {
|
|
3987
|
+
throw new Error('decide runtime: runtime is disposing or disposed');
|
|
3988
|
+
}
|
|
3989
|
+
if (actor === undefined || sessionIdentity === undefined) {
|
|
3990
|
+
throw new Error(
|
|
3991
|
+
'decide runtime: init(session) must be called before apply',
|
|
3992
|
+
);
|
|
3993
|
+
}
|
|
3994
|
+
if (currentSignal !== undefined || currentTurnId !== undefined) {
|
|
3995
|
+
throw new Error('decide runtime: another runtime turn is active');
|
|
3996
|
+
}
|
|
3997
|
+
const recorded = appliedControlReceipts.get(key);
|
|
3998
|
+
if (recorded !== undefined) return recorded;
|
|
3999
|
+
signal.throwIfAborted();
|
|
4000
|
+
refreshReconciliationProjection();
|
|
4001
|
+
|
|
4002
|
+
const turnId = ++turnSequence;
|
|
4003
|
+
const callId = `apply-${++applyCallSequence}`;
|
|
4004
|
+
currentTurnId = turnId;
|
|
4005
|
+
currentSignal = signal;
|
|
4006
|
+
currentAborts = abortReasonClassifier(signal);
|
|
4007
|
+
controlPlaneError = undefined;
|
|
4008
|
+
let accepted = false;
|
|
4009
|
+
let receipt: PlaybookControlReceipt | undefined;
|
|
4010
|
+
let operationError: unknown;
|
|
4011
|
+
let settlementError: unknown;
|
|
4012
|
+
let lateDeliveryError: unknown;
|
|
4013
|
+
const position = { turnId, callId };
|
|
4014
|
+
const preAcceptanceFinish = (
|
|
4015
|
+
reason: string,
|
|
4016
|
+
): Record<string, unknown> => ({
|
|
4017
|
+
actionId,
|
|
4018
|
+
key,
|
|
4019
|
+
disposition: 'rejected',
|
|
4020
|
+
reason,
|
|
4021
|
+
});
|
|
4022
|
+
try {
|
|
4023
|
+
try {
|
|
4024
|
+
try {
|
|
4025
|
+
await emitTrace(
|
|
4026
|
+
'apply.started',
|
|
4027
|
+
{
|
|
4028
|
+
actionId,
|
|
4029
|
+
key,
|
|
4030
|
+
...stateIdentity(currentState()),
|
|
4031
|
+
},
|
|
4032
|
+
position,
|
|
4033
|
+
currentAborts,
|
|
4034
|
+
);
|
|
4035
|
+
} catch (error) {
|
|
4036
|
+
latchControlPlaneError(error, signal);
|
|
4037
|
+
try {
|
|
4038
|
+
await emitTrace(
|
|
4039
|
+
'apply.finished',
|
|
4040
|
+
{
|
|
4041
|
+
...preAcceptanceFinish('apply.started trace sink rejected'),
|
|
4042
|
+
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
4043
|
+
error: normalizedControlError(error),
|
|
4044
|
+
},
|
|
4045
|
+
position,
|
|
4046
|
+
currentAborts,
|
|
4047
|
+
);
|
|
4048
|
+
} catch {
|
|
4049
|
+
// Preserve the start failure after one best-effort finish attempt.
|
|
4050
|
+
}
|
|
4051
|
+
throw error;
|
|
4052
|
+
}
|
|
4053
|
+
await flush();
|
|
4054
|
+
if (signal.aborted) {
|
|
4055
|
+
try {
|
|
4056
|
+
await emitTrace(
|
|
4057
|
+
'apply.finished',
|
|
4058
|
+
{
|
|
4059
|
+
...preAcceptanceFinish('aborted before acceptance'),
|
|
4060
|
+
status: 'aborted',
|
|
4061
|
+
error: normalizedControlError(signal.reason),
|
|
4062
|
+
},
|
|
4063
|
+
position,
|
|
4064
|
+
currentAborts,
|
|
4065
|
+
);
|
|
4066
|
+
} catch (error) {
|
|
4067
|
+
// A rejecting abort-finish sink outranks the abort at settlement.
|
|
4068
|
+
settlementError ??= error;
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
signal.throwIfAborted();
|
|
4072
|
+
const candidate = deriveControlCandidates().find(
|
|
4073
|
+
({ action }) => action.id === actionId,
|
|
4074
|
+
);
|
|
4075
|
+
if (candidate === undefined) {
|
|
4076
|
+
receipt = frozenControlReceipt({
|
|
4077
|
+
disposition: 'rejected',
|
|
4078
|
+
reason: `action ${JSON.stringify(actionId)} is not currently advertised`,
|
|
4079
|
+
});
|
|
4080
|
+
} else {
|
|
4081
|
+
// Acceptance is the line after which this boundary always records
|
|
4082
|
+
// and returns a receipt: the requested effect may now exist.
|
|
4083
|
+
accepted = true;
|
|
4084
|
+
try {
|
|
4085
|
+
let run: PlaybookRunResult;
|
|
4086
|
+
if (candidate.kind === 'abandon') {
|
|
4087
|
+
signal.throwIfAborted();
|
|
4088
|
+
run = {
|
|
4089
|
+
outcome: 'unresolved-effect',
|
|
4090
|
+
state: currentState(),
|
|
4091
|
+
};
|
|
4092
|
+
} else {
|
|
4093
|
+
if (candidate.deferredRestoreOperationId !== undefined) {
|
|
4094
|
+
await restoreDeferredReconciliation(
|
|
4095
|
+
candidate.deferredRestoreOperationId,
|
|
4096
|
+
signal,
|
|
4097
|
+
true,
|
|
4098
|
+
);
|
|
4099
|
+
} else {
|
|
4100
|
+
// The host owns receipt reconstruction. Reconciliation only
|
|
4101
|
+
// re-reads its authoritative ledger; it never calls a player
|
|
4102
|
+
// or judge to manufacture missing semantic evidence.
|
|
4103
|
+
refreshReconciliationProjection();
|
|
4104
|
+
}
|
|
4105
|
+
signal.throwIfAborted();
|
|
4106
|
+
run = {
|
|
4107
|
+
outcome: hasUnresolvedReconciliation()
|
|
4108
|
+
? 'no-action'
|
|
4109
|
+
: 'quiescent',
|
|
4110
|
+
state: currentState(),
|
|
4111
|
+
};
|
|
4112
|
+
}
|
|
4113
|
+
if (controlPlaneError !== undefined) throw controlPlaneError;
|
|
4114
|
+
receipt = frozenControlReceipt({
|
|
4115
|
+
disposition: 'executed',
|
|
4116
|
+
run,
|
|
4117
|
+
});
|
|
4118
|
+
} catch (error) {
|
|
4119
|
+
receipt = frozenControlReceipt({
|
|
4120
|
+
disposition: 'failed',
|
|
4121
|
+
error: normalizedControlError(error),
|
|
4122
|
+
});
|
|
4123
|
+
}
|
|
4124
|
+
appliedControlReceipts.set(key, receipt);
|
|
4125
|
+
}
|
|
4126
|
+
} catch (error) {
|
|
4127
|
+
operationError = error;
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
try {
|
|
4131
|
+
await flush();
|
|
4132
|
+
} catch (error) {
|
|
4133
|
+
settlementError = error;
|
|
4134
|
+
}
|
|
4135
|
+
settlementError ??= controlPlaneError;
|
|
4136
|
+
if (accepted && settlementError !== undefined) {
|
|
4137
|
+
receipt = frozenControlReceipt({
|
|
4138
|
+
disposition: 'failed',
|
|
4139
|
+
error: normalizedControlError(settlementError),
|
|
4140
|
+
});
|
|
4141
|
+
appliedControlReceipts.set(key, receipt);
|
|
4142
|
+
settlementError = undefined;
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4145
|
+
if (receipt !== undefined) {
|
|
4146
|
+
const finishFailures: unknown[] = [];
|
|
4147
|
+
try {
|
|
4148
|
+
await emitTrace(
|
|
4149
|
+
'apply.finished',
|
|
4150
|
+
{ actionId, key, ...receipt },
|
|
4151
|
+
position,
|
|
4152
|
+
currentAborts,
|
|
4153
|
+
);
|
|
4154
|
+
} catch (error) {
|
|
4155
|
+
if (!accepted || !isAbortFailure(error, signal)) {
|
|
4156
|
+
collectFailure(finishFailures, error);
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
try {
|
|
4160
|
+
await flush();
|
|
4161
|
+
} catch (error) {
|
|
4162
|
+
if (!accepted || !isAbortFailure(error, signal)) {
|
|
4163
|
+
collectFailure(finishFailures, error);
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
if (finishFailures.length > 0) {
|
|
4167
|
+
const failure =
|
|
4168
|
+
finishFailures.length === 1
|
|
4169
|
+
? finishFailures[0]
|
|
4170
|
+
: new AggregateError(
|
|
4171
|
+
finishFailures,
|
|
4172
|
+
'DECIDE apply settlement emissions failed',
|
|
4173
|
+
);
|
|
4174
|
+
if (accepted) lateDeliveryError = failure;
|
|
4175
|
+
else settlementError = failure;
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
} finally {
|
|
4179
|
+
currentSignal = undefined;
|
|
4180
|
+
currentAborts = undefined;
|
|
4181
|
+
currentTurnId = undefined;
|
|
4182
|
+
controlPlaneError = undefined;
|
|
4183
|
+
if (accepted && receipt !== undefined) {
|
|
4184
|
+
appliedControlReceipts.set(key, receipt);
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
if (lateDeliveryError !== undefined) {
|
|
4188
|
+
collectFailure(emissionFailures, lateDeliveryError);
|
|
4189
|
+
}
|
|
4190
|
+
if (accepted && receipt !== undefined) return receipt;
|
|
4191
|
+
const failure = settlementError ?? operationError;
|
|
4192
|
+
if (failure !== undefined) throw failure;
|
|
4193
|
+
if (receipt === undefined) {
|
|
4194
|
+
throw new Error('decide runtime: apply produced no receipt');
|
|
4195
|
+
}
|
|
4196
|
+
return receipt;
|
|
4197
|
+
};
|
|
4198
|
+
|
|
1805
4199
|
const resultForSnapshot = (signal?: AbortSignal): PlaybookRunResult => {
|
|
1806
4200
|
const live = actor;
|
|
1807
4201
|
if (!live) throw new Error('decide runtime: actor is not initialized');
|
|
@@ -1928,6 +4322,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1928
4322
|
activeEmissionCalls.clear();
|
|
1929
4323
|
emissionQueue.clear();
|
|
1930
4324
|
judgeQueue.clear();
|
|
4325
|
+
semanticCompletionQueue.clear();
|
|
1931
4326
|
actor = undefined;
|
|
1932
4327
|
currentSignal = undefined;
|
|
1933
4328
|
currentAborts = undefined;
|
|
@@ -1945,7 +4340,20 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1945
4340
|
judgeCallSequence = 0;
|
|
1946
4341
|
playerCallSequence = 0;
|
|
1947
4342
|
playbookCallSequence = 0;
|
|
4343
|
+
applyCallSequence = 0;
|
|
1948
4344
|
playbookCallTurnIds.clear();
|
|
4345
|
+
governedOutputsByBoundaryId.clear();
|
|
4346
|
+
governedFailuresByBoundaryId.clear();
|
|
4347
|
+
governedEvidenceByBoundaryId.clear();
|
|
4348
|
+
governedReceiptsByBoundaryId.clear();
|
|
4349
|
+
pendingProposalCohort.clear();
|
|
4350
|
+
activeProposalCohort = undefined;
|
|
4351
|
+
completedProposalCohortTurnId = undefined;
|
|
4352
|
+
deferredOperationId = undefined;
|
|
4353
|
+
hiddenDeferredOperationId = undefined;
|
|
4354
|
+
unresolvedSemanticBoundaryIds.clear();
|
|
4355
|
+
appliedControlReceipts.clear();
|
|
4356
|
+
activeDeferredContinuation = undefined;
|
|
1949
4357
|
lifecycleStarted = false;
|
|
1950
4358
|
};
|
|
1951
4359
|
|
|
@@ -1971,6 +4379,10 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1971
4379
|
ports = identity.ports;
|
|
1972
4380
|
sessionIdentity = identity;
|
|
1973
4381
|
try {
|
|
4382
|
+
if (deferredEffects !== undefined) {
|
|
4383
|
+
effectLedgerMirror = readEffectLedger();
|
|
4384
|
+
synchronizeDeferredProjection(effectLedgerMirror);
|
|
4385
|
+
}
|
|
1974
4386
|
suppressInspectionEmissions = false;
|
|
1975
4387
|
createRuntimeActor();
|
|
1976
4388
|
const state = currentState();
|
|
@@ -1989,6 +4401,10 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1989
4401
|
}
|
|
1990
4402
|
},
|
|
1991
4403
|
|
|
4404
|
+
describe: describeControlView,
|
|
4405
|
+
unresolvedEffectEnvelopes: unresolvedEffectEnvelopeIdentities,
|
|
4406
|
+
apply: applyControlAction,
|
|
4407
|
+
|
|
1992
4408
|
// DR-014 §1 / DR-031 §5 / PBRT-45: JSON-safe capture of a parked
|
|
1993
4409
|
// session, including one already-started suspended REVIEW call.
|
|
1994
4410
|
// Defined only at a safe capture point — initialized, not disposing
|
|
@@ -2045,8 +4461,12 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2045
4461
|
const context = (
|
|
2046
4462
|
actor.getSnapshot() as SnapshotFrom<typeof decideMachine>
|
|
2047
4463
|
).context as unknown as Record<string, unknown>;
|
|
4464
|
+
if (deferredEffects !== undefined) {
|
|
4465
|
+
synchronizeDeferredProjection(readEffectLedger());
|
|
4466
|
+
}
|
|
4467
|
+
const unresolved = hasUnresolvedReconciliation();
|
|
2048
4468
|
return {
|
|
2049
|
-
schemaVersion:
|
|
4469
|
+
schemaVersion: 4,
|
|
2050
4470
|
playbookId: sessionIdentity.playbookId,
|
|
2051
4471
|
machine,
|
|
2052
4472
|
roleResumeTokens: snapshotRoleResumeTokens(),
|
|
@@ -2058,14 +4478,15 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2058
4478
|
playbookCall: playbookCallSequence,
|
|
2059
4479
|
},
|
|
2060
4480
|
state,
|
|
2061
|
-
pendingBossQuestions:
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
4481
|
+
pendingBossQuestions: unresolved
|
|
4482
|
+
? []
|
|
4483
|
+
: visiblePendingQuestionsForState(state, context).map((pending) => ({
|
|
4484
|
+
questionId: pending.questionId,
|
|
4485
|
+
asker: pending.asker,
|
|
4486
|
+
question: pending.question,
|
|
4487
|
+
sourceItem: pending.sourceItem,
|
|
4488
|
+
})),
|
|
4489
|
+
effectLedger: effectLedgerMirror,
|
|
2069
4490
|
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
2070
4491
|
};
|
|
2071
4492
|
},
|
|
@@ -2095,6 +4516,28 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2095
4516
|
identity.playbookId,
|
|
2096
4517
|
{ allowSuspendedCall: true },
|
|
2097
4518
|
);
|
|
4519
|
+
if (deferredEffects === undefined) {
|
|
4520
|
+
if (
|
|
4521
|
+
boundSnapshot.effectLedger.revision !== 0 ||
|
|
4522
|
+
boundSnapshot.effectLedger.boundaries.length !== 0 ||
|
|
4523
|
+
boundSnapshot.effectLedger.logicalOperations.length !== 0
|
|
4524
|
+
) {
|
|
4525
|
+
throw new TypeError(
|
|
4526
|
+
'decide runtime snapshot effectLedger must be the canonical empty ledger',
|
|
4527
|
+
);
|
|
4528
|
+
}
|
|
4529
|
+
} else {
|
|
4530
|
+
const current = readEffectLedger();
|
|
4531
|
+
if (
|
|
4532
|
+
stableJson(current, 'DECIDE current effect ledger') !==
|
|
4533
|
+
stableJson(boundSnapshot.effectLedger, 'DECIDE snapshot effect ledger')
|
|
4534
|
+
) {
|
|
4535
|
+
throw new TypeError(
|
|
4536
|
+
'decide runtime snapshot effectLedger must equal the current host mirror',
|
|
4537
|
+
);
|
|
4538
|
+
}
|
|
4539
|
+
effectLedgerMirror = current;
|
|
4540
|
+
}
|
|
2098
4541
|
const suspendedCall = boundSnapshot.suspendedCall;
|
|
2099
4542
|
let finishInitialization!: () => void;
|
|
2100
4543
|
const initialization = new Promise<void>((resolve) => {
|
|
@@ -2113,6 +4556,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2113
4556
|
judgeCallSequence = boundSnapshot.sequences.judgeCall;
|
|
2114
4557
|
playerCallSequence = boundSnapshot.sequences.playerCall;
|
|
2115
4558
|
playbookCallSequence = boundSnapshot.sequences.playbookCall;
|
|
4559
|
+
applyCallSequence = boundSnapshot.sequences.trace;
|
|
2116
4560
|
if (identity.playerSessions) {
|
|
2117
4561
|
priorExternalRoleTokens = snapshotRoleResumeTokens();
|
|
2118
4562
|
}
|
|
@@ -2121,6 +4565,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2121
4565
|
if (suspendedCall !== undefined) {
|
|
2122
4566
|
playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
|
|
2123
4567
|
}
|
|
4568
|
+
synchronizeDeferredProjection(effectLedgerMirror);
|
|
2124
4569
|
suppressInspectionEmissions = true;
|
|
2125
4570
|
createRuntimeActor(boundSnapshot.machine);
|
|
2126
4571
|
actor?.start();
|
|
@@ -2203,23 +4648,91 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2203
4648
|
const event = await classify(turn.text, turn.signal);
|
|
2204
4649
|
if (!event) {
|
|
2205
4650
|
const state = currentState();
|
|
2206
|
-
|
|
4651
|
+
if (!hasUnresolvedReconciliation()) {
|
|
4652
|
+
await emitBoundaryStatus('No playbook action classified.', state);
|
|
4653
|
+
}
|
|
2207
4654
|
result = { outcome: 'no-action', state };
|
|
2208
4655
|
} else if (event.type === 'NO_ACTION') {
|
|
2209
4656
|
result = { outcome: 'no-action', state: currentState() };
|
|
2210
4657
|
} else {
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
4658
|
+
const before = currentState();
|
|
4659
|
+
const boundCommitWait =
|
|
4660
|
+
deferredEffects !== undefined &&
|
|
4661
|
+
deferredOperationId !== undefined &&
|
|
4662
|
+
before.activeStateIds.includes('awaitBossReply');
|
|
4663
|
+
if (boundCommitWait && event.type === 'BOSS_INTERRUPT') {
|
|
4664
|
+
await parkDeferredContinuation(turn.signal);
|
|
4665
|
+
result = { outcome: 'no-action', state: currentState() };
|
|
4666
|
+
} else {
|
|
4667
|
+
const prepared =
|
|
4668
|
+
boundCommitWait &&
|
|
4669
|
+
event.type === 'BOSS_REPLY' &&
|
|
4670
|
+
event.questionId === 'commitCoderProposal'
|
|
4671
|
+
? await prepareDeferredContinuation(turn.signal, event)
|
|
4672
|
+
: undefined;
|
|
4673
|
+
if (prepared?.proceed === false) {
|
|
4674
|
+
result = { outcome: 'no-action', state: currentState() };
|
|
4675
|
+
} else {
|
|
4676
|
+
await emitBoundaryStatus(event.type, before);
|
|
4677
|
+
if (actor.getSnapshot().status === 'done') {
|
|
4678
|
+
stopActor();
|
|
4679
|
+
startActor();
|
|
4680
|
+
}
|
|
2216
4681
|
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
4682
|
+
const deferredMachineCheckpoint =
|
|
4683
|
+
prepared?.proceed === true
|
|
4684
|
+
? detachPersistedMachineSnapshot(
|
|
4685
|
+
actor.getPersistedSnapshot(),
|
|
4686
|
+
)
|
|
4687
|
+
: undefined;
|
|
4688
|
+
if (deferredMachineCheckpoint !== undefined) {
|
|
4689
|
+
suppressInspectionEmissions = true;
|
|
4690
|
+
}
|
|
2220
4691
|
|
|
2221
|
-
|
|
2222
|
-
|
|
4692
|
+
try {
|
|
4693
|
+
actor.send(event);
|
|
4694
|
+
await driveToQuiescence();
|
|
4695
|
+
await drainBoundaryCallsAndEmissions();
|
|
4696
|
+
await prepared?.acknowledgement;
|
|
4697
|
+
if (
|
|
4698
|
+
deferredMachineCheckpoint !== undefined &&
|
|
4699
|
+
hiddenDeferredOperationId !== undefined
|
|
4700
|
+
) {
|
|
4701
|
+
stopActor();
|
|
4702
|
+
createRuntimeActor(deferredMachineCheckpoint);
|
|
4703
|
+
actor.start();
|
|
4704
|
+
previousState = before;
|
|
4705
|
+
suppressInspectionEmissions = false;
|
|
4706
|
+
}
|
|
4707
|
+
} catch (error) {
|
|
4708
|
+
activeDeferredContinuation?.result.reject(error);
|
|
4709
|
+
if (deferredEffects !== undefined) {
|
|
4710
|
+
try {
|
|
4711
|
+
synchronizeDeferredProjection(readEffectLedger());
|
|
4712
|
+
hiddenDeferredOperationId ??= deferredOperationId;
|
|
4713
|
+
} catch {
|
|
4714
|
+
hiddenDeferredOperationId ??= deferredOperationId;
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
if (deferredMachineCheckpoint !== undefined) {
|
|
4718
|
+
try {
|
|
4719
|
+
stopActor();
|
|
4720
|
+
createRuntimeActor(deferredMachineCheckpoint);
|
|
4721
|
+
actor.start();
|
|
4722
|
+
previousState = before;
|
|
4723
|
+
} finally {
|
|
4724
|
+
suppressInspectionEmissions = false;
|
|
4725
|
+
}
|
|
4726
|
+
}
|
|
4727
|
+
throw error;
|
|
4728
|
+
} finally {
|
|
4729
|
+
activeDeferredContinuation = undefined;
|
|
4730
|
+
}
|
|
4731
|
+
|
|
4732
|
+
if (controlPlaneError !== undefined) throw controlPlaneError;
|
|
4733
|
+
result = resultForSnapshot(turn.signal);
|
|
4734
|
+
}
|
|
4735
|
+
}
|
|
2223
4736
|
}
|
|
2224
4737
|
}
|
|
2225
4738
|
settlement = {
|
|
@@ -2330,6 +4843,10 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2330
4843
|
if (currentSignal !== undefined) {
|
|
2331
4844
|
throw new Error('decide runtime: another runtime turn is active');
|
|
2332
4845
|
}
|
|
4846
|
+
refreshReconciliationProjection();
|
|
4847
|
+
if (hasUnresolvedReconciliation()) {
|
|
4848
|
+
return { outcome: 'no-action', state: currentState() };
|
|
4849
|
+
}
|
|
2333
4850
|
currentTurnId = playbookCallTurnIds.get(callId);
|
|
2334
4851
|
currentSignal = signal;
|
|
2335
4852
|
currentAborts = abortReasonClassifier(signal);
|
|
@@ -2462,6 +4979,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2462
4979
|
activeEmissionCalls.clear();
|
|
2463
4980
|
emissionQueue.clear();
|
|
2464
4981
|
judgeQueue.clear();
|
|
4982
|
+
semanticCompletionQueue.clear();
|
|
2465
4983
|
actor = undefined;
|
|
2466
4984
|
currentSignal = undefined;
|
|
2467
4985
|
currentAborts = undefined;
|
|
@@ -2472,6 +4990,19 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2472
4990
|
sessionIdentity = undefined;
|
|
2473
4991
|
previousState = undefined;
|
|
2474
4992
|
controlPlaneError = undefined;
|
|
4993
|
+
governedOutputsByBoundaryId.clear();
|
|
4994
|
+
governedFailuresByBoundaryId.clear();
|
|
4995
|
+
governedEvidenceByBoundaryId.clear();
|
|
4996
|
+
governedReceiptsByBoundaryId.clear();
|
|
4997
|
+
pendingProposalCohort.clear();
|
|
4998
|
+
activeProposalCohort = undefined;
|
|
4999
|
+
completedProposalCohortTurnId = undefined;
|
|
5000
|
+
deferredOperationId = undefined;
|
|
5001
|
+
hiddenDeferredOperationId = undefined;
|
|
5002
|
+
unresolvedSemanticBoundaryIds.clear();
|
|
5003
|
+
appliedControlReceipts.clear();
|
|
5004
|
+
applyCallSequence = 0;
|
|
5005
|
+
activeDeferredContinuation = undefined;
|
|
2475
5006
|
disposed = true;
|
|
2476
5007
|
}
|
|
2477
5008
|
if (failures.length === 1) throw failures[0];
|
|
@@ -2488,6 +5019,16 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2488
5019
|
return nestedBridge;
|
|
2489
5020
|
},
|
|
2490
5021
|
};
|
|
5022
|
+
}
|
|
5023
|
+
|
|
5024
|
+
export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
5025
|
+
DecidePlaybookRuntimeConstruction
|
|
5026
|
+
> = (factoryInput) => {
|
|
5027
|
+
const construction = schema3Construction(factoryInput);
|
|
5028
|
+
return createDecidePlaybookRuntime(
|
|
5029
|
+
construction.configuredOptions,
|
|
5030
|
+
construction.hostCapabilities,
|
|
5031
|
+
);
|
|
2491
5032
|
};
|
|
2492
5033
|
|
|
2493
5034
|
export const _internal = {
|
|
@@ -2497,7 +5038,6 @@ export const _internal = {
|
|
|
2497
5038
|
buildClassifierPrompt,
|
|
2498
5039
|
parseClassification,
|
|
2499
5040
|
buildAdjudicatorPrompt,
|
|
2500
|
-
parseAdjudication,
|
|
2501
5041
|
combineSignals,
|
|
2502
5042
|
pendingQuestionsFromContext,
|
|
2503
5043
|
pendingQuestionsForState,
|
|
@@ -2508,6 +5048,7 @@ export const _internal = {
|
|
|
2508
5048
|
ROLE_STATE_IDS,
|
|
2509
5049
|
VERBATIM_PAYLOAD_FIELDS,
|
|
2510
5050
|
BOSS_INTERRUPT_TARGETS,
|
|
5051
|
+
UNFINISHED_FINAL_STATE_IDS,
|
|
2511
5052
|
CONTINUATION_PREAMBLE,
|
|
2512
5053
|
TELEMETRY_TOPIC,
|
|
2513
5054
|
};
|