@sublang/playbook 7.0.0 → 8.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 (61) hide show
  1. package/README.md +17 -4
  2. package/docs/cli.md +74 -29
  3. package/docs/configuration.md +209 -112
  4. package/docs/embedding.md +71 -25
  5. package/package.json +4 -3
  6. package/reference/sdlc/captain.playbook/captain.playbook.js +3 -3
  7. package/reference/sdlc/captain.playbook/captain.playbook.ts +3 -3
  8. package/reference/sdlc/code.md +1 -1
  9. package/reference/sdlc/code.playbook/bin/interactive-session.js +816 -0
  10. package/reference/sdlc/code.playbook/bin/launch-config.js +1078 -116
  11. package/reference/sdlc/code.playbook/bin/playbook.js +489 -34
  12. package/reference/sdlc/code.playbook/bin/run.js +283 -298
  13. package/reference/sdlc/code.playbook/bin/session-store.js +818 -26
  14. package/reference/sdlc/code.playbook/code.fsm.d.ts +5 -5
  15. package/reference/sdlc/code.playbook/code.fsm.introspect.js +2 -2
  16. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +2 -2
  17. package/reference/sdlc/code.playbook/code.fsm.js +7 -11
  18. package/reference/sdlc/code.playbook/code.fsm.ts +9 -17
  19. package/reference/sdlc/code.playbook/code.gears.md +1 -1
  20. package/reference/sdlc/code.playbook/code.playbook.d.ts +2 -1
  21. package/reference/sdlc/code.playbook/code.playbook.js +12 -13
  22. package/reference/sdlc/code.playbook/code.playbook.ts +22 -15
  23. package/reference/sdlc/code.playbook/code.registry.d.ts +5 -13
  24. package/reference/sdlc/code.playbook/code.registry.js +3 -10
  25. package/reference/sdlc/code.playbook/code.registry.ts +7 -32
  26. package/reference/sdlc/code.playbook/playbook-captain.d.ts +39 -14
  27. package/reference/sdlc/code.playbook/playbook-captain.js +970 -289
  28. package/reference/sdlc/code.playbook/playbook-captain.ts +1403 -396
  29. package/reference/sdlc/code.playbook/playbook.config.template.yaml +41 -49
  30. package/reference/sdlc/decide.md +4 -4
  31. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +9 -9
  32. package/reference/sdlc/decide.playbook/decide.fsm.js +21 -14
  33. package/reference/sdlc/decide.playbook/decide.fsm.ts +27 -23
  34. package/reference/sdlc/decide.playbook/decide.gears.md +3 -5
  35. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +9 -13
  36. package/reference/sdlc/decide.playbook/decide.playbook.js +171 -134
  37. package/reference/sdlc/decide.playbook/decide.playbook.ts +238 -162
  38. package/reference/sdlc/decide.playbook/decide.registry.d.ts +5 -13
  39. package/reference/sdlc/decide.playbook/decide.registry.js +3 -9
  40. package/reference/sdlc/decide.playbook/decide.registry.ts +7 -31
  41. package/reference/sdlc/review.md +4 -5
  42. package/reference/sdlc/review.playbook/review.fsm.d.ts +9 -11
  43. package/reference/sdlc/review.playbook/review.fsm.js +30 -24
  44. package/reference/sdlc/review.playbook/review.fsm.ts +39 -35
  45. package/reference/sdlc/review.playbook/review.gears.md +6 -5
  46. package/reference/sdlc/review.playbook/review.playbook.d.ts +2 -1
  47. package/reference/sdlc/review.playbook/review.playbook.js +16 -21
  48. package/reference/sdlc/review.playbook/review.playbook.ts +26 -26
  49. package/reference/sdlc/review.playbook/review.registry.d.ts +5 -13
  50. package/reference/sdlc/review.playbook/review.registry.js +3 -16
  51. package/reference/sdlc/review.playbook/review.registry.ts +7 -38
  52. package/slc/gears2fsm.md +27 -23
  53. package/slc/link.md +113 -93
  54. package/slc/text2gears.md +19 -18
  55. package/src/runtime.d.ts +20 -16
  56. package/src/runtime.ts +19 -23
  57. package/src/xstate-playbook-runtime.d.ts +21 -17
  58. package/src/xstate-playbook-runtime.js +241 -149
  59. package/src/xstate-playbook-runtime.ts +331 -178
  60. package/src/xstate-runtime.js +63 -24
  61. package/src/xstate-runtime.ts +96 -28
@@ -66,13 +66,13 @@ export interface PlaybookPendingBossQuestionContext {
66
66
  questionId: string;
67
67
  resumeStateId: string;
68
68
  sourceItem: string;
69
- player: string;
69
+ asker: { kind: 'captain' } | { kind: 'role'; roleId: string };
70
70
  question: string;
71
71
  }
72
72
 
