@sublang/playbook 8.0.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/docs/cli.md +66 -23
- package/docs/configuration.md +13 -8
- package/docs/embedding.md +45 -14
- package/package.json +7 -3
- package/reference/sdlc/captain.md +14 -10
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +33 -13
- package/reference/sdlc/captain.playbook/captain.fsm.js +80 -9
- package/reference/sdlc/captain.playbook/captain.fsm.ts +137 -18
- package/reference/sdlc/captain.playbook/captain.gears.md +10 -6
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +5 -1
- package/reference/sdlc/captain.playbook/captain.playbook.js +151 -10
- package/reference/sdlc/captain.playbook/captain.playbook.ts +200 -14
- package/reference/sdlc/code.md +0 -1
- package/reference/sdlc/code.playbook/bin/interactive-session.js +170 -17
- package/reference/sdlc/code.playbook/bin/launch-config.js +136 -4
- package/reference/sdlc/code.playbook/bin/playbook.js +81 -4
- package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
- package/reference/sdlc/code.playbook/bin/run.js +365 -63
- package/reference/sdlc/code.playbook/bin/session-store.js +2877 -209
- package/reference/sdlc/code.playbook/code.fsm.d.ts +11 -1
- package/reference/sdlc/code.playbook/code.fsm.js +85 -29
- package/reference/sdlc/code.playbook/code.fsm.ts +95 -33
- package/reference/sdlc/code.playbook/code.gears.md +0 -2
- package/reference/sdlc/code.playbook/code.playbook.d.ts +5 -2
- package/reference/sdlc/code.playbook/code.playbook.js +67 -4
- package/reference/sdlc/code.playbook/code.playbook.ts +87 -8
- package/reference/sdlc/code.playbook/code.registry.d.ts +10 -3
- package/reference/sdlc/code.playbook/code.registry.js +10 -3
- package/reference/sdlc/code.playbook/code.registry.ts +23 -5
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +99 -7
- package/reference/sdlc/code.playbook/playbook-captain.js +1894 -82
- package/reference/sdlc/code.playbook/playbook-captain.ts +2809 -109
- package/reference/sdlc/decide.md +0 -1
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +8 -1
- package/reference/sdlc/decide.playbook/decide.fsm.js +80 -29
- package/reference/sdlc/decide.playbook/decide.fsm.ts +89 -31
- package/reference/sdlc/decide.playbook/decide.gears.md +0 -1
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +15 -5
- package/reference/sdlc/decide.playbook/decide.playbook.js +1994 -191
- package/reference/sdlc/decide.playbook/decide.playbook.ts +3209 -404
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +7 -3
- package/reference/sdlc/decide.playbook/decide.registry.js +10 -3
- package/reference/sdlc/decide.playbook/decide.registry.ts +20 -5
- package/reference/sdlc/review.playbook/review.fsm.d.ts +7 -0
- package/reference/sdlc/review.playbook/review.fsm.js +133 -12
- package/reference/sdlc/review.playbook/review.fsm.ts +140 -12
- package/reference/sdlc/review.playbook/review.playbook.d.ts +5 -2
- package/reference/sdlc/review.playbook/review.playbook.js +78 -4
- package/reference/sdlc/review.playbook/review.playbook.ts +95 -8
- package/reference/sdlc/review.playbook/review.registry.d.ts +10 -3
- package/reference/sdlc/review.playbook/review.registry.js +10 -3
- package/reference/sdlc/review.playbook/review.registry.ts +23 -5
- package/slc/gears2fsm.md +25 -7
- package/slc/link.md +727 -82
- package/src/accepted-outcome.d.ts +18 -0
- package/src/accepted-outcome.js +94 -0
- package/src/accepted-outcome.ts +140 -0
- package/src/runtime.d.ts +165 -3
- package/src/runtime.ts +214 -2
- package/src/xstate-playbook-runtime.d.ts +162 -13
- package/src/xstate-playbook-runtime.js +3344 -564
- package/src/xstate-playbook-runtime.ts +4873 -637
- package/src/xstate-runtime.d.ts +76 -8
- package/src/xstate-runtime.js +1001 -64
- package/src/xstate-runtime.ts +1640 -91
|
@@ -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)) {
|
|
@@ -114,20 +143,51 @@ function snapshotDecideRuntimeOptions(value: unknown): PlaybookRuntimeOptions {
|
|
|
114
143
|
return Object.freeze({});
|
|
115
144
|
}
|
|
116
145
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
};
|
|
146
|
+
interface AuthoredStateConfig {
|
|
147
|
+
readonly meta?: {
|
|
148
|
+
readonly playbook?: {
|
|
149
|
+
readonly stateId?: unknown;
|
|
150
|
+
readonly description?: unknown;
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
readonly states?: Readonly<Record<string, AuthoredStateConfig>>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function authoredStateDescriptions(
|
|
157
|
+
states: Readonly<Record<string, AuthoredStateConfig>> | undefined,
|
|
158
|
+
): Readonly<Record<string, string>> {
|
|
159
|
+
const descriptions: Record<string, string> = {};
|
|
160
|
+
const visit = (
|
|
161
|
+
children: Readonly<Record<string, AuthoredStateConfig>> | undefined,
|
|
162
|
+
): void => {
|
|
163
|
+
for (const state of Object.values(children ?? {})) {
|
|
164
|
+
const stateId = state.meta?.playbook?.stateId;
|
|
165
|
+
const description = state.meta?.playbook?.description;
|
|
166
|
+
if (
|
|
167
|
+
typeof stateId === 'string' &&
|
|
168
|
+
typeof description === 'string' &&
|
|
169
|
+
description.trim().length > 0
|
|
170
|
+
) {
|
|
171
|
+
const existing = descriptions[stateId];
|
|
172
|
+
if (existing !== undefined && existing !== description) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`DECIDE state ${stateId} declares conflicting descriptions`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
descriptions[stateId] = description;
|
|
178
|
+
}
|
|
179
|
+
visit(state.states);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
visit(states);
|
|
183
|
+
return Object.freeze(descriptions);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const STATE_DESCRIPTIONS = authoredStateDescriptions(
|
|
187
|
+
decideMachine.config.states as
|
|
188
|
+
| Readonly<Record<string, AuthoredStateConfig>>
|
|
189
|
+
| undefined,
|
|
190
|
+
);
|
|
131
191
|
|
|
132
192
|
const ROLE_STATES = [
|
|
133
193
|
{ stateId: 'askCoderProposal', role: 'coder', sourceItem: 'DECIDE-1' },
|
|
@@ -143,6 +203,61 @@ const ROLE_STATE_IDS: ReadonlySet<string> = new Set(
|
|
|
143
203
|
ROLE_STATES.map((state) => state.stateId),
|
|
144
204
|
);
|
|
145
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
|
+
|
|
146
261
|
const ROLE_IDS = ['coder', 'reviewer'] as const;
|
|
147
262
|
const ROLE_ID_SET: ReadonlySet<string> = new Set(ROLE_IDS);
|
|
148
263
|
|
|
@@ -154,9 +269,16 @@ const BOSS_INTERRUPT_TARGETS = ['independentProposals'] as const;
|
|
|
154
269
|
const BOSS_INTERRUPT_TARGET_IDS: ReadonlySet<string> = new Set(
|
|
155
270
|
BOSS_INTERRUPT_TARGETS,
|
|
156
271
|
);
|
|
272
|
+
const UNFINISHED_FINAL_STATE_IDS: ReadonlySet<string> = new Set([
|
|
273
|
+
'reportedReviewFailure',
|
|
274
|
+
]);
|
|
157
275
|
|
|
158
276
|
const TELEMETRY_TOPIC = 'playbook.fsm.state';
|
|
159
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';
|
|
160
282
|
|
|
161
283
|
const CONTINUATION_PREAMBLE =
|
|
162
284
|
'You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.';
|
|
@@ -230,6 +352,44 @@ function requiredFieldsFor(description: string): string[] {
|
|
|
230
352
|
return fields;
|
|
231
353
|
}
|
|
232
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
|
+
|
|
233
393
|
// LLM judges routinely wrap JSON in prose/fences or damage its tail. Match
|
|
234
394
|
// CODE's recovery contract: scan candidate starts in document order, prefer a
|
|
235
395
|
// strict balanced value at each position, then repair trailing commas and
|
|
@@ -298,6 +458,14 @@ function stableJson(value: unknown, path: string): string {
|
|
|
298
458
|
return JSON.stringify(sortJson(snapshotJsonValue(value, path)));
|
|
299
459
|
}
|
|
300
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
|
+
|
|
301
469
|
function stripCodeFence(text: string): string {
|
|
302
470
|
const fence = text.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/i);
|
|
303
471
|
return fence ? fence[1].trim() : text;
|
|
@@ -461,7 +629,9 @@ function parseClassification(
|
|
|
461
629
|
function buildAdjudicatorPrompt(
|
|
462
630
|
input: PlayerInput,
|
|
463
631
|
playerOutput: string,
|
|
632
|
+
correction?: { readonly reply: string; readonly error: string },
|
|
464
633
|
): string {
|
|
634
|
+
const outcomes = governedOutcomesFor(input);
|
|
465
635
|
const lines: string[] = [];
|
|
466
636
|
lines.push('You are the guard adjudicator for a playbook state machine.');
|
|
467
637
|
lines.push(
|
|
@@ -483,72 +653,44 @@ function buildAdjudicatorPrompt(
|
|
|
483
653
|
'Guards (choose exactly one; the descriptions are authoritative and must be applied as written):',
|
|
484
654
|
);
|
|
485
655
|
for (const [guard, description] of Object.entries(input.result)) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
for (const description of Object.values(input.result)) {
|
|
490
|
-
for (const field of requiredFieldsFor(description)) {
|
|
491
|
-
if (VERBATIM_PAYLOAD_FIELDS.has(field)) runtimeOwnedFields.add(field);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
if (runtimeOwnedFields.size > 0) {
|
|
495
|
-
lines.push('');
|
|
656
|
+
const semanticFields = Object.entries(outcomes[guard]?.fields ?? {})
|
|
657
|
+
.filter(([, authority]) => authority === 'semantic')
|
|
658
|
+
.map(([field]) => field);
|
|
496
659
|
lines.push(
|
|
497
|
-
|
|
660
|
+
`- ${guard}: semantic fields: ${
|
|
661
|
+
semanticFields.length === 0 ? '(none)' : semanticFields.join(', ')
|
|
662
|
+
}; ${description}`,
|
|
498
663
|
);
|
|
499
664
|
}
|
|
500
665
|
lines.push('');
|
|
501
666
|
lines.push(
|
|
502
|
-
'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.',
|
|
503
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
|
+
}
|
|
504
683
|
return lines.join('\n');
|
|
505
684
|
}
|
|
506
685
|
|
|
507
|
-
function
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if (!obj || typeof obj.guard !== 'string' || obj.guard.trim() === '') {
|
|
514
|
-
throw new Error('adjudicator returned empty or malformed JSON');
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
const guard = obj.guard;
|
|
518
|
-
if (!Object.prototype.hasOwnProperty.call(input.result, guard)) {
|
|
519
|
-
throw new Error(
|
|
520
|
-
`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',
|
|
521
692
|
);
|
|
522
693
|
}
|
|
523
|
-
|
|
524
|
-
const requiredFields = requiredFieldsFor(input.result[guard]);
|
|
525
|
-
const allowedFields = new Set(['guard', ...requiredFields]);
|
|
526
|
-
for (const key of Reflect.ownKeys(obj)) {
|
|
527
|
-
if (typeof key !== 'string' || !allowedFields.has(key)) {
|
|
528
|
-
throw new Error(
|
|
529
|
-
`adjudicator response for guard "${guard}" included undeclared field "${String(key)}"`,
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
const output: Record<string, unknown> = { guard };
|
|
534
|
-
for (const field of requiredFields) {
|
|
535
|
-
if (VERBATIM_PAYLOAD_FIELDS.has(field)) {
|
|
536
|
-
output[field] = finalText;
|
|
537
|
-
continue;
|
|
538
|
-
}
|
|
539
|
-
const value = obj[field];
|
|
540
|
-
if (
|
|
541
|
-
value === undefined ||
|
|
542
|
-
value === null ||
|
|
543
|
-
(typeof value === 'string' && value.trim() === '')
|
|
544
|
-
) {
|
|
545
|
-
throw new Error(
|
|
546
|
-
`adjudicator response for guard "${guard}" missing required field "${field}"`,
|
|
547
|
-
);
|
|
548
|
-
}
|
|
549
|
-
output[field] = value;
|
|
550
|
-
}
|
|
551
|
-
return output as PlayerOutput;
|
|
552
694
|
}
|
|
553
695
|
|
|
554
696
|
function combineSignals(
|
|
@@ -580,10 +722,394 @@ function isEmptyFinalText(finalText: string | undefined): boolean {
|
|
|
580
722
|
return finalText === undefined || finalText.trim().length === 0;
|
|
581
723
|
}
|
|
582
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
|
+
|
|
583
1088
|
function isAbortFailure(error: unknown, signal: AbortSignal): boolean {
|
|
584
1089
|
return signal.aborted && Object.is(error, signal.reason);
|
|
585
1090
|
}
|
|
586
1091
|
|
|
1092
|
+
interface AbortReasonClassifier {
|
|
1093
|
+
isAbortReason(error: unknown): boolean;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function abortReasonClassifier(
|
|
1097
|
+
...sources: readonly (AbortSignal | AbortReasonClassifier | undefined)[]
|
|
1098
|
+
): AbortReasonClassifier {
|
|
1099
|
+
const captured = sources.filter(
|
|
1100
|
+
(source): source is AbortSignal | AbortReasonClassifier =>
|
|
1101
|
+
source !== undefined,
|
|
1102
|
+
);
|
|
1103
|
+
return Object.freeze({
|
|
1104
|
+
isAbortReason: (error: unknown): boolean =>
|
|
1105
|
+
captured.some((source) =>
|
|
1106
|
+
source instanceof AbortSignal
|
|
1107
|
+
? isAbortFailure(error, source)
|
|
1108
|
+
: source.isAbortReason(error),
|
|
1109
|
+
),
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
|
|
587
1113
|
function pendingQuestionsFromContext(
|
|
588
1114
|
context: Record<string, unknown>,
|
|
589
1115
|
): PendingBossQuestion[] {
|
|
@@ -635,6 +1161,33 @@ const STATUS_STATE_IDS: ReadonlySet<string> = new Set([
|
|
|
635
1161
|
'failed',
|
|
636
1162
|
]);
|
|
637
1163
|
|
|
1164
|
+
// PBRT-45: a question is pending only while its authored reply-wait state
|
|
1165
|
+
// is active. The context retains an answered question through the resumed
|
|
1166
|
+
// player call so the Q+A continuation prompt can quote it, and each branch
|
|
1167
|
+
// keeps its own entry through the parallel region — so an unfiltered
|
|
1168
|
+
// projection would report the answered question as still awaiting during
|
|
1169
|
+
// the resume, and both branch questions after only one remains pending.
|
|
1170
|
+
const RESUME_WAIT_STATE_IDS: Readonly<Record<string, string>> = {
|
|
1171
|
+
...Object.fromEntries(
|
|
1172
|
+
Object.entries(WAIT_STATE_RESUME_IDS).map(([waitStateId, resumeStateId]) => [
|
|
1173
|
+
resumeStateId,
|
|
1174
|
+
waitStateId,
|
|
1175
|
+
]),
|
|
1176
|
+
),
|
|
1177
|
+
commitCoderProposal: 'awaitBossReply',
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
function pendingQuestionsForState(
|
|
1181
|
+
state: PlaybookState,
|
|
1182
|
+
context: Record<string, unknown>,
|
|
1183
|
+
): PendingBossQuestion[] {
|
|
1184
|
+
return pendingQuestionsFromContext(context).filter((pending) =>
|
|
1185
|
+
state.activeStateIds.includes(
|
|
1186
|
+
RESUME_WAIT_STATE_IDS[pending.resumeStateId] ?? '',
|
|
1187
|
+
),
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
638
1191
|
function questionForWaitState(
|
|
639
1192
|
stateId: string,
|
|
640
1193
|
pendingQuestions: readonly PendingBossQuestion[],
|
|
@@ -687,8 +1240,12 @@ function telemetryPayload(
|
|
|
687
1240
|
state: PlaybookState,
|
|
688
1241
|
event: unknown,
|
|
689
1242
|
context: Record<string, unknown>,
|
|
1243
|
+
hiddenQuestionId?: string,
|
|
690
1244
|
): JsonValue {
|
|
691
|
-
const pendingBossQuestions =
|
|
1245
|
+
const pendingBossQuestions = pendingQuestionsForState(
|
|
1246
|
+
state,
|
|
1247
|
+
context,
|
|
1248
|
+
).filter(({ questionId }) => questionId !== hiddenQuestionId);
|
|
692
1249
|
const prior = previousState ?? state;
|
|
693
1250
|
const payload = {
|
|
694
1251
|
from: prior.value,
|
|
@@ -705,10 +1262,56 @@ function telemetryPayload(
|
|
|
705
1262
|
return payload;
|
|
706
1263
|
}
|
|
707
1264
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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);
|
|
711
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
|
+
);
|
|
712
1315
|
|
|
713
1316
|
type SessionIdentity = Readonly<PlaybookSession>;
|
|
714
1317
|
|
|
@@ -716,6 +1319,9 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
716
1319
|
let sessionIdentity: SessionIdentity | undefined;
|
|
717
1320
|
let actor: ReturnType<typeof createActor> | undefined;
|
|
718
1321
|
let currentSignal: AbortSignal | undefined;
|
|
1322
|
+
let currentAborts: AbortReasonClassifier | undefined;
|
|
1323
|
+
const actorSettlementAborts: AbortReasonClassifier[] = [];
|
|
1324
|
+
let actorSettlementErrorAborts: AbortReasonClassifier | undefined;
|
|
719
1325
|
let currentTurnId: number | undefined;
|
|
720
1326
|
let previousState: PlaybookState | undefined;
|
|
721
1327
|
let suppressInspectionEmissions = false;
|
|
@@ -725,6 +1331,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
725
1331
|
let judgeCallSequence = 0;
|
|
726
1332
|
let playerCallSequence = 0;
|
|
727
1333
|
let playbookCallSequence = 0;
|
|
1334
|
+
let applyCallSequence = 0;
|
|
728
1335
|
let lifecycleStarted = false;
|
|
729
1336
|
let initInFlight: Promise<void> | undefined;
|
|
730
1337
|
let disposed = false;
|
|
@@ -738,6 +1345,35 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
738
1345
|
const activeEmissionCalls = new Set<Promise<void>>();
|
|
739
1346
|
const emissionQueue = new PQueue({ concurrency: 1 });
|
|
740
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;
|
|
741
1377
|
|
|
742
1378
|
const collectFailure = (failures: unknown[], error: unknown): void => {
|
|
743
1379
|
if (error instanceof AggregateError) {
|
|
@@ -754,26 +1390,35 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
754
1390
|
): void => {
|
|
755
1391
|
if (!isAbortFailure(error, signal)) controlPlaneError ??= error;
|
|
756
1392
|
};
|
|
757
|
-
const latchInspectionError = (
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1393
|
+
const latchInspectionError = (
|
|
1394
|
+
error: unknown,
|
|
1395
|
+
aborts: AbortReasonClassifier | undefined = currentAborts,
|
|
1396
|
+
): void => {
|
|
1397
|
+
if (aborts?.isAbortReason(error)) return;
|
|
1398
|
+
if (currentSignal !== undefined) controlPlaneError ??= error;
|
|
1399
|
+
else collectFailure(emissionFailures, error);
|
|
763
1400
|
};
|
|
764
|
-
const enqueue = (
|
|
1401
|
+
const enqueue = (
|
|
1402
|
+
fn: () => Promise<void>,
|
|
1403
|
+
aborts: AbortReasonClassifier | undefined = currentAborts,
|
|
1404
|
+
): Promise<void> => {
|
|
1405
|
+
const enqueueAborts = aborts;
|
|
765
1406
|
const queued = emissionQueue.add(fn);
|
|
766
1407
|
activeEmissionCalls.add(queued);
|
|
767
1408
|
void queued.then(
|
|
768
1409
|
() => activeEmissionCalls.delete(queued),
|
|
769
1410
|
(error: unknown) => {
|
|
770
1411
|
activeEmissionCalls.delete(queued);
|
|
771
|
-
|
|
1412
|
+
if (!enqueueAborts?.isAbortReason(error)) {
|
|
1413
|
+
collectFailure(emissionFailures, error);
|
|
1414
|
+
}
|
|
772
1415
|
},
|
|
773
1416
|
);
|
|
774
1417
|
return queued;
|
|
775
1418
|
};
|
|
776
|
-
const flush = async (
|
|
1419
|
+
const flush = async (
|
|
1420
|
+
_aborts: AbortReasonClassifier | undefined = currentAborts,
|
|
1421
|
+
): Promise<void> => {
|
|
777
1422
|
while (true) {
|
|
778
1423
|
const active = [...activeEmissionCalls];
|
|
779
1424
|
if (active.length > 0) await Promise.allSettled(active);
|
|
@@ -789,8 +1434,15 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
789
1434
|
if (emissionFailures.length === 0) return;
|
|
790
1435
|
const failures = emissionFailures;
|
|
791
1436
|
emissionFailures = [];
|
|
792
|
-
|
|
793
|
-
|
|
1437
|
+
const failure =
|
|
1438
|
+
failures.length === 1
|
|
1439
|
+
? failures[0]
|
|
1440
|
+
: new AggregateError(failures, 'decide runtime emissions failed');
|
|
1441
|
+
// Enqueue ownership already classified every stored failure as distinct.
|
|
1442
|
+
// Preserve that classification if an unrelated public boundary drains
|
|
1443
|
+
// it with a signal whose reason happens to be the same object.
|
|
1444
|
+
if (currentSignal !== undefined) controlPlaneError ??= failure;
|
|
1445
|
+
throw failure;
|
|
794
1446
|
};
|
|
795
1447
|
const drainBoundaryCallsAndEmissions = async (): Promise<void> => {
|
|
796
1448
|
while (true) {
|
|
@@ -824,6 +1476,11 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
824
1476
|
|
|
825
1477
|
const bindSession = (nextSession: PlaybookSession): SessionIdentity => {
|
|
826
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
|
+
}
|
|
827
1484
|
if (bound.roleBindings === undefined) return bound;
|
|
828
1485
|
const actual = Object.keys(bound.roleBindings).sort();
|
|
829
1486
|
const expected = [...ROLE_IDS].sort();
|
|
@@ -1004,68 +1661,379 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1004
1661
|
pendingCall,
|
|
1005
1662
|
});
|
|
1006
1663
|
};
|
|
1007
|
-
const
|
|
1008
|
-
return state.stateId === undefined ? {} : { stateId: state.stateId };
|
|
1009
|
-
};
|
|
1010
|
-
const enqueueTracedEmission = (
|
|
1011
|
-
type: PlaybookTraceType,
|
|
1012
|
-
payload: unknown,
|
|
1013
|
-
meta: { turnId?: number; callId?: string } = {},
|
|
1014
|
-
describedEmission?: (runtimePorts: PlaybookPorts) => Promise<void>,
|
|
1015
|
-
): Promise<void> => {
|
|
1016
|
-
const runtimePorts = requirePorts();
|
|
1017
|
-
const identity = requireSessionIdentity();
|
|
1018
|
-
const jsonPayload = snapshotJsonValue(payload, `trace ${type} payload`);
|
|
1019
|
-
const trace: PlaybookTraceEvent = Object.freeze({
|
|
1020
|
-
schemaVersion: 3,
|
|
1021
|
-
sessionId: identity.sessionId,
|
|
1022
|
-
playbookId: identity.playbookId,
|
|
1023
|
-
rootSessionId: identity.rootSessionId,
|
|
1024
|
-
...(identity.parentSessionId !== undefined
|
|
1025
|
-
? { parentSessionId: identity.parentSessionId }
|
|
1026
|
-
: {}),
|
|
1027
|
-
...(identity.parentCallId !== undefined
|
|
1028
|
-
? { parentCallId: identity.parentCallId }
|
|
1029
|
-
: {}),
|
|
1030
|
-
depth: identity.depth,
|
|
1031
|
-
sequence: ++traceSequence,
|
|
1032
|
-
timestamp: Date.now(),
|
|
1033
|
-
type,
|
|
1034
|
-
...(meta.turnId !== undefined ? { turnId: meta.turnId } : {}),
|
|
1035
|
-
...(meta.callId !== undefined ? { callId: meta.callId } : {}),
|
|
1036
|
-
payload: jsonPayload,
|
|
1037
|
-
});
|
|
1038
|
-
return enqueue(async () => {
|
|
1039
|
-
await runtimePorts.emitTelemetry({ topic: TRACE_TOPIC, payload: trace });
|
|
1040
|
-
await describedEmission?.(runtimePorts);
|
|
1041
|
-
});
|
|
1042
|
-
};
|
|
1043
|
-
const emitTrace = (
|
|
1044
|
-
type: PlaybookTraceType,
|
|
1045
|
-
payload: unknown,
|
|
1046
|
-
meta: { turnId?: number; callId?: string } = {},
|
|
1047
|
-
): Promise<void> => enqueueTracedEmission(type, payload, meta);
|
|
1048
|
-
const emitBoundaryStatus = async (
|
|
1049
|
-
message: string,
|
|
1664
|
+
const visiblePendingQuestionsForState = (
|
|
1050
1665
|
state: PlaybookState,
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1666
|
+
context: Record<string, unknown>,
|
|
1667
|
+
): PendingBossQuestion[] =>
|
|
1668
|
+
pendingQuestionsForState(state, context).filter(
|
|
1669
|
+
({ questionId }) =>
|
|
1670
|
+
questionId !== 'commitCoderProposal' ||
|
|
1671
|
+
hiddenDeferredOperationId === undefined,
|
|
1054
1672
|
);
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
{ turnId: currentTurnId },
|
|
1065
|
-
(runtimePorts) => runtimePorts.emitStatus(message),
|
|
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,
|
|
1066
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];
|
|
1067
1689
|
};
|
|
1068
|
-
|
|
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
|
+
};
|
|
1931
|
+
const stateIdentity = (state: PlaybookState): { stateId?: string } => {
|
|
1932
|
+
return state.stateId === undefined ? {} : { stateId: state.stateId };
|
|
1933
|
+
};
|
|
1934
|
+
const enqueueTracedEmission = (
|
|
1935
|
+
type: PlaybookTraceType,
|
|
1936
|
+
payload: unknown,
|
|
1937
|
+
meta: { turnId?: number; callId?: string } = {},
|
|
1938
|
+
describedEmission?: (runtimePorts: PlaybookPorts) => Promise<void>,
|
|
1939
|
+
aborts?: AbortReasonClassifier,
|
|
1940
|
+
): Promise<void> => {
|
|
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 => {
|
|
1956
|
+
const identity = requireSessionIdentity();
|
|
1957
|
+
const jsonPayload = snapshotJsonValue(payload, `trace ${type} payload`);
|
|
1958
|
+
return Object.freeze({
|
|
1959
|
+
schemaVersion: 4,
|
|
1960
|
+
sessionId: identity.sessionId,
|
|
1961
|
+
playbookId: identity.playbookId,
|
|
1962
|
+
rootSessionId: identity.rootSessionId,
|
|
1963
|
+
...(identity.parentSessionId !== undefined
|
|
1964
|
+
? { parentSessionId: identity.parentSessionId }
|
|
1965
|
+
: {}),
|
|
1966
|
+
...(identity.parentCallId !== undefined
|
|
1967
|
+
? { parentCallId: identity.parentCallId }
|
|
1968
|
+
: {}),
|
|
1969
|
+
depth: identity.depth,
|
|
1970
|
+
sequence: ++traceSequence,
|
|
1971
|
+
timestamp: Date.now(),
|
|
1972
|
+
type,
|
|
1973
|
+
...(meta.turnId !== undefined ? { turnId: meta.turnId } : {}),
|
|
1974
|
+
...(meta.callId !== undefined ? { callId: meta.callId } : {}),
|
|
1975
|
+
payload: jsonPayload,
|
|
1976
|
+
});
|
|
1977
|
+
};
|
|
1978
|
+
const emitTrace = (
|
|
1979
|
+
type: PlaybookTraceType,
|
|
1980
|
+
payload: unknown,
|
|
1981
|
+
meta: { turnId?: number; callId?: string } = {},
|
|
1982
|
+
aborts?: AbortReasonClassifier,
|
|
1983
|
+
): Promise<void> => enqueueTracedEmission(type, payload, meta, undefined, aborts);
|
|
1984
|
+
const enqueueAcceptedOutcomeEmission = (
|
|
1985
|
+
acceptedOutcome: AcceptedOutcomeReceipt,
|
|
1986
|
+
state: PlaybookState,
|
|
1987
|
+
aborts?: AbortReasonClassifier,
|
|
1988
|
+
): Promise<void> => {
|
|
1989
|
+
const message = `→ ${acceptedOutcome.acceptedOutcome}`;
|
|
1990
|
+
const acceptedTrace = createTraceEvent(
|
|
1991
|
+
'outcome.accepted',
|
|
1992
|
+
acceptedOutcome,
|
|
1993
|
+
{ turnId: currentTurnId },
|
|
1994
|
+
);
|
|
1995
|
+
const statusTrace = createTraceEvent(
|
|
1996
|
+
'status.emitted',
|
|
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
|
+
: {}),
|
|
2029
|
+
message,
|
|
2030
|
+
state,
|
|
2031
|
+
},
|
|
2032
|
+
{ turnId: currentTurnId },
|
|
2033
|
+
(runtimePorts) => runtimePorts.emitStatus(message),
|
|
2034
|
+
);
|
|
2035
|
+
};
|
|
2036
|
+
|
|
1069
2037
|
const emitCallStarted = async (
|
|
1070
2038
|
startedType: 'player.call.started' | 'judge.call.started',
|
|
1071
2039
|
finishedType: 'player.call.finished' | 'judge.call.finished',
|
|
@@ -1073,8 +2041,9 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1073
2041
|
meta: { turnId?: number; callId?: string },
|
|
1074
2042
|
signal: AbortSignal,
|
|
1075
2043
|
): Promise<void> => {
|
|
2044
|
+
const aborts = abortReasonClassifier(signal);
|
|
1076
2045
|
try {
|
|
1077
|
-
await emitTrace(startedType, identity, meta);
|
|
2046
|
+
await emitTrace(startedType, identity, meta, aborts);
|
|
1078
2047
|
} catch (error) {
|
|
1079
2048
|
latchControlPlaneError(error, signal);
|
|
1080
2049
|
try {
|
|
@@ -1082,13 +2051,17 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1082
2051
|
finishedType,
|
|
1083
2052
|
{
|
|
1084
2053
|
...identity,
|
|
1085
|
-
|
|
2054
|
+
// A started-trace sink rejection causally identical to the
|
|
2055
|
+
// boundary reason is the abort's own evidence: the pair
|
|
2056
|
+
// finishes 'aborted', not 'error' (DR-036 §4).
|
|
2057
|
+
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
1086
2058
|
error: normalizeErrorFull(error) ?? {
|
|
1087
2059
|
name: 'Error',
|
|
1088
2060
|
message: String(error),
|
|
1089
2061
|
},
|
|
1090
2062
|
},
|
|
1091
2063
|
meta,
|
|
2064
|
+
aborts,
|
|
1092
2065
|
);
|
|
1093
2066
|
} catch {
|
|
1094
2067
|
// Preserve the start failure after one best-effort finish attempt.
|
|
@@ -1103,6 +2076,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1103
2076
|
purpose: 'boss-input-classification' | 'player-output-adjudication',
|
|
1104
2077
|
callStateId: string | undefined,
|
|
1105
2078
|
): Promise<string> => {
|
|
2079
|
+
const aborts = abortReasonClassifier(signal);
|
|
1106
2080
|
const identity = {
|
|
1107
2081
|
purpose,
|
|
1108
2082
|
...(callStateId !== undefined ? { stateId: callStateId } : {}),
|
|
@@ -1138,13 +2112,17 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1138
2112
|
'judge.call.finished',
|
|
1139
2113
|
{
|
|
1140
2114
|
...identity,
|
|
1141
|
-
|
|
2115
|
+
// Only the exact abort reason is cancellation; a distinct
|
|
2116
|
+
// failure under an aborted signal stays an error
|
|
2117
|
+
// (slc/link.md §Abort).
|
|
2118
|
+
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
1142
2119
|
error: normalizeErrorFull(error) ?? {
|
|
1143
2120
|
name: 'Error',
|
|
1144
2121
|
message: String(error),
|
|
1145
2122
|
},
|
|
1146
2123
|
},
|
|
1147
2124
|
{ turnId: currentTurnId, callId },
|
|
2125
|
+
aborts,
|
|
1148
2126
|
);
|
|
1149
2127
|
throw error;
|
|
1150
2128
|
}
|
|
@@ -1153,6 +2131,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1153
2131
|
'judge.call.finished',
|
|
1154
2132
|
{ ...identity, status: 'ok', reply: finalText },
|
|
1155
2133
|
{ turnId: currentTurnId, callId },
|
|
2134
|
+
aborts,
|
|
1156
2135
|
);
|
|
1157
2136
|
return finalText;
|
|
1158
2137
|
});
|
|
@@ -1173,11 +2152,12 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1173
2152
|
const runPlayerCall = async (
|
|
1174
2153
|
input: PlayerInput,
|
|
1175
2154
|
signal: AbortSignal,
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
2155
|
+
continuation?: {
|
|
2156
|
+
readonly resume?: string | false;
|
|
2157
|
+
readonly callId: string;
|
|
2158
|
+
},
|
|
2159
|
+
): Promise<DeferredContinuationEnvelope> => {
|
|
2160
|
+
const aborts = abortReasonClassifier(signal);
|
|
1181
2161
|
if (!ROLE_ID_SET.has(input.role)) {
|
|
1182
2162
|
throw new TypeError(
|
|
1183
2163
|
`DECIDE player input role must name a declared local role`,
|
|
@@ -1190,12 +2170,17 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1190
2170
|
let resume: PlayerCallOptions['resume'];
|
|
1191
2171
|
try {
|
|
1192
2172
|
signal.throwIfAborted();
|
|
1193
|
-
resume =
|
|
2173
|
+
resume =
|
|
2174
|
+
continuation !== undefined &&
|
|
2175
|
+
Object.prototype.hasOwnProperty.call(continuation, 'resume')
|
|
2176
|
+
? continuation.resume!
|
|
2177
|
+
: selectPlayerResume(roleId, playerId);
|
|
1194
2178
|
} catch (error) {
|
|
1195
2179
|
latchControlPlaneError(error, signal);
|
|
1196
2180
|
throw error;
|
|
1197
2181
|
}
|
|
1198
|
-
const callId =
|
|
2182
|
+
const callId =
|
|
2183
|
+
continuation?.callId ?? `player-${++playerCallSequence}`;
|
|
1199
2184
|
const identity = {
|
|
1200
2185
|
stateId: input.stateId,
|
|
1201
2186
|
sourceItem: input.sourceItem,
|
|
@@ -1208,13 +2193,17 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1208
2193
|
'player.call.finished',
|
|
1209
2194
|
{
|
|
1210
2195
|
...identity,
|
|
1211
|
-
|
|
2196
|
+
// Only the exact abort reason is cancellation; a distinct
|
|
2197
|
+
// failure under an aborted signal stays an error
|
|
2198
|
+
// (slc/link.md §Abort).
|
|
2199
|
+
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
1212
2200
|
error: normalizeErrorFull(error) ?? {
|
|
1213
2201
|
name: 'Error',
|
|
1214
2202
|
message: String(error),
|
|
1215
2203
|
},
|
|
1216
2204
|
},
|
|
1217
2205
|
{ turnId: currentTurnId, callId },
|
|
2206
|
+
aborts,
|
|
1218
2207
|
);
|
|
1219
2208
|
if (inFlightPlayerKeys.has(playerKey)) {
|
|
1220
2209
|
const error = new Error(
|
|
@@ -1310,10 +2299,12 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1310
2299
|
: {}),
|
|
1311
2300
|
},
|
|
1312
2301
|
{ turnId: currentTurnId, callId },
|
|
2302
|
+
aborts,
|
|
1313
2303
|
);
|
|
1314
2304
|
return {
|
|
1315
2305
|
roleId,
|
|
1316
2306
|
...(playerId === undefined ? {} : { playerId }),
|
|
2307
|
+
callId,
|
|
1317
2308
|
result,
|
|
1318
2309
|
};
|
|
1319
2310
|
} finally {
|
|
@@ -1321,153 +2312,1060 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1321
2312
|
}
|
|
1322
2313
|
};
|
|
1323
2314
|
|
|
1324
|
-
const
|
|
2315
|
+
const governedBoundarySeed = (
|
|
1325
2316
|
input: PlayerInput,
|
|
1326
|
-
|
|
1327
|
-
):
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
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
|
+
};
|
|
1333
2349
|
};
|
|
1334
2350
|
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
combined.throwIfAborted();
|
|
1348
|
-
|
|
1349
|
-
let { roleId, playerId, result } = await callPlayer(input, combined);
|
|
1350
|
-
if (result.status === 'ok' && isEmptyFinalText(result.finalText)) {
|
|
1351
|
-
// DR-028: an `ok` result whose finalText is missing, empty, or
|
|
1352
|
-
// whitespace-only earns exactly one corrective re-ask — the same
|
|
1353
|
-
// composed call repeated, traced by runPlayerCall as its own
|
|
1354
|
-
// player-call pair, with the resume selection re-read from the
|
|
1355
|
-
// token map the first result left (PBRT-38). An abort that lands
|
|
1356
|
-
// between the two calls ends the turn without the re-ask (aborts
|
|
1357
|
-
// are never retried), and a rejecting finish emission rejects
|
|
1358
|
-
// `callPlayer` itself, so it never reaches this branch (PBRT-47).
|
|
1359
|
-
combined.throwIfAborted();
|
|
1360
|
-
({ roleId, playerId, result } = await callPlayer(input, combined));
|
|
1361
|
-
}
|
|
1362
|
-
if (result.status !== 'ok') {
|
|
1363
|
-
throw new Error(
|
|
1364
|
-
`${roleLabel(roleId)}${
|
|
1365
|
-
playerId === undefined ? '' : ` (${playerId})`
|
|
1366
|
-
} returned status "${result.status}"${
|
|
1367
|
-
result.error ? `: ${result.error}` : ''
|
|
1368
|
-
}`,
|
|
1369
|
-
);
|
|
1370
|
-
}
|
|
1371
|
-
const finalText = result.finalText ?? '';
|
|
1372
|
-
if (isEmptyFinalText(finalText)) {
|
|
1373
|
-
throw new Error(
|
|
1374
|
-
`${roleLabel(roleId)}${
|
|
1375
|
-
playerId === undefined ? '' : ` (${playerId})`
|
|
1376
|
-
} returned status "ok" with no finalText`,
|
|
1377
|
-
);
|
|
1378
|
-
}
|
|
1379
|
-
combined.throwIfAborted();
|
|
1380
|
-
|
|
1381
|
-
try {
|
|
1382
|
-
const prompt = buildAdjudicatorPrompt(input, finalText);
|
|
1383
|
-
return parseAdjudication(
|
|
1384
|
-
await callJudge(
|
|
1385
|
-
prompt,
|
|
1386
|
-
combined,
|
|
1387
|
-
'player-output-adjudication',
|
|
1388
|
-
input.stateId,
|
|
1389
|
-
),
|
|
1390
|
-
input,
|
|
1391
|
-
finalText,
|
|
1392
|
-
);
|
|
1393
|
-
} catch (error) {
|
|
1394
|
-
latchControlPlaneError(error, combined);
|
|
1395
|
-
throw error;
|
|
1396
|
-
}
|
|
1397
|
-
},
|
|
1398
|
-
);
|
|
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
|
+
};
|
|
1399
2363
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
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([
|
|
1416
2448
|
{
|
|
1417
|
-
|
|
1418
|
-
|
|
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,
|
|
1419
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',
|
|
1420
2469
|
);
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
try {
|
|
1425
|
-
await emitTrace(
|
|
1426
|
-
'playbook.call.finished',
|
|
1427
|
-
{
|
|
1428
|
-
stateId: event.stateId,
|
|
1429
|
-
playbookId: event.playbookId,
|
|
1430
|
-
text: event.text,
|
|
1431
|
-
result: event.result,
|
|
1432
|
-
},
|
|
1433
|
-
{
|
|
1434
|
-
...(turnId === undefined ? {} : { turnId }),
|
|
1435
|
-
callId: event.callId,
|
|
1436
|
-
},
|
|
1437
|
-
);
|
|
1438
|
-
} finally {
|
|
1439
|
-
playbookCallTurnIds.delete(event.callId);
|
|
1440
|
-
}
|
|
1441
|
-
},
|
|
1442
|
-
drain: flush,
|
|
1443
|
-
bindResumeSignal: (signal) => {
|
|
1444
|
-
currentSignal = signal;
|
|
1445
|
-
},
|
|
1446
|
-
onControlPlaneError: (error) => {
|
|
1447
|
-
const signal = currentSignal;
|
|
1448
|
-
if (!signal || !isAbortFailure(error, signal)) {
|
|
1449
|
-
controlPlaneError ??= error;
|
|
1450
|
-
}
|
|
1451
|
-
},
|
|
1452
|
-
onBackgroundError: (error) => {
|
|
1453
|
-
collectFailure(emissionFailures, error);
|
|
1454
|
-
},
|
|
1455
|
-
});
|
|
2470
|
+
}
|
|
2471
|
+
return true;
|
|
2472
|
+
};
|
|
1456
2473
|
|
|
1457
|
-
const
|
|
1458
|
-
|
|
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
|
+
|
|
3093
|
+
const callPlayer = (
|
|
3094
|
+
input: PlayerInput,
|
|
3095
|
+
signal: AbortSignal,
|
|
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());
|
|
3171
|
+
};
|
|
3172
|
+
|
|
3173
|
+
const player = fromPromise<PlayerOutput, PlayerInput>(
|
|
3174
|
+
async ({ input, signal }) => {
|
|
3175
|
+
const combined = combineSignals(signal, currentSignal);
|
|
3176
|
+
const settlementAborts = abortReasonClassifier(combined);
|
|
3177
|
+
try {
|
|
3178
|
+
// XState starts invoked actors while publishing the entering snapshot.
|
|
3179
|
+
// Yield through the runtime emission queue before crossing the player
|
|
3180
|
+
// boundary so state trace/status always precede its call-start trace.
|
|
3181
|
+
combined.throwIfAborted();
|
|
3182
|
+
try {
|
|
3183
|
+
await flush(settlementAborts);
|
|
3184
|
+
} catch (error) {
|
|
3185
|
+
latchControlPlaneError(error, combined);
|
|
3186
|
+
throw error;
|
|
3187
|
+
}
|
|
3188
|
+
combined.throwIfAborted();
|
|
3189
|
+
|
|
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
|
+
) {
|
|
3202
|
+
// DR-028: an `ok` result whose finalText is missing, empty, or
|
|
3203
|
+
// whitespace-only earns exactly one corrective re-ask — the same
|
|
3204
|
+
// composed call repeated, traced by runPlayerCall as its own
|
|
3205
|
+
// player-call pair, with the resume selection re-read from the
|
|
3206
|
+
// token map the first result left (PBRT-38). An abort that lands
|
|
3207
|
+
// between the two calls ends the turn without the re-ask (aborts
|
|
3208
|
+
// are never retried), and a rejecting finish emission rejects
|
|
3209
|
+
// `callPlayer` itself, so it never reaches this branch (PBRT-47).
|
|
3210
|
+
combined.throwIfAborted();
|
|
3211
|
+
({ roleId, playerId, callId, result } = await callPlayer(
|
|
3212
|
+
input,
|
|
3213
|
+
combined,
|
|
3214
|
+
));
|
|
3215
|
+
}
|
|
3216
|
+
if (result.status !== 'ok') {
|
|
3217
|
+
throw new Error(
|
|
3218
|
+
`${roleLabel(roleId)}${
|
|
3219
|
+
playerId === undefined ? '' : ` (${playerId})`
|
|
3220
|
+
} returned status "${result.status}"${
|
|
3221
|
+
result.error ? `: ${result.error}` : ''
|
|
3222
|
+
}`,
|
|
3223
|
+
);
|
|
3224
|
+
}
|
|
3225
|
+
const finalText = result.finalText ?? '';
|
|
3226
|
+
if (isEmptyFinalText(finalText)) {
|
|
3227
|
+
throw new Error(
|
|
3228
|
+
`${roleLabel(roleId)}${
|
|
3229
|
+
playerId === undefined ? '' : ` (${playerId})`
|
|
3230
|
+
} returned status "ok" with no finalText`,
|
|
3231
|
+
);
|
|
3232
|
+
}
|
|
3233
|
+
combined.throwIfAborted();
|
|
3234
|
+
|
|
3235
|
+
const governedOutput = governedPlayerOutputs.get(result);
|
|
3236
|
+
if (governedOutput !== undefined) {
|
|
3237
|
+
governedPlayerOutputs.delete(result);
|
|
3238
|
+
return governedOutput;
|
|
3239
|
+
}
|
|
3240
|
+
throw new Error(
|
|
3241
|
+
'DECIDE governed player result was not reconciled against repository evidence',
|
|
3242
|
+
);
|
|
3243
|
+
} finally {
|
|
3244
|
+
actorSettlementAborts.push(settlementAborts);
|
|
3245
|
+
}
|
|
3246
|
+
},
|
|
3247
|
+
);
|
|
3248
|
+
|
|
3249
|
+
nestedBridge = createNestedPlaybookBridge<PlaybookInput>({
|
|
3250
|
+
nextCallId: () => `playbook-${++playbookCallSequence}`,
|
|
3251
|
+
getBoundarySignal: () => currentSignal,
|
|
3252
|
+
callPlaybook: (request, signal) =>
|
|
3253
|
+
trackBoundaryCall(
|
|
3254
|
+
Promise.resolve(requirePorts().callPlaybook(request, signal)),
|
|
3255
|
+
),
|
|
3256
|
+
emitStarted: async (event, aborts) => {
|
|
3257
|
+
playbookCallTurnIds.set(event.callId, currentTurnId);
|
|
3258
|
+
await emitTrace(
|
|
3259
|
+
'playbook.call.started',
|
|
3260
|
+
{
|
|
3261
|
+
stateId: event.stateId,
|
|
3262
|
+
playbookId: event.playbookId,
|
|
3263
|
+
text: event.text,
|
|
3264
|
+
},
|
|
3265
|
+
{
|
|
3266
|
+
...(currentTurnId === undefined ? {} : { turnId: currentTurnId }),
|
|
3267
|
+
callId: event.callId,
|
|
3268
|
+
},
|
|
3269
|
+
aborts,
|
|
3270
|
+
);
|
|
3271
|
+
},
|
|
3272
|
+
emitFinished: async (event, aborts) => {
|
|
3273
|
+
const turnId = playbookCallTurnIds.get(event.callId);
|
|
3274
|
+
try {
|
|
3275
|
+
await emitTrace(
|
|
3276
|
+
'playbook.call.finished',
|
|
3277
|
+
{
|
|
3278
|
+
stateId: event.stateId,
|
|
3279
|
+
playbookId: event.playbookId,
|
|
3280
|
+
text: event.text,
|
|
3281
|
+
result: event.result,
|
|
3282
|
+
},
|
|
3283
|
+
{
|
|
3284
|
+
...(turnId === undefined ? {} : { turnId }),
|
|
3285
|
+
callId: event.callId,
|
|
3286
|
+
},
|
|
3287
|
+
aborts,
|
|
3288
|
+
);
|
|
3289
|
+
} finally {
|
|
3290
|
+
playbookCallTurnIds.delete(event.callId);
|
|
3291
|
+
}
|
|
3292
|
+
},
|
|
3293
|
+
drain: flush,
|
|
3294
|
+
bindResumeSignal: (signal, aborts) => {
|
|
3295
|
+
currentSignal = signal;
|
|
3296
|
+
currentAborts = aborts ?? abortReasonClassifier(signal);
|
|
3297
|
+
},
|
|
3298
|
+
bindActorSettlement: (aborts) => {
|
|
3299
|
+
actorSettlementAborts.push(aborts);
|
|
3300
|
+
},
|
|
3301
|
+
onControlPlaneError: (error, aborts) => {
|
|
3302
|
+
if (
|
|
3303
|
+
!aborts?.isAbortReason(error) &&
|
|
3304
|
+
!currentAborts?.isAbortReason(error)
|
|
3305
|
+
) {
|
|
3306
|
+
controlPlaneError ??= error;
|
|
3307
|
+
}
|
|
3308
|
+
},
|
|
3309
|
+
onBackgroundError: (error, aborts) => {
|
|
3310
|
+
if (!aborts?.isAbortReason(error)) {
|
|
3311
|
+
collectFailure(emissionFailures, error);
|
|
3312
|
+
}
|
|
3313
|
+
},
|
|
1459
3314
|
});
|
|
1460
3315
|
|
|
3316
|
+
const providedMachine = decideMachine.provide({
|
|
3317
|
+
actors: { player, playbook: nestedBridge.actorLogic },
|
|
3318
|
+
});
|
|
3319
|
+
|
|
3320
|
+
const consumeActorSettlementAborts = (
|
|
3321
|
+
forSnapshot = false,
|
|
3322
|
+
): AbortReasonClassifier | undefined => {
|
|
3323
|
+
const aborts = actorSettlementAborts.shift() ?? actorSettlementErrorAborts;
|
|
3324
|
+
actorSettlementErrorAborts = undefined;
|
|
3325
|
+
if (forSnapshot && aborts !== undefined) {
|
|
3326
|
+
actorSettlementErrorAborts = aborts;
|
|
3327
|
+
queueMicrotask(() => {
|
|
3328
|
+
if (actorSettlementErrorAborts === aborts) {
|
|
3329
|
+
actorSettlementErrorAborts = undefined;
|
|
3330
|
+
}
|
|
3331
|
+
});
|
|
3332
|
+
}
|
|
3333
|
+
return aborts;
|
|
3334
|
+
};
|
|
3335
|
+
|
|
1461
3336
|
const inspect = (event: InspectionEvent): void => {
|
|
1462
|
-
if (event.type !== '@xstate.snapshot') return;
|
|
1463
3337
|
if (actor === undefined || event.actorRef !== actor) return;
|
|
1464
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;
|
|
3348
|
+
const settlementAborts = consumeActorSettlementAborts(true);
|
|
1465
3349
|
try {
|
|
1466
3350
|
const snapshot = event.snapshot as SnapshotFrom<typeof decideMachine>;
|
|
1467
3351
|
const state = normalizePlaybookSnapshot(snapshot);
|
|
1468
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
|
+
}
|
|
1469
3359
|
const context = snapshot.context as unknown as Record<string, unknown>;
|
|
1470
|
-
const fsmPayload = telemetryPayload(
|
|
3360
|
+
const fsmPayload = telemetryPayload(
|
|
3361
|
+
prior,
|
|
3362
|
+
state,
|
|
3363
|
+
event.event,
|
|
3364
|
+
context,
|
|
3365
|
+
hiddenDeferredOperationId === undefined
|
|
3366
|
+
? undefined
|
|
3367
|
+
: 'commitCoderProposal',
|
|
3368
|
+
);
|
|
1471
3369
|
const describedFsmPayload = snapshotJsonValue(
|
|
1472
3370
|
fsmPayload,
|
|
1473
3371
|
'described FSM telemetry',
|
|
@@ -1482,11 +3380,12 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1482
3380
|
topic: TELEMETRY_TOPIC,
|
|
1483
3381
|
payload: describedFsmPayload,
|
|
1484
3382
|
}),
|
|
3383
|
+
settlementAborts,
|
|
1485
3384
|
).catch(() => undefined);
|
|
1486
3385
|
|
|
1487
3386
|
const priorIds = new Set(previousState?.activeStateIds ?? []);
|
|
1488
3387
|
previousState = state;
|
|
1489
|
-
const pendingQuestions =
|
|
3388
|
+
const pendingQuestions = visiblePendingQuestionsForState(state, context);
|
|
1490
3389
|
const bossRelevantStateIds = state.activeStateIds.filter((stateId) =>
|
|
1491
3390
|
STATUS_STATE_IDS.has(stateId),
|
|
1492
3391
|
);
|
|
@@ -1507,9 +3406,18 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1507
3406
|
tracePayload,
|
|
1508
3407
|
{ turnId: currentTurnId },
|
|
1509
3408
|
(emissionPorts) => emissionPorts.emitStatus(message, data),
|
|
3409
|
+
settlementAborts,
|
|
1510
3410
|
).catch(() => undefined);
|
|
1511
3411
|
};
|
|
1512
3412
|
|
|
3413
|
+
for (const acceptedOutcome of acceptedOutcomes) {
|
|
3414
|
+
void enqueueAcceptedOutcomeEmission(
|
|
3415
|
+
acceptedOutcome,
|
|
3416
|
+
state,
|
|
3417
|
+
settlementAborts,
|
|
3418
|
+
).catch(() => undefined);
|
|
3419
|
+
}
|
|
3420
|
+
|
|
1513
3421
|
for (const activeStateId of state.activeStateIds) {
|
|
1514
3422
|
if (
|
|
1515
3423
|
priorIds.has(activeStateId) ||
|
|
@@ -1552,89 +3460,740 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1552
3460
|
);
|
|
1553
3461
|
}
|
|
1554
3462
|
} catch (error) {
|
|
1555
|
-
|
|
3463
|
+
acceptedOutcomeConsumer.reset();
|
|
3464
|
+
latchInspectionError(error, settlementAborts);
|
|
3465
|
+
}
|
|
3466
|
+
};
|
|
3467
|
+
|
|
3468
|
+
const createRuntimeActor = (machineSnapshot?: JsonValue): void => {
|
|
3469
|
+
previousState = undefined;
|
|
3470
|
+
acceptedOutcomeConsumer.reset();
|
|
3471
|
+
// DR-014 §1: a restore rehydrates the persisted machine snapshot;
|
|
3472
|
+
// XState derives context/value from it and ignores `input` then.
|
|
3473
|
+
actor = createActor(providedMachine, {
|
|
3474
|
+
input: fsmInput,
|
|
3475
|
+
...(machineSnapshot === undefined
|
|
3476
|
+
? {}
|
|
3477
|
+
: {
|
|
3478
|
+
snapshot: machineSnapshot as unknown as SnapshotFrom<
|
|
3479
|
+
typeof decideMachine
|
|
3480
|
+
>,
|
|
3481
|
+
}),
|
|
3482
|
+
inspect,
|
|
3483
|
+
});
|
|
3484
|
+
// A synchronous FSM action throw errors the actor without any pending
|
|
3485
|
+
// boundary await to observe it; unobserved, XState would surface it via
|
|
3486
|
+
// reportUnhandledError as an uncaughtException. Observe it here: latch
|
|
3487
|
+
// it as a control error while a turn signal is active (unless it is
|
|
3488
|
+
// the abort reason itself), otherwise collect it with the emission
|
|
3489
|
+
// failures (slc/link.md §Abort).
|
|
3490
|
+
actor.subscribe({
|
|
3491
|
+
error: (error) =>
|
|
3492
|
+
latchInspectionError(error, consumeActorSettlementAborts()),
|
|
3493
|
+
});
|
|
3494
|
+
};
|
|
3495
|
+
|
|
3496
|
+
// PBRT-6: the single seam that stops this runtime's actor. Stopping a
|
|
3497
|
+
// still-running actor fires one more `@xstate.snapshot` for the *unchanged*
|
|
3498
|
+
// state value with `status: 'stopped'`; `inspect` cannot tell that disposal
|
|
3499
|
+
// artifact from a state entry, so unsuppressed it re-emits the parked
|
|
3500
|
+
// state's telemetry and a phantom self-loop transition. Suppression is a
|
|
3501
|
+
// property of stopping, not a rule each caller must remember — every stop
|
|
3502
|
+
// goes through here so no later site can reintroduce the omission.
|
|
3503
|
+
const stopActor = (): void => {
|
|
3504
|
+
if (!actor) return;
|
|
3505
|
+
suppressInspectionEmissions = true;
|
|
3506
|
+
acceptedOutcomeConsumer.reset();
|
|
3507
|
+
actor.stop();
|
|
3508
|
+
};
|
|
3509
|
+
|
|
3510
|
+
const startActor = (): void => {
|
|
3511
|
+
createRuntimeActor();
|
|
3512
|
+
// A fresh actor's emissions are real state entries again.
|
|
3513
|
+
suppressInspectionEmissions = false;
|
|
3514
|
+
actor?.start();
|
|
3515
|
+
};
|
|
3516
|
+
|
|
3517
|
+
const driveToQuiescence = async (): Promise<void> => {
|
|
3518
|
+
const live = actor;
|
|
3519
|
+
if (!live) throw new Error('decide runtime: actor is not initialized');
|
|
3520
|
+
await waitForPlaybookQuiescence(live, { pendingCalls: nestedBridge });
|
|
3521
|
+
};
|
|
3522
|
+
|
|
3523
|
+
const classify = async (
|
|
3524
|
+
text: string,
|
|
3525
|
+
signal: AbortSignal,
|
|
3526
|
+
): Promise<DecideEvent | { type: 'NO_ACTION' } | null> => {
|
|
3527
|
+
const live = actor;
|
|
3528
|
+
if (!live) throw new Error('decide runtime: actor is not initialized');
|
|
3529
|
+
refreshReconciliationProjection();
|
|
3530
|
+
if (hasUnresolvedReconciliation()) return null;
|
|
3531
|
+
const snapshot = live.getSnapshot() as SnapshotFrom<typeof decideMachine>;
|
|
3532
|
+
const context = snapshot.context as unknown as Record<string, unknown>;
|
|
3533
|
+
const state = normalizePlaybookSnapshot(snapshot, {
|
|
3534
|
+
pendingCall: nestedBridge.getPendingCall(),
|
|
3535
|
+
});
|
|
3536
|
+
const pendingQuestions = visiblePendingQuestionsForState(state, context);
|
|
3537
|
+
const failed = state.activeStateIds.includes('failed');
|
|
3538
|
+
if (
|
|
3539
|
+
pendingQuestions.length === 0 &&
|
|
3540
|
+
(snapshot.status === 'done' ||
|
|
3541
|
+
state.activeStateIds.includes('ready') ||
|
|
3542
|
+
(failed &&
|
|
3543
|
+
automaticReplayPolicy.allowsFailureStateRetry()))
|
|
3544
|
+
) {
|
|
3545
|
+
return { type: 'START_DECIDE', callerTopic: text };
|
|
3546
|
+
}
|
|
3547
|
+
if (pendingQuestions.length === 0) return null;
|
|
3548
|
+
const prompt = buildClassifierPrompt(text, {
|
|
3549
|
+
state,
|
|
3550
|
+
pendingQuestions,
|
|
3551
|
+
});
|
|
3552
|
+
const raw = await callJudge(
|
|
3553
|
+
prompt,
|
|
3554
|
+
signal,
|
|
3555
|
+
'boss-input-classification',
|
|
3556
|
+
state.stateId,
|
|
3557
|
+
);
|
|
3558
|
+
return parseClassification(
|
|
3559
|
+
raw,
|
|
3560
|
+
text,
|
|
3561
|
+
pendingQuestions.map(({ questionId }) => questionId),
|
|
3562
|
+
);
|
|
3563
|
+
};
|
|
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();
|
|
1556
3842
|
}
|
|
3843
|
+
return 'restored';
|
|
1557
3844
|
};
|
|
1558
3845
|
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
actor = createActor(providedMachine, {
|
|
1564
|
-
input: fsmInput,
|
|
1565
|
-
...(machineSnapshot === undefined
|
|
1566
|
-
? {}
|
|
1567
|
-
: {
|
|
1568
|
-
snapshot: machineSnapshot as unknown as SnapshotFrom<
|
|
1569
|
-
typeof decideMachine
|
|
1570
|
-
>,
|
|
1571
|
-
}),
|
|
1572
|
-
inspect,
|
|
1573
|
-
});
|
|
3846
|
+
type UnresolvedControlCandidate = {
|
|
3847
|
+
readonly action: PlaybookControlAction;
|
|
3848
|
+
readonly kind: 'reconcile' | 'abandon';
|
|
3849
|
+
readonly deferredRestoreOperationId?: string;
|
|
1574
3850
|
};
|
|
3851
|
+
type UnresolvedControlCandidates = readonly UnresolvedControlCandidate[];
|
|
1575
3852
|
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
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
|
+
];
|
|
1587
3881
|
};
|
|
1588
3882
|
|
|
1589
|
-
const
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
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();
|
|
1594
3893
|
};
|
|
1595
3894
|
|
|
1596
|
-
const
|
|
3895
|
+
const describeControlView = (): PlaybookControlView => {
|
|
3896
|
+
if (disposed || disposalPromise !== undefined) {
|
|
3897
|
+
throw new Error('decide runtime: runtime is disposing or disposed');
|
|
3898
|
+
}
|
|
1597
3899
|
const live = actor;
|
|
1598
|
-
if (
|
|
1599
|
-
|
|
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
|
+
);
|
|
1600
3944
|
};
|
|
1601
3945
|
|
|
1602
|
-
const
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
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');
|
|
1621
3969
|
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
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,
|
|
1626
4021
|
});
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
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;
|
|
1638
4197
|
};
|
|
1639
4198
|
|
|
1640
4199
|
const resultForSnapshot = (signal?: AbortSignal): PlaybookRunResult => {
|
|
@@ -1644,34 +4203,58 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1644
4203
|
const pendingCall = nestedBridge.getPendingCall();
|
|
1645
4204
|
const state = normalizePlaybookSnapshot(snapshot, { pendingCall });
|
|
1646
4205
|
const context = snapshot.context as unknown as Record<string, unknown>;
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
4206
|
+
const abortedResult = (abortSignal: AbortSignal): PlaybookRunResult => ({
|
|
4207
|
+
outcome: 'aborted',
|
|
4208
|
+
state,
|
|
4209
|
+
...(abortSignal.reason === undefined
|
|
4210
|
+
? {}
|
|
4211
|
+
: {
|
|
4212
|
+
error: normalizeErrorFull(abortSignal.reason) ?? {
|
|
4213
|
+
name: 'AbortError',
|
|
4214
|
+
message: String(abortSignal.reason),
|
|
4215
|
+
},
|
|
4216
|
+
}),
|
|
4217
|
+
});
|
|
4218
|
+
if (snapshot.status === 'error') {
|
|
4219
|
+
// An errored actor outranks a coincident abort unless the actor's
|
|
4220
|
+
// error is the abort reason itself (slc/link.md §Abort).
|
|
4221
|
+
const actorError = (snapshot as { error?: unknown }).error;
|
|
4222
|
+
if (
|
|
4223
|
+
actorError !== undefined &&
|
|
4224
|
+
signal !== undefined &&
|
|
4225
|
+
isAbortFailure(actorError, signal)
|
|
4226
|
+
) {
|
|
4227
|
+
return abortedResult(signal);
|
|
4228
|
+
}
|
|
4229
|
+
throw (
|
|
4230
|
+
actorError ?? new Error('decide runtime actor entered error status')
|
|
4231
|
+
);
|
|
1660
4232
|
}
|
|
4233
|
+
// Terminal completion outranks a coincident abort (DR-036 §3): reporting
|
|
4234
|
+
// 'aborted' over a completed machine would hide a terminal state that the
|
|
4235
|
+
// next turn silently restarts, duplicating the workflow's side effects.
|
|
1661
4236
|
if (snapshot.status === 'done') {
|
|
1662
4237
|
const output = (snapshot as { output?: unknown }).output;
|
|
1663
4238
|
if (output !== undefined) assertJsonSafe(output, 'terminal output');
|
|
4239
|
+
const stateDescription = state.activeStateIds.includes('done')
|
|
4240
|
+
? STATE_DESCRIPTIONS.done
|
|
4241
|
+
: state.activeStateIds.includes('reportedReviewFailure')
|
|
4242
|
+
? STATE_DESCRIPTIONS.reportedReviewFailure
|
|
4243
|
+
: undefined;
|
|
4244
|
+
if (stateDescription === undefined) {
|
|
4245
|
+
throw new Error(
|
|
4246
|
+
'decide runtime: completed actor has no authored final-state description',
|
|
4247
|
+
);
|
|
4248
|
+
}
|
|
1664
4249
|
return {
|
|
1665
4250
|
outcome: 'terminal',
|
|
1666
4251
|
state,
|
|
4252
|
+
stateDescription,
|
|
1667
4253
|
...(output === undefined ? {} : { output }),
|
|
1668
4254
|
};
|
|
1669
4255
|
}
|
|
1670
|
-
if (
|
|
1671
|
-
|
|
1672
|
-
(snapshot as { error?: unknown }).error ??
|
|
1673
|
-
new Error('decide runtime actor entered error status')
|
|
1674
|
-
);
|
|
4256
|
+
if (signal?.aborted) {
|
|
4257
|
+
return abortedResult(signal);
|
|
1675
4258
|
}
|
|
1676
4259
|
if (state.activeStateIds.includes('failed')) {
|
|
1677
4260
|
const error = normalizeErrorFull(context.lastError);
|
|
@@ -1739,8 +4322,12 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1739
4322
|
activeEmissionCalls.clear();
|
|
1740
4323
|
emissionQueue.clear();
|
|
1741
4324
|
judgeQueue.clear();
|
|
4325
|
+
semanticCompletionQueue.clear();
|
|
1742
4326
|
actor = undefined;
|
|
1743
4327
|
currentSignal = undefined;
|
|
4328
|
+
currentAborts = undefined;
|
|
4329
|
+
actorSettlementAborts.length = 0;
|
|
4330
|
+
actorSettlementErrorAborts = undefined;
|
|
1744
4331
|
currentTurnId = undefined;
|
|
1745
4332
|
ports = undefined;
|
|
1746
4333
|
sessionIdentity = undefined;
|
|
@@ -1753,7 +4340,20 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1753
4340
|
judgeCallSequence = 0;
|
|
1754
4341
|
playerCallSequence = 0;
|
|
1755
4342
|
playbookCallSequence = 0;
|
|
4343
|
+
applyCallSequence = 0;
|
|
1756
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;
|
|
1757
4357
|
lifecycleStarted = false;
|
|
1758
4358
|
};
|
|
1759
4359
|
|
|
@@ -1779,6 +4379,10 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1779
4379
|
ports = identity.ports;
|
|
1780
4380
|
sessionIdentity = identity;
|
|
1781
4381
|
try {
|
|
4382
|
+
if (deferredEffects !== undefined) {
|
|
4383
|
+
effectLedgerMirror = readEffectLedger();
|
|
4384
|
+
synchronizeDeferredProjection(effectLedgerMirror);
|
|
4385
|
+
}
|
|
1782
4386
|
suppressInspectionEmissions = false;
|
|
1783
4387
|
createRuntimeActor();
|
|
1784
4388
|
const state = currentState();
|
|
@@ -1797,6 +4401,10 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1797
4401
|
}
|
|
1798
4402
|
},
|
|
1799
4403
|
|
|
4404
|
+
describe: describeControlView,
|
|
4405
|
+
unresolvedEffectEnvelopes: unresolvedEffectEnvelopeIdentities,
|
|
4406
|
+
apply: applyControlAction,
|
|
4407
|
+
|
|
1800
4408
|
// DR-014 §1 / DR-031 §5 / PBRT-45: JSON-safe capture of a parked
|
|
1801
4409
|
// session, including one already-started suspended REVIEW call.
|
|
1802
4410
|
// Defined only at a safe capture point — initialized, not disposing
|
|
@@ -1853,8 +4461,12 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1853
4461
|
const context = (
|
|
1854
4462
|
actor.getSnapshot() as SnapshotFrom<typeof decideMachine>
|
|
1855
4463
|
).context as unknown as Record<string, unknown>;
|
|
4464
|
+
if (deferredEffects !== undefined) {
|
|
4465
|
+
synchronizeDeferredProjection(readEffectLedger());
|
|
4466
|
+
}
|
|
4467
|
+
const unresolved = hasUnresolvedReconciliation();
|
|
1856
4468
|
return {
|
|
1857
|
-
schemaVersion:
|
|
4469
|
+
schemaVersion: 4,
|
|
1858
4470
|
playbookId: sessionIdentity.playbookId,
|
|
1859
4471
|
machine,
|
|
1860
4472
|
roleResumeTokens: snapshotRoleResumeTokens(),
|
|
@@ -1866,14 +4478,15 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1866
4478
|
playbookCall: playbookCallSequence,
|
|
1867
4479
|
},
|
|
1868
4480
|
state,
|
|
1869
|
-
pendingBossQuestions:
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
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,
|
|
1877
4490
|
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
1878
4491
|
};
|
|
1879
4492
|
},
|
|
@@ -1903,6 +4516,28 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1903
4516
|
identity.playbookId,
|
|
1904
4517
|
{ allowSuspendedCall: true },
|
|
1905
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
|
+
}
|
|
1906
4541
|
const suspendedCall = boundSnapshot.suspendedCall;
|
|
1907
4542
|
let finishInitialization!: () => void;
|
|
1908
4543
|
const initialization = new Promise<void>((resolve) => {
|
|
@@ -1921,6 +4556,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1921
4556
|
judgeCallSequence = boundSnapshot.sequences.judgeCall;
|
|
1922
4557
|
playerCallSequence = boundSnapshot.sequences.playerCall;
|
|
1923
4558
|
playbookCallSequence = boundSnapshot.sequences.playbookCall;
|
|
4559
|
+
applyCallSequence = boundSnapshot.sequences.trace;
|
|
1924
4560
|
if (identity.playerSessions) {
|
|
1925
4561
|
priorExternalRoleTokens = snapshotRoleResumeTokens();
|
|
1926
4562
|
}
|
|
@@ -1929,6 +4565,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1929
4565
|
if (suspendedCall !== undefined) {
|
|
1930
4566
|
playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
|
|
1931
4567
|
}
|
|
4568
|
+
synchronizeDeferredProjection(effectLedgerMirror);
|
|
1932
4569
|
suppressInspectionEmissions = true;
|
|
1933
4570
|
createRuntimeActor(boundSnapshot.machine);
|
|
1934
4571
|
actor?.start();
|
|
@@ -1993,12 +4630,17 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1993
4630
|
const turnId = ++turnSequence;
|
|
1994
4631
|
currentTurnId = turnId;
|
|
1995
4632
|
currentSignal = turn.signal;
|
|
4633
|
+
currentAborts = abortReasonClassifier(turn.signal);
|
|
1996
4634
|
controlPlaneError = undefined;
|
|
1997
4635
|
let result: PlaybookRunResult = resultForSnapshot(turn.signal);
|
|
1998
4636
|
let settlement: unknown = result;
|
|
1999
4637
|
const failures: unknown[] = [];
|
|
2000
4638
|
try {
|
|
2001
4639
|
await emitTrace('boss.input.received', { text: turn.text }, { turnId });
|
|
4640
|
+
// A boundary entered aborted records the attempted input, then refuses
|
|
4641
|
+
// delivery before deterministic mapping or the classifier can perform
|
|
4642
|
+
// any host-visible work (DR-036 §5).
|
|
4643
|
+
turn.signal.throwIfAborted();
|
|
2002
4644
|
if (turn.text.trim().length === 0) {
|
|
2003
4645
|
const state = currentState();
|
|
2004
4646
|
result = { outcome: 'no-action', state };
|
|
@@ -2006,23 +4648,91 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2006
4648
|
const event = await classify(turn.text, turn.signal);
|
|
2007
4649
|
if (!event) {
|
|
2008
4650
|
const state = currentState();
|
|
2009
|
-
|
|
4651
|
+
if (!hasUnresolvedReconciliation()) {
|
|
4652
|
+
await emitBoundaryStatus('No playbook action classified.', state);
|
|
4653
|
+
}
|
|
2010
4654
|
result = { outcome: 'no-action', state };
|
|
2011
4655
|
} else if (event.type === 'NO_ACTION') {
|
|
2012
4656
|
result = { outcome: 'no-action', state: currentState() };
|
|
2013
4657
|
} else {
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
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
|
+
}
|
|
2019
4681
|
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
4682
|
+
const deferredMachineCheckpoint =
|
|
4683
|
+
prepared?.proceed === true
|
|
4684
|
+
? detachPersistedMachineSnapshot(
|
|
4685
|
+
actor.getPersistedSnapshot(),
|
|
4686
|
+
)
|
|
4687
|
+
: undefined;
|
|
4688
|
+
if (deferredMachineCheckpoint !== undefined) {
|
|
4689
|
+
suppressInspectionEmissions = true;
|
|
4690
|
+
}
|
|
2023
4691
|
|
|
2024
|
-
|
|
2025
|
-
|
|
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
|
+
}
|
|
2026
4736
|
}
|
|
2027
4737
|
}
|
|
2028
4738
|
settlement = {
|
|
@@ -2031,15 +4741,18 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2031
4741
|
};
|
|
2032
4742
|
} catch (error) {
|
|
2033
4743
|
const primaryError = controlPlaneError;
|
|
4744
|
+
// Only a rejection that is the exact abort reason settles as the
|
|
4745
|
+
// cancellation; a distinct failure observed while the signal is
|
|
4746
|
+
// aborted remains a control error (slc/link.md §Abort).
|
|
2034
4747
|
if (primaryError !== undefined) {
|
|
2035
4748
|
collectFailure(failures, primaryError);
|
|
2036
|
-
} else if (!turn.signal
|
|
4749
|
+
} else if (!isAbortFailure(error, turn.signal)) {
|
|
2037
4750
|
collectFailure(failures, error);
|
|
2038
4751
|
}
|
|
2039
4752
|
const state = currentState();
|
|
2040
4753
|
const effectiveError = primaryError ?? error;
|
|
2041
4754
|
result =
|
|
2042
|
-
turn.signal
|
|
4755
|
+
isAbortFailure(error, turn.signal) && primaryError === undefined
|
|
2043
4756
|
? resultForSnapshot(turn.signal)
|
|
2044
4757
|
: {
|
|
2045
4758
|
outcome: 'failed',
|
|
@@ -2060,13 +4773,13 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2060
4773
|
} catch (error) {
|
|
2061
4774
|
const primaryError = controlPlaneError;
|
|
2062
4775
|
const effectiveError = primaryError ?? error;
|
|
2063
|
-
|
|
4776
|
+
// A drain rejection that is the exact abort reason evidences the
|
|
4777
|
+
// cancellation, not a control-plane failure (slc/link.md §Abort).
|
|
4778
|
+
const drainAborted = isAbortFailure(effectiveError, turn.signal);
|
|
4779
|
+
if (!drainAborted) collectFailure(failures, effectiveError);
|
|
2064
4780
|
const state = currentState();
|
|
2065
4781
|
result = {
|
|
2066
|
-
outcome:
|
|
2067
|
-
turn.signal.aborted && primaryError === undefined
|
|
2068
|
-
? 'aborted'
|
|
2069
|
-
: 'failed',
|
|
4782
|
+
outcome: drainAborted ? 'aborted' : 'failed',
|
|
2070
4783
|
state,
|
|
2071
4784
|
error: normalizeErrorFull(effectiveError) ?? {
|
|
2072
4785
|
name: 'Error',
|
|
@@ -2075,18 +4788,28 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2075
4788
|
};
|
|
2076
4789
|
settlement = { ...result, ...stateIdentity(state) };
|
|
2077
4790
|
}
|
|
2078
|
-
currentSignal = undefined;
|
|
2079
4791
|
try {
|
|
2080
4792
|
await emitTrace('boss.input.settled', settlement, { turnId });
|
|
2081
4793
|
} catch (error) {
|
|
2082
|
-
|
|
4794
|
+
// A settlement-trace rejection that is the exact abort reason also
|
|
4795
|
+
// evidences the cancellation (slc/link.md §Abort).
|
|
4796
|
+
if (!isAbortFailure(error, turn.signal)) {
|
|
4797
|
+
collectFailure(failures, error);
|
|
4798
|
+
}
|
|
2083
4799
|
}
|
|
2084
4800
|
try {
|
|
2085
4801
|
await flush();
|
|
2086
4802
|
} catch (error) {
|
|
2087
|
-
|
|
4803
|
+
// A late flush rejection that is the exact abort reason likewise
|
|
4804
|
+
// evidences the cancellation; the settled result already labels
|
|
4805
|
+
// the turn aborted then (slc/link.md §Abort).
|
|
4806
|
+
if (!isAbortFailure(error, turn.signal)) {
|
|
4807
|
+
collectFailure(failures, error);
|
|
4808
|
+
}
|
|
2088
4809
|
} finally {
|
|
2089
4810
|
const primaryError = controlPlaneError;
|
|
4811
|
+
currentSignal = undefined;
|
|
4812
|
+
currentAborts = undefined;
|
|
2090
4813
|
currentTurnId = undefined;
|
|
2091
4814
|
controlPlaneError = undefined;
|
|
2092
4815
|
if (primaryError !== undefined) throw primaryError;
|
|
@@ -2120,8 +4843,13 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2120
4843
|
if (currentSignal !== undefined) {
|
|
2121
4844
|
throw new Error('decide runtime: another runtime turn is active');
|
|
2122
4845
|
}
|
|
4846
|
+
refreshReconciliationProjection();
|
|
4847
|
+
if (hasUnresolvedReconciliation()) {
|
|
4848
|
+
return { outcome: 'no-action', state: currentState() };
|
|
4849
|
+
}
|
|
2123
4850
|
currentTurnId = playbookCallTurnIds.get(callId);
|
|
2124
4851
|
currentSignal = signal;
|
|
4852
|
+
currentAborts = abortReasonClassifier(signal);
|
|
2125
4853
|
controlPlaneError = undefined;
|
|
2126
4854
|
let runResult: PlaybookRunResult | undefined;
|
|
2127
4855
|
let operationError: unknown;
|
|
@@ -2148,13 +4876,56 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2148
4876
|
} catch (error) {
|
|
2149
4877
|
drainError = error;
|
|
2150
4878
|
}
|
|
2151
|
-
const
|
|
4879
|
+
const aborts = currentAborts ?? abortReasonClassifier(signal);
|
|
4880
|
+
// The control latch has already classified its failure as distinct
|
|
4881
|
+
// under the operation that owned it. Only still-unclassified drain and
|
|
4882
|
+
// operation candidates may be cancellation evidence for this resume.
|
|
4883
|
+
const controlFailure = controlPlaneError;
|
|
4884
|
+
const drainAbort =
|
|
4885
|
+
controlFailure === undefined &&
|
|
4886
|
+
drainError !== undefined &&
|
|
4887
|
+
aborts.isAbortReason(drainError);
|
|
4888
|
+
const operationAbort =
|
|
4889
|
+
controlFailure === undefined &&
|
|
4890
|
+
operationError !== undefined &&
|
|
4891
|
+
aborts.isAbortReason(operationError);
|
|
4892
|
+
const abortEvidence =
|
|
4893
|
+
(drainAbort ? drainError : undefined) ??
|
|
4894
|
+
(operationAbort ? operationError : undefined);
|
|
4895
|
+
const failure =
|
|
4896
|
+
controlFailure ??
|
|
4897
|
+
(drainAbort ? undefined : drainError) ??
|
|
4898
|
+
(operationAbort ? undefined : operationError);
|
|
2152
4899
|
currentSignal = undefined;
|
|
4900
|
+
currentAborts = undefined;
|
|
2153
4901
|
currentTurnId = undefined;
|
|
2154
4902
|
controlPlaneError = undefined;
|
|
2155
4903
|
if (failure !== undefined) throw failure;
|
|
4904
|
+
if (
|
|
4905
|
+
abortEvidence !== undefined &&
|
|
4906
|
+
runResult?.outcome !== 'terminal' &&
|
|
4907
|
+
runResult?.outcome !== 'suspended'
|
|
4908
|
+
) {
|
|
4909
|
+
const state = currentState();
|
|
4910
|
+
runResult = {
|
|
4911
|
+
outcome: 'aborted',
|
|
4912
|
+
state,
|
|
4913
|
+
error: normalizeErrorFull(abortEvidence) ?? {
|
|
4914
|
+
name: 'AbortError',
|
|
4915
|
+
message: String(abortEvidence),
|
|
4916
|
+
},
|
|
4917
|
+
};
|
|
4918
|
+
}
|
|
2156
4919
|
if (runResult === undefined) {
|
|
2157
|
-
|
|
4920
|
+
if (signal.aborted) {
|
|
4921
|
+
// Every candidate was the abort's own evidence: settle on the
|
|
4922
|
+
// machine's state under the aborted boundary signal (DR-036 §4).
|
|
4923
|
+
runResult = resultForSnapshot(signal);
|
|
4924
|
+
} else {
|
|
4925
|
+
throw new Error(
|
|
4926
|
+
'decide runtime: playbook resume produced no result',
|
|
4927
|
+
);
|
|
4928
|
+
}
|
|
2158
4929
|
}
|
|
2159
4930
|
return runResult;
|
|
2160
4931
|
},
|
|
@@ -2208,13 +4979,30 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2208
4979
|
activeEmissionCalls.clear();
|
|
2209
4980
|
emissionQueue.clear();
|
|
2210
4981
|
judgeQueue.clear();
|
|
4982
|
+
semanticCompletionQueue.clear();
|
|
2211
4983
|
actor = undefined;
|
|
2212
4984
|
currentSignal = undefined;
|
|
4985
|
+
currentAborts = undefined;
|
|
4986
|
+
actorSettlementAborts.length = 0;
|
|
4987
|
+
actorSettlementErrorAborts = undefined;
|
|
2213
4988
|
currentTurnId = undefined;
|
|
2214
4989
|
ports = undefined;
|
|
2215
4990
|
sessionIdentity = undefined;
|
|
2216
4991
|
previousState = undefined;
|
|
2217
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;
|
|
2218
5006
|
disposed = true;
|
|
2219
5007
|
}
|
|
2220
5008
|
if (failures.length === 1) throw failures[0];
|
|
@@ -2224,7 +5012,23 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2224
5012
|
})();
|
|
2225
5013
|
return disposalPromise;
|
|
2226
5014
|
},
|
|
5015
|
+
|
|
5016
|
+
// @internal — test-only parity with the shared factory's bridge escape
|
|
5017
|
+
// hatch. This is hidden by the PlaybookRuntime return type.
|
|
5018
|
+
_getNestedBridge() {
|
|
5019
|
+
return nestedBridge;
|
|
5020
|
+
},
|
|
2227
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
|
+
);
|
|
2228
5032
|
};
|
|
2229
5033
|
|
|
2230
5034
|
export const _internal = {
|
|
@@ -2234,9 +5038,9 @@ export const _internal = {
|
|
|
2234
5038
|
buildClassifierPrompt,
|
|
2235
5039
|
parseClassification,
|
|
2236
5040
|
buildAdjudicatorPrompt,
|
|
2237
|
-
parseAdjudication,
|
|
2238
5041
|
combineSignals,
|
|
2239
5042
|
pendingQuestionsFromContext,
|
|
5043
|
+
pendingQuestionsForState,
|
|
2240
5044
|
normalizeErrorCompact,
|
|
2241
5045
|
normalizeErrorFull,
|
|
2242
5046
|
STATE_DESCRIPTIONS,
|
|
@@ -2244,6 +5048,7 @@ export const _internal = {
|
|
|
2244
5048
|
ROLE_STATE_IDS,
|
|
2245
5049
|
VERBATIM_PAYLOAD_FIELDS,
|
|
2246
5050
|
BOSS_INTERRUPT_TARGETS,
|
|
5051
|
+
UNFINISHED_FINAL_STATE_IDS,
|
|
2247
5052
|
CONTINUATION_PREAMBLE,
|
|
2248
5053
|
TELEMETRY_TOPIC,
|
|
2249
5054
|
};
|