@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.
- package/docs/cli.md +56 -52
- package/docs/configuration.md +2 -2
- package/docs/embedding.md +68 -32
- package/package.json +11 -3
- package/reference/sdlc/code.playbook/bin/interactive-session.js +1 -0
- package/reference/sdlc/code.playbook/bin/launch-config.js +2 -0
- package/reference/sdlc/code.playbook/bin/playbook.js +42 -4
- package/reference/sdlc/code.playbook/bin/portable-codec.js +190 -0
- package/reference/sdlc/code.playbook/bin/replay-observer.js +18 -2
- package/reference/sdlc/code.playbook/bin/run.js +62 -18
- package/reference/sdlc/code.playbook/bin/session-host.js +104 -0
- package/reference/sdlc/code.playbook/bin/session-store.js +622 -65
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +5 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +53 -14
- package/reference/sdlc/code.playbook/playbook-captain.ts +68 -21
- package/reference/sdlc/code.playbook/session-host.d.ts +75 -0
- package/reference/sdlc/code.playbook/session-host.js +18 -0
- package/reference/sdlc/code.playbook/session-store.d.ts +176 -1
- package/reference/sdlc/code.playbook/session-store.js +22 -6
- package/src/xstate-playbook-runtime.js +12 -13
- package/src/xstate-playbook-runtime.ts +14 -21
|
@@ -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
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
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
|
|
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
|
-
//
|
|
4138
|
-
//
|
|
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 ===
|
|
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 ===
|
|
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
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
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
|
|
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
|
-
//
|
|
6212
|
-
//
|
|
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 ===
|
|
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 ===
|
|
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;
|