@sublang/playbook 9.0.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/cli.md +51 -8
- package/docs/embedding.md +38 -12
- package/package.json +7 -3
- package/reference/sdlc/captain.md +14 -10
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +33 -13
- package/reference/sdlc/captain.playbook/captain.fsm.js +80 -9
- package/reference/sdlc/captain.playbook/captain.fsm.ts +137 -18
- package/reference/sdlc/captain.playbook/captain.gears.md +10 -6
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +5 -1
- package/reference/sdlc/captain.playbook/captain.playbook.js +140 -10
- package/reference/sdlc/captain.playbook/captain.playbook.ts +188 -16
- package/reference/sdlc/code.md +0 -1
- package/reference/sdlc/code.playbook/bin/interactive-session.js +170 -17
- package/reference/sdlc/code.playbook/bin/launch-config.js +136 -4
- package/reference/sdlc/code.playbook/bin/playbook.js +81 -4
- package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
- package/reference/sdlc/code.playbook/bin/run.js +365 -63
- package/reference/sdlc/code.playbook/bin/session-store.js +2877 -209
- package/reference/sdlc/code.playbook/code.fsm.d.ts +7 -0
- package/reference/sdlc/code.playbook/code.fsm.js +74 -25
- package/reference/sdlc/code.playbook/code.fsm.ts +83 -29
- package/reference/sdlc/code.playbook/code.gears.md +0 -2
- package/reference/sdlc/code.playbook/code.playbook.d.ts +5 -2
- package/reference/sdlc/code.playbook/code.playbook.js +54 -2
- package/reference/sdlc/code.playbook/code.playbook.ts +75 -6
- package/reference/sdlc/code.playbook/code.registry.d.ts +10 -3
- package/reference/sdlc/code.playbook/code.registry.js +10 -3
- package/reference/sdlc/code.playbook/code.registry.ts +23 -5
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +99 -7
- package/reference/sdlc/code.playbook/playbook-captain.js +1850 -72
- package/reference/sdlc/code.playbook/playbook-captain.ts +2759 -96
- package/reference/sdlc/decide.md +0 -1
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +7 -0
- package/reference/sdlc/decide.playbook/decide.fsm.js +80 -29
- package/reference/sdlc/decide.playbook/decide.fsm.ts +89 -31
- package/reference/sdlc/decide.playbook/decide.gears.md +0 -1
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +13 -5
- package/reference/sdlc/decide.playbook/decide.playbook.js +1712 -91
- package/reference/sdlc/decide.playbook/decide.playbook.ts +2677 -136
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +7 -3
- package/reference/sdlc/decide.playbook/decide.registry.js +10 -3
- package/reference/sdlc/decide.playbook/decide.registry.ts +20 -5
- package/reference/sdlc/review.playbook/review.fsm.d.ts +7 -0
- package/reference/sdlc/review.playbook/review.fsm.js +133 -12
- package/reference/sdlc/review.playbook/review.fsm.ts +140 -12
- package/reference/sdlc/review.playbook/review.playbook.d.ts +5 -2
- package/reference/sdlc/review.playbook/review.playbook.js +65 -2
- package/reference/sdlc/review.playbook/review.playbook.ts +83 -6
- package/reference/sdlc/review.playbook/review.registry.d.ts +10 -3
- package/reference/sdlc/review.playbook/review.registry.js +10 -3
- package/reference/sdlc/review.playbook/review.registry.ts +23 -5
- package/slc/gears2fsm.md +6 -5
- package/slc/link.md +544 -41
- package/src/accepted-outcome.d.ts +18 -0
- package/src/accepted-outcome.js +94 -0
- package/src/accepted-outcome.ts +140 -0
- package/src/runtime.d.ts +164 -3
- package/src/runtime.ts +213 -2
- package/src/xstate-playbook-runtime.d.ts +149 -10
- package/src/xstate-playbook-runtime.js +2569 -270
- package/src/xstate-playbook-runtime.ts +4133 -490
- package/src/xstate-runtime.d.ts +59 -1
- package/src/xstate-runtime.js +866 -7
- package/src/xstate-runtime.ts +1397 -7
|
@@ -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');
|
|
@@ -62,13 +64,64 @@ const ROLE_STATES = [
|
|
|
62
64
|
{ stateId: 'commitCoderProposal', role: 'coder', sourceItem: 'DECIDE-3' },
|
|
63
65
|
];
|
|
64
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
|
+
});
|
|
65
113
|
const ROLE_IDS = ['coder', 'reviewer'];
|
|
66
114
|
const ROLE_ID_SET = new Set(ROLE_IDS);
|
|
67
115
|
const roleLabel = (roleId) => roleId === 'coder' ? 'Coder' : 'Reviewer';
|
|
68
116
|
const BOSS_INTERRUPT_TARGETS = ['independentProposals'];
|
|
69
117
|
const BOSS_INTERRUPT_TARGET_IDS = new Set(BOSS_INTERRUPT_TARGETS);
|
|
118
|
+
const UNFINISHED_FINAL_STATE_IDS = new Set([
|
|
119
|
+
'reportedReviewFailure',
|
|
120
|
+
]);
|
|
70
121
|
const TELEMETRY_TOPIC = 'playbook.fsm.state';
|
|
71
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';
|
|
72
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.';
|
|
73
126
|
const PLACEHOLDER_FIELDS = [['<caller-topic>', 'callerTopic']];
|
|
74
127
|
const VERBATIM_PAYLOAD_FIELDS = new Set([
|
|
@@ -123,6 +176,27 @@ function requiredFieldsFor(description) {
|
|
|
123
176
|
}
|
|
124
177
|
return fields;
|
|
125
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
|
+
}
|
|
126
200
|
// LLM judges routinely wrap JSON in prose/fences or damage its tail. Match
|
|
127
201
|
// CODE's recovery contract: scan candidate starts in document order, prefer a
|
|
128
202
|
// strict balanced value at each position, then repair trailing commas and
|
|
@@ -194,6 +268,14 @@ function sortJson(value) {
|
|
|
194
268
|
function stableJson(value, path) {
|
|
195
269
|
return JSON.stringify(sortJson(snapshotJsonValue(value, path)));
|
|
196
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
|
+
}
|
|
197
279
|
function stripCodeFence(text) {
|
|
198
280
|
const fence = text.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/i);
|
|
199
281
|
return fence ? fence[1].trim() : text;
|
|
@@ -323,7 +405,8 @@ function parseClassification(raw, text, pendingQuestionIds = []) {
|
|
|
323
405
|
}
|
|
324
406
|
return null;
|
|
325
407
|
}
|
|
326
|
-
function buildAdjudicatorPrompt(input, playerOutput) {
|
|
408
|
+
function buildAdjudicatorPrompt(input, playerOutput, correction) {
|
|
409
|
+
const outcomes = governedOutcomesFor(input);
|
|
327
410
|
const lines = [];
|
|
328
411
|
lines.push('You are the guard adjudicator for a playbook state machine.');
|
|
329
412
|
lines.push('This is hidden control work. Do not call tools, inspect files, or ' +
|
|
@@ -339,54 +422,32 @@ function buildAdjudicatorPrompt(input, playerOutput) {
|
|
|
339
422
|
lines.push('');
|
|
340
423
|
lines.push('Guards (choose exactly one; the descriptions are authoritative and must be applied as written):');
|
|
341
424
|
for (const [guard, description] of Object.entries(input.result)) {
|
|
342
|
-
|
|
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}`);
|
|
343
429
|
}
|
|
344
|
-
|
|
345
|
-
for
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
runtimeOwnedFields.add(field);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
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) {
|
|
352
434
|
lines.push('');
|
|
353
|
-
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.');
|
|
354
441
|
}
|
|
355
|
-
lines.push('');
|
|
356
|
-
lines.push('Reply with a single JSON object: { "guard": "<one of the guard names above>", ...any payload fields the chosen guard description requires }.');
|
|
357
442
|
return lines.join('\n');
|
|
358
443
|
}
|
|
359
|
-
function
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
throw new Error('adjudicator returned empty or malformed JSON');
|
|
363
|
-
}
|
|
364
|
-
const guard = obj.guard;
|
|
365
|
-
if (!Object.prototype.hasOwnProperty.call(input.result, guard)) {
|
|
366
|
-
throw new Error(`adjudicator returned undeclared guard "${guard}" for ${input.sourceItem}`);
|
|
367
|
-
}
|
|
368
|
-
const requiredFields = requiredFieldsFor(input.result[guard]);
|
|
369
|
-
const allowedFields = new Set(['guard', ...requiredFields]);
|
|
370
|
-
for (const key of Reflect.ownKeys(obj)) {
|
|
371
|
-
if (typeof key !== 'string' || !allowedFields.has(key)) {
|
|
372
|
-
throw new Error(`adjudicator response for guard "${guard}" included undeclared field "${String(key)}"`);
|
|
373
|
-
}
|
|
444
|
+
function parseGovernedSemanticCandidate(raw) {
|
|
445
|
+
try {
|
|
446
|
+
return parseJudgeJson(raw);
|
|
374
447
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (VERBATIM_PAYLOAD_FIELDS.has(field)) {
|
|
378
|
-
output[field] = finalText;
|
|
379
|
-
continue;
|
|
380
|
-
}
|
|
381
|
-
const value = obj[field];
|
|
382
|
-
if (value === undefined ||
|
|
383
|
-
value === null ||
|
|
384
|
-
(typeof value === 'string' && value.trim() === '')) {
|
|
385
|
-
throw new Error(`adjudicator response for guard "${guard}" missing required field "${field}"`);
|
|
386
|
-
}
|
|
387
|
-
output[field] = value;
|
|
448
|
+
catch (error) {
|
|
449
|
+
throw new PlaybookSemanticCandidateStructureError(error instanceof Error ? error.message : 'reply is not valid JSON');
|
|
388
450
|
}
|
|
389
|
-
return output;
|
|
390
451
|
}
|
|
391
452
|
function combineSignals(a, b) {
|
|
392
453
|
return combineAbortSignals(a, b);
|
|
@@ -407,6 +468,125 @@ function normalizeErrorFull(err) {
|
|
|
407
468
|
function isEmptyFinalText(finalText) {
|
|
408
469
|
return finalText === undefined || finalText.trim().length === 0;
|
|
409
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
|
+
}
|
|
410
590
|
function isAbortFailure(error, signal) {
|
|
411
591
|
return signal.aborted && Object.is(error, signal.reason);
|
|
412
592
|
}
|
|
@@ -510,8 +690,8 @@ function normalizedTransitionEvent(event) {
|
|
|
510
690
|
}
|
|
511
691
|
return snapshotJsonValue(descriptor, 'FSM event');
|
|
512
692
|
}
|
|
513
|
-
function telemetryPayload(previousState, state, event, context) {
|
|
514
|
-
const pendingBossQuestions = pendingQuestionsForState(state, context);
|
|
693
|
+
function telemetryPayload(previousState, state, event, context, hiddenQuestionId) {
|
|
694
|
+
const pendingBossQuestions = pendingQuestionsForState(state, context).filter(({ questionId }) => questionId !== hiddenQuestionId);
|
|
515
695
|
const prior = previousState ?? state;
|
|
516
696
|
const payload = {
|
|
517
697
|
from: prior.value,
|
|
@@ -527,8 +707,13 @@ function telemetryPayload(previousState, state, event, context) {
|
|
|
527
707
|
assertJsonSafe(payload);
|
|
528
708
|
return payload;
|
|
529
709
|
}
|
|
530
|
-
|
|
710
|
+
function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
711
|
+
const automaticReplayPolicy = createAutomaticReplayPolicy(deferredEffects);
|
|
531
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);
|
|
532
717
|
let ports;
|
|
533
718
|
let sessionIdentity;
|
|
534
719
|
let actor;
|
|
@@ -545,6 +730,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
545
730
|
let judgeCallSequence = 0;
|
|
546
731
|
let playerCallSequence = 0;
|
|
547
732
|
let playbookCallSequence = 0;
|
|
733
|
+
let applyCallSequence = 0;
|
|
548
734
|
let lifecycleStarted = false;
|
|
549
735
|
let initInFlight;
|
|
550
736
|
let disposed = false;
|
|
@@ -558,6 +744,23 @@ export const createPlaybookRuntime = (options) => {
|
|
|
558
744
|
const activeEmissionCalls = new Set();
|
|
559
745
|
const emissionQueue = new PQueue({ concurrency: 1 });
|
|
560
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;
|
|
561
764
|
const collectFailure = (failures, error) => {
|
|
562
765
|
if (error instanceof AggregateError) {
|
|
563
766
|
for (const nested of error.errors)
|
|
@@ -647,6 +850,9 @@ export const createPlaybookRuntime = (options) => {
|
|
|
647
850
|
};
|
|
648
851
|
const bindSession = (nextSession) => {
|
|
649
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
|
+
}
|
|
650
856
|
if (bound.roleBindings === undefined)
|
|
651
857
|
return bound;
|
|
652
858
|
const actual = Object.keys(bound.roleBindings).sort();
|
|
@@ -779,15 +985,216 @@ export const createPlaybookRuntime = (options) => {
|
|
|
779
985
|
pendingCall,
|
|
780
986
|
});
|
|
781
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
|
+
};
|
|
782
1182
|
const stateIdentity = (state) => {
|
|
783
1183
|
return state.stateId === undefined ? {} : { stateId: state.stateId };
|
|
784
1184
|
};
|
|
785
1185
|
const enqueueTracedEmission = (type, payload, meta = {}, describedEmission, aborts) => {
|
|
786
|
-
const
|
|
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 = {}) => {
|
|
787
1194
|
const identity = requireSessionIdentity();
|
|
788
1195
|
const jsonPayload = snapshotJsonValue(payload, `trace ${type} payload`);
|
|
789
|
-
|
|
790
|
-
schemaVersion:
|
|
1196
|
+
return Object.freeze({
|
|
1197
|
+
schemaVersion: 4,
|
|
791
1198
|
sessionId: identity.sessionId,
|
|
792
1199
|
playbookId: identity.playbookId,
|
|
793
1200
|
rootSessionId: identity.rootSessionId,
|
|
@@ -805,12 +1212,25 @@ export const createPlaybookRuntime = (options) => {
|
|
|
805
1212
|
...(meta.callId !== undefined ? { callId: meta.callId } : {}),
|
|
806
1213
|
payload: jsonPayload,
|
|
807
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 });
|
|
808
1221
|
return enqueue(async () => {
|
|
809
|
-
|
|
810
|
-
await
|
|
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);
|
|
811
1232
|
}, aborts);
|
|
812
1233
|
};
|
|
813
|
-
const emitTrace = (type, payload, meta = {}, aborts) => enqueueTracedEmission(type, payload, meta, undefined, aborts);
|
|
814
1234
|
const emitBoundaryStatus = async (message, state) => {
|
|
815
1235
|
const bossRelevantStateIds = state.activeStateIds.filter((stateId) => STATUS_STATE_IDS.has(stateId));
|
|
816
1236
|
await enqueueTracedEmission('status.emitted', {
|
|
@@ -896,7 +1316,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
896
1316
|
return queued;
|
|
897
1317
|
};
|
|
898
1318
|
const callJudge = (prompt, signal, purpose, callStateId) => trackBoundaryCall(runJudgeCall(prompt, signal, purpose, callStateId));
|
|
899
|
-
const runPlayerCall = async (input, signal) => {
|
|
1319
|
+
const runPlayerCall = async (input, signal, continuation) => {
|
|
900
1320
|
const aborts = abortReasonClassifier(signal);
|
|
901
1321
|
if (!ROLE_ID_SET.has(input.role)) {
|
|
902
1322
|
throw new TypeError(`DECIDE player input role must name a declared local role`);
|
|
@@ -908,13 +1328,17 @@ export const createPlaybookRuntime = (options) => {
|
|
|
908
1328
|
let resume;
|
|
909
1329
|
try {
|
|
910
1330
|
signal.throwIfAborted();
|
|
911
|
-
resume =
|
|
1331
|
+
resume =
|
|
1332
|
+
continuation !== undefined &&
|
|
1333
|
+
Object.prototype.hasOwnProperty.call(continuation, 'resume')
|
|
1334
|
+
? continuation.resume
|
|
1335
|
+
: selectPlayerResume(roleId, playerId);
|
|
912
1336
|
}
|
|
913
1337
|
catch (error) {
|
|
914
1338
|
latchControlPlaneError(error, signal);
|
|
915
1339
|
throw error;
|
|
916
1340
|
}
|
|
917
|
-
const callId = `player-${++playerCallSequence}`;
|
|
1341
|
+
const callId = continuation?.callId ?? `player-${++playerCallSequence}`;
|
|
918
1342
|
const identity = {
|
|
919
1343
|
stateId: input.stateId,
|
|
920
1344
|
sourceItem: input.sourceItem,
|
|
@@ -1013,6 +1437,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1013
1437
|
return {
|
|
1014
1438
|
roleId,
|
|
1015
1439
|
...(playerId === undefined ? {} : { playerId }),
|
|
1440
|
+
callId,
|
|
1016
1441
|
result,
|
|
1017
1442
|
};
|
|
1018
1443
|
}
|
|
@@ -1020,8 +1445,590 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1020
1445
|
inFlightPlayerKeys.delete(playerKey);
|
|
1021
1446
|
}
|
|
1022
1447
|
};
|
|
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
|
+
};
|
|
1468
|
+
};
|
|
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 = [];
|
|
1843
|
+
try {
|
|
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
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
catch (error) {
|
|
1943
|
+
for (const member of Object.values(members))
|
|
1944
|
+
member?.result.reject(error);
|
|
1945
|
+
}
|
|
1946
|
+
finally {
|
|
1947
|
+
completedProposalCohortTurnId = turnId;
|
|
1948
|
+
pendingProposalCohort.clear();
|
|
1949
|
+
activeProposalCohort = undefined;
|
|
1950
|
+
}
|
|
1951
|
+
};
|
|
1952
|
+
const queueProposalCohortMember = (input, signal) => {
|
|
1953
|
+
if (pendingProposalCohort.has(input.role)) {
|
|
1954
|
+
throw new Error(`DECIDE proposal cohort already registered role ${input.role}`);
|
|
1955
|
+
}
|
|
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();
|
|
1970
|
+
}
|
|
1971
|
+
return member.result.promise;
|
|
1972
|
+
};
|
|
1023
1973
|
const callPlayer = (input, signal) => {
|
|
1024
|
-
|
|
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());
|
|
1025
2032
|
};
|
|
1026
2033
|
const player = fromPromise(async ({ input, signal }) => {
|
|
1027
2034
|
const combined = combineSignals(signal, currentSignal);
|
|
@@ -1039,8 +2046,10 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1039
2046
|
throw error;
|
|
1040
2047
|
}
|
|
1041
2048
|
combined.throwIfAborted();
|
|
1042
|
-
let { roleId, playerId, result } = await callPlayer(input, combined);
|
|
1043
|
-
if (result.status === 'ok' &&
|
|
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)) {
|
|
1044
2053
|
// DR-028: an `ok` result whose finalText is missing, empty, or
|
|
1045
2054
|
// whitespace-only earns exactly one corrective re-ask — the same
|
|
1046
2055
|
// composed call repeated, traced by runPlayerCall as its own
|
|
@@ -1050,7 +2059,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1050
2059
|
// are never retried), and a rejecting finish emission rejects
|
|
1051
2060
|
// `callPlayer` itself, so it never reaches this branch (PBRT-47).
|
|
1052
2061
|
combined.throwIfAborted();
|
|
1053
|
-
({ roleId, playerId, result } = await callPlayer(input, combined));
|
|
2062
|
+
({ roleId, playerId, callId, result } = await callPlayer(input, combined));
|
|
1054
2063
|
}
|
|
1055
2064
|
if (result.status !== 'ok') {
|
|
1056
2065
|
throw new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "${result.status}"${result.error ? `: ${result.error}` : ''}`);
|
|
@@ -1060,14 +2069,12 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1060
2069
|
throw new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "ok" with no finalText`);
|
|
1061
2070
|
}
|
|
1062
2071
|
combined.throwIfAborted();
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
catch (error) {
|
|
1068
|
-
latchControlPlaneError(error, combined);
|
|
1069
|
-
throw error;
|
|
2072
|
+
const governedOutput = governedPlayerOutputs.get(result);
|
|
2073
|
+
if (governedOutput !== undefined) {
|
|
2074
|
+
governedPlayerOutputs.delete(result);
|
|
2075
|
+
return governedOutput;
|
|
1070
2076
|
}
|
|
2077
|
+
throw new Error('DECIDE governed player result was not reconciled against repository evidence');
|
|
1071
2078
|
}
|
|
1072
2079
|
finally {
|
|
1073
2080
|
actorSettlementAborts.push(settlementAborts);
|
|
@@ -1142,19 +2149,37 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1142
2149
|
return aborts;
|
|
1143
2150
|
};
|
|
1144
2151
|
const inspect = (event) => {
|
|
1145
|
-
if (event.type !== '@xstate.snapshot')
|
|
1146
|
-
return;
|
|
1147
2152
|
if (actor === undefined || event.actorRef !== actor)
|
|
1148
2153
|
return;
|
|
1149
2154
|
if (suppressInspectionEmissions)
|
|
1150
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;
|
|
1151
2167
|
const settlementAborts = consumeActorSettlementAborts(true);
|
|
1152
2168
|
try {
|
|
1153
2169
|
const snapshot = event.snapshot;
|
|
1154
2170
|
const state = normalizePlaybookSnapshot(snapshot);
|
|
1155
2171
|
const prior = previousState ?? state;
|
|
2172
|
+
let acceptedOutcomes = [];
|
|
2173
|
+
try {
|
|
2174
|
+
acceptedOutcomes = acceptedOutcomeConsumer.confirm(previousState, state);
|
|
2175
|
+
}
|
|
2176
|
+
catch (error) {
|
|
2177
|
+
latchInspectionError(error);
|
|
2178
|
+
}
|
|
1156
2179
|
const context = snapshot.context;
|
|
1157
|
-
const fsmPayload = telemetryPayload(prior, state, event.event, context
|
|
2180
|
+
const fsmPayload = telemetryPayload(prior, state, event.event, context, hiddenDeferredOperationId === undefined
|
|
2181
|
+
? undefined
|
|
2182
|
+
: 'commitCoderProposal');
|
|
1158
2183
|
const describedFsmPayload = snapshotJsonValue(fsmPayload, 'described FSM telemetry');
|
|
1159
2184
|
void enqueueTracedEmission('fsm.transition', fsmPayload, { turnId: currentTurnId }, (emissionPorts) => emissionPorts.emitTelemetry({
|
|
1160
2185
|
topic: TELEMETRY_TOPIC,
|
|
@@ -1162,7 +2187,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1162
2187
|
}), settlementAborts).catch(() => undefined);
|
|
1163
2188
|
const priorIds = new Set(previousState?.activeStateIds ?? []);
|
|
1164
2189
|
previousState = state;
|
|
1165
|
-
const pendingQuestions =
|
|
2190
|
+
const pendingQuestions = visiblePendingQuestionsForState(state, context);
|
|
1166
2191
|
const bossRelevantStateIds = state.activeStateIds.filter((stateId) => STATUS_STATE_IDS.has(stateId));
|
|
1167
2192
|
const scheduleStatus = (message, stateId, data) => {
|
|
1168
2193
|
const tracePayload = {
|
|
@@ -1174,6 +2199,9 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1174
2199
|
assertJsonSafe(tracePayload);
|
|
1175
2200
|
void enqueueTracedEmission('status.emitted', tracePayload, { turnId: currentTurnId }, (emissionPorts) => emissionPorts.emitStatus(message, data), settlementAborts).catch(() => undefined);
|
|
1176
2201
|
};
|
|
2202
|
+
for (const acceptedOutcome of acceptedOutcomes) {
|
|
2203
|
+
void enqueueAcceptedOutcomeEmission(acceptedOutcome, state, settlementAborts).catch(() => undefined);
|
|
2204
|
+
}
|
|
1177
2205
|
for (const activeStateId of state.activeStateIds) {
|
|
1178
2206
|
if (priorIds.has(activeStateId) ||
|
|
1179
2207
|
!STATUS_STATE_IDS.has(activeStateId)) {
|
|
@@ -1200,11 +2228,13 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1200
2228
|
}
|
|
1201
2229
|
}
|
|
1202
2230
|
catch (error) {
|
|
2231
|
+
acceptedOutcomeConsumer.reset();
|
|
1203
2232
|
latchInspectionError(error, settlementAborts);
|
|
1204
2233
|
}
|
|
1205
2234
|
};
|
|
1206
2235
|
const createRuntimeActor = (machineSnapshot) => {
|
|
1207
2236
|
previousState = undefined;
|
|
2237
|
+
acceptedOutcomeConsumer.reset();
|
|
1208
2238
|
// DR-014 §1: a restore rehydrates the persisted machine snapshot;
|
|
1209
2239
|
// XState derives context/value from it and ignores `input` then.
|
|
1210
2240
|
actor = createActor(providedMachine, {
|
|
@@ -1237,6 +2267,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1237
2267
|
if (!actor)
|
|
1238
2268
|
return;
|
|
1239
2269
|
suppressInspectionEmissions = true;
|
|
2270
|
+
acceptedOutcomeConsumer.reset();
|
|
1240
2271
|
actor.stop();
|
|
1241
2272
|
};
|
|
1242
2273
|
const startActor = () => {
|
|
@@ -1255,16 +2286,21 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1255
2286
|
const live = actor;
|
|
1256
2287
|
if (!live)
|
|
1257
2288
|
throw new Error('decide runtime: actor is not initialized');
|
|
2289
|
+
refreshReconciliationProjection();
|
|
2290
|
+
if (hasUnresolvedReconciliation())
|
|
2291
|
+
return null;
|
|
1258
2292
|
const snapshot = live.getSnapshot();
|
|
1259
2293
|
const context = snapshot.context;
|
|
1260
2294
|
const state = normalizePlaybookSnapshot(snapshot, {
|
|
1261
2295
|
pendingCall: nestedBridge.getPendingCall(),
|
|
1262
2296
|
});
|
|
1263
|
-
const pendingQuestions =
|
|
2297
|
+
const pendingQuestions = visiblePendingQuestionsForState(state, context);
|
|
2298
|
+
const failed = state.activeStateIds.includes('failed');
|
|
1264
2299
|
if (pendingQuestions.length === 0 &&
|
|
1265
2300
|
(snapshot.status === 'done' ||
|
|
1266
2301
|
state.activeStateIds.includes('ready') ||
|
|
1267
|
-
|
|
2302
|
+
(failed &&
|
|
2303
|
+
automaticReplayPolicy.allowsFailureStateRetry()))) {
|
|
1268
2304
|
return { type: 'START_DECIDE', callerTopic: text };
|
|
1269
2305
|
}
|
|
1270
2306
|
if (pendingQuestions.length === 0)
|
|
@@ -1276,6 +2312,458 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1276
2312
|
const raw = await callJudge(prompt, signal, 'boss-input-classification', state.stateId);
|
|
1277
2313
|
return parseClassification(raw, text, pendingQuestions.map(({ questionId }) => questionId));
|
|
1278
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
|
+
};
|
|
1279
2767
|
const resultForSnapshot = (signal) => {
|
|
1280
2768
|
const live = actor;
|
|
1281
2769
|
if (!live)
|
|
@@ -1399,6 +2887,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1399
2887
|
activeEmissionCalls.clear();
|
|
1400
2888
|
emissionQueue.clear();
|
|
1401
2889
|
judgeQueue.clear();
|
|
2890
|
+
semanticCompletionQueue.clear();
|
|
1402
2891
|
actor = undefined;
|
|
1403
2892
|
currentSignal = undefined;
|
|
1404
2893
|
currentAborts = undefined;
|
|
@@ -1416,7 +2905,20 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1416
2905
|
judgeCallSequence = 0;
|
|
1417
2906
|
playerCallSequence = 0;
|
|
1418
2907
|
playbookCallSequence = 0;
|
|
2908
|
+
applyCallSequence = 0;
|
|
1419
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;
|
|
1420
2922
|
lifecycleStarted = false;
|
|
1421
2923
|
};
|
|
1422
2924
|
return {
|
|
@@ -1437,6 +2939,10 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1437
2939
|
ports = identity.ports;
|
|
1438
2940
|
sessionIdentity = identity;
|
|
1439
2941
|
try {
|
|
2942
|
+
if (deferredEffects !== undefined) {
|
|
2943
|
+
effectLedgerMirror = readEffectLedger();
|
|
2944
|
+
synchronizeDeferredProjection(effectLedgerMirror);
|
|
2945
|
+
}
|
|
1440
2946
|
suppressInspectionEmissions = false;
|
|
1441
2947
|
createRuntimeActor();
|
|
1442
2948
|
const state = currentState();
|
|
@@ -1457,6 +2963,9 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1457
2963
|
initInFlight = undefined;
|
|
1458
2964
|
}
|
|
1459
2965
|
},
|
|
2966
|
+
describe: describeControlView,
|
|
2967
|
+
unresolvedEffectEnvelopes: unresolvedEffectEnvelopeIdentities,
|
|
2968
|
+
apply: applyControlAction,
|
|
1460
2969
|
// DR-014 §1 / DR-031 §5 / PBRT-45: JSON-safe capture of a parked
|
|
1461
2970
|
// session, including one already-started suspended REVIEW call.
|
|
1462
2971
|
// Defined only at a safe capture point — initialized, not disposing
|
|
@@ -1504,8 +3013,12 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1504
3013
|
return undefined;
|
|
1505
3014
|
const machine = detachPersistedMachineSnapshot(actor.getPersistedSnapshot());
|
|
1506
3015
|
const context = actor.getSnapshot().context;
|
|
3016
|
+
if (deferredEffects !== undefined) {
|
|
3017
|
+
synchronizeDeferredProjection(readEffectLedger());
|
|
3018
|
+
}
|
|
3019
|
+
const unresolved = hasUnresolvedReconciliation();
|
|
1507
3020
|
return {
|
|
1508
|
-
schemaVersion:
|
|
3021
|
+
schemaVersion: 4,
|
|
1509
3022
|
playbookId: sessionIdentity.playbookId,
|
|
1510
3023
|
machine,
|
|
1511
3024
|
roleResumeTokens: snapshotRoleResumeTokens(),
|
|
@@ -1517,12 +3030,15 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1517
3030
|
playbookCall: playbookCallSequence,
|
|
1518
3031
|
},
|
|
1519
3032
|
state,
|
|
1520
|
-
pendingBossQuestions:
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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,
|
|
1526
3042
|
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
1527
3043
|
};
|
|
1528
3044
|
},
|
|
@@ -1540,6 +3056,21 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1540
3056
|
}
|
|
1541
3057
|
const identity = bindSession(session);
|
|
1542
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
|
+
}
|
|
1543
3074
|
const suspendedCall = boundSnapshot.suspendedCall;
|
|
1544
3075
|
let finishInitialization;
|
|
1545
3076
|
const initialization = new Promise((resolve) => {
|
|
@@ -1556,6 +3087,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1556
3087
|
judgeCallSequence = boundSnapshot.sequences.judgeCall;
|
|
1557
3088
|
playerCallSequence = boundSnapshot.sequences.playerCall;
|
|
1558
3089
|
playbookCallSequence = boundSnapshot.sequences.playbookCall;
|
|
3090
|
+
applyCallSequence = boundSnapshot.sequences.trace;
|
|
1559
3091
|
if (identity.playerSessions) {
|
|
1560
3092
|
priorExternalRoleTokens = snapshotRoleResumeTokens();
|
|
1561
3093
|
}
|
|
@@ -1564,6 +3096,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1564
3096
|
if (suspendedCall !== undefined) {
|
|
1565
3097
|
playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
|
|
1566
3098
|
}
|
|
3099
|
+
synchronizeDeferredProjection(effectLedgerMirror);
|
|
1567
3100
|
suppressInspectionEmissions = true;
|
|
1568
3101
|
createRuntimeActor(boundSnapshot.machine);
|
|
1569
3102
|
actor?.start();
|
|
@@ -1635,24 +3168,90 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1635
3168
|
const event = await classify(turn.text, turn.signal);
|
|
1636
3169
|
if (!event) {
|
|
1637
3170
|
const state = currentState();
|
|
1638
|
-
|
|
3171
|
+
if (!hasUnresolvedReconciliation()) {
|
|
3172
|
+
await emitBoundaryStatus('No playbook action classified.', state);
|
|
3173
|
+
}
|
|
1639
3174
|
result = { outcome: 'no-action', state };
|
|
1640
3175
|
}
|
|
1641
3176
|
else if (event.type === 'NO_ACTION') {
|
|
1642
3177
|
result = { outcome: 'no-action', state: currentState() };
|
|
1643
3178
|
}
|
|
1644
3179
|
else {
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
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
|
+
}
|
|
1649
3254
|
}
|
|
1650
|
-
actor.send(event);
|
|
1651
|
-
await driveToQuiescence();
|
|
1652
|
-
await drainBoundaryCallsAndEmissions();
|
|
1653
|
-
if (controlPlaneError !== undefined)
|
|
1654
|
-
throw controlPlaneError;
|
|
1655
|
-
result = resultForSnapshot(turn.signal);
|
|
1656
3255
|
}
|
|
1657
3256
|
}
|
|
1658
3257
|
settlement = {
|
|
@@ -1759,6 +3358,10 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1759
3358
|
if (currentSignal !== undefined) {
|
|
1760
3359
|
throw new Error('decide runtime: another runtime turn is active');
|
|
1761
3360
|
}
|
|
3361
|
+
refreshReconciliationProjection();
|
|
3362
|
+
if (hasUnresolvedReconciliation()) {
|
|
3363
|
+
return { outcome: 'no-action', state: currentState() };
|
|
3364
|
+
}
|
|
1762
3365
|
currentTurnId = playbookCallTurnIds.get(callId);
|
|
1763
3366
|
currentSignal = signal;
|
|
1764
3367
|
currentAborts = abortReasonClassifier(signal);
|
|
@@ -1890,6 +3493,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1890
3493
|
activeEmissionCalls.clear();
|
|
1891
3494
|
emissionQueue.clear();
|
|
1892
3495
|
judgeQueue.clear();
|
|
3496
|
+
semanticCompletionQueue.clear();
|
|
1893
3497
|
actor = undefined;
|
|
1894
3498
|
currentSignal = undefined;
|
|
1895
3499
|
currentAborts = undefined;
|
|
@@ -1900,6 +3504,19 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1900
3504
|
sessionIdentity = undefined;
|
|
1901
3505
|
previousState = undefined;
|
|
1902
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;
|
|
1903
3520
|
disposed = true;
|
|
1904
3521
|
}
|
|
1905
3522
|
if (failures.length === 1)
|
|
@@ -1916,6 +3533,10 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1916
3533
|
return nestedBridge;
|
|
1917
3534
|
},
|
|
1918
3535
|
};
|
|
3536
|
+
}
|
|
3537
|
+
export const createPlaybookRuntime = (factoryInput) => {
|
|
3538
|
+
const construction = schema3Construction(factoryInput);
|
|
3539
|
+
return createDecidePlaybookRuntime(construction.configuredOptions, construction.hostCapabilities);
|
|
1919
3540
|
};
|
|
1920
3541
|
export const _internal = {
|
|
1921
3542
|
composePlayerPrompt,
|
|
@@ -1924,7 +3545,6 @@ export const _internal = {
|
|
|
1924
3545
|
buildClassifierPrompt,
|
|
1925
3546
|
parseClassification,
|
|
1926
3547
|
buildAdjudicatorPrompt,
|
|
1927
|
-
parseAdjudication,
|
|
1928
3548
|
combineSignals,
|
|
1929
3549
|
pendingQuestionsFromContext,
|
|
1930
3550
|
pendingQuestionsForState,
|
|
@@ -1935,6 +3555,7 @@ export const _internal = {
|
|
|
1935
3555
|
ROLE_STATE_IDS,
|
|
1936
3556
|
VERBATIM_PAYLOAD_FIELDS,
|
|
1937
3557
|
BOSS_INTERRUPT_TARGETS,
|
|
3558
|
+
UNFINISHED_FINAL_STATE_IDS,
|
|
1938
3559
|
CONTINUATION_PREAMBLE,
|
|
1939
3560
|
TELEMETRY_TOPIC,
|
|
1940
3561
|
};
|