@sublang/playbook 12.3.0 → 13.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.
@@ -35,6 +35,11 @@ interface PlaybookCaptainUnresolvedEffectSettlementInput {
35
35
  type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, 'model' | 'effort' | 'fastMode'>>;
36
36
  type PlayerLedgerSnapshotEntry = DeepReadonly<PlayerLedgerEntry>;
37
37
  export interface PlaybookCaptainDeps {
38
+ continuity?: {
39
+ beforeCall(participantId: string): Promise<void>;
40
+ acknowledged(participantId: string, token: string): void;
41
+ reset(participantId: string, reason: 'missing_hint' | 'rejected_hint'): Promise<void>;
42
+ };
38
43
  loadModule?: (specifier: string) => Promise<unknown>;
39
44
  createSessionId?: () => string;
40
45
  hostCapabilities?: Readonly<Record<string, PlaybookHostConstructionCapabilities>>;
@@ -949,6 +949,7 @@ function normalizeHostPlayerResult(value, expectedPlayerId) {
949
949
  'resumeToken',
950
950
  'finalText',
951
951
  'error',
952
+ 'errorCode',
952
953
  ]);
953
954
  const normalized = {};
954
955
  for (const key of Reflect.ownKeys(descriptors)) {
@@ -970,11 +971,16 @@ function normalizeHostPlayerResult(value, expectedPlayerId) {
970
971
  normalized[key] = descriptor.value;
971
972
  }
972
973
  const record = snapshotRecord(snapshotJsonValue(normalized, path), path);
973
- rejectSnapshotKeys(record, ['status', 'playerId', 'turnId', 'resumeToken', 'finalText', 'error'], path);
974
+ rejectSnapshotKeys(record, ['status', 'playerId', 'turnId', 'resumeToken', 'finalText', 'error', 'errorCode'], path);
974
975
  if (record.playerId !== expectedPlayerId) {
975
976
  throw new TypeError(`${path}.playerId does not match the requested player`);
976
977
  }
977
978
  snapshotInteger(record.turnId, `${path}.turnId`, 1);
979
+ if (record.errorCode !== undefined &&
980
+ (record.errorCode !== 'SESSION_RESUME_REJECTED' ||
981
+ record.status !== 'error' || record.resumeToken !== undefined)) {
982
+ throw new TypeError(`${path}.errorCode is not a definite resume rejection`);
983
+ }
978
984
  return validatePlayerResult({
979
985
  status: record.status,
980
986
  ...(record.resumeToken === undefined
@@ -1629,6 +1635,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
1629
1635
  const createSessionId = deps.createSessionId ?? randomUUID;
1630
1636
  const createCaptainRuntime = deps.createCaptainRuntime ?? createDefaultCaptainRuntime;
1631
1637
  const unresolvedEffectSettlement = deps.unresolvedEffectSettlement;
1638
+ const continuity = deps.continuity;
1632
1639
  let pendingHostCapabilities = deps.hostCapabilities;
1633
1640
  let currentEffectLedger = () => emptyPlaybookEffectLedger();
1634
1641
  // The returned shell must not retain the caller's aggregate dependency
@@ -2615,11 +2622,32 @@ export function createPlaybookCaptainShell(options, deps = {}) {
2615
2622
  try {
2616
2623
  let rawResult;
2617
2624
  try {
2618
- rawResult = await trackHostCall(frame, classifySettingsCall(() => context.callPlayer(binding.playerId, prompt, {
2619
- resume: options.resume,
2620
- settings,
2621
- })));
2622
- hostResolved = true;
2625
+ const call = async (resume) => {
2626
+ await continuity?.beforeCall(binding.playerId);
2627
+ signal.throwIfAborted();
2628
+ const raw = await trackHostCall(frame, classifySettingsCall(() => context.callPlayer(binding.playerId, prompt, { resume, settings })));
2629
+ hostResolved = true;
2630
+ return raw;
2631
+ };
2632
+ if (options.resume === false) {
2633
+ await continuity?.reset(binding.playerId, 'missing_hint');
2634
+ }
2635
+ rawResult = await call(options.resume);
2636
+ normalizeHostPlayerResult(rawResult, binding.playerId);
2637
+ if (typeof options.resume === 'string' && options.resume.length > 0 &&
2638
+ Object.getOwnPropertyDescriptor(rawResult, 'errorCode')?.value ===
2639
+ 'SESSION_RESUME_REJECTED') {
2640
+ if (playerTransactions.get(binding.playerId) !== calling ||
2641
+ calling.abandoned || signal.aborted || activeTurn !== admittedTurn ||
2642
+ frame.playerCallScope !== scope || !frames.includes(frame)) {
2643
+ signal.throwIfAborted();
2644
+ throw new Error(`${frameLabel(frame)} player rejection arrived after its runtime operation ended`);
2645
+ }
2646
+ delete ledger.resumeToken;
2647
+ await continuity?.reset(binding.playerId, 'rejected_hint');
2648
+ hostResolved = false;
2649
+ rawResult = await call(false);
2650
+ }
2623
2651
  }
2624
2652
  catch (error) {
2625
2653
  if (error instanceof AgentSettingsPreflightError) {
@@ -2914,6 +2942,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
2914
2942
  delete ledger.resumeToken;
2915
2943
  else
2916
2944
  ledger.resumeToken = resumeToken;
2945
+ if (pending.status === 'ok' && resumeToken !== undefined) {
2946
+ continuity?.acknowledged(binding.playerId, resumeToken);
2947
+ }
2917
2948
  }
2918
2949
  finally {
2919
2950
  playerTransactions.delete(binding.playerId);
@@ -4109,7 +4140,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
4109
4140
  };
4110
4141
  class CaptainContinuityError extends Error {
4111
4142
  constructor(cause) {
4112
- super('the session Captain conversation could not be resynchronized after one reseeded re-issue', { cause });
4143
+ super('the session Captain conversation lost continuity', { cause });
4113
4144
  this.name = 'CaptainContinuityError';
4114
4145
  }
4115
4146
  }
@@ -4121,6 +4152,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
4121
4152
  }
4122
4153
  const rawDurableCall = async (context, prompt, resume, attempt) => {
4123
4154
  const queued = captainQueue.add(async () => {
4155
+ context.signal.throwIfAborted();
4156
+ await continuity?.beforeCall('captain');
4124
4157
  context.signal.throwIfAborted();
4125
4158
  attempt.providerBoundaryEntered = true;
4126
4159
  const result = await classifySettingsCall(() => context.callCaptain(prompt, {
@@ -4134,11 +4167,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
4134
4167
  });
4135
4168
  return trackTurnCall(queued);
4136
4169
  };
4137
- // CAPTAIN-35: unsynchronized when the call throws, returns non-`ok`, or
4138
- // returns `ok` without a token. Exactly one re-issue on a fresh conversation
4139
- // seeded with the reseed digest plus the current ControlView digest. A
4140
- // conversation that is owed a reseed carries the digest on its very next
4141
- // call, so the turn after a failed reseed starts seeded rather than blank.
4170
+ // Only proven pre-execution rejection permits an immediate fresh call.
4171
+ // Other continuity loss leaves the journal reseed for the next Boss turn.
4142
4172
  const durableCall = async (context, compose) => {
4143
4173
  const startingConversation = conversation;
4144
4174
  const resume = startingConversation.kind === 'pinned'
@@ -4153,6 +4183,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
4153
4183
  let result;
4154
4184
  let failure;
4155
4185
  try {
4186
+ if (resume === false)
4187
+ await continuity?.reset('captain', 'missing_hint');
4156
4188
  result = await rawDurableCall(context, compose(seedFirstCall
4157
4189
  ? { reseedDigest: reseedDigest() }
4158
4190
  : startingConversation.kind === 'needsCatchUp'
@@ -4187,9 +4219,10 @@ export function createPlaybookCaptainShell(options, deps = {}) {
4187
4219
  const unsynchronized = failure !== undefined ||
4188
4220
  result === undefined ||
4189
4221
  result.status !== 'ok' ||
4190
- result.resumeToken === undefined;
4222
+ typeof result.resumeToken !== 'string' || result.resumeToken.trim().length === 0;
4191
4223
  if (!unsynchronized) {
4192
4224
  conversation = { kind: 'pinned', token: result.resumeToken };
4225
+ continuity?.acknowledged('captain', result.resumeToken);
4193
4226
  if (activeTurn) {
4194
4227
  activeTurn.captainSyncedJournalSeq = representedJournalSeq;
4195
4228
  }
@@ -4205,6 +4238,11 @@ export function createPlaybookCaptainShell(options, deps = {}) {
4205
4238
  // stays `needsSeeding` until a call comes back with a token, so a reseed
4206
4239
  // that itself fails leaves the obligation standing for the next turn.
4207
4240
  conversation = { kind: 'needsSeeding' };
4241
+ if (typeof resume !== 'string' || resume.length === 0 ||
4242
+ result?.status !== 'error' || result.errorCode !== 'SESSION_RESUME_REJECTED') {
4243
+ throw markControlFailure(new CaptainContinuityError(failure ?? result?.error ?? 'callCaptain did not establish continuation'));
4244
+ }
4245
+ await continuity?.reset('captain', 'rejected_hint');
4208
4246
  const recap = reseedDigest();
4209
4247
  let reissued;
4210
4248
  const reissueAttempt = { providerBoundaryEntered: false };
@@ -4221,11 +4259,12 @@ export function createPlaybookCaptainShell(options, deps = {}) {
4221
4259
  }
4222
4260
  throw markControlFailure(new CaptainContinuityError(error));
4223
4261
  }
4224
- if (reissued.status !== 'ok' || reissued.resumeToken === undefined) {
4262
+ if (reissued.status !== 'ok' || typeof reissued.resumeToken !== 'string' || reissued.resumeToken.trim().length === 0) {
4225
4263
  throw markControlFailure(new CaptainContinuityError(reissued.error ??
4226
4264
  `callCaptain status "${reissued.status}" without a resume token`));
4227
4265
  }
4228
4266
  conversation = { kind: 'pinned', token: reissued.resumeToken };
4267
+ continuity?.acknowledged('captain', reissued.resumeToken);
4229
4268
  if (activeTurn)
4230
4269
  activeTurn.captainSyncedJournalSeq = journalSeq;
4231
4270
  return {
@@ -103,6 +103,11 @@ type SnapshotAgentEnvelope = DeepReadonly<
103
103
  type PlayerLedgerSnapshotEntry = DeepReadonly<PlayerLedgerEntry>;
104
104
 
105
105
  export interface PlaybookCaptainDeps {
106
+ continuity?: {
107
+ beforeCall(participantId: string): Promise<void>;
108
+ acknowledged(participantId: string, token: string): void;
109
+ reset(participantId: string, reason: 'missing_hint' | 'rejected_hint'): Promise<void>;
110
+ };
106
111
  loadModule?: (specifier: string) => Promise<unknown>;
107
112
  createSessionId?: () => string;
108
113
  hostCapabilities?: Readonly<
@@ -1819,6 +1824,7 @@ function normalizeHostPlayerResult(
1819
1824
  'resumeToken',
1820
1825
  'finalText',
1821
1826
  'error',
1827
+ 'errorCode',
1822
1828
  ]);
1823
1829
  const normalized: Record<string, unknown> = {};
1824
1830
  for (const key of Reflect.ownKeys(descriptors)) {
@@ -1841,13 +1847,18 @@ function normalizeHostPlayerResult(
1841
1847
  const record = snapshotRecord(snapshotJsonValue(normalized, path), path);
1842
1848
  rejectSnapshotKeys(
1843
1849
  record,
1844
- ['status', 'playerId', 'turnId', 'resumeToken', 'finalText', 'error'],
1850
+ ['status', 'playerId', 'turnId', 'resumeToken', 'finalText', 'error', 'errorCode'],
1845
1851
  path,
1846
1852
  );
1847
1853
  if (record.playerId !== expectedPlayerId) {
1848
1854
  throw new TypeError(`${path}.playerId does not match the requested player`);
1849
1855
  }
1850
1856
  snapshotInteger(record.turnId, `${path}.turnId`, 1);
1857
+ if (record.errorCode !== undefined &&
1858
+ (record.errorCode !== 'SESSION_RESUME_REJECTED' ||
1859
+ record.status !== 'error' || record.resumeToken !== undefined)) {
1860
+ throw new TypeError(`${path}.errorCode is not a definite resume rejection`);
1861
+ }
1851
1862
  return validatePlayerResult(
1852
1863
  {
1853
1864
  status: record.status,
@@ -2988,6 +2999,7 @@ export function createPlaybookCaptainShell(
2988
2999
  PlaybookCaptainDeps['createCaptainRuntime']
2989
3000
  > = deps.createCaptainRuntime ?? createDefaultCaptainRuntime;
2990
3001
  const unresolvedEffectSettlement = deps.unresolvedEffectSettlement;
3002
+ const continuity = deps.continuity;
2991
3003
  let pendingHostCapabilities = deps.hostCapabilities;
2992
3004
  let currentEffectLedger = () => emptyPlaybookEffectLedger();
2993
3005
  // The returned shell must not retain the caller's aggregate dependency
@@ -4376,16 +4388,37 @@ export function createPlaybookCaptainShell(
4376
4388
  try {
4377
4389
  let rawResult: unknown;
4378
4390
  try {
4379
- rawResult = await trackHostCall(
4380
- frame,
4381
- classifySettingsCall(() =>
4382
- context.callPlayer(binding.playerId, prompt, {
4383
- resume: options.resume,
4384
- settings,
4385
- }),
4386
- ),
4387
- );
4388
- hostResolved = true;
4391
+ const call = async (resume: string | false): Promise<unknown> => {
4392
+ await continuity?.beforeCall(binding.playerId);
4393
+ signal.throwIfAborted();
4394
+ const raw = await trackHostCall(
4395
+ frame,
4396
+ classifySettingsCall(() =>
4397
+ context.callPlayer(binding.playerId, prompt, { resume, settings }),
4398
+ ),
4399
+ );
4400
+ hostResolved = true;
4401
+ return raw;
4402
+ };
4403
+ if (options.resume === false) {
4404
+ await continuity?.reset(binding.playerId, 'missing_hint');
4405
+ }
4406
+ rawResult = await call(options.resume);
4407
+ normalizeHostPlayerResult(rawResult, binding.playerId);
4408
+ if (typeof options.resume === 'string' && options.resume.length > 0 &&
4409
+ Object.getOwnPropertyDescriptor(rawResult as object, 'errorCode')?.value ===
4410
+ 'SESSION_RESUME_REJECTED') {
4411
+ if (playerTransactions.get(binding.playerId) !== calling ||
4412
+ calling.abandoned || signal.aborted || activeTurn !== admittedTurn ||
4413
+ frame.playerCallScope !== scope || !frames.includes(frame)) {
4414
+ signal.throwIfAborted();
4415
+ throw new Error(`${frameLabel(frame)} player rejection arrived after its runtime operation ended`);
4416
+ }
4417
+ delete ledger.resumeToken;
4418
+ await continuity?.reset(binding.playerId, 'rejected_hint');
4419
+ hostResolved = false;
4420
+ rawResult = await call(false);
4421
+ }
4389
4422
  } catch (error) {
4390
4423
  if (error instanceof AgentSettingsPreflightError) {
4391
4424
  if (
@@ -4739,6 +4772,9 @@ export function createPlaybookCaptainShell(
4739
4772
  try {
4740
4773
  if (resumeToken === undefined) delete ledger.resumeToken;
4741
4774
  else ledger.resumeToken = resumeToken;
4775
+ if (pending.status === 'ok' && resumeToken !== undefined) {
4776
+ continuity?.acknowledged(binding.playerId, resumeToken);
4777
+ }
4742
4778
  } finally {
4743
4779
  playerTransactions.delete(binding.playerId);
4744
4780
  }
@@ -6161,7 +6197,7 @@ export function createPlaybookCaptainShell(
6161
6197
  class CaptainContinuityError extends Error {
6162
6198
  constructor(cause: unknown) {
6163
6199
  super(
6164
- 'the session Captain conversation could not be resynchronized after one reseeded re-issue',
6200
+ 'the session Captain conversation lost continuity',
6165
6201
  { cause },
6166
6202
  );
6167
6203
  this.name = 'CaptainContinuityError';
@@ -6185,8 +6221,11 @@ export function createPlaybookCaptainShell(
6185
6221
  finalText?: string;
6186
6222
  resumeToken?: string;
6187
6223
  error?: string;
6224
+ errorCode?: 'SESSION_RESUME_REJECTED';
6188
6225
  }> => {
6189
6226
  const queued = captainQueue.add(async () => {
6227
+ context.signal.throwIfAborted();
6228
+ await continuity?.beforeCall('captain');
6190
6229
  context.signal.throwIfAborted();
6191
6230
  attempt.providerBoundaryEntered = true;
6192
6231
  const result = await classifySettingsCall(() =>
@@ -6205,14 +6244,12 @@ export function createPlaybookCaptainShell(
6205
6244
  finalText?: string;
6206
6245
  resumeToken?: string;
6207
6246
  error?: string;
6247
+ errorCode?: 'SESSION_RESUME_REJECTED';
6208
6248
  }>;
6209
6249
  };
6210
6250
 
6211
- // CAPTAIN-35: unsynchronized when the call throws, returns non-`ok`, or
6212
- // returns `ok` without a token. Exactly one re-issue on a fresh conversation
6213
- // seeded with the reseed digest plus the current ControlView digest. A
6214
- // conversation that is owed a reseed carries the digest on its very next
6215
- // call, so the turn after a failed reseed starts seeded rather than blank.
6251
+ // Only proven pre-execution rejection permits an immediate fresh call.
6252
+ // Other continuity loss leaves the journal reseed for the next Boss turn.
6216
6253
  const durableCall = async (
6217
6254
  context: CaptainContext,
6218
6255
  compose: (options: { reseedDigest?: string }) => string,
@@ -6229,10 +6266,11 @@ export function createPlaybookCaptainShell(
6229
6266
  const representedJournalSeq = journalSeq;
6230
6267
  const firstAttempt = { providerBoundaryEntered: false };
6231
6268
  let result:
6232
- | { status: string; finalText?: string; resumeToken?: string; error?: string }
6269
+ | { status: string; finalText?: string; resumeToken?: string; error?: string; errorCode?: 'SESSION_RESUME_REJECTED' }
6233
6270
  | undefined;
6234
6271
  let failure: unknown;
6235
6272
  try {
6273
+ if (resume === false) await continuity?.reset('captain', 'missing_hint');
6236
6274
  result = await rawDurableCall(
6237
6275
  context,
6238
6276
  compose(
@@ -6279,9 +6317,10 @@ export function createPlaybookCaptainShell(
6279
6317
  failure !== undefined ||
6280
6318
  result === undefined ||
6281
6319
  result.status !== 'ok' ||
6282
- result.resumeToken === undefined;
6320
+ typeof result.resumeToken !== 'string' || result.resumeToken.trim().length === 0;
6283
6321
  if (!unsynchronized) {
6284
6322
  conversation = { kind: 'pinned', token: result!.resumeToken! };
6323
+ continuity?.acknowledged('captain', result!.resumeToken!);
6285
6324
  if (activeTurn) {
6286
6325
  activeTurn.captainSyncedJournalSeq = representedJournalSeq;
6287
6326
  }
@@ -6297,9 +6336,16 @@ export function createPlaybookCaptainShell(
6297
6336
  // stays `needsSeeding` until a call comes back with a token, so a reseed
6298
6337
  // that itself fails leaves the obligation standing for the next turn.
6299
6338
  conversation = { kind: 'needsSeeding' };
6339
+ if (typeof resume !== 'string' || resume.length === 0 ||
6340
+ result?.status !== 'error' || result.errorCode !== 'SESSION_RESUME_REJECTED') {
6341
+ throw markControlFailure(new CaptainContinuityError(
6342
+ failure ?? result?.error ?? 'callCaptain did not establish continuation',
6343
+ ));
6344
+ }
6345
+ await continuity?.reset('captain', 'rejected_hint');
6300
6346
  const recap = reseedDigest();
6301
6347
  let reissued:
6302
- | { status: string; finalText?: string; resumeToken?: string; error?: string }
6348
+ | { status: string; finalText?: string; resumeToken?: string; error?: string; errorCode?: 'SESSION_RESUME_REJECTED' }
6303
6349
  | undefined;
6304
6350
  const reissueAttempt = { providerBoundaryEntered: false };
6305
6351
  try {
@@ -6319,7 +6365,7 @@ export function createPlaybookCaptainShell(
6319
6365
  }
6320
6366
  throw markControlFailure(new CaptainContinuityError(error));
6321
6367
  }
6322
- if (reissued.status !== 'ok' || reissued.resumeToken === undefined) {
6368
+ if (reissued.status !== 'ok' || typeof reissued.resumeToken !== 'string' || reissued.resumeToken.trim().length === 0) {
6323
6369
  throw markControlFailure(
6324
6370
  new CaptainContinuityError(
6325
6371
  reissued.error ??
@@ -6328,6 +6374,7 @@ export function createPlaybookCaptainShell(
6328
6374
  );
6329
6375
  }
6330
6376
  conversation = { kind: 'pinned', token: reissued.resumeToken };
6377
+ continuity?.acknowledged('captain', reissued.resumeToken);
6331
6378
  if (activeTurn) activeTurn.captainSyncedJournalSeq = journalSeq;
6332
6379
  return {
6333
6380
  ...(reissued.finalText !== undefined
@@ -0,0 +1,75 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
+
4
+ import type { PlaybookCaptainShell, PlaybookCaptainShellSnapshot } from './playbook-captain.js';
5
+ import type { PlaybookEffectLedger } from './host-capabilities.js';
6
+ import type { PlaybookSessionLifecycle, SessionExecutionProjection, SessionStructuralProjection, SessionFreshBoundary, SessionRecovery } from './session-store.js';
7
+
8
+ export interface SessionHost {
9
+ readonly shell: PlaybookCaptainShell;
10
+ readonly host: { runBossTurn(input: string): Promise<void>; abortActiveTurn(): void; dispose(): Promise<void>; [key: string]: any };
11
+ readonly snapshot: PlaybookCaptainShellSnapshot;
12
+ reconcileRepositoryEffects(): Promise<PlaybookEffectLedger>;
13
+ }
14
+ export interface SessionHostOptions {
15
+ readonly config: SessionExecutionProjection;
16
+ readonly sessionId?: string;
17
+ readonly cwd: string;
18
+ readonly sessionLease: PlaybookSessionLifecycle;
19
+ readonly loadModule: (specifier: string) => Promise<any>;
20
+ readonly restoreSnapshot?: PlaybookCaptainShellSnapshot;
21
+ readonly reconcileUncertainTurnReplay?: boolean;
22
+ readonly signal?: AbortSignal;
23
+ readonly [key: string]: any;
24
+ }
25
+ export declare function createCaptainSessionHost(options: SessionHostOptions): Promise<SessionHost>;
26
+ export declare function installRetainedGenerationsForLaunch(options: {
27
+ lease: PlaybookSessionLifecycle;
28
+ shell: PlaybookCaptainShell;
29
+ freshBoundary?: SessionFreshBoundary;
30
+ onFreshRecord?: (record: SessionRecovery | undefined) => void;
31
+ onLegacyRecord?: (record: any) => void | Promise<void>;
32
+ onInvalidRecord?: (record: any) => void | Promise<void>;
33
+ retainedGenerations?: Readonly<Record<string, any>>;
34
+ reconcileRepositoryEffects?: () => Promise<PlaybookEffectLedger>;
35
+ }): Promise<Readonly<Record<string, any>>>;
36
+ export declare function executionConfigFromPlan(plan: any): SessionExecutionProjection;
37
+ export declare function validateFrozenExecutionConfig(structural: SessionStructuralProjection, execution: SessionExecutionProjection, dependencies: { loadModule: (specifier: string) => Promise<any>; prepareRegistryModule?: (request: {id: string; from: string; authoredFrom: string}) => Promise<string | void> }): Promise<SessionExecutionProjection>;
38
+ export declare function driveHeadlessCaptainTurn(options: any): Promise<any>;
39
+ export declare function normalizeLaunchPlan(top: any, options?: any): Promise<any>;
40
+ export declare function loadLaunchPlan(options: any): Promise<any>;
41
+ export declare function composeGenericConfig(top: any, loadModule: (specifier: string) => Promise<any>, configPath?: string): Promise<any>;
42
+ export declare function projectTmuxConfig(plan: any): any;
43
+ export declare function resolveLaunchSessionsDir(options: any): string;
44
+
45
+ import type { SharedSessionStore, ReplayStreamStatus, ReplayStreamEntry, SessionGraph } from './session-store.js';
46
+ export interface OpenSessionHostOptions {
47
+ readonly store?: SharedSessionStore;
48
+ readonly sessionsDir?: string;
49
+ readonly sessionId?: string;
50
+ readonly mode?: 'new' | 'continue' | 'retry';
51
+ readonly cwd?: string;
52
+ readonly config?: SessionExecutionProjection;
53
+ readonly plan?: any;
54
+ readonly loadModule?: (specifier: string) => Promise<any>;
55
+ readonly prepareRegistryModule?: (request: {id: string; from: string; authoredFrom: string}) => Promise<string | void>;
56
+ readonly observers?: readonly { onRecord?(record: any): void | Promise<void> }[];
57
+ readonly onStoredRecord?: (record: ReplayStreamEntry, status: ReplayStreamStatus) => void | Promise<void>;
58
+ readonly onCheckpoint?: (record: SessionRecovery) => void | Promise<void>;
59
+ readonly onIncomplete?: () => void | Promise<void>;
60
+ readonly graphs?: readonly { playbookId: string; graph: SessionGraph | null }[];
61
+ readonly initialVisible?: readonly string[];
62
+ readonly [key: string]: any;
63
+ }
64
+ export interface SessionHostController {
65
+ readonly sessionId: string;
66
+ readonly host: SessionHost['host'];
67
+ readonly shell: PlaybookCaptainShell;
68
+ readonly lease: PlaybookSessionLifecycle;
69
+ read(): Promise<SessionRecovery | undefined>;
70
+ handleBossTurn(input: string): Promise<SessionRecovery>;
71
+ retry(): Promise<SessionRecovery>;
72
+ dispose(): Promise<void>;
73
+ }
74
+ export declare function openSessionHost(options: OpenSessionHostOptions): Promise<SessionHostController>;
75
+ export declare function discardSessionUncertain(store: SharedSessionStore, sessionId: string): Promise<SessionRecovery | undefined>;
@@ -0,0 +1,18 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
+
4
+ export {
5
+ createCaptainSessionHost,
6
+ installRetainedGenerationsForLaunch,
7
+ executionConfigFromPlan,
8
+ validateFrozenExecutionConfig,
9
+ driveHeadlessCaptainTurn,
10
+ } from './bin/run.js';
11
+ export {
12
+ normalizeLaunchPlan,
13
+ loadLaunchPlan,
14
+ composeGenericConfig,
15
+ projectTmuxConfig,
16
+ resolveLaunchSessionsDir,
17
+ } from './bin/launch-config.js';
18
+ export { openSessionHost, discardSessionUncertain } from './bin/session-host.js';
@@ -48,7 +48,7 @@ export type ReplayStreamStatus =
48
48
  export interface PlaybookSessionSummary {
49
49
  readonly schemaVersion: number;
50
50
  readonly sessionId: string;
51
- readonly state: 'settled' | 'uncertain';
51
+ readonly state: 'settled' | 'uncertain' | 'history-only';
52
52
  readonly cwd: string;
53
53
  readonly updatedAt: string;
54
54
  }
@@ -80,3 +80,178 @@ export interface PlaybookSessionLease {
80
80
  streamStatus(): ReplayStreamStatus;
81
81
  release(): Promise<ReplayStreamStatus>;
82
82
  }
83
+
84
+ /** Recovery data is interpreted by the version-aware codec, not by live runtimes. */
85
+ export type SessionSnapshot = Readonly<Record<string, any>>;
86
+ export interface SessionEffectLedger {
87
+ readonly schemaVersion: 1;
88
+ readonly revision: number;
89
+ readonly boundaries: readonly Readonly<Record<string, any>>[];
90
+ readonly logicalOperations: readonly Readonly<Record<string, any>>[];
91
+ }
92
+ export interface SessionUnresolvedEffect {
93
+ readonly classification: 'one-descendant-commit' | 'multiple-commits' |
94
+ 'rewritten-or-non-descendant' | 'worktree-only-change' |
95
+ 'concurrent-or-foreign-change' | 'observation-ambiguous' | 'incomplete';
96
+ readonly baselineHead: string;
97
+ readonly afterHead?: string;
98
+ readonly commitOid?: string;
99
+ }
100
+ export type SessionRetentionUpdate =
101
+ | { readonly kind: 'retain'; readonly rootPlaybookId: string; readonly generation: Readonly<Record<string, any>> }
102
+ | { readonly kind: 'clear'; readonly rootPlaybookId: string };
103
+
104
+ export interface SessionExecutionProjection {
105
+ readonly schemaVersion: 2;
106
+ readonly captain: Readonly<Record<string, any>>;
107
+ readonly players: readonly Readonly<Record<string, any>>[];
108
+ readonly catalog: Readonly<Record<string, any>>;
109
+ }
110
+ export interface SessionStructuralProjection {
111
+ readonly schemaVersion: 1;
112
+ readonly captain: Readonly<Record<string, any>>;
113
+ readonly players: readonly Readonly<Record<string, any>>[];
114
+ readonly catalog: Readonly<Record<string, any>>;
115
+ }
116
+ export interface SessionRecovery {
117
+ readonly schemaVersion: 6;
118
+ readonly kind: 'captain-session';
119
+ readonly sessionId: string;
120
+ readonly cwd: string;
121
+ readonly createdAt: string;
122
+ readonly updatedAt: string;
123
+ readonly state: 'settled' | 'uncertain';
124
+ readonly structuralProjection: SessionStructuralProjection;
125
+ readonly lastAppliedExecutionProjection: SessionExecutionProjection;
126
+ readonly snapshot: SessionSnapshot;
127
+ readonly effectLedger: SessionEffectLedger;
128
+ readonly unresolvedEffects: readonly SessionUnresolvedEffect[];
129
+ readonly retainedGenerations?: Readonly<Record<string, any>>;
130
+ readonly uncertain?: {
131
+ readonly input: string;
132
+ readonly attemptId: string;
133
+ readonly attemptNumber: number;
134
+ readonly baseUpdatedAt: string | null;
135
+ readonly markedAt: string;
136
+ readonly attemptedExecutionProjection: SessionExecutionProjection;
137
+ readonly abandonment?: Readonly<Record<string, any>>;
138
+ };
139
+ }
140
+ export interface SessionReplayCheckpoint {
141
+ readonly seq: number;
142
+ readonly sha256: string;
143
+ readonly incomplete: boolean;
144
+ }
145
+ export type SessionManifest = Omit<SessionRecovery, 'schemaVersion'> & {
146
+ readonly schemaVersion: 7;
147
+ readonly replay: SessionReplayCheckpoint;
148
+ readonly contextSeq: number;
149
+ } | {
150
+ readonly schemaVersion: 7;
151
+ readonly kind: 'captain-session';
152
+ readonly state: 'history-only';
153
+ readonly sessionId: string;
154
+ readonly cwd: string;
155
+ readonly createdAt: string;
156
+ readonly updatedAt: string;
157
+ readonly replay: SessionReplayCheckpoint;
158
+ readonly contextSeq: number | null;
159
+ readonly reason: string;
160
+ };
161
+ export type StoredSessionManifest = SessionManifest | {
162
+ readonly schemaVersion: number;
163
+ readonly sessionId: string;
164
+ readonly cwd?: unknown;
165
+ readonly [key: string]: unknown;
166
+ };
167
+ export interface SessionGraph {
168
+ readonly initial: string;
169
+ readonly nodes: readonly { readonly id: string; readonly kind: 'state' | 'final'; readonly tags: readonly string[]; readonly parent?: string; readonly role?: string; readonly description?: string }[];
170
+ readonly edges: readonly { readonly id: string; readonly from: string; readonly to: string; readonly event: string }[];
171
+ }
172
+ export interface SessionContext {
173
+ readonly type: 'session_context';
174
+ readonly timestamp: number;
175
+ readonly contextVersion: 1;
176
+ readonly captainId: string;
177
+ readonly configuration: SessionExecutionProjection;
178
+ readonly graphs: readonly { readonly playbookId: string; readonly graph: SessionGraph | null }[];
179
+ readonly initialVisible: readonly string[];
180
+ }
181
+ export interface SessionHistory extends ReplayStreamReadResult {
182
+ readonly synthetic?: true;
183
+ readonly missing: boolean;
184
+ readonly incomplete: boolean;
185
+ readonly pendingTail: boolean;
186
+ readonly damage?: { readonly seq: number; readonly offset: number; readonly reason: string };
187
+ readonly digests: readonly string[];
188
+ readonly completeBytes: number;
189
+ }
190
+ export interface SessionValidation {
191
+ readonly integrityValid: boolean;
192
+ readonly sessionId: string;
193
+ readonly resumable: boolean;
194
+ readonly reasons: readonly string[];
195
+ readonly manifest: StoredSessionManifest;
196
+ readonly history: SessionHistory;
197
+ }
198
+ export interface SessionHints {
199
+ readonly players: Readonly<Record<string, string>>;
200
+ readonly captain?: { readonly kind: 'pinned'; readonly token: string } | { readonly kind: 'needsCatchUp'; readonly resume: string | false; readonly afterJournalSeq: number };
201
+ }
202
+ export interface SessionFreshBoundary {
203
+ readonly cwd: string;
204
+ readonly structuralProjection: SessionStructuralProjection;
205
+ readonly executionProjection: SessionExecutionProjection;
206
+ readonly snapshot: SessionSnapshot;
207
+ readonly context?: SessionContext;
208
+ readonly onLegacyRecord?: (record: any) => void | Promise<void>;
209
+ readonly onInvalidRecord?: (record: any) => void | Promise<void>;
210
+ }
211
+ export interface PlaybookSessionLifecycle extends PlaybookSessionLease {
212
+ read(): Promise<SessionRecovery | undefined>;
213
+ readManifest(): Promise<StoredSessionManifest>;
214
+ initializeSettledWithPredecessor(options: SessionFreshBoundary): Promise<SessionRecovery>;
215
+ abandonFreshSettled(options: { expected: SessionRecovery }): Promise<boolean>;
216
+ beginTurn(options: { input: string; attemptId: string; attemptedExecutionProjection: SessionExecutionProjection }): Promise<SessionRecovery>;
217
+ beginRetry(options: { expectedAttemptId: string; nextAttemptId: string }): Promise<SessionRecovery>;
218
+ settle(options: { attemptId: string; snapshot: SessionSnapshot; unresolvedEffects: readonly SessionUnresolvedEffect[]; retentionUpdates?: readonly SessionRetentionUpdate[] }): Promise<SessionRecovery>;
219
+ discard(options: { attemptId: string }): Promise<SessionRecovery | undefined>;
220
+ beginUnresolvedEffectAbandonment(options: any): Promise<any>;
221
+ completeUnresolvedEffectAbandonment(options: any): Promise<any>;
222
+ recoverUnresolvedEffectAbandonment(): Promise<SessionRecovery | undefined>;
223
+ writeEffectLedger(authority: any, commands: readonly Readonly<Record<string, any>>[]): Promise<SessionEffectLedger>;
224
+ assertOwner(): Promise<any>;
225
+ assertContinuable(context?: { cwd?: string; executionProjection?: SessionExecutionProjection }): Promise<SessionValidation>;
226
+ recordContext(context: SessionContext): Promise<number>;
227
+ consumeHints(): Promise<SessionHints>;
228
+ acknowledgeHint(participantId: string, token: string): void;
229
+ clearHint(participantId: string): void;
230
+ }
231
+ export interface SharedSessionStore {
232
+ readonly sessionsDir: string;
233
+ prepare(): Promise<void>;
234
+ read(sessionId: string): Promise<SessionRecovery>;
235
+ readManifest(sessionId: string): Promise<StoredSessionManifest>;
236
+ /** Observation only; mutations still require acquiring the lease. */
237
+ readLeaseState(sessionId: string): Promise<'active' | 'idle' | 'unknown'>;
238
+ readHistory(sessionId: string, options?: ReplayStreamReadOptions): Promise<SessionHistory>;
239
+ readStream(sessionId: string, options?: ReplayStreamReadOptions): Promise<ReplayStreamReadResult>;
240
+ readSummary(sessionId: string): Promise<PlaybookSessionSummary>;
241
+ listSummaries(): Promise<PlaybookSessionListResult>;
242
+ latest(options?: { preferredCwd?: string; onLegacyRecord?: (record: any) => void | Promise<void> }): Promise<SessionRecovery>;
243
+ acquire(sessionId: string): Promise<PlaybookSessionLifecycle>;
244
+ acquireManagement(sessionId: string): Promise<{ readonly sessionId: string; readonly ownerToken: string; assertOwner(): Promise<unknown>; release(): Promise<void> }>;
245
+ validate(sessionId: string, context?: { cwd?: string; executionProjection?: SessionExecutionProjection }): Promise<SessionValidation>;
246
+ delete(sessionId: string): Promise<void>;
247
+ migrateLegacyDefault(options?: { env?: Readonly<Record<string, string | undefined>>; homeDir?: string }): Promise<{ sourceDir: string; migrated: readonly string[]; skipped: readonly { sessionId: string; reason: string }[] }>;
248
+ migrate(sessionId: string, options?: { sourcePath?: string; cwd?: string; backupDir?: string }): Promise<{ manifest: SessionManifest; migrated: boolean; reasons: readonly string[] }>;
249
+ }
250
+ export declare function createSessionStore(options?: { sessionsDir?: string; env?: Readonly<Record<string, string | undefined>>; homeDir?: string; [key: string]: any }): SharedSessionStore;
251
+ export declare function validateSessionManifest(value: unknown): SessionManifest;
252
+ export declare function validateSessionContext(value: unknown): SessionContext;
253
+ export declare function projectCaptainSessionStructure(value: SessionExecutionProjection): SessionStructuralProjection;
254
+ export declare function validateCaptainSessionExecutionProjection(value: unknown): SessionExecutionProjection;
255
+ export declare function validateCaptainSessionStructuralProjection(value: unknown): SessionStructuralProjection;
256
+ export declare function assertCaptainSessionExecutionCompatible(structural: SessionStructuralProjection, execution: SessionExecutionProjection): SessionExecutionProjection;
257
+ export declare function attachSessionHints(snapshot: SessionSnapshot, hints: SessionHints): SessionSnapshot;