@sublang/playbook 17.2.0 → 17.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/docs/cli.md +6 -5
- package/docs/configuration.md +72 -18
- package/docs/embedding.md +94 -0
- package/package.json +34 -5
- package/reference/sdlc/captain.md +6 -4
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +5 -0
- package/reference/sdlc/captain.playbook/captain.fsm.js +4 -3
- package/reference/sdlc/captain.playbook/captain.fsm.ts +9 -4
- package/reference/sdlc/captain.playbook/captain.gears.md +6 -3
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +2 -0
- package/reference/sdlc/captain.playbook/captain.playbook.js +7 -3
- package/reference/sdlc/captain.playbook/captain.playbook.ts +8 -4
- package/reference/sdlc/code.playbook/asset-types.d.ts +32 -0
- package/reference/sdlc/code.playbook/bin/launch-config.js +84 -31
- package/reference/sdlc/code.playbook/bin/playbook.js +5 -3
- package/reference/sdlc/code.playbook/bin/portable-codec.js +12 -5
- package/reference/sdlc/code.playbook/bin/run.js +74 -7
- package/reference/sdlc/code.playbook/bin/session-host.js +16 -4
- package/reference/sdlc/code.playbook/bin/session-media.js +46 -0
- package/reference/sdlc/code.playbook/bin/session-store.js +164 -25
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +23 -4
- package/reference/sdlc/code.playbook/playbook-captain.js +188 -23
- package/reference/sdlc/code.playbook/playbook-captain.ts +223 -31
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +22 -5
- package/reference/sdlc/code.playbook/session-assets.d.ts +53 -0
- package/reference/sdlc/code.playbook/session-assets.js +302 -0
- package/reference/sdlc/code.playbook/session-host.d.ts +9 -1
- package/reference/sdlc/code.playbook/session-store.d.ts +26 -2
- package/reference/sdlc/code.playbook/worker-evidence.d.ts +8 -0
- package/reference/sdlc/code.playbook/worker-evidence.js +51 -0
- package/reference/sdlc/inspect.md +40 -0
- package/reference/sdlc/inspect.playbook/inspect.fsm.d.ts +168 -0
- package/reference/sdlc/inspect.playbook/inspect.fsm.js +354 -0
- package/reference/sdlc/inspect.playbook/inspect.fsm.ts +455 -0
- package/reference/sdlc/inspect.playbook/inspect.gears.md +36 -0
- package/reference/sdlc/inspect.playbook/inspect.playbook.d.ts +17 -0
- package/reference/sdlc/inspect.playbook/inspect.playbook.js +142 -0
- package/reference/sdlc/inspect.playbook/inspect.playbook.ts +184 -0
- package/reference/sdlc/inspect.playbook/inspect.registry.d.ts +41 -0
- package/reference/sdlc/inspect.playbook/inspect.registry.js +51 -0
- package/reference/sdlc/inspect.playbook/inspect.registry.ts +110 -0
- package/slc/workflow-contracts.json +43 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
3
|
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { normalizeSessionTurnInput } from '../session-assets.js';
|
|
5
6
|
import { createCaptainSessionStore, isUncertainTurnDiscardable, projectCaptainSessionStructure } from './session-store.js';
|
|
6
7
|
import { createCaptainSessionHost, executionConfigFromPlan, installRetainedGenerationsForLaunch, validateFrozenExecutionConfig } from './run.js';
|
|
7
8
|
import { createReplayRecordObserver } from './replay-observer.js';
|
|
@@ -33,9 +34,10 @@ export async function openSessionHost(options) {
|
|
|
33
34
|
if (record !== undefined) await lease.assertContinuable({ cwd, executionProjection: selected });
|
|
34
35
|
const config = await validateFrozenExecutionConfig(structure, selected, { loadModule, prepareRegistryModule: options.prepareRegistryModule });
|
|
35
36
|
const replay = createReplayRecordObserver({ lease, onIncomplete: options.onIncomplete ?? (() => {}), onStored: options.onStoredRecord });
|
|
36
|
-
let terminal, replies = [];
|
|
37
|
+
let terminal, replies = [], submittedAttachments = [];
|
|
37
38
|
const observe = {
|
|
38
39
|
async onRecord(value) {
|
|
40
|
+
if (value.type === 'turn_started' && submittedAttachments.length) value = { ...value, turn: { ...value.turn, attachments: submittedAttachments } };
|
|
39
41
|
if (value.type === "turn_finished" || value.type === "turn_aborted") terminal = value;
|
|
40
42
|
if (value.type === "captain_reply") replies.push(value);
|
|
41
43
|
await replay.observer.onRecord(value);
|
|
@@ -53,14 +55,21 @@ export async function openSessionHost(options) {
|
|
|
53
55
|
const execute = async (input, retry, actionId, shellActionId) => {
|
|
54
56
|
assertIdle();
|
|
55
57
|
let attemptId;
|
|
58
|
+
let attachments = [];
|
|
56
59
|
const operation = (async () => {
|
|
57
60
|
let prior = await lease.read();
|
|
58
61
|
if (retry) {
|
|
59
62
|
if (prior?.state !== 'uncertain') throw new Error('session has no uncertain turn to retry');
|
|
60
63
|
input = prior.uncertain.input;
|
|
64
|
+
attachments = prior.uncertain.attachments ?? [];
|
|
61
65
|
if (!retryPending) throw new Error(RECOVERY_REOPEN);
|
|
62
66
|
} else if (prior?.state !== 'settled') throw new Error(RECOVERY_REOPEN);
|
|
63
|
-
if (actionId === undefined && shellActionId === undefined
|
|
67
|
+
if (actionId === undefined && shellActionId === undefined) {
|
|
68
|
+
const normalized = normalizeSessionTurnInput(retry ? { text: input, attachments } : input);
|
|
69
|
+
input = normalized.text; attachments = normalized.attachments;
|
|
70
|
+
if (!input.trim() && attachments.length === 0) throw new Error('session input must contain text or attachments');
|
|
71
|
+
if (attachments.length) await lease.resolveAttachments(attachments, { signal: options.signal });
|
|
72
|
+
}
|
|
64
73
|
await created.reconcileRepositoryEffects();
|
|
65
74
|
// The selection is made after reconciliation, so the advertised
|
|
66
75
|
// reading it is validated against is the one this turn settles
|
|
@@ -74,11 +83,13 @@ export async function openSessionHost(options) {
|
|
|
74
83
|
}
|
|
75
84
|
terminal = undefined; replies = [];
|
|
76
85
|
attemptId = options.createAttemptId?.() ?? randomUUID();
|
|
77
|
-
const marked = retry ? await lease.beginRetry({ expectedAttemptId: prior.uncertain.attemptId, nextAttemptId: attemptId }) : await lease.beginTurn({ input, attemptId, attemptedExecutionProjection: config });
|
|
86
|
+
const marked = retry ? await lease.beginRetry({ expectedAttemptId: prior.uncertain.attemptId, nextAttemptId: attemptId }) : await lease.beginTurn({ input, attachments, attemptId, attemptedExecutionProjection: config });
|
|
78
87
|
retryPending = false;
|
|
79
88
|
await replay.flushStoredRecords();
|
|
80
89
|
await options.onCheckpoint?.(marked);
|
|
81
90
|
await lease.assertOwner();
|
|
91
|
+
submittedAttachments = attachments;
|
|
92
|
+
created.shell.setTurnAttachments?.(attachments);
|
|
82
93
|
await created.host.runBossTurn(input);
|
|
83
94
|
if (terminal?.type !== "turn_finished" || replies.length !== 1 || typeof replies[0].text !== "string" || replies[0].text.trim().length === 0) throw new Error("Captain turn did not finish with one reply; session remains uncertain");
|
|
84
95
|
const settlement = created.shell.exportSettlement();
|
|
@@ -101,12 +112,13 @@ export async function openSessionHost(options) {
|
|
|
101
112
|
throw error;
|
|
102
113
|
});
|
|
103
114
|
active = operation;
|
|
104
|
-
try { return await operation; } finally { active = undefined; }
|
|
115
|
+
try { return await operation; } finally { active = undefined; submittedAttachments = []; }
|
|
105
116
|
};
|
|
106
117
|
|
|
107
118
|
const dispose = () => {
|
|
108
119
|
if (closed) return Promise.resolve();
|
|
109
120
|
if (closing) return closing;
|
|
121
|
+
created.cancelPendingApprovals();
|
|
110
122
|
closing = (async () => {
|
|
111
123
|
try { await active; } catch { /* Preserve the durable uncertain marker. */ }
|
|
112
124
|
await created.host.dispose();
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
+
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { externalizeAgentEvent } from '../session-assets.js';
|
|
6
|
+
|
|
7
|
+
/** Externalize authentic observed media before the opaque replay writer sees it. */
|
|
8
|
+
export function createSessionMediaProjector({ lease, onEvidence }) {
|
|
9
|
+
const players = new Map();
|
|
10
|
+
let preparation;
|
|
11
|
+
const seen = new Map();
|
|
12
|
+
const beginPreparation = (runtimeSessionId) => {
|
|
13
|
+
if (preparation) throw new Error('a preparation media scope is already active');
|
|
14
|
+
const scope = { callId: randomUUID(), origin: { kind: 'preparation', actorId: 'captain', runtimeSessionId } };
|
|
15
|
+
preparation = scope;
|
|
16
|
+
return () => { if (preparation === scope) preparation = undefined; seen.delete(scope.callId); };
|
|
17
|
+
};
|
|
18
|
+
const project = async (record) => {
|
|
19
|
+
if (record.type === 'captain_telemetry' && record.topic === 'playbook.trace') {
|
|
20
|
+
const trace = record.payload;
|
|
21
|
+
if (trace?.schemaVersion === 4 && trace.type === 'player.call.started' && typeof trace.callId === 'string' && typeof trace.sessionId === 'string' && typeof trace.payload?.playerId === 'string') {
|
|
22
|
+
players.set(trace.payload.playerId, { callId: trace.callId, origin: { kind: 'player', actorId: trace.payload.playerId, runtimeSessionId: trace.sessionId } });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (record.type === 'player_prompt' && !players.has(record.playerId)) players.set(record.playerId, { callId: randomUUID(), origin: { kind: 'player', actorId: record.playerId } });
|
|
26
|
+
if (record.type === 'player_finished') { const scope = players.get(record.playerId); if (scope) seen.delete(scope.callId); players.delete(record.playerId); }
|
|
27
|
+
if (record.type !== 'player_event' && record.type !== 'captain_event') return [record];
|
|
28
|
+
const event = record.event;
|
|
29
|
+
const scope = record.type === 'player_event' ? players.get(record.playerId) : preparation;
|
|
30
|
+
try {
|
|
31
|
+
const { event: projected, asset } = await externalizeAgentEvent(lease, event);
|
|
32
|
+
const stored = projected === event ? record : { ...record, event: projected };
|
|
33
|
+
if (!asset || !scope || typeof record.turnId !== 'number') return [stored];
|
|
34
|
+
let emitted = seen.get(scope.callId);
|
|
35
|
+
if (!emitted) { emitted = new Set(); seen.set(scope.callId, emitted); }
|
|
36
|
+
if (emitted.has(asset.assetId)) return [stored];
|
|
37
|
+
emitted.add(asset.assetId);
|
|
38
|
+
const origin = { ...scope.origin, ...(event.payload.toolUseId ? { toolUseId: event.payload.toolUseId } : {}) };
|
|
39
|
+
await onEvidence?.(asset, origin);
|
|
40
|
+
return [stored, { type: 'playbook_evidence', timestamp: record.timestamp, turnId: record.turnId, callId: scope.callId, origin, asset }];
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return [{ type: 'runtime_error', timestamp: record.timestamp, turnId: record.turnId, sourceRecordType: record.type, message: `Media could not be saved: ${error instanceof Error ? error.message : String(error)}` }];
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return Object.freeze({ project, beginPreparation });
|
|
46
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// complete retained-generation map under guarded source-first publication.
|
|
8
8
|
|
|
9
9
|
import { createHash, randomUUID } from 'node:crypto';
|
|
10
|
+
import { createAssetStore, parseAssetUri, validateAssetRef, validateAssetRefs } from '../session-assets.js';
|
|
10
11
|
import {
|
|
11
12
|
SESSION_MANIFEST_VERSION, EMPTY_REPLAY_SHA256, sha256,
|
|
12
13
|
validateSessionManifest, recoveryFromManifest, manifestFromRecovery,
|
|
@@ -637,6 +638,12 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
637
638
|
const replayReadCursors = new Map();
|
|
638
639
|
const replayReadQueues = new Map();
|
|
639
640
|
const portableWriters = new Map();
|
|
641
|
+
const assetStores = new Map();
|
|
642
|
+
const assetStoreFor = (sessionId) => {
|
|
643
|
+
assertSessionId(sessionId);
|
|
644
|
+
if (!assetStores.has(sessionId)) assetStores.set(sessionId, createAssetStore({ directory: join(sessionsDir, `${sessionId}.assets`), ...(options.maxAssetBytes === undefined ? {} : { maxAssetBytes: options.maxAssetBytes }) }));
|
|
645
|
+
return assetStores.get(sessionId);
|
|
646
|
+
};
|
|
640
647
|
|
|
641
648
|
if (!isAbsolute(sessionsDir)) {
|
|
642
649
|
throw new Error('Captain session store path must be absolute');
|
|
@@ -758,7 +765,11 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
758
765
|
if (value.sessionId !== sessionId) throw new Error('session manifest identity does not match its filename');
|
|
759
766
|
return value.schemaVersion === 7 ? validateSessionManifest(value) : value;
|
|
760
767
|
};
|
|
761
|
-
const prepare = () =>
|
|
768
|
+
const prepare = async () => {
|
|
769
|
+
await prepareSessionPermissions(sessionsDir, fs);
|
|
770
|
+
let names; try { names = await fs.readdir(sessionsDir); } catch (cause) { if (cause?.code === 'ENOENT') return; throw cause; }
|
|
771
|
+
for (const name of names) if (name.endsWith('.assets') && SESSION_ID_PATTERN.test(name.slice(0, -7))) await assetStoreFor(name.slice(0, -7)).prepare();
|
|
772
|
+
};
|
|
762
773
|
const readHistory = async (sessionId, options = {}) => {
|
|
763
774
|
assertSessionId(sessionId);
|
|
764
775
|
const history = await readSessionHistory({ sessionsDir, path: recordsPathFor(sessionId), fs, afterSeq: options.afterSeq ?? 0 });
|
|
@@ -771,6 +782,35 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
771
782
|
} catch { return history; }
|
|
772
783
|
return legacyJournalHistory(legacy, options.afterSeq ?? 0, history);
|
|
773
784
|
};
|
|
785
|
+
// Only our typed reference locations participate; opaque tool/text data is history.
|
|
786
|
+
const assertAssetInventory = (manifest, entries) => {
|
|
787
|
+
const declared = new Map((manifest.assets?.entries ?? []).map((reference) => [reference.assetId, reference]));
|
|
788
|
+
const check = (reference) => {
|
|
789
|
+
const valid = validateAssetRef(reference);
|
|
790
|
+
if (declared.get(valid.assetId)?.byteLength !== valid.byteLength) throw new Error(`session asset reference is absent from its checkpoint inventory: ${valid.assetId}`);
|
|
791
|
+
};
|
|
792
|
+
const checkRefs = (references) => { for (const reference of references ?? []) check(reference); };
|
|
793
|
+
const snapshot = (value) => {
|
|
794
|
+
if (!value) return;
|
|
795
|
+
checkRefs(value.pendingAttachments);
|
|
796
|
+
for (const frame of value.frames ?? []) checkRefs(frame.attachments);
|
|
797
|
+
for (const item of value.journal ?? []) if (item.kind === 'boss' && typeof item.payload === 'object' && item.payload !== null) checkRefs(item.payload.attachments);
|
|
798
|
+
};
|
|
799
|
+
snapshot(manifest.snapshot); snapshot(manifest.uncertain?.progress?.snapshot);
|
|
800
|
+
checkRefs(manifest.uncertain?.attachments);
|
|
801
|
+
for (const generation of Object.values(manifest.retainedGenerations ?? {})) for (const frame of generation.frames ?? []) checkRefs(frame.attachments);
|
|
802
|
+
for (const { record } of entries) {
|
|
803
|
+
if (record.type === 'turn_started') checkRefs(record.turn?.attachments);
|
|
804
|
+
else if (record.type === 'playbook_evidence') check(record.asset);
|
|
805
|
+
else if (record.type === 'player_event' || record.type === 'captain_event') {
|
|
806
|
+
const event = record.event;
|
|
807
|
+
if (event?.type === 'media' && event.payload?.source?.type === 'uri') {
|
|
808
|
+
const id = parseAssetUri(event.payload.source.uri);
|
|
809
|
+
if (id && !declared.has(id)) throw new Error(`session media asset is absent from its checkpoint inventory: ${id}`);
|
|
810
|
+
} else if (event?.type === 'tool_result' && event.payload?.output?.type === 'asset_reference') check(event.payload.output.asset);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
};
|
|
774
814
|
const validate = async (sessionId, context = {}) => {
|
|
775
815
|
const manifest = await readManifest(sessionId);
|
|
776
816
|
const history = await readHistory(sessionId);
|
|
@@ -790,6 +830,12 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
790
830
|
integrityValid = false;
|
|
791
831
|
reasons.push('session replay contains provider continuation fields');
|
|
792
832
|
}
|
|
833
|
+
try { assertAssetInventory(manifest, history.entries.filter(({ seq }) => seq <= manifest.replay.seq)); }
|
|
834
|
+
catch (cause) { integrityValid = false; reasons.push(errorMessage(cause)); }
|
|
835
|
+
for (const reference of manifest.assets?.entries ?? []) {
|
|
836
|
+
try { const reader = await assetStoreFor(sessionId).openAsset(reference); await reader.close(); }
|
|
837
|
+
catch (cause) { integrityValid = false; reasons.push(`session asset is unavailable: ${errorMessage(cause)}`); }
|
|
838
|
+
}
|
|
793
839
|
if (manifest.state === 'history-only') reasons.push(manifest.reason);
|
|
794
840
|
if (manifest.replay.incomplete || history.incomplete) reasons.push('session replay is incomplete');
|
|
795
841
|
if (history.digests[manifest.replay.seq] !== manifest.replay.sha256) { integrityValid = false; reasons.push('session replay checkpoint digest does not match'); }
|
|
@@ -1111,6 +1157,18 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
1111
1157
|
};
|
|
1112
1158
|
|
|
1113
1159
|
const deleteRecord = async (sessionId) => {
|
|
1160
|
+
const assetsDirectory = join(sessionsDir, `${sessionId}.assets`);
|
|
1161
|
+
try {
|
|
1162
|
+
await assertPrivateDirectory(assetsDirectory, fs);
|
|
1163
|
+
const names = await fs.readdir(assetsDirectory);
|
|
1164
|
+
for (const name of names) {
|
|
1165
|
+
if (!/^(?:[0-9a-f]{64}(?:\.json)?|\.import-[0-9a-f-]+\.tmp)$/.test(name)) throw new Error('unknown session asset entry prevents deletion');
|
|
1166
|
+
await assertPrivateRegularPath(join(assetsDirectory, name), 0o600, fs, 'session asset');
|
|
1167
|
+
}
|
|
1168
|
+
for (const name of names) await fs.unlink(join(assetsDirectory, name));
|
|
1169
|
+
await fs.rmdir(assetsDirectory);
|
|
1170
|
+
await syncDirectory(sessionsDir, fs);
|
|
1171
|
+
} catch (cause) { if (cause?.code !== 'ENOENT') throw cause; }
|
|
1114
1172
|
for (const suffix of ['.records.jsonl', '.hints.json', '.spex.json', '.json']) {
|
|
1115
1173
|
const path = join(sessionsDir, `${sessionId}${suffix}`);
|
|
1116
1174
|
try { await assertPrivateRegularPath(path, 0o600, fs, 'session file'); await fs.unlink(path); await syncDirectory(sessionsDir, fs); }
|
|
@@ -1396,6 +1454,7 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
1396
1454
|
const acquire = async (sessionId, management = false) => {
|
|
1397
1455
|
assertSessionId(sessionId);
|
|
1398
1456
|
await prepareSessionPermissions(sessionsDir, fs, sessionId);
|
|
1457
|
+
await assetStoreFor(sessionId).prepare();
|
|
1399
1458
|
let stage;
|
|
1400
1459
|
let stagePublished = false;
|
|
1401
1460
|
try {
|
|
@@ -1451,6 +1510,7 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
1451
1510
|
readHistory,
|
|
1452
1511
|
validateSession: validate,
|
|
1453
1512
|
portableWriters,
|
|
1513
|
+
assetStore: assetStoreFor(sessionId),
|
|
1454
1514
|
writeRecord,
|
|
1455
1515
|
syncRecordDirectory: () => syncDirectory(sessionsDir, fs),
|
|
1456
1516
|
deleteRecord,
|
|
@@ -1684,8 +1744,39 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
1684
1744
|
finally { await lease.release(); }
|
|
1685
1745
|
};
|
|
1686
1746
|
|
|
1747
|
+
const exportBundle = async (sessionId) => {
|
|
1748
|
+
assertSessionId(sessionId);
|
|
1749
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1750
|
+
const manifestText = await readPrivateRegularFile(recordPathFor(sessionId), 0o600, fs, 'record');
|
|
1751
|
+
const manifest = validateSessionManifest(JSON.parse(manifestText));
|
|
1752
|
+
const replayBytes = Buffer.from(await readPrivateRegularFile(recordsPathFor(sessionId), 0o600, fs, 'replay stream'), 'utf8');
|
|
1753
|
+
let end = 0;
|
|
1754
|
+
for (let seq = 0; seq < manifest.replay.seq; seq++) {
|
|
1755
|
+
const next = replayBytes.indexOf(10, end);
|
|
1756
|
+
if (next === -1) throw new Error('session bundle is missing its checkpoint replay prefix');
|
|
1757
|
+
end = next + 1;
|
|
1758
|
+
}
|
|
1759
|
+
const replay = replayBytes.subarray(0, end);
|
|
1760
|
+
if (sha256(replay) !== manifest.replay.sha256 || manifest.replay.incomplete) throw new Error('session bundle replay checkpoint is incomplete or corrupt');
|
|
1761
|
+
assertAssetInventory(manifest, parseSessionHistory(replay).entries);
|
|
1762
|
+
const assets = [];
|
|
1763
|
+
for (const reference of manifest.assets?.entries ?? []) {
|
|
1764
|
+
const reader = await assetStoreFor(sessionId).openAsset(reference); await reader.close();
|
|
1765
|
+
const hash = reference.assetId.slice(7);
|
|
1766
|
+
assets.push(Object.freeze({ assetId: reference.assetId, path: hash, metadataPath: `${hash}.json`, byteLength: reference.byteLength }));
|
|
1767
|
+
}
|
|
1768
|
+
if (manifestText !== await readPrivateRegularFile(recordPathFor(sessionId), 0o600, fs, 'record')) continue;
|
|
1769
|
+
return Object.freeze({ sessionId, manifest: Buffer.from(manifestText, 'utf8'), replay, assets: Object.freeze(assets) });
|
|
1770
|
+
}
|
|
1771
|
+
throw new Error('session changed while exporting its bundle; retry');
|
|
1772
|
+
};
|
|
1773
|
+
|
|
1687
1774
|
return Object.freeze({
|
|
1688
1775
|
sessionsDir,
|
|
1776
|
+
readAsset: (sessionId, reference, options) => assetStoreFor(sessionId).readAsset(reference, options),
|
|
1777
|
+
openAsset: (sessionId, reference, options) => assetStoreFor(sessionId).openAsset(reference, options),
|
|
1778
|
+
describeAsset: (sessionId, assetId) => assetStoreFor(sessionId).describeAsset(assetId),
|
|
1779
|
+
exportBundle,
|
|
1689
1780
|
prepare,
|
|
1690
1781
|
migrate,
|
|
1691
1782
|
migrateLegacyDefault,
|
|
@@ -2540,6 +2631,7 @@ async function createLease({
|
|
|
2540
2631
|
readHistory,
|
|
2541
2632
|
validateSession,
|
|
2542
2633
|
portableWriters,
|
|
2634
|
+
assetStore,
|
|
2543
2635
|
writeRecord,
|
|
2544
2636
|
syncRecordDirectory,
|
|
2545
2637
|
deleteRecord,
|
|
@@ -2636,7 +2728,9 @@ async function createLease({
|
|
|
2636
2728
|
const seq = history.digests[durableSeq] === undefined ? previousManifest?.replay?.seq ?? 0 : durableSeq;
|
|
2637
2729
|
const digest = history.digests[seq] ?? previousManifest?.replay?.sha256 ?? EMPTY_REPLAY_SHA256;
|
|
2638
2730
|
const replay = { seq, sha256: digest, incomplete: previousManifest?.replay?.incomplete === true || status.incomplete || history.incomplete || history.digests[seq] === undefined };
|
|
2639
|
-
const
|
|
2731
|
+
const entries = await assetStore.listAssets();
|
|
2732
|
+
const assets = entries.length === 0 ? undefined : { version: 1, entries: await Promise.all(entries.map(({ assetId }) => assetStore.describeAsset(assetId))) };
|
|
2733
|
+
const manifest = manifestFromRecovery(recovery, replay, contextSeq, assets);
|
|
2640
2734
|
return manifest;
|
|
2641
2735
|
};
|
|
2642
2736
|
portableWriters.set(sessionId, { checkpoint, sync: replayWriter.checkpoint, published: (manifest) => { previousManifest = manifest; } });
|
|
@@ -2928,6 +3022,8 @@ async function createLease({
|
|
|
2928
3022
|
source.effectLedger,
|
|
2929
3023
|
);
|
|
2930
3024
|
|
|
3025
|
+
const inheritedAssets = new Map(Object.values(retainedGenerations).flatMap((generation) => generation.frames.flatMap((frame) => frame.attachments ?? [])).map((reference) => [reference.assetId, reference]));
|
|
3026
|
+
for (const reference of inheritedAssets.values()) await assetStore.copyAsset(sourceLease, reference);
|
|
2931
3027
|
await sourceLease.assertOwner();
|
|
2932
3028
|
await assertOwnerUnchecked();
|
|
2933
3029
|
let sourcePublished = false;
|
|
@@ -3026,11 +3122,12 @@ async function createLease({
|
|
|
3026
3122
|
|
|
3027
3123
|
const beginTurn = ({
|
|
3028
3124
|
input,
|
|
3125
|
+
attachments,
|
|
3029
3126
|
attemptId,
|
|
3030
3127
|
attemptedExecutionProjection,
|
|
3031
3128
|
} = {}) =>
|
|
3032
3129
|
runExclusive(async () => {
|
|
3033
|
-
assertAcceptedInput(input);
|
|
3130
|
+
assertAcceptedInput(input, attachments);
|
|
3034
3131
|
assertUuid(attemptId, 'Captain session attempt id');
|
|
3035
3132
|
const attempted = validateCaptainSessionExecutionProjection(
|
|
3036
3133
|
attemptedExecutionProjection,
|
|
@@ -3065,6 +3162,7 @@ async function createLease({
|
|
|
3065
3162
|
uncertain: {
|
|
3066
3163
|
baseUpdatedAt: prior.updatedAt,
|
|
3067
3164
|
input,
|
|
3165
|
+
...(attachments?.length ? { attachments: validateAssetRefs(attachments) } : {}),
|
|
3068
3166
|
attemptId,
|
|
3069
3167
|
attemptNumber: 1,
|
|
3070
3168
|
markedAt: timestamp,
|
|
@@ -3119,6 +3217,7 @@ async function createLease({
|
|
|
3119
3217
|
baseUpdatedAt: prior.uncertain.baseUpdatedAt,
|
|
3120
3218
|
...(prior.uncertain.progress === undefined ? {} : { progress: prior.uncertain.progress }),
|
|
3121
3219
|
input: prior.uncertain.input,
|
|
3220
|
+
...(prior.uncertain.attachments === undefined ? {} : { attachments: prior.uncertain.attachments }),
|
|
3122
3221
|
attemptId: nextAttemptId,
|
|
3123
3222
|
attemptNumber: prior.uncertain.attemptNumber + 1,
|
|
3124
3223
|
markedAt: timestamp,
|
|
@@ -3637,11 +3736,11 @@ async function createLease({
|
|
|
3637
3736
|
if (Object.hasOwn(step, 'result') && !['completion', 'answer'].includes(step.kind)) throw new Error('A step result requires its saved start');
|
|
3638
3737
|
progress.steps.push(step);
|
|
3639
3738
|
} else {
|
|
3640
|
-
const { result, ...start } = step;
|
|
3739
|
+
const { result, workerEvidence, ...start } = step;
|
|
3641
3740
|
const saved = progress.steps[index];
|
|
3642
|
-
const { result: priorResult, ...priorStart } = saved;
|
|
3741
|
+
const { result: priorResult, workerEvidence: priorEvidence, ...priorStart } = saved;
|
|
3643
3742
|
if (!isDeepStrictEqual(start, priorStart) ||
|
|
3644
|
-
(priorResult !== undefined && !isDeepStrictEqual(priorResult, result))) {
|
|
3743
|
+
(priorResult !== undefined && (!isDeepStrictEqual(priorResult, result) || !isDeepStrictEqual(priorEvidence, workerEvidence)))) {
|
|
3645
3744
|
throw new Error('A saved step cannot be replaced');
|
|
3646
3745
|
}
|
|
3647
3746
|
progress.steps[index] = step;
|
|
@@ -3728,6 +3827,11 @@ async function createLease({
|
|
|
3728
3827
|
sessionId,
|
|
3729
3828
|
ownerToken: owner.ownerToken,
|
|
3730
3829
|
append,
|
|
3830
|
+
importAsset: (input) => runExclusive(async () => { await assertOwnerUnchecked(); return assetStore.importAsset(input); }),
|
|
3831
|
+
resolveAttachments: (references, options) => runExclusive(async () => { await assertOwnerUnchecked(); if (!Array.isArray(references)) throw new TypeError('attachments must be an array'); return Promise.all(references.map((reference) => assetStore.resolveAttachment(reference, options))); }),
|
|
3832
|
+
readAsset: (reference, options) => assetStore.readAsset(reference, options),
|
|
3833
|
+
openAsset: (reference, options) => assetStore.openAsset(reference, options),
|
|
3834
|
+
describeAsset: (assetId) => assetStore.describeAsset(assetId),
|
|
3731
3835
|
recordContext,
|
|
3732
3836
|
consumeHints,
|
|
3733
3837
|
acknowledgeHint,
|
|
@@ -4089,7 +4193,7 @@ function validateCanonicalCaptainSessionRecord(
|
|
|
4089
4193
|
exactOptionalKeys(
|
|
4090
4194
|
uncertain,
|
|
4091
4195
|
UNCERTAIN_KEYS,
|
|
4092
|
-
['abandonment', 'progress'],
|
|
4196
|
+
['abandonment', 'progress', 'attachments'],
|
|
4093
4197
|
'Captain session record uncertain',
|
|
4094
4198
|
);
|
|
4095
4199
|
if (uncertain.baseUpdatedAt !== null) {
|
|
@@ -4110,7 +4214,7 @@ function validateCanonicalCaptainSessionRecord(
|
|
|
4110
4214
|
);
|
|
4111
4215
|
}
|
|
4112
4216
|
}
|
|
4113
|
-
assertAcceptedInput(uncertain.input);
|
|
4217
|
+
assertAcceptedInput(uncertain.input, uncertain.attachments);
|
|
4114
4218
|
assertUuid(uncertain.attemptId, 'Captain session attempt id');
|
|
4115
4219
|
if (
|
|
4116
4220
|
!Number.isSafeInteger(uncertain.attemptNumber) ||
|
|
@@ -4172,8 +4276,9 @@ function validateCanonicalCaptainSessionRecord(
|
|
|
4172
4276
|
exactOptionalKeys(progress, ['snapshot', 'steps'], ['positionStepId'], 'Captain progress');
|
|
4173
4277
|
if (!Array.isArray(progress.steps)) throw new Error('Captain steps must be an array');
|
|
4174
4278
|
const ids = new Set();
|
|
4279
|
+
const workerBoundaryIds = new Set();
|
|
4175
4280
|
for (const step of progress.steps) {
|
|
4176
|
-
exactOptionalKeys(requireRecord(step, 'Captain step'), ['id', 'kind', 'stateId', 'runtimeSessionId', 'playbookId'], ['result'], 'Captain step');
|
|
4281
|
+
exactOptionalKeys(requireRecord(step, 'Captain step'), ['id', 'kind', 'stateId', 'runtimeSessionId', 'playbookId'], ['result', 'workerEvidence'], 'Captain step');
|
|
4177
4282
|
assertUuid(step.id, 'Captain step id');
|
|
4178
4283
|
assertUuid(step.runtimeSessionId, 'Captain step runtime');
|
|
4179
4284
|
if (ids.has(step.id)) throw new Error('Captain step ids must be unique');
|
|
@@ -4209,6 +4314,22 @@ function validateCanonicalCaptainSessionRecord(
|
|
|
4209
4314
|
}
|
|
4210
4315
|
requireCanonicalNonblank(step.stateId, 'Captain step state');
|
|
4211
4316
|
if (!Object.hasOwn(structural.catalog, step.playbookId)) throw new Error('Captain step names an unknown playbook');
|
|
4317
|
+
if (step.workerEvidence !== undefined) {
|
|
4318
|
+
const evidence = requireRecord(step.workerEvidence, 'Worker evidence');
|
|
4319
|
+
exactOptionalKeys(evidence, ['boundaryId', 'playerId'], [], 'Worker evidence');
|
|
4320
|
+
assertUuid(evidence.boundaryId, 'Worker evidence boundary');
|
|
4321
|
+
requireCanonicalNonblank(evidence.playerId, 'Worker evidence player');
|
|
4322
|
+
const boundary = record.effectLedger.boundaries.find((entry) => entry.boundaryId === evidence.boundaryId);
|
|
4323
|
+
if (step.kind !== 'player' || !step.result || typeof step.result !== 'object' || Array.isArray(step.result) ||
|
|
4324
|
+
!boundary || boundary.playbookId !== step.playbookId || boundary.sourceStateId !== step.stateId ||
|
|
4325
|
+
workerBoundaryIds.has(evidence.boundaryId) || isDeepStrictEqual(boundary, record.snapshot.effectLedger.boundaries[boundary.sequence - 1]) ||
|
|
4326
|
+
!boundary.physicalReceipt || !boundary.finalText?.trim() ||
|
|
4327
|
+
boundary.semanticCandidate?.guard !== step.result.guard || typeof step.result.guard !== 'string' || step.result.guard === 'needsBossReply' ||
|
|
4328
|
+
structural.catalog[step.playbookId]?.roles?.[boundary.roleId]?.playerId !== evidence.playerId) {
|
|
4329
|
+
throw new Error('Worker evidence must name its accepted player boundary');
|
|
4330
|
+
}
|
|
4331
|
+
workerBoundaryIds.add(evidence.boundaryId);
|
|
4332
|
+
}
|
|
4212
4333
|
}
|
|
4213
4334
|
if (progress.positionStepId !== undefined && progress.positionStepId !== null && !ids.has(progress.positionStepId)) throw new Error('Captain progress positionStepId must name a recorded step');
|
|
4214
4335
|
if (progress.snapshot === null && progress.positionStepId != null) throw new Error('Captain progress positionStepId requires a saved position');
|
|
@@ -5312,7 +5433,7 @@ function assertReleasedSchema2CaptainSessionRecord(record) {
|
|
|
5312
5433
|
);
|
|
5313
5434
|
}
|
|
5314
5435
|
}
|
|
5315
|
-
assertAcceptedInput(uncertain.input);
|
|
5436
|
+
assertAcceptedInput(uncertain.input, uncertain.attachments);
|
|
5316
5437
|
assertUuid(uncertain.attemptId, 'Captain session attempt id');
|
|
5317
5438
|
if (
|
|
5318
5439
|
!Number.isSafeInteger(uncertain.attemptNumber) ||
|
|
@@ -5402,7 +5523,14 @@ function validateCaptainSessionProjection(
|
|
|
5402
5523
|
: ['id', 'adapter', 'model', 'effort'],
|
|
5403
5524
|
structural
|
|
5404
5525
|
? ['instruction', 'permissions']
|
|
5405
|
-
: [
|
|
5526
|
+
: [
|
|
5527
|
+
'instruction',
|
|
5528
|
+
'permissions',
|
|
5529
|
+
'fastMode',
|
|
5530
|
+
'subagentModel',
|
|
5531
|
+
'subagentEffort',
|
|
5532
|
+
'browser',
|
|
5533
|
+
],
|
|
5406
5534
|
playerPath,
|
|
5407
5535
|
);
|
|
5408
5536
|
assertPlayerId(player.id, `${playerPath}.id`);
|
|
@@ -5503,16 +5631,13 @@ function validateCaptainSessionProjection(
|
|
|
5503
5631
|
structural
|
|
5504
5632
|
? ['playerId']
|
|
5505
5633
|
: ['playerId', 'model', 'effort'],
|
|
5506
|
-
structural ? [] : ['fastMode', 'subagentModel'],
|
|
5634
|
+
structural ? [] : ['fastMode', 'subagentModel', 'subagentEffort'],
|
|
5507
5635
|
bindingPath,
|
|
5508
5636
|
);
|
|
5509
5637
|
assertPlayerId(binding.playerId, `${bindingPath}.playerId`);
|
|
5510
5638
|
if (!structural) {
|
|
5511
5639
|
validateProjectedFastMode(binding, `${bindingPath}.fastMode`);
|
|
5512
|
-
|
|
5513
|
-
binding,
|
|
5514
|
-
`${bindingPath}.subagentModel`,
|
|
5515
|
-
);
|
|
5640
|
+
validateProjectedSubagentTuning(binding, bindingPath);
|
|
5516
5641
|
validateTuningSelection(binding.model, `${bindingPath}.model`);
|
|
5517
5642
|
const player = projection.players.find(
|
|
5518
5643
|
(candidate) => candidate.id === binding.playerId,
|
|
@@ -5560,10 +5685,14 @@ function validateProjectedFastMode(record, path) {
|
|
|
5560
5685
|
}
|
|
5561
5686
|
}
|
|
5562
5687
|
|
|
5563
|
-
// Like fast mode, absence is the canonical provider default (DR-075)
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5688
|
+
// Like fast mode, absence is the canonical provider default (DR-075); the
|
|
5689
|
+
// subagent model may be the literal `inherit`, and an absent subagent effort
|
|
5690
|
+
// leaves each subagent's effort to the agent (DR-076).
|
|
5691
|
+
function validateProjectedSubagentTuning(record, path) {
|
|
5692
|
+
for (const field of ['subagentModel', 'subagentEffort']) {
|
|
5693
|
+
if (Object.hasOwn(record, field)) {
|
|
5694
|
+
requireNonblank(record[field], `${path}.${field}`);
|
|
5695
|
+
}
|
|
5567
5696
|
}
|
|
5568
5697
|
}
|
|
5569
5698
|
|
|
@@ -5571,7 +5700,8 @@ function validateProjectedAgent(value, path, { structural, hasId = false }) {
|
|
|
5571
5700
|
const agent = requireRecord(value, path);
|
|
5572
5701
|
if (!structural) {
|
|
5573
5702
|
validateProjectedFastMode(agent, `${path}.fastMode`);
|
|
5574
|
-
|
|
5703
|
+
if (agent.browser !== undefined && typeof agent.browser !== 'boolean') throw new Error(`${path}.browser must be a boolean`);
|
|
5704
|
+
validateProjectedSubagentTuning(agent, path);
|
|
5575
5705
|
}
|
|
5576
5706
|
if (!hasId) {
|
|
5577
5707
|
exactOptionalKeys(
|
|
@@ -5579,7 +5709,14 @@ function validateProjectedAgent(value, path, { structural, hasId = false }) {
|
|
|
5579
5709
|
structural ? ['adapter'] : ['adapter', 'model', 'effort'],
|
|
5580
5710
|
structural
|
|
5581
5711
|
? ['instruction', 'permissions']
|
|
5582
|
-
: [
|
|
5712
|
+
: [
|
|
5713
|
+
'instruction',
|
|
5714
|
+
'permissions',
|
|
5715
|
+
'fastMode',
|
|
5716
|
+
'subagentModel',
|
|
5717
|
+
'subagentEffort',
|
|
5718
|
+
'browser',
|
|
5719
|
+
],
|
|
5583
5720
|
path,
|
|
5584
5721
|
);
|
|
5585
5722
|
}
|
|
@@ -6131,9 +6268,10 @@ function validateRetainedGenerationFrame(
|
|
|
6131
6268
|
'roleBindings',
|
|
6132
6269
|
'runtime',
|
|
6133
6270
|
],
|
|
6134
|
-
['parentSessionId', 'parentCallId', 'request', 'inputs'],
|
|
6271
|
+
['parentSessionId', 'parentCallId', 'request', 'inputs', 'attachments'],
|
|
6135
6272
|
path,
|
|
6136
6273
|
);
|
|
6274
|
+
if (frame.attachments !== undefined) validateAssetRefs(frame.attachments);
|
|
6137
6275
|
if (frame.request !== undefined) {
|
|
6138
6276
|
if (index !== 0) throw new Error(`${path}.request belongs only to the root`);
|
|
6139
6277
|
requireNonblank(frame.request, `${path}.request`);
|
|
@@ -6356,8 +6494,9 @@ async function requireUncertainRecord(record, attemptId) {
|
|
|
6356
6494
|
return record;
|
|
6357
6495
|
}
|
|
6358
6496
|
|
|
6359
|
-
function assertAcceptedInput(value) {
|
|
6360
|
-
if (
|
|
6497
|
+
function assertAcceptedInput(value, attachments) {
|
|
6498
|
+
if (attachments !== undefined) validateAssetRefs(attachments);
|
|
6499
|
+
if (typeof value !== 'string' || (value.trim().length === 0 && !attachments?.length)) {
|
|
6361
6500
|
throw new Error('Captain session input must be a non-empty string');
|
|
6362
6501
|
}
|
|
6363
6502
|
}
|
|
@@ -3,21 +3,28 @@ import type { Effort, PermissionPolicy } from '@sublang/cligent';
|
|
|
3
3
|
import type { JsonValue, PlaybookEffectLedger, PlaybookEffectLedgerCommandBatch, PlaybookControlAction, PlaybookFailureCause, PlaybookRuntime, PlaybookRuntimeSnapshot, PlaybookStepRecord } from '@sublang/playbook/runtime';
|
|
4
4
|
import { type CaptainControllerPort } from '../captain.playbook/captain.playbook.js';
|
|
5
5
|
import type { PlaybookSummaryPolicy } from './code.registry.js';
|
|
6
|
+
import type { Attachment } from '@sublang/cligent';
|
|
7
|
+
import type { SessionAssetRef } from './session-assets.js';
|
|
6
8
|
interface SessionAgent {
|
|
9
|
+
/** Mutable next-turn execution capability; never structural identity. */
|
|
10
|
+
readonly browser?: boolean;
|
|
7
11
|
readonly adapter: string;
|
|
8
12
|
readonly model: TuningSelection;
|
|
9
13
|
readonly effort: TuningSelection<Effort>;
|
|
10
14
|
/** Adapter-scoped fast mode. Absence is the provider default; `false` is a
|
|
11
15
|
* literal request, so this carries no provider-default sentinel. */
|
|
12
16
|
readonly fastMode?: boolean;
|
|
13
|
-
/** The model every subagent of this agent runs on (DR-075)
|
|
14
|
-
* provider default. */
|
|
17
|
+
/** The model every subagent of this agent runs on (DR-075), or `inherit`
|
|
18
|
+
* for the agent's own model (DR-076). Absence is the provider default. */
|
|
15
19
|
readonly subagentModel?: string;
|
|
20
|
+
/** The effort every subagent of this agent runs at (DR-076). Absence leaves
|
|
21
|
+
* each subagent's effort to the agent. */
|
|
22
|
+
readonly subagentEffort?: string;
|
|
16
23
|
readonly instruction?: string;
|
|
17
24
|
readonly permissions?: PermissionPolicy;
|
|
18
25
|
}
|
|
19
26
|
/** Per-call tuning: erased from every structural envelope and snapshot. */
|
|
20
|
-
type TuningKey = 'model' | 'effort' | 'fastMode' | 'subagentModel';
|
|
27
|
+
type TuningKey = 'model' | 'effort' | 'fastMode' | 'subagentModel' | 'subagentEffort';
|
|
21
28
|
interface PlayerLedgerEntry {
|
|
22
29
|
readonly adapter: string;
|
|
23
30
|
readonly instruction?: string;
|
|
@@ -37,7 +44,7 @@ interface PlaybookCaptainUnresolvedEffectSettlementInput {
|
|
|
37
44
|
readonly rootPlaybookId: string;
|
|
38
45
|
readonly unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[];
|
|
39
46
|
}
|
|
40
|
-
type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, TuningKey>>;
|
|
47
|
+
type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, TuningKey | 'browser'>>;
|
|
41
48
|
type PlayerLedgerSnapshotEntry = DeepReadonly<PlayerLedgerEntry>;
|
|
42
49
|
export interface ProgressChange {
|
|
43
50
|
snapshot?: PlaybookCaptainShellSnapshot | null;
|
|
@@ -45,6 +52,10 @@ export interface ProgressChange {
|
|
|
45
52
|
kind: PlaybookStepRecord['kind'] | 'preparation' | 'completion' | 'answer';
|
|
46
53
|
runtimeSessionId: string;
|
|
47
54
|
playbookId: string;
|
|
55
|
+
workerEvidence?: {
|
|
56
|
+
boundaryId: string;
|
|
57
|
+
playerId: string;
|
|
58
|
+
};
|
|
48
59
|
};
|
|
49
60
|
}
|
|
50
61
|
export interface InterruptedReport {
|
|
@@ -54,6 +65,9 @@ export interface InterruptedReport {
|
|
|
54
65
|
unresolvedEffects?: readonly PlaybookCaptainUnresolvedEffect[];
|
|
55
66
|
}
|
|
56
67
|
export interface PlaybookCaptainDeps {
|
|
68
|
+
onTurnAttachments?: (references: readonly SessionAssetRef[]) => void;
|
|
69
|
+
beginPreparationMedia?: (runtimeSessionId: string) => () => void;
|
|
70
|
+
resolveAttachments?: (references: readonly SessionAssetRef[], signal: AbortSignal) => Promise<readonly Attachment[]>;
|
|
57
71
|
/** Stop the host's active turn, including admitted tool calls, on preparation expiry or a required save failure. */
|
|
58
72
|
abortPreparation?: (reason?: string) => void;
|
|
59
73
|
recordProgress?: (change: ProgressChange) => Promise<void>;
|
|
@@ -161,6 +175,7 @@ export interface PlaybookCaptainFrameSnapshot {
|
|
|
161
175
|
readonly parentCallId?: string;
|
|
162
176
|
readonly request?: string;
|
|
163
177
|
readonly inputs?: readonly string[];
|
|
178
|
+
readonly attachments?: readonly SessionAssetRef[];
|
|
164
179
|
readonly options: JsonValue;
|
|
165
180
|
readonly roleBindings: Readonly<Record<string, string>>;
|
|
166
181
|
readonly runtime: DeepReadonly<PlaybookRuntimeSnapshot>;
|
|
@@ -183,6 +198,7 @@ interface PlaybookCaptainShellSnapshotFields {
|
|
|
183
198
|
readonly journal: number;
|
|
184
199
|
};
|
|
185
200
|
readonly journal: readonly PlaybookCaptainJournalRecord[];
|
|
201
|
+
readonly pendingAttachments?: readonly SessionAssetRef[];
|
|
186
202
|
readonly lastAction?: 'respond' | 'start' | 'switch' | 'resume' | 'dismiss' | 'deliver' | 'runtime' | 'recover';
|
|
187
203
|
readonly lastSettlementStatus?: 'ok' | 'rejected' | 'failed';
|
|
188
204
|
}
|
|
@@ -238,6 +254,9 @@ export interface PlaybookCaptainSettlement {
|
|
|
238
254
|
}
|
|
239
255
|
/** tmux and headless front ends share this one durable Captain shell API. */
|
|
240
256
|
export interface PlaybookCaptainShell extends Captain {
|
|
257
|
+
/** Bind already-owned evidence to the next visible Boss turn without changing its text. */
|
|
258
|
+
setTurnAttachments(references: readonly SessionAssetRef[]): void;
|
|
259
|
+
recordMediaEvidence(reference: SessionAssetRef, runtimeSessionId?: string): void;
|
|
241
260
|
installRetainedGenerations(generations: Readonly<Record<string, PlaybookCaptainRetainedGeneration>>): Promise<void>;
|
|
242
261
|
exportSnapshot(): PlaybookCaptainShellSnapshot | undefined;
|
|
243
262
|
exportSettlement(): PlaybookCaptainSettlement | undefined;
|