73
73
  export interface PlaybookPlayerInput {
74
74
  stateId: string;
75
- player: string;
75
+ role: string;
76
76
  sourceItem: string;
77
77
  prompt: string;
78
78
  result: Readonly<Record<string, string>>;
@@ -113,7 +113,7 @@ export type JudgePurpose =
113
113
  export interface RuntimeBoundaryCalls {
114
114
  callPlayer(
115
115
  input: PlaybookPlayerInput,
116
- playerId: string,
116
+ roleId: string,
117
117
  prompt: string,
118
118
  signal: AbortSignal,
119
119
  ): Promise<PlayerResult>;
@@ -151,11 +151,14 @@ export interface ScheduledStatus {
151
151
  }
152
152
 
153
153
  /** Boss-facing identity for one FSM state whose invoked actor is `player`. */
154
- export interface XStatePlayerStateStatus {
155
- player: string;
154
+ export interface XStateRoleStateStatus {
155
+ role: string;
156
156
  label: string;
157
157
  }
158
158
 
159
+ /** Invocation-scoped lookup exposed only while composing a player prompt. */
160
+ export type XStatePromptIdentity = (roleId: string) => string;
161
+
159
162
  export interface XStateBossEventFieldSpec {
160
163
  /** The judge supplies routing data; the runtime supplies exact Boss text. */
161
164
  source: 'judge' | 'text';
@@ -247,7 +250,7 @@ export const RUNTIME_ABI = 1;
247
250
 
248
251
  /** The linked-artifact schema versions this engine accepts (DR-022). */
249
252
  export const SUPPORTED_ARTIFACT_SCHEMAS: readonly number[] = Object.freeze([
250
- 1,
253
+ 2,
251
254
  ]);
252
255
 
253
256
  /** A linked artifact's declared link-time compatibility values (DR-022). */
@@ -259,14 +262,17 @@ export interface XStatePlaybookRuntimeCompat {
259
262
  }
260
263
 
261
264
  // PBRT-50: validate a declaration against the loaded engine, schema first,
262
- // so one clear diagnostic covers a fully skewed artifact. Absent means a
263
- // legacy artifact emitted before the DR-022 contract; those must keep
264
- // loading unchanged (DR-019 §4), so there is nothing to check.
265
+ // so one clear diagnostic covers a fully skewed artifact. Declaration-free
266
+ // artifacts are schema 1 and cannot be interpreted as local-role artifacts.
265
267
  function assertRuntimeCompat(
266
268
  compat: XStatePlaybookRuntimeCompat | undefined,
267
269
  label: string,
268
270
  ): void {
269
- if (compat === undefined) return;
271
+ if (compat === undefined) {
272
+ throw new TypeError(
273
+ `${label} spec.compat is required for local-role artifacts`,
274
+ );
275
+ }
270
276
  if (compat === null || typeof compat !== 'object') {
271
277
  throw new TypeError(`${label} spec.compat must be an object`);
272
278
  }
@@ -355,8 +361,8 @@ export interface XStatePlaybookRuntimeSpec<TOptions> {
355
361
  label?: string;
356
362
  /**
357
363
  * Link-time compatibility declaration checked at construction against the
358
- * loaded engine's self-report (DR-022). Absent: a legacy artifact emitted
359
- * before the contract constructed with no compatibility check.
364
+ * loaded engine's self-report (DR-022). Absent declarations reject because
365
+ * their overloaded player metadata has no safe local-role interpretation.
360
366
  */
361
367
  compat?: XStatePlaybookRuntimeCompat;
362
368
  /** Validate and JSON-snapshot the caller's per-run options. */
@@ -397,14 +403,15 @@ export interface XStatePlaybookRuntimeSpec<TOptions> {
397
403
  * recoverable FSM-result failures instead.
398
404
  */
399
405
  captainStrategy?: XStateCaptainStrategy<TOptions>;
400
- /** Status line emitted after classification; canonical metadata defaults to the event type, legacy artifacts to none. */
406
+ /** Status line emitted after classification; metadata defaults to the event type. */
401
407
  classificationStatus?: (event: EventObject) => string | undefined;
402
- /** Complete FSM-derived Boss-facing metadata for every `player` state; its presence selects the canonical status profile. */
403
- playerStates?: Readonly<Record<string, XStatePlayerStateStatus>>;
404
- /** Map a player-invoking state's input to the host player id. Default: lowercased player name. */
405
- resolvePlayerId?: (input: PlaybookPlayerInput, options: TOptions) => string;
408
+ /** Complete FSM-derived Boss-facing metadata for every `player` state. */
409
+ roleStates?: Readonly<Record<string, XStateRoleStateStatus>>;
406
410
  /** Compose the player prompt. Default: continuation blocks + `<field>` placeholder substitution. */
407
- composePlayerPrompt?: (input: PlaybookPlayerInput) => string;
411
+ composePlayerPrompt?: (
412
+ input: PlaybookPlayerInput,
413
+ promptIdentity: XStatePromptIdentity,
414
+ ) => string;
408
415
  /** Compose the direct-Captain prompt. Default: continuation blocks + placeholder substitution with deterministic JSON rendering. */
409
416
  composeCaptainPrompt?: (input: PlaybookCaptainInput) => string;
410
417
  /** Linker-known exceptions to the default kebab-token → camel-field mapping. */
@@ -594,16 +601,34 @@ export function pendingBossQuestionFromContext(
594
601
  typeof candidate.questionId !== 'string' ||
595
602
  typeof candidate.resumeStateId !== 'string' ||
596
603
  typeof candidate.sourceItem !== 'string' ||
597
- typeof candidate.player !== 'string' ||
604
+ !isPlainObject(candidate.asker) ||
598
605
  typeof candidate.question !== 'string'
599
606
  ) {
600
607
  return undefined;
601
608
  }
609
+ let asker: PlaybookPendingBossQuestionContext['asker'];
610
+ if (candidate.asker.kind === 'captain') {
611
+ if (Object.keys(candidate.asker).some((key) => key !== 'kind')) {
612
+ return undefined;
613
+ }
614
+ asker = { kind: 'captain' };
615
+ } else if (
616
+ candidate.asker.kind === 'role' &&
617
+ typeof candidate.asker.roleId === 'string' &&
618
+ candidate.asker.roleId.trim().length > 0 &&
619
+ Object.keys(candidate.asker).every(
620
+ (key) => key === 'kind' || key === 'roleId',
621
+ )
622
+ ) {
623
+ asker = { kind: 'role', roleId: candidate.asker.roleId };
624
+ } else {
625
+ return undefined;
626
+ }
602
627
  return {
603
628
  questionId: candidate.questionId,
604
629
  resumeStateId: candidate.resumeStateId,
605
630
  sourceItem: candidate.sourceItem,
606
- player: candidate.player,
631
+ asker,
607
632
  question: candidate.question,
608
633
  };
609
634
  }
@@ -707,11 +732,6 @@ export function defaultComposeCaptainPrompt(
707
732
  return blocks.join('\n\n');
708
733
  }
709
734
 
710
- /** Default player binding: each player to its lowercased name. */
711
- export function defaultResolvePlayerId(input: PlaybookPlayerInput): string {
712
- return input.player.toLowerCase();
713
- }
714
-
715
735
  /**
716
736
  * Default required-field extraction (slc/link.md §Captain adjudication).
717
737
  * Limited to the description's `Output shall include` / `输出应包含` clause;
@@ -748,7 +768,7 @@ export function defaultBuildJudgePrompt(
748
768
  'and outcome descriptions. Reply with exactly one JSON object and no prose.',
749
769
  );
750
770
  lines.push('');
751
- lines.push(`The ${input.player} just produced this output:`);
771
+ lines.push(`The ${input.role} role just produced this output:`);
752
772
  lines.push('');
753
773
  lines.push('```');
754
774
  lines.push(finalText);
@@ -856,7 +876,8 @@ function validateBossReplyOutput(
856
876
 
857
877
  // ---------------------------------------------------------------------------
858
878
  // Delegated-player actor bridge. One PromiseActorLogic the machine invokes
859
- // from every player-invoking state: resolve the playerId, compose the prompt,
879
+ // from every player-invoking state: retain the role, resolve any bound player
880
+ // identity privately, compose the prompt through the ephemeral identity lookup,
860
881
  // await callPlayer, adjudicate the finalText. An `ok` result with a missing,
861
882
  // empty, or whitespace-only finalText earns exactly one corrective re-ask of
862
883
  // the same composed call (DR-028); a non-`ok` result, or a second such empty
@@ -867,8 +888,8 @@ function validateBossReplyOutput(
867
888
  // which only fires on actor.stop(), not on Boss abort.
868
889
  // ---------------------------------------------------------------------------
869
890
 
870
- export interface PlayerBridgeSpec {
871
- resolvePlayerId: (input: PlaybookPlayerInput) => string;
891
+ interface PlayerBridgeSpec {
892
+ resolveRoleId: (input: PlaybookPlayerInput) => string;
872
893
  composePlayerPrompt: (input: PlaybookPlayerInput) => string;
873
894
  adjudication: PlayerAdjudicationSpec;
874
895
  resumableStateIds: ReadonlySet<string>;
@@ -884,12 +905,24 @@ export function createPlayerBridge(
884
905
  return fromPromise<PlaybookActorOutput, PlaybookPlayerInput>(
885
906
  async ({ input, signal }) => {
886
907
  const activeSignal = combineAbortSignals(signal, getActiveSignal?.());
887
- const playerId = spec.resolvePlayerId(input);
888
- const prompt = spec.composePlayerPrompt(input);
908
+ let roleId: string;
909
+ let prompt: string;
910
+ try {
911
+ roleId = spec.resolveRoleId(input);
912
+ prompt = spec.composePlayerPrompt(input);
913
+ } catch (error) {
914
+ onControlPlaneError?.(error);
915
+ throw error;
916
+ }
889
917
  const callPlayer = (resume: string | false) =>
890
918
  boundary
891
- ? boundary.callPlayer(input, playerId, prompt, activeSignal)
892
- : ports.callPlayer(playerId, prompt, activeSignal, { resume });
919
+ ? boundary.callPlayer(
920
+ input,
921
+ roleId,
922
+ prompt,
923
+ activeSignal,
924
+ )
925
+ : ports.callPlayer(roleId, prompt, activeSignal, { resume });
893
926
  let result = await callPlayer(false);
894
927
  if (result.status === 'ok' && isEmptyFinalText(result.finalText)) {
895
928
  // An abort that lands between the empty first result and the
@@ -1063,10 +1096,10 @@ function collectInvokeSources(machine: AnyStateMachine): ReadonlySet<string> {
1063
1096
  return sources;
1064
1097
  }
1065
1098
 
1066
- function collectPlayerStatePlayers(
1099
+ function collectPlayerStateRoles(
1067
1100
  machine: AnyStateMachine,
1068
1101
  ): ReadonlyMap<string, string> {
1069
- const players = new Map<string, string>();
1102
+ const roles = new Map<string, string>();
1070
1103
  const visit = (stateDef: unknown, stateKey: string): void => {
1071
1104
  if (!isPlainObject(stateDef)) return;
1072
1105
  const invoke = stateDef.invoke;
@@ -1092,15 +1125,15 @@ function collectPlayerStatePlayers(
1092
1125
  'player state metadata must use a non-empty state id',
1093
1126
  );
1094
1127
  }
1095
- const player = isPlainObject(playbookMeta)
1096
- ? playbookMeta.player
1128
+ const role = isPlainObject(playbookMeta)
1129
+ ? playbookMeta.role
1097
1130
  : undefined;
1098
- if (typeof player !== 'string' || player.trim().length === 0) {
1131
+ if (typeof role !== 'string' || role.trim().length === 0) {
1099
1132
  throw new TypeError(
1100
- `player state ${stateId} meta.playbook.player must be a non-empty string`,
1133
+ `player state ${stateId} meta.playbook.role must be a non-empty string`,
1101
1134
  );
1102
1135
  }
1103
- players.set(stateId, player);
1136
+ roles.set(stateId, role);
1104
1137
  }
1105
1138
  if (isPlainObject(stateDef.states)) {
1106
1139
  for (const [childKey, child] of Object.entries(stateDef.states)) {
@@ -1114,7 +1147,7 @@ function collectPlayerStatePlayers(
1114
1147
  visit(stateDef, stateKey);
1115
1148
  }
1116
1149
  }
1117
- return players;
1150
+ return roles;
1118
1151
  }
1119
1152
 
1120
1153
  function transitionTargets(transition: unknown): string[] {
@@ -1268,55 +1301,68 @@ function makeDefaultNormalizeTransitionEvent(
1268
1301
  };
1269
1302
  }
1270
1303
 
1271
- function snapshotPlayerStateStatuses(
1272
- value: Readonly<Record<string, XStatePlayerStateStatus>> | undefined,
1304
+ function snapshotRoleStateStatuses(
1305
+ value: unknown,
1273
1306
  label: string,
1274
1307
  machine: AnyStateMachine,
1275
1308
  stateDescriptions: ReadonlyMap<string, string>,
1276
- ): ReadonlyMap<string, XStatePlayerStateStatus> {
1277
- if (value === undefined) return new Map();
1278
- if (!isPlainObject(value)) {
1279
- throw new TypeError(`${label} playerStates must be an object`);
1309
+ ): ReadonlyMap<string, XStateRoleStateStatus> {
1310
+ if (value === undefined) {
1311
+ throw new TypeError(`${label} roleStates must be supplied for schema 2`);
1312
+ }
1313
+ const captured = snapshotJsonValue(value, `${label} roleStates`);
1314
+ if (!isPlainObject(captured)) {
1315
+ throw new TypeError(`${label} roleStates must be an object`);
1280
1316
  }
1281
- const declared = collectPlayerStatePlayers(machine);
1282
- const statuses = new Map<string, XStatePlayerStateStatus>();
1283
- for (const [stateId, candidate] of Object.entries(value)) {
1317
+ const declared = collectPlayerStateRoles(machine);
1318
+ const statuses = new Map<string, XStateRoleStateStatus>();
1319
+ for (const [stateId, candidate] of Object.entries(captured)) {
1284
1320
  if (!declared.has(stateId)) {
1285
1321
  throw new TypeError(
1286
- `${label} playerStates.${stateId} does not name a player state`,
1322
+ `${label} roleStates.${stateId} does not name a player state`,
1287
1323
  );
1288
1324
  }
1325
+ if (isPlainObject(candidate)) {
1326
+ const extra = Object.keys(candidate).find(
1327
+ (key) => key !== 'role' && key !== 'label',
1328
+ );
1329
+ if (extra !== undefined) {
1330
+ throw new TypeError(
1331
+ `${label} roleStates.${stateId}.${extra} is not allowed`,
1332
+ );
1333
+ }
1334
+ }
1289
1335
  if (
1290
1336
  !isPlainObject(candidate) ||
1291
- typeof candidate.player !== 'string' ||
1292
- candidate.player.trim().length === 0 ||
1337
+ typeof candidate.role !== 'string' ||
1338
+ candidate.role.trim().length === 0 ||
1293
1339
  typeof candidate.label !== 'string' ||
1294
1340
  candidate.label.trim().length === 0
1295
1341
  ) {
1296
1342
  throw new TypeError(
1297
- `${label} playerStates.${stateId} must carry non-empty player and label strings`,
1343
+ `${label} roleStates.${stateId} must carry non-empty role and label strings`,
1298
1344
  );
1299
1345
  }
1300
1346
  const expectedLabel = stateDescriptions.get(stateId);
1301
1347
  if (candidate.label !== expectedLabel) {
1302
1348
  throw new TypeError(
1303
- `${label} playerStates.${stateId}.label must equal its FSM description`,
1349
+ `${label} roleStates.${stateId}.label must equal its FSM description`,
1304
1350
  );
1305
1351
  }
1306
- if (candidate.player !== declared.get(stateId)) {
1352
+ if (candidate.role !== declared.get(stateId)) {
1307
1353
  throw new TypeError(
1308
- `${label} playerStates.${stateId}.player must equal its FSM player`,
1354
+ `${label} roleStates.${stateId}.role must equal its FSM role`,
1309
1355
  );
1310
1356
  }
1311
1357
  statuses.set(stateId, {
1312
- player: candidate.player,
1358
+ role: candidate.role,
1313
1359
  label: candidate.label,
1314
1360
  });
1315
1361
  }
1316
1362
  for (const stateId of declared.keys()) {
1317
1363
  if (!statuses.has(stateId)) {
1318
1364
  throw new TypeError(
1319
- `${label} playerStates must declare player state ${stateId}`,
1365
+ `${label} roleStates must declare player state ${stateId}`,
1320
1366
  );
1321
1367
  }
1322
1368
  }
@@ -1331,39 +1377,14 @@ function settlingGuard(event: unknown): string | undefined {
1331
1377
  : undefined;
1332
1378
  }
1333
1379
 
1334
- function legacyStatusesForState(
1335
- state: PlaybookState,
1336
- context: Record<string, unknown>,
1337
- ): ScheduledStatus[] {
1338
- const stateId = state.stateId;
1339
- if (stateId === undefined || SUPPRESSED_ENTRY_STATES.has(stateId)) return [];
1340
- if (stateId === 'awaitBossReply') {
1341
- const pending = pendingBossQuestionFromContext(context);
1342
- return [
1343
- {
1344
- message:
1345
- pending === undefined
1346
- ? 'Awaiting Boss reply.'
1347
- : `${pending.player} asks: ${pending.question}`,
1348
- },
1349
- ];
1350
- }
1351
- if (stateId === 'failed') {
1352
- const lastError = normalizeErrorFull(context.lastError);
1353
- return [
1354
- {
1355
- message: 'Workflow failed; awaiting Boss recovery.',
1356
- ...(lastError === undefined
1357
- ? {}
1358
- : { data: snapshotJsonValue({ lastError }, 'failed status data') }),
1359
- },
1360
- ];
1361
- }
1362
- return [{ message: `Entered ${stateId}.` }];
1380
+ function askerLabel(
1381
+ asker: PlaybookPendingBossQuestionContext['asker'],
1382
+ ): string {
1383
+ return asker.kind === 'captain' ? 'Captain' : asker.roleId;
1363
1384
  }
1364
1385
 
1365
1386
  function makeDefaultStatusesForState(
1366
- playerStates: ReadonlyMap<string, XStatePlayerStateStatus>,
1387
+ roleStates: ReadonlyMap<string, XStateRoleStateStatus>,
1367
1388
  ): NonNullable<XStatePlaybookRuntimeSpec<unknown>['statusesForState']> {
1368
1389
  return (state, context, event): ScheduledStatus[] => {
1369
1390
  const statuses: ScheduledStatus[] = [];
@@ -1381,11 +1402,11 @@ function makeDefaultStatusesForState(
1381
1402
  }
1382
1403
  return [
1383
1404
  ...statuses,
1384
- { message: `${pending.player} asks: ${pending.question}` },
1405
+ { message: `${askerLabel(pending.asker)} asks: ${pending.question}` },
1385
1406
  {
1386
1407
  message:
1387
1408
  `◆ awaiting Boss reply · ${pending.resumeStateId} · ` +
1388
- `${pending.player} · ${pending.sourceItem}`,
1409
+ `${askerLabel(pending.asker)} · ${pending.sourceItem}`,
1389
1410
  },
1390
1411
  ];
1391
1412
  }
@@ -1406,10 +1427,10 @@ function makeDefaultStatusesForState(
1406
1427
  },
1407
1428
  ];
1408
1429
  }
1409
- const playerState = playerStates.get(stateId);
1410
- if (playerState !== undefined) {
1430
+ const roleState = roleStates.get(stateId);
1431
+ if (roleState !== undefined) {
1411
1432
  statuses.push({
1412
- message: `⤷ ${playerState.player}: ${playerState.label}`,
1433
+ message: `⤷ ${roleState.role}: ${roleState.label}`,
1413
1434
  });
1414
1435
  }
1415
1436
  return statuses;
@@ -1672,7 +1693,7 @@ function makeDefaultClassifyBossText(
1672
1693
  if (pending !== undefined) {
1673
1694
  lines.push(
1674
1695
  `Pending question id: ${pending.questionId}`,
1675
- `Pending asking player: ${pending.player}`,
1696
+ `Pending asker: ${askerLabel(pending.asker)}`,
1676
1697
  `Pending Boss question: ${pending.question}`,
1677
1698
  );
1678
1699
  }
@@ -1846,6 +1867,17 @@ export function createXStatePlaybookRuntime<TOptions>(
1846
1867
  // DR-022 / PBRT-50: reject an incompatible artifact declaration before any
1847
1868
  // machine interpretation, against this loaded engine's own self-report.
1848
1869
  assertRuntimeCompat(spec.compat, label);
1870
+ const specDescriptors = Object.getOwnPropertyDescriptors(spec);
1871
+ if (Object.prototype.hasOwnProperty.call(specDescriptors, 'playerStates')) {
1872
+ throw new TypeError(
1873
+ `${label} schema-2 artifacts must supply roleStates, not playerStates`,
1874
+ );
1875
+ }
1876
+ if (Object.prototype.hasOwnProperty.call(specDescriptors, 'resolvePlayerId')) {
1877
+ throw new TypeError(
1878
+ `${label} schema-2 artifacts must not derive concrete player bindings`,
1879
+ );
1880
+ }
1849
1881
  if (machineDeclaresParallelState(machine)) {
1850
1882
  throw new Error(
1851
1883
  `${label} uses a parallel state; the shared runtime supports only single-region FSMs`,
@@ -1857,13 +1889,22 @@ export function createXStatePlaybookRuntime<TOptions>(
1857
1889
  // DR-029: source state descriptions label the control actions the
1858
1890
  // runtime advertises through `describe()`.
1859
1891
  const stateDescriptions = stateDescriptionsFromMachine(machine);
1860
- const hasCanonicalStatusProfile = spec.playerStates !== undefined;
1861
- const playerStates = snapshotPlayerStateStatuses(
1862
- spec.playerStates,
1892
+ const roleStatesDescriptor = specDescriptors.roleStates;
1893
+ if (
1894
+ roleStatesDescriptor !== undefined &&
1895
+ !Object.prototype.hasOwnProperty.call(roleStatesDescriptor, 'value')
1896
+ ) {
1897
+ throw new TypeError(`${label} roleStates must be an own data property`);
1898
+ }
1899
+ const roleStates = snapshotRoleStateStatuses(
1900
+ roleStatesDescriptor?.value,
1863
1901
  label,
1864
1902
  machine,
1865
1903
  stateDescriptions,
1866
1904
  );
1905
+ const declaredRoleIds = Object.freeze([
1906
+ ...new Set([...roleStates.values()].map(({ role }) => role)),
1907
+ ]);
1867
1908
  // PBRT-52: the artifact's own ControlView context projection. Nothing is
1868
1909
  // exported by default, so an FSM context member — including one added
1869
1910
  // after this artifact was linked — is private until named here. The two
@@ -1880,7 +1921,6 @@ export function createXStatePlaybookRuntime<TOptions>(
1880
1921
  );
1881
1922
  }
1882
1923
  }
1883
- const resolvePlayerIdSpec = spec.resolvePlayerId;
1884
1924
  const composePlayerPrompt =
1885
1925
  spec.composePlayerPrompt ??
1886
1926
  ((input: PlaybookPlayerInput) =>
@@ -1919,14 +1959,10 @@ export function createXStatePlaybookRuntime<TOptions>(
1919
1959
  makeDefaultNormalizeTransitionEvent(spec.transitionEventFields ?? []);
1920
1960
  const statusesForState =
1921
1961
  spec.statusesForState ??
1922
- (hasCanonicalStatusProfile
1923
- ? makeDefaultStatusesForState(playerStates)
1924
- : legacyStatusesForState);
1962
+ makeDefaultStatusesForState(roleStates);
1925
1963
  const classificationStatus =
1926
1964
  spec.classificationStatus ??
1927
- (hasCanonicalStatusProfile
1928
- ? (event: EventObject) => event.type
1929
- : () => undefined);
1965
+ ((event: EventObject) => event.type);
1930
1966
  const machineInput =
1931
1967
  spec.machineInput ?? ((options: TOptions) => options as unknown);
1932
1968
  const scriptCwd =
@@ -1977,8 +2013,8 @@ export function createXStatePlaybookRuntime<TOptions>(
1977
2013
  // acceptance records nothing, so a later call with that key may still
1978
2014
  // execute.
1979
2015
  const appliedReceipts = new Map<string, PlaybookControlReceipt>();
1980
- const playerResumeTokens = new Map<string, string>();
1981
- const activePlayerIds = new Set<string>();
2016
+ const privateResumeTokens = new Map<string, string>();
2017
+ const activePlayerKeys = new Set<string>();
1982
2018
  const playbookCallTurnIds = new Map<string, number | undefined>();
1983
2019
  // Captain and judge work share one serialized lane (slc/link.md
1984
2020
  // §Session lifecycle).
@@ -1991,69 +2027,191 @@ export function createXStatePlaybookRuntime<TOptions>(
1991
2027
  // imperative boundaries await their queued work directly.
1992
2028
  let emissionFailure: unknown;
1993
2029
 
1994
- function selectPlayerResume(playerId: string): string | false {
2030
+ function bindSession(nextSession: PlaybookSession): PlaybookSession {
2031
+ const bound = snapshotPlaybookSession(nextSession);
2032
+ if (bound.roleBindings === undefined) return bound;
2033
+ const actual = Object.keys(bound.roleBindings).sort();
2034
+ const expected = [...declaredRoleIds].sort();
2035
+ const missing = expected.filter((roleId) => !actual.includes(roleId));
2036
+ const extra = actual.filter((roleId) => !expected.includes(roleId));
2037
+ if (missing.length > 0 || extra.length > 0) {
2038
+ throw new TypeError(
2039
+ `${label} session roleBindings must cover exactly [${expected.join(', ')}]` +
2040
+ `${missing.length === 0 ? '' : `; missing [${missing.join(', ')}]`}` +
2041
+ `${extra.length === 0 ? '' : `; extra [${extra.join(', ')}]`}`,
2042
+ );
2043
+ }
2044
+ return bound;
2045
+ }
2046
+
2047
+ function requireRoleId(input: PlaybookPlayerInput): string {
2048
+ const roleId = input.role;
2049
+ if (
2050
+ typeof roleId !== 'string' ||
2051
+ roleId.trim().length === 0 ||
2052
+ !declaredRoleIds.includes(roleId)
2053
+ ) {
2054
+ throw new TypeError(
2055
+ `${label} player input role must name a declared local role`,
2056
+ );
2057
+ }
2058
+ return roleId;
2059
+ }
2060
+
2061
+ function resolvedPlayerId(roleId: string): string | undefined {
2062
+ return session?.roleBindings?.[roleId]?.playerId;
2063
+ }
2064
+
2065
+ function promptIdentity(roleId: string): string {
2066
+ if (!declaredRoleIds.includes(roleId)) {
2067
+ throw new TypeError(
2068
+ `${label} prompt identity lookup rejected undeclared role ${roleId}`,
2069
+ );
2070
+ }
2071
+ return session?.roleBindings?.[roleId]?.promptIdentity ?? roleId;
2072
+ }
2073
+
2074
+ function composeBoundPlayerPrompt(input: PlaybookPlayerInput): string {
2075
+ let active = true;
2076
+ const lookup: XStatePromptIdentity = (roleId) => {
2077
+ if (!active) {
2078
+ throw new Error(
2079
+ `${label} prompt identity lookup is no longer active`,
2080
+ );
2081
+ }
2082
+ return promptIdentity(roleId);
2083
+ };
2084
+ try {
2085
+ return composePlayerPrompt(input, lookup);
2086
+ } finally {
2087
+ active = false;
2088
+ }
2089
+ }
2090
+
2091
+ function continuationKey(
2092
+ roleId: string,
2093
+ playerId: string | undefined,
2094
+ ): string {
2095
+ return playerId ?? roleId;
2096
+ }
2097
+
2098
+ function roleTokensByContinuationKey(
2099
+ tokens: Readonly<Record<string, string>>,
2100
+ ): Map<string, string> {
2101
+ const byKey = new Map<string, string>();
2102
+ for (const [roleId, token] of Object.entries(tokens)) {
2103
+ if (!declaredRoleIds.includes(roleId)) {
2104
+ throw new TypeError(
2105
+ `runtime role tokens contain unknown role ${roleId}`,
2106
+ );
2107
+ }
2108
+ const key = continuationKey(roleId, resolvedPlayerId(roleId));
2109
+ const existing = byKey.get(key);
2110
+ if (existing !== undefined && existing !== token) {
2111
+ throw new TypeError(
2112
+ `runtime snapshot assigns conflicting tokens to roles bound to player ${key}`,
2113
+ );
2114
+ }
2115
+ byKey.set(key, token);
2116
+ }
2117
+ const rolesByKey = new Map<string, string[]>();
2118
+ for (const roleId of declaredRoleIds) {
2119
+ const key = continuationKey(roleId, resolvedPlayerId(roleId));
2120
+ rolesByKey.set(key, [...(rolesByKey.get(key) ?? []), roleId]);
2121
+ }
2122
+ for (const [key, roles] of rolesByKey) {
2123
+ if (roles.length < 2) continue;
2124
+ const present = roles.filter((roleId) => tokens[roleId] !== undefined);
2125
+ if (present.length !== 0 && present.length !== roles.length) {
2126
+ throw new TypeError(
2127
+ `runtime role tokens must project player ${key} through every aliased role [${roles.join(', ')}]`,
2128
+ );
2129
+ }
2130
+ }
2131
+ return byKey;
2132
+ }
2133
+
2134
+ function selectPlayerResume(
2135
+ roleId: string,
2136
+ playerId: string | undefined,
2137
+ ): string | false {
2138
+ const key = continuationKey(roleId, playerId);
1995
2139
  const selected = session?.playerSessions
1996
- ? session.playerSessions.select(playerId)
1997
- : playerResumeTokens.get(playerId) ?? false;
2140
+ ? session.playerSessions.select(roleId)
2141
+ : privateResumeTokens.get(key) ?? false;
1998
2142
  if (
1999
2143
  selected !== false &&
2000
2144
  (typeof selected !== 'string' || selected.trim().length === 0)
2001
2145
  ) {
2002
2146
  throw new TypeError(
2003
- `player session store returned an invalid resume token for ${playerId}`,
2147
+ `player session store returned an invalid resume token for role ${roleId}`,
2004
2148
  );
2005
2149
  }
2006
2150
  return selected;
2007
2151
  }
2008
2152
 
2009
2153
  function updatePlayerResume(
2010
- playerId: string,
2011
- resumeToken: string | undefined,
2154
+ roleId: string,
2155
+ playerId: string | undefined,
2156
+ result: PlayerResult,
2012
2157
  ): void {
2158
+ const resumeToken = result.resumeToken;
2159
+ if (resumeToken === undefined && result.status !== 'ok') return;
2160
+ const key = continuationKey(roleId, playerId);
2013
2161
  if (session?.playerSessions) {
2014
- session.playerSessions.update(playerId, resumeToken);
2015
- } else if (resumeToken !== undefined && resumeToken.trim().length > 0) {
2016
- playerResumeTokens.set(playerId, resumeToken);
2162
+ session.playerSessions.update(roleId, resumeToken);
2163
+ } else if (resumeToken !== undefined) {
2164
+ privateResumeTokens.set(key, resumeToken);
2017
2165
  } else {
2018
- playerResumeTokens.delete(playerId);
2166
+ privateResumeTokens.delete(key);
2019
2167
  }
2020
2168
  }
2021
2169
 
2022
- function snapshotPlayerResumeTokens(): Record<string, string> {
2023
- const raw = session?.playerSessions
2024
- ? session.playerSessions.snapshot()
2025
- : Object.fromEntries(playerResumeTokens);
2170
+ function snapshotRoleResumeTokens(): Record<string, string> {
2171
+ const raw = snapshotJsonValue(
2172
+ session?.playerSessions
2173
+ ? session.playerSessions.snapshot()
2174
+ : Object.fromEntries(
2175
+ declaredRoleIds.flatMap((roleId) => {
2176
+ const token = privateResumeTokens.get(
2177
+ continuationKey(roleId, resolvedPlayerId(roleId)),
2178
+ );
2179
+ return token === undefined ? [] : [[roleId, token]];
2180
+ }),
2181
+ ),
2182
+ 'player session store snapshot',
2183
+ );
2026
2184
  if (!isPlainObject(raw)) {
2027
2185
  throw new TypeError('player session store snapshot must be an object');
2028
2186
  }
2029
2187
  const detached: Record<string, string> = {};
2030
- for (const [playerId, token] of Object.entries(raw)) {
2031
- if (playerId.trim().length === 0) {
2188
+ for (const [roleId, token] of Object.entries(raw)) {
2189
+ if (!declaredRoleIds.includes(roleId)) {
2032
2190
  throw new TypeError(
2033
- 'player session store snapshot player ids must be non-empty',
2191
+ `player session store snapshot contains unknown role ${roleId}`,
2034
2192
  );
2035
2193
  }
2036
2194
  if (typeof token !== 'string' || token.trim().length === 0) {
2037
2195
  throw new TypeError(
2038
- `player session store snapshot token for ${playerId} must be a non-empty string`,
2196
+ `player session store snapshot token for ${roleId} must be a non-empty string`,
2039
2197
  );
2040
2198
  }
2041
- detached[playerId] = token;
2199
+ detached[roleId] = token;
2042
2200
  }
2201
+ roleTokensByContinuationKey(detached);
2043
2202
  return detached;
2044
2203
  }
2045
2204
 
2046
- function restorePlayerResumeTokens(
2205
+ function restoreRoleResumeTokens(
2047
2206
  tokens: Readonly<Record<string, string>>,
2048
2207
  ): void {
2208
+ const byKey = roleTokensByContinuationKey(tokens);
2049
2209
  if (session?.playerSessions) {
2050
2210
  session.playerSessions.restore(tokens);
2051
2211
  return;
2052
2212
  }
2053
- playerResumeTokens.clear();
2054
- for (const [playerId, token] of Object.entries(tokens)) {
2055
- playerResumeTokens.set(playerId, token);
2056
- }
2213
+ privateResumeTokens.clear();
2214
+ for (const [key, token] of byKey) privateResumeTokens.set(key, token);
2057
2215
  }
2058
2216
 
2059
2217
  function enqueueEmission(fn: () => Promise<void>): Promise<void> {
@@ -2111,7 +2269,7 @@ export function createXStatePlaybookRuntime<TOptions>(
2111
2269
  const currentSession = requireSession();
2112
2270
  const safePayload = snapshotJsonValue(payload, `trace ${type} payload`);
2113
2271
  return {
2114
- schemaVersion: 2,
2272
+ schemaVersion: 3,
2115
2273
  sessionId: currentSession.sessionId,
2116
2274
  playbookId: currentSession.playbookId,
2117
2275
  rootSessionId: currentSession.rootSessionId,
@@ -2260,25 +2418,31 @@ export function createXStatePlaybookRuntime<TOptions>(
2260
2418
  }
2261
2419
 
2262
2420
  const boundary: RuntimeBoundaryCalls = {
2263
- async callPlayer(input, playerId, prompt, signal): Promise<PlayerResult> {
2421
+ async callPlayer(
2422
+ input,
2423
+ roleId,
2424
+ prompt,
2425
+ signal,
2426
+ ): Promise<PlayerResult> {
2264
2427
  // State-entry telemetry/status must precede the call they describe.
2265
2428
  await drainEmissions();
2266
2429
  const turnId = activeTurnId;
2267
2430
  const stateId = input.stateId;
2431
+ const playerId = resolvedPlayerId(roleId);
2268
2432
  let resume: string | false;
2269
2433
  try {
2270
2434
  signal.throwIfAborted();
2271
- resume = selectPlayerResume(playerId);
2435
+ resume = selectPlayerResume(roleId, playerId);
2272
2436
  } catch (error) {
2273
2437
  if (!signal.aborted) controlPlaneError ??= error;
2274
2438
  throw error;
2275
2439
  }
2276
2440
  const callId = `player-${++playerCallSequence}`;
2277
2441
  const identity = {
2278
- purpose: 'captain' as const,
2279
2442
  ...stateIdentity(stateId),
2280
2443
  sourceItem: input.sourceItem,
2281
- playerId,
2444
+ roleId,
2445
+ ...(playerId === undefined ? {} : { playerId }),
2282
2446
  resume,
2283
2447
  };
2284
2448
  const position: TracePosition = {
@@ -2286,9 +2450,10 @@ export function createXStatePlaybookRuntime<TOptions>(
2286
2450
  callId,
2287
2451
  };
2288
2452
 
2289
- if (activePlayerIds.has(playerId)) {
2453
+ const playerKey = continuationKey(roleId, playerId);
2454
+ if (activePlayerKeys.has(playerKey)) {
2290
2455
  const error = new Error(
2291
- `simultaneous calls to resolved player ${playerId} are not allowed`,
2456
+ `simultaneous calls to player key ${playerKey} are not allowed`,
2292
2457
  );
2293
2458
  await emitCallStarted(
2294
2459
  'player.call.started',
@@ -2303,7 +2468,7 @@ export function createXStatePlaybookRuntime<TOptions>(
2303
2468
  );
2304
2469
  throw error;
2305
2470
  }
2306
- activePlayerIds.add(playerId);
2471
+ activePlayerKeys.add(playerKey);
2307
2472
 
2308
2473
  try {
2309
2474
  await emitTrace(
@@ -2320,7 +2485,7 @@ export function createXStatePlaybookRuntime<TOptions>(
2320
2485
  // as `aborted` through the catch below.
2321
2486
  signal.throwIfAborted();
2322
2487
  rawResult = await requireHostPorts().callPlayer(
2323
- playerId,
2488
+ roleId,
2324
2489
  prompt,
2325
2490
  signal,
2326
2491
  { resume },
@@ -2366,13 +2531,7 @@ export function createXStatePlaybookRuntime<TOptions>(
2366
2531
  }
2367
2532
 
2368
2533
  try {
2369
- updatePlayerResume(
2370
- playerId,
2371
- typeof result.resumeToken === 'string' &&
2372
- result.resumeToken.trim().length > 0
2373
- ? result.resumeToken
2374
- : undefined,
2375
- );
2534
+ updatePlayerResume(roleId, playerId, result);
2376
2535
  } catch (error) {
2377
2536
  if (!signal.aborted) controlPlaneError ??= error;
2378
2537
  try {
@@ -2406,7 +2565,7 @@ export function createXStatePlaybookRuntime<TOptions>(
2406
2565
  );
2407
2566
  return result;
2408
2567
  } finally {
2409
- activePlayerIds.delete(playerId);
2568
+ activePlayerKeys.delete(playerKey);
2410
2569
  }
2411
2570
  },
2412
2571
 
@@ -2611,19 +2770,13 @@ export function createXStatePlaybookRuntime<TOptions>(
2611
2770
  },
2612
2771
  };
2613
2772
 
2614
- function resolvePlayerId(input: PlaybookPlayerInput): string {
2615
- return resolvePlayerIdSpec
2616
- ? resolvePlayerIdSpec(input, boundOptions)
2617
- : defaultResolvePlayerId(input);
2618
- }
2619
-
2620
2773
  function playerActor(
2621
2774
  ports: PlaybookPorts,
2622
2775
  ): PromiseActorLogic<PlaybookActorOutput, PlaybookPlayerInput> {
2623
2776
  return createPlayerBridge(
2624
2777
  {
2625
- resolvePlayerId,
2626
- composePlayerPrompt,
2778
+ resolveRoleId: requireRoleId,
2779
+ composePlayerPrompt: composeBoundPlayerPrompt,
2627
2780
  adjudication,
2628
2781
  resumableStateIds,
2629
2782
  },
@@ -3123,8 +3276,8 @@ export function createXStatePlaybookRuntime<TOptions>(
3123
3276
  // The session-start error remains authoritative.
3124
3277
  }
3125
3278
  }
3126
- playerResumeTokens.clear();
3127
- activePlayerIds.clear();
3279
+ privateResumeTokens.clear();
3280
+ activePlayerKeys.clear();
3128
3281
  playbookCallTurnIds.clear();
3129
3282
  activeEmissionCalls.clear();
3130
3283
  emissionQueue.clear();
@@ -3332,7 +3485,7 @@ export function createXStatePlaybookRuntime<TOptions>(
3332
3485
  if (initialized || disposed || disposalPromise !== undefined) {
3333
3486
  throw new Error('createPlaybookRuntime.init: already initialized');
3334
3487
  }
3335
- const boundSession = snapshotPlaybookSession(nextSession);
3488
+ const boundSession = bindSession(nextSession);
3336
3489
  initialized = true;
3337
3490
  let finishInitialization!: () => void;
3338
3491
  const initialization = new Promise<void>((resolve) => {
@@ -3408,10 +3561,10 @@ export function createXStatePlaybookRuntime<TOptions>(
3408
3561
  .context as Record<string, unknown>;
3409
3562
  const pending = pendingBossQuestionFromContext(context ?? {});
3410
3563
  return {
3411
- schemaVersion: 2,
3564
+ schemaVersion: 3,
3412
3565
  playbookId: session.playbookId,
3413
3566
  machine: machineSnapshot,
3414
- playerResumeTokens: snapshotPlayerResumeTokens(),
3567
+ roleResumeTokens: snapshotRoleResumeTokens(),
3415
3568
  sequences: {
3416
3569
  trace: traceSequence,
3417
3570
  turn: turnSequence,
@@ -3429,7 +3582,7 @@ export function createXStatePlaybookRuntime<TOptions>(
3429
3582
  : [
3430
3583
  {
3431
3584
  questionId: pending.questionId,
3432
- player: pending.player,
3585
+ asker: pending.asker,
3433
3586
  question: pending.question,
3434
3587
  sourceItem: pending.sourceItem,
3435
3588
  },
@@ -3450,16 +3603,21 @@ export function createXStatePlaybookRuntime<TOptions>(
3450
3603
  if (initialized || disposed || disposalPromise !== undefined) {
3451
3604
  throw new Error('createPlaybookRuntime.restore: already initialized');
3452
3605
  }
3453
- const boundSession = snapshotPlaybookSession(nextSession);
3606
+ const boundSession = bindSession(nextSession);
3454
3607
  const boundSnapshot = assertPlaybookRuntimeSnapshot(
3455
3608
  snapshot,
3456
3609
  boundSession.playbookId,
3457
3610
  { allowSuspendedCall: true },
3458
3611
  );
3459
- const suspendedCall =
3460
- boundSnapshot.schemaVersion === 2
3461
- ? boundSnapshot.suspendedCall
3462
- : undefined;
3612
+ if (
3613
+ declaredActors.has('captain') &&
3614
+ boundSnapshot.sequences.captainCall === undefined
3615
+ ) {
3616
+ throw new TypeError(
3617
+ 'runtime snapshot sequences.captainCall is required for a direct-Captain artifact',
3618
+ );
3619
+ }
3620
+ const suspendedCall = boundSnapshot.suspendedCall;
3463
3621
  let priorExternalPlayerTokens:
3464
3622
  | Readonly<Record<string, string>>
3465
3623
  | undefined;
@@ -3479,22 +3637,17 @@ export function createXStatePlaybookRuntime<TOptions>(
3479
3637
  judgeCallSequence = boundSnapshot.sequences.judgeCall;
3480
3638
  playerCallSequence = boundSnapshot.sequences.playerCall;
3481
3639
  playbookCallSequence = boundSnapshot.sequences.playbookCall;
3482
- captainCallSequence =
3483
- boundSnapshot.sequences.captainCall ??
3484
- // Legacy schema-v1 snapshots predate this dedicated counter.
3485
- // Every Captain call already consumed at least one trace number,
3486
- // so the global trace counter is a collision-safe id floor.
3487
- boundSnapshot.sequences.trace;
3640
+ captainCallSequence = boundSnapshot.sequences.captainCall ?? 0;
3488
3641
  // The runtime snapshot carries no apply counter (PBRT-50); every
3489
3642
  // apply boundary consumed trace numbers, so the persisted trace
3490
3643
  // counter is a collision-safe id floor here too, keeping
3491
3644
  // `apply-<n>` call ids unique across restore.
3492
3645
  applyCallSequence = boundSnapshot.sequences.trace;
3493
3646
  if (boundSession.playerSessions) {
3494
- priorExternalPlayerTokens = snapshotPlayerResumeTokens();
3647
+ priorExternalPlayerTokens = snapshotRoleResumeTokens();
3495
3648
  externalStoreRestoreAttempted = true;
3496
3649
  }
3497
- restorePlayerResumeTokens(boundSnapshot.playerResumeTokens);
3650
+ restoreRoleResumeTokens(boundSnapshot.roleResumeTokens);
3498
3651
  nestedBridge.prepareRestore(suspendedCall);
3499
3652
  if (suspendedCall !== undefined) {
3500
3653
  playbookCallTurnIds.set(
@@ -3604,7 +3757,7 @@ export function createXStatePlaybookRuntime<TOptions>(
3604
3757
  : [
3605
3758
  {
3606
3759
  questionId: pending.questionId,
3607
- player: pending.player,
3760
+ asker: pending.asker,
3608
3761
  question: pending.question,
3609
3762
  sourceItem: pending.sourceItem,
3610
3763
  },
@@ -4176,9 +4329,9 @@ export function createXStatePlaybookRuntime<TOptions>(
4176
4329
  // engagement tree. Child disposal must not erase a token its
4177
4330
  // caller will resume. The private fallback remains runtime-owned.
4178
4331
  if (session?.playerSessions === undefined) {
4179
- playerResumeTokens.clear();
4332
+ privateResumeTokens.clear();
4180
4333
  }
4181
- activePlayerIds.clear();
4334
+ activePlayerKeys.clear();
4182
4335
  playbookCallTurnIds.clear();
4183
4336
  activeEmissionCalls.clear();
4184
4337
  emissionQueue.clear();