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