@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
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
// onError routes to the quiescent failed state.
|
|
16
16
|
// Output profile: bespoke parallel runtime with the shared nested-call
|
|
17
17
|
// bridge (slc/link.md §Output)
|
|
18
|
+
import { randomUUID } from 'node:crypto';
|
|
18
19
|
import PQueue from 'p-queue';
|
|
19
20
|
import { createActor, fromPromise } from 'xstate';
|
|
20
|
-
import {
|
|
21
|
+
import { createAcceptedOutcomeConsumer, } from '../../../src/accepted-outcome.js';
|
|
22
|
+
import { assertJsonSafe, assertPlaybookEffectLedger, assertPlaybookRuntimeSnapshot, combineAbortSignals, createNestedPlaybookBridge, detachPersistedMachineSnapshot, normalizeError, normalizePlaybookSnapshot, PlaybookSemanticCandidateStructureError, reconcilePlaybookSemanticEvidence, snapshotJsonValue, snapshotPlaybookSession, validatePlayerResult, waitForPlaybookQuiescence, } from '../../../src/xstate-runtime.js';
|
|
21
23
|
import decideMachine from './decide.fsm.js';
|
|
22
24
|
function snapshotDecideRuntimeOptions(value) {
|
|
23
25
|
const captured = snapshotJsonValue(value, 'DECIDE runtime options');
|
|
@@ -30,19 +32,28 @@ function snapshotDecideRuntimeOptions(value) {
|
|
|
30
32
|
}
|
|
31
33
|
return Object.freeze({});
|
|
32
34
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
35
|
+
function authoredStateDescriptions(states) {
|
|
36
|
+
const descriptions = {};
|
|
37
|
+
const visit = (children) => {
|
|
38
|
+
for (const state of Object.values(children ?? {})) {
|
|
39
|
+
const stateId = state.meta?.playbook?.stateId;
|
|
40
|
+
const description = state.meta?.playbook?.description;
|
|
41
|
+
if (typeof stateId === 'string' &&
|
|
42
|
+
typeof description === 'string' &&
|
|
43
|
+
description.trim().length > 0) {
|
|
44
|
+
const existing = descriptions[stateId];
|
|
45
|
+
if (existing !== undefined && existing !== description) {
|
|
46
|
+
throw new Error(`DECIDE state ${stateId} declares conflicting descriptions`);
|
|
47
|
+
}
|
|
48
|
+
descriptions[stateId] = description;
|
|
49
|
+
}
|
|
50
|
+
visit(state.states);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
visit(states);
|
|
54
|
+
return Object.freeze(descriptions);
|
|
55
|
+
}
|
|
56
|
+
const STATE_DESCRIPTIONS = authoredStateDescriptions(decideMachine.config.states);
|
|
46
57
|
const ROLE_STATES = [
|
|
47
58
|
{ stateId: 'askCoderProposal', role: 'coder', sourceItem: 'DECIDE-1' },
|
|
48
59
|
{
|
|
@@ -53,13 +64,64 @@ const ROLE_STATES = [
|
|
|
53
64
|
{ stateId: 'commitCoderProposal', role: 'coder', sourceItem: 'DECIDE-3' },
|
|
54
65
|
];
|
|
55
66
|
const ROLE_STATE_IDS = new Set(ROLE_STATES.map((state) => state.stateId));
|
|
67
|
+
const ACCEPTED_OUTCOME_DECLARATIONS = Object.freeze({
|
|
68
|
+
askCoderProposal: new Set(['proposed', 'needsBossReply']),
|
|
69
|
+
askReviewerProposal: new Set(['proposed', 'needsBossReply']),
|
|
70
|
+
commitCoderProposal: new Set(['committed', 'needsBossReply']),
|
|
71
|
+
});
|
|
72
|
+
const DECIDE_OUTCOME_AUTHORITY = Object.freeze({
|
|
73
|
+
governedPlayerStates: Object.freeze({
|
|
74
|
+
askCoderProposal: Object.freeze({
|
|
75
|
+
proposed: Object.freeze({
|
|
76
|
+
fields: Object.freeze({ coderProposal: 'presentation' }),
|
|
77
|
+
repositoryDisposition: 'unchanged',
|
|
78
|
+
}),
|
|
79
|
+
needsBossReply: Object.freeze({
|
|
80
|
+
fields: Object.freeze({ question: 'presentation' }),
|
|
81
|
+
repositoryDisposition: 'unchanged',
|
|
82
|
+
}),
|
|
83
|
+
}),
|
|
84
|
+
askReviewerProposal: Object.freeze({
|
|
85
|
+
proposed: Object.freeze({
|
|
86
|
+
fields: Object.freeze({ reviewerProposal: 'presentation' }),
|
|
87
|
+
repositoryDisposition: 'unchanged',
|
|
88
|
+
}),
|
|
89
|
+
needsBossReply: Object.freeze({
|
|
90
|
+
fields: Object.freeze({ question: 'presentation' }),
|
|
91
|
+
repositoryDisposition: 'unchanged',
|
|
92
|
+
}),
|
|
93
|
+
}),
|
|
94
|
+
commitCoderProposal: Object.freeze({
|
|
95
|
+
committed: Object.freeze({
|
|
96
|
+
fields: Object.freeze({
|
|
97
|
+
coderOutput: 'presentation',
|
|
98
|
+
latestCommit: 'effect',
|
|
99
|
+
}),
|
|
100
|
+
repositoryDisposition: 'one-descendant-commit',
|
|
101
|
+
}),
|
|
102
|
+
needsBossReply: Object.freeze({
|
|
103
|
+
fields: Object.freeze({ question: 'presentation' }),
|
|
104
|
+
repositoryDisposition: 'deferred',
|
|
105
|
+
}),
|
|
106
|
+
}),
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
const PROPOSAL_STATE_BY_ROLE = Object.freeze({
|
|
110
|
+
coder: 'askCoderProposal',
|
|
111
|
+
reviewer: 'askReviewerProposal',
|
|
112
|
+
});
|
|
56
113
|
const ROLE_IDS = ['coder', 'reviewer'];
|
|
57
114
|
const ROLE_ID_SET = new Set(ROLE_IDS);
|
|
58
115
|
const roleLabel = (roleId) => roleId === 'coder' ? 'Coder' : 'Reviewer';
|
|
59
116
|
const BOSS_INTERRUPT_TARGETS = ['independentProposals'];
|
|
60
117
|
const BOSS_INTERRUPT_TARGET_IDS = new Set(BOSS_INTERRUPT_TARGETS);
|
|
118
|
+
const UNFINISHED_FINAL_STATE_IDS = new Set([
|
|
119
|
+
'reportedReviewFailure',
|
|
120
|
+
]);
|
|
61
121
|
const TELEMETRY_TOPIC = 'playbook.fsm.state';
|
|
62
122
|
const TRACE_TOPIC = 'playbook.trace';
|
|
123
|
+
const UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID = 'reconcile:unresolved-effect';
|
|
124
|
+
const UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID = 'abandon:unresolved-effect';
|
|
63
125
|
const CONTINUATION_PREAMBLE = 'You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.';
|
|
64
126
|
const PLACEHOLDER_FIELDS = [['<caller-topic>', 'callerTopic']];
|
|
65
127
|
const VERBATIM_PAYLOAD_FIELDS = new Set([
|
|
@@ -114,6 +176,27 @@ function requiredFieldsFor(description) {
|
|
|
114
176
|
}
|
|
115
177
|
return fields;
|
|
116
178
|
}
|
|
179
|
+
function governedOutcomesFor(input) {
|
|
180
|
+
const outcomes = DECIDE_OUTCOME_AUTHORITY.governedPlayerStates[input.stateId];
|
|
181
|
+
if (outcomes === undefined) {
|
|
182
|
+
throw new TypeError(`DECIDE governed player state ${JSON.stringify(input.stateId)} has no outcome authority`);
|
|
183
|
+
}
|
|
184
|
+
const declaredGuards = Object.keys(outcomes).sort();
|
|
185
|
+
const authoredGuards = Object.keys(input.result).sort();
|
|
186
|
+
if (declaredGuards.length !== authoredGuards.length ||
|
|
187
|
+
declaredGuards.some((guard, index) => guard !== authoredGuards[index])) {
|
|
188
|
+
throw new TypeError(`DECIDE governed player state ${input.stateId} changed its authored outcome set`);
|
|
189
|
+
}
|
|
190
|
+
for (const guard of declaredGuards) {
|
|
191
|
+
const required = requiredFieldsFor(input.result[guard]).sort();
|
|
192
|
+
const authoritative = Object.keys(outcomes[guard].fields).sort();
|
|
193
|
+
if (required.length !== authoritative.length ||
|
|
194
|
+
required.some((field, index) => field !== authoritative[index])) {
|
|
195
|
+
throw new TypeError(`DECIDE governed outcome ${input.stateId}.${guard} changed its payload fields`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return outcomes;
|
|
199
|
+
}
|
|
117
200
|
// LLM judges routinely wrap JSON in prose/fences or damage its tail. Match
|
|
118
201
|
// CODE's recovery contract: scan candidate starts in document order, prefer a
|
|
119
202
|
// strict balanced value at each position, then repair trailing commas and
|
|
@@ -185,6 +268,14 @@ function sortJson(value) {
|
|
|
185
268
|
function stableJson(value, path) {
|
|
186
269
|
return JSON.stringify(sortJson(snapshotJsonValue(value, path)));
|
|
187
270
|
}
|
|
271
|
+
function deepFreeze(value) {
|
|
272
|
+
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
273
|
+
Object.freeze(value);
|
|
274
|
+
for (const member of Object.values(value))
|
|
275
|
+
deepFreeze(member);
|
|
276
|
+
}
|
|
277
|
+
return value;
|
|
278
|
+
}
|
|
188
279
|
function stripCodeFence(text) {
|
|
189
280
|
const fence = text.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/i);
|
|
190
281
|
return fence ? fence[1].trim() : text;
|
|
@@ -314,7 +405,8 @@ function parseClassification(raw, text, pendingQuestionIds = []) {
|
|
|
314
405
|
}
|
|
315
406
|
return null;
|
|
316
407
|
}
|
|
317
|
-
function buildAdjudicatorPrompt(input, playerOutput) {
|
|
408
|
+
function buildAdjudicatorPrompt(input, playerOutput, correction) {
|
|
409
|
+
const outcomes = governedOutcomesFor(input);
|
|
318
410
|
const lines = [];
|
|
319
411
|
lines.push('You are the guard adjudicator for a playbook state machine.');
|
|
320
412
|
lines.push('This is hidden control work. Do not call tools, inspect files, or ' +
|
|
@@ -330,54 +422,32 @@ function buildAdjudicatorPrompt(input, playerOutput) {
|
|
|
330
422
|
lines.push('');
|
|
331
423
|
lines.push('Guards (choose exactly one; the descriptions are authoritative and must be applied as written):');
|
|
332
424
|
for (const [guard, description] of Object.entries(input.result)) {
|
|
333
|
-
|
|
425
|
+
const semanticFields = Object.entries(outcomes[guard]?.fields ?? {})
|
|
426
|
+
.filter(([, authority]) => authority === 'semantic')
|
|
427
|
+
.map(([field]) => field);
|
|
428
|
+
lines.push(`- ${guard}: semantic fields: ${semanticFields.length === 0 ? '(none)' : semanticFields.join(', ')}; ${description}`);
|
|
334
429
|
}
|
|
335
|
-
|
|
336
|
-
for
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
runtimeOwnedFields.add(field);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
if (runtimeOwnedFields.size > 0) {
|
|
430
|
+
lines.push('');
|
|
431
|
+
lines.push('Reply with exactly the chosen `guard` and every semantic-owned field for that guard, and no other field.');
|
|
432
|
+
lines.push('Do not include presentation-, effect-, or runtime-owned fields; the runtime supplies those from authoritative evidence.');
|
|
433
|
+
if (correction !== undefined) {
|
|
343
434
|
lines.push('');
|
|
344
|
-
lines.push(
|
|
435
|
+
lines.push('Your first reply was structurally invalid:');
|
|
436
|
+
lines.push('"""');
|
|
437
|
+
lines.push(correction.reply);
|
|
438
|
+
lines.push('"""');
|
|
439
|
+
lines.push(`Validation error: ${correction.error}`);
|
|
440
|
+
lines.push('Correct only that structure using the same player output and outcome schema.');
|
|
345
441
|
}
|
|
346
|
-
lines.push('');
|
|
347
|
-
lines.push('Reply with a single JSON object: { "guard": "<one of the guard names above>", ...any payload fields the chosen guard description requires }.');
|
|
348
442
|
return lines.join('\n');
|
|
349
443
|
}
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
throw new Error('adjudicator returned empty or malformed JSON');
|
|
354
|
-
}
|
|
355
|
-
const guard = obj.guard;
|
|
356
|
-
if (!Object.prototype.hasOwnProperty.call(input.result, guard)) {
|
|
357
|
-
throw new Error(`adjudicator returned undeclared guard "${guard}" for ${input.sourceItem}`);
|
|
358
|
-
}
|
|
359
|
-
const requiredFields = requiredFieldsFor(input.result[guard]);
|
|
360
|
-
const allowedFields = new Set(['guard', ...requiredFields]);
|
|
361
|
-
for (const key of Reflect.ownKeys(obj)) {
|
|
362
|
-
if (typeof key !== 'string' || !allowedFields.has(key)) {
|
|
363
|
-
throw new Error(`adjudicator response for guard "${guard}" included undeclared field "${String(key)}"`);
|
|
364
|
-
}
|
|
444
|
+
function parseGovernedSemanticCandidate(raw) {
|
|
445
|
+
try {
|
|
446
|
+
return parseJudgeJson(raw);
|
|
365
447
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (VERBATIM_PAYLOAD_FIELDS.has(field)) {
|
|
369
|
-
output[field] = finalText;
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
const value = obj[field];
|
|
373
|
-
if (value === undefined ||
|
|
374
|
-
value === null ||
|
|
375
|
-
(typeof value === 'string' && value.trim() === '')) {
|
|
376
|
-
throw new Error(`adjudicator response for guard "${guard}" missing required field "${field}"`);
|
|
377
|
-
}
|
|
378
|
-
output[field] = value;
|
|
448
|
+
catch (error) {
|
|
449
|
+
throw new PlaybookSemanticCandidateStructureError(error instanceof Error ? error.message : 'reply is not valid JSON');
|
|
379
450
|
}
|
|
380
|
-
return output;
|
|
381
451
|
}
|
|
382
452
|
function combineSignals(a, b) {
|
|
383
453
|
return combineAbortSignals(a, b);
|
|
@@ -398,9 +468,136 @@ function normalizeErrorFull(err) {
|
|
|
398
468
|
function isEmptyFinalText(finalText) {
|
|
399
469
|
return finalText === undefined || finalText.trim().length === 0;
|
|
400
470
|
}
|
|
471
|
+
function schema3Construction(value) {
|
|
472
|
+
if (!isPlainObject(value)) {
|
|
473
|
+
throw new TypeError('DECIDE schema-3 factory input must be a plain object');
|
|
474
|
+
}
|
|
475
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
476
|
+
const keys = Reflect.ownKeys(value);
|
|
477
|
+
if (keys.length !== 2 ||
|
|
478
|
+
!keys.includes('configuredOptions') ||
|
|
479
|
+
!keys.includes('hostCapabilities') ||
|
|
480
|
+
keys.some((key) => {
|
|
481
|
+
const descriptor = descriptors[key];
|
|
482
|
+
return (descriptor?.get !== undefined ||
|
|
483
|
+
descriptor?.set !== undefined ||
|
|
484
|
+
descriptor?.enumerable !== true ||
|
|
485
|
+
!Object.prototype.hasOwnProperty.call(descriptor, 'value'));
|
|
486
|
+
})) {
|
|
487
|
+
throw new TypeError('DECIDE schema-3 factory input must contain exactly configuredOptions and hostCapabilities data properties');
|
|
488
|
+
}
|
|
489
|
+
const configuredOptions = descriptors.configuredOptions.value;
|
|
490
|
+
if (configuredOptions !== null &&
|
|
491
|
+
typeof configuredOptions === 'object' &&
|
|
492
|
+
Object.prototype.hasOwnProperty.call(configuredOptions, 'hostCapabilities')) {
|
|
493
|
+
throw new TypeError('DECIDE configured options must not contain hostCapabilities');
|
|
494
|
+
}
|
|
495
|
+
const hostCapabilities = descriptors.hostCapabilities.value;
|
|
496
|
+
if (hostCapabilities === null ||
|
|
497
|
+
typeof hostCapabilities !== 'object' ||
|
|
498
|
+
Array.isArray(hostCapabilities)) {
|
|
499
|
+
throw new TypeError('DECIDE schema-3 factory input hostCapabilities must be a live object');
|
|
500
|
+
}
|
|
501
|
+
const repositoryDescriptor = Object.getOwnPropertyDescriptor(hostCapabilities, 'repository');
|
|
502
|
+
const authorityDescriptor = Object.getOwnPropertyDescriptor(hostCapabilities, 'authority');
|
|
503
|
+
const effectLedgerDescriptor = Object.getOwnPropertyDescriptor(hostCapabilities, 'effectLedger');
|
|
504
|
+
const repository = repositoryDescriptor?.value;
|
|
505
|
+
const authority = authorityDescriptor?.value;
|
|
506
|
+
const effectLedger = effectLedgerDescriptor?.value;
|
|
507
|
+
if (authorityDescriptor === undefined ||
|
|
508
|
+
authorityDescriptor.get !== undefined ||
|
|
509
|
+
authorityDescriptor.set !== undefined ||
|
|
510
|
+
!Object.prototype.hasOwnProperty.call(authorityDescriptor, 'value') ||
|
|
511
|
+
!isPlainObject(authority) ||
|
|
512
|
+
authority.artifactSchema !== 3 ||
|
|
513
|
+
authority.playbookId !== 'decide' ||
|
|
514
|
+
typeof authority.sessionId !== 'string' ||
|
|
515
|
+
authority.sessionId.length === 0 ||
|
|
516
|
+
!Array.isArray(authority.requiredRoleIds) ||
|
|
517
|
+
stableJson([...authority.requiredRoleIds].sort(), 'DECIDE host authority required roles') !== stableJson([...ROLE_IDS].sort(), 'DECIDE required roles') ||
|
|
518
|
+
!Array.isArray(authority.concurrentRoleSets) ||
|
|
519
|
+
authority.concurrentRoleSets.length !== 1 ||
|
|
520
|
+
!Array.isArray(authority.concurrentRoleSets[0]) ||
|
|
521
|
+
stableJson([...authority.concurrentRoleSets[0]].sort(), 'DECIDE host authority concurrent roles') !== stableJson([...ROLE_IDS].sort(), 'DECIDE concurrent roles')) {
|
|
522
|
+
throw new TypeError('DECIDE schema-3 factory input hostCapabilities.authority must identify one schema-3 playbook session');
|
|
523
|
+
}
|
|
524
|
+
if (repositoryDescriptor === undefined ||
|
|
525
|
+
repositoryDescriptor.get !== undefined ||
|
|
526
|
+
repositoryDescriptor.set !== undefined ||
|
|
527
|
+
!Object.prototype.hasOwnProperty.call(repositoryDescriptor, 'value') ||
|
|
528
|
+
!isPlainObject(repository) ||
|
|
529
|
+
typeof repository.runExclusive !== 'function' ||
|
|
530
|
+
typeof repository.runCohort !== 'function' ||
|
|
531
|
+
typeof repository.runDeferred !== 'function') {
|
|
532
|
+
throw new TypeError('DECIDE schema-3 factory input hostCapabilities.repository must expose runExclusive, runCohort, and runDeferred functions');
|
|
533
|
+
}
|
|
534
|
+
if (effectLedgerDescriptor === undefined ||
|
|
535
|
+
effectLedgerDescriptor.get !== undefined ||
|
|
536
|
+
effectLedgerDescriptor.set !== undefined ||
|
|
537
|
+
!Object.prototype.hasOwnProperty.call(effectLedgerDescriptor, 'value') ||
|
|
538
|
+
!isPlainObject(effectLedger) ||
|
|
539
|
+
typeof effectLedger.snapshot !== 'function' ||
|
|
540
|
+
typeof effectLedger.writeAhead !== 'function') {
|
|
541
|
+
throw new TypeError('DECIDE schema-3 factory input hostCapabilities.effectLedger must expose snapshot and writeAhead functions');
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
configuredOptions: configuredOptions,
|
|
545
|
+
hostCapabilities: {
|
|
546
|
+
authority: authority,
|
|
547
|
+
repository: repository,
|
|
548
|
+
effectLedger: effectLedger,
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function deferredValue() {
|
|
553
|
+
let resolve;
|
|
554
|
+
let reject;
|
|
555
|
+
const promise = new Promise((onResolve, onReject) => {
|
|
556
|
+
resolve = onResolve;
|
|
557
|
+
reject = onReject;
|
|
558
|
+
});
|
|
559
|
+
return { promise, resolve, reject };
|
|
560
|
+
}
|
|
561
|
+
function hasCompleteUnchangedReceipt(boundary) {
|
|
562
|
+
return boundary.physicalReceipt?.classification === 'unchanged';
|
|
563
|
+
}
|
|
564
|
+
// A corrective call is bound to the exact physical boundary it would repeat.
|
|
565
|
+
// A failed-state restart replays the whole entry event. Cooperative host
|
|
566
|
+
// attempts are serialized in ledger order, so the latest durable boundary
|
|
567
|
+
// identifies the causal host attempt even when a nested or sibling runtime
|
|
568
|
+
// wrote it; every boundary in that attempt must have a complete unchanged
|
|
569
|
+
// receipt.
|
|
570
|
+
// The durable ledger remains the authority in both cases; no process-local
|
|
571
|
+
// player result or presentation text can make a replay safe.
|
|
572
|
+
function createAutomaticReplayPolicy(evidence) {
|
|
573
|
+
const readLedger = () => assertPlaybookEffectLedger(evidence.effectLedger.snapshot(), 'DECIDE automatic-replay effect ledger');
|
|
574
|
+
return Object.freeze({
|
|
575
|
+
allowsEmptyOkCorrection(runtimeSessionId, callId) {
|
|
576
|
+
const matching = readLedger().boundaries.filter((boundary) => boundary.runtimeSessionId === runtimeSessionId &&
|
|
577
|
+
boundary.callId === callId);
|
|
578
|
+
return (matching.length === 1 && hasCompleteUnchangedReceipt(matching[0]));
|
|
579
|
+
},
|
|
580
|
+
allowsFailureStateRetry() {
|
|
581
|
+
const ledger = readLedger();
|
|
582
|
+
const latest = ledger.boundaries.at(-1);
|
|
583
|
+
if (latest === undefined)
|
|
584
|
+
return false;
|
|
585
|
+
const attempt = ledger.boundaries.filter((boundary) => boundary.attemptId === latest.attemptId);
|
|
586
|
+
return (attempt.length > 0 && attempt.every(hasCompleteUnchangedReceipt));
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
}
|
|
401
590
|
function isAbortFailure(error, signal) {
|
|
402
591
|
return signal.aborted && Object.is(error, signal.reason);
|
|
403
592
|
}
|
|
593
|
+
function abortReasonClassifier(...sources) {
|
|
594
|
+
const captured = sources.filter((source) => source !== undefined);
|
|
595
|
+
return Object.freeze({
|
|
596
|
+
isAbortReason: (error) => captured.some((source) => source instanceof AbortSignal
|
|
597
|
+
? isAbortFailure(error, source)
|
|
598
|
+
: source.isAbortReason(error)),
|
|
599
|
+
});
|
|
600
|
+
}
|
|
404
601
|
function pendingQuestionsFromContext(context) {
|
|
405
602
|
const pending = context.pendingBossQuestions;
|
|
406
603
|
if (pending === undefined ||
|
|
@@ -440,6 +637,22 @@ const STATUS_STATE_IDS = new Set([
|
|
|
440
637
|
...WAIT_STATE_IDS,
|
|
441
638
|
'failed',
|
|
442
639
|
]);
|
|
640
|
+
// PBRT-45: a question is pending only while its authored reply-wait state
|
|
641
|
+
// is active. The context retains an answered question through the resumed
|
|
642
|
+
// player call so the Q+A continuation prompt can quote it, and each branch
|
|
643
|
+
// keeps its own entry through the parallel region — so an unfiltered
|
|
644
|
+
// projection would report the answered question as still awaiting during
|
|
645
|
+
// the resume, and both branch questions after only one remains pending.
|
|
646
|
+
const RESUME_WAIT_STATE_IDS = {
|
|
647
|
+
...Object.fromEntries(Object.entries(WAIT_STATE_RESUME_IDS).map(([waitStateId, resumeStateId]) => [
|
|
648
|
+
resumeStateId,
|
|
649
|
+
waitStateId,
|
|
650
|
+
])),
|
|
651
|
+
commitCoderProposal: 'awaitBossReply',
|
|
652
|
+
};
|
|
653
|
+
function pendingQuestionsForState(state, context) {
|
|
654
|
+
return pendingQuestionsFromContext(context).filter((pending) => state.activeStateIds.includes(RESUME_WAIT_STATE_IDS[pending.resumeStateId] ?? ''));
|
|
655
|
+
}
|
|
443
656
|
function questionForWaitState(stateId, pendingQuestions) {
|
|
444
657
|
const resumeStateId = WAIT_STATE_RESUME_IDS[stateId];
|
|
445
658
|
if (resumeStateId !== undefined) {
|
|
@@ -477,8 +690,8 @@ function normalizedTransitionEvent(event) {
|
|
|
477
690
|
}
|
|
478
691
|
return snapshotJsonValue(descriptor, 'FSM event');
|
|
479
692
|
}
|
|
480
|
-
function telemetryPayload(previousState, state, event, context) {
|
|
481
|
-
const pendingBossQuestions =
|
|
693
|
+
function telemetryPayload(previousState, state, event, context, hiddenQuestionId) {
|
|
694
|
+
const pendingBossQuestions = pendingQuestionsForState(state, context).filter(({ questionId }) => questionId !== hiddenQuestionId);
|
|
482
695
|
const prior = previousState ?? state;
|
|
483
696
|
const payload = {
|
|
484
697
|
from: prior.value,
|
|
@@ -494,12 +707,20 @@ function telemetryPayload(previousState, state, event, context) {
|
|
|
494
707
|
assertJsonSafe(payload);
|
|
495
708
|
return payload;
|
|
496
709
|
}
|
|
497
|
-
|
|
710
|
+
function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
711
|
+
const automaticReplayPolicy = createAutomaticReplayPolicy(deferredEffects);
|
|
498
712
|
const fsmInput = snapshotDecideRuntimeOptions(options);
|
|
713
|
+
const readEffectLedger = () => assertPlaybookEffectLedger(deferredEffects.effectLedger.snapshot(), 'DECIDE current host effect ledger');
|
|
714
|
+
let effectLedgerMirror = readEffectLedger();
|
|
715
|
+
const acceptedOutcomeConsumer = createAcceptedOutcomeConsumer((source, acceptedOutcome) => Object.prototype.hasOwnProperty.call(ACCEPTED_OUTCOME_DECLARATIONS, source) &&
|
|
716
|
+
ACCEPTED_OUTCOME_DECLARATIONS[source]?.has(acceptedOutcome) === true);
|
|
499
717
|
let ports;
|
|
500
718
|
let sessionIdentity;
|
|
501
719
|
let actor;
|
|
502
720
|
let currentSignal;
|
|
721
|
+
let currentAborts;
|
|
722
|
+
const actorSettlementAborts = [];
|
|
723
|
+
let actorSettlementErrorAborts;
|
|
503
724
|
let currentTurnId;
|
|
504
725
|
let previousState;
|
|
505
726
|
let suppressInspectionEmissions = false;
|
|
@@ -509,6 +730,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
509
730
|
let judgeCallSequence = 0;
|
|
510
731
|
let playerCallSequence = 0;
|
|
511
732
|
let playbookCallSequence = 0;
|
|
733
|
+
let applyCallSequence = 0;
|
|
512
734
|
let lifecycleStarted = false;
|
|
513
735
|
let initInFlight;
|
|
514
736
|
let disposed = false;
|
|
@@ -522,6 +744,23 @@ export const createPlaybookRuntime = (options) => {
|
|
|
522
744
|
const activeEmissionCalls = new Set();
|
|
523
745
|
const emissionQueue = new PQueue({ concurrency: 1 });
|
|
524
746
|
const judgeQueue = new PQueue({ concurrency: 1 });
|
|
747
|
+
// A proposal cohort completes both semantic callbacks concurrently at the
|
|
748
|
+
// repository seam. Serialize each whole adjudication/correction transaction
|
|
749
|
+
// so their durable correction-budget compare-and-swaps cannot race.
|
|
750
|
+
const semanticCompletionQueue = new PQueue({ concurrency: 1 });
|
|
751
|
+
const governedOutputsByBoundaryId = new Map();
|
|
752
|
+
const governedFailuresByBoundaryId = new Map();
|
|
753
|
+
const governedEvidenceByBoundaryId = new Map();
|
|
754
|
+
const governedReceiptsByBoundaryId = new Map();
|
|
755
|
+
const governedPlayerOutputs = new WeakMap();
|
|
756
|
+
const unresolvedSemanticBoundaryIds = new Set();
|
|
757
|
+
const appliedControlReceipts = new Map();
|
|
758
|
+
const pendingProposalCohort = new Map();
|
|
759
|
+
let activeProposalCohort;
|
|
760
|
+
let completedProposalCohortTurnId;
|
|
761
|
+
let deferredOperationId;
|
|
762
|
+
let hiddenDeferredOperationId;
|
|
763
|
+
let activeDeferredContinuation;
|
|
525
764
|
const collectFailure = (failures, error) => {
|
|
526
765
|
if (error instanceof AggregateError) {
|
|
527
766
|
for (const nested of error.errors)
|
|
@@ -536,24 +775,27 @@ export const createPlaybookRuntime = (options) => {
|
|
|
536
775
|
if (!isAbortFailure(error, signal))
|
|
537
776
|
controlPlaneError ??= error;
|
|
538
777
|
};
|
|
539
|
-
const latchInspectionError = (error) => {
|
|
540
|
-
if (
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
778
|
+
const latchInspectionError = (error, aborts = currentAborts) => {
|
|
779
|
+
if (aborts?.isAbortReason(error))
|
|
780
|
+
return;
|
|
781
|
+
if (currentSignal !== undefined)
|
|
782
|
+
controlPlaneError ??= error;
|
|
783
|
+
else
|
|
544
784
|
collectFailure(emissionFailures, error);
|
|
545
|
-
}
|
|
546
785
|
};
|
|
547
|
-
const enqueue = (fn) => {
|
|
786
|
+
const enqueue = (fn, aborts = currentAborts) => {
|
|
787
|
+
const enqueueAborts = aborts;
|
|
548
788
|
const queued = emissionQueue.add(fn);
|
|
549
789
|
activeEmissionCalls.add(queued);
|
|
550
790
|
void queued.then(() => activeEmissionCalls.delete(queued), (error) => {
|
|
551
791
|
activeEmissionCalls.delete(queued);
|
|
552
|
-
|
|
792
|
+
if (!enqueueAborts?.isAbortReason(error)) {
|
|
793
|
+
collectFailure(emissionFailures, error);
|
|
794
|
+
}
|
|
553
795
|
});
|
|
554
796
|
return queued;
|
|
555
797
|
};
|
|
556
|
-
const flush = async () => {
|
|
798
|
+
const flush = async (_aborts = currentAborts) => {
|
|
557
799
|
while (true) {
|
|
558
800
|
const active = [...activeEmissionCalls];
|
|
559
801
|
if (active.length > 0)
|
|
@@ -569,9 +811,15 @@ export const createPlaybookRuntime = (options) => {
|
|
|
569
811
|
return;
|
|
570
812
|
const failures = emissionFailures;
|
|
571
813
|
emissionFailures = [];
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
814
|
+
const failure = failures.length === 1
|
|
815
|
+
? failures[0]
|
|
816
|
+
: new AggregateError(failures, 'decide runtime emissions failed');
|
|
817
|
+
// Enqueue ownership already classified every stored failure as distinct.
|
|
818
|
+
// Preserve that classification if an unrelated public boundary drains
|
|
819
|
+
// it with a signal whose reason happens to be the same object.
|
|
820
|
+
if (currentSignal !== undefined)
|
|
821
|
+
controlPlaneError ??= failure;
|
|
822
|
+
throw failure;
|
|
575
823
|
};
|
|
576
824
|
const drainBoundaryCallsAndEmissions = async () => {
|
|
577
825
|
while (true) {
|
|
@@ -602,6 +850,9 @@ export const createPlaybookRuntime = (options) => {
|
|
|
602
850
|
};
|
|
603
851
|
const bindSession = (nextSession) => {
|
|
604
852
|
const bound = snapshotPlaybookSession(nextSession);
|
|
853
|
+
if (bound.playbookId !== deferredEffects.authority.playbookId) {
|
|
854
|
+
throw new TypeError('DECIDE runtime playbook identity must match its bound schema-3 host authority');
|
|
855
|
+
}
|
|
605
856
|
if (bound.roleBindings === undefined)
|
|
606
857
|
return bound;
|
|
607
858
|
const actual = Object.keys(bound.roleBindings).sort();
|
|
@@ -734,15 +985,216 @@ export const createPlaybookRuntime = (options) => {
|
|
|
734
985
|
pendingCall,
|
|
735
986
|
});
|
|
736
987
|
};
|
|
988
|
+
const visiblePendingQuestionsForState = (state, context) => pendingQuestionsForState(state, context).filter(({ questionId }) => questionId !== 'commitCoderProposal' ||
|
|
989
|
+
hiddenDeferredOperationId === undefined);
|
|
990
|
+
const openDeferredOperation = (ledger = effectLedgerMirror) => {
|
|
991
|
+
if (sessionIdentity === undefined)
|
|
992
|
+
return undefined;
|
|
993
|
+
const open = ledger.logicalOperations.filter((operation) => operation.playbookId === sessionIdentity.playbookId &&
|
|
994
|
+
operation.runtimeSessionId === sessionIdentity.sessionId &&
|
|
995
|
+
operation.logicalReceipt === undefined);
|
|
996
|
+
if (open.length > 1) {
|
|
997
|
+
throw new TypeError('DECIDE runtime has multiple open deferred logical operations');
|
|
998
|
+
}
|
|
999
|
+
return open[0];
|
|
1000
|
+
};
|
|
1001
|
+
const runtimeBoundaryIsOwned = (boundary) => sessionIdentity !== undefined &&
|
|
1002
|
+
boundary.playbookId === sessionIdentity.playbookId &&
|
|
1003
|
+
boundary.runtimeSessionId === sessionIdentity.sessionId;
|
|
1004
|
+
const governedOutcomesForBoundary = (boundary) => {
|
|
1005
|
+
const outcomes = DECIDE_OUTCOME_AUTHORITY.governedPlayerStates[boundary.sourceStateId];
|
|
1006
|
+
if (outcomes === undefined ||
|
|
1007
|
+
!isPlainObject(boundary.sourceOutcomeSchema)) {
|
|
1008
|
+
return undefined;
|
|
1009
|
+
}
|
|
1010
|
+
const authoredGuards = Object.keys(boundary.sourceOutcomeSchema).sort();
|
|
1011
|
+
const governedGuards = Object.keys(outcomes).sort();
|
|
1012
|
+
if (authoredGuards.length !== governedGuards.length ||
|
|
1013
|
+
authoredGuards.some((guard, index) => guard !== governedGuards[index])) {
|
|
1014
|
+
return undefined;
|
|
1015
|
+
}
|
|
1016
|
+
for (const guard of governedGuards) {
|
|
1017
|
+
const description = boundary.sourceOutcomeSchema[guard];
|
|
1018
|
+
if (typeof description !== 'string')
|
|
1019
|
+
return undefined;
|
|
1020
|
+
const authoredFields = [
|
|
1021
|
+
...new Set(requiredFieldsFor(description)),
|
|
1022
|
+
].sort();
|
|
1023
|
+
const governedFields = Object.keys(outcomes[guard].fields).sort();
|
|
1024
|
+
if (authoredFields.length !== governedFields.length ||
|
|
1025
|
+
authoredFields.some((field, index) => field !== governedFields[index])) {
|
|
1026
|
+
return undefined;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const dispositions = [
|
|
1030
|
+
...new Set(Object.values(outcomes).map(({ repositoryDisposition }) => repositoryDisposition)),
|
|
1031
|
+
];
|
|
1032
|
+
const actualDispositions = new Set(boundary.dispositions);
|
|
1033
|
+
return actualDispositions.size === boundary.dispositions.length &&
|
|
1034
|
+
actualDispositions.size === dispositions.length &&
|
|
1035
|
+
dispositions.every((disposition) => actualDispositions.has(disposition))
|
|
1036
|
+
? outcomes
|
|
1037
|
+
: undefined;
|
|
1038
|
+
};
|
|
1039
|
+
const persistedBoundaryReconciliation = (boundary, ledger) => {
|
|
1040
|
+
const outcomes = governedOutcomesForBoundary(boundary);
|
|
1041
|
+
if (outcomes === undefined || boundary.semanticCandidate === undefined) {
|
|
1042
|
+
return undefined;
|
|
1043
|
+
}
|
|
1044
|
+
let receipt = boundary.physicalReceipt;
|
|
1045
|
+
let awaitingLogicalReceipt = false;
|
|
1046
|
+
let historicalDeferred = false;
|
|
1047
|
+
const logicalOperation = boundary.logicalOperationId === undefined
|
|
1048
|
+
? undefined
|
|
1049
|
+
: ledger.logicalOperations.find(({ operationId }) => operationId === boundary.logicalOperationId);
|
|
1050
|
+
if (boundary.logicalOperationId !== undefined &&
|
|
1051
|
+
logicalOperation === undefined) {
|
|
1052
|
+
return undefined;
|
|
1053
|
+
}
|
|
1054
|
+
if (logicalOperation !== undefined) {
|
|
1055
|
+
if (logicalOperation.boundaryIds.at(-1) !== boundary.boundaryId) {
|
|
1056
|
+
historicalDeferred = true;
|
|
1057
|
+
}
|
|
1058
|
+
else if (logicalOperation.logicalReceipt !== undefined) {
|
|
1059
|
+
receipt = logicalOperation.logicalReceipt;
|
|
1060
|
+
}
|
|
1061
|
+
else if (logicalOperation.pendingQuestion === undefined ||
|
|
1062
|
+
logicalOperation.checkpoint === undefined ||
|
|
1063
|
+
!Object.prototype.hasOwnProperty.call(logicalOperation, 'playerContinuation')) {
|
|
1064
|
+
return undefined;
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
awaitingLogicalReceipt = true;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
try {
|
|
1071
|
+
const reconciliation = reconcilePlaybookSemanticEvidence({
|
|
1072
|
+
outcomes,
|
|
1073
|
+
semanticCandidate: boundary.semanticCandidate,
|
|
1074
|
+
finalText: boundary.finalText,
|
|
1075
|
+
receipt,
|
|
1076
|
+
});
|
|
1077
|
+
if (awaitingLogicalReceipt && reconciliation.status !== 'deferred') {
|
|
1078
|
+
return undefined;
|
|
1079
|
+
}
|
|
1080
|
+
return { reconciliation, historicalDeferred };
|
|
1081
|
+
}
|
|
1082
|
+
catch {
|
|
1083
|
+
return undefined;
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
const boundaryNeedsSemanticReconciliation = (boundary, ledger) => {
|
|
1087
|
+
if (!runtimeBoundaryIsOwned(boundary))
|
|
1088
|
+
return false;
|
|
1089
|
+
if (governedOutcomesForBoundary(boundary) === undefined)
|
|
1090
|
+
return true;
|
|
1091
|
+
const persisted = persistedBoundaryReconciliation(boundary, ledger);
|
|
1092
|
+
if (persisted !== undefined) {
|
|
1093
|
+
if (persisted.reconciliation.status === 'unresolved')
|
|
1094
|
+
return true;
|
|
1095
|
+
if (persisted.historicalDeferred) {
|
|
1096
|
+
return persisted.reconciliation.status !== 'deferred';
|
|
1097
|
+
}
|
|
1098
|
+
if (persisted.reconciliation.status === 'deferred' &&
|
|
1099
|
+
boundary.logicalOperationId === undefined) {
|
|
1100
|
+
return true;
|
|
1101
|
+
}
|
|
1102
|
+
return false;
|
|
1103
|
+
}
|
|
1104
|
+
if (boundary.physicalReceipt === undefined)
|
|
1105
|
+
return true;
|
|
1106
|
+
if (typeof boundary.finalText === 'string' &&
|
|
1107
|
+
boundary.finalText.trim().length > 0) {
|
|
1108
|
+
return true;
|
|
1109
|
+
}
|
|
1110
|
+
return boundary.physicalReceipt.classification !== 'unchanged';
|
|
1111
|
+
};
|
|
1112
|
+
const refreshUnresolvedSemanticReconciliation = (ledger = effectLedgerMirror) => {
|
|
1113
|
+
unresolvedSemanticBoundaryIds.clear();
|
|
1114
|
+
for (const boundary of ledger.boundaries) {
|
|
1115
|
+
if (boundaryNeedsSemanticReconciliation(boundary, ledger)) {
|
|
1116
|
+
unresolvedSemanticBoundaryIds.add(boundary.boundaryId);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
const synchronizeDeferredProjection = (ledger = effectLedgerMirror) => {
|
|
1121
|
+
if (deferredEffects === undefined)
|
|
1122
|
+
return;
|
|
1123
|
+
effectLedgerMirror = ledger;
|
|
1124
|
+
refreshUnresolvedSemanticReconciliation(ledger);
|
|
1125
|
+
const operation = openDeferredOperation(ledger);
|
|
1126
|
+
if (operation === undefined) {
|
|
1127
|
+
deferredOperationId = undefined;
|
|
1128
|
+
hiddenDeferredOperationId = undefined;
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
deferredOperationId = operation.operationId;
|
|
1132
|
+
hiddenDeferredOperationId =
|
|
1133
|
+
operation.logicalReceipt === undefined &&
|
|
1134
|
+
(operation.checkpointRestorationEligible ||
|
|
1135
|
+
operation.pendingQuestion === undefined)
|
|
1136
|
+
? operation.operationId
|
|
1137
|
+
: undefined;
|
|
1138
|
+
};
|
|
1139
|
+
const hasUnresolvedReconciliation = () => hiddenDeferredOperationId !== undefined ||
|
|
1140
|
+
unresolvedSemanticBoundaryIds.size > 0;
|
|
1141
|
+
const refreshReconciliationProjection = () => {
|
|
1142
|
+
synchronizeDeferredProjection(readEffectLedger());
|
|
1143
|
+
};
|
|
1144
|
+
const unresolvedEffectEnvelopeIdentities = () => {
|
|
1145
|
+
if (sessionIdentity === undefined)
|
|
1146
|
+
return [];
|
|
1147
|
+
refreshReconciliationProjection();
|
|
1148
|
+
if (!hasUnresolvedReconciliation())
|
|
1149
|
+
return [];
|
|
1150
|
+
const boundaryIds = new Set(unresolvedSemanticBoundaryIds);
|
|
1151
|
+
const operationIds = new Set();
|
|
1152
|
+
if (hiddenDeferredOperationId !== undefined) {
|
|
1153
|
+
operationIds.add(hiddenDeferredOperationId);
|
|
1154
|
+
}
|
|
1155
|
+
for (const boundaryId of [...boundaryIds]) {
|
|
1156
|
+
const boundary = effectLedgerMirror.boundaries.find((candidate) => candidate.boundaryId === boundaryId);
|
|
1157
|
+
const operation = boundary?.logicalOperationId === undefined
|
|
1158
|
+
? undefined
|
|
1159
|
+
: effectLedgerMirror.logicalOperations.find(({ operationId }) => operationId === boundary.logicalOperationId);
|
|
1160
|
+
if (operation === undefined)
|
|
1161
|
+
continue;
|
|
1162
|
+
operationIds.add(operation.operationId);
|
|
1163
|
+
for (const memberId of operation.boundaryIds) {
|
|
1164
|
+
boundaryIds.delete(memberId);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
const ordered = [
|
|
1168
|
+
...[...boundaryIds].map((boundaryId) => ({
|
|
1169
|
+
order: effectLedgerMirror.boundaries.find((candidate) => candidate.boundaryId === boundaryId)?.sequence ?? Number.MAX_SAFE_INTEGER,
|
|
1170
|
+
value: { kind: 'boundary', boundaryId },
|
|
1171
|
+
})),
|
|
1172
|
+
...[...operationIds].map((operationId) => {
|
|
1173
|
+
const operation = effectLedgerMirror.logicalOperations.find((candidate) => candidate.operationId === operationId);
|
|
1174
|
+
return {
|
|
1175
|
+
order: effectLedgerMirror.boundaries.find(({ boundaryId }) => boundaryId === operation?.boundaryIds[0])?.sequence ?? Number.MAX_SAFE_INTEGER,
|
|
1176
|
+
value: { kind: 'logical-operation', operationId },
|
|
1177
|
+
};
|
|
1178
|
+
}),
|
|
1179
|
+
].sort((left, right) => left.order - right.order);
|
|
1180
|
+
return deepFreeze(snapshotJsonValue(ordered.map(({ value }) => value), 'DECIDE unresolved effect envelope identities'));
|
|
1181
|
+
};
|
|
737
1182
|
const stateIdentity = (state) => {
|
|
738
1183
|
return state.stateId === undefined ? {} : { stateId: state.stateId };
|
|
739
1184
|
};
|
|
740
|
-
const enqueueTracedEmission = (type, payload, meta = {}, describedEmission) => {
|
|
741
|
-
const
|
|
1185
|
+
const enqueueTracedEmission = (type, payload, meta = {}, describedEmission, aborts) => {
|
|
1186
|
+
const trace = createTraceEvent(type, payload, meta);
|
|
1187
|
+
return enqueue(async () => {
|
|
1188
|
+
const runtimePorts = requirePorts();
|
|
1189
|
+
await runtimePorts.emitTelemetry({ topic: TRACE_TOPIC, payload: trace });
|
|
1190
|
+
await describedEmission?.(runtimePorts);
|
|
1191
|
+
}, aborts);
|
|
1192
|
+
};
|
|
1193
|
+
const createTraceEvent = (type, payload, meta = {}) => {
|
|
742
1194
|
const identity = requireSessionIdentity();
|
|
743
1195
|
const jsonPayload = snapshotJsonValue(payload, `trace ${type} payload`);
|
|
744
|
-
|
|
745
|
-
schemaVersion:
|
|
1196
|
+
return Object.freeze({
|
|
1197
|
+
schemaVersion: 4,
|
|
746
1198
|
sessionId: identity.sessionId,
|
|
747
1199
|
playbookId: identity.playbookId,
|
|
748
1200
|
rootSessionId: identity.rootSessionId,
|
|
@@ -760,12 +1212,25 @@ export const createPlaybookRuntime = (options) => {
|
|
|
760
1212
|
...(meta.callId !== undefined ? { callId: meta.callId } : {}),
|
|
761
1213
|
payload: jsonPayload,
|
|
762
1214
|
});
|
|
1215
|
+
};
|
|
1216
|
+
const emitTrace = (type, payload, meta = {}, aborts) => enqueueTracedEmission(type, payload, meta, undefined, aborts);
|
|
1217
|
+
const enqueueAcceptedOutcomeEmission = (acceptedOutcome, state, aborts) => {
|
|
1218
|
+
const message = `→ ${acceptedOutcome.acceptedOutcome}`;
|
|
1219
|
+
const acceptedTrace = createTraceEvent('outcome.accepted', acceptedOutcome, { turnId: currentTurnId });
|
|
1220
|
+
const statusTrace = createTraceEvent('status.emitted', { stateId: acceptedOutcome.target, message, state }, { turnId: currentTurnId });
|
|
763
1221
|
return enqueue(async () => {
|
|
764
|
-
|
|
765
|
-
await
|
|
766
|
-
|
|
1222
|
+
const runtimePorts = requirePorts();
|
|
1223
|
+
await runtimePorts.emitTelemetry({
|
|
1224
|
+
topic: TRACE_TOPIC,
|
|
1225
|
+
payload: acceptedTrace,
|
|
1226
|
+
});
|
|
1227
|
+
await runtimePorts.emitTelemetry({
|
|
1228
|
+
topic: TRACE_TOPIC,
|
|
1229
|
+
payload: statusTrace,
|
|
1230
|
+
});
|
|
1231
|
+
await runtimePorts.emitStatus(message);
|
|
1232
|
+
}, aborts);
|
|
767
1233
|
};
|
|
768
|
-
const emitTrace = (type, payload, meta = {}) => enqueueTracedEmission(type, payload, meta);
|
|
769
1234
|
const emitBoundaryStatus = async (message, state) => {
|
|
770
1235
|
const bossRelevantStateIds = state.activeStateIds.filter((stateId) => STATUS_STATE_IDS.has(stateId));
|
|
771
1236
|
await enqueueTracedEmission('status.emitted', {
|
|
@@ -777,20 +1242,24 @@ export const createPlaybookRuntime = (options) => {
|
|
|
777
1242
|
}, { turnId: currentTurnId }, (runtimePorts) => runtimePorts.emitStatus(message));
|
|
778
1243
|
};
|
|
779
1244
|
const emitCallStarted = async (startedType, finishedType, identity, meta, signal) => {
|
|
1245
|
+
const aborts = abortReasonClassifier(signal);
|
|
780
1246
|
try {
|
|
781
|
-
await emitTrace(startedType, identity, meta);
|
|
1247
|
+
await emitTrace(startedType, identity, meta, aborts);
|
|
782
1248
|
}
|
|
783
1249
|
catch (error) {
|
|
784
1250
|
latchControlPlaneError(error, signal);
|
|
785
1251
|
try {
|
|
786
1252
|
await emitTrace(finishedType, {
|
|
787
1253
|
...identity,
|
|
788
|
-
|
|
1254
|
+
// A started-trace sink rejection causally identical to the
|
|
1255
|
+
// boundary reason is the abort's own evidence: the pair
|
|
1256
|
+
// finishes 'aborted', not 'error' (DR-036 §4).
|
|
1257
|
+
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
789
1258
|
error: normalizeErrorFull(error) ?? {
|
|
790
1259
|
name: 'Error',
|
|
791
1260
|
message: String(error),
|
|
792
1261
|
},
|
|
793
|
-
}, meta);
|
|
1262
|
+
}, meta, aborts);
|
|
794
1263
|
}
|
|
795
1264
|
catch {
|
|
796
1265
|
// Preserve the start failure after one best-effort finish attempt.
|
|
@@ -799,6 +1268,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
799
1268
|
}
|
|
800
1269
|
};
|
|
801
1270
|
const runJudgeCall = async (prompt, signal, purpose, callStateId) => {
|
|
1271
|
+
const aborts = abortReasonClassifier(signal);
|
|
802
1272
|
const identity = {
|
|
803
1273
|
purpose,
|
|
804
1274
|
...(callStateId !== undefined ? { stateId: callStateId } : {}),
|
|
@@ -826,15 +1296,18 @@ export const createPlaybookRuntime = (options) => {
|
|
|
826
1296
|
latchControlPlaneError(error, signal);
|
|
827
1297
|
await emitTrace('judge.call.finished', {
|
|
828
1298
|
...identity,
|
|
829
|
-
|
|
1299
|
+
// Only the exact abort reason is cancellation; a distinct
|
|
1300
|
+
// failure under an aborted signal stays an error
|
|
1301
|
+
// (slc/link.md §Abort).
|
|
1302
|
+
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
830
1303
|
error: normalizeErrorFull(error) ?? {
|
|
831
1304
|
name: 'Error',
|
|
832
1305
|
message: String(error),
|
|
833
1306
|
},
|
|
834
|
-
}, { turnId: currentTurnId, callId });
|
|
1307
|
+
}, { turnId: currentTurnId, callId }, aborts);
|
|
835
1308
|
throw error;
|
|
836
1309
|
}
|
|
837
|
-
await emitTrace('judge.call.finished', { ...identity, status: 'ok', reply: finalText }, { turnId: currentTurnId, callId });
|
|
1310
|
+
await emitTrace('judge.call.finished', { ...identity, status: 'ok', reply: finalText }, { turnId: currentTurnId, callId }, aborts);
|
|
838
1311
|
return finalText;
|
|
839
1312
|
});
|
|
840
1313
|
if (queued === undefined) {
|
|
@@ -843,7 +1316,8 @@ export const createPlaybookRuntime = (options) => {
|
|
|
843
1316
|
return queued;
|
|
844
1317
|
};
|
|
845
1318
|
const callJudge = (prompt, signal, purpose, callStateId) => trackBoundaryCall(runJudgeCall(prompt, signal, purpose, callStateId));
|
|
846
|
-
const runPlayerCall = async (input, signal) => {
|
|
1319
|
+
const runPlayerCall = async (input, signal, continuation) => {
|
|
1320
|
+
const aborts = abortReasonClassifier(signal);
|
|
847
1321
|
if (!ROLE_ID_SET.has(input.role)) {
|
|
848
1322
|
throw new TypeError(`DECIDE player input role must name a declared local role`);
|
|
849
1323
|
}
|
|
@@ -854,13 +1328,17 @@ export const createPlaybookRuntime = (options) => {
|
|
|
854
1328
|
let resume;
|
|
855
1329
|
try {
|
|
856
1330
|
signal.throwIfAborted();
|
|
857
|
-
resume =
|
|
1331
|
+
resume =
|
|
1332
|
+
continuation !== undefined &&
|
|
1333
|
+
Object.prototype.hasOwnProperty.call(continuation, 'resume')
|
|
1334
|
+
? continuation.resume
|
|
1335
|
+
: selectPlayerResume(roleId, playerId);
|
|
858
1336
|
}
|
|
859
1337
|
catch (error) {
|
|
860
1338
|
latchControlPlaneError(error, signal);
|
|
861
1339
|
throw error;
|
|
862
1340
|
}
|
|
863
|
-
const callId = `player-${++playerCallSequence}`;
|
|
1341
|
+
const callId = continuation?.callId ?? `player-${++playerCallSequence}`;
|
|
864
1342
|
const identity = {
|
|
865
1343
|
stateId: input.stateId,
|
|
866
1344
|
sourceItem: input.sourceItem,
|
|
@@ -870,12 +1348,15 @@ export const createPlaybookRuntime = (options) => {
|
|
|
870
1348
|
};
|
|
871
1349
|
const emitFailure = (error) => emitTrace('player.call.finished', {
|
|
872
1350
|
...identity,
|
|
873
|
-
|
|
1351
|
+
// Only the exact abort reason is cancellation; a distinct
|
|
1352
|
+
// failure under an aborted signal stays an error
|
|
1353
|
+
// (slc/link.md §Abort).
|
|
1354
|
+
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
874
1355
|
error: normalizeErrorFull(error) ?? {
|
|
875
1356
|
name: 'Error',
|
|
876
1357
|
message: String(error),
|
|
877
1358
|
},
|
|
878
|
-
}, { turnId: currentTurnId, callId });
|
|
1359
|
+
}, { turnId: currentTurnId, callId }, aborts);
|
|
879
1360
|
if (inFlightPlayerKeys.has(playerKey)) {
|
|
880
1361
|
const error = new Error(`resolved player key "${playerKey}" already has an in-flight call`);
|
|
881
1362
|
await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt }, { turnId: currentTurnId, callId }, signal);
|
|
@@ -952,10 +1433,11 @@ export const createPlaybookRuntime = (options) => {
|
|
|
952
1433
|
...(result.error !== undefined
|
|
953
1434
|
? { error: normalizeErrorFull(result.error) }
|
|
954
1435
|
: {}),
|
|
955
|
-
}, { turnId: currentTurnId, callId });
|
|
1436
|
+
}, { turnId: currentTurnId, callId }, aborts);
|
|
956
1437
|
return {
|
|
957
1438
|
roleId,
|
|
958
1439
|
...(playerId === undefined ? {} : { playerId }),
|
|
1440
|
+
callId,
|
|
959
1441
|
result,
|
|
960
1442
|
};
|
|
961
1443
|
}
|
|
@@ -963,57 +1445,646 @@ export const createPlaybookRuntime = (options) => {
|
|
|
963
1445
|
inFlightPlayerKeys.delete(playerKey);
|
|
964
1446
|
}
|
|
965
1447
|
};
|
|
966
|
-
const
|
|
967
|
-
|
|
1448
|
+
const governedBoundarySeed = (input, callId) => {
|
|
1449
|
+
const outcomes = governedOutcomesFor(input);
|
|
1450
|
+
if (currentTurnId === undefined ||
|
|
1451
|
+
!Number.isSafeInteger(currentTurnId) ||
|
|
1452
|
+
currentTurnId <= 0) {
|
|
1453
|
+
throw new Error('DECIDE governed player call requires an active positive turn id');
|
|
1454
|
+
}
|
|
1455
|
+
return {
|
|
1456
|
+
boundaryId: randomUUID(),
|
|
1457
|
+
runtimeSessionId: requireSessionIdentity().sessionId,
|
|
1458
|
+
turnId: currentTurnId,
|
|
1459
|
+
callId,
|
|
1460
|
+
roleId: input.role,
|
|
1461
|
+
sourceStateId: input.stateId,
|
|
1462
|
+
sourceOutcomeSchema: snapshotJsonValue(input.result, 'DECIDE governed player source outcome schema'),
|
|
1463
|
+
dispositions: [
|
|
1464
|
+
...new Set(Object.values(outcomes).map(({ repositoryDisposition }) => repositoryDisposition)),
|
|
1465
|
+
],
|
|
1466
|
+
correctionBudget: { limit: 1, spent: false },
|
|
1467
|
+
};
|
|
968
1468
|
};
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1469
|
+
const unresolvedGovernedEvidence = (boundaryId, reason, error) => {
|
|
1470
|
+
governedFailuresByBoundaryId.set(boundaryId, error instanceof Error
|
|
1471
|
+
? error
|
|
1472
|
+
: new Error(`DECIDE governed outcome remains unresolved: ${reason}`));
|
|
1473
|
+
};
|
|
1474
|
+
const spendSemanticCorrectionBudget = async (boundary, receipt, finalText, semanticCandidate) => {
|
|
1475
|
+
const ledger = readEffectLedger();
|
|
1476
|
+
const current = ledger.boundaries.find(({ boundaryId }) => boundaryId === boundary.boundaryId);
|
|
1477
|
+
if (current === undefined ||
|
|
1478
|
+
current.correctionBudget.limit !== 1 ||
|
|
1479
|
+
current.correctionBudget.spent) {
|
|
1480
|
+
return false;
|
|
1481
|
+
}
|
|
1482
|
+
if (current.finalText !== undefined && current.finalText !== finalText) {
|
|
1483
|
+
throw new TypeError('DECIDE correction budget conflicts with retained finalText');
|
|
1484
|
+
}
|
|
1485
|
+
if (current.physicalReceipt !== undefined &&
|
|
1486
|
+
stableJson(current.physicalReceipt, 'DECIDE retained receipt') !==
|
|
1487
|
+
stableJson(receipt, 'DECIDE correction receipt')) {
|
|
1488
|
+
throw new TypeError('DECIDE correction budget conflicts with retained repository receipt');
|
|
1489
|
+
}
|
|
1490
|
+
if (semanticCandidate !== undefined &&
|
|
1491
|
+
current.semanticCandidate !== undefined &&
|
|
1492
|
+
stableJson(current.semanticCandidate, 'DECIDE retained candidate') !==
|
|
1493
|
+
stableJson(semanticCandidate, 'DECIDE correction candidate')) {
|
|
1494
|
+
throw new TypeError('DECIDE correction budget conflicts with retained semantic candidate');
|
|
1495
|
+
}
|
|
1496
|
+
const next = {
|
|
1497
|
+
...current,
|
|
1498
|
+
...(receipt.after === undefined ? {} : { after: receipt.after }),
|
|
1499
|
+
physicalReceipt: receipt,
|
|
1500
|
+
finalText,
|
|
1501
|
+
...(semanticCandidate === undefined ? {} : { semanticCandidate }),
|
|
1502
|
+
correctionBudget: { limit: 1, spent: true },
|
|
1503
|
+
};
|
|
1504
|
+
const cohortMembers = current.cohortId === undefined
|
|
1505
|
+
? [current]
|
|
1506
|
+
: ledger.boundaries.filter(({ cohortId }) => cohortId === current.cohortId);
|
|
1507
|
+
if (cohortMembers.length === 0) {
|
|
1508
|
+
throw new TypeError('DECIDE correction boundary lost its repository cohort');
|
|
1509
|
+
}
|
|
1510
|
+
const replacements = cohortMembers.map((member) => ({
|
|
1511
|
+
expected: member,
|
|
1512
|
+
next: member.boundaryId === current.boundaryId
|
|
1513
|
+
? next
|
|
1514
|
+
: {
|
|
1515
|
+
...member,
|
|
1516
|
+
...(receipt.after === undefined
|
|
1517
|
+
? {}
|
|
1518
|
+
: { after: receipt.after }),
|
|
1519
|
+
physicalReceipt: receipt,
|
|
1520
|
+
},
|
|
1521
|
+
}));
|
|
1522
|
+
const acknowledged = assertPlaybookEffectLedger(await deferredEffects.effectLedger.writeAhead([
|
|
1523
|
+
{
|
|
1524
|
+
kind: 'replace-boundaries',
|
|
1525
|
+
// A cohort's shared receipt must become visible for every member in
|
|
1526
|
+
// one ledger revision. Publishing only this member's correction
|
|
1527
|
+
// spend would transiently create an invalid half-complete cohort.
|
|
1528
|
+
replacements,
|
|
1529
|
+
},
|
|
1530
|
+
]), 'DECIDE semantic correction budget acknowledgement');
|
|
1531
|
+
synchronizeDeferredProjection(acknowledged);
|
|
1532
|
+
const spent = acknowledged.boundaries.find(({ boundaryId }) => boundaryId === boundary.boundaryId);
|
|
1533
|
+
if (spent === undefined ||
|
|
1534
|
+
stableJson(spent, 'DECIDE acknowledged correction boundary') !==
|
|
1535
|
+
stableJson(next, 'DECIDE expected correction boundary')) {
|
|
1536
|
+
throw new TypeError('DECIDE semantic correction budget spend was not acknowledged exactly');
|
|
1537
|
+
}
|
|
1538
|
+
return true;
|
|
1539
|
+
};
|
|
1540
|
+
const completionEvidenceFor = (input, roleId, playerId, signal, operationId) => async (completion) => {
|
|
1541
|
+
const queued = await semanticCompletionQueue.add(async () => {
|
|
1542
|
+
const { boundary, operation } = completion;
|
|
1543
|
+
const outcomes = governedOutcomesFor(input);
|
|
1544
|
+
const expectedDispositions = [
|
|
1545
|
+
...new Set(Object.values(outcomes).map(({ repositoryDisposition }) => repositoryDisposition)),
|
|
1546
|
+
];
|
|
1547
|
+
const session = requireSessionIdentity();
|
|
1548
|
+
if (boundary.playbookId !== session.playbookId ||
|
|
1549
|
+
boundary.runtimeSessionId !== session.sessionId ||
|
|
1550
|
+
boundary.turnId !== currentTurnId ||
|
|
1551
|
+
boundary.roleId !== roleId ||
|
|
1552
|
+
(completion.roleId !== undefined && completion.roleId !== roleId) ||
|
|
1553
|
+
boundary.sourceStateId !== input.stateId ||
|
|
1554
|
+
stableJson(boundary.dispositions, 'DECIDE boundary dispositions') !==
|
|
1555
|
+
stableJson(expectedDispositions, 'DECIDE authority dispositions') ||
|
|
1556
|
+
stableJson(boundary.sourceOutcomeSchema, 'DECIDE boundary source schema') !==
|
|
1557
|
+
stableJson(input.result, 'DECIDE authored source schema')) {
|
|
1558
|
+
throw new TypeError('DECIDE governed semantic reconciliation source schema changed');
|
|
1559
|
+
}
|
|
1560
|
+
governedReceiptsByBoundaryId.set(boundary.boundaryId, {
|
|
1561
|
+
physicalReceipt: completion.receipt,
|
|
1562
|
+
outcomeReceipt: completion.outcomeReceipt,
|
|
1563
|
+
});
|
|
1564
|
+
if (operation.status !== 'fulfilled' ||
|
|
1565
|
+
operation.value.status !== 'ok' ||
|
|
1566
|
+
isEmptyFinalText(operation.value.finalText)) {
|
|
1567
|
+
const incomplete = operation.status === 'fulfilled' &&
|
|
1568
|
+
operation.value.status === 'ok' &&
|
|
1569
|
+
operation.value.finalText !== undefined
|
|
1570
|
+
? { finalText: operation.value.finalText }
|
|
1571
|
+
: {};
|
|
1572
|
+
if (operation.status === 'fulfilled' &&
|
|
1573
|
+
operation.value.status === 'ok' &&
|
|
1574
|
+
operation.value.finalText !== undefined) {
|
|
1575
|
+
governedEvidenceByBoundaryId.set(boundary.boundaryId, {
|
|
1576
|
+
finalText: operation.value.finalText,
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
return operationId === undefined
|
|
1580
|
+
? incomplete
|
|
1581
|
+
: { ...incomplete, unresolved: true };
|
|
1582
|
+
}
|
|
1583
|
+
const finalText = operation.value.finalText;
|
|
1584
|
+
let raw;
|
|
1585
|
+
try {
|
|
1586
|
+
raw = await callJudge(buildAdjudicatorPrompt(input, finalText), signal, 'player-output-adjudication', input.stateId);
|
|
1587
|
+
}
|
|
1588
|
+
catch (error) {
|
|
1589
|
+
unresolvedGovernedEvidence(boundary.boundaryId, 'judge transport failed', error);
|
|
1590
|
+
governedEvidenceByBoundaryId.set(boundary.boundaryId, { finalText });
|
|
1591
|
+
return { finalText, unresolved: true };
|
|
1592
|
+
}
|
|
1593
|
+
let candidate;
|
|
1594
|
+
let retainedCandidate;
|
|
1595
|
+
let reconciliation;
|
|
1596
|
+
let structuralError;
|
|
1597
|
+
const retainCandidate = (value) => {
|
|
1598
|
+
try {
|
|
1599
|
+
retainedCandidate = snapshotJsonValue(value, 'DECIDE recoverable governed semantic candidate');
|
|
1600
|
+
}
|
|
1601
|
+
catch {
|
|
1602
|
+
// A non-detachable reply still retains presentation and receipt.
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
try {
|
|
1606
|
+
candidate = parseGovernedSemanticCandidate(raw);
|
|
1607
|
+
retainCandidate(candidate);
|
|
1608
|
+
reconciliation = reconcilePlaybookSemanticEvidence({
|
|
1609
|
+
outcomes,
|
|
1610
|
+
semanticCandidate: candidate,
|
|
1611
|
+
finalText,
|
|
1612
|
+
receipt: completion.outcomeReceipt,
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
catch (error) {
|
|
1616
|
+
if (!(error instanceof PlaybookSemanticCandidateStructureError)) {
|
|
1617
|
+
throw error;
|
|
1618
|
+
}
|
|
1619
|
+
structuralError = error;
|
|
1620
|
+
}
|
|
1621
|
+
if (structuralError !== undefined) {
|
|
1622
|
+
const spent = await spendSemanticCorrectionBudget(boundary, completion.receipt, finalText, retainedCandidate);
|
|
1623
|
+
if (!spent || signal.aborted) {
|
|
1624
|
+
unresolvedGovernedEvidence(boundary.boundaryId, 'semantic correction budget is unavailable', signal.aborted ? signal.reason : undefined);
|
|
1625
|
+
governedEvidenceByBoundaryId.set(boundary.boundaryId, {
|
|
1626
|
+
finalText,
|
|
1627
|
+
...(retainedCandidate === undefined
|
|
1628
|
+
? {}
|
|
1629
|
+
: { semanticCandidate: retainedCandidate }),
|
|
1630
|
+
});
|
|
1631
|
+
return {
|
|
1632
|
+
finalText,
|
|
1633
|
+
...(retainedCandidate === undefined
|
|
1634
|
+
? {}
|
|
1635
|
+
: { semanticCandidate: retainedCandidate }),
|
|
1636
|
+
unresolved: true,
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
let correctiveRaw;
|
|
1640
|
+
try {
|
|
1641
|
+
correctiveRaw = await callJudge(buildAdjudicatorPrompt(input, finalText, {
|
|
1642
|
+
reply: raw,
|
|
1643
|
+
error: structuralError.message,
|
|
1644
|
+
}), signal, 'player-output-adjudication', input.stateId);
|
|
1645
|
+
}
|
|
1646
|
+
catch (error) {
|
|
1647
|
+
unresolvedGovernedEvidence(boundary.boundaryId, 'corrective judge failed', error);
|
|
1648
|
+
governedEvidenceByBoundaryId.set(boundary.boundaryId, {
|
|
1649
|
+
finalText,
|
|
1650
|
+
...(retainedCandidate === undefined
|
|
1651
|
+
? {}
|
|
1652
|
+
: { semanticCandidate: retainedCandidate }),
|
|
1653
|
+
});
|
|
1654
|
+
return {
|
|
1655
|
+
finalText,
|
|
1656
|
+
...(retainedCandidate === undefined
|
|
1657
|
+
? {}
|
|
1658
|
+
: { semanticCandidate: retainedCandidate }),
|
|
1659
|
+
unresolved: true,
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
try {
|
|
1663
|
+
candidate = parseGovernedSemanticCandidate(correctiveRaw);
|
|
1664
|
+
retainCandidate(candidate);
|
|
1665
|
+
reconciliation = reconcilePlaybookSemanticEvidence({
|
|
1666
|
+
outcomes,
|
|
1667
|
+
semanticCandidate: candidate,
|
|
1668
|
+
finalText,
|
|
1669
|
+
receipt: completion.outcomeReceipt,
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
catch (error) {
|
|
1673
|
+
if (!(error instanceof PlaybookSemanticCandidateStructureError)) {
|
|
1674
|
+
throw error;
|
|
1675
|
+
}
|
|
1676
|
+
unresolvedGovernedEvidence(boundary.boundaryId, 'corrective semantic candidate is invalid');
|
|
1677
|
+
governedEvidenceByBoundaryId.set(boundary.boundaryId, {
|
|
1678
|
+
finalText,
|
|
1679
|
+
...(retainedCandidate === undefined
|
|
1680
|
+
? {}
|
|
1681
|
+
: { semanticCandidate: retainedCandidate }),
|
|
1682
|
+
});
|
|
1683
|
+
return {
|
|
1684
|
+
finalText,
|
|
1685
|
+
...(retainedCandidate === undefined
|
|
1686
|
+
? {}
|
|
1687
|
+
: { semanticCandidate: retainedCandidate }),
|
|
1688
|
+
unresolved: true,
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
if (reconciliation === undefined) {
|
|
1693
|
+
throw new Error('DECIDE semantic reconciliation produced no decision');
|
|
1694
|
+
}
|
|
1695
|
+
const semanticCandidate = snapshotJsonValue(reconciliation.evidence.semanticCandidate, 'DECIDE governed semantic candidate');
|
|
1696
|
+
governedEvidenceByBoundaryId.set(boundary.boundaryId, {
|
|
1697
|
+
finalText,
|
|
1698
|
+
semanticCandidate,
|
|
1699
|
+
});
|
|
1700
|
+
if (reconciliation.status === 'unresolved') {
|
|
1701
|
+
unresolvedGovernedEvidence(boundary.boundaryId, reconciliation.reason);
|
|
1702
|
+
return { finalText, semanticCandidate, unresolved: true };
|
|
1703
|
+
}
|
|
1704
|
+
const output = reconciliation.output;
|
|
1705
|
+
governedOutputsByBoundaryId.set(boundary.boundaryId, output);
|
|
1706
|
+
if (reconciliation.status !== 'deferred') {
|
|
1707
|
+
return { finalText, semanticCandidate };
|
|
1708
|
+
}
|
|
1709
|
+
if (output.guard !== 'needsBossReply' ||
|
|
1710
|
+
typeof output.question !== 'string' ||
|
|
1711
|
+
output.question.trim() === '') {
|
|
1712
|
+
throw new TypeError('DECIDE deferred outcome must carry one exact Boss question');
|
|
1713
|
+
}
|
|
1714
|
+
const pendingQuestion = {
|
|
1715
|
+
questionId: input.stateId,
|
|
1716
|
+
asker: { kind: 'role', roleId },
|
|
1717
|
+
question: output.question,
|
|
1718
|
+
sourceItem: input.sourceItem,
|
|
1719
|
+
};
|
|
1720
|
+
return {
|
|
1721
|
+
finalText,
|
|
1722
|
+
semanticCandidate,
|
|
1723
|
+
deferred: {
|
|
1724
|
+
operationId: operationId ?? randomUUID(),
|
|
1725
|
+
pendingQuestion,
|
|
1726
|
+
playerContinuation: snapshotJsonValue(selectPlayerResume(roleId, playerId), 'DECIDE deferred player continuation'),
|
|
1727
|
+
},
|
|
1728
|
+
};
|
|
1729
|
+
});
|
|
1730
|
+
if (queued === undefined) {
|
|
1731
|
+
throw new Error('DECIDE semantic completion produced no evidence');
|
|
1732
|
+
}
|
|
1733
|
+
return queued;
|
|
1734
|
+
};
|
|
1735
|
+
const acknowledgeGovernedPlayerResult = (value, boundaryId) => {
|
|
1736
|
+
const ledger = assertPlaybookEffectLedger(value.effectLedger, 'DECIDE repository settlement effect ledger');
|
|
1737
|
+
const completed = ledger.boundaries.find((candidate) => candidate.boundaryId === boundaryId);
|
|
1738
|
+
if (completed === undefined ||
|
|
1739
|
+
completed.physicalReceipt === undefined ||
|
|
1740
|
+
stableJson(completed.physicalReceipt, 'DECIDE completed receipt') !==
|
|
1741
|
+
stableJson(value.receipt, 'DECIDE acknowledged receipt')) {
|
|
1742
|
+
throw new TypeError('DECIDE repository settlement did not acknowledge its completed boundary');
|
|
1743
|
+
}
|
|
1744
|
+
const expectedEvidence = governedEvidenceByBoundaryId.get(boundaryId);
|
|
1745
|
+
const expectedReceipts = governedReceiptsByBoundaryId.get(boundaryId);
|
|
1746
|
+
if (expectedReceipts !== undefined &&
|
|
1747
|
+
stableJson(completed.physicalReceipt, 'DECIDE completed physical receipt') !==
|
|
1748
|
+
stableJson(expectedReceipts.physicalReceipt, 'DECIDE reconciled physical receipt')) {
|
|
1749
|
+
throw new TypeError('DECIDE repository settlement changed the physical receipt used during reconciliation');
|
|
1750
|
+
}
|
|
1751
|
+
if (expectedEvidence !== undefined &&
|
|
1752
|
+
(completed.finalText !== expectedEvidence.finalText ||
|
|
1753
|
+
(expectedEvidence.semanticCandidate === undefined
|
|
1754
|
+
? completed.semanticCandidate !== undefined
|
|
1755
|
+
: completed.semanticCandidate === undefined ||
|
|
1756
|
+
stableJson(completed.semanticCandidate, 'DECIDE completed semantic candidate') !==
|
|
1757
|
+
stableJson(expectedEvidence.semanticCandidate, 'DECIDE expected semantic candidate')))) {
|
|
1758
|
+
throw new TypeError('DECIDE repository settlement did not acknowledge its exact governed evidence');
|
|
1759
|
+
}
|
|
1760
|
+
governedEvidenceByBoundaryId.delete(boundaryId);
|
|
1761
|
+
governedReceiptsByBoundaryId.delete(boundaryId);
|
|
1762
|
+
synchronizeDeferredProjection(ledger);
|
|
1763
|
+
if (value.operation.status === 'rejected') {
|
|
1764
|
+
throw value.operation.reason;
|
|
1765
|
+
}
|
|
1766
|
+
const result = validatePlayerResult(value.operation.value);
|
|
1767
|
+
const output = governedOutputsByBoundaryId.get(boundaryId);
|
|
1768
|
+
const failure = governedFailuresByBoundaryId.get(boundaryId);
|
|
1769
|
+
if (expectedReceipts !== undefined) {
|
|
1770
|
+
const continued = 'status' in value && value.status === 'continued'
|
|
1771
|
+
? value
|
|
1772
|
+
: undefined;
|
|
1773
|
+
const acknowledgedOutcomeReceipt = continued === undefined ? value.receipt : continued.logicalReceipt;
|
|
1774
|
+
if (acknowledgedOutcomeReceipt !== undefined &&
|
|
1775
|
+
stableJson(acknowledgedOutcomeReceipt, 'DECIDE acknowledged outcome receipt') !==
|
|
1776
|
+
stableJson(expectedReceipts.outcomeReceipt, 'DECIDE reconciled outcome receipt')) {
|
|
1777
|
+
throw new TypeError('DECIDE repository settlement changed the outcome receipt used during reconciliation');
|
|
1778
|
+
}
|
|
1779
|
+
if (continued !== undefined &&
|
|
1780
|
+
output !== undefined &&
|
|
1781
|
+
output.guard !== 'needsBossReply' &&
|
|
1782
|
+
acknowledgedOutcomeReceipt === undefined) {
|
|
1783
|
+
throw new TypeError('DECIDE completed deferred continuation omitted its reconciled logical receipt');
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
governedOutputsByBoundaryId.delete(boundaryId);
|
|
1787
|
+
governedFailuresByBoundaryId.delete(boundaryId);
|
|
1788
|
+
const linkedOperationId = completed.logicalOperationId;
|
|
1789
|
+
if (value.deferredStatus === 'unresolved') {
|
|
1790
|
+
if (linkedOperationId === undefined) {
|
|
1791
|
+
throw new TypeError('DECIDE unresolved deferred settlement omitted its logical operation');
|
|
1792
|
+
}
|
|
1793
|
+
deferredOperationId = linkedOperationId;
|
|
1794
|
+
hiddenDeferredOperationId = linkedOperationId;
|
|
1795
|
+
}
|
|
1796
|
+
else if (value.deferredStatus === 'bound') {
|
|
1797
|
+
if (linkedOperationId === undefined) {
|
|
1798
|
+
throw new TypeError('DECIDE bound deferred settlement omitted its logical operation');
|
|
1799
|
+
}
|
|
1800
|
+
deferredOperationId = linkedOperationId;
|
|
1801
|
+
hiddenDeferredOperationId = undefined;
|
|
1802
|
+
}
|
|
1803
|
+
else if ('status' in value &&
|
|
1804
|
+
value.status === 'continued' &&
|
|
1805
|
+
value
|
|
1806
|
+
.logicalReceipt === undefined &&
|
|
1807
|
+
linkedOperationId !== undefined) {
|
|
1808
|
+
deferredOperationId = linkedOperationId;
|
|
1809
|
+
hiddenDeferredOperationId = linkedOperationId;
|
|
1810
|
+
}
|
|
1811
|
+
else if (linkedOperationId !== undefined) {
|
|
1812
|
+
deferredOperationId = undefined;
|
|
1813
|
+
hiddenDeferredOperationId = undefined;
|
|
1814
|
+
}
|
|
1815
|
+
if (failure !== undefined)
|
|
1816
|
+
throw failure;
|
|
1817
|
+
if (value.deferredStatus === 'unresolved') {
|
|
1818
|
+
throw new Error('DECIDE deferred repository settlement remains unresolved');
|
|
1819
|
+
}
|
|
1820
|
+
if (output !== undefined) {
|
|
1821
|
+
governedPlayerOutputs.set(result, output);
|
|
1822
|
+
}
|
|
1823
|
+
else if (result.status === 'ok' &&
|
|
1824
|
+
!isEmptyFinalText(result.finalText)) {
|
|
1825
|
+
throw new Error('DECIDE governed player result has no reconciled semantic output');
|
|
1826
|
+
}
|
|
1827
|
+
return result;
|
|
1828
|
+
};
|
|
1829
|
+
const shouldRunProposalCohort = (input) => {
|
|
1830
|
+
if (input.stateId !== PROPOSAL_STATE_BY_ROLE[input.role] ||
|
|
1831
|
+
completedProposalCohortTurnId === currentTurnId) {
|
|
1832
|
+
return false;
|
|
1833
|
+
}
|
|
1834
|
+
const state = currentState();
|
|
1835
|
+
return Object.values(PROPOSAL_STATE_BY_ROLE).every((stateId) => state.activeStateIds.includes(stateId));
|
|
1836
|
+
};
|
|
1837
|
+
const settleProposalCohort = async () => {
|
|
1838
|
+
const members = Object.fromEntries(ROLE_IDS.map((roleId) => [roleId, pendingProposalCohort.get(roleId)]));
|
|
1839
|
+
const turnId = currentTurnId;
|
|
1840
|
+
const cohortSignal = currentSignal;
|
|
1841
|
+
const cohortOperations = new AbortController();
|
|
1842
|
+
const completionOrder = [];
|
|
974
1843
|
try {
|
|
975
|
-
|
|
1844
|
+
if (turnId === undefined ||
|
|
1845
|
+
cohortSignal === undefined ||
|
|
1846
|
+
members.coder === undefined ||
|
|
1847
|
+
members.reviewer === undefined) {
|
|
1848
|
+
throw new Error('DECIDE proposal cohort started without both governed members');
|
|
1849
|
+
}
|
|
1850
|
+
for (const roleId of ROLE_IDS)
|
|
1851
|
+
members[roleId].signal.throwIfAborted();
|
|
1852
|
+
const operations = Object.fromEntries(ROLE_IDS.map((roleId) => [
|
|
1853
|
+
roleId,
|
|
1854
|
+
async () => {
|
|
1855
|
+
const member = members[roleId];
|
|
1856
|
+
try {
|
|
1857
|
+
const operationSignal = combineSignals(member.signal, cohortOperations.signal);
|
|
1858
|
+
const envelope = await runPlayerCall(member.input, operationSignal, { callId: member.callId });
|
|
1859
|
+
member.envelope = envelope;
|
|
1860
|
+
if (envelope.result.status !== 'ok' &&
|
|
1861
|
+
!cohortOperations.signal.aborted) {
|
|
1862
|
+
cohortOperations.abort(new Error(`DECIDE proposal cohort ${roleId} returned status ${JSON.stringify(envelope.result.status)}`));
|
|
1863
|
+
}
|
|
1864
|
+
return envelope.result;
|
|
1865
|
+
}
|
|
1866
|
+
catch (error) {
|
|
1867
|
+
if (!cohortOperations.signal.aborted) {
|
|
1868
|
+
cohortOperations.abort(error);
|
|
1869
|
+
}
|
|
1870
|
+
throw error;
|
|
1871
|
+
}
|
|
1872
|
+
finally {
|
|
1873
|
+
completionOrder.push(roleId);
|
|
1874
|
+
}
|
|
1875
|
+
},
|
|
1876
|
+
]));
|
|
1877
|
+
const invocationId = randomUUID();
|
|
1878
|
+
const settled = await deferredEffects.repository.runCohort({
|
|
1879
|
+
signal: cohortSignal,
|
|
1880
|
+
invocationId,
|
|
1881
|
+
roleIds: ROLE_IDS,
|
|
1882
|
+
dispositionsByRole: {
|
|
1883
|
+
coder: ['unchanged'],
|
|
1884
|
+
reviewer: ['unchanged'],
|
|
1885
|
+
},
|
|
1886
|
+
effectBoundaries: {
|
|
1887
|
+
coder: members.coder.effectBoundary,
|
|
1888
|
+
reviewer: members.reviewer.effectBoundary,
|
|
1889
|
+
},
|
|
1890
|
+
operations,
|
|
1891
|
+
completeEffectBoundary: (completion) => {
|
|
1892
|
+
const member = members[completion.roleId];
|
|
1893
|
+
if (member === undefined) {
|
|
1894
|
+
throw new TypeError(`DECIDE proposal cohort completed unknown role ${String(completion.roleId)}`);
|
|
1895
|
+
}
|
|
1896
|
+
return completionEvidenceFor(member.input, completion.roleId, resolvedPlayerId(completion.roleId), member.signal)(completion);
|
|
1897
|
+
},
|
|
1898
|
+
});
|
|
1899
|
+
if (settled.invocationId !== invocationId) {
|
|
1900
|
+
throw new TypeError('DECIDE repository cohort changed its invocation identity');
|
|
1901
|
+
}
|
|
1902
|
+
const releaseOrder = [
|
|
1903
|
+
...completionOrder,
|
|
1904
|
+
...ROLE_IDS.filter((roleId) => !completionOrder.includes(roleId)),
|
|
1905
|
+
];
|
|
1906
|
+
const acknowledgedResults = new Map();
|
|
1907
|
+
const acknowledgementFailures = [];
|
|
1908
|
+
for (const roleId of releaseOrder) {
|
|
1909
|
+
const member = members[roleId];
|
|
1910
|
+
try {
|
|
1911
|
+
const result = acknowledgeGovernedPlayerResult({
|
|
1912
|
+
operation: settled.operations[roleId],
|
|
1913
|
+
receipt: settled.receipts[roleId],
|
|
1914
|
+
effectLedger: settled.effectLedger,
|
|
1915
|
+
}, member.effectBoundary.boundaryId);
|
|
1916
|
+
if (member.envelope === undefined) {
|
|
1917
|
+
throw new TypeError(`DECIDE repository cohort omitted its ${roleId} invocation`);
|
|
1918
|
+
}
|
|
1919
|
+
acknowledgedResults.set(roleId, result);
|
|
1920
|
+
}
|
|
1921
|
+
catch (error) {
|
|
1922
|
+
acknowledgementFailures.push(error);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
if (acknowledgementFailures.length > 0) {
|
|
1926
|
+
for (const result of acknowledgedResults.values()) {
|
|
1927
|
+
governedPlayerOutputs.delete(result);
|
|
1928
|
+
}
|
|
1929
|
+
throw acknowledgementFailures.length === 1
|
|
1930
|
+
? acknowledgementFailures[0]
|
|
1931
|
+
: new AggregateError(acknowledgementFailures, 'DECIDE proposal cohort reconciliation failed');
|
|
1932
|
+
}
|
|
1933
|
+
for (const roleId of releaseOrder) {
|
|
1934
|
+
const member = members[roleId];
|
|
1935
|
+
const result = acknowledgedResults.get(roleId);
|
|
1936
|
+
if (member.envelope === undefined || result === undefined) {
|
|
1937
|
+
throw new Error(`DECIDE proposal cohort lost its ${roleId} acknowledgement`);
|
|
1938
|
+
}
|
|
1939
|
+
member.result.resolve({ ...member.envelope, result });
|
|
1940
|
+
}
|
|
976
1941
|
}
|
|
977
1942
|
catch (error) {
|
|
978
|
-
|
|
979
|
-
|
|
1943
|
+
for (const member of Object.values(members))
|
|
1944
|
+
member?.result.reject(error);
|
|
980
1945
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
// whitespace-only earns exactly one corrective re-ask — the same
|
|
986
|
-
// composed call repeated, traced by runPlayerCall as its own
|
|
987
|
-
// player-call pair, with the resume selection re-read from the
|
|
988
|
-
// token map the first result left (PBRT-38). An abort that lands
|
|
989
|
-
// between the two calls ends the turn without the re-ask (aborts
|
|
990
|
-
// are never retried), and a rejecting finish emission rejects
|
|
991
|
-
// `callPlayer` itself, so it never reaches this branch (PBRT-47).
|
|
992
|
-
combined.throwIfAborted();
|
|
993
|
-
({ roleId, playerId, result } = await callPlayer(input, combined));
|
|
1946
|
+
finally {
|
|
1947
|
+
completedProposalCohortTurnId = turnId;
|
|
1948
|
+
pendingProposalCohort.clear();
|
|
1949
|
+
activeProposalCohort = undefined;
|
|
994
1950
|
}
|
|
995
|
-
|
|
996
|
-
|
|
1951
|
+
};
|
|
1952
|
+
const queueProposalCohortMember = (input, signal) => {
|
|
1953
|
+
if (pendingProposalCohort.has(input.role)) {
|
|
1954
|
+
throw new Error(`DECIDE proposal cohort already registered role ${input.role}`);
|
|
997
1955
|
}
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1956
|
+
const callId = `player-${++playerCallSequence}`;
|
|
1957
|
+
const member = {
|
|
1958
|
+
input,
|
|
1959
|
+
signal,
|
|
1960
|
+
callId,
|
|
1961
|
+
effectBoundary: governedBoundarySeed(input, callId),
|
|
1962
|
+
result: deferredValue(),
|
|
1963
|
+
};
|
|
1964
|
+
pendingProposalCohort.set(input.role, member);
|
|
1965
|
+
if (pendingProposalCohort.size === ROLE_IDS.length) {
|
|
1966
|
+
if (activeProposalCohort !== undefined) {
|
|
1967
|
+
throw new Error('DECIDE proposal cohort was started more than once');
|
|
1968
|
+
}
|
|
1969
|
+
activeProposalCohort = settleProposalCohort();
|
|
1001
1970
|
}
|
|
1002
|
-
|
|
1971
|
+
return member.result.promise;
|
|
1972
|
+
};
|
|
1973
|
+
const callPlayer = (input, signal) => {
|
|
1974
|
+
const invocation = async () => {
|
|
1975
|
+
const active = activeDeferredContinuation;
|
|
1976
|
+
if (active === undefined && hasUnresolvedReconciliation()) {
|
|
1977
|
+
throw new Error('DECIDE governed semantic reconciliation remains unresolved');
|
|
1978
|
+
}
|
|
1979
|
+
if (active !== undefined) {
|
|
1980
|
+
if (active.effectBoundary.runtimeSessionId !==
|
|
1981
|
+
requireSessionIdentity().sessionId ||
|
|
1982
|
+
active.effectBoundary.turnId !== currentTurnId ||
|
|
1983
|
+
active.effectBoundary.roleId !== input.role ||
|
|
1984
|
+
active.effectBoundary.sourceStateId !== input.stateId ||
|
|
1985
|
+
active.playerContinuation === undefined) {
|
|
1986
|
+
throw new TypeError('DECIDE deferred continuation did not invoke its bound player boundary');
|
|
1987
|
+
}
|
|
1988
|
+
active.input = input;
|
|
1989
|
+
active.playerId = resolvedPlayerId(input.role);
|
|
1990
|
+
try {
|
|
1991
|
+
const envelope = await runPlayerCall(input, signal, {
|
|
1992
|
+
callId: active.effectBoundary.callId,
|
|
1993
|
+
resume: active.playerContinuation,
|
|
1994
|
+
});
|
|
1995
|
+
active.result.resolve(envelope.result);
|
|
1996
|
+
const acknowledgedResult = await active.acknowledged.promise;
|
|
1997
|
+
return { ...envelope, result: acknowledgedResult };
|
|
1998
|
+
}
|
|
1999
|
+
catch (error) {
|
|
2000
|
+
active.result.reject(error);
|
|
2001
|
+
try {
|
|
2002
|
+
await active.acknowledged.promise;
|
|
2003
|
+
}
|
|
2004
|
+
catch (acknowledgementError) {
|
|
2005
|
+
throw acknowledgementError;
|
|
2006
|
+
}
|
|
2007
|
+
throw error;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
if (shouldRunProposalCohort(input)) {
|
|
2011
|
+
return queueProposalCohortMember(input, signal);
|
|
2012
|
+
}
|
|
2013
|
+
const callId = `player-${++playerCallSequence}`;
|
|
2014
|
+
const effectBoundary = governedBoundarySeed(input, callId);
|
|
2015
|
+
let envelope;
|
|
2016
|
+
const settled = await deferredEffects.repository.runExclusive({
|
|
2017
|
+
signal,
|
|
2018
|
+
effectBoundary,
|
|
2019
|
+
operation: async () => {
|
|
2020
|
+
envelope = await runPlayerCall(input, signal, { callId });
|
|
2021
|
+
return envelope.result;
|
|
2022
|
+
},
|
|
2023
|
+
completeEffectBoundary: completionEvidenceFor(input, input.role, resolvedPlayerId(input.role), signal),
|
|
2024
|
+
});
|
|
2025
|
+
const result = acknowledgeGovernedPlayerResult(settled, effectBoundary.boundaryId);
|
|
2026
|
+
if (envelope === undefined) {
|
|
2027
|
+
throw new TypeError('DECIDE repository settlement omitted its player invocation');
|
|
2028
|
+
}
|
|
2029
|
+
return { ...envelope, result };
|
|
2030
|
+
};
|
|
2031
|
+
return trackBoundaryCall(invocation());
|
|
2032
|
+
};
|
|
2033
|
+
const player = fromPromise(async ({ input, signal }) => {
|
|
2034
|
+
const combined = combineSignals(signal, currentSignal);
|
|
2035
|
+
const settlementAborts = abortReasonClassifier(combined);
|
|
1003
2036
|
try {
|
|
1004
|
-
|
|
1005
|
-
|
|
2037
|
+
// XState starts invoked actors while publishing the entering snapshot.
|
|
2038
|
+
// Yield through the runtime emission queue before crossing the player
|
|
2039
|
+
// boundary so state trace/status always precede its call-start trace.
|
|
2040
|
+
combined.throwIfAborted();
|
|
2041
|
+
try {
|
|
2042
|
+
await flush(settlementAborts);
|
|
2043
|
+
}
|
|
2044
|
+
catch (error) {
|
|
2045
|
+
latchControlPlaneError(error, combined);
|
|
2046
|
+
throw error;
|
|
2047
|
+
}
|
|
2048
|
+
combined.throwIfAborted();
|
|
2049
|
+
let { roleId, playerId, callId, result } = await callPlayer(input, combined);
|
|
2050
|
+
if (result.status === 'ok' &&
|
|
2051
|
+
isEmptyFinalText(result.finalText) &&
|
|
2052
|
+
automaticReplayPolicy.allowsEmptyOkCorrection(requireSessionIdentity().sessionId, callId)) {
|
|
2053
|
+
// DR-028: an `ok` result whose finalText is missing, empty, or
|
|
2054
|
+
// whitespace-only earns exactly one corrective re-ask — the same
|
|
2055
|
+
// composed call repeated, traced by runPlayerCall as its own
|
|
2056
|
+
// player-call pair, with the resume selection re-read from the
|
|
2057
|
+
// token map the first result left (PBRT-38). An abort that lands
|
|
2058
|
+
// between the two calls ends the turn without the re-ask (aborts
|
|
2059
|
+
// are never retried), and a rejecting finish emission rejects
|
|
2060
|
+
// `callPlayer` itself, so it never reaches this branch (PBRT-47).
|
|
2061
|
+
combined.throwIfAborted();
|
|
2062
|
+
({ roleId, playerId, callId, result } = await callPlayer(input, combined));
|
|
2063
|
+
}
|
|
2064
|
+
if (result.status !== 'ok') {
|
|
2065
|
+
throw new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "${result.status}"${result.error ? `: ${result.error}` : ''}`);
|
|
2066
|
+
}
|
|
2067
|
+
const finalText = result.finalText ?? '';
|
|
2068
|
+
if (isEmptyFinalText(finalText)) {
|
|
2069
|
+
throw new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "ok" with no finalText`);
|
|
2070
|
+
}
|
|
2071
|
+
combined.throwIfAborted();
|
|
2072
|
+
const governedOutput = governedPlayerOutputs.get(result);
|
|
2073
|
+
if (governedOutput !== undefined) {
|
|
2074
|
+
governedPlayerOutputs.delete(result);
|
|
2075
|
+
return governedOutput;
|
|
2076
|
+
}
|
|
2077
|
+
throw new Error('DECIDE governed player result was not reconciled against repository evidence');
|
|
1006
2078
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
throw error;
|
|
2079
|
+
finally {
|
|
2080
|
+
actorSettlementAborts.push(settlementAborts);
|
|
1010
2081
|
}
|
|
1011
2082
|
});
|
|
1012
2083
|
nestedBridge = createNestedPlaybookBridge({
|
|
1013
2084
|
nextCallId: () => `playbook-${++playbookCallSequence}`,
|
|
1014
2085
|
getBoundarySignal: () => currentSignal,
|
|
1015
2086
|
callPlaybook: (request, signal) => trackBoundaryCall(Promise.resolve(requirePorts().callPlaybook(request, signal))),
|
|
1016
|
-
emitStarted: async (event) => {
|
|
2087
|
+
emitStarted: async (event, aborts) => {
|
|
1017
2088
|
playbookCallTurnIds.set(event.callId, currentTurnId);
|
|
1018
2089
|
await emitTrace('playbook.call.started', {
|
|
1019
2090
|
stateId: event.stateId,
|
|
@@ -1022,9 +2093,9 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1022
2093
|
}, {
|
|
1023
2094
|
...(currentTurnId === undefined ? {} : { turnId: currentTurnId }),
|
|
1024
2095
|
callId: event.callId,
|
|
1025
|
-
});
|
|
2096
|
+
}, aborts);
|
|
1026
2097
|
},
|
|
1027
|
-
emitFinished: async (event) => {
|
|
2098
|
+
emitFinished: async (event, aborts) => {
|
|
1028
2099
|
const turnId = playbookCallTurnIds.get(event.callId);
|
|
1029
2100
|
try {
|
|
1030
2101
|
await emitTrace('playbook.call.finished', {
|
|
@@ -1035,50 +2106,88 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1035
2106
|
}, {
|
|
1036
2107
|
...(turnId === undefined ? {} : { turnId }),
|
|
1037
2108
|
callId: event.callId,
|
|
1038
|
-
});
|
|
2109
|
+
}, aborts);
|
|
1039
2110
|
}
|
|
1040
2111
|
finally {
|
|
1041
2112
|
playbookCallTurnIds.delete(event.callId);
|
|
1042
2113
|
}
|
|
1043
2114
|
},
|
|
1044
2115
|
drain: flush,
|
|
1045
|
-
bindResumeSignal: (signal) => {
|
|
2116
|
+
bindResumeSignal: (signal, aborts) => {
|
|
1046
2117
|
currentSignal = signal;
|
|
2118
|
+
currentAborts = aborts ?? abortReasonClassifier(signal);
|
|
1047
2119
|
},
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
2120
|
+
bindActorSettlement: (aborts) => {
|
|
2121
|
+
actorSettlementAborts.push(aborts);
|
|
2122
|
+
},
|
|
2123
|
+
onControlPlaneError: (error, aborts) => {
|
|
2124
|
+
if (!aborts?.isAbortReason(error) &&
|
|
2125
|
+
!currentAborts?.isAbortReason(error)) {
|
|
1051
2126
|
controlPlaneError ??= error;
|
|
1052
2127
|
}
|
|
1053
2128
|
},
|
|
1054
|
-
onBackgroundError: (error) => {
|
|
1055
|
-
|
|
2129
|
+
onBackgroundError: (error, aborts) => {
|
|
2130
|
+
if (!aborts?.isAbortReason(error)) {
|
|
2131
|
+
collectFailure(emissionFailures, error);
|
|
2132
|
+
}
|
|
1056
2133
|
},
|
|
1057
2134
|
});
|
|
1058
2135
|
const providedMachine = decideMachine.provide({
|
|
1059
2136
|
actors: { player, playbook: nestedBridge.actorLogic },
|
|
1060
2137
|
});
|
|
2138
|
+
const consumeActorSettlementAborts = (forSnapshot = false) => {
|
|
2139
|
+
const aborts = actorSettlementAborts.shift() ?? actorSettlementErrorAborts;
|
|
2140
|
+
actorSettlementErrorAborts = undefined;
|
|
2141
|
+
if (forSnapshot && aborts !== undefined) {
|
|
2142
|
+
actorSettlementErrorAborts = aborts;
|
|
2143
|
+
queueMicrotask(() => {
|
|
2144
|
+
if (actorSettlementErrorAborts === aborts) {
|
|
2145
|
+
actorSettlementErrorAborts = undefined;
|
|
2146
|
+
}
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
return aborts;
|
|
2150
|
+
};
|
|
1061
2151
|
const inspect = (event) => {
|
|
1062
|
-
if (event.type !== '@xstate.snapshot')
|
|
1063
|
-
return;
|
|
1064
2152
|
if (actor === undefined || event.actorRef !== actor)
|
|
1065
2153
|
return;
|
|
1066
2154
|
if (suppressInspectionEmissions)
|
|
1067
2155
|
return;
|
|
2156
|
+
if (event.type === '@xstate.action') {
|
|
2157
|
+
try {
|
|
2158
|
+
acceptedOutcomeConsumer.capture(event.action);
|
|
2159
|
+
}
|
|
2160
|
+
catch (error) {
|
|
2161
|
+
latchInspectionError(error);
|
|
2162
|
+
}
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
if (event.type !== '@xstate.snapshot')
|
|
2166
|
+
return;
|
|
2167
|
+
const settlementAborts = consumeActorSettlementAborts(true);
|
|
1068
2168
|
try {
|
|
1069
2169
|
const snapshot = event.snapshot;
|
|
1070
2170
|
const state = normalizePlaybookSnapshot(snapshot);
|
|
1071
2171
|
const prior = previousState ?? state;
|
|
2172
|
+
let acceptedOutcomes = [];
|
|
2173
|
+
try {
|
|
2174
|
+
acceptedOutcomes = acceptedOutcomeConsumer.confirm(previousState, state);
|
|
2175
|
+
}
|
|
2176
|
+
catch (error) {
|
|
2177
|
+
latchInspectionError(error);
|
|
2178
|
+
}
|
|
1072
2179
|
const context = snapshot.context;
|
|
1073
|
-
const fsmPayload = telemetryPayload(prior, state, event.event, context
|
|
2180
|
+
const fsmPayload = telemetryPayload(prior, state, event.event, context, hiddenDeferredOperationId === undefined
|
|
2181
|
+
? undefined
|
|
2182
|
+
: 'commitCoderProposal');
|
|
1074
2183
|
const describedFsmPayload = snapshotJsonValue(fsmPayload, 'described FSM telemetry');
|
|
1075
2184
|
void enqueueTracedEmission('fsm.transition', fsmPayload, { turnId: currentTurnId }, (emissionPorts) => emissionPorts.emitTelemetry({
|
|
1076
2185
|
topic: TELEMETRY_TOPIC,
|
|
1077
2186
|
payload: describedFsmPayload,
|
|
1078
|
-
})).catch(() => undefined);
|
|
2187
|
+
}), settlementAborts).catch(() => undefined);
|
|
1079
2188
|
const priorIds = new Set(previousState?.activeStateIds ?? []);
|
|
1080
2189
|
previousState = state;
|
|
1081
|
-
const pendingQuestions =
|
|
2190
|
+
const pendingQuestions = visiblePendingQuestionsForState(state, context);
|
|
1082
2191
|
const bossRelevantStateIds = state.activeStateIds.filter((stateId) => STATUS_STATE_IDS.has(stateId));
|
|
1083
2192
|
const scheduleStatus = (message, stateId, data) => {
|
|
1084
2193
|
const tracePayload = {
|
|
@@ -1088,8 +2197,11 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1088
2197
|
...(data !== undefined ? { data } : {}),
|
|
1089
2198
|
};
|
|
1090
2199
|
assertJsonSafe(tracePayload);
|
|
1091
|
-
void enqueueTracedEmission('status.emitted', tracePayload, { turnId: currentTurnId }, (emissionPorts) => emissionPorts.emitStatus(message, data)).catch(() => undefined);
|
|
2200
|
+
void enqueueTracedEmission('status.emitted', tracePayload, { turnId: currentTurnId }, (emissionPorts) => emissionPorts.emitStatus(message, data), settlementAborts).catch(() => undefined);
|
|
1092
2201
|
};
|
|
2202
|
+
for (const acceptedOutcome of acceptedOutcomes) {
|
|
2203
|
+
void enqueueAcceptedOutcomeEmission(acceptedOutcome, state, settlementAborts).catch(() => undefined);
|
|
2204
|
+
}
|
|
1093
2205
|
for (const activeStateId of state.activeStateIds) {
|
|
1094
2206
|
if (priorIds.has(activeStateId) ||
|
|
1095
2207
|
!STATUS_STATE_IDS.has(activeStateId)) {
|
|
@@ -1116,11 +2228,13 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1116
2228
|
}
|
|
1117
2229
|
}
|
|
1118
2230
|
catch (error) {
|
|
1119
|
-
|
|
2231
|
+
acceptedOutcomeConsumer.reset();
|
|
2232
|
+
latchInspectionError(error, settlementAborts);
|
|
1120
2233
|
}
|
|
1121
2234
|
};
|
|
1122
2235
|
const createRuntimeActor = (machineSnapshot) => {
|
|
1123
2236
|
previousState = undefined;
|
|
2237
|
+
acceptedOutcomeConsumer.reset();
|
|
1124
2238
|
// DR-014 §1: a restore rehydrates the persisted machine snapshot;
|
|
1125
2239
|
// XState derives context/value from it and ignores `input` then.
|
|
1126
2240
|
actor = createActor(providedMachine, {
|
|
@@ -1132,6 +2246,15 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1132
2246
|
}),
|
|
1133
2247
|
inspect,
|
|
1134
2248
|
});
|
|
2249
|
+
// A synchronous FSM action throw errors the actor without any pending
|
|
2250
|
+
// boundary await to observe it; unobserved, XState would surface it via
|
|
2251
|
+
// reportUnhandledError as an uncaughtException. Observe it here: latch
|
|
2252
|
+
// it as a control error while a turn signal is active (unless it is
|
|
2253
|
+
// the abort reason itself), otherwise collect it with the emission
|
|
2254
|
+
// failures (slc/link.md §Abort).
|
|
2255
|
+
actor.subscribe({
|
|
2256
|
+
error: (error) => latchInspectionError(error, consumeActorSettlementAborts()),
|
|
2257
|
+
});
|
|
1135
2258
|
};
|
|
1136
2259
|
// PBRT-6: the single seam that stops this runtime's actor. Stopping a
|
|
1137
2260
|
// still-running actor fires one more `@xstate.snapshot` for the *unchanged*
|
|
@@ -1144,6 +2267,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1144
2267
|
if (!actor)
|
|
1145
2268
|
return;
|
|
1146
2269
|
suppressInspectionEmissions = true;
|
|
2270
|
+
acceptedOutcomeConsumer.reset();
|
|
1147
2271
|
actor.stop();
|
|
1148
2272
|
};
|
|
1149
2273
|
const startActor = () => {
|
|
@@ -1162,16 +2286,21 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1162
2286
|
const live = actor;
|
|
1163
2287
|
if (!live)
|
|
1164
2288
|
throw new Error('decide runtime: actor is not initialized');
|
|
2289
|
+
refreshReconciliationProjection();
|
|
2290
|
+
if (hasUnresolvedReconciliation())
|
|
2291
|
+
return null;
|
|
1165
2292
|
const snapshot = live.getSnapshot();
|
|
1166
2293
|
const context = snapshot.context;
|
|
1167
2294
|
const state = normalizePlaybookSnapshot(snapshot, {
|
|
1168
2295
|
pendingCall: nestedBridge.getPendingCall(),
|
|
1169
2296
|
});
|
|
1170
|
-
const pendingQuestions =
|
|
2297
|
+
const pendingQuestions = visiblePendingQuestionsForState(state, context);
|
|
2298
|
+
const failed = state.activeStateIds.includes('failed');
|
|
1171
2299
|
if (pendingQuestions.length === 0 &&
|
|
1172
2300
|
(snapshot.status === 'done' ||
|
|
1173
2301
|
state.activeStateIds.includes('ready') ||
|
|
1174
|
-
|
|
2302
|
+
(failed &&
|
|
2303
|
+
automaticReplayPolicy.allowsFailureStateRetry()))) {
|
|
1175
2304
|
return { type: 'START_DECIDE', callerTopic: text };
|
|
1176
2305
|
}
|
|
1177
2306
|
if (pendingQuestions.length === 0)
|
|
@@ -1183,6 +2312,458 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1183
2312
|
const raw = await callJudge(prompt, signal, 'boss-input-classification', state.stateId);
|
|
1184
2313
|
return parseClassification(raw, text, pendingQuestions.map(({ questionId }) => questionId));
|
|
1185
2314
|
};
|
|
2315
|
+
const deferredBoundarySeed = (operationId, callId) => {
|
|
2316
|
+
const operation = effectLedgerMirror.logicalOperations.find((candidate) => candidate.operationId === operationId);
|
|
2317
|
+
const latestBoundaryId = operation?.boundaryIds.at(-1);
|
|
2318
|
+
const priorBoundary = effectLedgerMirror.boundaries.find((candidate) => candidate.boundaryId === latestBoundaryId);
|
|
2319
|
+
if (operation === undefined || priorBoundary === undefined) {
|
|
2320
|
+
throw new TypeError('DECIDE deferred logical operation has no linked physical boundary');
|
|
2321
|
+
}
|
|
2322
|
+
if (currentTurnId === undefined ||
|
|
2323
|
+
!Number.isSafeInteger(currentTurnId) ||
|
|
2324
|
+
currentTurnId <= 0) {
|
|
2325
|
+
throw new Error('DECIDE deferred continuation requires an active positive turn id');
|
|
2326
|
+
}
|
|
2327
|
+
return {
|
|
2328
|
+
boundaryId: randomUUID(),
|
|
2329
|
+
runtimeSessionId: requireSessionIdentity().sessionId,
|
|
2330
|
+
turnId: currentTurnId,
|
|
2331
|
+
callId,
|
|
2332
|
+
roleId: priorBoundary.roleId,
|
|
2333
|
+
sourceStateId: priorBoundary.sourceStateId,
|
|
2334
|
+
sourceOutcomeSchema: snapshotJsonValue(priorBoundary.sourceOutcomeSchema, 'DECIDE deferred source outcome schema'),
|
|
2335
|
+
dispositions: [...priorBoundary.dispositions],
|
|
2336
|
+
correctionBudget: { limit: 1, spent: false },
|
|
2337
|
+
};
|
|
2338
|
+
};
|
|
2339
|
+
const prepareDeferredContinuation = async (signal, resumeEvent) => {
|
|
2340
|
+
if (deferredEffects === undefined || deferredOperationId === undefined) {
|
|
2341
|
+
throw new TypeError('DECIDE deferred Boss reply has no host-bound logical operation');
|
|
2342
|
+
}
|
|
2343
|
+
const operationId = deferredOperationId;
|
|
2344
|
+
const callId = `player-${++playerCallSequence}`;
|
|
2345
|
+
const effectBoundary = deferredBoundarySeed(operationId, callId);
|
|
2346
|
+
const active = {
|
|
2347
|
+
operationId,
|
|
2348
|
+
effectBoundary,
|
|
2349
|
+
result: deferredValue(),
|
|
2350
|
+
acknowledged: deferredValue(),
|
|
2351
|
+
};
|
|
2352
|
+
const readiness = deferredValue();
|
|
2353
|
+
const repositoryCall = deferredEffects.repository.runDeferred({
|
|
2354
|
+
mode: 'continue',
|
|
2355
|
+
signal,
|
|
2356
|
+
operationId,
|
|
2357
|
+
effectBoundary,
|
|
2358
|
+
operation: async ({ playerContinuation }) => {
|
|
2359
|
+
if (playerContinuation !== false &&
|
|
2360
|
+
(typeof playerContinuation !== 'string' ||
|
|
2361
|
+
playerContinuation.trim() === '')) {
|
|
2362
|
+
throw new TypeError('DECIDE bound deferred player continuation is invalid');
|
|
2363
|
+
}
|
|
2364
|
+
active.playerContinuation = playerContinuation;
|
|
2365
|
+
readiness.resolve({ status: 'ready' });
|
|
2366
|
+
return active.result.promise;
|
|
2367
|
+
},
|
|
2368
|
+
completeEffectBoundary: async (completion) => {
|
|
2369
|
+
if (active.input === undefined) {
|
|
2370
|
+
throw new TypeError('DECIDE deferred continuation omitted its authored player input');
|
|
2371
|
+
}
|
|
2372
|
+
return completionEvidenceFor(active.input, active.input.role, active.playerId, signal, operationId)(completion);
|
|
2373
|
+
},
|
|
2374
|
+
});
|
|
2375
|
+
void repositoryCall.then((value) => readiness.resolve({ status: 'settled', value }), (reason) => readiness.resolve({ status: 'rejected', reason }));
|
|
2376
|
+
const prepared = await readiness.promise;
|
|
2377
|
+
if (prepared.status === 'rejected')
|
|
2378
|
+
throw prepared.reason;
|
|
2379
|
+
if (prepared.status === 'settled') {
|
|
2380
|
+
synchronizeDeferredProjection(assertPlaybookEffectLedger(prepared.value.effectLedger, 'DECIDE deferred checkpoint-mismatch effect ledger'));
|
|
2381
|
+
hiddenDeferredOperationId = operationId;
|
|
2382
|
+
return { proceed: false };
|
|
2383
|
+
}
|
|
2384
|
+
activeDeferredContinuation = active;
|
|
2385
|
+
const acknowledgement = repositoryCall.then((value) => {
|
|
2386
|
+
if (value.status !== 'continued') {
|
|
2387
|
+
throw new TypeError('DECIDE deferred repository changed status after starting its operation');
|
|
2388
|
+
}
|
|
2389
|
+
const result = acknowledgeGovernedPlayerResult(value, effectBoundary.boundaryId);
|
|
2390
|
+
if (hiddenDeferredOperationId === undefined) {
|
|
2391
|
+
suppressInspectionEmissions = false;
|
|
2392
|
+
const live = actor;
|
|
2393
|
+
if (live === undefined) {
|
|
2394
|
+
throw new Error('DECIDE deferred acknowledgement lost its runtime actor');
|
|
2395
|
+
}
|
|
2396
|
+
// Entry into the governed continuation was hidden until its durable
|
|
2397
|
+
// effect acknowledgement. Publish that exact current root snapshot
|
|
2398
|
+
// before releasing the actor output, so an accepted-outcome marker
|
|
2399
|
+
// is confirmed against commitCoderProposal rather than the earlier
|
|
2400
|
+
// awaitBossReply snapshot.
|
|
2401
|
+
inspect({
|
|
2402
|
+
type: '@xstate.snapshot',
|
|
2403
|
+
actorRef: live,
|
|
2404
|
+
event: resumeEvent,
|
|
2405
|
+
snapshot: live.getSnapshot(),
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
active.acknowledged.resolve(result);
|
|
2409
|
+
}, (error) => {
|
|
2410
|
+
active.acknowledged.reject(error);
|
|
2411
|
+
throw error;
|
|
2412
|
+
}).catch((error) => {
|
|
2413
|
+
active.acknowledged.reject(error);
|
|
2414
|
+
throw error;
|
|
2415
|
+
});
|
|
2416
|
+
return { proceed: true, acknowledgement };
|
|
2417
|
+
};
|
|
2418
|
+
const parkDeferredContinuation = async (signal) => {
|
|
2419
|
+
if (deferredEffects === undefined || deferredOperationId === undefined) {
|
|
2420
|
+
return;
|
|
2421
|
+
}
|
|
2422
|
+
const operationId = deferredOperationId;
|
|
2423
|
+
const parked = await deferredEffects.repository.runDeferred({
|
|
2424
|
+
mode: 'park',
|
|
2425
|
+
signal,
|
|
2426
|
+
operationId,
|
|
2427
|
+
});
|
|
2428
|
+
synchronizeDeferredProjection(assertPlaybookEffectLedger(parked.effectLedger, 'DECIDE deferred park effect ledger'));
|
|
2429
|
+
hiddenDeferredOperationId = operationId;
|
|
2430
|
+
};
|
|
2431
|
+
const restoreDeferredReconciliation = async (operationId, signal, publishQuestion) => {
|
|
2432
|
+
const restored = await deferredEffects.repository.runDeferred({
|
|
2433
|
+
mode: 'restore',
|
|
2434
|
+
signal,
|
|
2435
|
+
operationId,
|
|
2436
|
+
});
|
|
2437
|
+
synchronizeDeferredProjection(assertPlaybookEffectLedger(restored.effectLedger, 'DECIDE deferred restoration effect ledger'));
|
|
2438
|
+
if (restored.status === 'parked') {
|
|
2439
|
+
throw new TypeError('DECIDE deferred restoration returned an invalid parked status');
|
|
2440
|
+
}
|
|
2441
|
+
if (restored.status !== 'restored') {
|
|
2442
|
+
if (hiddenDeferredOperationId !== operationId) {
|
|
2443
|
+
throw new TypeError('DECIDE unresolved deferred restoration lost its operation identity');
|
|
2444
|
+
}
|
|
2445
|
+
return restored.status;
|
|
2446
|
+
}
|
|
2447
|
+
if (hiddenDeferredOperationId !== undefined) {
|
|
2448
|
+
throw new TypeError('DECIDE restored deferred operation remained unresolved');
|
|
2449
|
+
}
|
|
2450
|
+
if (openDeferredOperation()?.operationId !== operationId) {
|
|
2451
|
+
throw new TypeError('DECIDE restored deferred operation is not the current bound wait');
|
|
2452
|
+
}
|
|
2453
|
+
const live = actor;
|
|
2454
|
+
if (live === undefined) {
|
|
2455
|
+
throw new Error('decide runtime: init(session) must be called first');
|
|
2456
|
+
}
|
|
2457
|
+
const state = currentState();
|
|
2458
|
+
const context = live.getSnapshot().context;
|
|
2459
|
+
const pending = questionForWaitState('awaitBossReply', visiblePendingQuestionsForState(state, context));
|
|
2460
|
+
const operation = openDeferredOperation();
|
|
2461
|
+
const projectedPending = pending === undefined
|
|
2462
|
+
? undefined
|
|
2463
|
+
: {
|
|
2464
|
+
questionId: pending.questionId,
|
|
2465
|
+
asker: pending.asker,
|
|
2466
|
+
question: pending.question,
|
|
2467
|
+
sourceItem: pending.sourceItem,
|
|
2468
|
+
};
|
|
2469
|
+
if (pending === undefined ||
|
|
2470
|
+
operation?.pendingQuestion === undefined ||
|
|
2471
|
+
stableJson(projectedPending, 'DECIDE restored FSM pending question') !==
|
|
2472
|
+
stableJson(operation.pendingQuestion, 'DECIDE restored deferred pending question')) {
|
|
2473
|
+
throw new TypeError('DECIDE restored deferred wait does not equal its FSM question');
|
|
2474
|
+
}
|
|
2475
|
+
if (publishQuestion) {
|
|
2476
|
+
await emitBoundaryStatus(`${pending.asker.roleId} asks: ${pending.question}`, state);
|
|
2477
|
+
await emitBoundaryStatus(`◆ awaiting Boss reply · ${pending.resumeStateId} · ${pending.asker.roleId} · ${pending.sourceItem}`, state);
|
|
2478
|
+
await flush();
|
|
2479
|
+
}
|
|
2480
|
+
return 'restored';
|
|
2481
|
+
};
|
|
2482
|
+
const unresolvedControlCandidates = () => {
|
|
2483
|
+
if (!hasUnresolvedReconciliation())
|
|
2484
|
+
return [];
|
|
2485
|
+
const operation = hiddenDeferredOperationId === undefined
|
|
2486
|
+
? undefined
|
|
2487
|
+
: effectLedgerMirror.logicalOperations.find(({ operationId }) => operationId === hiddenDeferredOperationId);
|
|
2488
|
+
return [
|
|
2489
|
+
{
|
|
2490
|
+
action: {
|
|
2491
|
+
id: UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID,
|
|
2492
|
+
label: 'Retry unresolved effect reconciliation',
|
|
2493
|
+
},
|
|
2494
|
+
kind: 'reconcile',
|
|
2495
|
+
...(operation?.checkpointRestorationEligible === true
|
|
2496
|
+
? { deferredRestoreOperationId: operation.operationId }
|
|
2497
|
+
: {}),
|
|
2498
|
+
},
|
|
2499
|
+
{
|
|
2500
|
+
action: {
|
|
2501
|
+
id: UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID,
|
|
2502
|
+
label: 'Abandon unresolved workflow attempt',
|
|
2503
|
+
},
|
|
2504
|
+
kind: 'abandon',
|
|
2505
|
+
},
|
|
2506
|
+
];
|
|
2507
|
+
};
|
|
2508
|
+
const deriveControlCandidates = () => {
|
|
2509
|
+
const state = currentState();
|
|
2510
|
+
if (state.status !== 'active' ||
|
|
2511
|
+
!state.quiescent ||
|
|
2512
|
+
nestedBridge.getPendingCall() !== undefined) {
|
|
2513
|
+
return [];
|
|
2514
|
+
}
|
|
2515
|
+
return unresolvedControlCandidates();
|
|
2516
|
+
};
|
|
2517
|
+
const describeControlView = () => {
|
|
2518
|
+
if (disposed || disposalPromise !== undefined) {
|
|
2519
|
+
throw new Error('decide runtime: runtime is disposing or disposed');
|
|
2520
|
+
}
|
|
2521
|
+
const live = actor;
|
|
2522
|
+
if (live === undefined || sessionIdentity === undefined) {
|
|
2523
|
+
throw new Error('decide runtime: init(session) must be called before describe');
|
|
2524
|
+
}
|
|
2525
|
+
if (currentSignal !== undefined || currentTurnId !== undefined) {
|
|
2526
|
+
throw new Error('decide runtime: another runtime turn is active');
|
|
2527
|
+
}
|
|
2528
|
+
refreshReconciliationProjection();
|
|
2529
|
+
const state = currentState();
|
|
2530
|
+
const context = live.getSnapshot().context;
|
|
2531
|
+
const unresolved = hasUnresolvedReconciliation();
|
|
2532
|
+
const pendingQuestions = unresolved
|
|
2533
|
+
? []
|
|
2534
|
+
: visiblePendingQuestionsForState(state, context).map((pending) => Object.freeze({
|
|
2535
|
+
questionId: pending.questionId,
|
|
2536
|
+
asker: pending.asker,
|
|
2537
|
+
question: pending.question,
|
|
2538
|
+
sourceItem: pending.sourceItem,
|
|
2539
|
+
}));
|
|
2540
|
+
const actions = deriveControlCandidates().map(({ action }) => ({
|
|
2541
|
+
...action,
|
|
2542
|
+
}));
|
|
2543
|
+
const lastError = normalizeErrorFull(context.lastError);
|
|
2544
|
+
const stateDescription = unresolved || state.stateId === undefined
|
|
2545
|
+
? undefined
|
|
2546
|
+
: STATE_DESCRIPTIONS[state.stateId];
|
|
2547
|
+
return deepFreeze(snapshotJsonValue({
|
|
2548
|
+
state,
|
|
2549
|
+
...(stateDescription === undefined ? {} : { stateDescription }),
|
|
2550
|
+
pendingQuestions,
|
|
2551
|
+
...(lastError === undefined ? {} : { lastError }),
|
|
2552
|
+
actions,
|
|
2553
|
+
}, 'DECIDE control view'));
|
|
2554
|
+
};
|
|
2555
|
+
const normalizedControlError = (error) => normalizeErrorFull(error) ?? {
|
|
2556
|
+
name: error instanceof Error ? error.name : 'Error',
|
|
2557
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2558
|
+
};
|
|
2559
|
+
const frozenControlReceipt = (receipt) => deepFreeze(snapshotJsonValue(receipt, 'DECIDE apply receipt'));
|
|
2560
|
+
const applyControlAction = async (input) => {
|
|
2561
|
+
if (input === null || typeof input !== 'object') {
|
|
2562
|
+
throw new TypeError('decide runtime: apply input must be an object');
|
|
2563
|
+
}
|
|
2564
|
+
const { actionId, key, signal } = input;
|
|
2565
|
+
if (typeof actionId !== 'string' || actionId.length === 0) {
|
|
2566
|
+
throw new TypeError('decide runtime: apply actionId must be a non-empty string');
|
|
2567
|
+
}
|
|
2568
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
2569
|
+
throw new TypeError('decide runtime: apply key must be a non-empty string');
|
|
2570
|
+
}
|
|
2571
|
+
if (!(signal instanceof AbortSignal)) {
|
|
2572
|
+
throw new TypeError('decide runtime: apply signal must be an AbortSignal');
|
|
2573
|
+
}
|
|
2574
|
+
if (disposed || disposalPromise !== undefined) {
|
|
2575
|
+
throw new Error('decide runtime: runtime is disposing or disposed');
|
|
2576
|
+
}
|
|
2577
|
+
if (actor === undefined || sessionIdentity === undefined) {
|
|
2578
|
+
throw new Error('decide runtime: init(session) must be called before apply');
|
|
2579
|
+
}
|
|
2580
|
+
if (currentSignal !== undefined || currentTurnId !== undefined) {
|
|
2581
|
+
throw new Error('decide runtime: another runtime turn is active');
|
|
2582
|
+
}
|
|
2583
|
+
const recorded = appliedControlReceipts.get(key);
|
|
2584
|
+
if (recorded !== undefined)
|
|
2585
|
+
return recorded;
|
|
2586
|
+
signal.throwIfAborted();
|
|
2587
|
+
refreshReconciliationProjection();
|
|
2588
|
+
const turnId = ++turnSequence;
|
|
2589
|
+
const callId = `apply-${++applyCallSequence}`;
|
|
2590
|
+
currentTurnId = turnId;
|
|
2591
|
+
currentSignal = signal;
|
|
2592
|
+
currentAborts = abortReasonClassifier(signal);
|
|
2593
|
+
controlPlaneError = undefined;
|
|
2594
|
+
let accepted = false;
|
|
2595
|
+
let receipt;
|
|
2596
|
+
let operationError;
|
|
2597
|
+
let settlementError;
|
|
2598
|
+
let lateDeliveryError;
|
|
2599
|
+
const position = { turnId, callId };
|
|
2600
|
+
const preAcceptanceFinish = (reason) => ({
|
|
2601
|
+
actionId,
|
|
2602
|
+
key,
|
|
2603
|
+
disposition: 'rejected',
|
|
2604
|
+
reason,
|
|
2605
|
+
});
|
|
2606
|
+
try {
|
|
2607
|
+
try {
|
|
2608
|
+
try {
|
|
2609
|
+
await emitTrace('apply.started', {
|
|
2610
|
+
actionId,
|
|
2611
|
+
key,
|
|
2612
|
+
...stateIdentity(currentState()),
|
|
2613
|
+
}, position, currentAborts);
|
|
2614
|
+
}
|
|
2615
|
+
catch (error) {
|
|
2616
|
+
latchControlPlaneError(error, signal);
|
|
2617
|
+
try {
|
|
2618
|
+
await emitTrace('apply.finished', {
|
|
2619
|
+
...preAcceptanceFinish('apply.started trace sink rejected'),
|
|
2620
|
+
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
2621
|
+
error: normalizedControlError(error),
|
|
2622
|
+
}, position, currentAborts);
|
|
2623
|
+
}
|
|
2624
|
+
catch {
|
|
2625
|
+
// Preserve the start failure after one best-effort finish attempt.
|
|
2626
|
+
}
|
|
2627
|
+
throw error;
|
|
2628
|
+
}
|
|
2629
|
+
await flush();
|
|
2630
|
+
if (signal.aborted) {
|
|
2631
|
+
try {
|
|
2632
|
+
await emitTrace('apply.finished', {
|
|
2633
|
+
...preAcceptanceFinish('aborted before acceptance'),
|
|
2634
|
+
status: 'aborted',
|
|
2635
|
+
error: normalizedControlError(signal.reason),
|
|
2636
|
+
}, position, currentAborts);
|
|
2637
|
+
}
|
|
2638
|
+
catch (error) {
|
|
2639
|
+
// A rejecting abort-finish sink outranks the abort at settlement.
|
|
2640
|
+
settlementError ??= error;
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
signal.throwIfAborted();
|
|
2644
|
+
const candidate = deriveControlCandidates().find(({ action }) => action.id === actionId);
|
|
2645
|
+
if (candidate === undefined) {
|
|
2646
|
+
receipt = frozenControlReceipt({
|
|
2647
|
+
disposition: 'rejected',
|
|
2648
|
+
reason: `action ${JSON.stringify(actionId)} is not currently advertised`,
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
else {
|
|
2652
|
+
// Acceptance is the line after which this boundary always records
|
|
2653
|
+
// and returns a receipt: the requested effect may now exist.
|
|
2654
|
+
accepted = true;
|
|
2655
|
+
try {
|
|
2656
|
+
let run;
|
|
2657
|
+
if (candidate.kind === 'abandon') {
|
|
2658
|
+
signal.throwIfAborted();
|
|
2659
|
+
run = {
|
|
2660
|
+
outcome: 'unresolved-effect',
|
|
2661
|
+
state: currentState(),
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
else {
|
|
2665
|
+
if (candidate.deferredRestoreOperationId !== undefined) {
|
|
2666
|
+
await restoreDeferredReconciliation(candidate.deferredRestoreOperationId, signal, true);
|
|
2667
|
+
}
|
|
2668
|
+
else {
|
|
2669
|
+
// The host owns receipt reconstruction. Reconciliation only
|
|
2670
|
+
// re-reads its authoritative ledger; it never calls a player
|
|
2671
|
+
// or judge to manufacture missing semantic evidence.
|
|
2672
|
+
refreshReconciliationProjection();
|
|
2673
|
+
}
|
|
2674
|
+
signal.throwIfAborted();
|
|
2675
|
+
run = {
|
|
2676
|
+
outcome: hasUnresolvedReconciliation()
|
|
2677
|
+
? 'no-action'
|
|
2678
|
+
: 'quiescent',
|
|
2679
|
+
state: currentState(),
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
if (controlPlaneError !== undefined)
|
|
2683
|
+
throw controlPlaneError;
|
|
2684
|
+
receipt = frozenControlReceipt({
|
|
2685
|
+
disposition: 'executed',
|
|
2686
|
+
run,
|
|
2687
|
+
});
|
|
2688
|
+
}
|
|
2689
|
+
catch (error) {
|
|
2690
|
+
receipt = frozenControlReceipt({
|
|
2691
|
+
disposition: 'failed',
|
|
2692
|
+
error: normalizedControlError(error),
|
|
2693
|
+
});
|
|
2694
|
+
}
|
|
2695
|
+
appliedControlReceipts.set(key, receipt);
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
catch (error) {
|
|
2699
|
+
operationError = error;
|
|
2700
|
+
}
|
|
2701
|
+
try {
|
|
2702
|
+
await flush();
|
|
2703
|
+
}
|
|
2704
|
+
catch (error) {
|
|
2705
|
+
settlementError = error;
|
|
2706
|
+
}
|
|
2707
|
+
settlementError ??= controlPlaneError;
|
|
2708
|
+
if (accepted && settlementError !== undefined) {
|
|
2709
|
+
receipt = frozenControlReceipt({
|
|
2710
|
+
disposition: 'failed',
|
|
2711
|
+
error: normalizedControlError(settlementError),
|
|
2712
|
+
});
|
|
2713
|
+
appliedControlReceipts.set(key, receipt);
|
|
2714
|
+
settlementError = undefined;
|
|
2715
|
+
}
|
|
2716
|
+
if (receipt !== undefined) {
|
|
2717
|
+
const finishFailures = [];
|
|
2718
|
+
try {
|
|
2719
|
+
await emitTrace('apply.finished', { actionId, key, ...receipt }, position, currentAborts);
|
|
2720
|
+
}
|
|
2721
|
+
catch (error) {
|
|
2722
|
+
if (!accepted || !isAbortFailure(error, signal)) {
|
|
2723
|
+
collectFailure(finishFailures, error);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
try {
|
|
2727
|
+
await flush();
|
|
2728
|
+
}
|
|
2729
|
+
catch (error) {
|
|
2730
|
+
if (!accepted || !isAbortFailure(error, signal)) {
|
|
2731
|
+
collectFailure(finishFailures, error);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
if (finishFailures.length > 0) {
|
|
2735
|
+
const failure = finishFailures.length === 1
|
|
2736
|
+
? finishFailures[0]
|
|
2737
|
+
: new AggregateError(finishFailures, 'DECIDE apply settlement emissions failed');
|
|
2738
|
+
if (accepted)
|
|
2739
|
+
lateDeliveryError = failure;
|
|
2740
|
+
else
|
|
2741
|
+
settlementError = failure;
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
finally {
|
|
2746
|
+
currentSignal = undefined;
|
|
2747
|
+
currentAborts = undefined;
|
|
2748
|
+
currentTurnId = undefined;
|
|
2749
|
+
controlPlaneError = undefined;
|
|
2750
|
+
if (accepted && receipt !== undefined) {
|
|
2751
|
+
appliedControlReceipts.set(key, receipt);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
if (lateDeliveryError !== undefined) {
|
|
2755
|
+
collectFailure(emissionFailures, lateDeliveryError);
|
|
2756
|
+
}
|
|
2757
|
+
if (accepted && receipt !== undefined)
|
|
2758
|
+
return receipt;
|
|
2759
|
+
const failure = settlementError ?? operationError;
|
|
2760
|
+
if (failure !== undefined)
|
|
2761
|
+
throw failure;
|
|
2762
|
+
if (receipt === undefined) {
|
|
2763
|
+
throw new Error('decide runtime: apply produced no receipt');
|
|
2764
|
+
}
|
|
2765
|
+
return receipt;
|
|
2766
|
+
};
|
|
1186
2767
|
const resultForSnapshot = (signal) => {
|
|
1187
2768
|
const live = actor;
|
|
1188
2769
|
if (!live)
|
|
@@ -1191,33 +2772,53 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1191
2772
|
const pendingCall = nestedBridge.getPendingCall();
|
|
1192
2773
|
const state = normalizePlaybookSnapshot(snapshot, { pendingCall });
|
|
1193
2774
|
const context = snapshot.context;
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
: {
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
2775
|
+
const abortedResult = (abortSignal) => ({
|
|
2776
|
+
outcome: 'aborted',
|
|
2777
|
+
state,
|
|
2778
|
+
...(abortSignal.reason === undefined
|
|
2779
|
+
? {}
|
|
2780
|
+
: {
|
|
2781
|
+
error: normalizeErrorFull(abortSignal.reason) ?? {
|
|
2782
|
+
name: 'AbortError',
|
|
2783
|
+
message: String(abortSignal.reason),
|
|
2784
|
+
},
|
|
2785
|
+
}),
|
|
2786
|
+
});
|
|
2787
|
+
if (snapshot.status === 'error') {
|
|
2788
|
+
// An errored actor outranks a coincident abort unless the actor's
|
|
2789
|
+
// error is the abort reason itself (slc/link.md §Abort).
|
|
2790
|
+
const actorError = snapshot.error;
|
|
2791
|
+
if (actorError !== undefined &&
|
|
2792
|
+
signal !== undefined &&
|
|
2793
|
+
isAbortFailure(actorError, signal)) {
|
|
2794
|
+
return abortedResult(signal);
|
|
2795
|
+
}
|
|
2796
|
+
throw (actorError ?? new Error('decide runtime actor entered error status'));
|
|
2797
|
+
}
|
|
2798
|
+
// Terminal completion outranks a coincident abort (DR-036 §3): reporting
|
|
2799
|
+
// 'aborted' over a completed machine would hide a terminal state that the
|
|
2800
|
+
// next turn silently restarts, duplicating the workflow's side effects.
|
|
1208
2801
|
if (snapshot.status === 'done') {
|
|
1209
2802
|
const output = snapshot.output;
|
|
1210
2803
|
if (output !== undefined)
|
|
1211
2804
|
assertJsonSafe(output, 'terminal output');
|
|
2805
|
+
const stateDescription = state.activeStateIds.includes('done')
|
|
2806
|
+
? STATE_DESCRIPTIONS.done
|
|
2807
|
+
: state.activeStateIds.includes('reportedReviewFailure')
|
|
2808
|
+
? STATE_DESCRIPTIONS.reportedReviewFailure
|
|
2809
|
+
: undefined;
|
|
2810
|
+
if (stateDescription === undefined) {
|
|
2811
|
+
throw new Error('decide runtime: completed actor has no authored final-state description');
|
|
2812
|
+
}
|
|
1212
2813
|
return {
|
|
1213
2814
|
outcome: 'terminal',
|
|
1214
2815
|
state,
|
|
2816
|
+
stateDescription,
|
|
1215
2817
|
...(output === undefined ? {} : { output }),
|
|
1216
2818
|
};
|
|
1217
2819
|
}
|
|
1218
|
-
if (
|
|
1219
|
-
|
|
1220
|
-
new Error('decide runtime actor entered error status'));
|
|
2820
|
+
if (signal?.aborted) {
|
|
2821
|
+
return abortedResult(signal);
|
|
1221
2822
|
}
|
|
1222
2823
|
if (state.activeStateIds.includes('failed')) {
|
|
1223
2824
|
const error = normalizeErrorFull(context.lastError);
|
|
@@ -1286,8 +2887,12 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1286
2887
|
activeEmissionCalls.clear();
|
|
1287
2888
|
emissionQueue.clear();
|
|
1288
2889
|
judgeQueue.clear();
|
|
2890
|
+
semanticCompletionQueue.clear();
|
|
1289
2891
|
actor = undefined;
|
|
1290
2892
|
currentSignal = undefined;
|
|
2893
|
+
currentAborts = undefined;
|
|
2894
|
+
actorSettlementAborts.length = 0;
|
|
2895
|
+
actorSettlementErrorAborts = undefined;
|
|
1291
2896
|
currentTurnId = undefined;
|
|
1292
2897
|
ports = undefined;
|
|
1293
2898
|
sessionIdentity = undefined;
|
|
@@ -1300,7 +2905,20 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1300
2905
|
judgeCallSequence = 0;
|
|
1301
2906
|
playerCallSequence = 0;
|
|
1302
2907
|
playbookCallSequence = 0;
|
|
2908
|
+
applyCallSequence = 0;
|
|
1303
2909
|
playbookCallTurnIds.clear();
|
|
2910
|
+
governedOutputsByBoundaryId.clear();
|
|
2911
|
+
governedFailuresByBoundaryId.clear();
|
|
2912
|
+
governedEvidenceByBoundaryId.clear();
|
|
2913
|
+
governedReceiptsByBoundaryId.clear();
|
|
2914
|
+
pendingProposalCohort.clear();
|
|
2915
|
+
activeProposalCohort = undefined;
|
|
2916
|
+
completedProposalCohortTurnId = undefined;
|
|
2917
|
+
deferredOperationId = undefined;
|
|
2918
|
+
hiddenDeferredOperationId = undefined;
|
|
2919
|
+
unresolvedSemanticBoundaryIds.clear();
|
|
2920
|
+
appliedControlReceipts.clear();
|
|
2921
|
+
activeDeferredContinuation = undefined;
|
|
1304
2922
|
lifecycleStarted = false;
|
|
1305
2923
|
};
|
|
1306
2924
|
return {
|
|
@@ -1321,6 +2939,10 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1321
2939
|
ports = identity.ports;
|
|
1322
2940
|
sessionIdentity = identity;
|
|
1323
2941
|
try {
|
|
2942
|
+
if (deferredEffects !== undefined) {
|
|
2943
|
+
effectLedgerMirror = readEffectLedger();
|
|
2944
|
+
synchronizeDeferredProjection(effectLedgerMirror);
|
|
2945
|
+
}
|
|
1324
2946
|
suppressInspectionEmissions = false;
|
|
1325
2947
|
createRuntimeActor();
|
|
1326
2948
|
const state = currentState();
|
|
@@ -1341,6 +2963,9 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1341
2963
|
initInFlight = undefined;
|
|
1342
2964
|
}
|
|
1343
2965
|
},
|
|
2966
|
+
describe: describeControlView,
|
|
2967
|
+
unresolvedEffectEnvelopes: unresolvedEffectEnvelopeIdentities,
|
|
2968
|
+
apply: applyControlAction,
|
|
1344
2969
|
// DR-014 §1 / DR-031 §5 / PBRT-45: JSON-safe capture of a parked
|
|
1345
2970
|
// session, including one already-started suspended REVIEW call.
|
|
1346
2971
|
// Defined only at a safe capture point — initialized, not disposing
|
|
@@ -1388,8 +3013,12 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1388
3013
|
return undefined;
|
|
1389
3014
|
const machine = detachPersistedMachineSnapshot(actor.getPersistedSnapshot());
|
|
1390
3015
|
const context = actor.getSnapshot().context;
|
|
3016
|
+
if (deferredEffects !== undefined) {
|
|
3017
|
+
synchronizeDeferredProjection(readEffectLedger());
|
|
3018
|
+
}
|
|
3019
|
+
const unresolved = hasUnresolvedReconciliation();
|
|
1391
3020
|
return {
|
|
1392
|
-
schemaVersion:
|
|
3021
|
+
schemaVersion: 4,
|
|
1393
3022
|
playbookId: sessionIdentity.playbookId,
|
|
1394
3023
|
machine,
|
|
1395
3024
|
roleResumeTokens: snapshotRoleResumeTokens(),
|
|
@@ -1401,12 +3030,15 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1401
3030
|
playbookCall: playbookCallSequence,
|
|
1402
3031
|
},
|
|
1403
3032
|
state,
|
|
1404
|
-
pendingBossQuestions:
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
3033
|
+
pendingBossQuestions: unresolved
|
|
3034
|
+
? []
|
|
3035
|
+
: visiblePendingQuestionsForState(state, context).map((pending) => ({
|
|
3036
|
+
questionId: pending.questionId,
|
|
3037
|
+
asker: pending.asker,
|
|
3038
|
+
question: pending.question,
|
|
3039
|
+
sourceItem: pending.sourceItem,
|
|
3040
|
+
})),
|
|
3041
|
+
effectLedger: effectLedgerMirror,
|
|
1410
3042
|
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
1411
3043
|
};
|
|
1412
3044
|
},
|
|
@@ -1424,6 +3056,21 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1424
3056
|
}
|
|
1425
3057
|
const identity = bindSession(session);
|
|
1426
3058
|
const boundSnapshot = assertPlaybookRuntimeSnapshot(snapshot, identity.playbookId, { allowSuspendedCall: true });
|
|
3059
|
+
if (deferredEffects === undefined) {
|
|
3060
|
+
if (boundSnapshot.effectLedger.revision !== 0 ||
|
|
3061
|
+
boundSnapshot.effectLedger.boundaries.length !== 0 ||
|
|
3062
|
+
boundSnapshot.effectLedger.logicalOperations.length !== 0) {
|
|
3063
|
+
throw new TypeError('decide runtime snapshot effectLedger must be the canonical empty ledger');
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
else {
|
|
3067
|
+
const current = readEffectLedger();
|
|
3068
|
+
if (stableJson(current, 'DECIDE current effect ledger') !==
|
|
3069
|
+
stableJson(boundSnapshot.effectLedger, 'DECIDE snapshot effect ledger')) {
|
|
3070
|
+
throw new TypeError('decide runtime snapshot effectLedger must equal the current host mirror');
|
|
3071
|
+
}
|
|
3072
|
+
effectLedgerMirror = current;
|
|
3073
|
+
}
|
|
1427
3074
|
const suspendedCall = boundSnapshot.suspendedCall;
|
|
1428
3075
|
let finishInitialization;
|
|
1429
3076
|
const initialization = new Promise((resolve) => {
|
|
@@ -1440,6 +3087,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1440
3087
|
judgeCallSequence = boundSnapshot.sequences.judgeCall;
|
|
1441
3088
|
playerCallSequence = boundSnapshot.sequences.playerCall;
|
|
1442
3089
|
playbookCallSequence = boundSnapshot.sequences.playbookCall;
|
|
3090
|
+
applyCallSequence = boundSnapshot.sequences.trace;
|
|
1443
3091
|
if (identity.playerSessions) {
|
|
1444
3092
|
priorExternalRoleTokens = snapshotRoleResumeTokens();
|
|
1445
3093
|
}
|
|
@@ -1448,6 +3096,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1448
3096
|
if (suspendedCall !== undefined) {
|
|
1449
3097
|
playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
|
|
1450
3098
|
}
|
|
3099
|
+
synchronizeDeferredProjection(effectLedgerMirror);
|
|
1451
3100
|
suppressInspectionEmissions = true;
|
|
1452
3101
|
createRuntimeActor(boundSnapshot.machine);
|
|
1453
3102
|
actor?.start();
|
|
@@ -1500,12 +3149,17 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1500
3149
|
const turnId = ++turnSequence;
|
|
1501
3150
|
currentTurnId = turnId;
|
|
1502
3151
|
currentSignal = turn.signal;
|
|
3152
|
+
currentAborts = abortReasonClassifier(turn.signal);
|
|
1503
3153
|
controlPlaneError = undefined;
|
|
1504
3154
|
let result = resultForSnapshot(turn.signal);
|
|
1505
3155
|
let settlement = result;
|
|
1506
3156
|
const failures = [];
|
|
1507
3157
|
try {
|
|
1508
3158
|
await emitTrace('boss.input.received', { text: turn.text }, { turnId });
|
|
3159
|
+
// A boundary entered aborted records the attempted input, then refuses
|
|
3160
|
+
// delivery before deterministic mapping or the classifier can perform
|
|
3161
|
+
// any host-visible work (DR-036 §5).
|
|
3162
|
+
turn.signal.throwIfAborted();
|
|
1509
3163
|
if (turn.text.trim().length === 0) {
|
|
1510
3164
|
const state = currentState();
|
|
1511
3165
|
result = { outcome: 'no-action', state };
|
|
@@ -1514,24 +3168,90 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1514
3168
|
const event = await classify(turn.text, turn.signal);
|
|
1515
3169
|
if (!event) {
|
|
1516
3170
|
const state = currentState();
|
|
1517
|
-
|
|
3171
|
+
if (!hasUnresolvedReconciliation()) {
|
|
3172
|
+
await emitBoundaryStatus('No playbook action classified.', state);
|
|
3173
|
+
}
|
|
1518
3174
|
result = { outcome: 'no-action', state };
|
|
1519
3175
|
}
|
|
1520
3176
|
else if (event.type === 'NO_ACTION') {
|
|
1521
3177
|
result = { outcome: 'no-action', state: currentState() };
|
|
1522
3178
|
}
|
|
1523
3179
|
else {
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
3180
|
+
const before = currentState();
|
|
3181
|
+
const boundCommitWait = deferredEffects !== undefined &&
|
|
3182
|
+
deferredOperationId !== undefined &&
|
|
3183
|
+
before.activeStateIds.includes('awaitBossReply');
|
|
3184
|
+
if (boundCommitWait && event.type === 'BOSS_INTERRUPT') {
|
|
3185
|
+
await parkDeferredContinuation(turn.signal);
|
|
3186
|
+
result = { outcome: 'no-action', state: currentState() };
|
|
3187
|
+
}
|
|
3188
|
+
else {
|
|
3189
|
+
const prepared = boundCommitWait &&
|
|
3190
|
+
event.type === 'BOSS_REPLY' &&
|
|
3191
|
+
event.questionId === 'commitCoderProposal'
|
|
3192
|
+
? await prepareDeferredContinuation(turn.signal, event)
|
|
3193
|
+
: undefined;
|
|
3194
|
+
if (prepared?.proceed === false) {
|
|
3195
|
+
result = { outcome: 'no-action', state: currentState() };
|
|
3196
|
+
}
|
|
3197
|
+
else {
|
|
3198
|
+
await emitBoundaryStatus(event.type, before);
|
|
3199
|
+
if (actor.getSnapshot().status === 'done') {
|
|
3200
|
+
stopActor();
|
|
3201
|
+
startActor();
|
|
3202
|
+
}
|
|
3203
|
+
const deferredMachineCheckpoint = prepared?.proceed === true
|
|
3204
|
+
? detachPersistedMachineSnapshot(actor.getPersistedSnapshot())
|
|
3205
|
+
: undefined;
|
|
3206
|
+
if (deferredMachineCheckpoint !== undefined) {
|
|
3207
|
+
suppressInspectionEmissions = true;
|
|
3208
|
+
}
|
|
3209
|
+
try {
|
|
3210
|
+
actor.send(event);
|
|
3211
|
+
await driveToQuiescence();
|
|
3212
|
+
await drainBoundaryCallsAndEmissions();
|
|
3213
|
+
await prepared?.acknowledgement;
|
|
3214
|
+
if (deferredMachineCheckpoint !== undefined &&
|
|
3215
|
+
hiddenDeferredOperationId !== undefined) {
|
|
3216
|
+
stopActor();
|
|
3217
|
+
createRuntimeActor(deferredMachineCheckpoint);
|
|
3218
|
+
actor.start();
|
|
3219
|
+
previousState = before;
|
|
3220
|
+
suppressInspectionEmissions = false;
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
catch (error) {
|
|
3224
|
+
activeDeferredContinuation?.result.reject(error);
|
|
3225
|
+
if (deferredEffects !== undefined) {
|
|
3226
|
+
try {
|
|
3227
|
+
synchronizeDeferredProjection(readEffectLedger());
|
|
3228
|
+
hiddenDeferredOperationId ??= deferredOperationId;
|
|
3229
|
+
}
|
|
3230
|
+
catch {
|
|
3231
|
+
hiddenDeferredOperationId ??= deferredOperationId;
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
if (deferredMachineCheckpoint !== undefined) {
|
|
3235
|
+
try {
|
|
3236
|
+
stopActor();
|
|
3237
|
+
createRuntimeActor(deferredMachineCheckpoint);
|
|
3238
|
+
actor.start();
|
|
3239
|
+
previousState = before;
|
|
3240
|
+
}
|
|
3241
|
+
finally {
|
|
3242
|
+
suppressInspectionEmissions = false;
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
throw error;
|
|
3246
|
+
}
|
|
3247
|
+
finally {
|
|
3248
|
+
activeDeferredContinuation = undefined;
|
|
3249
|
+
}
|
|
3250
|
+
if (controlPlaneError !== undefined)
|
|
3251
|
+
throw controlPlaneError;
|
|
3252
|
+
result = resultForSnapshot(turn.signal);
|
|
3253
|
+
}
|
|
1528
3254
|
}
|
|
1529
|
-
actor.send(event);
|
|
1530
|
-
await driveToQuiescence();
|
|
1531
|
-
await drainBoundaryCallsAndEmissions();
|
|
1532
|
-
if (controlPlaneError !== undefined)
|
|
1533
|
-
throw controlPlaneError;
|
|
1534
|
-
result = resultForSnapshot(turn.signal);
|
|
1535
3255
|
}
|
|
1536
3256
|
}
|
|
1537
3257
|
settlement = {
|
|
@@ -1541,16 +3261,19 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1541
3261
|
}
|
|
1542
3262
|
catch (error) {
|
|
1543
3263
|
const primaryError = controlPlaneError;
|
|
3264
|
+
// Only a rejection that is the exact abort reason settles as the
|
|
3265
|
+
// cancellation; a distinct failure observed while the signal is
|
|
3266
|
+
// aborted remains a control error (slc/link.md §Abort).
|
|
1544
3267
|
if (primaryError !== undefined) {
|
|
1545
3268
|
collectFailure(failures, primaryError);
|
|
1546
3269
|
}
|
|
1547
|
-
else if (!turn.signal
|
|
3270
|
+
else if (!isAbortFailure(error, turn.signal)) {
|
|
1548
3271
|
collectFailure(failures, error);
|
|
1549
3272
|
}
|
|
1550
3273
|
const state = currentState();
|
|
1551
3274
|
const effectiveError = primaryError ?? error;
|
|
1552
3275
|
result =
|
|
1553
|
-
turn.signal
|
|
3276
|
+
isAbortFailure(error, turn.signal) && primaryError === undefined
|
|
1554
3277
|
? resultForSnapshot(turn.signal)
|
|
1555
3278
|
: {
|
|
1556
3279
|
outcome: 'failed',
|
|
@@ -1571,12 +3294,14 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1571
3294
|
catch (error) {
|
|
1572
3295
|
const primaryError = controlPlaneError;
|
|
1573
3296
|
const effectiveError = primaryError ?? error;
|
|
1574
|
-
|
|
3297
|
+
// A drain rejection that is the exact abort reason evidences the
|
|
3298
|
+
// cancellation, not a control-plane failure (slc/link.md §Abort).
|
|
3299
|
+
const drainAborted = isAbortFailure(effectiveError, turn.signal);
|
|
3300
|
+
if (!drainAborted)
|
|
3301
|
+
collectFailure(failures, effectiveError);
|
|
1575
3302
|
const state = currentState();
|
|
1576
3303
|
result = {
|
|
1577
|
-
outcome:
|
|
1578
|
-
? 'aborted'
|
|
1579
|
-
: 'failed',
|
|
3304
|
+
outcome: drainAborted ? 'aborted' : 'failed',
|
|
1580
3305
|
state,
|
|
1581
3306
|
error: normalizeErrorFull(effectiveError) ?? {
|
|
1582
3307
|
name: 'Error',
|
|
@@ -1585,21 +3310,31 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1585
3310
|
};
|
|
1586
3311
|
settlement = { ...result, ...stateIdentity(state) };
|
|
1587
3312
|
}
|
|
1588
|
-
currentSignal = undefined;
|
|
1589
3313
|
try {
|
|
1590
3314
|
await emitTrace('boss.input.settled', settlement, { turnId });
|
|
1591
3315
|
}
|
|
1592
3316
|
catch (error) {
|
|
1593
|
-
|
|
3317
|
+
// A settlement-trace rejection that is the exact abort reason also
|
|
3318
|
+
// evidences the cancellation (slc/link.md §Abort).
|
|
3319
|
+
if (!isAbortFailure(error, turn.signal)) {
|
|
3320
|
+
collectFailure(failures, error);
|
|
3321
|
+
}
|
|
1594
3322
|
}
|
|
1595
3323
|
try {
|
|
1596
3324
|
await flush();
|
|
1597
3325
|
}
|
|
1598
3326
|
catch (error) {
|
|
1599
|
-
|
|
3327
|
+
// A late flush rejection that is the exact abort reason likewise
|
|
3328
|
+
// evidences the cancellation; the settled result already labels
|
|
3329
|
+
// the turn aborted then (slc/link.md §Abort).
|
|
3330
|
+
if (!isAbortFailure(error, turn.signal)) {
|
|
3331
|
+
collectFailure(failures, error);
|
|
3332
|
+
}
|
|
1600
3333
|
}
|
|
1601
3334
|
finally {
|
|
1602
3335
|
const primaryError = controlPlaneError;
|
|
3336
|
+
currentSignal = undefined;
|
|
3337
|
+
currentAborts = undefined;
|
|
1603
3338
|
currentTurnId = undefined;
|
|
1604
3339
|
controlPlaneError = undefined;
|
|
1605
3340
|
if (primaryError !== undefined)
|
|
@@ -1623,8 +3358,13 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1623
3358
|
if (currentSignal !== undefined) {
|
|
1624
3359
|
throw new Error('decide runtime: another runtime turn is active');
|
|
1625
3360
|
}
|
|
3361
|
+
refreshReconciliationProjection();
|
|
3362
|
+
if (hasUnresolvedReconciliation()) {
|
|
3363
|
+
return { outcome: 'no-action', state: currentState() };
|
|
3364
|
+
}
|
|
1626
3365
|
currentTurnId = playbookCallTurnIds.get(callId);
|
|
1627
3366
|
currentSignal = signal;
|
|
3367
|
+
currentAborts = abortReasonClassifier(signal);
|
|
1628
3368
|
controlPlaneError = undefined;
|
|
1629
3369
|
let runResult;
|
|
1630
3370
|
let operationError;
|
|
@@ -1654,14 +3394,50 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1654
3394
|
catch (error) {
|
|
1655
3395
|
drainError = error;
|
|
1656
3396
|
}
|
|
1657
|
-
const
|
|
3397
|
+
const aborts = currentAborts ?? abortReasonClassifier(signal);
|
|
3398
|
+
// The control latch has already classified its failure as distinct
|
|
3399
|
+
// under the operation that owned it. Only still-unclassified drain and
|
|
3400
|
+
// operation candidates may be cancellation evidence for this resume.
|
|
3401
|
+
const controlFailure = controlPlaneError;
|
|
3402
|
+
const drainAbort = controlFailure === undefined &&
|
|
3403
|
+
drainError !== undefined &&
|
|
3404
|
+
aborts.isAbortReason(drainError);
|
|
3405
|
+
const operationAbort = controlFailure === undefined &&
|
|
3406
|
+
operationError !== undefined &&
|
|
3407
|
+
aborts.isAbortReason(operationError);
|
|
3408
|
+
const abortEvidence = (drainAbort ? drainError : undefined) ??
|
|
3409
|
+
(operationAbort ? operationError : undefined);
|
|
3410
|
+
const failure = controlFailure ??
|
|
3411
|
+
(drainAbort ? undefined : drainError) ??
|
|
3412
|
+
(operationAbort ? undefined : operationError);
|
|
1658
3413
|
currentSignal = undefined;
|
|
3414
|
+
currentAborts = undefined;
|
|
1659
3415
|
currentTurnId = undefined;
|
|
1660
3416
|
controlPlaneError = undefined;
|
|
1661
3417
|
if (failure !== undefined)
|
|
1662
3418
|
throw failure;
|
|
3419
|
+
if (abortEvidence !== undefined &&
|
|
3420
|
+
runResult?.outcome !== 'terminal' &&
|
|
3421
|
+
runResult?.outcome !== 'suspended') {
|
|
3422
|
+
const state = currentState();
|
|
3423
|
+
runResult = {
|
|
3424
|
+
outcome: 'aborted',
|
|
3425
|
+
state,
|
|
3426
|
+
error: normalizeErrorFull(abortEvidence) ?? {
|
|
3427
|
+
name: 'AbortError',
|
|
3428
|
+
message: String(abortEvidence),
|
|
3429
|
+
},
|
|
3430
|
+
};
|
|
3431
|
+
}
|
|
1663
3432
|
if (runResult === undefined) {
|
|
1664
|
-
|
|
3433
|
+
if (signal.aborted) {
|
|
3434
|
+
// Every candidate was the abort's own evidence: settle on the
|
|
3435
|
+
// machine's state under the aborted boundary signal (DR-036 §4).
|
|
3436
|
+
runResult = resultForSnapshot(signal);
|
|
3437
|
+
}
|
|
3438
|
+
else {
|
|
3439
|
+
throw new Error('decide runtime: playbook resume produced no result');
|
|
3440
|
+
}
|
|
1665
3441
|
}
|
|
1666
3442
|
return runResult;
|
|
1667
3443
|
},
|
|
@@ -1717,13 +3493,30 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1717
3493
|
activeEmissionCalls.clear();
|
|
1718
3494
|
emissionQueue.clear();
|
|
1719
3495
|
judgeQueue.clear();
|
|
3496
|
+
semanticCompletionQueue.clear();
|
|
1720
3497
|
actor = undefined;
|
|
1721
3498
|
currentSignal = undefined;
|
|
3499
|
+
currentAborts = undefined;
|
|
3500
|
+
actorSettlementAborts.length = 0;
|
|
3501
|
+
actorSettlementErrorAborts = undefined;
|
|
1722
3502
|
currentTurnId = undefined;
|
|
1723
3503
|
ports = undefined;
|
|
1724
3504
|
sessionIdentity = undefined;
|
|
1725
3505
|
previousState = undefined;
|
|
1726
3506
|
controlPlaneError = undefined;
|
|
3507
|
+
governedOutputsByBoundaryId.clear();
|
|
3508
|
+
governedFailuresByBoundaryId.clear();
|
|
3509
|
+
governedEvidenceByBoundaryId.clear();
|
|
3510
|
+
governedReceiptsByBoundaryId.clear();
|
|
3511
|
+
pendingProposalCohort.clear();
|
|
3512
|
+
activeProposalCohort = undefined;
|
|
3513
|
+
completedProposalCohortTurnId = undefined;
|
|
3514
|
+
deferredOperationId = undefined;
|
|
3515
|
+
hiddenDeferredOperationId = undefined;
|
|
3516
|
+
unresolvedSemanticBoundaryIds.clear();
|
|
3517
|
+
appliedControlReceipts.clear();
|
|
3518
|
+
applyCallSequence = 0;
|
|
3519
|
+
activeDeferredContinuation = undefined;
|
|
1727
3520
|
disposed = true;
|
|
1728
3521
|
}
|
|
1729
3522
|
if (failures.length === 1)
|
|
@@ -1734,7 +3527,16 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1734
3527
|
})();
|
|
1735
3528
|
return disposalPromise;
|
|
1736
3529
|
},
|
|
3530
|
+
// @internal — test-only parity with the shared factory's bridge escape
|
|
3531
|
+
// hatch. This is hidden by the PlaybookRuntime return type.
|
|
3532
|
+
_getNestedBridge() {
|
|
3533
|
+
return nestedBridge;
|
|
3534
|
+
},
|
|
1737
3535
|
};
|
|
3536
|
+
}
|
|
3537
|
+
export const createPlaybookRuntime = (factoryInput) => {
|
|
3538
|
+
const construction = schema3Construction(factoryInput);
|
|
3539
|
+
return createDecidePlaybookRuntime(construction.configuredOptions, construction.hostCapabilities);
|
|
1738
3540
|
};
|
|
1739
3541
|
export const _internal = {
|
|
1740
3542
|
composePlayerPrompt,
|
|
@@ -1743,9 +3545,9 @@ export const _internal = {
|
|
|
1743
3545
|
buildClassifierPrompt,
|
|
1744
3546
|
parseClassification,
|
|
1745
3547
|
buildAdjudicatorPrompt,
|
|
1746
|
-
parseAdjudication,
|
|
1747
3548
|
combineSignals,
|
|
1748
3549
|
pendingQuestionsFromContext,
|
|
3550
|
+
pendingQuestionsForState,
|
|
1749
3551
|
normalizeErrorCompact,
|
|
1750
3552
|
normalizeErrorFull,
|
|
1751
3553
|
STATE_DESCRIPTIONS,
|
|
@@ -1753,6 +3555,7 @@ export const _internal = {
|
|
|
1753
3555
|
ROLE_STATE_IDS,
|
|
1754
3556
|
VERBATIM_PAYLOAD_FIELDS,
|
|
1755
3557
|
BOSS_INTERRUPT_TARGETS,
|
|
3558
|
+
UNFINISHED_FINAL_STATE_IDS,
|
|
1756
3559
|
CONTINUATION_PREAMBLE,
|
|
1757
3560
|
TELEMETRY_TOPIC,
|
|
1758
3561
|
};
|