@sublang/playbook 15.0.0 → 16.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/embedding.md +9 -7
- package/package.json +3 -1
- package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
- package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
- package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
- package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
- package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
- package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
- package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
- package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
- package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
- package/reference/sdlc/code.playbook/bin/repository-effects.js +54 -13
- package/reference/sdlc/code.playbook/code.fsm.d.ts +143 -90
- package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
- package/reference/sdlc/code.playbook/code.fsm.js +609 -649
- package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
- package/reference/sdlc/code.playbook/code.gears.md +39 -29
- package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
- package/reference/sdlc/code.playbook/code.playbook.js +166 -147
- package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
- package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
- package/reference/sdlc/code.playbook/code.registry.ts +6 -2
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +42 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +829 -194
- package/reference/sdlc/code.playbook/playbook-captain.ts +1227 -220
- package/reference/sdlc/decide.md +1 -0
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +270 -154
- package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
- package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
- package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -67
- package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3570
- package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5061
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
- package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
- package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +113 -131
- package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
- package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
- package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
- package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
- package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
- package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
- package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
- package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +92 -85
- package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
- package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
- package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
- package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
- package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
- package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
- package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
- package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
- package/reference/sdlc/review.md +3 -3
- package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
- package/reference/sdlc/review.playbook/review.fsm.js +479 -550
- package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
- package/reference/sdlc/review.playbook/review.gears.md +32 -29
- package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
- package/reference/sdlc/review.playbook/review.playbook.js +180 -155
- package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
- package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
- package/reference/sdlc/review.playbook/review.registry.js +3 -3
- package/reference/sdlc/review.playbook/review.registry.ts +9 -5
- package/slc/gears2fsm.md +72 -3
- package/slc/link.md +50 -27
- package/slc/materialize-link.mjs +51 -13
- package/slc/optimize.md +4 -1
- package/slc/prefix-prompts.mjs +191 -0
- package/slc/prefix.md +203 -0
- package/slc/slc.pin-inputs.json +11 -1
- package/slc/text2gears.md +71 -11
- package/src/xstate-playbook-runtime.d.ts +82 -9
- package/src/xstate-playbook-runtime.js +1224 -283
- package/src/xstate-playbook-runtime.ts +1671 -338
- package/src/xstate-runtime.d.ts +3 -1
- package/src/xstate-runtime.js +9 -2
- package/src/xstate-runtime.ts +12 -2
|
@@ -1,3602 +1,187 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// Abort strategy: natural rejection; every player-invoking state's
|
|
15
|
-
// onError routes to the quiescent failed state.
|
|
16
|
-
// Output profile: bespoke parallel runtime with the shared nested-call
|
|
17
|
-
// bridge (slc/link.md §Output)
|
|
18
|
-
import { randomUUID } from 'node:crypto';
|
|
19
|
-
import PQueue from 'p-queue';
|
|
20
|
-
import { createActor, fromPromise } from 'xstate';
|
|
21
|
-
import { createAcceptedOutcomeConsumer, } from '../../../src/accepted-outcome.js';
|
|
22
|
-
import { assertJsonSafe, assertPlaybookEffectLedger, assertPlaybookRuntimeSnapshot, combineAbortSignals, composePlayerContinuation, createNestedPlaybookBridge, detachPersistedMachineSnapshot, normalizeError, normalizePlaybookSnapshot, PlaybookSemanticCandidateStructureError, reconcilePlaybookSemanticEvidence, renderGovernedOutcomeContract, snapshotJsonValue, snapshotPlaybookSession, terminalOutcomesFromMachine, validatePlayerResult, waitForPlaybookQuiescence, } from '../../../src/xstate-runtime.js';
|
|
23
|
-
import decideMachine from './decide.fsm.js';
|
|
24
|
-
function snapshotDecideRuntimeOptions(value) {
|
|
25
|
-
const captured = snapshotJsonValue(value, 'DECIDE runtime options');
|
|
26
|
-
if (!isPlainObject(captured)) {
|
|
27
|
-
throw new TypeError('DECIDE runtime options must be an object');
|
|
28
|
-
}
|
|
29
|
-
const [unknown] = Object.keys(captured);
|
|
30
|
-
if (unknown !== undefined) {
|
|
31
|
-
throw new TypeError(`DECIDE runtime options.${unknown} is not declared`);
|
|
32
|
-
}
|
|
33
|
-
return Object.freeze({});
|
|
34
|
-
}
|
|
35
|
-
function authoredStateDescriptions(states) {
|
|
36
|
-
const descriptions = {};
|
|
37
|
-
const visit = (children) => {
|
|
38
|
-
for (const state of Object.values(children ?? {})) {
|
|
39
|
-
const stateId = state.meta?.playbook?.stateId;
|
|
40
|
-
const description = state.meta?.playbook?.description;
|
|
41
|
-
if (typeof stateId === 'string' &&
|
|
42
|
-
typeof description === 'string' &&
|
|
43
|
-
description.trim().length > 0) {
|
|
44
|
-
const existing = descriptions[stateId];
|
|
45
|
-
if (existing !== undefined && existing !== description) {
|
|
46
|
-
throw new Error(`DECIDE state ${stateId} declares conflicting descriptions`);
|
|
47
|
-
}
|
|
48
|
-
descriptions[stateId] = description;
|
|
49
|
-
}
|
|
50
|
-
visit(state.states);
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
visit(states);
|
|
54
|
-
return Object.freeze(descriptions);
|
|
55
|
-
}
|
|
56
|
-
const STATE_DESCRIPTIONS = authoredStateDescriptions(decideMachine.config.states);
|
|
57
|
-
// DR-048: each root final state's compiled terminal kind, read from this
|
|
58
|
-
// artifact through the shared reader the factory-backed runtimes use.
|
|
59
|
-
const TERMINAL_KINDS = terminalOutcomesFromMachine(decideMachine, 'DECIDE');
|
|
60
|
-
const ROLE_STATES = [
|
|
61
|
-
{ stateId: 'askCoderProposal', role: 'coder', sourceItem: 'DECIDE-1' },
|
|
62
|
-
{
|
|
63
|
-
stateId: 'askReviewerProposal',
|
|
64
|
-
role: 'reviewer',
|
|
65
|
-
sourceItem: 'DECIDE-2',
|
|
66
|
-
},
|
|
67
|
-
{ stateId: 'commitCoderProposal', role: 'coder', sourceItem: 'DECIDE-3' },
|
|
68
|
-
];
|
|
69
|
-
const ROLE_STATE_IDS = new Set(ROLE_STATES.map((state) => state.stateId));
|
|
70
|
-
const ACCEPTED_OUTCOME_DECLARATIONS = Object.freeze({
|
|
71
|
-
askCoderProposal: new Set(['proposed', 'needsBossReply']),
|
|
72
|
-
askReviewerProposal: new Set(['proposed', 'needsBossReply']),
|
|
73
|
-
commitCoderProposal: new Set(['committed', 'needsBossReply']),
|
|
74
|
-
});
|
|
75
|
-
const DECIDE_OUTCOME_AUTHORITY = Object.freeze({
|
|
76
|
-
governedPlayerStates: Object.freeze({
|
|
77
|
-
askCoderProposal: Object.freeze({
|
|
78
|
-
proposed: Object.freeze({
|
|
79
|
-
fields: Object.freeze({ coderProposal: 'presentation' }),
|
|
80
|
-
repositoryDisposition: 'unchanged',
|
|
81
|
-
}),
|
|
82
|
-
needsBossReply: Object.freeze({
|
|
83
|
-
fields: Object.freeze({ question: 'presentation' }),
|
|
84
|
-
repositoryDisposition: 'unchanged',
|
|
85
|
-
}),
|
|
86
|
-
}),
|
|
87
|
-
askReviewerProposal: Object.freeze({
|
|
88
|
-
proposed: Object.freeze({
|
|
89
|
-
fields: Object.freeze({ reviewerProposal: 'presentation' }),
|
|
90
|
-
repositoryDisposition: 'unchanged',
|
|
91
|
-
}),
|
|
92
|
-
needsBossReply: Object.freeze({
|
|
93
|
-
fields: Object.freeze({ question: 'presentation' }),
|
|
94
|
-
repositoryDisposition: 'unchanged',
|
|
95
|
-
}),
|
|
96
|
-
}),
|
|
97
|
-
commitCoderProposal: Object.freeze({
|
|
98
|
-
committed: Object.freeze({
|
|
99
|
-
fields: Object.freeze({
|
|
100
|
-
coderOutput: 'presentation',
|
|
101
|
-
latestCommit: 'effect',
|
|
102
|
-
}),
|
|
103
|
-
repositoryDisposition: 'one-descendant-commit',
|
|
104
|
-
}),
|
|
105
|
-
needsBossReply: Object.freeze({
|
|
106
|
-
fields: Object.freeze({ question: 'presentation' }),
|
|
107
|
-
repositoryDisposition: 'deferred',
|
|
108
|
-
}),
|
|
109
|
-
}),
|
|
110
|
-
}),
|
|
111
|
-
});
|
|
112
|
-
const PROPOSAL_STATE_BY_ROLE = Object.freeze({
|
|
113
|
-
coder: 'askCoderProposal',
|
|
114
|
-
reviewer: 'askReviewerProposal',
|
|
115
|
-
});
|
|
116
|
-
const ROLE_IDS = ['coder', 'reviewer'];
|
|
117
|
-
const ROLE_ID_SET = new Set(ROLE_IDS);
|
|
118
|
-
const roleLabel = (roleId) => roleId === 'coder' ? 'Coder' : 'Reviewer';
|
|
119
|
-
const BOSS_INTERRUPT_TARGETS = ['independentProposals'];
|
|
120
|
-
const BOSS_INTERRUPT_TARGET_IDS = new Set(BOSS_INTERRUPT_TARGETS);
|
|
3
|
+
// Generated by slc/materialize-link.mjs; governed by the complete slc/link.md.
|
|
4
|
+
// FSM: "./decide.fsm.js"; export: decideMachine; strategies: shared defaults with labelled string relays and nested actors.
|
|
5
|
+
import { decideMachine as machine } from "./decide.fsm.js";
|
|
6
|
+
import { createXStatePlaybookRuntime, snapshotJsonValue, composePlayerContinuation, } from '@sublang/playbook/xstate-runtime';
|
|
7
|
+
const OPTION_SCHEMA = {};
|
|
8
|
+
const INPUT_MAPPING = {};
|
|
9
|
+
const RESUMABLE_STATE_IDS = new Set([
|
|
10
|
+
"askCoderProposal",
|
|
11
|
+
"askReviewerProposal",
|
|
12
|
+
"synthesizeCommit"
|
|
13
|
+
]);
|
|
121
14
|
const UNFINISHED_FINAL_STATE_IDS = new Set([
|
|
122
|
-
|
|
15
|
+
"reportedReviewFailure"
|
|
123
16
|
]);
|
|
124
|
-
const TELEMETRY_TOPIC = 'playbook.fsm.state';
|
|
125
|
-
const TRACE_TOPIC = 'playbook.trace';
|
|
126
|
-
const UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID = 'reconcile:unresolved-effect';
|
|
127
|
-
const UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID = 'abandon:unresolved-effect';
|
|
128
|
-
const PLACEHOLDER_FIELDS = [
|
|
129
|
-
['<caller-topic>', 'callerTopic'],
|
|
130
|
-
['<reviewer-proposal>', 'reviewerProposal'],
|
|
131
|
-
];
|
|
132
17
|
const VERBATIM_PAYLOAD_FIELDS = new Set([
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
18
|
+
"coderProposal",
|
|
19
|
+
"reviewerProposal",
|
|
20
|
+
"coderOutput"
|
|
136
21
|
]);
|
|
137
|
-
function
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
fields.push(field[1]);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return fields;
|
|
175
|
-
}
|
|
176
|
-
function governedOutcomesFor(input) {
|
|
177
|
-
const outcomes = DECIDE_OUTCOME_AUTHORITY.governedPlayerStates[input.stateId];
|
|
178
|
-
if (outcomes === undefined) {
|
|
179
|
-
throw new TypeError(`DECIDE governed player state ${JSON.stringify(input.stateId)} has no outcome authority`);
|
|
180
|
-
}
|
|
181
|
-
const declaredGuards = Object.keys(outcomes).sort();
|
|
182
|
-
const authoredGuards = Object.keys(input.result).sort();
|
|
183
|
-
if (declaredGuards.length !== authoredGuards.length ||
|
|
184
|
-
declaredGuards.some((guard, index) => guard !== authoredGuards[index])) {
|
|
185
|
-
throw new TypeError(`DECIDE governed player state ${input.stateId} changed its authored outcome set`);
|
|
186
|
-
}
|
|
187
|
-
for (const guard of declaredGuards) {
|
|
188
|
-
const required = requiredFieldsFor(input.result[guard]).sort();
|
|
189
|
-
const authoritative = Object.keys(outcomes[guard].fields).sort();
|
|
190
|
-
if (required.length !== authoritative.length ||
|
|
191
|
-
required.some((field, index) => field !== authoritative[index])) {
|
|
192
|
-
throw new TypeError(`DECIDE governed outcome ${input.stateId}.${guard} changed its payload fields`);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return outcomes;
|
|
196
|
-
}
|
|
197
|
-
// LLM judges routinely wrap JSON in prose/fences or damage its tail. Match
|
|
198
|
-
// CODE's recovery contract: scan candidate starts in document order, prefer a
|
|
199
|
-
// strict balanced value at each position, then repair trailing commas and
|
|
200
|
-
// truncation before considering a later candidate.
|
|
201
|
-
function extractJson(raw) {
|
|
202
|
-
try {
|
|
203
|
-
const parsed = parseJudgeJson(raw);
|
|
204
|
-
return isPlainObject(parsed) ? parsed : null;
|
|
205
|
-
}
|
|
206
|
-
catch {
|
|
207
|
-
return null;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
function parseJudgeJson(raw) {
|
|
211
|
-
const text = stripCodeFence(raw.trim());
|
|
212
|
-
try {
|
|
213
|
-
return JSON.parse(text);
|
|
214
|
-
}
|
|
215
|
-
catch {
|
|
216
|
-
// Fall through to candidate extraction and repair.
|
|
217
|
-
}
|
|
218
|
-
const starts = [];
|
|
219
|
-
for (let index = 0; index < text.length; index++) {
|
|
220
|
-
if (text[index] === '{' || text[index] === '[')
|
|
221
|
-
starts.push(index);
|
|
222
|
-
}
|
|
223
|
-
let firstValue;
|
|
224
|
-
for (const start of starts) {
|
|
225
|
-
let parsedHere;
|
|
226
|
-
for (const repair of [false, true]) {
|
|
227
|
-
const candidate = extractJsonValue(text, start, repair);
|
|
228
|
-
if (candidate === undefined)
|
|
229
|
-
continue;
|
|
230
|
-
try {
|
|
231
|
-
parsedHere = { value: JSON.parse(candidate) };
|
|
232
|
-
}
|
|
233
|
-
catch {
|
|
234
|
-
// Try repair at this position, then continue in document order.
|
|
22
|
+
export function validateOptions(value) {
|
|
23
|
+
const captured = snapshotJsonValue(value === undefined ? {} : value, "DECIDE runtime options");
|
|
24
|
+
if (captured === null || typeof captured !== 'object' || Array.isArray(captured)) {
|
|
25
|
+
throw new TypeError("DECIDE runtime options must be an object");
|
|
26
|
+
}
|
|
27
|
+
const fields = captured;
|
|
28
|
+
for (const key of Object.keys(captured)) {
|
|
29
|
+
if (!Object.hasOwn(OPTION_SCHEMA, key))
|
|
30
|
+
throw new TypeError("DECIDE runtime options." + key + ' is not declared');
|
|
31
|
+
}
|
|
32
|
+
for (const [key, schema] of Object.entries(OPTION_SCHEMA)) {
|
|
33
|
+
if (!Object.hasOwn(captured, key)) {
|
|
34
|
+
if (schema.required)
|
|
35
|
+
throw new TypeError("DECIDE runtime options." + key + ' is required');
|
|
36
|
+
}
|
|
37
|
+
else if (typeof fields[key] !== schema.type || (schema.type === 'number' && !Number.isFinite(fields[key]))) {
|
|
38
|
+
throw new TypeError("DECIDE runtime options." + key + ' must be a ' + schema.type);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return captured;
|
|
42
|
+
}
|
|
43
|
+
function renderPlayerPrompt(input, promptIdentity, resuming = false) {
|
|
44
|
+
const fields = input;
|
|
45
|
+
const mapping = {};
|
|
46
|
+
const identities = { "coder-llm": "coder", "reviewer-llm": "reviewer" };
|
|
47
|
+
const omitted = new Set([]);
|
|
48
|
+
const fieldFor = (token) => Object.hasOwn(mapping, token) ? mapping[token] : (token === '#' ? 'irNumber' : token.replace(/-([A-Za-z0-9_$])/g, (_match, next) => next.toUpperCase()));
|
|
49
|
+
const parts = input.prompt.split(/(\r?\n)/);
|
|
50
|
+
let body = '';
|
|
51
|
+
for (let index = 0; index < parts.length; index += 2) {
|
|
52
|
+
const line = parts[index];
|
|
53
|
+
if (omitted.has(line)) {
|
|
54
|
+
const token = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>$/.exec(line)[1];
|
|
55
|
+
if (fields[fieldFor(token)] === '')
|
|
235
56
|
continue;
|
|
236
|
-
}
|
|
237
|
-
break;
|
|
238
|
-
}
|
|
239
|
-
if (parsedHere === undefined)
|
|
240
|
-
continue;
|
|
241
|
-
if (isPlainObject(parsedHere.value))
|
|
242
|
-
return parsedHere.value;
|
|
243
|
-
firstValue ??= parsedHere;
|
|
244
|
-
}
|
|
245
|
-
if (firstValue !== undefined)
|
|
246
|
-
return firstValue.value;
|
|
247
|
-
throw new Error('judge response is not valid JSON');
|
|
248
|
-
}
|
|
249
|
-
function isPlainObject(value) {
|
|
250
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
251
|
-
}
|
|
252
|
-
function sortJson(value) {
|
|
253
|
-
if (Array.isArray(value))
|
|
254
|
-
return value.map((entry) => sortJson(entry));
|
|
255
|
-
if (value !== null && typeof value === 'object') {
|
|
256
|
-
const record = value;
|
|
257
|
-
const sorted = {};
|
|
258
|
-
for (const key of Object.keys(record).sort()) {
|
|
259
|
-
sorted[key] = sortJson(record[key]);
|
|
260
|
-
}
|
|
261
|
-
return sorted;
|
|
262
|
-
}
|
|
263
|
-
return value;
|
|
264
|
-
}
|
|
265
|
-
function stableJson(value, path) {
|
|
266
|
-
return JSON.stringify(sortJson(snapshotJsonValue(value, path)));
|
|
267
|
-
}
|
|
268
|
-
function deepFreeze(value) {
|
|
269
|
-
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
270
|
-
Object.freeze(value);
|
|
271
|
-
for (const member of Object.values(value))
|
|
272
|
-
deepFreeze(member);
|
|
273
|
-
}
|
|
274
|
-
return value;
|
|
275
|
-
}
|
|
276
|
-
function stripCodeFence(text) {
|
|
277
|
-
const fence = text.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/i);
|
|
278
|
-
return fence ? fence[1].trim() : text;
|
|
279
|
-
}
|
|
280
|
-
function extractJsonValue(text, start, repair) {
|
|
281
|
-
const stack = [];
|
|
282
|
-
let output = '';
|
|
283
|
-
let inString = false;
|
|
284
|
-
let escaped = false;
|
|
285
|
-
for (let index = start; index < text.length; index++) {
|
|
286
|
-
const character = text[index];
|
|
287
|
-
if (inString) {
|
|
288
|
-
output += character;
|
|
289
|
-
if (escaped)
|
|
290
|
-
escaped = false;
|
|
291
|
-
else if (character === '\\')
|
|
292
|
-
escaped = true;
|
|
293
|
-
else if (character === '"')
|
|
294
|
-
inString = false;
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
if (character === '"') {
|
|
298
|
-
inString = true;
|
|
299
|
-
output += character;
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
if (character === '{' || character === '[') {
|
|
303
|
-
stack.push(character === '{' ? '}' : ']');
|
|
304
|
-
output += character;
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
if (character === '}' || character === ']') {
|
|
308
|
-
if (repair)
|
|
309
|
-
output = dropTrailingComma(output);
|
|
310
|
-
output += character;
|
|
311
|
-
stack.pop();
|
|
312
|
-
if (stack.length === 0)
|
|
313
|
-
return output;
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
output += character;
|
|
317
|
-
}
|
|
318
|
-
if (!repair)
|
|
319
|
-
return undefined;
|
|
320
|
-
if (inString)
|
|
321
|
-
output += '"';
|
|
322
|
-
output = dropTrailingComma(output);
|
|
323
|
-
while (stack.length > 0)
|
|
324
|
-
output += stack.pop();
|
|
325
|
-
return output;
|
|
326
|
-
}
|
|
327
|
-
function dropTrailingComma(value) {
|
|
328
|
-
return value.replace(/,(\s*)$/, '$1');
|
|
329
|
-
}
|
|
330
|
-
function buildClassifierPrompt(text, ctx) {
|
|
331
|
-
const lines = [];
|
|
332
|
-
lines.push('You are the Boss-input classifier for the decide playbook.');
|
|
333
|
-
lines.push('Classify the Boss message into exactly one FSM event, or into no event.');
|
|
334
|
-
lines.push('');
|
|
335
|
-
lines.push(`Current FSM state: ${JSON.stringify(ctx.state.value)}`);
|
|
336
|
-
lines.push(`Active state ids: ${ctx.state.activeStateIds.join(', ')}`);
|
|
337
|
-
if (ctx.pendingQuestions.length > 0) {
|
|
338
|
-
lines.push('Pending Boss questions:');
|
|
339
|
-
for (const pending of ctx.pendingQuestions) {
|
|
340
|
-
lines.push(`- ${pending.questionId} (${pending.asker.roleId}): ${pending.question}`);
|
|
341
|
-
}
|
|
342
|
-
lines.push('If the Boss message answers a pending question, classify it as BOSS_REPLY; if it is a fresh directive, classify it accordingly.');
|
|
343
|
-
}
|
|
344
|
-
lines.push('');
|
|
345
|
-
lines.push('Events and payload contracts:');
|
|
346
|
-
lines.push(`- BOSS_INTERRUPT: required targetId string, exactly ${BOSS_INTERRUPT_TARGETS[0]}; the runtime attaches the exact Boss text as bossIntent.`);
|
|
347
|
-
lines.push('- BOSS_REPLY: optional questionId when exactly one question is pending and required when several are pending; the runtime attaches the exact Boss text as answer.');
|
|
348
|
-
lines.push('- NO_ACTION: no fields.');
|
|
349
|
-
lines.push('');
|
|
350
|
-
lines.push('Boss message:');
|
|
351
|
-
lines.push(text);
|
|
352
|
-
lines.push('');
|
|
353
|
-
lines.push('Reply with a single JSON object: { "type": "<EVENT_TYPE>", ...declared fields }.');
|
|
354
|
-
lines.push('Use NO_ACTION when no FSM action applies.');
|
|
355
|
-
return lines.join('\n');
|
|
356
|
-
}
|
|
357
|
-
function parseClassification(raw, text, pendingQuestionIds = []) {
|
|
358
|
-
const obj = extractJson(raw);
|
|
359
|
-
if (!obj)
|
|
360
|
-
return null;
|
|
361
|
-
const exactKeys = (...keys) => {
|
|
362
|
-
const ownKeys = Reflect.ownKeys(obj);
|
|
363
|
-
return (ownKeys.length === keys.length &&
|
|
364
|
-
ownKeys.every((key) => typeof key === 'string' && keys.includes(key)));
|
|
365
|
-
};
|
|
366
|
-
const eventType = obj.type;
|
|
367
|
-
if (eventType === 'NO_ACTION') {
|
|
368
|
-
return exactKeys('type') ? { type: 'NO_ACTION' } : null;
|
|
369
|
-
}
|
|
370
|
-
if (eventType === 'BOSS_INTERRUPT') {
|
|
371
|
-
if (exactKeys('type', 'targetId') &&
|
|
372
|
-
typeof obj.targetId === 'string' &&
|
|
373
|
-
BOSS_INTERRUPT_TARGET_IDS.has(obj.targetId)) {
|
|
374
|
-
return {
|
|
375
|
-
type: 'BOSS_INTERRUPT',
|
|
376
|
-
targetId: obj.targetId,
|
|
377
|
-
bossIntent: text,
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
return null;
|
|
381
|
-
}
|
|
382
|
-
if (eventType === 'BOSS_REPLY') {
|
|
383
|
-
if (pendingQuestionIds.length === 0)
|
|
384
|
-
return null;
|
|
385
|
-
if (typeof obj.questionId === 'string') {
|
|
386
|
-
return exactKeys('type', 'questionId') &&
|
|
387
|
-
pendingQuestionIds.includes(obj.questionId)
|
|
388
|
-
? {
|
|
389
|
-
type: 'BOSS_REPLY',
|
|
390
|
-
questionId: obj.questionId,
|
|
391
|
-
answer: text,
|
|
392
|
-
}
|
|
393
|
-
: null;
|
|
394
|
-
}
|
|
395
|
-
return exactKeys('type') && pendingQuestionIds.length === 1
|
|
396
|
-
? {
|
|
397
|
-
type: 'BOSS_REPLY',
|
|
398
|
-
questionId: pendingQuestionIds[0],
|
|
399
|
-
answer: text,
|
|
400
|
-
}
|
|
401
|
-
: null;
|
|
402
|
-
}
|
|
403
|
-
return null;
|
|
404
|
-
}
|
|
405
|
-
function buildAdjudicatorPrompt(input, playerOutput, correction) {
|
|
406
|
-
const outcomes = governedOutcomesFor(input);
|
|
407
|
-
const lines = [];
|
|
408
|
-
lines.push('You are the guard adjudicator for a playbook state machine.');
|
|
409
|
-
lines.push('This is hidden control work. Do not call tools, inspect files, or ' +
|
|
410
|
-
'seek external evidence. Decide only from the supplied player output ' +
|
|
411
|
-
'and guard descriptions. Reply with exactly one JSON object and no prose.');
|
|
412
|
-
lines.push(`The role "${roleLabel(input.role)}" produced the output below for source item ${input.sourceItem}.`);
|
|
413
|
-
lines.push('Choose exactly one guard whose description matches that output.');
|
|
414
|
-
lines.push('');
|
|
415
|
-
lines.push('Player output (verbatim):');
|
|
416
|
-
lines.push('"""');
|
|
417
|
-
lines.push(playerOutput);
|
|
418
|
-
lines.push('"""');
|
|
419
|
-
lines.push('');
|
|
420
|
-
lines.push('Guards (choose exactly one; the descriptions are authoritative and must be applied as written):');
|
|
421
|
-
// The shared engine's renderer (slc/link.md §Captain adjudication): each
|
|
422
|
-
// arm's meaning verbatim, with its `Output shall include` clause — authored
|
|
423
|
-
// for the complete actor output — replaced by the reply contract the
|
|
424
|
-
// outcome authority gives the judge. Rendering the clause verbatim asked
|
|
425
|
-
// the judge for presentation-owned `coderProposal` or `question`, which the
|
|
426
|
-
// reconciler rejects, spending the single correction on a self-inflicted
|
|
427
|
-
// defect.
|
|
428
|
-
for (const [guard, description] of Object.entries(input.result)) {
|
|
429
|
-
lines.push(...renderGovernedOutcomeContract(guard, description, outcomes[guard]));
|
|
430
|
-
}
|
|
431
|
-
lines.push('');
|
|
432
|
-
lines.push("Pick exactly one declared `guard` and reply with exactly that outcome's reply shape above: `guard` plus its semantic-owned fields and nothing else.");
|
|
433
|
-
lines.push('A field listed as runtime-supplied is owned by presentation, effect, or runtime evidence; the runtime fills it itself, and a reply that includes one is structurally invalid.');
|
|
434
|
-
if (correction !== undefined) {
|
|
435
|
-
lines.push('');
|
|
436
|
-
lines.push('Your first reply was structurally invalid:');
|
|
437
|
-
lines.push('"""');
|
|
438
|
-
lines.push(correction.reply);
|
|
439
|
-
lines.push('"""');
|
|
440
|
-
lines.push(`Validation error: ${correction.error}`);
|
|
441
|
-
lines.push('Correct only that structure using the same player output and outcome schema.');
|
|
442
|
-
}
|
|
443
|
-
return lines.join('\n');
|
|
444
|
-
}
|
|
445
|
-
function parseGovernedSemanticCandidate(raw) {
|
|
446
|
-
try {
|
|
447
|
-
return parseJudgeJson(raw);
|
|
448
|
-
}
|
|
449
|
-
catch (error) {
|
|
450
|
-
throw new PlaybookSemanticCandidateStructureError(error instanceof Error ? error.message : 'reply is not valid JSON');
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
function combineSignals(a, b) {
|
|
454
|
-
return combineAbortSignals(a, b);
|
|
455
|
-
}
|
|
456
|
-
function normalizeErrorCompact(err) {
|
|
457
|
-
if (err === undefined || err === null)
|
|
458
|
-
return undefined;
|
|
459
|
-
const normalized = normalizeError(err);
|
|
460
|
-
return { name: normalized.name, message: normalized.message };
|
|
461
|
-
}
|
|
462
|
-
function normalizeErrorFull(err) {
|
|
463
|
-
return err === undefined || err === null ? undefined : normalizeError(err);
|
|
464
|
-
}
|
|
465
|
-
// DR-028's unified empty predicate: a missing, empty, or whitespace-only
|
|
466
|
-
// `finalText` on an `ok` player result is the one shape that earns exactly
|
|
467
|
-
// one corrective re-ask before the existing failure path applies. Mirrors
|
|
468
|
-
// the shared engine's predicate (PBRT-9).
|
|
469
|
-
function isEmptyFinalText(finalText) {
|
|
470
|
-
return finalText === undefined || finalText.trim().length === 0;
|
|
471
|
-
}
|
|
472
|
-
function schema3Construction(value) {
|
|
473
|
-
if (!isPlainObject(value)) {
|
|
474
|
-
throw new TypeError('DECIDE schema-3 factory input must be a plain object');
|
|
475
|
-
}
|
|
476
|
-
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
477
|
-
const keys = Reflect.ownKeys(value);
|
|
478
|
-
if (keys.length !== 2 ||
|
|
479
|
-
!keys.includes('configuredOptions') ||
|
|
480
|
-
!keys.includes('hostCapabilities') ||
|
|
481
|
-
keys.some((key) => {
|
|
482
|
-
const descriptor = descriptors[key];
|
|
483
|
-
return (descriptor?.get !== undefined ||
|
|
484
|
-
descriptor?.set !== undefined ||
|
|
485
|
-
descriptor?.enumerable !== true ||
|
|
486
|
-
!Object.prototype.hasOwnProperty.call(descriptor, 'value'));
|
|
487
|
-
})) {
|
|
488
|
-
throw new TypeError('DECIDE schema-3 factory input must contain exactly configuredOptions and hostCapabilities data properties');
|
|
489
|
-
}
|
|
490
|
-
const configuredOptions = descriptors.configuredOptions.value;
|
|
491
|
-
if (configuredOptions !== null &&
|
|
492
|
-
typeof configuredOptions === 'object' &&
|
|
493
|
-
Object.prototype.hasOwnProperty.call(configuredOptions, 'hostCapabilities')) {
|
|
494
|
-
throw new TypeError('DECIDE configured options must not contain hostCapabilities');
|
|
495
|
-
}
|
|
496
|
-
const hostCapabilities = descriptors.hostCapabilities.value;
|
|
497
|
-
if (hostCapabilities === null ||
|
|
498
|
-
typeof hostCapabilities !== 'object' ||
|
|
499
|
-
Array.isArray(hostCapabilities)) {
|
|
500
|
-
throw new TypeError('DECIDE schema-3 factory input hostCapabilities must be a live object');
|
|
501
|
-
}
|
|
502
|
-
const repositoryDescriptor = Object.getOwnPropertyDescriptor(hostCapabilities, 'repository');
|
|
503
|
-
const authorityDescriptor = Object.getOwnPropertyDescriptor(hostCapabilities, 'authority');
|
|
504
|
-
const effectLedgerDescriptor = Object.getOwnPropertyDescriptor(hostCapabilities, 'effectLedger');
|
|
505
|
-
const repository = repositoryDescriptor?.value;
|
|
506
|
-
const authority = authorityDescriptor?.value;
|
|
507
|
-
const effectLedger = effectLedgerDescriptor?.value;
|
|
508
|
-
if (authorityDescriptor === undefined ||
|
|
509
|
-
authorityDescriptor.get !== undefined ||
|
|
510
|
-
authorityDescriptor.set !== undefined ||
|
|
511
|
-
!Object.prototype.hasOwnProperty.call(authorityDescriptor, 'value') ||
|
|
512
|
-
!isPlainObject(authority) ||
|
|
513
|
-
authority.artifactSchema !== 3 ||
|
|
514
|
-
authority.playbookId !== 'decide' ||
|
|
515
|
-
typeof authority.sessionId !== 'string' ||
|
|
516
|
-
authority.sessionId.length === 0 ||
|
|
517
|
-
!Array.isArray(authority.requiredRoleIds) ||
|
|
518
|
-
stableJson([...authority.requiredRoleIds].sort(), 'DECIDE host authority required roles') !== stableJson([...ROLE_IDS].sort(), 'DECIDE required roles') ||
|
|
519
|
-
!Array.isArray(authority.concurrentRoleSets) ||
|
|
520
|
-
authority.concurrentRoleSets.length !== 1 ||
|
|
521
|
-
!Array.isArray(authority.concurrentRoleSets[0]) ||
|
|
522
|
-
stableJson([...authority.concurrentRoleSets[0]].sort(), 'DECIDE host authority concurrent roles') !== stableJson([...ROLE_IDS].sort(), 'DECIDE concurrent roles')) {
|
|
523
|
-
throw new TypeError('DECIDE schema-3 factory input hostCapabilities.authority must identify one schema-3 playbook session');
|
|
524
|
-
}
|
|
525
|
-
if (repositoryDescriptor === undefined ||
|
|
526
|
-
repositoryDescriptor.get !== undefined ||
|
|
527
|
-
repositoryDescriptor.set !== undefined ||
|
|
528
|
-
!Object.prototype.hasOwnProperty.call(repositoryDescriptor, 'value') ||
|
|
529
|
-
!isPlainObject(repository) ||
|
|
530
|
-
typeof repository.runExclusive !== 'function' ||
|
|
531
|
-
typeof repository.runCohort !== 'function' ||
|
|
532
|
-
typeof repository.runDeferred !== 'function') {
|
|
533
|
-
throw new TypeError('DECIDE schema-3 factory input hostCapabilities.repository must expose runExclusive, runCohort, and runDeferred functions');
|
|
534
|
-
}
|
|
535
|
-
if (effectLedgerDescriptor === undefined ||
|
|
536
|
-
effectLedgerDescriptor.get !== undefined ||
|
|
537
|
-
effectLedgerDescriptor.set !== undefined ||
|
|
538
|
-
!Object.prototype.hasOwnProperty.call(effectLedgerDescriptor, 'value') ||
|
|
539
|
-
!isPlainObject(effectLedger) ||
|
|
540
|
-
typeof effectLedger.snapshot !== 'function' ||
|
|
541
|
-
typeof effectLedger.writeAhead !== 'function') {
|
|
542
|
-
throw new TypeError('DECIDE schema-3 factory input hostCapabilities.effectLedger must expose snapshot and writeAhead functions');
|
|
543
|
-
}
|
|
544
|
-
return {
|
|
545
|
-
configuredOptions: configuredOptions,
|
|
546
|
-
hostCapabilities: {
|
|
547
|
-
authority: authority,
|
|
548
|
-
repository: repository,
|
|
549
|
-
effectLedger: effectLedger,
|
|
550
|
-
},
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
function deferredValue() {
|
|
554
|
-
let resolve;
|
|
555
|
-
let reject;
|
|
556
|
-
const promise = new Promise((onResolve, onReject) => {
|
|
557
|
-
resolve = onResolve;
|
|
558
|
-
reject = onReject;
|
|
559
|
-
});
|
|
560
|
-
return { promise, resolve, reject };
|
|
561
|
-
}
|
|
562
|
-
function hasCompleteUnchangedReceipt(boundary) {
|
|
563
|
-
return boundary.physicalReceipt?.classification === 'unchanged';
|
|
564
|
-
}
|
|
565
|
-
// A corrective call is bound to the exact physical boundary it would repeat.
|
|
566
|
-
// A failed-state restart replays the whole entry event. Cooperative host
|
|
567
|
-
// attempts are serialized in ledger order, so the latest durable boundary
|
|
568
|
-
// identifies the causal host attempt even when a nested or sibling runtime
|
|
569
|
-
// wrote it; every boundary in that attempt must have a complete unchanged
|
|
570
|
-
// receipt.
|
|
571
|
-
// The durable ledger remains the authority in both cases; no process-local
|
|
572
|
-
// player result or presentation text can make a replay safe.
|
|
573
|
-
function createAutomaticReplayPolicy(evidence) {
|
|
574
|
-
const readLedger = () => assertPlaybookEffectLedger(evidence.effectLedger.snapshot(), 'DECIDE automatic-replay effect ledger');
|
|
575
|
-
return Object.freeze({
|
|
576
|
-
allowsEmptyOkCorrection(runtimeSessionId, callId) {
|
|
577
|
-
const matching = readLedger().boundaries.filter((boundary) => boundary.runtimeSessionId === runtimeSessionId &&
|
|
578
|
-
boundary.callId === callId);
|
|
579
|
-
return (matching.length === 1 && hasCompleteUnchangedReceipt(matching[0]));
|
|
580
|
-
},
|
|
581
|
-
allowsFailureStateRetry() {
|
|
582
|
-
const ledger = readLedger();
|
|
583
|
-
const latest = ledger.boundaries.at(-1);
|
|
584
|
-
if (latest === undefined)
|
|
585
|
-
return false;
|
|
586
|
-
const attempt = ledger.boundaries.filter((boundary) => boundary.attemptId === latest.attemptId);
|
|
587
|
-
return (attempt.length > 0 && attempt.every(hasCompleteUnchangedReceipt));
|
|
588
|
-
},
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
function isAbortFailure(error, signal) {
|
|
592
|
-
return signal.aborted && Object.is(error, signal.reason);
|
|
593
|
-
}
|
|
594
|
-
function abortReasonClassifier(...sources) {
|
|
595
|
-
const captured = sources.filter((source) => source !== undefined);
|
|
596
|
-
return Object.freeze({
|
|
597
|
-
isAbortReason: (error) => captured.some((source) => source instanceof AbortSignal
|
|
598
|
-
? isAbortFailure(error, source)
|
|
599
|
-
: source.isAbortReason(error)),
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
function pendingQuestionsFromContext(context) {
|
|
603
|
-
const pending = context.pendingBossQuestions;
|
|
604
|
-
if (pending === undefined ||
|
|
605
|
-
pending === null ||
|
|
606
|
-
typeof pending !== 'object') {
|
|
607
|
-
return [];
|
|
608
|
-
}
|
|
609
|
-
const questions = [];
|
|
610
|
-
for (const [key, value] of Object.entries(pending)) {
|
|
611
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
612
|
-
continue;
|
|
613
|
-
}
|
|
614
|
-
const obj = value;
|
|
615
|
-
if (typeof obj.questionId === 'string' &&
|
|
616
|
-
obj.questionId === key &&
|
|
617
|
-
typeof obj.resumeStateId === 'string' &&
|
|
618
|
-
typeof obj.sourceItem === 'string' &&
|
|
619
|
-
isPlainObject(obj.asker) &&
|
|
620
|
-
obj.asker.kind === 'role' &&
|
|
621
|
-
ROLE_ID_SET.has(String(obj.asker.roleId)) &&
|
|
622
|
-
typeof obj.question === 'string') {
|
|
623
|
-
questions.push(obj);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
return questions.sort((left, right) => left.questionId.localeCompare(right.questionId));
|
|
627
|
-
}
|
|
628
|
-
const WAIT_STATE_RESUME_IDS = {
|
|
629
|
-
waitCoderProposalReply: 'askCoderProposal',
|
|
630
|
-
waitReviewerProposalReply: 'askReviewerProposal',
|
|
631
|
-
};
|
|
632
|
-
const WAIT_STATE_IDS = new Set([
|
|
633
|
-
...Object.keys(WAIT_STATE_RESUME_IDS),
|
|
634
|
-
'awaitBossReply',
|
|
635
|
-
]);
|
|
636
|
-
const STATUS_STATE_IDS = new Set([
|
|
637
|
-
...ROLE_STATE_IDS,
|
|
638
|
-
...WAIT_STATE_IDS,
|
|
639
|
-
'failed',
|
|
640
|
-
]);
|
|
641
|
-
// PBRT-45: a question is pending only while its authored reply-wait state
|
|
642
|
-
// is active. The context retains an answered question through the resumed
|
|
643
|
-
// player call so the Q+A continuation prompt can quote it, and each branch
|
|
644
|
-
// keeps its own entry through the parallel region — so an unfiltered
|
|
645
|
-
// projection would report the answered question as still awaiting during
|
|
646
|
-
// the resume, and both branch questions after only one remains pending.
|
|
647
|
-
const RESUME_WAIT_STATE_IDS = {
|
|
648
|
-
...Object.fromEntries(Object.entries(WAIT_STATE_RESUME_IDS).map(([waitStateId, resumeStateId]) => [
|
|
649
|
-
resumeStateId,
|
|
650
|
-
waitStateId,
|
|
651
|
-
])),
|
|
652
|
-
commitCoderProposal: 'awaitBossReply',
|
|
653
|
-
};
|
|
654
|
-
function pendingQuestionsForState(state, context) {
|
|
655
|
-
return pendingQuestionsFromContext(context).filter((pending) => state.activeStateIds.includes(RESUME_WAIT_STATE_IDS[pending.resumeStateId] ?? ''));
|
|
656
|
-
}
|
|
657
|
-
function questionForWaitState(stateId, pendingQuestions) {
|
|
658
|
-
const resumeStateId = WAIT_STATE_RESUME_IDS[stateId];
|
|
659
|
-
if (resumeStateId !== undefined) {
|
|
660
|
-
return pendingQuestions.find((pending) => pending.resumeStateId === resumeStateId);
|
|
661
|
-
}
|
|
662
|
-
return stateId === 'awaitBossReply' && pendingQuestions.length === 1
|
|
663
|
-
? pendingQuestions[0]
|
|
664
|
-
: undefined;
|
|
665
|
-
}
|
|
666
|
-
const TRANSITION_EVENT_FIELDS = [
|
|
667
|
-
'callerTopic',
|
|
668
|
-
'targetId',
|
|
669
|
-
'bossIntent',
|
|
670
|
-
'questionId',
|
|
671
|
-
'answer',
|
|
672
|
-
];
|
|
673
|
-
function normalizedTransitionEvent(event) {
|
|
674
|
-
if (event === null || typeof event !== 'object') {
|
|
675
|
-
return snapshotJsonValue(event ?? null, 'FSM event');
|
|
676
|
-
}
|
|
677
|
-
const source = event;
|
|
678
|
-
const descriptor = {};
|
|
679
|
-
if (typeof source.type === 'string')
|
|
680
|
-
descriptor.type = source.type;
|
|
681
|
-
for (const field of TRANSITION_EVENT_FIELDS) {
|
|
682
|
-
if (typeof source[field] === 'string') {
|
|
683
|
-
descriptor[field] = source[field];
|
|
684
57
|
}
|
|
58
|
+
body += line.replace(/<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g, (match, token) => {
|
|
59
|
+
const value = Object.hasOwn(identities, token) ? promptIdentity(identities[token]) : fields[fieldFor(token)];
|
|
60
|
+
if (typeof value !== 'string')
|
|
61
|
+
return match;
|
|
62
|
+
return line.startsWith('> ') ? value.replace(/\r?\n/g, (ending) => ending + '> ') : value;
|
|
63
|
+
}) + (parts[index + 1] ?? '');
|
|
685
64
|
}
|
|
686
|
-
|
|
687
|
-
descriptor.output = snapshotJsonValue(source.output, 'FSM event output');
|
|
688
|
-
}
|
|
689
|
-
if (source.error !== undefined) {
|
|
690
|
-
descriptor.error = snapshotJsonValue(normalizeError(source.error), 'FSM event error');
|
|
691
|
-
}
|
|
692
|
-
return snapshotJsonValue(descriptor, 'FSM event');
|
|
693
|
-
}
|
|
694
|
-
function telemetryPayload(previousState, state, event, context, hiddenQuestionId) {
|
|
695
|
-
const pendingBossQuestions = pendingQuestionsForState(state, context).filter(({ questionId }) => questionId !== hiddenQuestionId);
|
|
696
|
-
const prior = previousState ?? state;
|
|
697
|
-
const payload = {
|
|
698
|
-
from: prior.value,
|
|
699
|
-
to: state.value,
|
|
700
|
-
event: normalizedTransitionEvent(event),
|
|
701
|
-
previousState: prior,
|
|
702
|
-
state,
|
|
703
|
-
...(pendingBossQuestions.length > 0 ? { pendingBossQuestions } : {}),
|
|
704
|
-
...(state.activeStateIds.includes('failed')
|
|
705
|
-
? { lastError: normalizeErrorFull(context.lastError) ?? null }
|
|
706
|
-
: {}),
|
|
707
|
-
};
|
|
708
|
-
assertJsonSafe(payload);
|
|
709
|
-
return payload;
|
|
65
|
+
return composePlayerContinuation(input, body, resuming);
|
|
710
66
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
let initInFlight;
|
|
737
|
-
let disposed = false;
|
|
738
|
-
let disposalPromise;
|
|
739
|
-
let controlPlaneError;
|
|
740
|
-
let nestedBridge;
|
|
741
|
-
const privateResumeTokens = new Map();
|
|
742
|
-
const playbookCallTurnIds = new Map();
|
|
743
|
-
const inFlightPlayerKeys = new Set();
|
|
744
|
-
const activeBoundaryCalls = new Set();
|
|
745
|
-
const activeEmissionCalls = new Set();
|
|
746
|
-
const emissionQueue = new PQueue({ concurrency: 1 });
|
|
747
|
-
const judgeQueue = new PQueue({ concurrency: 1 });
|
|
748
|
-
// A proposal cohort completes both semantic callbacks concurrently at the
|
|
749
|
-
// repository seam. Serialize each whole adjudication/correction transaction
|
|
750
|
-
// so their durable correction-budget compare-and-swaps cannot race.
|
|
751
|
-
const semanticCompletionQueue = new PQueue({ concurrency: 1 });
|
|
752
|
-
const governedOutputsByBoundaryId = new Map();
|
|
753
|
-
const governedFailuresByBoundaryId = new Map();
|
|
754
|
-
const governedEvidenceByBoundaryId = new Map();
|
|
755
|
-
const governedReceiptsByBoundaryId = new Map();
|
|
756
|
-
const governedPlayerOutputs = new WeakMap();
|
|
757
|
-
const unresolvedSemanticBoundaryIds = new Set();
|
|
758
|
-
const appliedControlReceipts = new Map();
|
|
759
|
-
const pendingProposalCohort = new Map();
|
|
760
|
-
let activeProposalCohort;
|
|
761
|
-
let completedProposalCohortTurnId;
|
|
762
|
-
let deferredOperationId;
|
|
763
|
-
let hiddenDeferredOperationId;
|
|
764
|
-
let activeDeferredContinuation;
|
|
765
|
-
const collectFailure = (failures, error) => {
|
|
766
|
-
if (error instanceof AggregateError) {
|
|
767
|
-
for (const nested of error.errors)
|
|
768
|
-
collectFailure(failures, nested);
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
if (!failures.some((failure) => Object.is(failure, error))) {
|
|
772
|
-
failures.push(error);
|
|
773
|
-
}
|
|
774
|
-
};
|
|
775
|
-
const latchControlPlaneError = (error, signal) => {
|
|
776
|
-
if (!isAbortFailure(error, signal))
|
|
777
|
-
controlPlaneError ??= error;
|
|
778
|
-
};
|
|
779
|
-
const latchInspectionError = (error, aborts = currentAborts) => {
|
|
780
|
-
if (aborts?.isAbortReason(error))
|
|
781
|
-
return;
|
|
782
|
-
if (currentSignal !== undefined)
|
|
783
|
-
controlPlaneError ??= error;
|
|
784
|
-
else
|
|
785
|
-
collectFailure(emissionFailures, error);
|
|
786
|
-
};
|
|
787
|
-
const enqueue = (fn, aborts = currentAborts) => {
|
|
788
|
-
const enqueueAborts = aborts;
|
|
789
|
-
const queued = emissionQueue.add(fn);
|
|
790
|
-
activeEmissionCalls.add(queued);
|
|
791
|
-
void queued.then(() => activeEmissionCalls.delete(queued), (error) => {
|
|
792
|
-
activeEmissionCalls.delete(queued);
|
|
793
|
-
if (!enqueueAborts?.isAbortReason(error)) {
|
|
794
|
-
collectFailure(emissionFailures, error);
|
|
795
|
-
}
|
|
796
|
-
});
|
|
797
|
-
return queued;
|
|
798
|
-
};
|
|
799
|
-
const flush = async (_aborts = currentAborts) => {
|
|
800
|
-
while (true) {
|
|
801
|
-
const active = [...activeEmissionCalls];
|
|
802
|
-
if (active.length > 0)
|
|
803
|
-
await Promise.allSettled(active);
|
|
804
|
-
await emissionQueue.onIdle();
|
|
805
|
-
if (activeEmissionCalls.size === 0 &&
|
|
806
|
-
emissionQueue.size === 0 &&
|
|
807
|
-
emissionQueue.pending === 0) {
|
|
808
|
-
break;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
if (emissionFailures.length === 0)
|
|
812
|
-
return;
|
|
813
|
-
const failures = emissionFailures;
|
|
814
|
-
emissionFailures = [];
|
|
815
|
-
const failure = failures.length === 1
|
|
816
|
-
? failures[0]
|
|
817
|
-
: new AggregateError(failures, 'decide runtime emissions failed');
|
|
818
|
-
// Enqueue ownership already classified every stored failure as distinct.
|
|
819
|
-
// Preserve that classification if an unrelated public boundary drains
|
|
820
|
-
// it with a signal whose reason happens to be the same object.
|
|
821
|
-
if (currentSignal !== undefined)
|
|
822
|
-
controlPlaneError ??= failure;
|
|
823
|
-
throw failure;
|
|
824
|
-
};
|
|
825
|
-
const drainBoundaryCallsAndEmissions = async () => {
|
|
826
|
-
while (true) {
|
|
827
|
-
if (activeBoundaryCalls.size > 0) {
|
|
828
|
-
await Promise.allSettled([...activeBoundaryCalls]);
|
|
829
|
-
}
|
|
830
|
-
await flush();
|
|
831
|
-
if (activeBoundaryCalls.size === 0)
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
};
|
|
835
|
-
const trackBoundaryCall = (call) => {
|
|
836
|
-
activeBoundaryCalls.add(call);
|
|
837
|
-
void call.then(() => activeBoundaryCalls.delete(call), () => activeBoundaryCalls.delete(call));
|
|
838
|
-
return call;
|
|
839
|
-
};
|
|
840
|
-
const requirePorts = () => {
|
|
841
|
-
if (!ports) {
|
|
842
|
-
throw new Error('decide runtime: init(session) must be called first');
|
|
843
|
-
}
|
|
844
|
-
return ports;
|
|
845
|
-
};
|
|
846
|
-
const requireSessionIdentity = () => {
|
|
847
|
-
if (!sessionIdentity) {
|
|
848
|
-
throw new Error('decide runtime: init(session) must be called first');
|
|
849
|
-
}
|
|
850
|
-
return sessionIdentity;
|
|
851
|
-
};
|
|
852
|
-
const bindSession = (nextSession) => {
|
|
853
|
-
const bound = snapshotPlaybookSession(nextSession);
|
|
854
|
-
if (bound.playbookId !== deferredEffects.authority.playbookId) {
|
|
855
|
-
throw new TypeError('DECIDE runtime playbook identity must match its bound schema-3 host authority');
|
|
856
|
-
}
|
|
857
|
-
if (bound.roleBindings === undefined)
|
|
858
|
-
return bound;
|
|
859
|
-
const actual = Object.keys(bound.roleBindings).sort();
|
|
860
|
-
const expected = [...ROLE_IDS].sort();
|
|
861
|
-
const missing = expected.filter((roleId) => !actual.includes(roleId));
|
|
862
|
-
const extra = actual.filter((roleId) => !ROLE_ID_SET.has(roleId));
|
|
863
|
-
if (missing.length > 0 || extra.length > 0) {
|
|
864
|
-
throw new TypeError(`DECIDE session roleBindings must cover exactly [${expected.join(', ')}]` +
|
|
865
|
-
`${missing.length === 0 ? '' : `; missing [${missing.join(', ')}]`}` +
|
|
866
|
-
`${extra.length === 0 ? '' : `; extra [${extra.join(', ')}]`}`);
|
|
867
|
-
}
|
|
868
|
-
return bound;
|
|
869
|
-
};
|
|
870
|
-
const resolvedPlayerId = (roleId) => requireSessionIdentity().roleBindings?.[roleId]?.playerId;
|
|
871
|
-
const promptIdentity = (roleId) => requireSessionIdentity().roleBindings?.[roleId]?.promptIdentity ?? roleId;
|
|
872
|
-
const composeInvocationPrompt = (input, resuming = false) => {
|
|
873
|
-
let active = true;
|
|
874
|
-
const lookup = (roleId) => {
|
|
875
|
-
if (!active) {
|
|
876
|
-
throw new Error('DECIDE prompt identity lookup is no longer active for this invocation');
|
|
877
|
-
}
|
|
878
|
-
if (!ROLE_ID_SET.has(roleId)) {
|
|
879
|
-
throw new TypeError(`DECIDE prompt identity lookup rejected undeclared role ${String(roleId)}`);
|
|
880
|
-
}
|
|
881
|
-
return promptIdentity(roleId);
|
|
882
|
-
};
|
|
883
|
-
try {
|
|
884
|
-
return composePlayerPrompt(input, lookup, resuming);
|
|
885
|
-
}
|
|
886
|
-
finally {
|
|
887
|
-
active = false;
|
|
888
|
-
}
|
|
889
|
-
};
|
|
890
|
-
const continuationKey = (roleId, playerId) => playerId ?? roleId;
|
|
891
|
-
const tokensByContinuationKey = (tokens) => {
|
|
892
|
-
const byKey = new Map();
|
|
893
|
-
for (const [roleId, token] of Object.entries(tokens)) {
|
|
894
|
-
if (!ROLE_ID_SET.has(roleId)) {
|
|
895
|
-
throw new TypeError(`DECIDE role tokens contain unknown role ${roleId}`);
|
|
896
|
-
}
|
|
897
|
-
const typedRole = roleId;
|
|
898
|
-
const key = continuationKey(typedRole, resolvedPlayerId(typedRole));
|
|
899
|
-
const prior = byKey.get(key);
|
|
900
|
-
if (prior !== undefined && prior !== token) {
|
|
901
|
-
throw new TypeError(`DECIDE runtime snapshot assigns conflicting tokens to roles bound to player ${key}`);
|
|
902
|
-
}
|
|
903
|
-
byKey.set(key, token);
|
|
904
|
-
}
|
|
905
|
-
const rolesByKey = new Map();
|
|
906
|
-
for (const roleId of ROLE_IDS) {
|
|
907
|
-
const key = continuationKey(roleId, resolvedPlayerId(roleId));
|
|
908
|
-
rolesByKey.set(key, [...(rolesByKey.get(key) ?? []), roleId]);
|
|
909
|
-
}
|
|
910
|
-
for (const [key, roles] of rolesByKey) {
|
|
911
|
-
if (roles.length < 2)
|
|
912
|
-
continue;
|
|
913
|
-
const present = roles.filter((roleId) => tokens[roleId] !== undefined);
|
|
914
|
-
if (present.length !== 0 && present.length !== roles.length) {
|
|
915
|
-
throw new TypeError(`DECIDE role tokens must project player ${key} through every aliased role [${roles.join(', ')}]`);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
return byKey;
|
|
919
|
-
};
|
|
920
|
-
const selectPlayerResume = (roleId, playerId) => {
|
|
921
|
-
const session = requireSessionIdentity();
|
|
922
|
-
const selected = session.playerSessions
|
|
923
|
-
? session.playerSessions.select(roleId)
|
|
924
|
-
: privateResumeTokens.get(continuationKey(roleId, playerId)) ?? false;
|
|
925
|
-
if (selected !== false &&
|
|
926
|
-
(typeof selected !== 'string' || selected.trim().length === 0)) {
|
|
927
|
-
throw new TypeError(`player session store returned an invalid resume token for role ${roleId}`);
|
|
928
|
-
}
|
|
929
|
-
return selected;
|
|
930
|
-
};
|
|
931
|
-
const updatePlayerResume = (roleId, playerId, result) => {
|
|
932
|
-
if (result.resumeToken === undefined && result.status !== 'ok')
|
|
933
|
-
return;
|
|
934
|
-
const session = requireSessionIdentity();
|
|
935
|
-
if (session.playerSessions) {
|
|
936
|
-
session.playerSessions.update(roleId, result.resumeToken);
|
|
937
|
-
}
|
|
938
|
-
else if (result.resumeToken !== undefined) {
|
|
939
|
-
privateResumeTokens.set(continuationKey(roleId, playerId), result.resumeToken);
|
|
940
|
-
}
|
|
941
|
-
else {
|
|
942
|
-
privateResumeTokens.delete(continuationKey(roleId, playerId));
|
|
943
|
-
}
|
|
944
|
-
};
|
|
945
|
-
const snapshotRoleResumeTokens = () => {
|
|
946
|
-
const session = requireSessionIdentity();
|
|
947
|
-
const captured = snapshotJsonValue(session.playerSessions
|
|
948
|
-
? session.playerSessions.snapshot()
|
|
949
|
-
: Object.fromEntries(ROLE_IDS.flatMap((roleId) => {
|
|
950
|
-
const token = privateResumeTokens.get(continuationKey(roleId, resolvedPlayerId(roleId)));
|
|
951
|
-
return token === undefined ? [] : [[roleId, token]];
|
|
952
|
-
})), 'player session store snapshot');
|
|
953
|
-
if (!isPlainObject(captured)) {
|
|
954
|
-
throw new TypeError('player session store snapshot must be an object');
|
|
955
|
-
}
|
|
956
|
-
const tokens = {};
|
|
957
|
-
for (const [roleId, token] of Object.entries(captured)) {
|
|
958
|
-
if (!ROLE_ID_SET.has(roleId)) {
|
|
959
|
-
throw new TypeError(`player session store snapshot contains unknown role ${roleId}`);
|
|
960
|
-
}
|
|
961
|
-
if (typeof token !== 'string' || token.trim().length === 0) {
|
|
962
|
-
throw new TypeError(`player session store snapshot token for ${roleId} must be a non-empty string`);
|
|
963
|
-
}
|
|
964
|
-
tokens[roleId] = token;
|
|
965
|
-
}
|
|
966
|
-
tokensByContinuationKey(tokens);
|
|
967
|
-
return tokens;
|
|
968
|
-
};
|
|
969
|
-
const restoreRoleResumeTokens = (tokens) => {
|
|
970
|
-
const byKey = tokensByContinuationKey(tokens);
|
|
971
|
-
const session = requireSessionIdentity();
|
|
972
|
-
if (session.playerSessions) {
|
|
973
|
-
session.playerSessions.restore(tokens);
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
privateResumeTokens.clear();
|
|
977
|
-
for (const [key, token] of byKey)
|
|
978
|
-
privateResumeTokens.set(key, token);
|
|
979
|
-
};
|
|
980
|
-
const currentState = (pendingCall = nestedBridge.getPendingCall()) => {
|
|
981
|
-
const live = actor;
|
|
982
|
-
if (!live) {
|
|
983
|
-
throw new Error('decide runtime: actor is not initialized');
|
|
984
|
-
}
|
|
985
|
-
return normalizePlaybookSnapshot(live.getSnapshot(), {
|
|
986
|
-
pendingCall,
|
|
987
|
-
});
|
|
988
|
-
};
|
|
989
|
-
const visiblePendingQuestionsForState = (state, context) => pendingQuestionsForState(state, context).filter(({ questionId }) => questionId !== 'commitCoderProposal' ||
|
|
990
|
-
hiddenDeferredOperationId === undefined);
|
|
991
|
-
const openDeferredOperation = (ledger = effectLedgerMirror) => {
|
|
992
|
-
if (sessionIdentity === undefined)
|
|
993
|
-
return undefined;
|
|
994
|
-
const open = ledger.logicalOperations.filter((operation) => operation.playbookId === sessionIdentity.playbookId &&
|
|
995
|
-
operation.runtimeSessionId === sessionIdentity.sessionId &&
|
|
996
|
-
operation.logicalReceipt === undefined);
|
|
997
|
-
if (open.length > 1) {
|
|
998
|
-
throw new TypeError('DECIDE runtime has multiple open deferred logical operations');
|
|
999
|
-
}
|
|
1000
|
-
return open[0];
|
|
1001
|
-
};
|
|
1002
|
-
const runtimeBoundaryIsOwned = (boundary) => sessionIdentity !== undefined &&
|
|
1003
|
-
boundary.playbookId === sessionIdentity.playbookId &&
|
|
1004
|
-
boundary.runtimeSessionId === sessionIdentity.sessionId;
|
|
1005
|
-
const governedOutcomesForBoundary = (boundary) => {
|
|
1006
|
-
const outcomes = DECIDE_OUTCOME_AUTHORITY.governedPlayerStates[boundary.sourceStateId];
|
|
1007
|
-
if (outcomes === undefined ||
|
|
1008
|
-
!isPlainObject(boundary.sourceOutcomeSchema)) {
|
|
1009
|
-
return undefined;
|
|
1010
|
-
}
|
|
1011
|
-
const authoredGuards = Object.keys(boundary.sourceOutcomeSchema).sort();
|
|
1012
|
-
const governedGuards = Object.keys(outcomes).sort();
|
|
1013
|
-
if (authoredGuards.length !== governedGuards.length ||
|
|
1014
|
-
authoredGuards.some((guard, index) => guard !== governedGuards[index])) {
|
|
1015
|
-
return undefined;
|
|
1016
|
-
}
|
|
1017
|
-
for (const guard of governedGuards) {
|
|
1018
|
-
const description = boundary.sourceOutcomeSchema[guard];
|
|
1019
|
-
if (typeof description !== 'string')
|
|
1020
|
-
return undefined;
|
|
1021
|
-
const authoredFields = [
|
|
1022
|
-
...new Set(requiredFieldsFor(description)),
|
|
1023
|
-
].sort();
|
|
1024
|
-
const governedFields = Object.keys(outcomes[guard].fields).sort();
|
|
1025
|
-
if (authoredFields.length !== governedFields.length ||
|
|
1026
|
-
authoredFields.some((field, index) => field !== governedFields[index])) {
|
|
1027
|
-
return undefined;
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
const dispositions = [
|
|
1031
|
-
...new Set(Object.values(outcomes).map(({ repositoryDisposition }) => repositoryDisposition)),
|
|
1032
|
-
];
|
|
1033
|
-
const actualDispositions = new Set(boundary.dispositions);
|
|
1034
|
-
return actualDispositions.size === boundary.dispositions.length &&
|
|
1035
|
-
actualDispositions.size === dispositions.length &&
|
|
1036
|
-
dispositions.every((disposition) => actualDispositions.has(disposition))
|
|
1037
|
-
? outcomes
|
|
1038
|
-
: undefined;
|
|
1039
|
-
};
|
|
1040
|
-
const persistedBoundaryReconciliation = (boundary, ledger) => {
|
|
1041
|
-
const outcomes = governedOutcomesForBoundary(boundary);
|
|
1042
|
-
if (outcomes === undefined || boundary.semanticCandidate === undefined) {
|
|
1043
|
-
return undefined;
|
|
1044
|
-
}
|
|
1045
|
-
let receipt = boundary.physicalReceipt;
|
|
1046
|
-
let awaitingLogicalReceipt = false;
|
|
1047
|
-
let historicalDeferred = false;
|
|
1048
|
-
const logicalOperation = boundary.logicalOperationId === undefined
|
|
1049
|
-
? undefined
|
|
1050
|
-
: ledger.logicalOperations.find(({ operationId }) => operationId === boundary.logicalOperationId);
|
|
1051
|
-
if (boundary.logicalOperationId !== undefined &&
|
|
1052
|
-
logicalOperation === undefined) {
|
|
1053
|
-
return undefined;
|
|
1054
|
-
}
|
|
1055
|
-
if (logicalOperation !== undefined) {
|
|
1056
|
-
if (logicalOperation.boundaryIds.at(-1) !== boundary.boundaryId) {
|
|
1057
|
-
historicalDeferred = true;
|
|
1058
|
-
}
|
|
1059
|
-
else if (logicalOperation.logicalReceipt !== undefined) {
|
|
1060
|
-
receipt = logicalOperation.logicalReceipt;
|
|
1061
|
-
}
|
|
1062
|
-
else if (logicalOperation.pendingQuestion === undefined ||
|
|
1063
|
-
logicalOperation.checkpoint === undefined ||
|
|
1064
|
-
!Object.prototype.hasOwnProperty.call(logicalOperation, 'playerContinuation')) {
|
|
1065
|
-
return undefined;
|
|
1066
|
-
}
|
|
1067
|
-
else {
|
|
1068
|
-
awaitingLogicalReceipt = true;
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
try {
|
|
1072
|
-
const reconciliation = reconcilePlaybookSemanticEvidence({
|
|
1073
|
-
outcomes,
|
|
1074
|
-
semanticCandidate: boundary.semanticCandidate,
|
|
1075
|
-
finalText: boundary.finalText,
|
|
1076
|
-
receipt,
|
|
1077
|
-
});
|
|
1078
|
-
if (awaitingLogicalReceipt && reconciliation.status !== 'deferred') {
|
|
1079
|
-
return undefined;
|
|
1080
|
-
}
|
|
1081
|
-
return { reconciliation, historicalDeferred };
|
|
1082
|
-
}
|
|
1083
|
-
catch {
|
|
1084
|
-
return undefined;
|
|
1085
|
-
}
|
|
1086
|
-
};
|
|
1087
|
-
const boundaryNeedsSemanticReconciliation = (boundary, ledger) => {
|
|
1088
|
-
if (!runtimeBoundaryIsOwned(boundary))
|
|
1089
|
-
return false;
|
|
1090
|
-
if (governedOutcomesForBoundary(boundary) === undefined)
|
|
1091
|
-
return true;
|
|
1092
|
-
const persisted = persistedBoundaryReconciliation(boundary, ledger);
|
|
1093
|
-
if (persisted !== undefined) {
|
|
1094
|
-
if (persisted.reconciliation.status === 'unresolved')
|
|
1095
|
-
return true;
|
|
1096
|
-
if (persisted.historicalDeferred) {
|
|
1097
|
-
return persisted.reconciliation.status !== 'deferred';
|
|
1098
|
-
}
|
|
1099
|
-
if (persisted.reconciliation.status === 'deferred' &&
|
|
1100
|
-
boundary.logicalOperationId === undefined) {
|
|
1101
|
-
return true;
|
|
1102
|
-
}
|
|
1103
|
-
return false;
|
|
1104
|
-
}
|
|
1105
|
-
if (boundary.physicalReceipt === undefined)
|
|
1106
|
-
return true;
|
|
1107
|
-
if (typeof boundary.finalText === 'string' &&
|
|
1108
|
-
boundary.finalText.trim().length > 0) {
|
|
1109
|
-
return true;
|
|
1110
|
-
}
|
|
1111
|
-
return boundary.physicalReceipt.classification !== 'unchanged';
|
|
1112
|
-
};
|
|
1113
|
-
const refreshUnresolvedSemanticReconciliation = (ledger = effectLedgerMirror) => {
|
|
1114
|
-
unresolvedSemanticBoundaryIds.clear();
|
|
1115
|
-
for (const boundary of ledger.boundaries) {
|
|
1116
|
-
if (boundaryNeedsSemanticReconciliation(boundary, ledger)) {
|
|
1117
|
-
unresolvedSemanticBoundaryIds.add(boundary.boundaryId);
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
};
|
|
1121
|
-
const synchronizeDeferredProjection = (ledger = effectLedgerMirror) => {
|
|
1122
|
-
if (deferredEffects === undefined)
|
|
1123
|
-
return;
|
|
1124
|
-
effectLedgerMirror = ledger;
|
|
1125
|
-
refreshUnresolvedSemanticReconciliation(ledger);
|
|
1126
|
-
const operation = openDeferredOperation(ledger);
|
|
1127
|
-
if (operation === undefined) {
|
|
1128
|
-
deferredOperationId = undefined;
|
|
1129
|
-
hiddenDeferredOperationId = undefined;
|
|
1130
|
-
return;
|
|
1131
|
-
}
|
|
1132
|
-
deferredOperationId = operation.operationId;
|
|
1133
|
-
hiddenDeferredOperationId =
|
|
1134
|
-
operation.logicalReceipt === undefined &&
|
|
1135
|
-
(operation.checkpointRestorationEligible ||
|
|
1136
|
-
operation.pendingQuestion === undefined)
|
|
1137
|
-
? operation.operationId
|
|
1138
|
-
: undefined;
|
|
1139
|
-
};
|
|
1140
|
-
const hasUnresolvedReconciliation = () => hiddenDeferredOperationId !== undefined ||
|
|
1141
|
-
unresolvedSemanticBoundaryIds.size > 0;
|
|
1142
|
-
const refreshReconciliationProjection = () => {
|
|
1143
|
-
synchronizeDeferredProjection(readEffectLedger());
|
|
1144
|
-
};
|
|
1145
|
-
const unresolvedEffectEnvelopeIdentities = () => {
|
|
1146
|
-
if (sessionIdentity === undefined)
|
|
1147
|
-
return [];
|
|
1148
|
-
refreshReconciliationProjection();
|
|
1149
|
-
if (!hasUnresolvedReconciliation())
|
|
1150
|
-
return [];
|
|
1151
|
-
const boundaryIds = new Set(unresolvedSemanticBoundaryIds);
|
|
1152
|
-
const operationIds = new Set();
|
|
1153
|
-
if (hiddenDeferredOperationId !== undefined) {
|
|
1154
|
-
operationIds.add(hiddenDeferredOperationId);
|
|
1155
|
-
}
|
|
1156
|
-
for (const boundaryId of [...boundaryIds]) {
|
|
1157
|
-
const boundary = effectLedgerMirror.boundaries.find((candidate) => candidate.boundaryId === boundaryId);
|
|
1158
|
-
const operation = boundary?.logicalOperationId === undefined
|
|
1159
|
-
? undefined
|
|
1160
|
-
: effectLedgerMirror.logicalOperations.find(({ operationId }) => operationId === boundary.logicalOperationId);
|
|
1161
|
-
if (operation === undefined)
|
|
1162
|
-
continue;
|
|
1163
|
-
operationIds.add(operation.operationId);
|
|
1164
|
-
for (const memberId of operation.boundaryIds) {
|
|
1165
|
-
boundaryIds.delete(memberId);
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
const ordered = [
|
|
1169
|
-
...[...boundaryIds].map((boundaryId) => ({
|
|
1170
|
-
order: effectLedgerMirror.boundaries.find((candidate) => candidate.boundaryId === boundaryId)?.sequence ?? Number.MAX_SAFE_INTEGER,
|
|
1171
|
-
value: { kind: 'boundary', boundaryId },
|
|
1172
|
-
})),
|
|
1173
|
-
...[...operationIds].map((operationId) => {
|
|
1174
|
-
const operation = effectLedgerMirror.logicalOperations.find((candidate) => candidate.operationId === operationId);
|
|
1175
|
-
return {
|
|
1176
|
-
order: effectLedgerMirror.boundaries.find(({ boundaryId }) => boundaryId === operation?.boundaryIds[0])?.sequence ?? Number.MAX_SAFE_INTEGER,
|
|
1177
|
-
value: { kind: 'logical-operation', operationId },
|
|
1178
|
-
};
|
|
1179
|
-
}),
|
|
1180
|
-
].sort((left, right) => left.order - right.order);
|
|
1181
|
-
return deepFreeze(snapshotJsonValue(ordered.map(({ value }) => value), 'DECIDE unresolved effect envelope identities'));
|
|
1182
|
-
};
|
|
1183
|
-
const stateIdentity = (state) => {
|
|
1184
|
-
return state.stateId === undefined ? {} : { stateId: state.stateId };
|
|
1185
|
-
};
|
|
1186
|
-
const enqueueTracedEmission = (type, payload, meta = {}, describedEmission, aborts) => {
|
|
1187
|
-
const trace = createTraceEvent(type, payload, meta);
|
|
1188
|
-
return enqueue(async () => {
|
|
1189
|
-
const runtimePorts = requirePorts();
|
|
1190
|
-
await runtimePorts.emitTelemetry({ topic: TRACE_TOPIC, payload: trace });
|
|
1191
|
-
await describedEmission?.(runtimePorts);
|
|
1192
|
-
}, aborts);
|
|
1193
|
-
};
|
|
1194
|
-
const createTraceEvent = (type, payload, meta = {}) => {
|
|
1195
|
-
const identity = requireSessionIdentity();
|
|
1196
|
-
const jsonPayload = snapshotJsonValue(payload, `trace ${type} payload`);
|
|
1197
|
-
return Object.freeze({
|
|
1198
|
-
schemaVersion: 4,
|
|
1199
|
-
sessionId: identity.sessionId,
|
|
1200
|
-
playbookId: identity.playbookId,
|
|
1201
|
-
rootSessionId: identity.rootSessionId,
|
|
1202
|
-
...(identity.parentSessionId !== undefined
|
|
1203
|
-
? { parentSessionId: identity.parentSessionId }
|
|
1204
|
-
: {}),
|
|
1205
|
-
...(identity.parentCallId !== undefined
|
|
1206
|
-
? { parentCallId: identity.parentCallId }
|
|
1207
|
-
: {}),
|
|
1208
|
-
depth: identity.depth,
|
|
1209
|
-
sequence: ++traceSequence,
|
|
1210
|
-
timestamp: Date.now(),
|
|
1211
|
-
type,
|
|
1212
|
-
...(meta.turnId !== undefined ? { turnId: meta.turnId } : {}),
|
|
1213
|
-
...(meta.callId !== undefined ? { callId: meta.callId } : {}),
|
|
1214
|
-
payload: jsonPayload,
|
|
1215
|
-
});
|
|
1216
|
-
};
|
|
1217
|
-
const emitTrace = (type, payload, meta = {}, aborts) => enqueueTracedEmission(type, payload, meta, undefined, aborts);
|
|
1218
|
-
const enqueueAcceptedOutcomeEmission = (acceptedOutcome, state, aborts) => {
|
|
1219
|
-
const message = `→ ${acceptedOutcome.acceptedOutcome}`;
|
|
1220
|
-
const acceptedTrace = createTraceEvent('outcome.accepted', acceptedOutcome, { turnId: currentTurnId });
|
|
1221
|
-
const statusTrace = createTraceEvent('status.emitted', { stateId: acceptedOutcome.target, message, state }, { turnId: currentTurnId });
|
|
1222
|
-
return enqueue(async () => {
|
|
1223
|
-
const runtimePorts = requirePorts();
|
|
1224
|
-
await runtimePorts.emitTelemetry({
|
|
1225
|
-
topic: TRACE_TOPIC,
|
|
1226
|
-
payload: acceptedTrace,
|
|
1227
|
-
});
|
|
1228
|
-
await runtimePorts.emitTelemetry({
|
|
1229
|
-
topic: TRACE_TOPIC,
|
|
1230
|
-
payload: statusTrace,
|
|
1231
|
-
});
|
|
1232
|
-
await runtimePorts.emitStatus(message);
|
|
1233
|
-
}, aborts);
|
|
1234
|
-
};
|
|
1235
|
-
const emitBoundaryStatus = async (message, state) => {
|
|
1236
|
-
const bossRelevantStateIds = state.activeStateIds.filter((stateId) => STATUS_STATE_IDS.has(stateId));
|
|
1237
|
-
await enqueueTracedEmission('status.emitted', {
|
|
1238
|
-
...(bossRelevantStateIds.length === 1
|
|
1239
|
-
? { stateId: bossRelevantStateIds[0] }
|
|
1240
|
-
: {}),
|
|
1241
|
-
message,
|
|
1242
|
-
state,
|
|
1243
|
-
}, { turnId: currentTurnId }, (runtimePorts) => runtimePorts.emitStatus(message));
|
|
1244
|
-
};
|
|
1245
|
-
const emitCallStarted = async (startedType, finishedType, identity, meta, signal) => {
|
|
1246
|
-
const aborts = abortReasonClassifier(signal);
|
|
1247
|
-
try {
|
|
1248
|
-
await emitTrace(startedType, identity, meta, aborts);
|
|
1249
|
-
}
|
|
1250
|
-
catch (error) {
|
|
1251
|
-
latchControlPlaneError(error, signal);
|
|
1252
|
-
try {
|
|
1253
|
-
await emitTrace(finishedType, {
|
|
1254
|
-
...identity,
|
|
1255
|
-
// A started-trace sink rejection causally identical to the
|
|
1256
|
-
// boundary reason is the abort's own evidence: the pair
|
|
1257
|
-
// finishes 'aborted', not 'error' (DR-036 §4).
|
|
1258
|
-
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
1259
|
-
error: normalizeErrorFull(error) ?? {
|
|
1260
|
-
name: 'Error',
|
|
1261
|
-
message: String(error),
|
|
67
|
+
const composePlayerPrompt = (input, promptIdentity, resuming) => renderPlayerPrompt(input, promptIdentity, resuming);
|
|
68
|
+
const runtimeSpec = {
|
|
69
|
+
composePlayerPrompt,
|
|
70
|
+
...{
|
|
71
|
+
"label": "DECIDE",
|
|
72
|
+
"compat": {
|
|
73
|
+
"artifactSchema": 3,
|
|
74
|
+
"runtimeAbi": 1
|
|
75
|
+
},
|
|
76
|
+
"entryEvent": {
|
|
77
|
+
"type": "START_DECIDE",
|
|
78
|
+
"textField": "callerTopic",
|
|
79
|
+
"contextField": "callerTopic"
|
|
80
|
+
},
|
|
81
|
+
"bossEvents": [
|
|
82
|
+
{
|
|
83
|
+
"type": "BOSS_INTERRUPT",
|
|
84
|
+
"fields": {
|
|
85
|
+
"targetId": {
|
|
86
|
+
"source": "judge",
|
|
87
|
+
"required": true,
|
|
88
|
+
"values": [
|
|
89
|
+
"independentProposals",
|
|
90
|
+
"synthesizeCommit"
|
|
91
|
+
]
|
|
1262
92
|
},
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
// Preserve the start failure after one best-effort finish attempt.
|
|
1267
|
-
}
|
|
1268
|
-
throw error;
|
|
1269
|
-
}
|
|
1270
|
-
};
|
|
1271
|
-
const runJudgeCall = async (prompt, signal, purpose, callStateId) => {
|
|
1272
|
-
const aborts = abortReasonClassifier(signal);
|
|
1273
|
-
const identity = {
|
|
1274
|
-
purpose,
|
|
1275
|
-
...(callStateId !== undefined ? { stateId: callStateId } : {}),
|
|
1276
|
-
};
|
|
1277
|
-
const queued = await judgeQueue.add(async () => {
|
|
1278
|
-
// Keep the complete queue task pending until an active coder promise
|
|
1279
|
-
// settles. PQueue's signal option may reject add() while that task is
|
|
1280
|
-
// still running, which would let the turn drain race a late finish.
|
|
1281
|
-
signal.throwIfAborted();
|
|
1282
|
-
const callId = `judge-${++judgeCallSequence}`;
|
|
1283
|
-
await emitCallStarted('judge.call.started', 'judge.call.finished', { ...identity, prompt }, { turnId: currentTurnId, callId }, signal);
|
|
1284
|
-
let finalText;
|
|
1285
|
-
try {
|
|
1286
|
-
signal.throwIfAborted();
|
|
1287
|
-
const reply = await requirePorts().callJudge(prompt, signal);
|
|
1288
|
-
if (typeof reply !== 'string') {
|
|
1289
|
-
throw new TypeError('judge result must be a string');
|
|
93
|
+
"callerTopic": {
|
|
94
|
+
"source": "text"
|
|
95
|
+
}
|
|
1290
96
|
}
|
|
1291
|
-
finalText = reply;
|
|
1292
|
-
// A cancelled parallel actor can outlive a judge port that ignores
|
|
1293
|
-
// its signal. Do not report that late resolution as success.
|
|
1294
|
-
signal.throwIfAborted();
|
|
1295
|
-
}
|
|
1296
|
-
catch (error) {
|
|
1297
|
-
latchControlPlaneError(error, signal);
|
|
1298
|
-
await emitTrace('judge.call.finished', {
|
|
1299
|
-
...identity,
|
|
1300
|
-
// Only the exact abort reason is cancellation; a distinct
|
|
1301
|
-
// failure under an aborted signal stays an error
|
|
1302
|
-
// (slc/link.md §Abort).
|
|
1303
|
-
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
1304
|
-
error: normalizeErrorFull(error) ?? {
|
|
1305
|
-
name: 'Error',
|
|
1306
|
-
message: String(error),
|
|
1307
|
-
},
|
|
1308
|
-
}, { turnId: currentTurnId, callId }, aborts);
|
|
1309
|
-
throw error;
|
|
1310
97
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
}
|
|
1317
|
-
return queued;
|
|
1318
|
-
};
|
|
1319
|
-
const callJudge = (prompt, signal, purpose, callStateId) => trackBoundaryCall(runJudgeCall(prompt, signal, purpose, callStateId));
|
|
1320
|
-
const runPlayerCall = async (input, signal, continuation) => {
|
|
1321
|
-
const aborts = abortReasonClassifier(signal);
|
|
1322
|
-
if (!ROLE_ID_SET.has(input.role)) {
|
|
1323
|
-
throw new TypeError(`DECIDE player input role must name a declared local role`);
|
|
1324
|
-
}
|
|
1325
|
-
const roleId = input.role;
|
|
1326
|
-
const playerId = resolvedPlayerId(roleId);
|
|
1327
|
-
const playerKey = continuationKey(roleId, playerId);
|
|
1328
|
-
const freshPrompt = composeInvocationPrompt(input);
|
|
1329
|
-
let resume;
|
|
1330
|
-
try {
|
|
1331
|
-
signal.throwIfAborted();
|
|
1332
|
-
resume =
|
|
1333
|
-
continuation !== undefined &&
|
|
1334
|
-
Object.prototype.hasOwnProperty.call(continuation, 'resume')
|
|
1335
|
-
? continuation.resume
|
|
1336
|
-
: selectPlayerResume(roleId, playerId);
|
|
1337
|
-
}
|
|
1338
|
-
catch (error) {
|
|
1339
|
-
latchControlPlaneError(error, signal);
|
|
1340
|
-
throw error;
|
|
1341
|
-
}
|
|
1342
|
-
const prompt = resume === false ? freshPrompt : composeInvocationPrompt(input, true);
|
|
1343
|
-
const callId = continuation?.callId ?? `player-${++playerCallSequence}`;
|
|
1344
|
-
const identity = {
|
|
1345
|
-
stateId: input.stateId,
|
|
1346
|
-
sourceItem: input.sourceItem,
|
|
1347
|
-
roleId,
|
|
1348
|
-
...(playerId === undefined ? {} : { playerId }),
|
|
1349
|
-
resume,
|
|
1350
|
-
};
|
|
1351
|
-
const emitFailure = (error) => emitTrace('player.call.finished', {
|
|
1352
|
-
...identity,
|
|
1353
|
-
// Only the exact abort reason is cancellation; a distinct
|
|
1354
|
-
// failure under an aborted signal stays an error
|
|
1355
|
-
// (slc/link.md §Abort).
|
|
1356
|
-
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
1357
|
-
error: normalizeErrorFull(error) ?? {
|
|
1358
|
-
name: 'Error',
|
|
1359
|
-
message: String(error),
|
|
98
|
+
],
|
|
99
|
+
"roleStates": {
|
|
100
|
+
"askCoderProposal": {
|
|
101
|
+
"role": "coder",
|
|
102
|
+
"label": "Coder is independently proposing a design for the topic."
|
|
1360
103
|
},
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt }, { turnId: currentTurnId, callId }, signal);
|
|
1365
|
-
await emitFailure(error);
|
|
1366
|
-
throw error;
|
|
1367
|
-
}
|
|
1368
|
-
inFlightPlayerKeys.add(playerKey);
|
|
1369
|
-
try {
|
|
1370
|
-
await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt }, { turnId: currentTurnId, callId }, signal);
|
|
1371
|
-
let rawResult;
|
|
1372
|
-
try {
|
|
1373
|
-
signal.throwIfAborted();
|
|
1374
|
-
const boundary = Promise.resolve(requirePorts().callPlayer(roleId, prompt, signal, { resume, ...(prompt === freshPrompt ? {} : { freshPrompt }) }));
|
|
1375
|
-
rawResult = await boundary;
|
|
1376
|
-
// An XState sibling cancellation does not cancel an arbitrary coder
|
|
1377
|
-
// promise. Re-check before a late resolution can mutate continuity or
|
|
1378
|
-
// masquerade as a successful boundary finish.
|
|
1379
|
-
signal.throwIfAborted();
|
|
1380
|
-
}
|
|
1381
|
-
catch (error) {
|
|
1382
|
-
// A rejected call produced no authoritative result, so the previous
|
|
1383
|
-
// token remains untouched. This also covers a coder promise that
|
|
1384
|
-
// resolves after its invocation signal was cancelled.
|
|
1385
|
-
latchControlPlaneError(error, signal);
|
|
1386
|
-
try {
|
|
1387
|
-
await emitFailure(error);
|
|
1388
|
-
}
|
|
1389
|
-
catch {
|
|
1390
|
-
// The original non-abort port rejection remains authoritative.
|
|
1391
|
-
}
|
|
1392
|
-
throw error;
|
|
1393
|
-
}
|
|
1394
|
-
let result;
|
|
1395
|
-
try {
|
|
1396
|
-
result = validatePlayerResult(rawResult);
|
|
1397
|
-
}
|
|
1398
|
-
catch (error) {
|
|
1399
|
-
latchControlPlaneError(error, signal);
|
|
1400
|
-
try {
|
|
1401
|
-
await emitFailure(error);
|
|
1402
|
-
}
|
|
1403
|
-
catch {
|
|
1404
|
-
// The malformed coder result remains authoritative.
|
|
1405
|
-
}
|
|
1406
|
-
throw error;
|
|
1407
|
-
}
|
|
1408
|
-
// The resolved result is authoritative even on aborted/error status.
|
|
1409
|
-
// Update continuation state before interpreting that status.
|
|
1410
|
-
try {
|
|
1411
|
-
updatePlayerResume(roleId, playerId, result);
|
|
1412
|
-
}
|
|
1413
|
-
catch (error) {
|
|
1414
|
-
latchControlPlaneError(error, signal);
|
|
1415
|
-
try {
|
|
1416
|
-
await emitFailure(error);
|
|
1417
|
-
}
|
|
1418
|
-
catch {
|
|
1419
|
-
// The continuation-store failure remains authoritative.
|
|
1420
|
-
}
|
|
1421
|
-
throw error;
|
|
1422
|
-
}
|
|
1423
|
-
// Keep this finish outside the boundary catch. A trace sink can record
|
|
1424
|
-
// the event and then reject; retrying from that catch would duplicate the
|
|
1425
|
-
// same call id and falsely recast an emission failure as a player error.
|
|
1426
|
-
await emitTrace('player.call.finished', {
|
|
1427
|
-
...identity,
|
|
1428
|
-
status: result.status,
|
|
1429
|
-
...(result.resumeToken !== undefined
|
|
1430
|
-
? { resumeToken: result.resumeToken }
|
|
1431
|
-
: {}),
|
|
1432
|
-
...(result.finalText !== undefined
|
|
1433
|
-
? { finalText: result.finalText }
|
|
1434
|
-
: {}),
|
|
1435
|
-
...(result.error !== undefined
|
|
1436
|
-
? { error: normalizeErrorFull(result.error) }
|
|
1437
|
-
: {}),
|
|
1438
|
-
}, { turnId: currentTurnId, callId }, aborts);
|
|
1439
|
-
return {
|
|
1440
|
-
roleId,
|
|
1441
|
-
...(playerId === undefined ? {} : { playerId }),
|
|
1442
|
-
callId,
|
|
1443
|
-
result,
|
|
1444
|
-
};
|
|
1445
|
-
}
|
|
1446
|
-
finally {
|
|
1447
|
-
inFlightPlayerKeys.delete(playerKey);
|
|
1448
|
-
}
|
|
1449
|
-
};
|
|
1450
|
-
const governedBoundarySeed = (input, callId) => {
|
|
1451
|
-
const outcomes = governedOutcomesFor(input);
|
|
1452
|
-
if (currentTurnId === undefined ||
|
|
1453
|
-
!Number.isSafeInteger(currentTurnId) ||
|
|
1454
|
-
currentTurnId <= 0) {
|
|
1455
|
-
throw new Error('DECIDE governed player call requires an active positive turn id');
|
|
1456
|
-
}
|
|
1457
|
-
return {
|
|
1458
|
-
boundaryId: randomUUID(),
|
|
1459
|
-
runtimeSessionId: requireSessionIdentity().sessionId,
|
|
1460
|
-
turnId: currentTurnId,
|
|
1461
|
-
callId,
|
|
1462
|
-
roleId: input.role,
|
|
1463
|
-
sourceStateId: input.stateId,
|
|
1464
|
-
sourceOutcomeSchema: snapshotJsonValue(input.result, 'DECIDE governed player source outcome schema'),
|
|
1465
|
-
dispositions: [
|
|
1466
|
-
...new Set(Object.values(outcomes).map(({ repositoryDisposition }) => repositoryDisposition)),
|
|
1467
|
-
],
|
|
1468
|
-
correctionBudget: { limit: 1, spent: false },
|
|
1469
|
-
};
|
|
1470
|
-
};
|
|
1471
|
-
const unresolvedGovernedEvidence = (boundaryId, reason, error) => {
|
|
1472
|
-
governedFailuresByBoundaryId.set(boundaryId, error instanceof Error
|
|
1473
|
-
? error
|
|
1474
|
-
: new Error(`DECIDE governed outcome remains unresolved: ${reason}`));
|
|
1475
|
-
};
|
|
1476
|
-
const spendSemanticCorrectionBudget = async (boundary, receipt, finalText, semanticCandidate) => {
|
|
1477
|
-
const ledger = readEffectLedger();
|
|
1478
|
-
const current = ledger.boundaries.find(({ boundaryId }) => boundaryId === boundary.boundaryId);
|
|
1479
|
-
if (current === undefined ||
|
|
1480
|
-
current.correctionBudget.limit !== 1 ||
|
|
1481
|
-
current.correctionBudget.spent) {
|
|
1482
|
-
return false;
|
|
1483
|
-
}
|
|
1484
|
-
if (current.finalText !== undefined && current.finalText !== finalText) {
|
|
1485
|
-
throw new TypeError('DECIDE correction budget conflicts with retained finalText');
|
|
1486
|
-
}
|
|
1487
|
-
if (current.physicalReceipt !== undefined &&
|
|
1488
|
-
stableJson(current.physicalReceipt, 'DECIDE retained receipt') !==
|
|
1489
|
-
stableJson(receipt, 'DECIDE correction receipt')) {
|
|
1490
|
-
throw new TypeError('DECIDE correction budget conflicts with retained repository receipt');
|
|
1491
|
-
}
|
|
1492
|
-
if (semanticCandidate !== undefined &&
|
|
1493
|
-
current.semanticCandidate !== undefined &&
|
|
1494
|
-
stableJson(current.semanticCandidate, 'DECIDE retained candidate') !==
|
|
1495
|
-
stableJson(semanticCandidate, 'DECIDE correction candidate')) {
|
|
1496
|
-
throw new TypeError('DECIDE correction budget conflicts with retained semantic candidate');
|
|
1497
|
-
}
|
|
1498
|
-
const next = {
|
|
1499
|
-
...current,
|
|
1500
|
-
...(receipt.after === undefined ? {} : { after: receipt.after }),
|
|
1501
|
-
physicalReceipt: receipt,
|
|
1502
|
-
finalText,
|
|
1503
|
-
...(semanticCandidate === undefined ? {} : { semanticCandidate }),
|
|
1504
|
-
correctionBudget: { limit: 1, spent: true },
|
|
1505
|
-
};
|
|
1506
|
-
const cohortMembers = current.cohortId === undefined
|
|
1507
|
-
? [current]
|
|
1508
|
-
: ledger.boundaries.filter(({ cohortId }) => cohortId === current.cohortId);
|
|
1509
|
-
if (cohortMembers.length === 0) {
|
|
1510
|
-
throw new TypeError('DECIDE correction boundary lost its repository cohort');
|
|
1511
|
-
}
|
|
1512
|
-
const replacements = cohortMembers.map((member) => ({
|
|
1513
|
-
expected: member,
|
|
1514
|
-
next: member.boundaryId === current.boundaryId
|
|
1515
|
-
? next
|
|
1516
|
-
: {
|
|
1517
|
-
...member,
|
|
1518
|
-
...(receipt.after === undefined
|
|
1519
|
-
? {}
|
|
1520
|
-
: { after: receipt.after }),
|
|
1521
|
-
physicalReceipt: receipt,
|
|
1522
|
-
},
|
|
1523
|
-
}));
|
|
1524
|
-
const acknowledged = assertPlaybookEffectLedger(await deferredEffects.effectLedger.writeAhead([
|
|
1525
|
-
{
|
|
1526
|
-
kind: 'replace-boundaries',
|
|
1527
|
-
// A cohort's shared receipt must become visible for every member in
|
|
1528
|
-
// one ledger revision. Publishing only this member's correction
|
|
1529
|
-
// spend would transiently create an invalid half-complete cohort.
|
|
1530
|
-
replacements,
|
|
104
|
+
"askReviewerProposal": {
|
|
105
|
+
"role": "reviewer",
|
|
106
|
+
"label": "Reviewer is independently proposing a design for the topic."
|
|
1531
107
|
},
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
if (spent === undefined ||
|
|
1536
|
-
stableJson(spent, 'DECIDE acknowledged correction boundary') !==
|
|
1537
|
-
stableJson(next, 'DECIDE expected correction boundary')) {
|
|
1538
|
-
throw new TypeError('DECIDE semantic correction budget spend was not acknowledged exactly');
|
|
1539
|
-
}
|
|
1540
|
-
return true;
|
|
1541
|
-
};
|
|
1542
|
-
const completionEvidenceFor = (input, roleId, playerId, signal, operationId) => async (completion) => {
|
|
1543
|
-
const queued = await semanticCompletionQueue.add(async () => {
|
|
1544
|
-
const { boundary, operation } = completion;
|
|
1545
|
-
const outcomes = governedOutcomesFor(input);
|
|
1546
|
-
const expectedDispositions = [
|
|
1547
|
-
...new Set(Object.values(outcomes).map(({ repositoryDisposition }) => repositoryDisposition)),
|
|
1548
|
-
];
|
|
1549
|
-
const session = requireSessionIdentity();
|
|
1550
|
-
if (boundary.playbookId !== session.playbookId ||
|
|
1551
|
-
boundary.runtimeSessionId !== session.sessionId ||
|
|
1552
|
-
boundary.turnId !== currentTurnId ||
|
|
1553
|
-
boundary.roleId !== roleId ||
|
|
1554
|
-
(completion.roleId !== undefined && completion.roleId !== roleId) ||
|
|
1555
|
-
boundary.sourceStateId !== input.stateId ||
|
|
1556
|
-
stableJson(boundary.dispositions, 'DECIDE boundary dispositions') !==
|
|
1557
|
-
stableJson(expectedDispositions, 'DECIDE authority dispositions') ||
|
|
1558
|
-
stableJson(boundary.sourceOutcomeSchema, 'DECIDE boundary source schema') !==
|
|
1559
|
-
stableJson(input.result, 'DECIDE authored source schema')) {
|
|
1560
|
-
throw new TypeError('DECIDE governed semantic reconciliation source schema changed');
|
|
1561
|
-
}
|
|
1562
|
-
governedReceiptsByBoundaryId.set(boundary.boundaryId, {
|
|
1563
|
-
physicalReceipt: completion.receipt,
|
|
1564
|
-
outcomeReceipt: completion.outcomeReceipt,
|
|
1565
|
-
});
|
|
1566
|
-
if (operation.status !== 'fulfilled' ||
|
|
1567
|
-
operation.value.status !== 'ok' ||
|
|
1568
|
-
isEmptyFinalText(operation.value.finalText)) {
|
|
1569
|
-
const incomplete = operation.status === 'fulfilled' &&
|
|
1570
|
-
operation.value.status === 'ok' &&
|
|
1571
|
-
operation.value.finalText !== undefined
|
|
1572
|
-
? { finalText: operation.value.finalText }
|
|
1573
|
-
: {};
|
|
1574
|
-
if (operation.status === 'fulfilled' &&
|
|
1575
|
-
operation.value.status === 'ok' &&
|
|
1576
|
-
operation.value.finalText !== undefined) {
|
|
1577
|
-
governedEvidenceByBoundaryId.set(boundary.boundaryId, {
|
|
1578
|
-
finalText: operation.value.finalText,
|
|
1579
|
-
});
|
|
1580
|
-
}
|
|
1581
|
-
return operationId === undefined
|
|
1582
|
-
? incomplete
|
|
1583
|
-
: { ...incomplete, unresolved: true };
|
|
1584
|
-
}
|
|
1585
|
-
const finalText = operation.value.finalText;
|
|
1586
|
-
let raw;
|
|
1587
|
-
try {
|
|
1588
|
-
raw = await callJudge(buildAdjudicatorPrompt(input, finalText), signal, 'player-output-adjudication', input.stateId);
|
|
1589
|
-
}
|
|
1590
|
-
catch (error) {
|
|
1591
|
-
unresolvedGovernedEvidence(boundary.boundaryId, 'judge transport failed', error);
|
|
1592
|
-
governedEvidenceByBoundaryId.set(boundary.boundaryId, { finalText });
|
|
1593
|
-
return { finalText, unresolved: true };
|
|
1594
|
-
}
|
|
1595
|
-
let candidate;
|
|
1596
|
-
let retainedCandidate;
|
|
1597
|
-
let reconciliation;
|
|
1598
|
-
let structuralError;
|
|
1599
|
-
const retainCandidate = (value) => {
|
|
1600
|
-
try {
|
|
1601
|
-
retainedCandidate = snapshotJsonValue(value, 'DECIDE recoverable governed semantic candidate');
|
|
1602
|
-
}
|
|
1603
|
-
catch {
|
|
1604
|
-
// A non-detachable reply still retains presentation and receipt.
|
|
1605
|
-
}
|
|
1606
|
-
};
|
|
1607
|
-
try {
|
|
1608
|
-
candidate = parseGovernedSemanticCandidate(raw);
|
|
1609
|
-
retainCandidate(candidate);
|
|
1610
|
-
reconciliation = reconcilePlaybookSemanticEvidence({
|
|
1611
|
-
outcomes,
|
|
1612
|
-
semanticCandidate: candidate,
|
|
1613
|
-
finalText,
|
|
1614
|
-
receipt: completion.outcomeReceipt,
|
|
1615
|
-
});
|
|
1616
|
-
}
|
|
1617
|
-
catch (error) {
|
|
1618
|
-
if (!(error instanceof PlaybookSemanticCandidateStructureError)) {
|
|
1619
|
-
throw error;
|
|
1620
|
-
}
|
|
1621
|
-
structuralError = error;
|
|
1622
|
-
}
|
|
1623
|
-
if (structuralError !== undefined) {
|
|
1624
|
-
const spent = await spendSemanticCorrectionBudget(boundary, completion.receipt, finalText, retainedCandidate);
|
|
1625
|
-
if (!spent || signal.aborted) {
|
|
1626
|
-
unresolvedGovernedEvidence(boundary.boundaryId, 'semantic correction budget is unavailable', signal.aborted ? signal.reason : undefined);
|
|
1627
|
-
governedEvidenceByBoundaryId.set(boundary.boundaryId, {
|
|
1628
|
-
finalText,
|
|
1629
|
-
...(retainedCandidate === undefined
|
|
1630
|
-
? {}
|
|
1631
|
-
: { semanticCandidate: retainedCandidate }),
|
|
1632
|
-
});
|
|
1633
|
-
return {
|
|
1634
|
-
finalText,
|
|
1635
|
-
...(retainedCandidate === undefined
|
|
1636
|
-
? {}
|
|
1637
|
-
: { semanticCandidate: retainedCandidate }),
|
|
1638
|
-
unresolved: true,
|
|
1639
|
-
};
|
|
1640
|
-
}
|
|
1641
|
-
let correctiveRaw;
|
|
1642
|
-
try {
|
|
1643
|
-
correctiveRaw = await callJudge(buildAdjudicatorPrompt(input, finalText, {
|
|
1644
|
-
reply: raw,
|
|
1645
|
-
error: structuralError.message,
|
|
1646
|
-
}), signal, 'player-output-adjudication', input.stateId);
|
|
1647
|
-
}
|
|
1648
|
-
catch (error) {
|
|
1649
|
-
unresolvedGovernedEvidence(boundary.boundaryId, 'corrective judge failed', error);
|
|
1650
|
-
governedEvidenceByBoundaryId.set(boundary.boundaryId, {
|
|
1651
|
-
finalText,
|
|
1652
|
-
...(retainedCandidate === undefined
|
|
1653
|
-
? {}
|
|
1654
|
-
: { semanticCandidate: retainedCandidate }),
|
|
1655
|
-
});
|
|
1656
|
-
return {
|
|
1657
|
-
finalText,
|
|
1658
|
-
...(retainedCandidate === undefined
|
|
1659
|
-
? {}
|
|
1660
|
-
: { semanticCandidate: retainedCandidate }),
|
|
1661
|
-
unresolved: true,
|
|
1662
|
-
};
|
|
1663
|
-
}
|
|
1664
|
-
try {
|
|
1665
|
-
candidate = parseGovernedSemanticCandidate(correctiveRaw);
|
|
1666
|
-
retainCandidate(candidate);
|
|
1667
|
-
reconciliation = reconcilePlaybookSemanticEvidence({
|
|
1668
|
-
outcomes,
|
|
1669
|
-
semanticCandidate: candidate,
|
|
1670
|
-
finalText,
|
|
1671
|
-
receipt: completion.outcomeReceipt,
|
|
1672
|
-
});
|
|
1673
|
-
}
|
|
1674
|
-
catch (error) {
|
|
1675
|
-
if (!(error instanceof PlaybookSemanticCandidateStructureError)) {
|
|
1676
|
-
throw error;
|
|
1677
|
-
}
|
|
1678
|
-
unresolvedGovernedEvidence(boundary.boundaryId, 'corrective semantic candidate is invalid');
|
|
1679
|
-
governedEvidenceByBoundaryId.set(boundary.boundaryId, {
|
|
1680
|
-
finalText,
|
|
1681
|
-
...(retainedCandidate === undefined
|
|
1682
|
-
? {}
|
|
1683
|
-
: { semanticCandidate: retainedCandidate }),
|
|
1684
|
-
});
|
|
1685
|
-
return {
|
|
1686
|
-
finalText,
|
|
1687
|
-
...(retainedCandidate === undefined
|
|
1688
|
-
? {}
|
|
1689
|
-
: { semanticCandidate: retainedCandidate }),
|
|
1690
|
-
unresolved: true,
|
|
1691
|
-
};
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
if (reconciliation === undefined) {
|
|
1695
|
-
throw new Error('DECIDE semantic reconciliation produced no decision');
|
|
1696
|
-
}
|
|
1697
|
-
const semanticCandidate = snapshotJsonValue(reconciliation.evidence.semanticCandidate, 'DECIDE governed semantic candidate');
|
|
1698
|
-
governedEvidenceByBoundaryId.set(boundary.boundaryId, {
|
|
1699
|
-
finalText,
|
|
1700
|
-
semanticCandidate,
|
|
1701
|
-
});
|
|
1702
|
-
if (reconciliation.status === 'unresolved') {
|
|
1703
|
-
unresolvedGovernedEvidence(boundary.boundaryId, reconciliation.reason);
|
|
1704
|
-
return { finalText, semanticCandidate, unresolved: true };
|
|
1705
|
-
}
|
|
1706
|
-
const output = reconciliation.output;
|
|
1707
|
-
governedOutputsByBoundaryId.set(boundary.boundaryId, output);
|
|
1708
|
-
if (reconciliation.status !== 'deferred') {
|
|
1709
|
-
return { finalText, semanticCandidate };
|
|
1710
|
-
}
|
|
1711
|
-
if (output.guard !== 'needsBossReply' ||
|
|
1712
|
-
typeof output.question !== 'string' ||
|
|
1713
|
-
output.question.trim() === '') {
|
|
1714
|
-
throw new TypeError('DECIDE deferred outcome must carry one exact Boss question');
|
|
1715
|
-
}
|
|
1716
|
-
const pendingQuestion = {
|
|
1717
|
-
questionId: input.stateId,
|
|
1718
|
-
asker: { kind: 'role', roleId },
|
|
1719
|
-
question: output.question,
|
|
1720
|
-
sourceItem: input.sourceItem,
|
|
1721
|
-
};
|
|
1722
|
-
return {
|
|
1723
|
-
finalText,
|
|
1724
|
-
semanticCandidate,
|
|
1725
|
-
deferred: {
|
|
1726
|
-
operationId: operationId ?? randomUUID(),
|
|
1727
|
-
pendingQuestion,
|
|
1728
|
-
playerContinuation: snapshotJsonValue(selectPlayerResume(roleId, playerId), 'DECIDE deferred player continuation'),
|
|
1729
|
-
},
|
|
1730
|
-
};
|
|
1731
|
-
});
|
|
1732
|
-
if (queued === undefined) {
|
|
1733
|
-
throw new Error('DECIDE semantic completion produced no evidence');
|
|
1734
|
-
}
|
|
1735
|
-
return queued;
|
|
1736
|
-
};
|
|
1737
|
-
const acknowledgeGovernedPlayerResult = (value, boundaryId) => {
|
|
1738
|
-
const ledger = assertPlaybookEffectLedger(value.effectLedger, 'DECIDE repository settlement effect ledger');
|
|
1739
|
-
const completed = ledger.boundaries.find((candidate) => candidate.boundaryId === boundaryId);
|
|
1740
|
-
if (completed === undefined ||
|
|
1741
|
-
completed.physicalReceipt === undefined ||
|
|
1742
|
-
stableJson(completed.physicalReceipt, 'DECIDE completed receipt') !==
|
|
1743
|
-
stableJson(value.receipt, 'DECIDE acknowledged receipt')) {
|
|
1744
|
-
throw new TypeError('DECIDE repository settlement did not acknowledge its completed boundary');
|
|
1745
|
-
}
|
|
1746
|
-
const expectedEvidence = governedEvidenceByBoundaryId.get(boundaryId);
|
|
1747
|
-
const expectedReceipts = governedReceiptsByBoundaryId.get(boundaryId);
|
|
1748
|
-
if (expectedReceipts !== undefined &&
|
|
1749
|
-
stableJson(completed.physicalReceipt, 'DECIDE completed physical receipt') !==
|
|
1750
|
-
stableJson(expectedReceipts.physicalReceipt, 'DECIDE reconciled physical receipt')) {
|
|
1751
|
-
throw new TypeError('DECIDE repository settlement changed the physical receipt used during reconciliation');
|
|
1752
|
-
}
|
|
1753
|
-
if (expectedEvidence !== undefined &&
|
|
1754
|
-
(completed.finalText !== expectedEvidence.finalText ||
|
|
1755
|
-
(expectedEvidence.semanticCandidate === undefined
|
|
1756
|
-
? completed.semanticCandidate !== undefined
|
|
1757
|
-
: completed.semanticCandidate === undefined ||
|
|
1758
|
-
stableJson(completed.semanticCandidate, 'DECIDE completed semantic candidate') !==
|
|
1759
|
-
stableJson(expectedEvidence.semanticCandidate, 'DECIDE expected semantic candidate')))) {
|
|
1760
|
-
throw new TypeError('DECIDE repository settlement did not acknowledge its exact governed evidence');
|
|
1761
|
-
}
|
|
1762
|
-
governedEvidenceByBoundaryId.delete(boundaryId);
|
|
1763
|
-
governedReceiptsByBoundaryId.delete(boundaryId);
|
|
1764
|
-
synchronizeDeferredProjection(ledger);
|
|
1765
|
-
if (value.operation.status === 'rejected') {
|
|
1766
|
-
throw value.operation.reason;
|
|
1767
|
-
}
|
|
1768
|
-
const result = validatePlayerResult(value.operation.value);
|
|
1769
|
-
const output = governedOutputsByBoundaryId.get(boundaryId);
|
|
1770
|
-
const failure = governedFailuresByBoundaryId.get(boundaryId);
|
|
1771
|
-
if (expectedReceipts !== undefined) {
|
|
1772
|
-
const continued = 'status' in value && value.status === 'continued'
|
|
1773
|
-
? value
|
|
1774
|
-
: undefined;
|
|
1775
|
-
const acknowledgedOutcomeReceipt = continued === undefined ? value.receipt : continued.logicalReceipt;
|
|
1776
|
-
if (acknowledgedOutcomeReceipt !== undefined &&
|
|
1777
|
-
stableJson(acknowledgedOutcomeReceipt, 'DECIDE acknowledged outcome receipt') !==
|
|
1778
|
-
stableJson(expectedReceipts.outcomeReceipt, 'DECIDE reconciled outcome receipt')) {
|
|
1779
|
-
throw new TypeError('DECIDE repository settlement changed the outcome receipt used during reconciliation');
|
|
1780
|
-
}
|
|
1781
|
-
if (continued !== undefined &&
|
|
1782
|
-
output !== undefined &&
|
|
1783
|
-
output.guard !== 'needsBossReply' &&
|
|
1784
|
-
acknowledgedOutcomeReceipt === undefined) {
|
|
1785
|
-
throw new TypeError('DECIDE completed deferred continuation omitted its reconciled logical receipt');
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
governedOutputsByBoundaryId.delete(boundaryId);
|
|
1789
|
-
governedFailuresByBoundaryId.delete(boundaryId);
|
|
1790
|
-
const linkedOperationId = completed.logicalOperationId;
|
|
1791
|
-
if (value.deferredStatus === 'unresolved') {
|
|
1792
|
-
if (linkedOperationId === undefined) {
|
|
1793
|
-
throw new TypeError('DECIDE unresolved deferred settlement omitted its logical operation');
|
|
1794
|
-
}
|
|
1795
|
-
deferredOperationId = linkedOperationId;
|
|
1796
|
-
hiddenDeferredOperationId = linkedOperationId;
|
|
1797
|
-
}
|
|
1798
|
-
else if (value.deferredStatus === 'bound') {
|
|
1799
|
-
if (linkedOperationId === undefined) {
|
|
1800
|
-
throw new TypeError('DECIDE bound deferred settlement omitted its logical operation');
|
|
1801
|
-
}
|
|
1802
|
-
deferredOperationId = linkedOperationId;
|
|
1803
|
-
hiddenDeferredOperationId = undefined;
|
|
1804
|
-
}
|
|
1805
|
-
else if ('status' in value &&
|
|
1806
|
-
value.status === 'continued' &&
|
|
1807
|
-
value
|
|
1808
|
-
.logicalReceipt === undefined &&
|
|
1809
|
-
linkedOperationId !== undefined) {
|
|
1810
|
-
deferredOperationId = linkedOperationId;
|
|
1811
|
-
hiddenDeferredOperationId = linkedOperationId;
|
|
1812
|
-
}
|
|
1813
|
-
else if (linkedOperationId !== undefined) {
|
|
1814
|
-
deferredOperationId = undefined;
|
|
1815
|
-
hiddenDeferredOperationId = undefined;
|
|
1816
|
-
}
|
|
1817
|
-
if (failure !== undefined)
|
|
1818
|
-
throw failure;
|
|
1819
|
-
if (value.deferredStatus === 'unresolved') {
|
|
1820
|
-
throw new Error('DECIDE deferred repository settlement remains unresolved');
|
|
1821
|
-
}
|
|
1822
|
-
if (output !== undefined) {
|
|
1823
|
-
governedPlayerOutputs.set(result, output);
|
|
1824
|
-
}
|
|
1825
|
-
else if (result.status === 'ok' &&
|
|
1826
|
-
!isEmptyFinalText(result.finalText)) {
|
|
1827
|
-
throw new Error('DECIDE governed player result has no reconciled semantic output');
|
|
1828
|
-
}
|
|
1829
|
-
return result;
|
|
1830
|
-
};
|
|
1831
|
-
const shouldRunProposalCohort = (input) => {
|
|
1832
|
-
if (input.stateId !== PROPOSAL_STATE_BY_ROLE[input.role] ||
|
|
1833
|
-
completedProposalCohortTurnId === currentTurnId) {
|
|
1834
|
-
return false;
|
|
1835
|
-
}
|
|
1836
|
-
const state = currentState();
|
|
1837
|
-
return Object.values(PROPOSAL_STATE_BY_ROLE).every((stateId) => state.activeStateIds.includes(stateId));
|
|
1838
|
-
};
|
|
1839
|
-
const settleProposalCohort = async () => {
|
|
1840
|
-
const members = Object.fromEntries(ROLE_IDS.map((roleId) => [roleId, pendingProposalCohort.get(roleId)]));
|
|
1841
|
-
const turnId = currentTurnId;
|
|
1842
|
-
const cohortSignal = currentSignal;
|
|
1843
|
-
const cohortOperations = new AbortController();
|
|
1844
|
-
const completionOrder = [];
|
|
1845
|
-
try {
|
|
1846
|
-
if (turnId === undefined ||
|
|
1847
|
-
cohortSignal === undefined ||
|
|
1848
|
-
members.coder === undefined ||
|
|
1849
|
-
members.reviewer === undefined) {
|
|
1850
|
-
throw new Error('DECIDE proposal cohort started without both governed members');
|
|
1851
|
-
}
|
|
1852
|
-
for (const roleId of ROLE_IDS)
|
|
1853
|
-
members[roleId].signal.throwIfAborted();
|
|
1854
|
-
const operations = Object.fromEntries(ROLE_IDS.map((roleId) => [
|
|
1855
|
-
roleId,
|
|
1856
|
-
async () => {
|
|
1857
|
-
const member = members[roleId];
|
|
1858
|
-
try {
|
|
1859
|
-
const operationSignal = combineSignals(member.signal, cohortOperations.signal);
|
|
1860
|
-
const envelope = await runPlayerCall(member.input, operationSignal, { callId: member.callId });
|
|
1861
|
-
member.envelope = envelope;
|
|
1862
|
-
if (envelope.result.status !== 'ok' &&
|
|
1863
|
-
!cohortOperations.signal.aborted) {
|
|
1864
|
-
cohortOperations.abort(new Error(`DECIDE proposal cohort ${roleId} returned status ${JSON.stringify(envelope.result.status)}`));
|
|
1865
|
-
}
|
|
1866
|
-
return envelope.result;
|
|
1867
|
-
}
|
|
1868
|
-
catch (error) {
|
|
1869
|
-
if (!cohortOperations.signal.aborted) {
|
|
1870
|
-
cohortOperations.abort(error);
|
|
1871
|
-
}
|
|
1872
|
-
throw error;
|
|
1873
|
-
}
|
|
1874
|
-
finally {
|
|
1875
|
-
completionOrder.push(roleId);
|
|
1876
|
-
}
|
|
1877
|
-
},
|
|
1878
|
-
]));
|
|
1879
|
-
const invocationId = randomUUID();
|
|
1880
|
-
const settled = await deferredEffects.repository.runCohort({
|
|
1881
|
-
signal: cohortSignal,
|
|
1882
|
-
invocationId,
|
|
1883
|
-
roleIds: ROLE_IDS,
|
|
1884
|
-
dispositionsByRole: {
|
|
1885
|
-
coder: ['unchanged'],
|
|
1886
|
-
reviewer: ['unchanged'],
|
|
1887
|
-
},
|
|
1888
|
-
effectBoundaries: {
|
|
1889
|
-
coder: members.coder.effectBoundary,
|
|
1890
|
-
reviewer: members.reviewer.effectBoundary,
|
|
1891
|
-
},
|
|
1892
|
-
operations,
|
|
1893
|
-
completeEffectBoundary: (completion) => {
|
|
1894
|
-
const member = members[completion.roleId];
|
|
1895
|
-
if (member === undefined) {
|
|
1896
|
-
throw new TypeError(`DECIDE proposal cohort completed unknown role ${String(completion.roleId)}`);
|
|
1897
|
-
}
|
|
1898
|
-
return completionEvidenceFor(member.input, completion.roleId, resolvedPlayerId(completion.roleId), member.signal)(completion);
|
|
1899
|
-
},
|
|
1900
|
-
});
|
|
1901
|
-
if (settled.invocationId !== invocationId) {
|
|
1902
|
-
throw new TypeError('DECIDE repository cohort changed its invocation identity');
|
|
1903
|
-
}
|
|
1904
|
-
const releaseOrder = [
|
|
1905
|
-
...completionOrder,
|
|
1906
|
-
...ROLE_IDS.filter((roleId) => !completionOrder.includes(roleId)),
|
|
1907
|
-
];
|
|
1908
|
-
const acknowledgedResults = new Map();
|
|
1909
|
-
const acknowledgementFailures = [];
|
|
1910
|
-
for (const roleId of releaseOrder) {
|
|
1911
|
-
const member = members[roleId];
|
|
1912
|
-
try {
|
|
1913
|
-
const result = acknowledgeGovernedPlayerResult({
|
|
1914
|
-
operation: settled.operations[roleId],
|
|
1915
|
-
receipt: settled.receipts[roleId],
|
|
1916
|
-
effectLedger: settled.effectLedger,
|
|
1917
|
-
}, member.effectBoundary.boundaryId);
|
|
1918
|
-
if (member.envelope === undefined) {
|
|
1919
|
-
throw new TypeError(`DECIDE repository cohort omitted its ${roleId} invocation`);
|
|
1920
|
-
}
|
|
1921
|
-
acknowledgedResults.set(roleId, result);
|
|
1922
|
-
}
|
|
1923
|
-
catch (error) {
|
|
1924
|
-
acknowledgementFailures.push(error);
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
if (acknowledgementFailures.length > 0) {
|
|
1928
|
-
for (const result of acknowledgedResults.values()) {
|
|
1929
|
-
governedPlayerOutputs.delete(result);
|
|
1930
|
-
}
|
|
1931
|
-
throw acknowledgementFailures.length === 1
|
|
1932
|
-
? acknowledgementFailures[0]
|
|
1933
|
-
: new AggregateError(acknowledgementFailures, 'DECIDE proposal cohort reconciliation failed');
|
|
1934
|
-
}
|
|
1935
|
-
for (const roleId of releaseOrder) {
|
|
1936
|
-
const member = members[roleId];
|
|
1937
|
-
const result = acknowledgedResults.get(roleId);
|
|
1938
|
-
if (member.envelope === undefined || result === undefined) {
|
|
1939
|
-
throw new Error(`DECIDE proposal cohort lost its ${roleId} acknowledgement`);
|
|
1940
|
-
}
|
|
1941
|
-
member.result.resolve({ ...member.envelope, result });
|
|
1942
|
-
}
|
|
1943
|
-
}
|
|
1944
|
-
catch (error) {
|
|
1945
|
-
for (const member of Object.values(members))
|
|
1946
|
-
member?.result.reject(error);
|
|
1947
|
-
}
|
|
1948
|
-
finally {
|
|
1949
|
-
completedProposalCohortTurnId = turnId;
|
|
1950
|
-
pendingProposalCohort.clear();
|
|
1951
|
-
activeProposalCohort = undefined;
|
|
1952
|
-
}
|
|
1953
|
-
};
|
|
1954
|
-
const queueProposalCohortMember = (input, signal) => {
|
|
1955
|
-
if (pendingProposalCohort.has(input.role)) {
|
|
1956
|
-
throw new Error(`DECIDE proposal cohort already registered role ${input.role}`);
|
|
1957
|
-
}
|
|
1958
|
-
const callId = `player-${++playerCallSequence}`;
|
|
1959
|
-
const member = {
|
|
1960
|
-
input,
|
|
1961
|
-
signal,
|
|
1962
|
-
callId,
|
|
1963
|
-
effectBoundary: governedBoundarySeed(input, callId),
|
|
1964
|
-
result: deferredValue(),
|
|
1965
|
-
};
|
|
1966
|
-
pendingProposalCohort.set(input.role, member);
|
|
1967
|
-
if (pendingProposalCohort.size === ROLE_IDS.length) {
|
|
1968
|
-
if (activeProposalCohort !== undefined) {
|
|
1969
|
-
throw new Error('DECIDE proposal cohort was started more than once');
|
|
1970
|
-
}
|
|
1971
|
-
activeProposalCohort = settleProposalCohort();
|
|
1972
|
-
}
|
|
1973
|
-
return member.result.promise;
|
|
1974
|
-
};
|
|
1975
|
-
const callPlayer = (input, signal) => {
|
|
1976
|
-
const invocation = async () => {
|
|
1977
|
-
const active = activeDeferredContinuation;
|
|
1978
|
-
if (active === undefined && hasUnresolvedReconciliation()) {
|
|
1979
|
-
throw new Error('DECIDE governed semantic reconciliation remains unresolved');
|
|
1980
|
-
}
|
|
1981
|
-
if (active !== undefined) {
|
|
1982
|
-
if (active.effectBoundary.runtimeSessionId !==
|
|
1983
|
-
requireSessionIdentity().sessionId ||
|
|
1984
|
-
active.effectBoundary.turnId !== currentTurnId ||
|
|
1985
|
-
active.effectBoundary.roleId !== input.role ||
|
|
1986
|
-
active.effectBoundary.sourceStateId !== input.stateId ||
|
|
1987
|
-
active.playerContinuation === undefined) {
|
|
1988
|
-
throw new TypeError('DECIDE deferred continuation did not invoke its bound player boundary');
|
|
1989
|
-
}
|
|
1990
|
-
active.input = input;
|
|
1991
|
-
active.playerId = resolvedPlayerId(input.role);
|
|
1992
|
-
try {
|
|
1993
|
-
const envelope = await runPlayerCall(input, signal, {
|
|
1994
|
-
callId: active.effectBoundary.callId,
|
|
1995
|
-
resume: active.playerContinuation,
|
|
1996
|
-
});
|
|
1997
|
-
active.result.resolve(envelope.result);
|
|
1998
|
-
const acknowledgedResult = await active.acknowledged.promise;
|
|
1999
|
-
return { ...envelope, result: acknowledgedResult };
|
|
2000
|
-
}
|
|
2001
|
-
catch (error) {
|
|
2002
|
-
active.result.reject(error);
|
|
2003
|
-
try {
|
|
2004
|
-
await active.acknowledged.promise;
|
|
2005
|
-
}
|
|
2006
|
-
catch (acknowledgementError) {
|
|
2007
|
-
throw acknowledgementError;
|
|
2008
|
-
}
|
|
2009
|
-
throw error;
|
|
2010
|
-
}
|
|
2011
|
-
}
|
|
2012
|
-
if (shouldRunProposalCohort(input)) {
|
|
2013
|
-
return queueProposalCohortMember(input, signal);
|
|
2014
|
-
}
|
|
2015
|
-
const callId = `player-${++playerCallSequence}`;
|
|
2016
|
-
const effectBoundary = governedBoundarySeed(input, callId);
|
|
2017
|
-
let envelope;
|
|
2018
|
-
const settled = await deferredEffects.repository.runExclusive({
|
|
2019
|
-
signal,
|
|
2020
|
-
effectBoundary,
|
|
2021
|
-
operation: async () => {
|
|
2022
|
-
envelope = await runPlayerCall(input, signal, { callId });
|
|
2023
|
-
return envelope.result;
|
|
2024
|
-
},
|
|
2025
|
-
completeEffectBoundary: completionEvidenceFor(input, input.role, resolvedPlayerId(input.role), signal),
|
|
2026
|
-
});
|
|
2027
|
-
const result = acknowledgeGovernedPlayerResult(settled, effectBoundary.boundaryId);
|
|
2028
|
-
if (envelope === undefined) {
|
|
2029
|
-
throw new TypeError('DECIDE repository settlement omitted its player invocation');
|
|
2030
|
-
}
|
|
2031
|
-
return { ...envelope, result };
|
|
2032
|
-
};
|
|
2033
|
-
return trackBoundaryCall(invocation());
|
|
2034
|
-
};
|
|
2035
|
-
const player = fromPromise(async ({ input, signal }) => {
|
|
2036
|
-
const combined = combineSignals(signal, currentSignal);
|
|
2037
|
-
const settlementAborts = abortReasonClassifier(combined);
|
|
2038
|
-
try {
|
|
2039
|
-
// XState starts invoked actors while publishing the entering snapshot.
|
|
2040
|
-
// Yield through the runtime emission queue before crossing the player
|
|
2041
|
-
// boundary so state trace/status always precede its call-start trace.
|
|
2042
|
-
combined.throwIfAborted();
|
|
2043
|
-
try {
|
|
2044
|
-
await flush(settlementAborts);
|
|
2045
|
-
}
|
|
2046
|
-
catch (error) {
|
|
2047
|
-
latchControlPlaneError(error, combined);
|
|
2048
|
-
throw error;
|
|
2049
|
-
}
|
|
2050
|
-
combined.throwIfAborted();
|
|
2051
|
-
let { roleId, playerId, callId, result } = await callPlayer(input, combined);
|
|
2052
|
-
if (result.status === 'ok' &&
|
|
2053
|
-
isEmptyFinalText(result.finalText) &&
|
|
2054
|
-
automaticReplayPolicy.allowsEmptyOkCorrection(requireSessionIdentity().sessionId, callId)) {
|
|
2055
|
-
// DR-028: an `ok` result whose finalText is missing, empty, or
|
|
2056
|
-
// whitespace-only earns exactly one corrective re-ask — the same
|
|
2057
|
-
// composed call repeated, traced by runPlayerCall as its own
|
|
2058
|
-
// player-call pair, with the resume selection re-read from the
|
|
2059
|
-
// token map the first result left (PBRT-38). An abort that lands
|
|
2060
|
-
// between the two calls ends the turn without the re-ask (aborts
|
|
2061
|
-
// are never retried), and a rejecting finish emission rejects
|
|
2062
|
-
// `callPlayer` itself, so it never reaches this branch (PBRT-47).
|
|
2063
|
-
combined.throwIfAborted();
|
|
2064
|
-
({ roleId, playerId, callId, result } = await callPlayer(input, combined));
|
|
2065
|
-
}
|
|
2066
|
-
if (result.status !== 'ok') {
|
|
2067
|
-
throw new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "${result.status}"${result.error ? `: ${result.error}` : ''}`);
|
|
2068
|
-
}
|
|
2069
|
-
const finalText = result.finalText ?? '';
|
|
2070
|
-
if (isEmptyFinalText(finalText)) {
|
|
2071
|
-
throw new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "ok" with no finalText`);
|
|
2072
|
-
}
|
|
2073
|
-
combined.throwIfAborted();
|
|
2074
|
-
const governedOutput = governedPlayerOutputs.get(result);
|
|
2075
|
-
if (governedOutput !== undefined) {
|
|
2076
|
-
governedPlayerOutputs.delete(result);
|
|
2077
|
-
return governedOutput;
|
|
2078
|
-
}
|
|
2079
|
-
throw new Error('DECIDE governed player result was not reconciled against repository evidence');
|
|
2080
|
-
}
|
|
2081
|
-
finally {
|
|
2082
|
-
actorSettlementAborts.push(settlementAborts);
|
|
2083
|
-
}
|
|
2084
|
-
});
|
|
2085
|
-
nestedBridge = createNestedPlaybookBridge({
|
|
2086
|
-
nextCallId: () => `playbook-${++playbookCallSequence}`,
|
|
2087
|
-
getBoundarySignal: () => currentSignal,
|
|
2088
|
-
callPlaybook: (request, signal) => trackBoundaryCall(Promise.resolve(requirePorts().callPlaybook(request, signal))),
|
|
2089
|
-
emitStarted: async (event, aborts) => {
|
|
2090
|
-
playbookCallTurnIds.set(event.callId, currentTurnId);
|
|
2091
|
-
await emitTrace('playbook.call.started', {
|
|
2092
|
-
stateId: event.stateId,
|
|
2093
|
-
playbookId: event.playbookId,
|
|
2094
|
-
text: event.text,
|
|
2095
|
-
}, {
|
|
2096
|
-
...(currentTurnId === undefined ? {} : { turnId: currentTurnId }),
|
|
2097
|
-
callId: event.callId,
|
|
2098
|
-
}, aborts);
|
|
2099
|
-
},
|
|
2100
|
-
emitFinished: async (event, aborts) => {
|
|
2101
|
-
const turnId = playbookCallTurnIds.get(event.callId);
|
|
2102
|
-
try {
|
|
2103
|
-
await emitTrace('playbook.call.finished', {
|
|
2104
|
-
stateId: event.stateId,
|
|
2105
|
-
playbookId: event.playbookId,
|
|
2106
|
-
text: event.text,
|
|
2107
|
-
result: event.result,
|
|
2108
|
-
}, {
|
|
2109
|
-
...(turnId === undefined ? {} : { turnId }),
|
|
2110
|
-
callId: event.callId,
|
|
2111
|
-
}, aborts);
|
|
2112
|
-
}
|
|
2113
|
-
finally {
|
|
2114
|
-
playbookCallTurnIds.delete(event.callId);
|
|
2115
|
-
}
|
|
2116
|
-
},
|
|
2117
|
-
drain: flush,
|
|
2118
|
-
bindResumeSignal: (signal, aborts) => {
|
|
2119
|
-
currentSignal = signal;
|
|
2120
|
-
currentAborts = aborts ?? abortReasonClassifier(signal);
|
|
2121
|
-
},
|
|
2122
|
-
bindActorSettlement: (aborts) => {
|
|
2123
|
-
actorSettlementAborts.push(aborts);
|
|
2124
|
-
},
|
|
2125
|
-
onControlPlaneError: (error, aborts) => {
|
|
2126
|
-
if (!aborts?.isAbortReason(error) &&
|
|
2127
|
-
!currentAborts?.isAbortReason(error)) {
|
|
2128
|
-
controlPlaneError ??= error;
|
|
2129
|
-
}
|
|
2130
|
-
},
|
|
2131
|
-
onBackgroundError: (error, aborts) => {
|
|
2132
|
-
if (!aborts?.isAbortReason(error)) {
|
|
2133
|
-
collectFailure(emissionFailures, error);
|
|
108
|
+
"synthesizeCommit": {
|
|
109
|
+
"role": "coder",
|
|
110
|
+
"label": "Coder synthesizes both proposals into the necessary DRs and/or spec items and commits the result as one new commit."
|
|
2134
111
|
}
|
|
2135
112
|
},
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
}
|
|
2151
|
-
return aborts;
|
|
2152
|
-
};
|
|
2153
|
-
const inspect = (event) => {
|
|
2154
|
-
if (actor === undefined || event.actorRef !== actor)
|
|
2155
|
-
return;
|
|
2156
|
-
if (suppressInspectionEmissions)
|
|
2157
|
-
return;
|
|
2158
|
-
if (event.type === '@xstate.action') {
|
|
2159
|
-
try {
|
|
2160
|
-
acceptedOutcomeConsumer.capture(event.action);
|
|
2161
|
-
}
|
|
2162
|
-
catch (error) {
|
|
2163
|
-
latchInspectionError(error);
|
|
2164
|
-
}
|
|
2165
|
-
return;
|
|
2166
|
-
}
|
|
2167
|
-
if (event.type !== '@xstate.snapshot')
|
|
2168
|
-
return;
|
|
2169
|
-
const settlementAborts = consumeActorSettlementAborts(true);
|
|
2170
|
-
try {
|
|
2171
|
-
const snapshot = event.snapshot;
|
|
2172
|
-
const state = normalizePlaybookSnapshot(snapshot);
|
|
2173
|
-
const prior = previousState ?? state;
|
|
2174
|
-
let acceptedOutcomes = [];
|
|
2175
|
-
try {
|
|
2176
|
-
acceptedOutcomes = acceptedOutcomeConsumer.confirm(previousState, state);
|
|
2177
|
-
}
|
|
2178
|
-
catch (error) {
|
|
2179
|
-
latchInspectionError(error);
|
|
2180
|
-
}
|
|
2181
|
-
const context = snapshot.context;
|
|
2182
|
-
const fsmPayload = telemetryPayload(prior, state, event.event, context, hiddenDeferredOperationId === undefined
|
|
2183
|
-
? undefined
|
|
2184
|
-
: 'commitCoderProposal');
|
|
2185
|
-
const describedFsmPayload = snapshotJsonValue(fsmPayload, 'described FSM telemetry');
|
|
2186
|
-
void enqueueTracedEmission('fsm.transition', fsmPayload, { turnId: currentTurnId }, (emissionPorts) => emissionPorts.emitTelemetry({
|
|
2187
|
-
topic: TELEMETRY_TOPIC,
|
|
2188
|
-
payload: describedFsmPayload,
|
|
2189
|
-
}), settlementAborts).catch(() => undefined);
|
|
2190
|
-
const priorIds = new Set(previousState?.activeStateIds ?? []);
|
|
2191
|
-
previousState = state;
|
|
2192
|
-
const pendingQuestions = visiblePendingQuestionsForState(state, context);
|
|
2193
|
-
const bossRelevantStateIds = state.activeStateIds.filter((stateId) => STATUS_STATE_IDS.has(stateId));
|
|
2194
|
-
const scheduleStatus = (message, stateId, data) => {
|
|
2195
|
-
const tracePayload = {
|
|
2196
|
-
...(bossRelevantStateIds.length === 1 ? { stateId } : {}),
|
|
2197
|
-
message,
|
|
2198
|
-
state,
|
|
2199
|
-
...(data !== undefined ? { data } : {}),
|
|
2200
|
-
};
|
|
2201
|
-
assertJsonSafe(tracePayload);
|
|
2202
|
-
void enqueueTracedEmission('status.emitted', tracePayload, { turnId: currentTurnId }, (emissionPorts) => emissionPorts.emitStatus(message, data), settlementAborts).catch(() => undefined);
|
|
2203
|
-
};
|
|
2204
|
-
for (const acceptedOutcome of acceptedOutcomes) {
|
|
2205
|
-
void enqueueAcceptedOutcomeEmission(acceptedOutcome, state, settlementAborts).catch(() => undefined);
|
|
2206
|
-
}
|
|
2207
|
-
for (const activeStateId of state.activeStateIds) {
|
|
2208
|
-
if (priorIds.has(activeStateId) ||
|
|
2209
|
-
!STATUS_STATE_IDS.has(activeStateId)) {
|
|
2210
|
-
continue;
|
|
2211
|
-
}
|
|
2212
|
-
if (WAIT_STATE_IDS.has(activeStateId)) {
|
|
2213
|
-
const pending = questionForWaitState(activeStateId, pendingQuestions);
|
|
2214
|
-
if (pending) {
|
|
2215
|
-
scheduleStatus(`${pending.asker.roleId} asks: ${pending.question}`, activeStateId);
|
|
2216
|
-
scheduleStatus(`◆ awaiting Boss reply · ${pending.resumeStateId} · ${pending.asker.roleId} · ${pending.sourceItem}`, activeStateId);
|
|
113
|
+
"outcomeAuthority": {
|
|
114
|
+
"governedPlayerStates": {
|
|
115
|
+
"askCoderProposal": {
|
|
116
|
+
"proposed": {
|
|
117
|
+
"fields": {
|
|
118
|
+
"coderProposal": "presentation"
|
|
119
|
+
},
|
|
120
|
+
"repositoryDisposition": "unchanged"
|
|
121
|
+
},
|
|
122
|
+
"needsBossReply": {
|
|
123
|
+
"fields": {
|
|
124
|
+
"question": "presentation"
|
|
125
|
+
},
|
|
126
|
+
"repositoryDisposition": "unchanged"
|
|
2217
127
|
}
|
|
2218
|
-
continue;
|
|
2219
|
-
}
|
|
2220
|
-
const lastError = activeStateId === 'failed'
|
|
2221
|
-
? normalizeErrorCompact(context.lastError)
|
|
2222
|
-
: undefined;
|
|
2223
|
-
const description = STATE_DESCRIPTIONS[activeStateId];
|
|
2224
|
-
if (description === undefined)
|
|
2225
|
-
continue;
|
|
2226
|
-
const roleState = ROLE_STATES.find((candidate) => candidate.stateId === activeStateId);
|
|
2227
|
-
scheduleStatus(roleState === undefined
|
|
2228
|
-
? '◆ workflow failed; awaiting Boss recovery.'
|
|
2229
|
-
: `⤷ ${roleLabel(roleState.role)}: ${description}`, activeStateId, lastError === undefined ? undefined : { lastError });
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
catch (error) {
|
|
2233
|
-
acceptedOutcomeConsumer.reset();
|
|
2234
|
-
latchInspectionError(error, settlementAborts);
|
|
2235
|
-
}
|
|
2236
|
-
};
|
|
2237
|
-
const createRuntimeActor = (machineSnapshot) => {
|
|
2238
|
-
previousState = undefined;
|
|
2239
|
-
acceptedOutcomeConsumer.reset();
|
|
2240
|
-
// DR-014 §1: a restore rehydrates the persisted machine snapshot;
|
|
2241
|
-
// XState derives context/value from it and ignores `input` then.
|
|
2242
|
-
actor = createActor(providedMachine, {
|
|
2243
|
-
input: fsmInput,
|
|
2244
|
-
...(machineSnapshot === undefined
|
|
2245
|
-
? {}
|
|
2246
|
-
: {
|
|
2247
|
-
snapshot: machineSnapshot,
|
|
2248
|
-
}),
|
|
2249
|
-
inspect,
|
|
2250
|
-
});
|
|
2251
|
-
// A synchronous FSM action throw errors the actor without any pending
|
|
2252
|
-
// boundary await to observe it; unobserved, XState would surface it via
|
|
2253
|
-
// reportUnhandledError as an uncaughtException. Observe it here: latch
|
|
2254
|
-
// it as a control error while a turn signal is active (unless it is
|
|
2255
|
-
// the abort reason itself), otherwise collect it with the emission
|
|
2256
|
-
// failures (slc/link.md §Abort).
|
|
2257
|
-
actor.subscribe({
|
|
2258
|
-
error: (error) => latchInspectionError(error, consumeActorSettlementAborts()),
|
|
2259
|
-
});
|
|
2260
|
-
};
|
|
2261
|
-
// PBRT-6: the single seam that stops this runtime's actor. Stopping a
|
|
2262
|
-
// still-running actor fires one more `@xstate.snapshot` for the *unchanged*
|
|
2263
|
-
// state value with `status: 'stopped'`; `inspect` cannot tell that disposal
|
|
2264
|
-
// artifact from a state entry, so unsuppressed it re-emits the parked
|
|
2265
|
-
// state's telemetry and a phantom self-loop transition. Suppression is a
|
|
2266
|
-
// property of stopping, not a rule each caller must remember — every stop
|
|
2267
|
-
// goes through here so no later site can reintroduce the omission.
|
|
2268
|
-
const stopActor = () => {
|
|
2269
|
-
if (!actor)
|
|
2270
|
-
return;
|
|
2271
|
-
suppressInspectionEmissions = true;
|
|
2272
|
-
acceptedOutcomeConsumer.reset();
|
|
2273
|
-
actor.stop();
|
|
2274
|
-
};
|
|
2275
|
-
const startActor = () => {
|
|
2276
|
-
createRuntimeActor();
|
|
2277
|
-
// A fresh actor's emissions are real state entries again.
|
|
2278
|
-
suppressInspectionEmissions = false;
|
|
2279
|
-
actor?.start();
|
|
2280
|
-
};
|
|
2281
|
-
const driveToQuiescence = async () => {
|
|
2282
|
-
const live = actor;
|
|
2283
|
-
if (!live)
|
|
2284
|
-
throw new Error('decide runtime: actor is not initialized');
|
|
2285
|
-
await waitForPlaybookQuiescence(live, { pendingCalls: nestedBridge });
|
|
2286
|
-
};
|
|
2287
|
-
const classify = async (text, signal) => {
|
|
2288
|
-
const live = actor;
|
|
2289
|
-
if (!live)
|
|
2290
|
-
throw new Error('decide runtime: actor is not initialized');
|
|
2291
|
-
refreshReconciliationProjection();
|
|
2292
|
-
if (hasUnresolvedReconciliation())
|
|
2293
|
-
return null;
|
|
2294
|
-
const snapshot = live.getSnapshot();
|
|
2295
|
-
const context = snapshot.context;
|
|
2296
|
-
const state = normalizePlaybookSnapshot(snapshot, {
|
|
2297
|
-
pendingCall: nestedBridge.getPendingCall(),
|
|
2298
|
-
});
|
|
2299
|
-
const pendingQuestions = visiblePendingQuestionsForState(state, context);
|
|
2300
|
-
const failed = state.activeStateIds.includes('failed');
|
|
2301
|
-
if (pendingQuestions.length === 0 &&
|
|
2302
|
-
(snapshot.status === 'done' ||
|
|
2303
|
-
state.activeStateIds.includes('ready') ||
|
|
2304
|
-
(failed &&
|
|
2305
|
-
automaticReplayPolicy.allowsFailureStateRetry()))) {
|
|
2306
|
-
return { type: 'START_DECIDE', callerTopic: text };
|
|
2307
|
-
}
|
|
2308
|
-
if (pendingQuestions.length === 0)
|
|
2309
|
-
return null;
|
|
2310
|
-
const prompt = buildClassifierPrompt(text, {
|
|
2311
|
-
state,
|
|
2312
|
-
pendingQuestions,
|
|
2313
|
-
});
|
|
2314
|
-
const raw = await callJudge(prompt, signal, 'boss-input-classification', state.stateId);
|
|
2315
|
-
return parseClassification(raw, text, pendingQuestions.map(({ questionId }) => questionId));
|
|
2316
|
-
};
|
|
2317
|
-
const deferredBoundarySeed = (operationId, callId) => {
|
|
2318
|
-
const operation = effectLedgerMirror.logicalOperations.find((candidate) => candidate.operationId === operationId);
|
|
2319
|
-
const latestBoundaryId = operation?.boundaryIds.at(-1);
|
|
2320
|
-
const priorBoundary = effectLedgerMirror.boundaries.find((candidate) => candidate.boundaryId === latestBoundaryId);
|
|
2321
|
-
if (operation === undefined || priorBoundary === undefined) {
|
|
2322
|
-
throw new TypeError('DECIDE deferred logical operation has no linked physical boundary');
|
|
2323
|
-
}
|
|
2324
|
-
if (currentTurnId === undefined ||
|
|
2325
|
-
!Number.isSafeInteger(currentTurnId) ||
|
|
2326
|
-
currentTurnId <= 0) {
|
|
2327
|
-
throw new Error('DECIDE deferred continuation requires an active positive turn id');
|
|
2328
|
-
}
|
|
2329
|
-
return {
|
|
2330
|
-
boundaryId: randomUUID(),
|
|
2331
|
-
runtimeSessionId: requireSessionIdentity().sessionId,
|
|
2332
|
-
turnId: currentTurnId,
|
|
2333
|
-
callId,
|
|
2334
|
-
roleId: priorBoundary.roleId,
|
|
2335
|
-
sourceStateId: priorBoundary.sourceStateId,
|
|
2336
|
-
sourceOutcomeSchema: snapshotJsonValue(priorBoundary.sourceOutcomeSchema, 'DECIDE deferred source outcome schema'),
|
|
2337
|
-
dispositions: [...priorBoundary.dispositions],
|
|
2338
|
-
correctionBudget: { limit: 1, spent: false },
|
|
2339
|
-
};
|
|
2340
|
-
};
|
|
2341
|
-
const prepareDeferredContinuation = async (signal, resumeEvent) => {
|
|
2342
|
-
if (deferredEffects === undefined || deferredOperationId === undefined) {
|
|
2343
|
-
throw new TypeError('DECIDE deferred Boss reply has no host-bound logical operation');
|
|
2344
|
-
}
|
|
2345
|
-
const operationId = deferredOperationId;
|
|
2346
|
-
const callId = `player-${++playerCallSequence}`;
|
|
2347
|
-
const effectBoundary = deferredBoundarySeed(operationId, callId);
|
|
2348
|
-
const active = {
|
|
2349
|
-
operationId,
|
|
2350
|
-
effectBoundary,
|
|
2351
|
-
result: deferredValue(),
|
|
2352
|
-
acknowledged: deferredValue(),
|
|
2353
|
-
};
|
|
2354
|
-
const readiness = deferredValue();
|
|
2355
|
-
const repositoryCall = deferredEffects.repository.runDeferred({
|
|
2356
|
-
mode: 'continue',
|
|
2357
|
-
signal,
|
|
2358
|
-
operationId,
|
|
2359
|
-
effectBoundary,
|
|
2360
|
-
operation: async ({ playerContinuation }) => {
|
|
2361
|
-
if (playerContinuation !== false &&
|
|
2362
|
-
(typeof playerContinuation !== 'string' ||
|
|
2363
|
-
playerContinuation.trim() === '')) {
|
|
2364
|
-
throw new TypeError('DECIDE bound deferred player continuation is invalid');
|
|
2365
|
-
}
|
|
2366
|
-
active.playerContinuation = playerContinuation;
|
|
2367
|
-
readiness.resolve({ status: 'ready' });
|
|
2368
|
-
return active.result.promise;
|
|
2369
|
-
},
|
|
2370
|
-
completeEffectBoundary: async (completion) => {
|
|
2371
|
-
if (active.input === undefined) {
|
|
2372
|
-
throw new TypeError('DECIDE deferred continuation omitted its authored player input');
|
|
2373
|
-
}
|
|
2374
|
-
return completionEvidenceFor(active.input, active.input.role, active.playerId, signal, operationId)(completion);
|
|
2375
|
-
},
|
|
2376
|
-
});
|
|
2377
|
-
void repositoryCall.then((value) => readiness.resolve({ status: 'settled', value }), (reason) => readiness.resolve({ status: 'rejected', reason }));
|
|
2378
|
-
const prepared = await readiness.promise;
|
|
2379
|
-
if (prepared.status === 'rejected')
|
|
2380
|
-
throw prepared.reason;
|
|
2381
|
-
if (prepared.status === 'settled') {
|
|
2382
|
-
synchronizeDeferredProjection(assertPlaybookEffectLedger(prepared.value.effectLedger, 'DECIDE deferred checkpoint-mismatch effect ledger'));
|
|
2383
|
-
hiddenDeferredOperationId = operationId;
|
|
2384
|
-
return { proceed: false };
|
|
2385
|
-
}
|
|
2386
|
-
activeDeferredContinuation = active;
|
|
2387
|
-
const acknowledgement = repositoryCall.then((value) => {
|
|
2388
|
-
if (value.status !== 'continued') {
|
|
2389
|
-
throw new TypeError('DECIDE deferred repository changed status after starting its operation');
|
|
2390
|
-
}
|
|
2391
|
-
const result = acknowledgeGovernedPlayerResult(value, effectBoundary.boundaryId);
|
|
2392
|
-
if (hiddenDeferredOperationId === undefined) {
|
|
2393
|
-
suppressInspectionEmissions = false;
|
|
2394
|
-
const live = actor;
|
|
2395
|
-
if (live === undefined) {
|
|
2396
|
-
throw new Error('DECIDE deferred acknowledgement lost its runtime actor');
|
|
2397
|
-
}
|
|
2398
|
-
// Entry into the governed continuation was hidden until its durable
|
|
2399
|
-
// effect acknowledgement. Publish that exact current root snapshot
|
|
2400
|
-
// before releasing the actor output, so an accepted-outcome marker
|
|
2401
|
-
// is confirmed against commitCoderProposal rather than the earlier
|
|
2402
|
-
// awaitBossReply snapshot.
|
|
2403
|
-
inspect({
|
|
2404
|
-
type: '@xstate.snapshot',
|
|
2405
|
-
actorRef: live,
|
|
2406
|
-
event: resumeEvent,
|
|
2407
|
-
snapshot: live.getSnapshot(),
|
|
2408
|
-
});
|
|
2409
|
-
}
|
|
2410
|
-
active.acknowledged.resolve(result);
|
|
2411
|
-
}, (error) => {
|
|
2412
|
-
active.acknowledged.reject(error);
|
|
2413
|
-
throw error;
|
|
2414
|
-
}).catch((error) => {
|
|
2415
|
-
active.acknowledged.reject(error);
|
|
2416
|
-
throw error;
|
|
2417
|
-
});
|
|
2418
|
-
return { proceed: true, acknowledgement };
|
|
2419
|
-
};
|
|
2420
|
-
const parkDeferredContinuation = async (signal) => {
|
|
2421
|
-
if (deferredEffects === undefined || deferredOperationId === undefined) {
|
|
2422
|
-
return;
|
|
2423
|
-
}
|
|
2424
|
-
const operationId = deferredOperationId;
|
|
2425
|
-
const parked = await deferredEffects.repository.runDeferred({
|
|
2426
|
-
mode: 'park',
|
|
2427
|
-
signal,
|
|
2428
|
-
operationId,
|
|
2429
|
-
});
|
|
2430
|
-
synchronizeDeferredProjection(assertPlaybookEffectLedger(parked.effectLedger, 'DECIDE deferred park effect ledger'));
|
|
2431
|
-
hiddenDeferredOperationId = operationId;
|
|
2432
|
-
};
|
|
2433
|
-
const restoreDeferredReconciliation = async (operationId, signal, publishQuestion) => {
|
|
2434
|
-
const restored = await deferredEffects.repository.runDeferred({
|
|
2435
|
-
mode: 'restore',
|
|
2436
|
-
signal,
|
|
2437
|
-
operationId,
|
|
2438
|
-
});
|
|
2439
|
-
synchronizeDeferredProjection(assertPlaybookEffectLedger(restored.effectLedger, 'DECIDE deferred restoration effect ledger'));
|
|
2440
|
-
if (restored.status === 'parked') {
|
|
2441
|
-
throw new TypeError('DECIDE deferred restoration returned an invalid parked status');
|
|
2442
|
-
}
|
|
2443
|
-
if (restored.status !== 'restored') {
|
|
2444
|
-
if (hiddenDeferredOperationId !== operationId) {
|
|
2445
|
-
throw new TypeError('DECIDE unresolved deferred restoration lost its operation identity');
|
|
2446
|
-
}
|
|
2447
|
-
return restored.status;
|
|
2448
|
-
}
|
|
2449
|
-
if (hiddenDeferredOperationId !== undefined) {
|
|
2450
|
-
throw new TypeError('DECIDE restored deferred operation remained unresolved');
|
|
2451
|
-
}
|
|
2452
|
-
if (openDeferredOperation()?.operationId !== operationId) {
|
|
2453
|
-
throw new TypeError('DECIDE restored deferred operation is not the current bound wait');
|
|
2454
|
-
}
|
|
2455
|
-
const live = actor;
|
|
2456
|
-
if (live === undefined) {
|
|
2457
|
-
throw new Error('decide runtime: init(session) must be called first');
|
|
2458
|
-
}
|
|
2459
|
-
const state = currentState();
|
|
2460
|
-
const context = live.getSnapshot().context;
|
|
2461
|
-
const pending = questionForWaitState('awaitBossReply', visiblePendingQuestionsForState(state, context));
|
|
2462
|
-
const operation = openDeferredOperation();
|
|
2463
|
-
const projectedPending = pending === undefined
|
|
2464
|
-
? undefined
|
|
2465
|
-
: {
|
|
2466
|
-
questionId: pending.questionId,
|
|
2467
|
-
asker: pending.asker,
|
|
2468
|
-
question: pending.question,
|
|
2469
|
-
sourceItem: pending.sourceItem,
|
|
2470
|
-
};
|
|
2471
|
-
if (pending === undefined ||
|
|
2472
|
-
operation?.pendingQuestion === undefined ||
|
|
2473
|
-
stableJson(projectedPending, 'DECIDE restored FSM pending question') !==
|
|
2474
|
-
stableJson(operation.pendingQuestion, 'DECIDE restored deferred pending question')) {
|
|
2475
|
-
throw new TypeError('DECIDE restored deferred wait does not equal its FSM question');
|
|
2476
|
-
}
|
|
2477
|
-
if (publishQuestion) {
|
|
2478
|
-
await emitBoundaryStatus(`${pending.asker.roleId} asks: ${pending.question}`, state);
|
|
2479
|
-
await emitBoundaryStatus(`◆ awaiting Boss reply · ${pending.resumeStateId} · ${pending.asker.roleId} · ${pending.sourceItem}`, state);
|
|
2480
|
-
await flush();
|
|
2481
|
-
}
|
|
2482
|
-
return 'restored';
|
|
2483
|
-
};
|
|
2484
|
-
/**
|
|
2485
|
-
* DR-063 §3: reconciliation re-reads the host's ledger, which the control
|
|
2486
|
-
* view has just done. With no eligible checkpoint restoration and a complete
|
|
2487
|
-
* receipt on every unresolved boundary, re-reading resolves nothing.
|
|
2488
|
-
*/
|
|
2489
|
-
const reconciliationStanding = (restorationEligible) => {
|
|
2490
|
-
if (restorationEligible)
|
|
2491
|
-
return { standing: 'ready' };
|
|
2492
|
-
const envelopes = unresolvedEffectEnvelopeIdentities();
|
|
2493
|
-
if (envelopes.length === 0)
|
|
2494
|
-
return { standing: 'ready' };
|
|
2495
|
-
const boundaryIds = envelopes.flatMap((envelope) => envelope.kind === 'boundary'
|
|
2496
|
-
? [envelope.boundaryId]
|
|
2497
|
-
: (effectLedgerMirror.logicalOperations.find(({ operationId }) => operationId === envelope.operationId)?.boundaryIds ?? []));
|
|
2498
|
-
if (boundaryIds.length === 0)
|
|
2499
|
-
return { standing: 'ready' };
|
|
2500
|
-
return boundaryIds.every((boundaryId) => effectLedgerMirror.boundaries.find((candidate) => candidate.boundaryId === boundaryId)?.physicalReceipt !== undefined)
|
|
2501
|
-
? { standing: 'no-op', reason: 'receipt-complete' }
|
|
2502
|
-
: { standing: 'ready' };
|
|
2503
|
-
};
|
|
2504
|
-
const unresolvedControlCandidates = () => {
|
|
2505
|
-
if (!hasUnresolvedReconciliation())
|
|
2506
|
-
return [];
|
|
2507
|
-
const operation = hiddenDeferredOperationId === undefined
|
|
2508
|
-
? undefined
|
|
2509
|
-
: effectLedgerMirror.logicalOperations.find(({ operationId }) => operationId === hiddenDeferredOperationId);
|
|
2510
|
-
const restorationEligible = operation?.checkpointRestorationEligible === true;
|
|
2511
|
-
return [
|
|
2512
|
-
{
|
|
2513
|
-
action: {
|
|
2514
|
-
id: UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID,
|
|
2515
|
-
label: 'Retry unresolved effect reconciliation',
|
|
2516
|
-
...reconciliationStanding(restorationEligible),
|
|
2517
128
|
},
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
action: {
|
|
2525
|
-
id: UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID,
|
|
2526
|
-
label: 'Abandon unresolved workflow attempt',
|
|
2527
|
-
// DR-063 §3: abandonment always changes the session's standing.
|
|
2528
|
-
standing: 'ready',
|
|
2529
|
-
},
|
|
2530
|
-
kind: 'abandon',
|
|
2531
|
-
},
|
|
2532
|
-
];
|
|
2533
|
-
};
|
|
2534
|
-
const deriveControlCandidates = () => {
|
|
2535
|
-
const state = currentState();
|
|
2536
|
-
if (state.status !== 'active' ||
|
|
2537
|
-
!state.quiescent ||
|
|
2538
|
-
nestedBridge.getPendingCall() !== undefined) {
|
|
2539
|
-
return [];
|
|
2540
|
-
}
|
|
2541
|
-
return unresolvedControlCandidates();
|
|
2542
|
-
};
|
|
2543
|
-
const describeControlView = () => {
|
|
2544
|
-
if (disposed || disposalPromise !== undefined) {
|
|
2545
|
-
throw new Error('decide runtime: runtime is disposing or disposed');
|
|
2546
|
-
}
|
|
2547
|
-
const live = actor;
|
|
2548
|
-
if (live === undefined || sessionIdentity === undefined) {
|
|
2549
|
-
throw new Error('decide runtime: init(session) must be called before describe');
|
|
2550
|
-
}
|
|
2551
|
-
if (currentSignal !== undefined || currentTurnId !== undefined) {
|
|
2552
|
-
throw new Error('decide runtime: another runtime turn is active');
|
|
2553
|
-
}
|
|
2554
|
-
refreshReconciliationProjection();
|
|
2555
|
-
const state = currentState();
|
|
2556
|
-
const context = live.getSnapshot().context;
|
|
2557
|
-
const unresolved = hasUnresolvedReconciliation();
|
|
2558
|
-
const pendingQuestions = unresolved
|
|
2559
|
-
? []
|
|
2560
|
-
: visiblePendingQuestionsForState(state, context).map((pending) => Object.freeze({
|
|
2561
|
-
questionId: pending.questionId,
|
|
2562
|
-
asker: pending.asker,
|
|
2563
|
-
question: pending.question,
|
|
2564
|
-
sourceItem: pending.sourceItem,
|
|
2565
|
-
}));
|
|
2566
|
-
const actions = deriveControlCandidates().map(({ action }) => ({
|
|
2567
|
-
...action,
|
|
2568
|
-
}));
|
|
2569
|
-
const lastError = normalizeErrorFull(context.lastError);
|
|
2570
|
-
const stateDescription = unresolved || state.stateId === undefined
|
|
2571
|
-
? undefined
|
|
2572
|
-
: STATE_DESCRIPTIONS[state.stateId];
|
|
2573
|
-
return deepFreeze(snapshotJsonValue({
|
|
2574
|
-
state,
|
|
2575
|
-
...(stateDescription === undefined ? {} : { stateDescription }),
|
|
2576
|
-
pendingQuestions,
|
|
2577
|
-
...(lastError === undefined ? {} : { lastError }),
|
|
2578
|
-
actions,
|
|
2579
|
-
}, 'DECIDE control view'));
|
|
2580
|
-
};
|
|
2581
|
-
const normalizedControlError = (error) => normalizeErrorFull(error) ?? {
|
|
2582
|
-
name: error instanceof Error ? error.name : 'Error',
|
|
2583
|
-
message: error instanceof Error ? error.message : String(error),
|
|
2584
|
-
};
|
|
2585
|
-
const frozenControlReceipt = (receipt) => deepFreeze(snapshotJsonValue(receipt, 'DECIDE apply receipt'));
|
|
2586
|
-
const applyControlAction = async (input) => {
|
|
2587
|
-
if (input === null || typeof input !== 'object') {
|
|
2588
|
-
throw new TypeError('decide runtime: apply input must be an object');
|
|
2589
|
-
}
|
|
2590
|
-
const { actionId, key, signal } = input;
|
|
2591
|
-
if (typeof actionId !== 'string' || actionId.length === 0) {
|
|
2592
|
-
throw new TypeError('decide runtime: apply actionId must be a non-empty string');
|
|
2593
|
-
}
|
|
2594
|
-
if (typeof key !== 'string' || key.length === 0) {
|
|
2595
|
-
throw new TypeError('decide runtime: apply key must be a non-empty string');
|
|
2596
|
-
}
|
|
2597
|
-
if (!(signal instanceof AbortSignal)) {
|
|
2598
|
-
throw new TypeError('decide runtime: apply signal must be an AbortSignal');
|
|
2599
|
-
}
|
|
2600
|
-
if (disposed || disposalPromise !== undefined) {
|
|
2601
|
-
throw new Error('decide runtime: runtime is disposing or disposed');
|
|
2602
|
-
}
|
|
2603
|
-
if (actor === undefined || sessionIdentity === undefined) {
|
|
2604
|
-
throw new Error('decide runtime: init(session) must be called before apply');
|
|
2605
|
-
}
|
|
2606
|
-
if (currentSignal !== undefined || currentTurnId !== undefined) {
|
|
2607
|
-
throw new Error('decide runtime: another runtime turn is active');
|
|
2608
|
-
}
|
|
2609
|
-
const recorded = appliedControlReceipts.get(key);
|
|
2610
|
-
if (recorded !== undefined)
|
|
2611
|
-
return recorded;
|
|
2612
|
-
signal.throwIfAborted();
|
|
2613
|
-
refreshReconciliationProjection();
|
|
2614
|
-
const turnId = ++turnSequence;
|
|
2615
|
-
const callId = `apply-${++applyCallSequence}`;
|
|
2616
|
-
currentTurnId = turnId;
|
|
2617
|
-
currentSignal = signal;
|
|
2618
|
-
currentAborts = abortReasonClassifier(signal);
|
|
2619
|
-
controlPlaneError = undefined;
|
|
2620
|
-
let accepted = false;
|
|
2621
|
-
let receipt;
|
|
2622
|
-
let operationError;
|
|
2623
|
-
let settlementError;
|
|
2624
|
-
let lateDeliveryError;
|
|
2625
|
-
const position = { turnId, callId };
|
|
2626
|
-
const preAcceptanceFinish = (reason) => ({
|
|
2627
|
-
actionId,
|
|
2628
|
-
key,
|
|
2629
|
-
disposition: 'rejected',
|
|
2630
|
-
reason,
|
|
2631
|
-
});
|
|
2632
|
-
try {
|
|
2633
|
-
try {
|
|
2634
|
-
try {
|
|
2635
|
-
await emitTrace('apply.started', {
|
|
2636
|
-
actionId,
|
|
2637
|
-
key,
|
|
2638
|
-
...stateIdentity(currentState()),
|
|
2639
|
-
}, position, currentAborts);
|
|
2640
|
-
}
|
|
2641
|
-
catch (error) {
|
|
2642
|
-
latchControlPlaneError(error, signal);
|
|
2643
|
-
try {
|
|
2644
|
-
await emitTrace('apply.finished', {
|
|
2645
|
-
...preAcceptanceFinish('apply.started trace sink rejected'),
|
|
2646
|
-
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
2647
|
-
error: normalizedControlError(error),
|
|
2648
|
-
}, position, currentAborts);
|
|
2649
|
-
}
|
|
2650
|
-
catch {
|
|
2651
|
-
// Preserve the start failure after one best-effort finish attempt.
|
|
2652
|
-
}
|
|
2653
|
-
throw error;
|
|
2654
|
-
}
|
|
2655
|
-
await flush();
|
|
2656
|
-
if (signal.aborted) {
|
|
2657
|
-
try {
|
|
2658
|
-
await emitTrace('apply.finished', {
|
|
2659
|
-
...preAcceptanceFinish('aborted before acceptance'),
|
|
2660
|
-
status: 'aborted',
|
|
2661
|
-
error: normalizedControlError(signal.reason),
|
|
2662
|
-
}, position, currentAborts);
|
|
2663
|
-
}
|
|
2664
|
-
catch (error) {
|
|
2665
|
-
// A rejecting abort-finish sink outranks the abort at settlement.
|
|
2666
|
-
settlementError ??= error;
|
|
2667
|
-
}
|
|
2668
|
-
}
|
|
2669
|
-
signal.throwIfAborted();
|
|
2670
|
-
const candidate = deriveControlCandidates().find(({ action }) => action.id === actionId);
|
|
2671
|
-
if (candidate === undefined) {
|
|
2672
|
-
receipt = frozenControlReceipt({
|
|
2673
|
-
disposition: 'rejected',
|
|
2674
|
-
reason: `action ${JSON.stringify(actionId)} is not currently advertised`,
|
|
2675
|
-
});
|
|
2676
|
-
}
|
|
2677
|
-
else {
|
|
2678
|
-
// Acceptance is the line after which this boundary always records
|
|
2679
|
-
// and returns a receipt: the requested effect may now exist.
|
|
2680
|
-
accepted = true;
|
|
2681
|
-
try {
|
|
2682
|
-
let run;
|
|
2683
|
-
if (candidate.kind === 'abandon') {
|
|
2684
|
-
signal.throwIfAborted();
|
|
2685
|
-
run = {
|
|
2686
|
-
outcome: 'unresolved-effect',
|
|
2687
|
-
state: currentState(),
|
|
2688
|
-
};
|
|
2689
|
-
}
|
|
2690
|
-
else {
|
|
2691
|
-
if (candidate.deferredRestoreOperationId !== undefined) {
|
|
2692
|
-
await restoreDeferredReconciliation(candidate.deferredRestoreOperationId, signal, true);
|
|
2693
|
-
}
|
|
2694
|
-
else {
|
|
2695
|
-
// The host owns receipt reconstruction. Reconciliation only
|
|
2696
|
-
// re-reads its authoritative ledger; it never calls a player
|
|
2697
|
-
// or judge to manufacture missing semantic evidence.
|
|
2698
|
-
refreshReconciliationProjection();
|
|
2699
|
-
}
|
|
2700
|
-
signal.throwIfAborted();
|
|
2701
|
-
run = {
|
|
2702
|
-
outcome: hasUnresolvedReconciliation()
|
|
2703
|
-
? 'no-action'
|
|
2704
|
-
: 'quiescent',
|
|
2705
|
-
state: currentState(),
|
|
2706
|
-
};
|
|
2707
|
-
}
|
|
2708
|
-
if (controlPlaneError !== undefined)
|
|
2709
|
-
throw controlPlaneError;
|
|
2710
|
-
receipt = frozenControlReceipt({
|
|
2711
|
-
disposition: 'executed',
|
|
2712
|
-
run,
|
|
2713
|
-
});
|
|
2714
|
-
}
|
|
2715
|
-
catch (error) {
|
|
2716
|
-
receipt = frozenControlReceipt({
|
|
2717
|
-
disposition: 'failed',
|
|
2718
|
-
error: normalizedControlError(error),
|
|
2719
|
-
});
|
|
2720
|
-
}
|
|
2721
|
-
appliedControlReceipts.set(key, receipt);
|
|
2722
|
-
}
|
|
2723
|
-
}
|
|
2724
|
-
catch (error) {
|
|
2725
|
-
operationError = error;
|
|
2726
|
-
}
|
|
2727
|
-
try {
|
|
2728
|
-
await flush();
|
|
2729
|
-
}
|
|
2730
|
-
catch (error) {
|
|
2731
|
-
settlementError = error;
|
|
2732
|
-
}
|
|
2733
|
-
settlementError ??= controlPlaneError;
|
|
2734
|
-
if (accepted && settlementError !== undefined) {
|
|
2735
|
-
receipt = frozenControlReceipt({
|
|
2736
|
-
disposition: 'failed',
|
|
2737
|
-
error: normalizedControlError(settlementError),
|
|
2738
|
-
});
|
|
2739
|
-
appliedControlReceipts.set(key, receipt);
|
|
2740
|
-
settlementError = undefined;
|
|
2741
|
-
}
|
|
2742
|
-
if (receipt !== undefined) {
|
|
2743
|
-
const finishFailures = [];
|
|
2744
|
-
try {
|
|
2745
|
-
await emitTrace('apply.finished', { actionId, key, ...receipt }, position, currentAborts);
|
|
2746
|
-
}
|
|
2747
|
-
catch (error) {
|
|
2748
|
-
if (!accepted || !isAbortFailure(error, signal)) {
|
|
2749
|
-
collectFailure(finishFailures, error);
|
|
2750
|
-
}
|
|
2751
|
-
}
|
|
2752
|
-
try {
|
|
2753
|
-
await flush();
|
|
2754
|
-
}
|
|
2755
|
-
catch (error) {
|
|
2756
|
-
if (!accepted || !isAbortFailure(error, signal)) {
|
|
2757
|
-
collectFailure(finishFailures, error);
|
|
2758
|
-
}
|
|
2759
|
-
}
|
|
2760
|
-
if (finishFailures.length > 0) {
|
|
2761
|
-
const failure = finishFailures.length === 1
|
|
2762
|
-
? finishFailures[0]
|
|
2763
|
-
: new AggregateError(finishFailures, 'DECIDE apply settlement emissions failed');
|
|
2764
|
-
if (accepted)
|
|
2765
|
-
lateDeliveryError = failure;
|
|
2766
|
-
else
|
|
2767
|
-
settlementError = failure;
|
|
2768
|
-
}
|
|
2769
|
-
}
|
|
2770
|
-
}
|
|
2771
|
-
finally {
|
|
2772
|
-
currentSignal = undefined;
|
|
2773
|
-
currentAborts = undefined;
|
|
2774
|
-
currentTurnId = undefined;
|
|
2775
|
-
controlPlaneError = undefined;
|
|
2776
|
-
if (accepted && receipt !== undefined) {
|
|
2777
|
-
appliedControlReceipts.set(key, receipt);
|
|
2778
|
-
}
|
|
2779
|
-
}
|
|
2780
|
-
if (lateDeliveryError !== undefined) {
|
|
2781
|
-
collectFailure(emissionFailures, lateDeliveryError);
|
|
2782
|
-
}
|
|
2783
|
-
if (accepted && receipt !== undefined)
|
|
2784
|
-
return receipt;
|
|
2785
|
-
const failure = settlementError ?? operationError;
|
|
2786
|
-
if (failure !== undefined)
|
|
2787
|
-
throw failure;
|
|
2788
|
-
if (receipt === undefined) {
|
|
2789
|
-
throw new Error('decide runtime: apply produced no receipt');
|
|
2790
|
-
}
|
|
2791
|
-
return receipt;
|
|
2792
|
-
};
|
|
2793
|
-
const resultForSnapshot = (signal) => {
|
|
2794
|
-
const live = actor;
|
|
2795
|
-
if (!live)
|
|
2796
|
-
throw new Error('decide runtime: actor is not initialized');
|
|
2797
|
-
const snapshot = live.getSnapshot();
|
|
2798
|
-
const pendingCall = nestedBridge.getPendingCall();
|
|
2799
|
-
const state = normalizePlaybookSnapshot(snapshot, { pendingCall });
|
|
2800
|
-
const context = snapshot.context;
|
|
2801
|
-
const abortedResult = (abortSignal) => ({
|
|
2802
|
-
outcome: 'aborted',
|
|
2803
|
-
state,
|
|
2804
|
-
...(abortSignal.reason === undefined
|
|
2805
|
-
? {}
|
|
2806
|
-
: {
|
|
2807
|
-
error: normalizeErrorFull(abortSignal.reason) ?? {
|
|
2808
|
-
name: 'AbortError',
|
|
2809
|
-
message: String(abortSignal.reason),
|
|
129
|
+
"askReviewerProposal": {
|
|
130
|
+
"proposed": {
|
|
131
|
+
"fields": {
|
|
132
|
+
"reviewerProposal": "presentation"
|
|
133
|
+
},
|
|
134
|
+
"repositoryDisposition": "unchanged"
|
|
2810
135
|
},
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
// An errored actor outranks a coincident abort unless the actor's
|
|
2815
|
-
// error is the abort reason itself (slc/link.md §Abort).
|
|
2816
|
-
const actorError = snapshot.error;
|
|
2817
|
-
if (actorError !== undefined &&
|
|
2818
|
-
signal !== undefined &&
|
|
2819
|
-
isAbortFailure(actorError, signal)) {
|
|
2820
|
-
return abortedResult(signal);
|
|
2821
|
-
}
|
|
2822
|
-
throw (actorError ?? new Error('decide runtime actor entered error status'));
|
|
2823
|
-
}
|
|
2824
|
-
// Terminal completion outranks a coincident abort (DR-036 §3): reporting
|
|
2825
|
-
// 'aborted' over a completed machine would hide a terminal state that the
|
|
2826
|
-
// next turn silently restarts, duplicating the workflow's side effects.
|
|
2827
|
-
if (snapshot.status === 'done') {
|
|
2828
|
-
const output = snapshot.output;
|
|
2829
|
-
if (output !== undefined)
|
|
2830
|
-
assertJsonSafe(output, 'terminal output');
|
|
2831
|
-
const finalStateId = state.activeStateIds.includes('done')
|
|
2832
|
-
? 'done'
|
|
2833
|
-
: state.activeStateIds.includes('reportedReviewFailure')
|
|
2834
|
-
? 'reportedReviewFailure'
|
|
2835
|
-
: undefined;
|
|
2836
|
-
const stateDescription = finalStateId === undefined
|
|
2837
|
-
? undefined
|
|
2838
|
-
: STATE_DESCRIPTIONS[finalStateId];
|
|
2839
|
-
if (finalStateId === undefined || stateDescription === undefined) {
|
|
2840
|
-
throw new Error('decide runtime: completed actor has no authored final-state description');
|
|
2841
|
-
}
|
|
2842
|
-
// DR-048: the reached final state's compiled terminal kind, read from
|
|
2843
|
-
// this artifact exactly as the shared factory reads it.
|
|
2844
|
-
const terminalKind = TERMINAL_KINDS.get(finalStateId);
|
|
2845
|
-
return {
|
|
2846
|
-
outcome: 'terminal',
|
|
2847
|
-
state,
|
|
2848
|
-
stateDescription,
|
|
2849
|
-
...(terminalKind === undefined
|
|
2850
|
-
? {}
|
|
2851
|
-
: {
|
|
2852
|
-
terminal: {
|
|
2853
|
-
stateId: finalStateId,
|
|
2854
|
-
kind: terminalKind,
|
|
2855
|
-
description: stateDescription,
|
|
136
|
+
"needsBossReply": {
|
|
137
|
+
"fields": {
|
|
138
|
+
"question": "presentation"
|
|
2856
139
|
},
|
|
2857
|
-
|
|
2858
|
-
...(output === undefined ? {} : { output }),
|
|
2859
|
-
};
|
|
2860
|
-
}
|
|
2861
|
-
if (signal?.aborted) {
|
|
2862
|
-
return abortedResult(signal);
|
|
2863
|
-
}
|
|
2864
|
-
if (state.activeStateIds.includes('failed')) {
|
|
2865
|
-
const error = normalizeErrorFull(context.lastError);
|
|
2866
|
-
return {
|
|
2867
|
-
outcome: 'failed',
|
|
2868
|
-
state,
|
|
2869
|
-
...(error === undefined ? {} : { error }),
|
|
2870
|
-
};
|
|
2871
|
-
}
|
|
2872
|
-
if (pendingCall) {
|
|
2873
|
-
return { outcome: 'suspended', state, pendingCall };
|
|
2874
|
-
}
|
|
2875
|
-
return { outcome: 'quiescent', state };
|
|
2876
|
-
};
|
|
2877
|
-
// Shared failed-start cleanup for init and restore: stop the actor,
|
|
2878
|
-
// drain queued work, optionally emit one best-effort session.disposed
|
|
2879
|
-
// boundary, and unbind every closure field so dispose stays callable.
|
|
2880
|
-
// The caller rethrows its original failure. A restore failure skips
|
|
2881
|
-
// the disposal trace — the parked session was never re-bound in this
|
|
2882
|
-
// process, so its persisted snapshot stays authoritative (DR-014 §2).
|
|
2883
|
-
const cleanupFailedStart = async (cause, options) => {
|
|
2884
|
-
let finalState;
|
|
2885
|
-
if (options.emitDisposal && actor) {
|
|
2886
|
-
try {
|
|
2887
|
-
finalState = currentState();
|
|
2888
|
-
}
|
|
2889
|
-
catch {
|
|
2890
|
-
// A state that cannot even normalize has no disposal descriptor.
|
|
2891
|
-
}
|
|
2892
|
-
}
|
|
2893
|
-
try {
|
|
2894
|
-
stopActor();
|
|
2895
|
-
}
|
|
2896
|
-
catch {
|
|
2897
|
-
// Preserve the original startup failure.
|
|
2898
|
-
}
|
|
2899
|
-
try {
|
|
2900
|
-
await nestedBridge.abortPending(cause);
|
|
2901
|
-
}
|
|
2902
|
-
catch {
|
|
2903
|
-
// Preserve the original startup failure.
|
|
2904
|
-
}
|
|
2905
|
-
try {
|
|
2906
|
-
await judgeQueue.onIdle();
|
|
2907
|
-
await drainBoundaryCallsAndEmissions();
|
|
2908
|
-
}
|
|
2909
|
-
catch {
|
|
2910
|
-
// Preserve the original startup failure.
|
|
2911
|
-
}
|
|
2912
|
-
if (options.emitDisposal) {
|
|
2913
|
-
try {
|
|
2914
|
-
await emitTrace('session.disposed', {
|
|
2915
|
-
...(finalState === undefined
|
|
2916
|
-
? {}
|
|
2917
|
-
: { state: finalState, ...stateIdentity(finalState) }),
|
|
2918
|
-
});
|
|
2919
|
-
await flush();
|
|
2920
|
-
}
|
|
2921
|
-
catch {
|
|
2922
|
-
// The session-start error remains authoritative.
|
|
2923
|
-
}
|
|
2924
|
-
}
|
|
2925
|
-
privateResumeTokens.clear();
|
|
2926
|
-
inFlightPlayerKeys.clear();
|
|
2927
|
-
activeBoundaryCalls.clear();
|
|
2928
|
-
activeEmissionCalls.clear();
|
|
2929
|
-
emissionQueue.clear();
|
|
2930
|
-
judgeQueue.clear();
|
|
2931
|
-
semanticCompletionQueue.clear();
|
|
2932
|
-
actor = undefined;
|
|
2933
|
-
currentSignal = undefined;
|
|
2934
|
-
currentAborts = undefined;
|
|
2935
|
-
actorSettlementAborts.length = 0;
|
|
2936
|
-
actorSettlementErrorAborts = undefined;
|
|
2937
|
-
currentTurnId = undefined;
|
|
2938
|
-
ports = undefined;
|
|
2939
|
-
sessionIdentity = undefined;
|
|
2940
|
-
previousState = undefined;
|
|
2941
|
-
suppressInspectionEmissions = false;
|
|
2942
|
-
controlPlaneError = undefined;
|
|
2943
|
-
emissionFailures = [];
|
|
2944
|
-
traceSequence = 0;
|
|
2945
|
-
turnSequence = 0;
|
|
2946
|
-
judgeCallSequence = 0;
|
|
2947
|
-
playerCallSequence = 0;
|
|
2948
|
-
playbookCallSequence = 0;
|
|
2949
|
-
applyCallSequence = 0;
|
|
2950
|
-
playbookCallTurnIds.clear();
|
|
2951
|
-
governedOutputsByBoundaryId.clear();
|
|
2952
|
-
governedFailuresByBoundaryId.clear();
|
|
2953
|
-
governedEvidenceByBoundaryId.clear();
|
|
2954
|
-
governedReceiptsByBoundaryId.clear();
|
|
2955
|
-
pendingProposalCohort.clear();
|
|
2956
|
-
activeProposalCohort = undefined;
|
|
2957
|
-
completedProposalCohortTurnId = undefined;
|
|
2958
|
-
deferredOperationId = undefined;
|
|
2959
|
-
hiddenDeferredOperationId = undefined;
|
|
2960
|
-
unresolvedSemanticBoundaryIds.clear();
|
|
2961
|
-
appliedControlReceipts.clear();
|
|
2962
|
-
activeDeferredContinuation = undefined;
|
|
2963
|
-
lifecycleStarted = false;
|
|
2964
|
-
};
|
|
2965
|
-
return {
|
|
2966
|
-
async init(session) {
|
|
2967
|
-
if (lifecycleStarted ||
|
|
2968
|
-
initInFlight !== undefined ||
|
|
2969
|
-
disposed ||
|
|
2970
|
-
disposalPromise !== undefined) {
|
|
2971
|
-
throw new Error('decide runtime: init(session) may only be called once');
|
|
2972
|
-
}
|
|
2973
|
-
const identity = bindSession(session);
|
|
2974
|
-
let finishInitialization;
|
|
2975
|
-
const initialization = new Promise((resolve) => {
|
|
2976
|
-
finishInitialization = resolve;
|
|
2977
|
-
});
|
|
2978
|
-
initInFlight = initialization;
|
|
2979
|
-
lifecycleStarted = true;
|
|
2980
|
-
ports = identity.ports;
|
|
2981
|
-
sessionIdentity = identity;
|
|
2982
|
-
try {
|
|
2983
|
-
if (deferredEffects !== undefined) {
|
|
2984
|
-
effectLedgerMirror = readEffectLedger();
|
|
2985
|
-
synchronizeDeferredProjection(effectLedgerMirror);
|
|
2986
|
-
}
|
|
2987
|
-
suppressInspectionEmissions = false;
|
|
2988
|
-
createRuntimeActor();
|
|
2989
|
-
const state = currentState();
|
|
2990
|
-
await emitTrace('session.started', {
|
|
2991
|
-
state,
|
|
2992
|
-
...stateIdentity(state),
|
|
2993
|
-
});
|
|
2994
|
-
actor?.start();
|
|
2995
|
-
await flush();
|
|
2996
|
-
}
|
|
2997
|
-
catch (error) {
|
|
2998
|
-
await cleanupFailedStart(error, { emitDisposal: true });
|
|
2999
|
-
throw error;
|
|
3000
|
-
}
|
|
3001
|
-
finally {
|
|
3002
|
-
finishInitialization();
|
|
3003
|
-
if (initInFlight === initialization)
|
|
3004
|
-
initInFlight = undefined;
|
|
3005
|
-
}
|
|
3006
|
-
},
|
|
3007
|
-
describe: describeControlView,
|
|
3008
|
-
unresolvedEffectEnvelopes: unresolvedEffectEnvelopeIdentities,
|
|
3009
|
-
apply: applyControlAction,
|
|
3010
|
-
// DR-014 §1 / DR-031 §5 / PBRT-45: JSON-safe capture of a parked
|
|
3011
|
-
// session, including one already-started suspended REVIEW call.
|
|
3012
|
-
// Defined only at a safe capture point — initialized, not disposing
|
|
3013
|
-
// or disposed, no active public boundary, and the actor quiescent with
|
|
3014
|
-
// status `active`.
|
|
3015
|
-
exportSnapshot() {
|
|
3016
|
-
if (!actor ||
|
|
3017
|
-
!sessionIdentity ||
|
|
3018
|
-
disposed ||
|
|
3019
|
-
disposalPromise !== undefined) {
|
|
3020
|
-
return undefined;
|
|
3021
|
-
}
|
|
3022
|
-
if (currentTurnId !== undefined || currentSignal !== undefined) {
|
|
3023
|
-
return undefined;
|
|
3024
|
-
}
|
|
3025
|
-
const pendingCall = nestedBridge.getPendingCall();
|
|
3026
|
-
const bridgeSuspendedCall = nestedBridge.getSuspendedCall();
|
|
3027
|
-
if ((pendingCall === undefined) !== (bridgeSuspendedCall === undefined)) {
|
|
3028
|
-
return undefined;
|
|
3029
|
-
}
|
|
3030
|
-
if (pendingCall !== undefined &&
|
|
3031
|
-
bridgeSuspendedCall !== undefined &&
|
|
3032
|
-
(pendingCall.callId !== bridgeSuspendedCall.callId ||
|
|
3033
|
-
pendingCall.playbookId !== bridgeSuspendedCall.playbookId ||
|
|
3034
|
-
pendingCall.childSessionId !== bridgeSuspendedCall.childSessionId)) {
|
|
3035
|
-
return undefined;
|
|
3036
|
-
}
|
|
3037
|
-
let suspendedCall;
|
|
3038
|
-
if (bridgeSuspendedCall !== undefined) {
|
|
3039
|
-
if (!playbookCallTurnIds.has(bridgeSuspendedCall.callId)) {
|
|
3040
|
-
return undefined;
|
|
3041
|
-
}
|
|
3042
|
-
const turnId = playbookCallTurnIds.get(bridgeSuspendedCall.callId);
|
|
3043
|
-
if (bridgeSuspendedCall.turnId !== undefined &&
|
|
3044
|
-
bridgeSuspendedCall.turnId !== turnId) {
|
|
3045
|
-
return undefined;
|
|
3046
|
-
}
|
|
3047
|
-
suspendedCall = {
|
|
3048
|
-
...bridgeSuspendedCall,
|
|
3049
|
-
...(turnId === undefined ? {} : { turnId }),
|
|
3050
|
-
};
|
|
3051
|
-
}
|
|
3052
|
-
const state = currentState();
|
|
3053
|
-
if (state.status !== 'active' || !state.quiescent)
|
|
3054
|
-
return undefined;
|
|
3055
|
-
const machine = detachPersistedMachineSnapshot(actor.getPersistedSnapshot());
|
|
3056
|
-
const context = actor.getSnapshot().context;
|
|
3057
|
-
if (deferredEffects !== undefined) {
|
|
3058
|
-
synchronizeDeferredProjection(readEffectLedger());
|
|
3059
|
-
}
|
|
3060
|
-
const unresolved = hasUnresolvedReconciliation();
|
|
3061
|
-
return {
|
|
3062
|
-
schemaVersion: 4,
|
|
3063
|
-
playbookId: sessionIdentity.playbookId,
|
|
3064
|
-
machine,
|
|
3065
|
-
roleResumeTokens: snapshotRoleResumeTokens(),
|
|
3066
|
-
sequences: {
|
|
3067
|
-
trace: traceSequence,
|
|
3068
|
-
turn: turnSequence,
|
|
3069
|
-
judgeCall: judgeCallSequence,
|
|
3070
|
-
playerCall: playerCallSequence,
|
|
3071
|
-
playbookCall: playbookCallSequence,
|
|
3072
|
-
},
|
|
3073
|
-
state,
|
|
3074
|
-
pendingBossQuestions: unresolved
|
|
3075
|
-
? []
|
|
3076
|
-
: visiblePendingQuestionsForState(state, context).map((pending) => ({
|
|
3077
|
-
questionId: pending.questionId,
|
|
3078
|
-
asker: pending.asker,
|
|
3079
|
-
question: pending.question,
|
|
3080
|
-
sourceItem: pending.sourceItem,
|
|
3081
|
-
})),
|
|
3082
|
-
effectLedger: effectLedgerMirror,
|
|
3083
|
-
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
3084
|
-
};
|
|
3085
|
-
},
|
|
3086
|
-
// DR-014 §1 / PBRT-45: alternative to `init` that rehydrates an
|
|
3087
|
-
// exported snapshot under the same immutable session identity.
|
|
3088
|
-
// Emits no `session.started`, transition trace, or human status —
|
|
3089
|
-
// the session already started; the next public boundary continues
|
|
3090
|
-
// the contiguous trace sequence.
|
|
3091
|
-
async restore(session, snapshot) {
|
|
3092
|
-
if (lifecycleStarted ||
|
|
3093
|
-
initInFlight !== undefined ||
|
|
3094
|
-
disposed ||
|
|
3095
|
-
disposalPromise !== undefined) {
|
|
3096
|
-
throw new Error('decide runtime: restore(session, snapshot) may only be called once');
|
|
3097
|
-
}
|
|
3098
|
-
const identity = bindSession(session);
|
|
3099
|
-
const boundSnapshot = assertPlaybookRuntimeSnapshot(snapshot, identity.playbookId, { allowSuspendedCall: true });
|
|
3100
|
-
if (deferredEffects === undefined) {
|
|
3101
|
-
if (boundSnapshot.effectLedger.revision !== 0 ||
|
|
3102
|
-
boundSnapshot.effectLedger.boundaries.length !== 0 ||
|
|
3103
|
-
boundSnapshot.effectLedger.logicalOperations.length !== 0) {
|
|
3104
|
-
throw new TypeError('decide runtime snapshot effectLedger must be the canonical empty ledger');
|
|
3105
|
-
}
|
|
3106
|
-
}
|
|
3107
|
-
else {
|
|
3108
|
-
const current = readEffectLedger();
|
|
3109
|
-
if (stableJson(current, 'DECIDE current effect ledger') !==
|
|
3110
|
-
stableJson(boundSnapshot.effectLedger, 'DECIDE snapshot effect ledger')) {
|
|
3111
|
-
throw new TypeError('decide runtime snapshot effectLedger must equal the current host mirror');
|
|
3112
|
-
}
|
|
3113
|
-
effectLedgerMirror = current;
|
|
3114
|
-
}
|
|
3115
|
-
const suspendedCall = boundSnapshot.suspendedCall;
|
|
3116
|
-
let finishInitialization;
|
|
3117
|
-
const initialization = new Promise((resolve) => {
|
|
3118
|
-
finishInitialization = resolve;
|
|
3119
|
-
});
|
|
3120
|
-
initInFlight = initialization;
|
|
3121
|
-
lifecycleStarted = true;
|
|
3122
|
-
ports = identity.ports;
|
|
3123
|
-
sessionIdentity = identity;
|
|
3124
|
-
let priorExternalRoleTokens;
|
|
3125
|
-
try {
|
|
3126
|
-
traceSequence = boundSnapshot.sequences.trace;
|
|
3127
|
-
turnSequence = boundSnapshot.sequences.turn;
|
|
3128
|
-
judgeCallSequence = boundSnapshot.sequences.judgeCall;
|
|
3129
|
-
playerCallSequence = boundSnapshot.sequences.playerCall;
|
|
3130
|
-
playbookCallSequence = boundSnapshot.sequences.playbookCall;
|
|
3131
|
-
applyCallSequence = boundSnapshot.sequences.trace;
|
|
3132
|
-
if (identity.playerSessions) {
|
|
3133
|
-
priorExternalRoleTokens = snapshotRoleResumeTokens();
|
|
3134
|
-
}
|
|
3135
|
-
restoreRoleResumeTokens(boundSnapshot.roleResumeTokens);
|
|
3136
|
-
nestedBridge.prepareRestore(suspendedCall);
|
|
3137
|
-
if (suspendedCall !== undefined) {
|
|
3138
|
-
playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
|
|
3139
|
-
}
|
|
3140
|
-
synchronizeDeferredProjection(effectLedgerMirror);
|
|
3141
|
-
suppressInspectionEmissions = true;
|
|
3142
|
-
createRuntimeActor(boundSnapshot.machine);
|
|
3143
|
-
actor?.start();
|
|
3144
|
-
const restoredState = currentState(suspendedCall);
|
|
3145
|
-
if (restoredState.status !== 'active') {
|
|
3146
|
-
throw new Error(`decide runtime: restored actor status is ${restoredState.status}, expected active`);
|
|
3147
|
-
}
|
|
3148
|
-
if (stableJson(restoredState, 'restored runtime state') !==
|
|
3149
|
-
stableJson(boundSnapshot.state, 'runtime snapshot state')) {
|
|
3150
|
-
throw new Error('decide runtime: restored actor state does not match snapshot state');
|
|
3151
|
-
}
|
|
3152
|
-
previousState = restoredState;
|
|
3153
|
-
await flush();
|
|
3154
|
-
suppressInspectionEmissions = false;
|
|
3155
|
-
// Final fallible step: after this publication the authoritative
|
|
3156
|
-
// child has rejoined ordinary resume/abort ownership, so no later
|
|
3157
|
-
// restore validation may trigger failed-start rollback.
|
|
3158
|
-
nestedBridge.confirmRestore();
|
|
3159
|
-
}
|
|
3160
|
-
catch (error) {
|
|
3161
|
-
let failure = error;
|
|
3162
|
-
if (priorExternalRoleTokens !== undefined) {
|
|
3163
|
-
try {
|
|
3164
|
-
identity.playerSessions.restore(priorExternalRoleTokens);
|
|
3165
|
-
}
|
|
3166
|
-
catch (rollbackError) {
|
|
3167
|
-
failure = new AggregateError([error, rollbackError], 'DECIDE restore and player continuation rollback failed');
|
|
140
|
+
"repositoryDisposition": "unchanged"
|
|
3168
141
|
}
|
|
3169
|
-
}
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
}
|
|
3178
|
-
},
|
|
3179
|
-
async handleBossInput(turn) {
|
|
3180
|
-
if (disposalPromise !== undefined) {
|
|
3181
|
-
throw new Error('decide runtime: runtime is disposing or disposed');
|
|
3182
|
-
}
|
|
3183
|
-
requirePorts();
|
|
3184
|
-
if (!actor) {
|
|
3185
|
-
throw new Error('decide runtime: init(session) must be called before handleBossInput');
|
|
3186
|
-
}
|
|
3187
|
-
if (currentSignal !== undefined) {
|
|
3188
|
-
throw new Error('decide runtime: another runtime turn is active');
|
|
3189
|
-
}
|
|
3190
|
-
const turnId = ++turnSequence;
|
|
3191
|
-
currentTurnId = turnId;
|
|
3192
|
-
currentSignal = turn.signal;
|
|
3193
|
-
currentAborts = abortReasonClassifier(turn.signal);
|
|
3194
|
-
controlPlaneError = undefined;
|
|
3195
|
-
let result = resultForSnapshot(turn.signal);
|
|
3196
|
-
let settlement = result;
|
|
3197
|
-
const failures = [];
|
|
3198
|
-
try {
|
|
3199
|
-
await emitTrace('boss.input.received', { text: turn.text }, { turnId });
|
|
3200
|
-
// A boundary entered aborted records the attempted input, then refuses
|
|
3201
|
-
// delivery before deterministic mapping or the classifier can perform
|
|
3202
|
-
// any host-visible work (DR-036 §5).
|
|
3203
|
-
turn.signal.throwIfAborted();
|
|
3204
|
-
if (turn.text.trim().length === 0) {
|
|
3205
|
-
const state = currentState();
|
|
3206
|
-
result = { outcome: 'no-action', state };
|
|
3207
|
-
}
|
|
3208
|
-
else {
|
|
3209
|
-
const event = await classify(turn.text, turn.signal);
|
|
3210
|
-
if (!event) {
|
|
3211
|
-
const state = currentState();
|
|
3212
|
-
if (!hasUnresolvedReconciliation()) {
|
|
3213
|
-
await emitBoundaryStatus('No playbook action classified.', state);
|
|
3214
|
-
}
|
|
3215
|
-
result = { outcome: 'no-action', state };
|
|
3216
|
-
}
|
|
3217
|
-
else if (event.type === 'NO_ACTION') {
|
|
3218
|
-
result = { outcome: 'no-action', state: currentState() };
|
|
3219
|
-
}
|
|
3220
|
-
else {
|
|
3221
|
-
const before = currentState();
|
|
3222
|
-
const boundCommitWait = deferredEffects !== undefined &&
|
|
3223
|
-
deferredOperationId !== undefined &&
|
|
3224
|
-
before.activeStateIds.includes('awaitBossReply');
|
|
3225
|
-
if (boundCommitWait && event.type === 'BOSS_INTERRUPT') {
|
|
3226
|
-
await parkDeferredContinuation(turn.signal);
|
|
3227
|
-
result = { outcome: 'no-action', state: currentState() };
|
|
3228
|
-
}
|
|
3229
|
-
else {
|
|
3230
|
-
const prepared = boundCommitWait &&
|
|
3231
|
-
event.type === 'BOSS_REPLY' &&
|
|
3232
|
-
event.questionId === 'commitCoderProposal'
|
|
3233
|
-
? await prepareDeferredContinuation(turn.signal, event)
|
|
3234
|
-
: undefined;
|
|
3235
|
-
if (prepared?.proceed === false) {
|
|
3236
|
-
result = { outcome: 'no-action', state: currentState() };
|
|
3237
|
-
}
|
|
3238
|
-
else {
|
|
3239
|
-
await emitBoundaryStatus(event.type, before);
|
|
3240
|
-
if (actor.getSnapshot().status === 'done') {
|
|
3241
|
-
stopActor();
|
|
3242
|
-
startActor();
|
|
3243
|
-
}
|
|
3244
|
-
const deferredMachineCheckpoint = prepared?.proceed === true
|
|
3245
|
-
? detachPersistedMachineSnapshot(actor.getPersistedSnapshot())
|
|
3246
|
-
: undefined;
|
|
3247
|
-
if (deferredMachineCheckpoint !== undefined) {
|
|
3248
|
-
suppressInspectionEmissions = true;
|
|
3249
|
-
}
|
|
3250
|
-
try {
|
|
3251
|
-
actor.send(event);
|
|
3252
|
-
await driveToQuiescence();
|
|
3253
|
-
await drainBoundaryCallsAndEmissions();
|
|
3254
|
-
await prepared?.acknowledgement;
|
|
3255
|
-
if (deferredMachineCheckpoint !== undefined &&
|
|
3256
|
-
hiddenDeferredOperationId !== undefined) {
|
|
3257
|
-
stopActor();
|
|
3258
|
-
createRuntimeActor(deferredMachineCheckpoint);
|
|
3259
|
-
actor.start();
|
|
3260
|
-
previousState = before;
|
|
3261
|
-
suppressInspectionEmissions = false;
|
|
3262
|
-
}
|
|
3263
|
-
}
|
|
3264
|
-
catch (error) {
|
|
3265
|
-
activeDeferredContinuation?.result.reject(error);
|
|
3266
|
-
if (deferredEffects !== undefined) {
|
|
3267
|
-
try {
|
|
3268
|
-
synchronizeDeferredProjection(readEffectLedger());
|
|
3269
|
-
hiddenDeferredOperationId ??= deferredOperationId;
|
|
3270
|
-
}
|
|
3271
|
-
catch {
|
|
3272
|
-
hiddenDeferredOperationId ??= deferredOperationId;
|
|
3273
|
-
}
|
|
3274
|
-
}
|
|
3275
|
-
if (deferredMachineCheckpoint !== undefined) {
|
|
3276
|
-
try {
|
|
3277
|
-
stopActor();
|
|
3278
|
-
createRuntimeActor(deferredMachineCheckpoint);
|
|
3279
|
-
actor.start();
|
|
3280
|
-
previousState = before;
|
|
3281
|
-
}
|
|
3282
|
-
finally {
|
|
3283
|
-
suppressInspectionEmissions = false;
|
|
3284
|
-
}
|
|
3285
|
-
}
|
|
3286
|
-
throw error;
|
|
3287
|
-
}
|
|
3288
|
-
finally {
|
|
3289
|
-
activeDeferredContinuation = undefined;
|
|
3290
|
-
}
|
|
3291
|
-
if (controlPlaneError !== undefined)
|
|
3292
|
-
throw controlPlaneError;
|
|
3293
|
-
result = resultForSnapshot(turn.signal);
|
|
3294
|
-
}
|
|
3295
|
-
}
|
|
3296
|
-
}
|
|
3297
|
-
}
|
|
3298
|
-
settlement = {
|
|
3299
|
-
...result,
|
|
3300
|
-
...stateIdentity(result.state),
|
|
3301
|
-
};
|
|
3302
|
-
}
|
|
3303
|
-
catch (error) {
|
|
3304
|
-
const primaryError = controlPlaneError;
|
|
3305
|
-
// Only a rejection that is the exact abort reason settles as the
|
|
3306
|
-
// cancellation; a distinct failure observed while the signal is
|
|
3307
|
-
// aborted remains a control error (slc/link.md §Abort).
|
|
3308
|
-
if (primaryError !== undefined) {
|
|
3309
|
-
collectFailure(failures, primaryError);
|
|
3310
|
-
}
|
|
3311
|
-
else if (!isAbortFailure(error, turn.signal)) {
|
|
3312
|
-
collectFailure(failures, error);
|
|
3313
|
-
}
|
|
3314
|
-
const state = currentState();
|
|
3315
|
-
const effectiveError = primaryError ?? error;
|
|
3316
|
-
result =
|
|
3317
|
-
isAbortFailure(error, turn.signal) && primaryError === undefined
|
|
3318
|
-
? resultForSnapshot(turn.signal)
|
|
3319
|
-
: {
|
|
3320
|
-
outcome: 'failed',
|
|
3321
|
-
state,
|
|
3322
|
-
error: normalizeErrorFull(effectiveError) ?? {
|
|
3323
|
-
name: 'Error',
|
|
3324
|
-
message: String(effectiveError),
|
|
3325
|
-
},
|
|
3326
|
-
};
|
|
3327
|
-
settlement = {
|
|
3328
|
-
...result,
|
|
3329
|
-
...stateIdentity(state),
|
|
3330
|
-
};
|
|
3331
|
-
}
|
|
3332
|
-
try {
|
|
3333
|
-
await drainBoundaryCallsAndEmissions();
|
|
3334
|
-
}
|
|
3335
|
-
catch (error) {
|
|
3336
|
-
const primaryError = controlPlaneError;
|
|
3337
|
-
const effectiveError = primaryError ?? error;
|
|
3338
|
-
// A drain rejection that is the exact abort reason evidences the
|
|
3339
|
-
// cancellation, not a control-plane failure (slc/link.md §Abort).
|
|
3340
|
-
const drainAborted = isAbortFailure(effectiveError, turn.signal);
|
|
3341
|
-
if (!drainAborted)
|
|
3342
|
-
collectFailure(failures, effectiveError);
|
|
3343
|
-
const state = currentState();
|
|
3344
|
-
result = {
|
|
3345
|
-
outcome: drainAborted ? 'aborted' : 'failed',
|
|
3346
|
-
state,
|
|
3347
|
-
error: normalizeErrorFull(effectiveError) ?? {
|
|
3348
|
-
name: 'Error',
|
|
3349
|
-
message: String(effectiveError),
|
|
3350
|
-
},
|
|
3351
|
-
};
|
|
3352
|
-
settlement = { ...result, ...stateIdentity(state) };
|
|
3353
|
-
}
|
|
3354
|
-
try {
|
|
3355
|
-
await emitTrace('boss.input.settled', settlement, { turnId });
|
|
3356
|
-
}
|
|
3357
|
-
catch (error) {
|
|
3358
|
-
// A settlement-trace rejection that is the exact abort reason also
|
|
3359
|
-
// evidences the cancellation (slc/link.md §Abort).
|
|
3360
|
-
if (!isAbortFailure(error, turn.signal)) {
|
|
3361
|
-
collectFailure(failures, error);
|
|
3362
|
-
}
|
|
3363
|
-
}
|
|
3364
|
-
try {
|
|
3365
|
-
await flush();
|
|
3366
|
-
}
|
|
3367
|
-
catch (error) {
|
|
3368
|
-
// A late flush rejection that is the exact abort reason likewise
|
|
3369
|
-
// evidences the cancellation; the settled result already labels
|
|
3370
|
-
// the turn aborted then (slc/link.md §Abort).
|
|
3371
|
-
if (!isAbortFailure(error, turn.signal)) {
|
|
3372
|
-
collectFailure(failures, error);
|
|
3373
|
-
}
|
|
3374
|
-
}
|
|
3375
|
-
finally {
|
|
3376
|
-
const primaryError = controlPlaneError;
|
|
3377
|
-
currentSignal = undefined;
|
|
3378
|
-
currentAborts = undefined;
|
|
3379
|
-
currentTurnId = undefined;
|
|
3380
|
-
controlPlaneError = undefined;
|
|
3381
|
-
if (primaryError !== undefined)
|
|
3382
|
-
throw primaryError;
|
|
3383
|
-
}
|
|
3384
|
-
if (failures.length === 1)
|
|
3385
|
-
throw failures[0];
|
|
3386
|
-
if (failures.length > 1) {
|
|
3387
|
-
throw new AggregateError(failures, 'decide runtime turn failed');
|
|
3388
|
-
}
|
|
3389
|
-
return result;
|
|
3390
|
-
},
|
|
3391
|
-
async resumePlaybookCall({ callId, result: childResult, signal, }) {
|
|
3392
|
-
if (disposalPromise !== undefined) {
|
|
3393
|
-
throw new Error('decide runtime: runtime is disposing or disposed');
|
|
3394
|
-
}
|
|
3395
|
-
requirePorts();
|
|
3396
|
-
if (!actor) {
|
|
3397
|
-
throw new Error('decide runtime: init(session) must be called before resumePlaybookCall');
|
|
3398
|
-
}
|
|
3399
|
-
if (currentSignal !== undefined) {
|
|
3400
|
-
throw new Error('decide runtime: another runtime turn is active');
|
|
3401
|
-
}
|
|
3402
|
-
refreshReconciliationProjection();
|
|
3403
|
-
if (hasUnresolvedReconciliation()) {
|
|
3404
|
-
return { outcome: 'no-action', state: currentState() };
|
|
3405
|
-
}
|
|
3406
|
-
currentTurnId = playbookCallTurnIds.get(callId);
|
|
3407
|
-
currentSignal = signal;
|
|
3408
|
-
currentAborts = abortReasonClassifier(signal);
|
|
3409
|
-
controlPlaneError = undefined;
|
|
3410
|
-
let runResult;
|
|
3411
|
-
let operationError;
|
|
3412
|
-
try {
|
|
3413
|
-
await nestedBridge.resume({
|
|
3414
|
-
callId,
|
|
3415
|
-
result: childResult,
|
|
3416
|
-
signal,
|
|
3417
|
-
});
|
|
3418
|
-
}
|
|
3419
|
-
catch (error) {
|
|
3420
|
-
operationError = error;
|
|
3421
|
-
}
|
|
3422
|
-
try {
|
|
3423
|
-
await waitForPlaybookQuiescence(actor, {
|
|
3424
|
-
pendingCalls: nestedBridge,
|
|
3425
|
-
});
|
|
3426
|
-
runResult = resultForSnapshot(signal);
|
|
3427
|
-
}
|
|
3428
|
-
catch (error) {
|
|
3429
|
-
operationError ??= error;
|
|
3430
|
-
}
|
|
3431
|
-
let drainError;
|
|
3432
|
-
try {
|
|
3433
|
-
await drainBoundaryCallsAndEmissions();
|
|
3434
|
-
}
|
|
3435
|
-
catch (error) {
|
|
3436
|
-
drainError = error;
|
|
3437
|
-
}
|
|
3438
|
-
const aborts = currentAborts ?? abortReasonClassifier(signal);
|
|
3439
|
-
// The control latch has already classified its failure as distinct
|
|
3440
|
-
// under the operation that owned it. Only still-unclassified drain and
|
|
3441
|
-
// operation candidates may be cancellation evidence for this resume.
|
|
3442
|
-
const controlFailure = controlPlaneError;
|
|
3443
|
-
const drainAbort = controlFailure === undefined &&
|
|
3444
|
-
drainError !== undefined &&
|
|
3445
|
-
aborts.isAbortReason(drainError);
|
|
3446
|
-
const operationAbort = controlFailure === undefined &&
|
|
3447
|
-
operationError !== undefined &&
|
|
3448
|
-
aborts.isAbortReason(operationError);
|
|
3449
|
-
const abortEvidence = (drainAbort ? drainError : undefined) ??
|
|
3450
|
-
(operationAbort ? operationError : undefined);
|
|
3451
|
-
const failure = controlFailure ??
|
|
3452
|
-
(drainAbort ? undefined : drainError) ??
|
|
3453
|
-
(operationAbort ? undefined : operationError);
|
|
3454
|
-
currentSignal = undefined;
|
|
3455
|
-
currentAborts = undefined;
|
|
3456
|
-
currentTurnId = undefined;
|
|
3457
|
-
controlPlaneError = undefined;
|
|
3458
|
-
if (failure !== undefined)
|
|
3459
|
-
throw failure;
|
|
3460
|
-
if (abortEvidence !== undefined &&
|
|
3461
|
-
runResult?.outcome !== 'terminal' &&
|
|
3462
|
-
runResult?.outcome !== 'suspended') {
|
|
3463
|
-
const state = currentState();
|
|
3464
|
-
runResult = {
|
|
3465
|
-
outcome: 'aborted',
|
|
3466
|
-
state,
|
|
3467
|
-
error: normalizeErrorFull(abortEvidence) ?? {
|
|
3468
|
-
name: 'AbortError',
|
|
3469
|
-
message: String(abortEvidence),
|
|
142
|
+
},
|
|
143
|
+
"synthesizeCommit": {
|
|
144
|
+
"committed": {
|
|
145
|
+
"fields": {
|
|
146
|
+
"coderOutput": "presentation",
|
|
147
|
+
"latestCommit": "effect"
|
|
148
|
+
},
|
|
149
|
+
"repositoryDisposition": "one-descendant-commit"
|
|
3470
150
|
},
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
runResult = resultForSnapshot(signal);
|
|
3478
|
-
}
|
|
3479
|
-
else {
|
|
3480
|
-
throw new Error('decide runtime: playbook resume produced no result');
|
|
151
|
+
"needsBossReply": {
|
|
152
|
+
"fields": {
|
|
153
|
+
"question": "presentation"
|
|
154
|
+
},
|
|
155
|
+
"repositoryDisposition": "deferred"
|
|
156
|
+
}
|
|
3481
157
|
}
|
|
3482
158
|
}
|
|
3483
|
-
return runResult;
|
|
3484
159
|
},
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
}
|
|
3505
|
-
catch (error) {
|
|
3506
|
-
collectFailure(failures, error);
|
|
3507
|
-
}
|
|
3508
|
-
try {
|
|
3509
|
-
await drainBoundaryCallsAndEmissions();
|
|
3510
|
-
}
|
|
3511
|
-
catch (error) {
|
|
3512
|
-
collectFailure(failures, error);
|
|
3513
|
-
}
|
|
3514
|
-
try {
|
|
3515
|
-
await emitTrace('session.disposed', {
|
|
3516
|
-
state: finalState,
|
|
3517
|
-
...stateIdentity(finalState),
|
|
3518
|
-
});
|
|
3519
|
-
}
|
|
3520
|
-
catch (error) {
|
|
3521
|
-
collectFailure(failures, error);
|
|
3522
|
-
}
|
|
3523
|
-
try {
|
|
3524
|
-
await flush();
|
|
3525
|
-
}
|
|
3526
|
-
catch (error) {
|
|
3527
|
-
collectFailure(failures, error);
|
|
3528
|
-
}
|
|
3529
|
-
finally {
|
|
3530
|
-
privateResumeTokens.clear();
|
|
3531
|
-
playbookCallTurnIds.clear();
|
|
3532
|
-
inFlightPlayerKeys.clear();
|
|
3533
|
-
activeBoundaryCalls.clear();
|
|
3534
|
-
activeEmissionCalls.clear();
|
|
3535
|
-
emissionQueue.clear();
|
|
3536
|
-
judgeQueue.clear();
|
|
3537
|
-
semanticCompletionQueue.clear();
|
|
3538
|
-
actor = undefined;
|
|
3539
|
-
currentSignal = undefined;
|
|
3540
|
-
currentAborts = undefined;
|
|
3541
|
-
actorSettlementAborts.length = 0;
|
|
3542
|
-
actorSettlementErrorAborts = undefined;
|
|
3543
|
-
currentTurnId = undefined;
|
|
3544
|
-
ports = undefined;
|
|
3545
|
-
sessionIdentity = undefined;
|
|
3546
|
-
previousState = undefined;
|
|
3547
|
-
controlPlaneError = undefined;
|
|
3548
|
-
governedOutputsByBoundaryId.clear();
|
|
3549
|
-
governedFailuresByBoundaryId.clear();
|
|
3550
|
-
governedEvidenceByBoundaryId.clear();
|
|
3551
|
-
governedReceiptsByBoundaryId.clear();
|
|
3552
|
-
pendingProposalCohort.clear();
|
|
3553
|
-
activeProposalCohort = undefined;
|
|
3554
|
-
completedProposalCohortTurnId = undefined;
|
|
3555
|
-
deferredOperationId = undefined;
|
|
3556
|
-
hiddenDeferredOperationId = undefined;
|
|
3557
|
-
unresolvedSemanticBoundaryIds.clear();
|
|
3558
|
-
appliedControlReceipts.clear();
|
|
3559
|
-
applyCallSequence = 0;
|
|
3560
|
-
activeDeferredContinuation = undefined;
|
|
3561
|
-
disposed = true;
|
|
3562
|
-
}
|
|
3563
|
-
if (failures.length === 1)
|
|
3564
|
-
throw failures[0];
|
|
3565
|
-
if (failures.length > 1) {
|
|
3566
|
-
throw new AggregateError(failures, 'decide runtime disposal failed');
|
|
3567
|
-
}
|
|
3568
|
-
})();
|
|
3569
|
-
return disposalPromise;
|
|
3570
|
-
},
|
|
3571
|
-
// @internal — test-only parity with the shared factory's bridge escape
|
|
3572
|
-
// hatch. This is hidden by the PlaybookRuntime return type.
|
|
3573
|
-
_getNestedBridge() {
|
|
3574
|
-
return nestedBridge;
|
|
3575
|
-
},
|
|
3576
|
-
};
|
|
3577
|
-
}
|
|
3578
|
-
export const createPlaybookRuntime = (factoryInput) => {
|
|
3579
|
-
const construction = schema3Construction(factoryInput);
|
|
3580
|
-
return createDecidePlaybookRuntime(construction.configuredOptions, construction.hostCapabilities);
|
|
160
|
+
"placeholderFields": {},
|
|
161
|
+
"transitionEventFields": [
|
|
162
|
+
"callerTopic",
|
|
163
|
+
"targetId",
|
|
164
|
+
"answer",
|
|
165
|
+
"questionId"
|
|
166
|
+
],
|
|
167
|
+
"controlContextFields": [
|
|
168
|
+
"decideCommit",
|
|
169
|
+
"evaluatedRevision",
|
|
170
|
+
"reviewStatus"
|
|
171
|
+
]
|
|
172
|
+
},
|
|
173
|
+
snapshotOptions: validateOptions,
|
|
174
|
+
machineInput: (options) => Object.fromEntries(Object.entries(INPUT_MAPPING).filter(([, key]) => options[key] !== undefined)
|
|
175
|
+
.map(([field, key]) => [field, options[key]])),
|
|
176
|
+
verbatimPayloadFields: VERBATIM_PAYLOAD_FIELDS,
|
|
177
|
+
resumableStateIds: RESUMABLE_STATE_IDS,
|
|
178
|
+
unfinishedFinalStateIds: UNFINISHED_FINAL_STATE_IDS,
|
|
3581
179
|
};
|
|
3582
180
|
export const _internal = {
|
|
3583
181
|
composePlayerPrompt,
|
|
3584
|
-
|
|
3585
|
-
extractJson,
|
|
3586
|
-
buildClassifierPrompt,
|
|
3587
|
-
parseClassification,
|
|
3588
|
-
buildAdjudicatorPrompt,
|
|
3589
|
-
combineSignals,
|
|
3590
|
-
pendingQuestionsFromContext,
|
|
3591
|
-
pendingQuestionsForState,
|
|
3592
|
-
normalizeErrorCompact,
|
|
3593
|
-
normalizeErrorFull,
|
|
3594
|
-
STATE_DESCRIPTIONS,
|
|
3595
|
-
ROLE_STATES,
|
|
3596
|
-
ROLE_STATE_IDS,
|
|
3597
|
-
VERBATIM_PAYLOAD_FIELDS,
|
|
3598
|
-
BOSS_INTERRUPT_TARGETS,
|
|
182
|
+
RESUMABLE_STATE_IDS,
|
|
3599
183
|
UNFINISHED_FINAL_STATE_IDS,
|
|
3600
|
-
|
|
184
|
+
VERBATIM_PAYLOAD_FIELDS,
|
|
3601
185
|
};
|
|
186
|
+
const createPlaybookRuntime = createXStatePlaybookRuntime(machine, runtimeSpec);
|
|
3602
187
|
export default createPlaybookRuntime;
|