tinker-agent 2.8.0 → 2.10.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/CHANGELOG.md +79 -1
- package/README.md +81 -11
- package/package.json +5 -3
- package/src/agent/runtime-context-capabilities.ts +19 -0
- package/src/agent/runtime-context-events.ts +127 -0
- package/src/agent/runtime-context-maintenance.ts +780 -0
- package/src/agent/runtime-hosted-session.ts +443 -0
- package/src/agent/runtime-interactions.ts +291 -0
- package/src/agent/runtime-prompt-scheduler.ts +182 -0
- package/src/agent/runtime-session-contracts.ts +317 -0
- package/src/agent/runtime-session.ts +250 -2130
- package/src/agent/runtime-skills.ts +544 -0
- package/src/cli/command-line.ts +26 -2
- package/src/cli/connect-runner.tsx +26 -0
- package/src/cli/main.ts +26 -0
- package/src/cli/output.ts +1 -1
- package/src/cli/public-cli-contract.ts +18 -0
- package/src/cli/public-config-contract.ts +1 -1
- package/src/cli/runner-dependencies.ts +6 -5
- package/src/cli/serve-runner.ts +45 -0
- package/src/cli/serve-runtime.ts +100 -0
- package/src/context/context-automation-policy.ts +12 -118
- package/src/context/context-swap-renderer.ts +14 -0
- package/src/events/types.ts +12 -0
- package/src/memory/memory-get-tool.ts +1 -1
- package/src/observation/observation-builder.ts +128 -48
- package/src/remote/client.ts +350 -0
- package/src/remote/config.ts +95 -0
- package/src/remote/http-server.ts +240 -0
- package/src/remote/protocol.ts +228 -0
- package/src/remote/service-store.ts +175 -0
- package/src/remote/service.ts +219 -0
- package/src/remote/sync-hub.ts +95 -0
- package/src/session/remote-history-reader.ts +143 -0
- package/src/session/resume-projection.ts +47 -21
- package/src/session/session-history-access.ts +238 -0
- package/src/session/session-store-context-readers.ts +183 -0
- package/src/session/session-store-ledger-writer.ts +315 -0
- package/src/session/session-store-record-writer.ts +318 -0
- package/src/session/session-store-recovery.ts +225 -0
- package/src/session/session-store-revisions.ts +1004 -0
- package/src/session/session-store-sql.ts +40 -0
- package/src/session/session-store-validation.ts +657 -0
- package/src/session/session-store.ts +756 -3186
- package/src/tools/bash-task.ts +46 -18
- package/src/tools/bash.ts +44 -2
- package/src/tools/glob.ts +107 -19
- package/src/tools/grep-output.ts +130 -0
- package/src/tools/grep-pagination.ts +73 -0
- package/src/tools/grep-path.ts +11 -0
- package/src/tools/grep-snippets.ts +111 -0
- package/src/tools/grep.ts +139 -154
- package/src/tools/read.ts +0 -9
- package/src/tools/recall.ts +106 -50
- package/src/tools/registry.ts +4 -6
- package/src/tools/ripgrep.ts +19 -26
- package/src/tools/shell-process.ts +30 -4
- package/src/tools/task-output-range.ts +146 -0
- package/src/tools/task-output-tool.ts +35 -5
- package/src/tools/task-output.ts +35 -0
- package/src/tools/task-stop.ts +2 -1
- package/src/tools/task-tool-args.ts +34 -0
- package/src/tools/terminal-screen.ts +11 -2
- package/src/tools/types.ts +39 -2
- package/src/tui/event-store.ts +23 -5
- package/src/tui/remote-app.tsx +210 -0
|
@@ -1,84 +1,56 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { chmod, mkdir, open, readdir, rename, rmdir } from "node:fs/promises";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
1
|
import { Database } from "bun:sqlite";
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
MessageId,
|
|
10
|
-
RuntimeIdFactory,
|
|
11
|
-
SessionId,
|
|
12
|
-
TurnId,
|
|
13
|
-
} from "../ids/runtime-id";
|
|
14
|
-
import { sha256, stableJsonStringify } from "../model/model-request-preflight";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { chmod, mkdir, open, readdir, rename, rmdir } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { MeasuredContextAnchor } from "../agent/context-meter";
|
|
15
6
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from "
|
|
7
|
+
InMemorySessionLedger,
|
|
8
|
+
type LedgerMutation,
|
|
9
|
+
type SessionLedgerCommitter,
|
|
10
|
+
} from "../agent/session-ledger";
|
|
11
|
+
import type { IterationIdentity } from "../agent/types";
|
|
20
12
|
import {
|
|
21
13
|
ContextProtocolError,
|
|
22
14
|
ContextProtocolValidator,
|
|
23
15
|
} from "../context/context-protocol-validator";
|
|
24
|
-
import {
|
|
25
|
-
activeOverrideManifestHash,
|
|
26
|
-
canonicalSequenceHash,
|
|
27
|
-
renderedMessageHash,
|
|
28
|
-
} from "../context/compiled-context-hash";
|
|
29
|
-
import {
|
|
30
|
-
ContextRevisionCompiler,
|
|
31
|
-
createInitialContextRevision,
|
|
32
|
-
} from "../context/context-revision-compiler";
|
|
33
|
-
import {
|
|
34
|
-
ContextSwapRenderer,
|
|
35
|
-
SWAP_OBSERVATION_FORMAT,
|
|
36
|
-
SWAP_TOOL_IMAGE_FORMAT,
|
|
37
|
-
} from "../context/context-swap-renderer";
|
|
38
|
-
import {
|
|
39
|
-
contentHash,
|
|
40
|
-
immutableRecord,
|
|
41
|
-
interruptedCompletionInputs,
|
|
42
|
-
observationForCompletion,
|
|
43
|
-
userMessageHash,
|
|
44
|
-
type CanonicalMessageRecord,
|
|
45
|
-
type ProtocolContextView,
|
|
46
|
-
type ProtocolFrame,
|
|
47
|
-
type ToolCompletion,
|
|
48
|
-
type ToolResultRecord,
|
|
49
|
-
} from "../context/protocol-frame";
|
|
50
|
-
import {
|
|
51
|
-
contextSurfaceChangeManifestHash,
|
|
52
|
-
contextSurfaceChanges,
|
|
53
|
-
validateStoredContextSurface,
|
|
54
|
-
type StoredContextSurfaceV8,
|
|
55
|
-
} from "../context/context-surface";
|
|
56
16
|
import type {
|
|
57
17
|
StoredContextRevisionV8,
|
|
58
18
|
StoredContextSnapshotV8,
|
|
59
|
-
StoredContextOverrideV8,
|
|
60
|
-
SwapOverride,
|
|
61
19
|
} from "../context/context-revision";
|
|
62
|
-
import
|
|
20
|
+
import { createInitialContextRevision } from "../context/context-revision-compiler";
|
|
63
21
|
import {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
} from "../agent/tool-result-content";
|
|
68
|
-
import { validateUserMessage, type ImageAssetRef } from "../image/image-types";
|
|
69
|
-
import { ImageAssetStore } from "../image/image-asset-store";
|
|
70
|
-
import type { MeasuredContextAnchor } from "../agent/context-meter";
|
|
71
|
-
import type { ProjectInstructionManifest } from "../instructions/project-instructions";
|
|
22
|
+
validateStoredContextSurface,
|
|
23
|
+
type StoredContextSurfaceV8,
|
|
24
|
+
} from "../context/context-surface";
|
|
72
25
|
import type {
|
|
73
26
|
ActiveTurnBoundary,
|
|
74
27
|
ClosedTurnBoundary,
|
|
75
28
|
} from "../context/prefix-retirement-planner";
|
|
29
|
+
import { contentHash, type ProtocolContextView } from "../context/protocol-frame";
|
|
30
|
+
import type {
|
|
31
|
+
ContextRevisionId,
|
|
32
|
+
IterationId,
|
|
33
|
+
MessageId,
|
|
34
|
+
RuntimeIdFactory,
|
|
35
|
+
SessionId,
|
|
36
|
+
TurnId,
|
|
37
|
+
} from "../ids/runtime-id";
|
|
38
|
+
import { ImageAssetStore } from "../image/image-asset-store";
|
|
39
|
+
import { type ImageAssetRef } from "../image/image-types";
|
|
40
|
+
import type { ProjectInstructionManifest } from "../instructions/project-instructions";
|
|
41
|
+
import { sha256, stableJsonStringify } from "../model/model-request-preflight";
|
|
76
42
|
import {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
43
|
+
cloneDiagnosticFiles,
|
|
44
|
+
rekeyProtocolView,
|
|
45
|
+
rekeyStoredToolCalls,
|
|
46
|
+
rewriteCloneRevisionHashes,
|
|
47
|
+
SESSION_SCOPED_TABLES,
|
|
48
|
+
} from "./session-clone-helpers";
|
|
49
|
+
import {
|
|
50
|
+
compatibilityContractDifferences,
|
|
51
|
+
decodeMeta,
|
|
52
|
+
normalizeSessionCompatibilityContract,
|
|
53
|
+
} from "./session-compatibility-codec";
|
|
82
54
|
import { SessionError, sessionOpenError, sessionWriteError } from "./session-errors";
|
|
83
55
|
import {
|
|
84
56
|
createSessionHistoryReader,
|
|
@@ -86,59 +58,26 @@ import {
|
|
|
86
58
|
} from "./session-history-reader";
|
|
87
59
|
import { SessionLease } from "./session-lock";
|
|
88
60
|
import {
|
|
89
|
-
SESSION_SCHEMA_V10_FINGERPRINT,
|
|
90
|
-
SESSION_SCHEMA_VERSION,
|
|
91
|
-
upgradeActiveTurnRetirementContract,
|
|
92
|
-
upgradeRecallIndexContract,
|
|
93
61
|
configureWritableDatabase,
|
|
94
62
|
createSessionSchema,
|
|
95
63
|
dropSessionCloneTriggers,
|
|
96
64
|
rebuildRecallIndex,
|
|
97
65
|
reinstallSessionCloneTriggers,
|
|
66
|
+
SESSION_SCHEMA_V10_FINGERPRINT,
|
|
67
|
+
SESSION_SCHEMA_VERSION,
|
|
68
|
+
upgradeActiveTurnRetirementContract,
|
|
69
|
+
upgradeRecallIndexContract,
|
|
98
70
|
verifyReadableSessionSchema,
|
|
99
71
|
verifyRecallIndex,
|
|
100
72
|
verifySessionSchema,
|
|
101
73
|
verifySqliteIntegrity,
|
|
102
74
|
} from "./session-schema";
|
|
103
75
|
import {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
} from "
|
|
108
|
-
import {
|
|
109
|
-
assertMeasuredContextAnchor,
|
|
110
|
-
enumFromSql,
|
|
111
|
-
nullableStringFromSql,
|
|
112
|
-
nullableTextFromSql,
|
|
113
|
-
numberFromSql,
|
|
114
|
-
recordFromSql,
|
|
115
|
-
sha256FromSql,
|
|
116
|
-
stringFromSql,
|
|
117
|
-
timestampFromSql,
|
|
118
|
-
} from "./session-store-value-codecs";
|
|
119
|
-
export { decodeStoredToolRawResult } from "./session-tool-result-codec";
|
|
120
|
-
import {
|
|
121
|
-
cloneDiagnosticFiles,
|
|
122
|
-
rekeyProtocolView,
|
|
123
|
-
rekeyStoredToolCalls,
|
|
124
|
-
rewriteCloneRevisionHashes,
|
|
125
|
-
SESSION_SCOPED_TABLES,
|
|
126
|
-
} from "./session-clone-helpers";
|
|
127
|
-
import {
|
|
128
|
-
assertPathMissing,
|
|
129
|
-
canonicalWorkspaceRoot,
|
|
130
|
-
chmodIfExists,
|
|
131
|
-
ensureSessionsRoot,
|
|
132
|
-
removeKnownInitializationFiles,
|
|
133
|
-
safeSessionDirectory,
|
|
134
|
-
unlinkIfExists,
|
|
135
|
-
validateSecureDirectory,
|
|
136
|
-
validateSecureFile,
|
|
137
|
-
validateSecureOptionalFile,
|
|
138
|
-
validateSessionsRoot,
|
|
139
|
-
} from "./session-store-filesystem";
|
|
76
|
+
loadMeasuredContextState,
|
|
77
|
+
readRetirementBoundaries,
|
|
78
|
+
requireActiveRevisionId,
|
|
79
|
+
} from "./session-store-context-readers";
|
|
140
80
|
import {
|
|
141
|
-
skillActivationManifestSha256,
|
|
142
81
|
type CloneSessionStoreInput,
|
|
143
82
|
type CommitPrefixRetirementRevisionInput,
|
|
144
83
|
type CommitPrefixRetirementRevisionOptions,
|
|
@@ -155,43 +94,72 @@ import {
|
|
|
155
94
|
type SessionCloseReason,
|
|
156
95
|
type SessionCompatibilityContract,
|
|
157
96
|
type SessionRecoveryResult,
|
|
158
|
-
type StoredMeasuredContextState,
|
|
159
97
|
type StoredSessionMetaV10,
|
|
160
98
|
type StoredSkillActivation,
|
|
161
99
|
} from "./session-store-contracts";
|
|
162
|
-
export * from "./session-store-contracts";
|
|
163
|
-
export { createSessionCompatibilityContract } from "./session-compatibility-codec";
|
|
164
100
|
import {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
101
|
+
assertPathMissing,
|
|
102
|
+
canonicalWorkspaceRoot,
|
|
103
|
+
chmodIfExists,
|
|
104
|
+
ensureSessionsRoot,
|
|
105
|
+
removeKnownInitializationFiles,
|
|
106
|
+
safeSessionDirectory,
|
|
107
|
+
unlinkIfExists,
|
|
108
|
+
validateSecureDirectory,
|
|
109
|
+
validateSecureFile,
|
|
110
|
+
validateSecureOptionalFile,
|
|
111
|
+
validateSessionsRoot,
|
|
112
|
+
} from "./session-store-filesystem";
|
|
113
|
+
import { SessionStoreLedgerWriter } from "./session-store-ledger-writer";
|
|
170
114
|
import {
|
|
171
|
-
decodeContextRevision,
|
|
172
|
-
decodeContextSurface,
|
|
173
115
|
decodeFrame,
|
|
174
116
|
decodeMessage,
|
|
175
117
|
decodeSkillActivation,
|
|
176
|
-
decodeStoredSwapOverride,
|
|
177
|
-
imageAssetRefFromAttachment,
|
|
178
118
|
decodeToolResult,
|
|
119
|
+
imageAssetRefFromAttachment,
|
|
179
120
|
loadMessageImageAttachments,
|
|
180
121
|
loadToolMessageContentBlocks,
|
|
181
|
-
protocolPrefixView,
|
|
182
|
-
stripStoredOverride,
|
|
183
122
|
} from "./session-store-record-codecs";
|
|
123
|
+
import {
|
|
124
|
+
insertContextSurface,
|
|
125
|
+
insertFrame,
|
|
126
|
+
insertMessage,
|
|
127
|
+
} from "./session-store-record-writer";
|
|
128
|
+
import { SessionStoreRecovery } from "./session-store-recovery";
|
|
129
|
+
import { SessionStoreRevisions } from "./session-store-revisions";
|
|
130
|
+
import { requireItem, requireSingleChange, runTransaction } from "./session-store-sql";
|
|
131
|
+
import { SessionStoreValidation } from "./session-store-validation";
|
|
132
|
+
import {
|
|
133
|
+
assertMeasuredContextAnchor,
|
|
134
|
+
enumFromSql,
|
|
135
|
+
nullableStringFromSql,
|
|
136
|
+
nullableTextFromSql,
|
|
137
|
+
numberFromSql,
|
|
138
|
+
recordFromSql,
|
|
139
|
+
stringFromSql,
|
|
140
|
+
} from "./session-store-value-codecs";
|
|
141
|
+
import {
|
|
142
|
+
canonicalHomeRoot,
|
|
143
|
+
resolveWorkspaceStorageRoot,
|
|
144
|
+
workspaceStorageRoot,
|
|
145
|
+
} from "./workspace-storage";
|
|
146
|
+
export { createSessionCompatibilityContract } from "./session-compatibility-codec";
|
|
147
|
+
export * from "./session-store-contracts";
|
|
148
|
+
export { decodeStoredToolCalls } from "./session-store-record-codecs";
|
|
149
|
+
export { decodeStoredToolRawResult } from "./session-tool-result-codec";
|
|
184
150
|
|
|
185
151
|
export class SessionStore implements SessionLedgerCommitter {
|
|
186
152
|
readonly sessionId: SessionId;
|
|
187
153
|
readonly workspaceRoot: string;
|
|
188
154
|
readonly sessionDirectory: string;
|
|
189
155
|
readonly databasePath: string;
|
|
156
|
+
private readonly validation: SessionStoreValidation;
|
|
157
|
+
private readonly recovery: SessionStoreRecovery;
|
|
158
|
+
private readonly revisions: SessionStoreRevisions;
|
|
159
|
+
private readonly ledgerWriter: SessionStoreLedgerWriter;
|
|
190
160
|
private closed = false;
|
|
191
161
|
private recallIndexRebuilt = false;
|
|
192
162
|
private readonly validator = new ContextProtocolValidator();
|
|
193
|
-
private readonly revisionCompiler = new ContextRevisionCompiler();
|
|
194
|
-
private readonly swapRenderer = new ContextSwapRenderer();
|
|
195
163
|
|
|
196
164
|
private constructor(
|
|
197
165
|
private readonly database: Database,
|
|
@@ -211,12 +179,84 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
211
179
|
this.databasePath = input.databasePath;
|
|
212
180
|
this.clock = input.clock;
|
|
213
181
|
this.homeRoot = input.homeRoot;
|
|
182
|
+
this.validation = new SessionStoreValidation(database, this.sessionId, (states) =>
|
|
183
|
+
this.loadSkillActivations(states),
|
|
184
|
+
);
|
|
185
|
+
this.revisions = new SessionStoreRevisions(
|
|
186
|
+
database,
|
|
187
|
+
this.sessionId,
|
|
188
|
+
this.clock,
|
|
189
|
+
{
|
|
190
|
+
loadContextSnapshot: () => this.loadContextSnapshot(),
|
|
191
|
+
assertContextRevisionBoundary: (turnId) =>
|
|
192
|
+
this.assertContextRevisionBoundary(turnId),
|
|
193
|
+
assertContextRevisionIdle: () => this.assertContextRevisionIdle(),
|
|
194
|
+
readMeta: () => this.readMeta(),
|
|
195
|
+
loadSkillActivations: (states) => this.loadSkillActivations(states),
|
|
196
|
+
},
|
|
197
|
+
() => this.requireOpen(),
|
|
198
|
+
(overrides, canonical) =>
|
|
199
|
+
this.validation.validateAddedOverrides(overrides, canonical),
|
|
200
|
+
);
|
|
201
|
+
this.ledgerWriter = new SessionStoreLedgerWriter(
|
|
202
|
+
database,
|
|
203
|
+
this.sessionId,
|
|
204
|
+
this.clock,
|
|
205
|
+
() => this.requireOpen(),
|
|
206
|
+
{
|
|
207
|
+
readMeta: () => this.readMeta(),
|
|
208
|
+
loadContextSnapshot: () => this.loadContextSnapshot(),
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
this.recovery = new SessionStoreRecovery(
|
|
212
|
+
database,
|
|
213
|
+
this.sessionId,
|
|
214
|
+
this.clock,
|
|
215
|
+
() => this.requireOpen(),
|
|
216
|
+
this.ledgerWriter,
|
|
217
|
+
{
|
|
218
|
+
loadProtocolView: () => this.loadProtocolView(),
|
|
219
|
+
validateAll: (options) => this.validateAll(options),
|
|
220
|
+
},
|
|
221
|
+
);
|
|
214
222
|
}
|
|
215
223
|
|
|
216
224
|
private readonly homeRoot?: string;
|
|
217
225
|
|
|
218
226
|
private readonly clock: () => string;
|
|
219
227
|
|
|
228
|
+
commit(mutation: LedgerMutation): void {
|
|
229
|
+
this.ledgerWriter.commit(mutation);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
commitSwapRevision(
|
|
233
|
+
input: CommitSwapRevisionInput,
|
|
234
|
+
options: CommitSwapRevisionOptions = {},
|
|
235
|
+
): Extract<StoredContextRevisionV8, { kind: "swap_only" }> {
|
|
236
|
+
return this.revisions.commitSwapRevision(input, options);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
commitPrefixRetirementRevision(
|
|
240
|
+
input: CommitPrefixRetirementRevisionInput,
|
|
241
|
+
options: CommitPrefixRetirementRevisionOptions = {},
|
|
242
|
+
): Extract<StoredContextRevisionV8, { kind: "prefix_retirement" }> {
|
|
243
|
+
return this.revisions.commitPrefixRetirementRevision(input, options);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
commitSurfaceRefresh(
|
|
247
|
+
input: CommitSurfaceRefreshInput,
|
|
248
|
+
options: CommitSurfaceRefreshOptions = {},
|
|
249
|
+
): Extract<StoredContextRevisionV8, { kind: "surface_refresh" }> {
|
|
250
|
+
return this.revisions.commitSurfaceRefresh(input, options);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
commitSkillsUpdate(
|
|
254
|
+
input: CommitSkillsUpdateInput,
|
|
255
|
+
options: CommitSkillsUpdateOptions = {},
|
|
256
|
+
): Extract<StoredContextRevisionV8, { kind: "skills_update" }> {
|
|
257
|
+
return this.revisions.commitSkillsUpdate(input, options);
|
|
258
|
+
}
|
|
259
|
+
|
|
220
260
|
static async createNew(input: CreateNewSessionStoreInput): Promise<SessionStore> {
|
|
221
261
|
const clock = input.clock ?? (() => new Date().toISOString());
|
|
222
262
|
const workspaceRoot = await canonicalWorkspaceRoot(input.workspaceRoot);
|
|
@@ -421,43 +461,12 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
421
461
|
}
|
|
422
462
|
}
|
|
423
463
|
|
|
424
|
-
commit(mutation: LedgerMutation): void {
|
|
425
|
-
this.requireOpen();
|
|
426
|
-
const now = this.clock();
|
|
427
|
-
try {
|
|
428
|
-
runTransaction(this.database, () => {
|
|
429
|
-
switch (mutation.kind) {
|
|
430
|
-
case "begin_turn":
|
|
431
|
-
this.commitBeginTurn(mutation, now);
|
|
432
|
-
break;
|
|
433
|
-
case "append_steering_users":
|
|
434
|
-
this.commitSteeringUsers(mutation, now);
|
|
435
|
-
break;
|
|
436
|
-
case "append_assistant":
|
|
437
|
-
this.commitAssistant(mutation, now);
|
|
438
|
-
break;
|
|
439
|
-
case "commit_tool_completions":
|
|
440
|
-
this.commitToolCompletions(mutation, now);
|
|
441
|
-
break;
|
|
442
|
-
case "finish_turn":
|
|
443
|
-
this.commitFinishTurn(mutation, now);
|
|
444
|
-
break;
|
|
445
|
-
}
|
|
446
|
-
});
|
|
447
|
-
} catch (error) {
|
|
448
|
-
if (error instanceof AdmissionStaleError) {
|
|
449
|
-
throw error;
|
|
450
|
-
}
|
|
451
|
-
throw sessionWriteError(mutation.kind, this.sessionId, error);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
464
|
beginIteration(iteration: IterationIdentity): void {
|
|
456
465
|
this.requireOpen();
|
|
457
466
|
const now = this.clock();
|
|
458
467
|
try {
|
|
459
468
|
runTransaction(this.database, () => {
|
|
460
|
-
const turn = this.requireTurnRow(iteration.turnId);
|
|
469
|
+
const turn = this.ledgerWriter.requireTurnRow(iteration.turnId);
|
|
461
470
|
if (
|
|
462
471
|
turn.status !== "open" ||
|
|
463
472
|
numberFromSql(turn.next_iteration_number, "next_iteration_number") !==
|
|
@@ -519,7 +528,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
519
528
|
updated.changes,
|
|
520
529
|
"finish continuing iteration",
|
|
521
530
|
);
|
|
522
|
-
this.touch(now);
|
|
531
|
+
this.ledgerWriter.touch(now);
|
|
523
532
|
});
|
|
524
533
|
} catch (error) {
|
|
525
534
|
throw sessionWriteError("finish_iteration", this.sessionId, error);
|
|
@@ -695,7 +704,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
695
704
|
written.changes,
|
|
696
705
|
"write measured context anchor",
|
|
697
706
|
);
|
|
698
|
-
this.touch(now);
|
|
707
|
+
this.ledgerWriter.touch(now);
|
|
699
708
|
});
|
|
700
709
|
} catch (error) {
|
|
701
710
|
throw sessionWriteError("write_context_measurement", this.sessionId, error);
|
|
@@ -704,7 +713,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
704
713
|
|
|
705
714
|
readActiveMeasuredContextAnchor(): MeasuredContextAnchor | undefined {
|
|
706
715
|
this.requireOpen();
|
|
707
|
-
const state = this.
|
|
716
|
+
const state = loadMeasuredContextState(this.database, this.sessionId);
|
|
708
717
|
if (state === undefined) {
|
|
709
718
|
return undefined;
|
|
710
719
|
}
|
|
@@ -757,7 +766,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
757
766
|
this.assertContextRevisionIdle();
|
|
758
767
|
const canonical = this.loadProtocolView();
|
|
759
768
|
this.validator.validate(canonical, { fullIntegrity: true });
|
|
760
|
-
return this.
|
|
769
|
+
return readRetirementBoundaries(this.database, canonical).closedTurns;
|
|
761
770
|
}
|
|
762
771
|
|
|
763
772
|
loadRetirementBoundaries(activeTurnId?: TurnId): {
|
|
@@ -771,2670 +780,740 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
771
780
|
allowOpenTail: activeTurnId !== undefined,
|
|
772
781
|
fullIntegrity: true,
|
|
773
782
|
});
|
|
774
|
-
return this.
|
|
783
|
+
return readRetirementBoundaries(this.database, canonical, activeTurnId);
|
|
775
784
|
}
|
|
776
785
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
readonly closedTurns: readonly ClosedTurnBoundary[];
|
|
782
|
-
readonly activeTurn?: ActiveTurnBoundary;
|
|
783
|
-
} {
|
|
786
|
+
loadSkillActivations(
|
|
787
|
+
states?: readonly StoredSkillActivation["state"][],
|
|
788
|
+
): readonly StoredSkillActivation[] {
|
|
789
|
+
this.requireOpen();
|
|
784
790
|
const rows = this.database
|
|
785
|
-
.query(
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
["open", "completed", "failed", "cancelled", "interrupted"] as const,
|
|
797
|
-
"turn status",
|
|
798
|
-
);
|
|
799
|
-
const frames = canonical.frames.filter((frame) => frame.turnId === turnId);
|
|
800
|
-
const messages = canonical.messages.filter(
|
|
801
|
-
(message) => message.role !== "system" && message.turnId === turnId,
|
|
802
|
-
);
|
|
803
|
-
const firstMessage = messages[0];
|
|
804
|
-
const lastMessage = messages.at(-1);
|
|
805
|
-
if (status === "open") {
|
|
806
|
-
if (
|
|
807
|
-
activeTurnId === undefined ||
|
|
808
|
-
turnId !== activeTurnId ||
|
|
809
|
-
index !== rows.length - 1 ||
|
|
810
|
-
turnNumber !== index + 1 ||
|
|
811
|
-
messages.length < 1 ||
|
|
812
|
-
frames.length < 1 ||
|
|
813
|
-
firstMessage?.role !== "user" ||
|
|
814
|
-
firstMessage.ordinal !== expectedOrdinal ||
|
|
815
|
-
lastMessage?.ordinal !== canonical.messages.length ||
|
|
816
|
-
frames.some((frame) => frame.state !== "closed")
|
|
817
|
-
) {
|
|
818
|
-
throw new Error(`Turn ${turnId} has an invalid active boundary.`);
|
|
819
|
-
}
|
|
820
|
-
activeTurn = Object.freeze({
|
|
821
|
-
turnId,
|
|
822
|
-
turnNumber,
|
|
823
|
-
firstOrdinal: expectedOrdinal,
|
|
824
|
-
});
|
|
825
|
-
expectedOrdinal = canonical.messages.length + 1;
|
|
826
|
-
continue;
|
|
827
|
-
}
|
|
828
|
-
let nextFrameOrdinal = expectedOrdinal;
|
|
829
|
-
for (const frame of frames) {
|
|
830
|
-
if (
|
|
831
|
-
frame.state !== "closed" ||
|
|
832
|
-
frame.firstOrdinal !== nextFrameOrdinal ||
|
|
833
|
-
frame.lastOrdinal === undefined
|
|
834
|
-
) {
|
|
835
|
-
throw new Error(`Turn ${turnId} has an invalid closed frame boundary.`);
|
|
836
|
-
}
|
|
837
|
-
nextFrameOrdinal = frame.lastOrdinal + 1;
|
|
838
|
-
}
|
|
839
|
-
if (
|
|
840
|
-
turnNumber !== index + 1 ||
|
|
841
|
-
frames.length < 1 ||
|
|
842
|
-
messages.length < 1 ||
|
|
843
|
-
firstMessage?.role !== "user" ||
|
|
844
|
-
firstMessage.ordinal !== expectedOrdinal ||
|
|
845
|
-
lastMessage === undefined ||
|
|
846
|
-
nextFrameOrdinal !== lastMessage.ordinal + 1
|
|
847
|
-
) {
|
|
848
|
-
throw new Error(`Turn ${turnId} has an invalid canonical boundary.`);
|
|
849
|
-
}
|
|
850
|
-
boundaries.push(
|
|
851
|
-
Object.freeze({
|
|
852
|
-
turnId,
|
|
853
|
-
turnNumber,
|
|
854
|
-
status,
|
|
855
|
-
firstOrdinal: expectedOrdinal,
|
|
856
|
-
lastOrdinal: lastMessage.ordinal,
|
|
857
|
-
frameCount: frames.length,
|
|
858
|
-
messageCount: messages.length,
|
|
859
|
-
}),
|
|
860
|
-
);
|
|
861
|
-
expectedOrdinal = lastMessage.ordinal + 1;
|
|
862
|
-
}
|
|
863
|
-
if (expectedOrdinal !== canonical.messages.length + 1) {
|
|
864
|
-
throw new Error("Closed turn boundaries do not cover canonical history.");
|
|
865
|
-
}
|
|
866
|
-
if ((activeTurnId === undefined) !== (activeTurn === undefined)) {
|
|
867
|
-
throw new Error("Active retirement boundary does not match the open turn.");
|
|
868
|
-
}
|
|
869
|
-
return Object.freeze({
|
|
870
|
-
closedTurns: Object.freeze(boundaries),
|
|
871
|
-
...(activeTurn === undefined ? {} : { activeTurn }),
|
|
872
|
-
});
|
|
791
|
+
.query(
|
|
792
|
+
`SELECT sa.*, m.ordinal AS activation_ordinal
|
|
793
|
+
FROM skill_activations sa
|
|
794
|
+
JOIN messages m ON m.message_id = sa.activation_message_id
|
|
795
|
+
ORDER BY m.ordinal`,
|
|
796
|
+
)
|
|
797
|
+
.all()
|
|
798
|
+
.map(decodeSkillActivation);
|
|
799
|
+
const filtered =
|
|
800
|
+
states === undefined ? rows : rows.filter((row) => states.includes(row.state));
|
|
801
|
+
return Object.freeze(filtered);
|
|
873
802
|
}
|
|
874
803
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
):
|
|
804
|
+
markSkillActivationsDispatched(input: {
|
|
805
|
+
iterationId: IterationId;
|
|
806
|
+
activationMessageIds: readonly MessageId[];
|
|
807
|
+
}): readonly StoredSkillActivation[] {
|
|
879
808
|
this.requireOpen();
|
|
880
|
-
|
|
809
|
+
if (input.activationMessageIds.length === 0) {
|
|
810
|
+
return Object.freeze([]);
|
|
811
|
+
}
|
|
812
|
+
if (
|
|
813
|
+
new Set(input.activationMessageIds).size !== input.activationMessageIds.length
|
|
814
|
+
) {
|
|
815
|
+
throw new Error("Agent Skill dispatch contains duplicate activation messages.");
|
|
816
|
+
}
|
|
881
817
|
const now = this.clock();
|
|
882
818
|
try {
|
|
883
819
|
return runTransaction(this.database, () => {
|
|
884
|
-
const
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
baseRevision.revisionId !== input.expectedBaseRevisionId ||
|
|
888
|
-
baseRevision.revisionNumber !== input.expectedBaseRevisionNumber ||
|
|
889
|
-
snapshot.canonical.messages.length !==
|
|
890
|
-
input.expectedCanonicalThroughOrdinal ||
|
|
891
|
-
baseRevision.activeOverrideManifestSha256 !==
|
|
892
|
-
input.expectedBaseActiveOverrideManifestSha256
|
|
893
|
-
) {
|
|
894
|
-
throw new Error("Context revision commit base is stale.");
|
|
895
|
-
}
|
|
896
|
-
this.assertContextRevisionBoundary(input.activeTurnId);
|
|
897
|
-
|
|
898
|
-
const active = this.revisionCompiler.compileActive(snapshot);
|
|
899
|
-
const candidateOverrides = [
|
|
900
|
-
...snapshot.activeOverrides,
|
|
901
|
-
...input.addedOverrides,
|
|
902
|
-
];
|
|
903
|
-
if (
|
|
904
|
-
new Set(candidateOverrides.map((override) => override.messageId)).size !==
|
|
905
|
-
candidateOverrides.length ||
|
|
906
|
-
activeOverrideManifestHash(candidateOverrides) !==
|
|
907
|
-
input.nextActiveOverrideManifestSha256
|
|
908
|
-
) {
|
|
909
|
-
throw new Error("Candidate override manifest is invalid.");
|
|
820
|
+
const iteration = this.ledgerWriter.requireIterationRow(input.iterationId);
|
|
821
|
+
if (iteration.outcome !== "open") {
|
|
822
|
+
throw new Error(`Iteration ${input.iterationId} is not open for dispatch.`);
|
|
910
823
|
}
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
canonical: snapshot.canonical,
|
|
914
|
-
activeOverrides: snapshot.activeOverrides,
|
|
915
|
-
addedOverrides: input.addedOverrides,
|
|
916
|
-
activeSurface: snapshot.surface,
|
|
917
|
-
});
|
|
918
|
-
if (
|
|
919
|
-
canonicalSequenceHash(
|
|
920
|
-
snapshot.canonical,
|
|
921
|
-
input.expectedCanonicalThroughOrdinal,
|
|
922
|
-
) !== input.canonicalSequenceSha256 ||
|
|
923
|
-
renderedMessageHash(
|
|
924
|
-
candidate.entries,
|
|
925
|
-
input.expectedCanonicalThroughOrdinal,
|
|
926
|
-
) !== input.renderedMessageSha256
|
|
927
|
-
) {
|
|
928
|
-
throw new Error("Candidate context revision prefix hash is invalid.");
|
|
929
|
-
}
|
|
930
|
-
this.validateAddedOverrides(input.addedOverrides, snapshot.canonical);
|
|
931
|
-
|
|
932
|
-
const revisionNumber = baseRevision.revisionNumber + 1;
|
|
933
|
-
const activeOverrideCount =
|
|
934
|
-
baseRevision.activeOverrideCount + input.addedOverrides.length;
|
|
935
|
-
options.faultInjector?.("before_revision_insert");
|
|
936
|
-
this.database
|
|
937
|
-
.query(
|
|
938
|
-
`INSERT INTO context_revisions (
|
|
939
|
-
revision_id, session_id, revision_number, parent_revision_id, kind,
|
|
940
|
-
surface_id, surface_sha256, keep_from_ordinal,
|
|
941
|
-
source_through_ordinal, added_override_count,
|
|
942
|
-
active_override_count, active_override_manifest_sha256,
|
|
943
|
-
canonical_sequence_sha256, rendered_message_sha256, policy_version,
|
|
944
|
-
renderer_format, plan_sha256, change_manifest_sha256, created_at
|
|
945
|
-
) VALUES (?, ?, ?, ?, 'swap_only', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?)`,
|
|
946
|
-
)
|
|
947
|
-
.run(
|
|
948
|
-
input.revisionId,
|
|
949
|
-
this.sessionId,
|
|
950
|
-
revisionNumber,
|
|
951
|
-
baseRevision.revisionId,
|
|
952
|
-
baseRevision.surfaceId,
|
|
953
|
-
baseRevision.surfaceSha256,
|
|
954
|
-
baseRevision.keepFromOrdinal,
|
|
955
|
-
input.expectedCanonicalThroughOrdinal,
|
|
956
|
-
input.addedOverrides.length,
|
|
957
|
-
activeOverrideCount,
|
|
958
|
-
input.nextActiveOverrideManifestSha256,
|
|
959
|
-
input.canonicalSequenceSha256,
|
|
960
|
-
input.renderedMessageSha256,
|
|
961
|
-
input.policyVersion,
|
|
962
|
-
input.rendererFormat,
|
|
963
|
-
input.planHash,
|
|
964
|
-
now,
|
|
965
|
-
);
|
|
966
|
-
options.faultInjector?.("after_revision_insert");
|
|
967
|
-
|
|
968
|
-
for (let index = 0; index < input.addedOverrides.length; index += 1) {
|
|
969
|
-
const override = requireItem(
|
|
970
|
-
input.addedOverrides,
|
|
971
|
-
index,
|
|
972
|
-
"added context override",
|
|
973
|
-
);
|
|
974
|
-
this.database
|
|
824
|
+
for (const messageId of input.activationMessageIds) {
|
|
825
|
+
const updated = this.database
|
|
975
826
|
.query(
|
|
976
|
-
`
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
rendered_content, rendered_content_sha256, original_bytes,
|
|
980
|
-
rendered_bytes, byte_savings, created_at
|
|
981
|
-
) VALUES (?, ?, ?, ?, ?, 'swapped', ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
827
|
+
`UPDATE skill_activations
|
|
828
|
+
SET state = 'dispatched', dispatched_iteration_id = ?, updated_at = ?
|
|
829
|
+
WHERE activation_message_id = ? AND session_id = ? AND state = 'pending'`,
|
|
982
830
|
)
|
|
983
|
-
.run(
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
input.rendererFormat,
|
|
990
|
-
override.source,
|
|
991
|
-
override.originalContentSha256,
|
|
992
|
-
override.renderedContent,
|
|
993
|
-
override.renderedContentSha256,
|
|
994
|
-
override.originalBytes,
|
|
995
|
-
override.renderedBytes,
|
|
996
|
-
override.byteSavings,
|
|
997
|
-
now,
|
|
998
|
-
);
|
|
999
|
-
if (index === 0) {
|
|
1000
|
-
options.faultInjector?.("after_first_override_insert");
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
options.faultInjector?.("after_overrides_insert");
|
|
1004
|
-
|
|
1005
|
-
const storedCandidateOverrides = this.database
|
|
1006
|
-
.query(
|
|
1007
|
-
`SELECT co.* FROM context_overrides co
|
|
1008
|
-
JOIN context_revisions cr
|
|
1009
|
-
ON cr.revision_id = co.introduced_revision_id
|
|
1010
|
-
WHERE cr.revision_number <= ? AND co.ordinal >= ?
|
|
1011
|
-
ORDER BY co.ordinal`,
|
|
1012
|
-
)
|
|
1013
|
-
.all(revisionNumber, baseRevision.keepFromOrdinal)
|
|
1014
|
-
.map(decodeStoredSwapOverride);
|
|
1015
|
-
if (
|
|
1016
|
-
storedCandidateOverrides.length !== activeOverrideCount ||
|
|
1017
|
-
activeOverrideManifestHash(storedCandidateOverrides) !==
|
|
1018
|
-
input.nextActiveOverrideManifestSha256
|
|
1019
|
-
) {
|
|
1020
|
-
throw new Error("Stored candidate override readback is invalid.");
|
|
831
|
+
.run(input.iterationId, now, messageId, this.sessionId);
|
|
832
|
+
requireSingleChange(
|
|
833
|
+
this.database,
|
|
834
|
+
updated.changes,
|
|
835
|
+
`dispatch Agent Skill activation ${messageId}`,
|
|
836
|
+
);
|
|
1021
837
|
}
|
|
1022
|
-
|
|
1023
|
-
this.
|
|
1024
|
-
|
|
1025
|
-
const switched = this.database
|
|
1026
|
-
.query(
|
|
1027
|
-
`UPDATE session_meta
|
|
1028
|
-
SET active_revision_id = ?, updated_at = ?
|
|
1029
|
-
WHERE singleton = 1 AND active_revision_id = ?`,
|
|
1030
|
-
)
|
|
1031
|
-
.run(input.revisionId, now, baseRevision.revisionId);
|
|
1032
|
-
requireSingleChange(
|
|
1033
|
-
this.database,
|
|
1034
|
-
switched.changes,
|
|
1035
|
-
"activate context revision",
|
|
838
|
+
this.ledgerWriter.touch(now);
|
|
839
|
+
const dispatched = this.loadSkillActivations(["dispatched"]).filter((row) =>
|
|
840
|
+
input.activationMessageIds.includes(row.activationMessageId),
|
|
1036
841
|
);
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
const readback = this.loadContextSnapshot();
|
|
1040
|
-
if (
|
|
1041
|
-
readback.revision.kind !== "swap_only" ||
|
|
1042
|
-
readback.revision.revisionId !== input.revisionId ||
|
|
1043
|
-
this.loadMeasuredContextState() !== undefined
|
|
1044
|
-
) {
|
|
1045
|
-
throw new Error("Committed context revision readback failed.");
|
|
842
|
+
if (dispatched.length !== input.activationMessageIds.length) {
|
|
843
|
+
throw new Error("Agent Skill dispatch readback failed.");
|
|
1046
844
|
}
|
|
1047
|
-
return
|
|
845
|
+
return Object.freeze(dispatched);
|
|
1048
846
|
});
|
|
1049
847
|
} catch (error) {
|
|
1050
|
-
|
|
1051
|
-
throw new Error("Failed context revision transaction changed active state.", {
|
|
1052
|
-
cause: error,
|
|
1053
|
-
});
|
|
1054
|
-
}
|
|
1055
|
-
throw sessionWriteError("commit_context_revision", this.sessionId, error);
|
|
848
|
+
throw sessionWriteError("dispatch_skill_activations", this.sessionId, error);
|
|
1056
849
|
}
|
|
1057
850
|
}
|
|
1058
851
|
|
|
1059
|
-
|
|
1060
|
-
input: CommitPrefixRetirementRevisionInput,
|
|
1061
|
-
options: CommitPrefixRetirementRevisionOptions = {},
|
|
1062
|
-
): Extract<StoredContextRevisionV8, { kind: "prefix_retirement" }> {
|
|
852
|
+
markResumed(): number {
|
|
1063
853
|
this.requireOpen();
|
|
1064
|
-
assertCommitPrefixRetirementRevisionInput(input);
|
|
1065
854
|
const now = this.clock();
|
|
1066
855
|
try {
|
|
1067
856
|
return runTransaction(this.database, () => {
|
|
1068
|
-
const
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
baseRevision.revisionId !== input.expectedBaseRevisionId ||
|
|
1072
|
-
baseRevision.revisionNumber !== input.expectedBaseRevisionNumber ||
|
|
1073
|
-
baseRevision.keepFromOrdinal !== input.expectedBaseKeepFromOrdinal ||
|
|
1074
|
-
snapshot.canonical.messages.length !==
|
|
1075
|
-
input.expectedCanonicalThroughOrdinal ||
|
|
1076
|
-
snapshot.surface.surfaceSha256 !== input.expectedSurfaceSha256 ||
|
|
1077
|
-
baseRevision.activeOverrideManifestSha256 !==
|
|
1078
|
-
input.expectedBaseActiveOverrideManifestSha256
|
|
1079
|
-
) {
|
|
1080
|
-
throw new Error("Prefix retirement commit base is stale.");
|
|
1081
|
-
}
|
|
1082
|
-
this.assertContextRevisionBoundary(input.activeTurnId);
|
|
1083
|
-
const retirementBoundaries = this.readRetirementBoundaries(
|
|
1084
|
-
snapshot.canonical,
|
|
1085
|
-
input.activeTurnId,
|
|
1086
|
-
);
|
|
1087
|
-
const closedTurns = retirementBoundaries.closedTurns;
|
|
1088
|
-
const activeTurns = closedTurns.filter(
|
|
1089
|
-
(turn) => turn.firstOrdinal >= baseRevision.keepFromOrdinal,
|
|
1090
|
-
);
|
|
1091
|
-
const nextBoundary = [
|
|
1092
|
-
...activeTurns,
|
|
1093
|
-
...(retirementBoundaries.activeTurn === undefined
|
|
1094
|
-
? []
|
|
1095
|
-
: [retirementBoundaries.activeTurn]),
|
|
1096
|
-
].find((turn) => turn.firstOrdinal === input.nextKeepFromOrdinal);
|
|
1097
|
-
const retiredTurns = activeTurns.filter(
|
|
1098
|
-
(turn) => turn.lastOrdinal < input.nextKeepFromOrdinal,
|
|
1099
|
-
);
|
|
1100
|
-
if (
|
|
1101
|
-
nextBoundary === undefined ||
|
|
1102
|
-
input.nextKeepFromOrdinal <= baseRevision.keepFromOrdinal ||
|
|
1103
|
-
retiredTurns.length !== input.retiredTurnCount ||
|
|
1104
|
-
retiredTurns.reduce((total, turn) => total + turn.frameCount, 0) !==
|
|
1105
|
-
input.retiredFrameCount ||
|
|
1106
|
-
retiredTurns.reduce((total, turn) => total + turn.messageCount, 0) !==
|
|
1107
|
-
input.retiredMessageCount
|
|
1108
|
-
) {
|
|
1109
|
-
throw new Error("Prefix retirement turn boundary is invalid.");
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
const active = this.revisionCompiler.compileActive(snapshot);
|
|
1113
|
-
const nextActiveOverrides = snapshot.activeOverrides.filter(
|
|
1114
|
-
(override) => override.ordinal >= input.nextKeepFromOrdinal,
|
|
1115
|
-
);
|
|
1116
|
-
const candidate = this.revisionCompiler.compileProspective({
|
|
1117
|
-
active,
|
|
1118
|
-
canonical: snapshot.canonical,
|
|
1119
|
-
activeOverrides: snapshot.activeOverrides,
|
|
1120
|
-
addedOverrides: [],
|
|
1121
|
-
activeSurface: snapshot.surface,
|
|
1122
|
-
keepFromOrdinal: input.nextKeepFromOrdinal,
|
|
1123
|
-
});
|
|
1124
|
-
if (
|
|
1125
|
-
input.retiredThroughOrdinal !== input.nextKeepFromOrdinal - 1 ||
|
|
1126
|
-
nextActiveOverrides.length !== input.nextActiveOverrideCount ||
|
|
1127
|
-
activeOverrideManifestHash(nextActiveOverrides) !==
|
|
1128
|
-
input.nextActiveOverrideManifestSha256 ||
|
|
1129
|
-
canonicalSequenceHash(
|
|
1130
|
-
snapshot.canonical,
|
|
1131
|
-
input.expectedCanonicalThroughOrdinal,
|
|
1132
|
-
) !== input.canonicalSequenceSha256 ||
|
|
1133
|
-
renderedMessageHash(
|
|
1134
|
-
candidate.entries,
|
|
1135
|
-
input.expectedCanonicalThroughOrdinal,
|
|
1136
|
-
) !== input.renderedMessageSha256
|
|
1137
|
-
) {
|
|
1138
|
-
throw new Error("Prefix retirement candidate is invalid.");
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
const revisionNumber = baseRevision.revisionNumber + 1;
|
|
1142
|
-
options.faultInjector?.("before_revision_insert");
|
|
1143
|
-
this.database
|
|
1144
|
-
.query(
|
|
1145
|
-
`INSERT INTO context_revisions (
|
|
1146
|
-
revision_id, session_id, revision_number, parent_revision_id, kind,
|
|
1147
|
-
surface_id, surface_sha256, keep_from_ordinal,
|
|
1148
|
-
source_through_ordinal, added_override_count, active_override_count,
|
|
1149
|
-
active_override_manifest_sha256, canonical_sequence_sha256,
|
|
1150
|
-
rendered_message_sha256, policy_version, renderer_format,
|
|
1151
|
-
plan_sha256, change_manifest_sha256, retired_through_ordinal,
|
|
1152
|
-
retired_turn_count, retired_frame_count, retired_message_count,
|
|
1153
|
-
created_at
|
|
1154
|
-
) VALUES (?, ?, ?, ?, 'prefix_retirement', ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, NULL, ?, NULL, ?, ?, ?, ?, ?)`,
|
|
1155
|
-
)
|
|
1156
|
-
.run(
|
|
1157
|
-
input.revisionId,
|
|
1158
|
-
this.sessionId,
|
|
1159
|
-
revisionNumber,
|
|
1160
|
-
baseRevision.revisionId,
|
|
1161
|
-
baseRevision.surfaceId,
|
|
1162
|
-
baseRevision.surfaceSha256,
|
|
1163
|
-
input.nextKeepFromOrdinal,
|
|
1164
|
-
input.expectedCanonicalThroughOrdinal,
|
|
1165
|
-
input.nextActiveOverrideCount,
|
|
1166
|
-
input.nextActiveOverrideManifestSha256,
|
|
1167
|
-
input.canonicalSequenceSha256,
|
|
1168
|
-
input.renderedMessageSha256,
|
|
1169
|
-
input.policyVersion,
|
|
1170
|
-
input.planHash,
|
|
1171
|
-
input.retiredThroughOrdinal,
|
|
1172
|
-
input.retiredTurnCount,
|
|
1173
|
-
input.retiredFrameCount,
|
|
1174
|
-
input.retiredMessageCount,
|
|
1175
|
-
now,
|
|
1176
|
-
);
|
|
1177
|
-
options.faultInjector?.("after_revision_insert");
|
|
1178
|
-
|
|
1179
|
-
const storedActiveOverrides = this.database
|
|
1180
|
-
.query(
|
|
1181
|
-
`SELECT co.* FROM context_overrides co
|
|
1182
|
-
JOIN context_revisions introduced
|
|
1183
|
-
ON introduced.revision_id = co.introduced_revision_id
|
|
1184
|
-
WHERE introduced.revision_number <= ? AND co.ordinal >= ?
|
|
1185
|
-
ORDER BY co.ordinal`,
|
|
1186
|
-
)
|
|
1187
|
-
.all(revisionNumber, input.nextKeepFromOrdinal)
|
|
1188
|
-
.map(decodeStoredSwapOverride);
|
|
1189
|
-
if (
|
|
1190
|
-
storedActiveOverrides.length !== input.nextActiveOverrideCount ||
|
|
1191
|
-
activeOverrideManifestHash(storedActiveOverrides) !==
|
|
1192
|
-
input.nextActiveOverrideManifestSha256
|
|
1193
|
-
) {
|
|
1194
|
-
throw new Error("Prefix retirement override readback is invalid.");
|
|
1195
|
-
}
|
|
1196
|
-
options.faultInjector?.("after_override_readback");
|
|
1197
|
-
|
|
1198
|
-
this.database.query("DELETE FROM context_measurement_state").run();
|
|
1199
|
-
options.faultInjector?.("after_measurement_delete");
|
|
1200
|
-
const switched = this.database
|
|
857
|
+
const meta = this.readMeta();
|
|
858
|
+
const next = meta.openCount + 1;
|
|
859
|
+
const updated = this.database
|
|
1201
860
|
.query(
|
|
1202
|
-
`UPDATE session_meta
|
|
1203
|
-
|
|
861
|
+
`UPDATE session_meta
|
|
862
|
+
SET open_count = ?, last_opened_at = ?, updated_at = ?,
|
|
863
|
+
last_closed_at = NULL, last_close_reason = NULL
|
|
864
|
+
WHERE singleton = 1 AND open_count = ?`,
|
|
1204
865
|
)
|
|
1205
|
-
.run(
|
|
1206
|
-
requireSingleChange(
|
|
1207
|
-
|
|
1208
|
-
switched.changes,
|
|
1209
|
-
"activate prefix retirement revision",
|
|
1210
|
-
);
|
|
1211
|
-
options.faultInjector?.("after_active_update");
|
|
1212
|
-
|
|
1213
|
-
const readback = this.loadContextSnapshot();
|
|
1214
|
-
if (
|
|
1215
|
-
readback.revision.kind !== "prefix_retirement" ||
|
|
1216
|
-
readback.revision.revisionId !== input.revisionId ||
|
|
1217
|
-
readback.revision.keepFromOrdinal !== input.nextKeepFromOrdinal ||
|
|
1218
|
-
this.loadMeasuredContextState() !== undefined
|
|
1219
|
-
) {
|
|
1220
|
-
throw new Error("Committed prefix retirement readback failed.");
|
|
1221
|
-
}
|
|
1222
|
-
options.faultInjector?.("after_snapshot_readback");
|
|
1223
|
-
return readback.revision;
|
|
866
|
+
.run(next, now, now, meta.openCount);
|
|
867
|
+
requireSingleChange(this.database, updated.changes, "increment open count");
|
|
868
|
+
return next;
|
|
1224
869
|
});
|
|
1225
870
|
} catch (error) {
|
|
1226
|
-
|
|
1227
|
-
throw new Error("Failed prefix retirement changed active state.", {
|
|
1228
|
-
cause: error,
|
|
1229
|
-
});
|
|
1230
|
-
}
|
|
1231
|
-
throw sessionWriteError("commit_prefix_retirement", this.sessionId, error);
|
|
871
|
+
throw sessionWriteError("mark_resumed", this.sessionId, error);
|
|
1232
872
|
}
|
|
1233
873
|
}
|
|
1234
874
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
): Extract<StoredContextRevisionV8, { kind: "surface_refresh" }> {
|
|
1239
|
-
this.requireOpen();
|
|
1240
|
-
assertCommitSurfaceRefreshInput(input);
|
|
1241
|
-
const now = this.clock();
|
|
1242
|
-
try {
|
|
1243
|
-
return runTransaction(this.database, () => {
|
|
1244
|
-
const snapshot = this.loadContextSnapshot();
|
|
1245
|
-
const baseRevision = snapshot.revision;
|
|
1246
|
-
if (
|
|
1247
|
-
baseRevision.revisionId !== input.expectedBaseRevisionId ||
|
|
1248
|
-
baseRevision.revisionNumber !== input.expectedBaseRevisionNumber ||
|
|
1249
|
-
snapshot.canonical.messages.length !==
|
|
1250
|
-
input.expectedCanonicalThroughOrdinal ||
|
|
1251
|
-
baseRevision.activeOverrideManifestSha256 !==
|
|
1252
|
-
input.expectedBaseActiveOverrideManifestSha256
|
|
1253
|
-
) {
|
|
1254
|
-
throw new Error("Context surface refresh base is stale.");
|
|
1255
|
-
}
|
|
1256
|
-
this.assertContextRevisionIdle();
|
|
1257
|
-
validateStoredContextSurface(input.surface);
|
|
1258
|
-
if (
|
|
1259
|
-
input.surface.sessionId !== this.sessionId ||
|
|
1260
|
-
input.surface.surfaceId === snapshot.surface.surfaceId ||
|
|
1261
|
-
input.surface.surfaceSha256 === snapshot.surface.surfaceSha256
|
|
1262
|
-
) {
|
|
1263
|
-
throw new Error("Context surface refresh does not introduce a new surface.");
|
|
1264
|
-
}
|
|
1265
|
-
const actualChanges = contextSurfaceChanges(snapshot.surface, input.surface);
|
|
1266
|
-
if (
|
|
1267
|
-
stableJsonStringify(actualChanges) !== stableJsonStringify(input.changes) ||
|
|
1268
|
-
contextSurfaceChangeManifestHash(actualChanges) !==
|
|
1269
|
-
input.changeManifestSha256 ||
|
|
1270
|
-
!Object.values(actualChanges).some(Boolean)
|
|
1271
|
-
) {
|
|
1272
|
-
throw new Error("Context surface refresh change manifest is invalid.");
|
|
1273
|
-
}
|
|
875
|
+
recoverInterruptedState(idFactory: RuntimeIdFactory): SessionRecoveryResult {
|
|
876
|
+
return this.recovery.recoverInterruptedState(idFactory, this.recallIndexRebuilt);
|
|
877
|
+
}
|
|
1274
878
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
});
|
|
1284
|
-
if (
|
|
1285
|
-
canonicalSequenceHash(
|
|
1286
|
-
snapshot.canonical,
|
|
1287
|
-
input.expectedCanonicalThroughOrdinal,
|
|
1288
|
-
) !== input.canonicalSequenceSha256 ||
|
|
1289
|
-
renderedMessageHash(
|
|
1290
|
-
candidate.entries,
|
|
1291
|
-
input.expectedCanonicalThroughOrdinal,
|
|
1292
|
-
) !== input.renderedMessageSha256
|
|
1293
|
-
) {
|
|
1294
|
-
throw new Error("Candidate context surface prefix hash is invalid.");
|
|
1295
|
-
}
|
|
879
|
+
historyReader(): SessionHistoryReader {
|
|
880
|
+
this.requireOpen();
|
|
881
|
+
return createSessionHistoryReader({
|
|
882
|
+
database: this.database,
|
|
883
|
+
sessionId: this.sessionId,
|
|
884
|
+
requireOpen: () => this.requireOpen(),
|
|
885
|
+
});
|
|
886
|
+
}
|
|
1296
887
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
) VALUES (?, ?, ?, ?, 'surface_refresh', ?, ?, ?, ?, 0, ?, ?, ?, ?, NULL, NULL, NULL, ?, ?)`,
|
|
1311
|
-
)
|
|
1312
|
-
.run(
|
|
1313
|
-
input.revisionId,
|
|
1314
|
-
this.sessionId,
|
|
1315
|
-
revisionNumber,
|
|
1316
|
-
baseRevision.revisionId,
|
|
1317
|
-
input.surface.surfaceId,
|
|
1318
|
-
input.surface.surfaceSha256,
|
|
1319
|
-
baseRevision.keepFromOrdinal,
|
|
1320
|
-
input.expectedCanonicalThroughOrdinal,
|
|
1321
|
-
baseRevision.activeOverrideCount,
|
|
1322
|
-
baseRevision.activeOverrideManifestSha256,
|
|
1323
|
-
input.canonicalSequenceSha256,
|
|
1324
|
-
input.renderedMessageSha256,
|
|
1325
|
-
input.changeManifestSha256,
|
|
1326
|
-
now,
|
|
1327
|
-
);
|
|
1328
|
-
options.faultInjector?.("after_revision_insert");
|
|
888
|
+
readCompletedTurnSnapshot(turnId: TurnId): CompletedTurnSnapshot {
|
|
889
|
+
this.requireOpen();
|
|
890
|
+
const turnRow = this.database
|
|
891
|
+
.query("SELECT status FROM turns WHERE turn_id = ?")
|
|
892
|
+
.get(turnId);
|
|
893
|
+
const status = enumFromSql(
|
|
894
|
+
recordFromSql(turnRow, "completed turn").status,
|
|
895
|
+
["open", "completed", "failed", "cancelled", "interrupted"] as const,
|
|
896
|
+
"turn status",
|
|
897
|
+
);
|
|
898
|
+
if (status !== "completed") {
|
|
899
|
+
throw new Error(`Turn ${turnId} is not completed.`);
|
|
900
|
+
}
|
|
1329
901
|
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
);
|
|
1343
|
-
options.faultInjector?.("after_active_update");
|
|
902
|
+
const rows = this.database
|
|
903
|
+
.query(
|
|
904
|
+
`SELECT ordinal, role, content, reasoning_content,
|
|
905
|
+
reasoning_content_present, name
|
|
906
|
+
FROM messages
|
|
907
|
+
WHERE turn_id = ?
|
|
908
|
+
ORDER BY ordinal`,
|
|
909
|
+
)
|
|
910
|
+
.all(turnId);
|
|
911
|
+
if (rows.length === 0) {
|
|
912
|
+
throw new Error(`Completed turn ${turnId} has no messages.`);
|
|
913
|
+
}
|
|
1344
914
|
|
|
1345
|
-
|
|
915
|
+
let previousOrdinal = 0;
|
|
916
|
+
const messages = rows.map((value): CompletedTurnMessageSnapshot => {
|
|
917
|
+
const row = recordFromSql(value, "completed turn message");
|
|
918
|
+
const ordinal = numberFromSql(row.ordinal, "completed turn ordinal");
|
|
919
|
+
if (ordinal < 1 || ordinal <= previousOrdinal) {
|
|
920
|
+
throw new Error("Completed turn message ordinals are invalid.");
|
|
921
|
+
}
|
|
922
|
+
previousOrdinal = ordinal;
|
|
923
|
+
const role = enumFromSql(
|
|
924
|
+
row.role,
|
|
925
|
+
["user", "assistant", "tool"] as const,
|
|
926
|
+
"completed turn message role",
|
|
927
|
+
);
|
|
928
|
+
if (role === "user") {
|
|
1346
929
|
if (
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
930
|
+
row.reasoning_content !== null ||
|
|
931
|
+
numberFromSql(row.reasoning_content_present, "reasoning_content_present") !==
|
|
932
|
+
0 ||
|
|
933
|
+
row.name !== null
|
|
1351
934
|
) {
|
|
1352
|
-
throw new Error("
|
|
935
|
+
throw new Error("Completed user message fields are invalid.");
|
|
1353
936
|
}
|
|
1354
|
-
return
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
throw new Error("Failed context surface transaction changed active state.", {
|
|
1359
|
-
cause: error,
|
|
937
|
+
return Object.freeze({
|
|
938
|
+
ordinal,
|
|
939
|
+
role,
|
|
940
|
+
content: stringFromSql(row.content, "completed user content"),
|
|
1360
941
|
});
|
|
1361
942
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
commitSkillsUpdate(
|
|
1367
|
-
input: CommitSkillsUpdateInput,
|
|
1368
|
-
options: CommitSkillsUpdateOptions = {},
|
|
1369
|
-
): Extract<StoredContextRevisionV8, { kind: "skills_update" }> {
|
|
1370
|
-
this.requireOpen();
|
|
1371
|
-
assertCommitSkillsUpdateInput(input);
|
|
1372
|
-
const now = this.clock();
|
|
1373
|
-
try {
|
|
1374
|
-
return runTransaction(this.database, () => {
|
|
1375
|
-
const snapshot = this.loadContextSnapshot();
|
|
1376
|
-
const baseRevision = snapshot.revision;
|
|
1377
|
-
if (
|
|
1378
|
-
baseRevision.revisionId !== input.expectedBaseRevisionId ||
|
|
1379
|
-
baseRevision.revisionNumber !== input.expectedBaseRevisionNumber ||
|
|
1380
|
-
snapshot.canonical.messages.length !==
|
|
1381
|
-
input.expectedCanonicalThroughOrdinal ||
|
|
1382
|
-
baseRevision.activeOverrideManifestSha256 !==
|
|
1383
|
-
input.expectedBaseActiveOverrideManifestSha256
|
|
1384
|
-
) {
|
|
1385
|
-
throw new Error("Agent Skills update base is stale.");
|
|
1386
|
-
}
|
|
1387
|
-
this.assertContextRevisionIdle();
|
|
1388
|
-
validateStoredContextSurface(input.surface);
|
|
1389
|
-
|
|
1390
|
-
const surfaceChanged =
|
|
1391
|
-
input.surface.surfaceSha256 !== snapshot.surface.surfaceSha256;
|
|
1392
|
-
const actualChanges = contextSurfaceChanges(snapshot.surface, input.surface);
|
|
1393
|
-
if (
|
|
1394
|
-
input.surface.sessionId !== this.sessionId ||
|
|
1395
|
-
stableJsonStringify(actualChanges) !== stableJsonStringify(input.changes) ||
|
|
1396
|
-
contextSurfaceChangeManifestHash(actualChanges) !==
|
|
1397
|
-
input.changeManifestSha256 ||
|
|
1398
|
-
(surfaceChanged && input.surface.surfaceId === snapshot.surface.surfaceId) ||
|
|
1399
|
-
(!surfaceChanged && input.surface.surfaceId !== snapshot.surface.surfaceId) ||
|
|
1400
|
-
(!surfaceChanged && Object.values(actualChanges).some(Boolean))
|
|
1401
|
-
) {
|
|
1402
|
-
throw new Error("Agent Skills surface change manifest is invalid.");
|
|
943
|
+
if (role === "assistant") {
|
|
944
|
+
if (row.name !== null) {
|
|
945
|
+
throw new Error("Completed assistant message name must be null.");
|
|
1403
946
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
entry.activationMessageId,
|
|
1408
|
-
entry,
|
|
1409
|
-
]),
|
|
947
|
+
const reasoningPresent = numberFromSql(
|
|
948
|
+
row.reasoning_content_present,
|
|
949
|
+
"reasoning_content_present",
|
|
1410
950
|
);
|
|
1411
|
-
if (
|
|
1412
|
-
|
|
1413
|
-
input.settlements.length !== unresolved.size ||
|
|
1414
|
-
new Set(input.settlements.map((entry) => entry.activationMessageId)).size !==
|
|
1415
|
-
input.settlements.length ||
|
|
1416
|
-
skillActivationManifestSha256(input.settlements) !==
|
|
1417
|
-
input.activationManifestSha256
|
|
1418
|
-
) {
|
|
1419
|
-
throw new Error("Agent Skills settlement manifest is invalid.");
|
|
951
|
+
if (reasoningPresent !== 0 && reasoningPresent !== 1) {
|
|
952
|
+
throw new Error("reasoning_content_present must be 0 or 1.");
|
|
1420
953
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
);
|
|
1424
|
-
const providedOverrides = new Map(
|
|
1425
|
-
input.addedOverrides.map((override) => [override.messageId, override]),
|
|
1426
|
-
);
|
|
1427
|
-
for (const settlement of input.settlements) {
|
|
1428
|
-
const activation = unresolved.get(settlement.activationMessageId);
|
|
1429
|
-
const message = canonicalMessages.get(settlement.activationMessageId);
|
|
1430
|
-
const override = providedOverrides.get(settlement.activationMessageId);
|
|
1431
|
-
if (
|
|
1432
|
-
activation === undefined ||
|
|
1433
|
-
message?.role !== "tool" ||
|
|
1434
|
-
message.name !== "Skill" ||
|
|
1435
|
-
override === undefined ||
|
|
1436
|
-
settlement.name !== activation.name ||
|
|
1437
|
-
(settlement.state === "promoted" &&
|
|
1438
|
-
(activation.state !== "dispatched" ||
|
|
1439
|
-
settlement.rejectionReason !== undefined)) ||
|
|
1440
|
-
(settlement.state === "rejected" &&
|
|
1441
|
-
(settlement.rejectionReason === undefined ||
|
|
1442
|
-
settlement.rejectionReason.trim() === "" ||
|
|
1443
|
-
settlement.rejectionReason.length > 256))
|
|
1444
|
-
) {
|
|
1445
|
-
throw new Error(
|
|
1446
|
-
`Agent Skill activation ${settlement.activationMessageId} cannot be settled.`,
|
|
1447
|
-
);
|
|
1448
|
-
}
|
|
1449
|
-
const activeManifest = input.surface.activeSkills.find(
|
|
1450
|
-
(entry) =>
|
|
1451
|
-
entry.name === activation.name &&
|
|
1452
|
-
entry.activationMessageId === activation.activationMessageId,
|
|
1453
|
-
);
|
|
1454
|
-
if (
|
|
1455
|
-
(settlement.state === "promoted" && activeManifest === undefined) ||
|
|
1456
|
-
(settlement.state === "rejected" && activeManifest !== undefined)
|
|
1457
|
-
) {
|
|
1458
|
-
throw new Error("Agent Skills active manifest does not match settlements.");
|
|
1459
|
-
}
|
|
1460
|
-
const expected = renderSkillActivationReceipt({
|
|
1461
|
-
message: {
|
|
1462
|
-
messageId: message.messageId,
|
|
1463
|
-
frameId: message.frameId,
|
|
1464
|
-
ordinal: message.ordinal,
|
|
1465
|
-
content: message.displayText,
|
|
1466
|
-
contentSha256: message.contentSha256,
|
|
1467
|
-
},
|
|
1468
|
-
name: activation.name,
|
|
1469
|
-
outcome:
|
|
1470
|
-
settlement.state === "promoted"
|
|
1471
|
-
? "promoted"
|
|
1472
|
-
: settlement.rejectionReason === "unavailable"
|
|
1473
|
-
? "unavailable"
|
|
1474
|
-
: "rejected",
|
|
1475
|
-
});
|
|
1476
|
-
if (stableJsonStringify(expected) !== stableJsonStringify(override)) {
|
|
1477
|
-
throw new Error("Agent Skill activation receipt is not deterministic.");
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
const active = this.revisionCompiler.compileActive(snapshot);
|
|
1482
|
-
const candidateOverrides = [
|
|
1483
|
-
...snapshot.activeOverrides,
|
|
1484
|
-
...input.addedOverrides,
|
|
1485
|
-
];
|
|
1486
|
-
if (
|
|
1487
|
-
new Set(candidateOverrides.map((override) => override.messageId)).size !==
|
|
1488
|
-
candidateOverrides.length ||
|
|
1489
|
-
activeOverrideManifestHash(candidateOverrides) !==
|
|
1490
|
-
input.nextActiveOverrideManifestSha256
|
|
1491
|
-
) {
|
|
1492
|
-
throw new Error("Agent Skills override manifest is invalid.");
|
|
1493
|
-
}
|
|
1494
|
-
const candidate = this.revisionCompiler.compileProspective({
|
|
1495
|
-
active,
|
|
1496
|
-
canonical: snapshot.canonical,
|
|
1497
|
-
activeOverrides: snapshot.activeOverrides,
|
|
1498
|
-
addedOverrides: input.addedOverrides,
|
|
1499
|
-
activeSurface: snapshot.surface,
|
|
1500
|
-
...(surfaceChanged ? { surface: input.surface } : {}),
|
|
1501
|
-
allowCombinedSurfaceAndOverrides: true,
|
|
1502
|
-
});
|
|
1503
|
-
if (
|
|
1504
|
-
canonicalSequenceHash(
|
|
1505
|
-
snapshot.canonical,
|
|
1506
|
-
input.expectedCanonicalThroughOrdinal,
|
|
1507
|
-
) !== input.canonicalSequenceSha256 ||
|
|
1508
|
-
renderedMessageHash(
|
|
1509
|
-
candidate.entries,
|
|
1510
|
-
input.expectedCanonicalThroughOrdinal,
|
|
1511
|
-
) !== input.renderedMessageSha256
|
|
1512
|
-
) {
|
|
1513
|
-
throw new Error("Agent Skills compiled context hashes are invalid.");
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
options.faultInjector?.("before_surface_insert");
|
|
1517
|
-
if (surfaceChanged) {
|
|
1518
|
-
insertContextSurface(this.database, input.surface);
|
|
1519
|
-
}
|
|
1520
|
-
options.faultInjector?.("after_surface_insert");
|
|
1521
|
-
const revisionNumber = baseRevision.revisionNumber + 1;
|
|
1522
|
-
const activeOverrideCount =
|
|
1523
|
-
baseRevision.activeOverrideCount + input.addedOverrides.length;
|
|
1524
|
-
this.database
|
|
1525
|
-
.query(
|
|
1526
|
-
`INSERT INTO context_revisions (
|
|
1527
|
-
revision_id, session_id, revision_number, parent_revision_id, kind,
|
|
1528
|
-
surface_id, surface_sha256, keep_from_ordinal,
|
|
1529
|
-
source_through_ordinal, added_override_count, active_override_count,
|
|
1530
|
-
active_override_manifest_sha256, canonical_sequence_sha256,
|
|
1531
|
-
rendered_message_sha256, policy_version, renderer_format,
|
|
1532
|
-
plan_sha256, change_manifest_sha256, activation_manifest_sha256,
|
|
1533
|
-
retired_through_ordinal, retired_turn_count, retired_frame_count,
|
|
1534
|
-
retired_message_count, created_at
|
|
1535
|
-
) VALUES (?, ?, ?, ?, 'skills_update', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, NULL, NULL, NULL, NULL, ?)`,
|
|
1536
|
-
)
|
|
1537
|
-
.run(
|
|
1538
|
-
input.revisionId,
|
|
1539
|
-
this.sessionId,
|
|
1540
|
-
revisionNumber,
|
|
1541
|
-
baseRevision.revisionId,
|
|
1542
|
-
input.surface.surfaceId,
|
|
1543
|
-
input.surface.surfaceSha256,
|
|
1544
|
-
baseRevision.keepFromOrdinal,
|
|
1545
|
-
input.expectedCanonicalThroughOrdinal,
|
|
1546
|
-
input.addedOverrides.length,
|
|
1547
|
-
activeOverrideCount,
|
|
1548
|
-
input.nextActiveOverrideManifestSha256,
|
|
1549
|
-
input.canonicalSequenceSha256,
|
|
1550
|
-
input.renderedMessageSha256,
|
|
1551
|
-
SKILL_POLICY_VERSION,
|
|
1552
|
-
SKILL_ACTIVATION_RECEIPT_FORMAT,
|
|
1553
|
-
input.changeManifestSha256,
|
|
1554
|
-
input.activationManifestSha256,
|
|
1555
|
-
now,
|
|
1556
|
-
);
|
|
1557
|
-
options.faultInjector?.("after_revision_insert");
|
|
1558
|
-
|
|
1559
|
-
for (let index = 0; index < input.addedOverrides.length; index += 1) {
|
|
1560
|
-
const override = requireItem(
|
|
1561
|
-
input.addedOverrides,
|
|
1562
|
-
index,
|
|
1563
|
-
"Agent Skill receipt override",
|
|
1564
|
-
);
|
|
1565
|
-
this.database
|
|
1566
|
-
.query(
|
|
1567
|
-
`INSERT INTO context_overrides (
|
|
1568
|
-
introduced_revision_id, session_id, message_id, frame_id, ordinal,
|
|
1569
|
-
representation, renderer_format, source, original_content_sha256,
|
|
1570
|
-
rendered_content, rendered_content_sha256, original_bytes,
|
|
1571
|
-
rendered_bytes, byte_savings, created_at
|
|
1572
|
-
) VALUES (?, ?, ?, ?, ?, 'swapped', ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1573
|
-
)
|
|
1574
|
-
.run(
|
|
1575
|
-
input.revisionId,
|
|
1576
|
-
this.sessionId,
|
|
1577
|
-
override.messageId,
|
|
1578
|
-
override.frameId,
|
|
1579
|
-
override.ordinal,
|
|
1580
|
-
SKILL_ACTIVATION_RECEIPT_FORMAT,
|
|
1581
|
-
override.source,
|
|
1582
|
-
override.originalContentSha256,
|
|
1583
|
-
override.renderedContent,
|
|
1584
|
-
override.renderedContentSha256,
|
|
1585
|
-
override.originalBytes,
|
|
1586
|
-
override.renderedBytes,
|
|
1587
|
-
override.byteSavings,
|
|
1588
|
-
now,
|
|
1589
|
-
);
|
|
1590
|
-
if (index === 0) {
|
|
1591
|
-
options.faultInjector?.("after_first_override_insert");
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
options.faultInjector?.("after_overrides_insert");
|
|
1595
|
-
|
|
1596
|
-
for (const settlement of input.settlements) {
|
|
1597
|
-
const activation = unresolved.get(settlement.activationMessageId)!;
|
|
1598
|
-
const updated =
|
|
1599
|
-
settlement.state === "promoted"
|
|
1600
|
-
? this.database
|
|
1601
|
-
.query(
|
|
1602
|
-
`UPDATE skill_activations
|
|
1603
|
-
SET state = 'promoted', settled_revision_id = ?, updated_at = ?
|
|
1604
|
-
WHERE activation_message_id = ? AND state = 'dispatched'`,
|
|
1605
|
-
)
|
|
1606
|
-
.run(input.revisionId, now, settlement.activationMessageId)
|
|
1607
|
-
: this.database
|
|
1608
|
-
.query(
|
|
1609
|
-
`UPDATE skill_activations
|
|
1610
|
-
SET state = 'rejected', settled_revision_id = ?, rejection_reason = ?, updated_at = ?
|
|
1611
|
-
WHERE activation_message_id = ? AND state = ?`,
|
|
1612
|
-
)
|
|
1613
|
-
.run(
|
|
1614
|
-
input.revisionId,
|
|
1615
|
-
settlement.rejectionReason!,
|
|
1616
|
-
now,
|
|
1617
|
-
settlement.activationMessageId,
|
|
1618
|
-
activation.state,
|
|
1619
|
-
);
|
|
1620
|
-
requireSingleChange(
|
|
1621
|
-
this.database,
|
|
1622
|
-
updated.changes,
|
|
1623
|
-
`settle Agent Skill activation ${settlement.activationMessageId}`,
|
|
1624
|
-
);
|
|
1625
|
-
}
|
|
1626
|
-
options.faultInjector?.("after_activations_update");
|
|
1627
|
-
|
|
1628
|
-
this.database.query("DELETE FROM context_measurement_state").run();
|
|
1629
|
-
options.faultInjector?.("after_measurement_delete");
|
|
1630
|
-
const switched = this.database
|
|
1631
|
-
.query(
|
|
1632
|
-
`UPDATE session_meta SET active_revision_id = ?, updated_at = ?
|
|
1633
|
-
WHERE singleton = 1 AND active_revision_id = ?`,
|
|
1634
|
-
)
|
|
1635
|
-
.run(input.revisionId, now, baseRevision.revisionId);
|
|
1636
|
-
requireSingleChange(
|
|
1637
|
-
this.database,
|
|
1638
|
-
switched.changes,
|
|
1639
|
-
"activate Agent Skills context revision",
|
|
1640
|
-
);
|
|
1641
|
-
options.faultInjector?.("after_active_update");
|
|
1642
|
-
|
|
1643
|
-
const readback = this.loadContextSnapshot();
|
|
1644
|
-
if (
|
|
1645
|
-
readback.revision.kind !== "skills_update" ||
|
|
1646
|
-
readback.revision.revisionId !== input.revisionId ||
|
|
1647
|
-
readback.surface.surfaceId !== input.surface.surfaceId ||
|
|
1648
|
-
this.loadMeasuredContextState() !== undefined ||
|
|
1649
|
-
this.loadSkillActivations(["pending", "dispatched"]).some((entry) =>
|
|
1650
|
-
input.settlements.some(
|
|
1651
|
-
(settlement) =>
|
|
1652
|
-
settlement.activationMessageId === entry.activationMessageId,
|
|
1653
|
-
),
|
|
1654
|
-
)
|
|
1655
|
-
) {
|
|
1656
|
-
throw new Error("Committed Agent Skills update readback failed.");
|
|
954
|
+
if (reasoningPresent === 0 && row.reasoning_content !== null) {
|
|
955
|
+
throw new Error("Absent assistant reasoning content must be null.");
|
|
1657
956
|
}
|
|
1658
|
-
return
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
957
|
+
return Object.freeze({
|
|
958
|
+
ordinal,
|
|
959
|
+
role,
|
|
960
|
+
content: nullableTextFromSql(row.content, "completed assistant content"),
|
|
961
|
+
...(reasoningPresent === 0
|
|
962
|
+
? {}
|
|
963
|
+
: {
|
|
964
|
+
reasoningContent: nullableTextFromSql(
|
|
965
|
+
row.reasoning_content,
|
|
966
|
+
"completed assistant reasoning content",
|
|
967
|
+
),
|
|
968
|
+
}),
|
|
1664
969
|
});
|
|
1665
970
|
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
overrides: readonly SwapOverride[],
|
|
1672
|
-
canonical: ProtocolContextView,
|
|
1673
|
-
): void {
|
|
1674
|
-
const messages = new Map(
|
|
1675
|
-
canonical.messages.map((message) => [message.messageId, message] as const),
|
|
1676
|
-
);
|
|
1677
|
-
const results = new Map(
|
|
1678
|
-
canonical.toolResults.map((result) => [result.toolMessageId, result] as const),
|
|
1679
|
-
);
|
|
1680
|
-
for (const override of overrides) {
|
|
1681
|
-
const message = messages.get(override.messageId);
|
|
1682
|
-
const result = results.get(override.messageId);
|
|
1683
|
-
if (message?.role !== "tool" || result === undefined) {
|
|
1684
|
-
throw new Error("Added context override does not target a tool result.");
|
|
1685
|
-
}
|
|
1686
|
-
const expected = this.swapRenderer.render({ message, result });
|
|
1687
|
-
if (stableJsonStringify(expected) !== stableJsonStringify(override)) {
|
|
1688
|
-
throw new Error("Added context override is not deterministic.");
|
|
971
|
+
if (
|
|
972
|
+
row.reasoning_content !== null ||
|
|
973
|
+
numberFromSql(row.reasoning_content_present, "reasoning_content_present") !== 0
|
|
974
|
+
) {
|
|
975
|
+
throw new Error("Completed tool message reasoning fields are invalid.");
|
|
1689
976
|
}
|
|
1690
|
-
|
|
977
|
+
return Object.freeze({
|
|
978
|
+
ordinal,
|
|
979
|
+
role,
|
|
980
|
+
name: stringFromSql(row.name, "completed tool name"),
|
|
981
|
+
content: stringFromSql(row.content, "completed tool content"),
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
return Object.freeze({ messages: Object.freeze(messages) });
|
|
1691
985
|
}
|
|
1692
986
|
|
|
1693
|
-
|
|
1694
|
-
states?: readonly StoredSkillActivation["state"][],
|
|
1695
|
-
): readonly StoredSkillActivation[] {
|
|
987
|
+
loadProtocolView(): ProtocolContextView {
|
|
1696
988
|
this.requireOpen();
|
|
1697
|
-
const
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
JOIN messages m ON m.message_id = sa.activation_message_id
|
|
1702
|
-
ORDER BY m.ordinal`,
|
|
1703
|
-
)
|
|
989
|
+
const imageAttachments = loadMessageImageAttachments(this.database);
|
|
990
|
+
const toolContentBlocks = loadToolMessageContentBlocks(this.database);
|
|
991
|
+
const frames = this.database
|
|
992
|
+
.query("SELECT * FROM protocol_frames ORDER BY first_ordinal")
|
|
1704
993
|
.all()
|
|
1705
|
-
.map(
|
|
1706
|
-
const
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
if (input.activationMessageIds.length === 0) {
|
|
1717
|
-
return Object.freeze([]);
|
|
1718
|
-
}
|
|
1719
|
-
if (
|
|
1720
|
-
new Set(input.activationMessageIds).size !== input.activationMessageIds.length
|
|
1721
|
-
) {
|
|
1722
|
-
throw new Error("Agent Skill dispatch contains duplicate activation messages.");
|
|
1723
|
-
}
|
|
1724
|
-
const now = this.clock();
|
|
1725
|
-
try {
|
|
1726
|
-
return runTransaction(this.database, () => {
|
|
1727
|
-
const iteration = this.requireIterationRow(input.iterationId);
|
|
1728
|
-
if (iteration.outcome !== "open") {
|
|
1729
|
-
throw new Error(`Iteration ${input.iterationId} is not open for dispatch.`);
|
|
1730
|
-
}
|
|
1731
|
-
for (const messageId of input.activationMessageIds) {
|
|
1732
|
-
const updated = this.database
|
|
1733
|
-
.query(
|
|
1734
|
-
`UPDATE skill_activations
|
|
1735
|
-
SET state = 'dispatched', dispatched_iteration_id = ?, updated_at = ?
|
|
1736
|
-
WHERE activation_message_id = ? AND session_id = ? AND state = 'pending'`,
|
|
1737
|
-
)
|
|
1738
|
-
.run(input.iterationId, now, messageId, this.sessionId);
|
|
1739
|
-
requireSingleChange(
|
|
1740
|
-
this.database,
|
|
1741
|
-
updated.changes,
|
|
1742
|
-
`dispatch Agent Skill activation ${messageId}`,
|
|
1743
|
-
);
|
|
1744
|
-
}
|
|
1745
|
-
this.touch(now);
|
|
1746
|
-
const dispatched = this.loadSkillActivations(["dispatched"]).filter((row) =>
|
|
1747
|
-
input.activationMessageIds.includes(row.activationMessageId),
|
|
994
|
+
.map(decodeFrame);
|
|
995
|
+
const messages = this.database
|
|
996
|
+
.query("SELECT * FROM messages ORDER BY ordinal")
|
|
997
|
+
.all()
|
|
998
|
+
.map((row) => {
|
|
999
|
+
const record = recordFromSql(row, "message");
|
|
1000
|
+
const messageId = stringFromSql(record.message_id, "message_id");
|
|
1001
|
+
const message = decodeMessage(
|
|
1002
|
+
row,
|
|
1003
|
+
imageAttachments.get(messageId),
|
|
1004
|
+
toolContentBlocks.get(messageId),
|
|
1748
1005
|
);
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
return Object.freeze(dispatched);
|
|
1753
|
-
});
|
|
1754
|
-
} catch (error) {
|
|
1755
|
-
throw sessionWriteError("dispatch_skill_activations", this.sessionId, error);
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
markResumed(): number {
|
|
1760
|
-
this.requireOpen();
|
|
1761
|
-
const now = this.clock();
|
|
1762
|
-
try {
|
|
1763
|
-
return runTransaction(this.database, () => {
|
|
1764
|
-
const meta = this.readMeta();
|
|
1765
|
-
const next = meta.openCount + 1;
|
|
1766
|
-
const updated = this.database
|
|
1767
|
-
.query(
|
|
1768
|
-
`UPDATE session_meta
|
|
1769
|
-
SET open_count = ?, last_opened_at = ?, updated_at = ?,
|
|
1770
|
-
last_closed_at = NULL, last_close_reason = NULL
|
|
1771
|
-
WHERE singleton = 1 AND open_count = ?`,
|
|
1772
|
-
)
|
|
1773
|
-
.run(next, now, now, meta.openCount);
|
|
1774
|
-
requireSingleChange(this.database, updated.changes, "increment open count");
|
|
1775
|
-
return next;
|
|
1006
|
+
imageAttachments.delete(messageId);
|
|
1007
|
+
toolContentBlocks.delete(messageId);
|
|
1008
|
+
return message;
|
|
1776
1009
|
});
|
|
1777
|
-
|
|
1778
|
-
throw
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
recoverInterruptedState(idFactory: RuntimeIdFactory): SessionRecoveryResult {
|
|
1783
|
-
this.requireOpen();
|
|
1784
|
-
const view = this.loadProtocolView();
|
|
1785
|
-
const openTurns = this.database
|
|
1786
|
-
.query("SELECT turn_id FROM turns WHERE status = 'open' ORDER BY turn_number")
|
|
1787
|
-
.all() as Array<{ turn_id: string }>;
|
|
1788
|
-
const openFrames = view.frames.filter((frame) => frame.state === "open");
|
|
1789
|
-
if (openTurns.length === 0 && openFrames.length === 0) {
|
|
1790
|
-
return {
|
|
1791
|
-
syntheticCompletionCount: 0,
|
|
1792
|
-
recallIndexRebuilt: this.recallIndexRebuilt,
|
|
1793
|
-
};
|
|
1010
|
+
if (imageAttachments.size > 0) {
|
|
1011
|
+
throw new Error("Image attachment rows reference unknown messages.");
|
|
1794
1012
|
}
|
|
1795
|
-
if (
|
|
1796
|
-
throw
|
|
1797
|
-
"Session has an invalid number of open turns or frames.",
|
|
1798
|
-
);
|
|
1013
|
+
if (toolContentBlocks.size > 0) {
|
|
1014
|
+
throw new Error("Tool content block rows reference unknown messages.");
|
|
1799
1015
|
}
|
|
1800
|
-
const
|
|
1801
|
-
const openIterations = this.database
|
|
1016
|
+
const toolResults = this.database
|
|
1802
1017
|
.query(
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
if (openIterations.length > 1) {
|
|
1807
|
-
throw this.recoveryError(`Turn ${turnId} has multiple open iterations.`);
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
const frame = openFrames[0];
|
|
1811
|
-
if (frame === undefined) {
|
|
1812
|
-
this.markOpenTurnInterrupted(
|
|
1813
|
-
turnId,
|
|
1814
|
-
openIterations[0]?.iteration_id as IterationId | undefined,
|
|
1815
|
-
);
|
|
1816
|
-
this.validateAll({ allowOpenTail: false });
|
|
1817
|
-
return {
|
|
1818
|
-
recoveredTurnId: turnId,
|
|
1819
|
-
syntheticCompletionCount: 0,
|
|
1820
|
-
recallIndexRebuilt: this.recallIndexRebuilt,
|
|
1821
|
-
};
|
|
1822
|
-
}
|
|
1823
|
-
if (
|
|
1824
|
-
frame.turnId !== turnId ||
|
|
1825
|
-
frame !== view.frames.at(-1) ||
|
|
1826
|
-
openIterations.length !== 1 ||
|
|
1827
|
-
frame.iterationId !== openIterations[0]?.iteration_id
|
|
1828
|
-
) {
|
|
1829
|
-
throw this.recoveryError(`Open frame ${frame.frameId} has invalid ownership.`);
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
const frameMessages = view.messages.filter(
|
|
1833
|
-
(message) => message.frameId === frame.frameId,
|
|
1834
|
-
);
|
|
1835
|
-
const assistant = frameMessages[0];
|
|
1836
|
-
if (assistant?.role !== "assistant" || assistant.toolCalls === undefined) {
|
|
1837
|
-
throw this.recoveryError(`Open frame ${frame.frameId} has no tool calls.`);
|
|
1838
|
-
}
|
|
1839
|
-
const missingCalls = assistant.toolCalls.slice(frameMessages.length - 1);
|
|
1840
|
-
if (missingCalls.length === 0) {
|
|
1841
|
-
throw this.recoveryError(`Open frame ${frame.frameId} has no missing call.`);
|
|
1842
|
-
}
|
|
1843
|
-
const completionInputs = interruptedCompletionInputs(missingCalls);
|
|
1844
|
-
const messages: CanonicalMessageRecord[] = [];
|
|
1845
|
-
const toolResults: ToolResultRecord[] = [];
|
|
1846
|
-
for (const input of completionInputs) {
|
|
1847
|
-
const createdAt = this.clock();
|
|
1848
|
-
const content = observationForCompletion(input);
|
|
1849
|
-
const displayText = toolResultDisplayText(content);
|
|
1850
|
-
const messageId = idFactory.createMessageId();
|
|
1851
|
-
const message = immutableRecord<CanonicalMessageRecord>({
|
|
1852
|
-
messageId,
|
|
1853
|
-
sessionId: this.sessionId,
|
|
1854
|
-
frameId: frame.frameId,
|
|
1855
|
-
ordinal: view.messages.length + messages.length + 1,
|
|
1856
|
-
contentSha256: canonicalToolResultContentHash(content),
|
|
1857
|
-
createdAt,
|
|
1858
|
-
role: "tool",
|
|
1859
|
-
turnId,
|
|
1860
|
-
iterationId: frame.iterationId,
|
|
1861
|
-
toolCallId: input.call.toolCallId,
|
|
1862
|
-
providerToolCallId: input.call.providerToolCallId,
|
|
1863
|
-
name: input.call.name,
|
|
1864
|
-
content,
|
|
1865
|
-
displayText,
|
|
1866
|
-
origin: "runtime",
|
|
1867
|
-
});
|
|
1868
|
-
const completion: ToolCompletion = immutableRecord({
|
|
1869
|
-
kind: "synthetic",
|
|
1870
|
-
reason: input.reason,
|
|
1871
|
-
});
|
|
1872
|
-
const result = immutableRecord<ToolResultRecord>({
|
|
1873
|
-
sessionId: this.sessionId,
|
|
1874
|
-
frameId: frame.frameId,
|
|
1875
|
-
toolCallId: input.call.toolCallId,
|
|
1876
|
-
toolMessageId: messageId,
|
|
1877
|
-
completion,
|
|
1878
|
-
observationSha256: canonicalToolResultContentHash(content),
|
|
1879
|
-
createdAt,
|
|
1880
|
-
});
|
|
1881
|
-
messages.push(message);
|
|
1882
|
-
toolResults.push(result);
|
|
1883
|
-
}
|
|
1884
|
-
const closedAt = this.clock();
|
|
1885
|
-
const closedFrame = immutableRecord<ProtocolFrame>({
|
|
1886
|
-
...frame,
|
|
1887
|
-
state: "closed",
|
|
1888
|
-
lastOrdinal: view.messages.length + messages.length,
|
|
1889
|
-
closedAt,
|
|
1890
|
-
});
|
|
1891
|
-
const candidate: ProtocolContextView = Object.freeze({
|
|
1892
|
-
...view,
|
|
1893
|
-
frames: Object.freeze(
|
|
1894
|
-
view.frames.map((entry) =>
|
|
1895
|
-
entry.frameId === frame.frameId ? closedFrame : entry,
|
|
1896
|
-
),
|
|
1897
|
-
),
|
|
1898
|
-
messages: Object.freeze([...view.messages, ...messages]),
|
|
1899
|
-
toolResults: Object.freeze([...view.toolResults, ...toolResults]),
|
|
1900
|
-
});
|
|
1901
|
-
this.validator.validate(candidate, { fullIntegrity: true });
|
|
1902
|
-
|
|
1903
|
-
try {
|
|
1904
|
-
runTransaction(this.database, () => {
|
|
1905
|
-
for (let index = 0; index < messages.length; index += 1) {
|
|
1906
|
-
insertMessage(
|
|
1907
|
-
this.database,
|
|
1908
|
-
requireItem(messages, index, "recovery message"),
|
|
1909
|
-
);
|
|
1910
|
-
insertToolResult(
|
|
1911
|
-
this.database,
|
|
1912
|
-
requireItem(toolResults, index, "recovery tool result"),
|
|
1913
|
-
);
|
|
1914
|
-
}
|
|
1915
|
-
const frameUpdate = this.database
|
|
1916
|
-
.query(
|
|
1917
|
-
`UPDATE protocol_frames SET state = 'closed', last_ordinal = ?, closed_at = ?
|
|
1918
|
-
WHERE frame_id = ? AND state = 'open' AND last_ordinal IS NULL`,
|
|
1919
|
-
)
|
|
1920
|
-
.run(closedFrame.lastOrdinal!, closedAt, frame.frameId);
|
|
1921
|
-
requireSingleChange(
|
|
1922
|
-
this.database,
|
|
1923
|
-
frameUpdate.changes,
|
|
1924
|
-
"close recovered frame",
|
|
1925
|
-
);
|
|
1926
|
-
this.markTerminalRows(
|
|
1927
|
-
turnId,
|
|
1928
|
-
frame.iterationId!,
|
|
1929
|
-
"interrupted",
|
|
1930
|
-
"interrupted",
|
|
1931
|
-
null,
|
|
1932
|
-
stableJsonStringify({ version: 1, reason: "process_interrupted" }),
|
|
1933
|
-
closedAt,
|
|
1934
|
-
);
|
|
1935
|
-
});
|
|
1936
|
-
} catch (error) {
|
|
1937
|
-
throw new SessionError(
|
|
1938
|
-
"SESSION_RECOVERY_FAILED",
|
|
1939
|
-
"recover_open_frame",
|
|
1940
|
-
`Failed to recover open frame ${frame.frameId}.`,
|
|
1941
|
-
{ sessionId: this.sessionId, frameId: frame.frameId, cause: error },
|
|
1942
|
-
);
|
|
1943
|
-
}
|
|
1944
|
-
this.validateAll({ allowOpenTail: false });
|
|
1945
|
-
return {
|
|
1946
|
-
recoveredTurnId: turnId,
|
|
1947
|
-
recoveredFrameId: frame.frameId,
|
|
1948
|
-
syntheticCompletionCount: messages.length,
|
|
1949
|
-
recallIndexRebuilt: this.recallIndexRebuilt,
|
|
1950
|
-
};
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
historyReader(): SessionHistoryReader {
|
|
1954
|
-
this.requireOpen();
|
|
1955
|
-
return createSessionHistoryReader({
|
|
1956
|
-
database: this.database,
|
|
1957
|
-
sessionId: this.sessionId,
|
|
1958
|
-
requireOpen: () => this.requireOpen(),
|
|
1959
|
-
});
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
readCompletedTurnSnapshot(turnId: TurnId): CompletedTurnSnapshot {
|
|
1963
|
-
this.requireOpen();
|
|
1964
|
-
const turnRow = this.database
|
|
1965
|
-
.query("SELECT status FROM turns WHERE turn_id = ?")
|
|
1966
|
-
.get(turnId);
|
|
1967
|
-
const status = enumFromSql(
|
|
1968
|
-
recordFromSql(turnRow, "completed turn").status,
|
|
1969
|
-
["open", "completed", "failed", "cancelled", "interrupted"] as const,
|
|
1970
|
-
"turn status",
|
|
1971
|
-
);
|
|
1972
|
-
if (status !== "completed") {
|
|
1973
|
-
throw new Error(`Turn ${turnId} is not completed.`);
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
const rows = this.database
|
|
1977
|
-
.query(
|
|
1978
|
-
`SELECT ordinal, role, content, reasoning_content,
|
|
1979
|
-
reasoning_content_present, name
|
|
1980
|
-
FROM messages
|
|
1981
|
-
WHERE turn_id = ?
|
|
1982
|
-
ORDER BY ordinal`,
|
|
1983
|
-
)
|
|
1984
|
-
.all(turnId);
|
|
1985
|
-
if (rows.length === 0) {
|
|
1986
|
-
throw new Error(`Completed turn ${turnId} has no messages.`);
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
let previousOrdinal = 0;
|
|
1990
|
-
const messages = rows.map((value): CompletedTurnMessageSnapshot => {
|
|
1991
|
-
const row = recordFromSql(value, "completed turn message");
|
|
1992
|
-
const ordinal = numberFromSql(row.ordinal, "completed turn ordinal");
|
|
1993
|
-
if (ordinal < 1 || ordinal <= previousOrdinal) {
|
|
1994
|
-
throw new Error("Completed turn message ordinals are invalid.");
|
|
1995
|
-
}
|
|
1996
|
-
previousOrdinal = ordinal;
|
|
1997
|
-
const role = enumFromSql(
|
|
1998
|
-
row.role,
|
|
1999
|
-
["user", "assistant", "tool"] as const,
|
|
2000
|
-
"completed turn message role",
|
|
2001
|
-
);
|
|
2002
|
-
if (role === "user") {
|
|
2003
|
-
if (
|
|
2004
|
-
row.reasoning_content !== null ||
|
|
2005
|
-
numberFromSql(row.reasoning_content_present, "reasoning_content_present") !==
|
|
2006
|
-
0 ||
|
|
2007
|
-
row.name !== null
|
|
2008
|
-
) {
|
|
2009
|
-
throw new Error("Completed user message fields are invalid.");
|
|
2010
|
-
}
|
|
2011
|
-
return Object.freeze({
|
|
2012
|
-
ordinal,
|
|
2013
|
-
role,
|
|
2014
|
-
content: stringFromSql(row.content, "completed user content"),
|
|
2015
|
-
});
|
|
2016
|
-
}
|
|
2017
|
-
if (role === "assistant") {
|
|
2018
|
-
if (row.name !== null) {
|
|
2019
|
-
throw new Error("Completed assistant message name must be null.");
|
|
2020
|
-
}
|
|
2021
|
-
const reasoningPresent = numberFromSql(
|
|
2022
|
-
row.reasoning_content_present,
|
|
2023
|
-
"reasoning_content_present",
|
|
2024
|
-
);
|
|
2025
|
-
if (reasoningPresent !== 0 && reasoningPresent !== 1) {
|
|
2026
|
-
throw new Error("reasoning_content_present must be 0 or 1.");
|
|
2027
|
-
}
|
|
2028
|
-
if (reasoningPresent === 0 && row.reasoning_content !== null) {
|
|
2029
|
-
throw new Error("Absent assistant reasoning content must be null.");
|
|
2030
|
-
}
|
|
2031
|
-
return Object.freeze({
|
|
2032
|
-
ordinal,
|
|
2033
|
-
role,
|
|
2034
|
-
content: nullableTextFromSql(row.content, "completed assistant content"),
|
|
2035
|
-
...(reasoningPresent === 0
|
|
2036
|
-
? {}
|
|
2037
|
-
: {
|
|
2038
|
-
reasoningContent: nullableTextFromSql(
|
|
2039
|
-
row.reasoning_content,
|
|
2040
|
-
"completed assistant reasoning content",
|
|
2041
|
-
),
|
|
2042
|
-
}),
|
|
2043
|
-
});
|
|
2044
|
-
}
|
|
2045
|
-
if (
|
|
2046
|
-
row.reasoning_content !== null ||
|
|
2047
|
-
numberFromSql(row.reasoning_content_present, "reasoning_content_present") !== 0
|
|
2048
|
-
) {
|
|
2049
|
-
throw new Error("Completed tool message reasoning fields are invalid.");
|
|
2050
|
-
}
|
|
2051
|
-
return Object.freeze({
|
|
2052
|
-
ordinal,
|
|
2053
|
-
role,
|
|
2054
|
-
name: stringFromSql(row.name, "completed tool name"),
|
|
2055
|
-
content: stringFromSql(row.content, "completed tool content"),
|
|
2056
|
-
});
|
|
2057
|
-
});
|
|
2058
|
-
return Object.freeze({ messages: Object.freeze(messages) });
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
loadProtocolView(): ProtocolContextView {
|
|
2062
|
-
this.requireOpen();
|
|
2063
|
-
const imageAttachments = loadMessageImageAttachments(this.database);
|
|
2064
|
-
const toolContentBlocks = loadToolMessageContentBlocks(this.database);
|
|
2065
|
-
const frames = this.database
|
|
2066
|
-
.query("SELECT * FROM protocol_frames ORDER BY first_ordinal")
|
|
2067
|
-
.all()
|
|
2068
|
-
.map(decodeFrame);
|
|
2069
|
-
const messages = this.database
|
|
2070
|
-
.query("SELECT * FROM messages ORDER BY ordinal")
|
|
2071
|
-
.all()
|
|
2072
|
-
.map((row) => {
|
|
2073
|
-
const record = recordFromSql(row, "message");
|
|
2074
|
-
const messageId = stringFromSql(record.message_id, "message_id");
|
|
2075
|
-
const message = decodeMessage(
|
|
2076
|
-
row,
|
|
2077
|
-
imageAttachments.get(messageId),
|
|
2078
|
-
toolContentBlocks.get(messageId),
|
|
2079
|
-
);
|
|
2080
|
-
imageAttachments.delete(messageId);
|
|
2081
|
-
toolContentBlocks.delete(messageId);
|
|
2082
|
-
return message;
|
|
2083
|
-
});
|
|
2084
|
-
if (imageAttachments.size > 0) {
|
|
2085
|
-
throw new Error("Image attachment rows reference unknown messages.");
|
|
2086
|
-
}
|
|
2087
|
-
if (toolContentBlocks.size > 0) {
|
|
2088
|
-
throw new Error("Tool content block rows reference unknown messages.");
|
|
2089
|
-
}
|
|
2090
|
-
const toolResults = this.database
|
|
2091
|
-
.query(
|
|
2092
|
-
`SELECT tr.* FROM tool_results tr
|
|
2093
|
-
JOIN messages m ON m.message_id = tr.tool_message_id
|
|
2094
|
-
ORDER BY m.ordinal`,
|
|
1018
|
+
`SELECT tr.* FROM tool_results tr
|
|
1019
|
+
JOIN messages m ON m.message_id = tr.tool_message_id
|
|
1020
|
+
ORDER BY m.ordinal`,
|
|
2095
1021
|
)
|
|
2096
1022
|
.all()
|
|
2097
1023
|
.map(decodeToolResult);
|
|
2098
|
-
return Object.freeze({
|
|
2099
|
-
sessionId: this.sessionId,
|
|
2100
|
-
faulted: false,
|
|
2101
|
-
frames: Object.freeze(frames),
|
|
2102
|
-
messages: Object.freeze(messages),
|
|
2103
|
-
toolResults: Object.freeze(toolResults),
|
|
2104
|
-
});
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
async verifyImageAssetFiles(): Promise<void> {
|
|
2108
|
-
this.requireOpen();
|
|
2109
|
-
const distinct = new Map<string, ImageAssetRef>();
|
|
2110
|
-
for (const message of this.loadProtocolView().messages) {
|
|
2111
|
-
const assets =
|
|
2112
|
-
message.role === "user"
|
|
2113
|
-
? (message.attachments ?? []).map(imageAssetRefFromAttachment)
|
|
2114
|
-
: message.role === "tool"
|
|
2115
|
-
? message.content.flatMap((block) =>
|
|
2116
|
-
block.type === "image" ? [block.asset] : [],
|
|
2117
|
-
)
|
|
2118
|
-
: [];
|
|
2119
|
-
for (const asset of assets) {
|
|
2120
|
-
const previous = distinct.get(asset.assetId);
|
|
2121
|
-
if (
|
|
2122
|
-
previous !== undefined &&
|
|
2123
|
-
stableJsonStringify(previous) !== stableJsonStringify(asset)
|
|
2124
|
-
) {
|
|
2125
|
-
throw new Error(`Conflicting metadata for image asset ${asset.assetId}.`);
|
|
2126
|
-
}
|
|
2127
|
-
distinct.set(asset.assetId, asset);
|
|
2128
|
-
}
|
|
2129
|
-
}
|
|
2130
|
-
if (distinct.size === 0) {
|
|
2131
|
-
return;
|
|
2132
|
-
}
|
|
2133
|
-
const store = await ImageAssetStore.open({
|
|
2134
|
-
workspaceRoot: this.workspaceRoot,
|
|
2135
|
-
...(this.homeRoot === undefined ? {} : { homeRoot: this.homeRoot }),
|
|
2136
|
-
});
|
|
2137
|
-
for (const asset of distinct.values()) {
|
|
2138
|
-
await store.verify(asset);
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
loadContextSnapshot(): StoredContextSnapshotV8 {
|
|
2143
|
-
this.requireOpen();
|
|
2144
|
-
const meta = this.readMeta();
|
|
2145
|
-
try {
|
|
2146
|
-
if (
|
|
2147
|
-
meta.sessionId !== this.sessionId ||
|
|
2148
|
-
meta.schemaFingerprint !== SESSION_SCHEMA_V10_FINGERPRINT
|
|
2149
|
-
) {
|
|
2150
|
-
throw new Error("Session metadata identity or schema fingerprint changed.");
|
|
2151
|
-
}
|
|
2152
|
-
const canonical = this.loadProtocolView();
|
|
2153
|
-
this.validator.validate(canonical, { fullIntegrity: true });
|
|
2154
|
-
const systemFrame = canonical.frames[0];
|
|
2155
|
-
const systemMessage = canonical.messages[0];
|
|
2156
|
-
if (
|
|
2157
|
-
systemFrame?.kind !== "system" ||
|
|
2158
|
-
systemFrame.firstOrdinal !== 1 ||
|
|
2159
|
-
systemMessage?.role !== "system" ||
|
|
2160
|
-
systemMessage.ordinal !== 1 ||
|
|
2161
|
-
sha256(systemMessage.content) !== meta.systemPromptSha256 ||
|
|
2162
|
-
canonical.messages.at(-1)?.ordinal !== canonical.messages.length
|
|
2163
|
-
) {
|
|
2164
|
-
throw new Error("Stored context snapshot ordinal or system invariant failed.");
|
|
2165
|
-
}
|
|
2166
|
-
return this.loadValidatedContextSnapshot(meta, canonical);
|
|
2167
|
-
} catch (error) {
|
|
2168
|
-
if (error instanceof SessionError) {
|
|
2169
|
-
throw error;
|
|
2170
|
-
}
|
|
2171
|
-
if (error instanceof ContextProtocolError) {
|
|
2172
|
-
throw new SessionError(
|
|
2173
|
-
"SESSION_PROTOCOL_INVALID",
|
|
2174
|
-
"load_context_snapshot",
|
|
2175
|
-
error.message,
|
|
2176
|
-
{
|
|
2177
|
-
sessionId: this.sessionId,
|
|
2178
|
-
frameId: error.frameId,
|
|
2179
|
-
messageId: error.messageId,
|
|
2180
|
-
toolCallId: error.toolCallId,
|
|
2181
|
-
cause: error,
|
|
2182
|
-
},
|
|
2183
|
-
);
|
|
2184
|
-
}
|
|
2185
|
-
throw new SessionError(
|
|
2186
|
-
"SESSION_INTEGRITY_FAILED",
|
|
2187
|
-
"load_context_snapshot",
|
|
2188
|
-
`Session context snapshot validation failed: ${errorMessage(error)}.`,
|
|
2189
|
-
{ sessionId: this.sessionId, cause: error },
|
|
2190
|
-
);
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
|
|
2194
|
-
readMeta(): StoredSessionMetaV10 {
|
|
2195
|
-
this.requireOpen();
|
|
2196
|
-
const rows = this.database.query("SELECT * FROM session_meta").all();
|
|
2197
|
-
if (rows.length !== 1) {
|
|
2198
|
-
throw new SessionError(
|
|
2199
|
-
"SESSION_INTEGRITY_FAILED",
|
|
2200
|
-
"read_meta",
|
|
2201
|
-
`Session metadata must contain exactly one row; found ${rows.length}.`,
|
|
2202
|
-
{ sessionId: this.sessionId },
|
|
2203
|
-
);
|
|
2204
|
-
}
|
|
2205
|
-
return decodeMeta(rows[0], this.sessionId);
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
readCreationSystemPrompt(): string {
|
|
2209
|
-
this.requireOpen();
|
|
2210
|
-
try {
|
|
2211
|
-
const frames = this.database
|
|
2212
|
-
.query("SELECT * FROM protocol_frames WHERE kind = 'system'")
|
|
2213
|
-
.all();
|
|
2214
|
-
if (frames.length !== 1) {
|
|
2215
|
-
throw new Error(`Expected one stored system frame; found ${frames.length}.`);
|
|
2216
|
-
}
|
|
2217
|
-
const frame = recordFromSql(frames[0], "stored system frame");
|
|
2218
|
-
if (
|
|
2219
|
-
frame.state !== "closed" ||
|
|
2220
|
-
numberFromSql(frame.first_ordinal, "first_ordinal") !== 1 ||
|
|
2221
|
-
numberFromSql(frame.last_ordinal, "last_ordinal") !== 1
|
|
2222
|
-
) {
|
|
2223
|
-
throw new Error("Stored system frame invariant failed.");
|
|
2224
|
-
}
|
|
2225
|
-
const frameId = stringFromSql(frame.frame_id, "frame_id");
|
|
2226
|
-
const messages = this.database
|
|
2227
|
-
.query("SELECT * FROM messages WHERE frame_id = ?")
|
|
2228
|
-
.all(frameId);
|
|
2229
|
-
if (messages.length !== 1) {
|
|
2230
|
-
throw new Error(
|
|
2231
|
-
`Expected one stored system message; found ${messages.length}.`,
|
|
2232
|
-
);
|
|
2233
|
-
}
|
|
2234
|
-
const row = recordFromSql(messages[0], "stored system message");
|
|
2235
|
-
if (
|
|
2236
|
-
numberFromSql(row.ordinal, "ordinal") !== 1 ||
|
|
2237
|
-
row.role !== "system" ||
|
|
2238
|
-
row.origin !== "runtime"
|
|
2239
|
-
) {
|
|
2240
|
-
throw new Error("Stored system message invariant failed.");
|
|
2241
|
-
}
|
|
2242
|
-
const content = stringFromSql(row.content, "content");
|
|
2243
|
-
if (content.trim() === "") {
|
|
2244
|
-
throw new Error("Stored system prompt must not be empty.");
|
|
2245
|
-
}
|
|
2246
|
-
if (
|
|
2247
|
-
stringFromSql(row.content_sha256, "content_sha256") !== contentHash(content)
|
|
2248
|
-
) {
|
|
2249
|
-
throw new Error("Stored system message content hash does not match.");
|
|
2250
|
-
}
|
|
2251
|
-
if (this.readMeta().systemPromptSha256 !== sha256(content)) {
|
|
2252
|
-
throw new Error("Stored system prompt metadata hash does not match.");
|
|
2253
|
-
}
|
|
2254
|
-
return content;
|
|
2255
|
-
} catch (error) {
|
|
2256
|
-
if (error instanceof SessionError && error.code === "SESSION_RECOVERY_FAILED") {
|
|
2257
|
-
throw error;
|
|
2258
|
-
}
|
|
2259
|
-
throw new SessionError(
|
|
2260
|
-
"SESSION_RECOVERY_FAILED",
|
|
2261
|
-
"read_creation_system_prompt",
|
|
2262
|
-
"Creation system prompt is missing or invalid.",
|
|
2263
|
-
{ sessionId: this.sessionId, cause: error },
|
|
2264
|
-
);
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
readProjectInstructionManifest(): ProjectInstructionManifest | undefined {
|
|
2269
|
-
return this.readMeta().projectInstruction;
|
|
2270
|
-
}
|
|
2271
|
-
|
|
2272
|
-
validateCreatingState(): void {
|
|
2273
|
-
this.requireOpen();
|
|
2274
|
-
const meta = this.readMeta();
|
|
2275
|
-
if (
|
|
2276
|
-
meta.initializationState !== "creating" ||
|
|
2277
|
-
meta.activeRevisionId !== null ||
|
|
2278
|
-
meta.sessionCompatibilityJson !== null ||
|
|
2279
|
-
meta.sessionCompatibilitySha256 !== null
|
|
2280
|
-
) {
|
|
2281
|
-
throw new SessionError(
|
|
2282
|
-
"SESSION_INTEGRITY_FAILED",
|
|
2283
|
-
"validate_creating_store",
|
|
2284
|
-
"Creating session metadata is invalid.",
|
|
2285
|
-
{ sessionId: this.sessionId },
|
|
2286
|
-
);
|
|
2287
|
-
}
|
|
2288
|
-
this.readCreationSystemPrompt();
|
|
2289
|
-
const counts = this.database
|
|
2290
|
-
.query(
|
|
2291
|
-
`SELECT
|
|
2292
|
-
(SELECT COUNT(*) FROM context_surfaces) AS surfaces,
|
|
2293
|
-
(SELECT COUNT(*) FROM context_revisions) AS revisions,
|
|
2294
|
-
(SELECT COUNT(*) FROM turns) AS turns`,
|
|
2295
|
-
)
|
|
2296
|
-
.get() as Record<string, unknown> | null;
|
|
2297
|
-
if (
|
|
2298
|
-
counts === null ||
|
|
2299
|
-
numberFromSql(counts.surfaces, "surfaces") !== 0 ||
|
|
2300
|
-
numberFromSql(counts.revisions, "revisions") !== 0 ||
|
|
2301
|
-
numberFromSql(counts.turns, "turns") !== 0
|
|
2302
|
-
) {
|
|
2303
|
-
throw new SessionError(
|
|
2304
|
-
"SESSION_INTEGRITY_FAILED",
|
|
2305
|
-
"validate_creating_store",
|
|
2306
|
-
"Creating session contains finalized or turn state.",
|
|
2307
|
-
{ sessionId: this.sessionId },
|
|
2308
|
-
);
|
|
2309
|
-
}
|
|
2310
|
-
const view = this.loadProtocolView();
|
|
2311
|
-
this.validator.validate(view, { fullIntegrity: true });
|
|
2312
|
-
if (view.messages.length !== 1 || view.frames.length !== 1) {
|
|
2313
|
-
throw new SessionError(
|
|
2314
|
-
"SESSION_INTEGRITY_FAILED",
|
|
2315
|
-
"validate_creating_store",
|
|
2316
|
-
"Creating session must contain only its creation system frame.",
|
|
2317
|
-
{ sessionId: this.sessionId },
|
|
2318
|
-
);
|
|
2319
|
-
}
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
nextTurnNumber(): number {
|
|
2323
|
-
return this.readMeta().nextTurnNumber;
|
|
2324
|
-
}
|
|
2325
|
-
|
|
2326
|
-
validateAll(options: { allowOpenTail: boolean }): ProtocolContextView {
|
|
2327
|
-
const meta = this.readMeta();
|
|
2328
|
-
if (
|
|
2329
|
-
meta.sessionId !== this.sessionId ||
|
|
2330
|
-
meta.schemaFingerprint !== SESSION_SCHEMA_V10_FINGERPRINT ||
|
|
2331
|
-
meta.initializationState !== "ready" ||
|
|
2332
|
-
meta.activeRevisionId === null
|
|
2333
|
-
) {
|
|
2334
|
-
throw new SessionError(
|
|
2335
|
-
"SESSION_SCHEMA_INVALID",
|
|
2336
|
-
"validate_store",
|
|
2337
|
-
"Session metadata identity or schema fingerprint does not match.",
|
|
2338
|
-
{ sessionId: this.sessionId },
|
|
2339
|
-
);
|
|
2340
|
-
}
|
|
2341
|
-
try {
|
|
2342
|
-
this.readCreationSystemPrompt();
|
|
2343
|
-
const view = this.loadProtocolView();
|
|
2344
|
-
this.validator.validate(view, {
|
|
2345
|
-
allowOpenTail: options.allowOpenTail,
|
|
2346
|
-
fullIntegrity: true,
|
|
2347
|
-
});
|
|
2348
|
-
this.loadValidatedContextSnapshot(meta, view);
|
|
2349
|
-
this.validateCounters(meta, view);
|
|
2350
|
-
return view;
|
|
2351
|
-
} catch (error) {
|
|
2352
|
-
if (error instanceof SessionError) {
|
|
2353
|
-
throw error;
|
|
2354
|
-
}
|
|
2355
|
-
if (error instanceof ContextProtocolError) {
|
|
2356
|
-
throw new SessionError(
|
|
2357
|
-
"SESSION_PROTOCOL_INVALID",
|
|
2358
|
-
"validate_store",
|
|
2359
|
-
error.message,
|
|
2360
|
-
{
|
|
2361
|
-
sessionId: this.sessionId,
|
|
2362
|
-
frameId: error.frameId,
|
|
2363
|
-
messageId: error.messageId,
|
|
2364
|
-
toolCallId: error.toolCallId,
|
|
2365
|
-
cause: error,
|
|
2366
|
-
},
|
|
2367
|
-
);
|
|
2368
|
-
}
|
|
2369
|
-
throw new SessionError(
|
|
2370
|
-
"SESSION_INTEGRITY_FAILED",
|
|
2371
|
-
"validate_store",
|
|
2372
|
-
`Session record validation failed: ${errorMessage(error)}.`,
|
|
2373
|
-
{ sessionId: this.sessionId, cause: error },
|
|
2374
|
-
);
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
|
|
2378
|
-
async cloneTo(input: CloneSessionStoreInput): Promise<void> {
|
|
2379
|
-
this.requireOpen();
|
|
2380
|
-
if (input.targetSessionId === this.sessionId) {
|
|
2381
|
-
throw new Error("Session clone target must differ from the source session.");
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
const sourceView = this.validateAll({ allowOpenTail: false });
|
|
2385
|
-
const sourceMeta = this.readMeta();
|
|
2386
|
-
const sessionsRoot = path.dirname(this.sessionDirectory);
|
|
2387
|
-
await validateSessionsRoot(sessionsRoot, this.sessionId);
|
|
2388
|
-
const targetDirectory = safeSessionDirectory(sessionsRoot, input.targetSessionId);
|
|
2389
|
-
await assertPathMissing(targetDirectory, input.targetSessionId);
|
|
2390
|
-
|
|
2391
|
-
const stagingDirectory = path.join(sessionsRoot, `.cloning-${randomUUID()}`);
|
|
2392
|
-
const stagingDatabasePath = path.join(stagingDirectory, "session.sqlite");
|
|
2393
|
-
let stagingDatabase: Database | undefined;
|
|
2394
|
-
let published = false;
|
|
2395
|
-
|
|
2396
|
-
try {
|
|
2397
|
-
await mkdir(stagingDirectory, { mode: 0o700 });
|
|
2398
|
-
await chmod(stagingDirectory, 0o700);
|
|
2399
|
-
input.faultInjector?.("after_staging_mkdir");
|
|
2400
|
-
this.database.query("VACUUM INTO ?").run(stagingDatabasePath);
|
|
2401
|
-
await chmod(stagingDatabasePath, 0o600);
|
|
2402
|
-
input.faultInjector?.("after_snapshot");
|
|
2403
|
-
|
|
2404
|
-
stagingDatabase = openWritableDatabase(stagingDatabasePath);
|
|
2405
|
-
verifySessionSchema(stagingDatabase, this.sessionId);
|
|
2406
|
-
dropSessionCloneTriggers(stagingDatabase);
|
|
2407
|
-
input.faultInjector?.("after_trigger_drop");
|
|
2408
|
-
|
|
2409
|
-
const targetCanonical = rekeyProtocolView(sourceView, input.targetSessionId);
|
|
2410
|
-
runTransaction(stagingDatabase, () => {
|
|
2411
|
-
stagingDatabase!.exec("PRAGMA defer_foreign_keys = ON");
|
|
2412
|
-
for (const table of SESSION_SCOPED_TABLES) {
|
|
2413
|
-
stagingDatabase!
|
|
2414
|
-
.query(`UPDATE ${table} SET session_id = ? WHERE session_id = ?`)
|
|
2415
|
-
.run(input.targetSessionId, this.sessionId);
|
|
2416
|
-
}
|
|
2417
|
-
rekeyStoredToolCalls(stagingDatabase!, input.targetSessionId);
|
|
2418
|
-
input.faultInjector?.("after_identity_update");
|
|
2419
|
-
|
|
2420
|
-
rewriteCloneRevisionHashes(stagingDatabase!, targetCanonical);
|
|
2421
|
-
input.faultInjector?.("after_revision_hash_rewrite");
|
|
2422
|
-
|
|
2423
|
-
if (stagingDatabase!.query("PRAGMA foreign_key_check").all().length !== 0) {
|
|
2424
|
-
throw new Error("Cloned session identity re-key broke foreign keys.");
|
|
2425
|
-
}
|
|
2426
|
-
});
|
|
2427
|
-
|
|
2428
|
-
reinstallSessionCloneTriggers(stagingDatabase);
|
|
2429
|
-
input.faultInjector?.("after_trigger_reinstall");
|
|
2430
|
-
verifySessionSchema(stagingDatabase, input.targetSessionId);
|
|
2431
|
-
verifySqliteIntegrity(stagingDatabase, input.targetSessionId);
|
|
2432
|
-
verifyRecallIndex(stagingDatabase, input.targetSessionId);
|
|
2433
|
-
input.faultInjector?.("after_recall_validation");
|
|
2434
|
-
const clonedStore = new SessionStore(stagingDatabase, this.lease, {
|
|
2435
|
-
sessionId: input.targetSessionId,
|
|
2436
|
-
workspaceRoot: this.workspaceRoot,
|
|
2437
|
-
sessionDirectory: stagingDirectory,
|
|
2438
|
-
databasePath: stagingDatabasePath,
|
|
2439
|
-
clock: this.clock,
|
|
2440
|
-
...(this.homeRoot === undefined ? {} : { homeRoot: this.homeRoot }),
|
|
2441
|
-
});
|
|
2442
|
-
clonedStore.validateAll({ allowOpenTail: false });
|
|
2443
|
-
await clonedStore.verifyImageAssetFiles();
|
|
2444
|
-
|
|
2445
|
-
await cloneDiagnosticFiles({
|
|
2446
|
-
sourceDirectory: this.sessionDirectory,
|
|
2447
|
-
stagingDirectory,
|
|
2448
|
-
sourceSessionId: this.sessionId,
|
|
2449
|
-
targetSessionId: input.targetSessionId,
|
|
2450
|
-
nextEventSequence: sourceMeta.nextEventSequence,
|
|
2451
|
-
faultInjector: input.faultInjector,
|
|
2452
|
-
});
|
|
2453
|
-
|
|
2454
|
-
stagingDatabase.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
2455
|
-
const standaloneJournal = stagingDatabase
|
|
2456
|
-
.query("PRAGMA journal_mode = DELETE")
|
|
2457
|
-
.get() as Record<string, unknown> | null;
|
|
2458
|
-
if (String(standaloneJournal?.journal_mode).toLowerCase() !== "delete") {
|
|
2459
|
-
throw new Error("Cloned session database did not leave WAL mode.");
|
|
2460
|
-
}
|
|
2461
|
-
stagingDatabase.close();
|
|
2462
|
-
stagingDatabase = undefined;
|
|
2463
|
-
await unlinkIfExists(`${stagingDatabasePath}-wal`);
|
|
2464
|
-
await unlinkIfExists(`${stagingDatabasePath}-shm`);
|
|
2465
|
-
await validateSecureDirectory(stagingDirectory, input.targetSessionId);
|
|
2466
|
-
await validateSecureFile(stagingDatabasePath, input.targetSessionId);
|
|
2467
|
-
await validateSecureOptionalFile(
|
|
2468
|
-
path.join(stagingDirectory, "events.jsonl"),
|
|
2469
|
-
input.targetSessionId,
|
|
2470
|
-
);
|
|
2471
|
-
await validateSecureOptionalFile(
|
|
2472
|
-
path.join(stagingDirectory, "observations.md"),
|
|
2473
|
-
input.targetSessionId,
|
|
2474
|
-
);
|
|
2475
|
-
input.faultInjector?.("after_artifact_validation");
|
|
2476
|
-
await assertPathMissing(targetDirectory, input.targetSessionId);
|
|
2477
|
-
input.faultInjector?.("before_publish_rename");
|
|
2478
|
-
await rename(stagingDirectory, targetDirectory);
|
|
2479
|
-
published = true;
|
|
2480
|
-
} finally {
|
|
2481
|
-
if (stagingDatabase !== undefined) {
|
|
2482
|
-
try {
|
|
2483
|
-
stagingDatabase.close();
|
|
2484
|
-
} catch {
|
|
2485
|
-
// Preserve the clone failure.
|
|
1024
|
+
return Object.freeze({
|
|
1025
|
+
sessionId: this.sessionId,
|
|
1026
|
+
faulted: false,
|
|
1027
|
+
frames: Object.freeze(frames),
|
|
1028
|
+
messages: Object.freeze(messages),
|
|
1029
|
+
toolResults: Object.freeze(toolResults),
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
async verifyImageAssetFiles(): Promise<void> {
|
|
1034
|
+
this.requireOpen();
|
|
1035
|
+
const distinct = new Map<string, ImageAssetRef>();
|
|
1036
|
+
for (const message of this.loadProtocolView().messages) {
|
|
1037
|
+
const assets =
|
|
1038
|
+
message.role === "user"
|
|
1039
|
+
? (message.attachments ?? []).map(imageAssetRefFromAttachment)
|
|
1040
|
+
: message.role === "tool"
|
|
1041
|
+
? message.content.flatMap((block) =>
|
|
1042
|
+
block.type === "image" ? [block.asset] : [],
|
|
1043
|
+
)
|
|
1044
|
+
: [];
|
|
1045
|
+
for (const asset of assets) {
|
|
1046
|
+
const previous = distinct.get(asset.assetId);
|
|
1047
|
+
if (
|
|
1048
|
+
previous !== undefined &&
|
|
1049
|
+
stableJsonStringify(previous) !== stableJsonStringify(asset)
|
|
1050
|
+
) {
|
|
1051
|
+
throw new Error(`Conflicting metadata for image asset ${asset.assetId}.`);
|
|
2486
1052
|
}
|
|
2487
|
-
|
|
2488
|
-
if (!published) {
|
|
2489
|
-
await removeKnownInitializationFiles(stagingDirectory).catch(() => undefined);
|
|
1053
|
+
distinct.set(asset.assetId, asset);
|
|
2490
1054
|
}
|
|
2491
1055
|
}
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
async close(reason: SessionCloseReason): Promise<void> {
|
|
2495
|
-
if (this.closed) {
|
|
1056
|
+
if (distinct.size === 0) {
|
|
2496
1057
|
return;
|
|
2497
1058
|
}
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
`UPDATE session_meta SET last_closed_at = ?, last_close_reason = ?, updated_at = ?
|
|
2505
|
-
WHERE singleton = 1`,
|
|
2506
|
-
)
|
|
2507
|
-
.run(now, reason, now);
|
|
2508
|
-
requireSingleChange(this.database, updated.changes, "close session activation");
|
|
2509
|
-
});
|
|
2510
|
-
} catch (error) {
|
|
2511
|
-
primaryError = sessionWriteError("close_session", this.sessionId, error);
|
|
2512
|
-
}
|
|
2513
|
-
try {
|
|
2514
|
-
this.database.close();
|
|
2515
|
-
} catch (error) {
|
|
2516
|
-
primaryError ??= error;
|
|
2517
|
-
}
|
|
2518
|
-
this.closed = true;
|
|
2519
|
-
try {
|
|
2520
|
-
await this.lease.release();
|
|
2521
|
-
} catch (error) {
|
|
2522
|
-
primaryError ??= error;
|
|
2523
|
-
}
|
|
2524
|
-
if (primaryError !== undefined) {
|
|
2525
|
-
throw asError(primaryError);
|
|
1059
|
+
const store = await ImageAssetStore.open({
|
|
1060
|
+
workspaceRoot: this.workspaceRoot,
|
|
1061
|
+
...(this.homeRoot === undefined ? {} : { homeRoot: this.homeRoot }),
|
|
1062
|
+
});
|
|
1063
|
+
for (const asset of distinct.values()) {
|
|
1064
|
+
await store.verify(asset);
|
|
2526
1065
|
}
|
|
2527
1066
|
}
|
|
2528
1067
|
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
}
|
|
1068
|
+
loadContextSnapshot(): StoredContextSnapshotV8 {
|
|
1069
|
+
this.requireOpen();
|
|
1070
|
+
const meta = this.readMeta();
|
|
2533
1071
|
try {
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
1072
|
+
if (
|
|
1073
|
+
meta.sessionId !== this.sessionId ||
|
|
1074
|
+
meta.schemaFingerprint !== SESSION_SCHEMA_V10_FINGERPRINT
|
|
1075
|
+
) {
|
|
1076
|
+
throw new Error("Session metadata identity or schema fingerprint changed.");
|
|
1077
|
+
}
|
|
1078
|
+
const canonical = this.loadProtocolView();
|
|
1079
|
+
this.validator.validate(canonical, { fullIntegrity: true });
|
|
1080
|
+
const systemFrame = canonical.frames[0];
|
|
1081
|
+
const systemMessage = canonical.messages[0];
|
|
1082
|
+
if (
|
|
1083
|
+
systemFrame?.kind !== "system" ||
|
|
1084
|
+
systemFrame.firstOrdinal !== 1 ||
|
|
1085
|
+
systemMessage?.role !== "system" ||
|
|
1086
|
+
systemMessage.ordinal !== 1 ||
|
|
1087
|
+
sha256(systemMessage.content) !== meta.systemPromptSha256 ||
|
|
1088
|
+
canonical.messages.at(-1)?.ordinal !== canonical.messages.length
|
|
1089
|
+
) {
|
|
1090
|
+
throw new Error("Stored context snapshot ordinal or system invariant failed.");
|
|
1091
|
+
}
|
|
1092
|
+
return this.validation.loadValidatedContextSnapshot(meta, canonical);
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
if (error instanceof SessionError) {
|
|
1095
|
+
throw error;
|
|
1096
|
+
}
|
|
1097
|
+
if (error instanceof ContextProtocolError) {
|
|
1098
|
+
throw new SessionError(
|
|
1099
|
+
"SESSION_PROTOCOL_INVALID",
|
|
1100
|
+
"load_context_snapshot",
|
|
1101
|
+
error.message,
|
|
1102
|
+
{
|
|
1103
|
+
sessionId: this.sessionId,
|
|
1104
|
+
frameId: error.frameId,
|
|
1105
|
+
messageId: error.messageId,
|
|
1106
|
+
toolCallId: error.toolCallId,
|
|
1107
|
+
cause: error,
|
|
1108
|
+
},
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
throw new SessionError(
|
|
1112
|
+
"SESSION_INTEGRITY_FAILED",
|
|
1113
|
+
"load_context_snapshot",
|
|
1114
|
+
`Session context snapshot validation failed: ${errorMessage(error)}.`,
|
|
1115
|
+
{ sessionId: this.sessionId, cause: error },
|
|
1116
|
+
);
|
|
2537
1117
|
}
|
|
2538
|
-
this.closed = true;
|
|
2539
|
-
await this.lease.release();
|
|
2540
1118
|
}
|
|
2541
1119
|
|
|
2542
|
-
|
|
1120
|
+
readMeta(): StoredSessionMetaV10 {
|
|
2543
1121
|
this.requireOpen();
|
|
2544
|
-
const
|
|
2545
|
-
|
|
2546
|
-
"session.sqlite-wal",
|
|
2547
|
-
"session.sqlite-shm",
|
|
2548
|
-
"events.jsonl",
|
|
2549
|
-
"observations.md",
|
|
2550
|
-
"active.lock",
|
|
2551
|
-
"active.lock.reclaim",
|
|
2552
|
-
]);
|
|
2553
|
-
const entries = await readdir(this.sessionDirectory);
|
|
2554
|
-
const unknown = entries.filter((entry) => !known.has(entry));
|
|
2555
|
-
if (unknown.length > 0) {
|
|
1122
|
+
const rows = this.database.query("SELECT * FROM session_meta").all();
|
|
1123
|
+
if (rows.length !== 1) {
|
|
2556
1124
|
throw new SessionError(
|
|
2557
|
-
"
|
|
2558
|
-
"
|
|
2559
|
-
`Session
|
|
1125
|
+
"SESSION_INTEGRITY_FAILED",
|
|
1126
|
+
"read_meta",
|
|
1127
|
+
`Session metadata must contain exactly one row; found ${rows.length}.`,
|
|
2560
1128
|
{ sessionId: this.sessionId },
|
|
2561
1129
|
);
|
|
2562
1130
|
}
|
|
1131
|
+
return decodeMeta(rows[0], this.sessionId);
|
|
1132
|
+
}
|
|
2563
1133
|
|
|
2564
|
-
|
|
2565
|
-
this.
|
|
2566
|
-
const tombstone = `${this.sessionDirectory}.deleting-${randomUUID()}`;
|
|
2567
|
-
try {
|
|
2568
|
-
await rename(this.sessionDirectory, tombstone);
|
|
2569
|
-
} catch (error) {
|
|
2570
|
-
this.closed = true;
|
|
2571
|
-
await this.lease.release().catch(() => undefined);
|
|
2572
|
-
throw error;
|
|
2573
|
-
}
|
|
2574
|
-
this.lease.relocate(tombstone);
|
|
2575
|
-
await this.lease.release();
|
|
2576
|
-
this.closed = true;
|
|
2577
|
-
|
|
1134
|
+
readCreationSystemPrompt(): string {
|
|
1135
|
+
this.requireOpen();
|
|
2578
1136
|
try {
|
|
2579
|
-
|
|
2580
|
-
|
|
1137
|
+
const frames = this.database
|
|
1138
|
+
.query("SELECT * FROM protocol_frames WHERE kind = 'system'")
|
|
1139
|
+
.all();
|
|
1140
|
+
if (frames.length !== 1) {
|
|
1141
|
+
throw new Error(`Expected one stored system frame; found ${frames.length}.`);
|
|
2581
1142
|
}
|
|
2582
|
-
|
|
1143
|
+
const frame = recordFromSql(frames[0], "stored system frame");
|
|
1144
|
+
if (
|
|
1145
|
+
frame.state !== "closed" ||
|
|
1146
|
+
numberFromSql(frame.first_ordinal, "first_ordinal") !== 1 ||
|
|
1147
|
+
numberFromSql(frame.last_ordinal, "last_ordinal") !== 1
|
|
1148
|
+
) {
|
|
1149
|
+
throw new Error("Stored system frame invariant failed.");
|
|
1150
|
+
}
|
|
1151
|
+
const frameId = stringFromSql(frame.frame_id, "frame_id");
|
|
1152
|
+
const messages = this.database
|
|
1153
|
+
.query("SELECT * FROM messages WHERE frame_id = ?")
|
|
1154
|
+
.all(frameId);
|
|
1155
|
+
if (messages.length !== 1) {
|
|
1156
|
+
throw new Error(
|
|
1157
|
+
`Expected one stored system message; found ${messages.length}.`,
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
const row = recordFromSql(messages[0], "stored system message");
|
|
1161
|
+
if (
|
|
1162
|
+
numberFromSql(row.ordinal, "ordinal") !== 1 ||
|
|
1163
|
+
row.role !== "system" ||
|
|
1164
|
+
row.origin !== "runtime"
|
|
1165
|
+
) {
|
|
1166
|
+
throw new Error("Stored system message invariant failed.");
|
|
1167
|
+
}
|
|
1168
|
+
const content = stringFromSql(row.content, "content");
|
|
1169
|
+
if (content.trim() === "") {
|
|
1170
|
+
throw new Error("Stored system prompt must not be empty.");
|
|
1171
|
+
}
|
|
1172
|
+
if (
|
|
1173
|
+
stringFromSql(row.content_sha256, "content_sha256") !== contentHash(content)
|
|
1174
|
+
) {
|
|
1175
|
+
throw new Error("Stored system message content hash does not match.");
|
|
1176
|
+
}
|
|
1177
|
+
if (this.readMeta().systemPromptSha256 !== sha256(content)) {
|
|
1178
|
+
throw new Error("Stored system prompt metadata hash does not match.");
|
|
1179
|
+
}
|
|
1180
|
+
return content;
|
|
2583
1181
|
} catch (error) {
|
|
1182
|
+
if (error instanceof SessionError && error.code === "SESSION_RECOVERY_FAILED") {
|
|
1183
|
+
throw error;
|
|
1184
|
+
}
|
|
2584
1185
|
throw new SessionError(
|
|
2585
|
-
"
|
|
2586
|
-
"
|
|
2587
|
-
|
|
1186
|
+
"SESSION_RECOVERY_FAILED",
|
|
1187
|
+
"read_creation_system_prompt",
|
|
1188
|
+
"Creation system prompt is missing or invalid.",
|
|
2588
1189
|
{ sessionId: this.sessionId, cause: error },
|
|
2589
1190
|
);
|
|
2590
1191
|
}
|
|
2591
1192
|
}
|
|
2592
1193
|
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
1194
|
+
readProjectInstructionManifest(): ProjectInstructionManifest | undefined {
|
|
1195
|
+
return this.readMeta().projectInstruction;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
validateCreatingState(): void {
|
|
1199
|
+
this.requireOpen();
|
|
2597
1200
|
const meta = this.readMeta();
|
|
2598
|
-
if (mutation.admissionBase !== undefined) {
|
|
2599
|
-
const snapshot = this.loadContextSnapshot();
|
|
2600
|
-
const head = snapshot.canonical.messages.at(-1);
|
|
2601
|
-
const base = mutation.admissionBase;
|
|
2602
|
-
if (
|
|
2603
|
-
head === undefined ||
|
|
2604
|
-
base.canonicalMessageCount !== snapshot.canonical.messages.length ||
|
|
2605
|
-
base.canonicalHeadMessageId !== head.messageId ||
|
|
2606
|
-
base.canonicalHeadContentSha256 !== head.contentSha256 ||
|
|
2607
|
-
base.activeRevisionId !== snapshot.revision.revisionId ||
|
|
2608
|
-
base.activeRevisionNumber !== snapshot.revision.revisionNumber ||
|
|
2609
|
-
base.surfaceSha256 !== snapshot.surface.surfaceSha256 ||
|
|
2610
|
-
base.sessionCompatibilitySha256 !== meta.sessionCompatibilitySha256 ||
|
|
2611
|
-
base.nextTurnNumber !== meta.nextTurnNumber
|
|
2612
|
-
) {
|
|
2613
|
-
throw new AdmissionStaleError();
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
1201
|
if (
|
|
2617
|
-
meta.initializationState !== "
|
|
2618
|
-
meta.
|
|
1202
|
+
meta.initializationState !== "creating" ||
|
|
1203
|
+
meta.activeRevisionId !== null ||
|
|
1204
|
+
meta.sessionCompatibilityJson !== null ||
|
|
1205
|
+
meta.sessionCompatibilitySha256 !== null
|
|
2619
1206
|
) {
|
|
2620
|
-
throw new
|
|
1207
|
+
throw new SessionError(
|
|
1208
|
+
"SESSION_INTEGRITY_FAILED",
|
|
1209
|
+
"validate_creating_store",
|
|
1210
|
+
"Creating session metadata is invalid.",
|
|
1211
|
+
{ sessionId: this.sessionId },
|
|
1212
|
+
);
|
|
2621
1213
|
}
|
|
2622
|
-
this.
|
|
2623
|
-
|
|
2624
|
-
`INSERT INTO turns (
|
|
2625
|
-
session_id, turn_id, turn_number, status, next_iteration_number,
|
|
2626
|
-
last_iteration_id, final_message_id, terminal_detail_json, started_at, finished_at
|
|
2627
|
-
) VALUES (?, ?, ?, 'open', 1, NULL, NULL, NULL, ?, NULL)`,
|
|
2628
|
-
)
|
|
2629
|
-
.run(this.sessionId, mutation.turn.turnId, mutation.turn.turnNumber, now);
|
|
2630
|
-
insertFrame(this.database, mutation.frame);
|
|
2631
|
-
insertMessage(this.database, mutation.message);
|
|
2632
|
-
const updated = this.database
|
|
1214
|
+
this.readCreationSystemPrompt();
|
|
1215
|
+
const counts = this.database
|
|
2633
1216
|
.query(
|
|
2634
|
-
`
|
|
2635
|
-
|
|
1217
|
+
`SELECT
|
|
1218
|
+
(SELECT COUNT(*) FROM context_surfaces) AS surfaces,
|
|
1219
|
+
(SELECT COUNT(*) FROM context_revisions) AS revisions,
|
|
1220
|
+
(SELECT COUNT(*) FROM turns) AS turns`,
|
|
2636
1221
|
)
|
|
2637
|
-
.
|
|
2638
|
-
requireSingleChange(this.database, updated.changes, "advance turn counter");
|
|
2639
|
-
}
|
|
2640
|
-
|
|
2641
|
-
private commitSteeringUsers(
|
|
2642
|
-
mutation: Extract<LedgerMutation, { kind: "append_steering_users" }>,
|
|
2643
|
-
now: string,
|
|
2644
|
-
): void {
|
|
2645
|
-
const turn = this.requireTurnRow(mutation.turn.turnId);
|
|
2646
|
-
if (turn.status !== "open") {
|
|
2647
|
-
throw new Error(`Turn ${mutation.turn.turnId} is not open.`);
|
|
2648
|
-
}
|
|
1222
|
+
.get() as Record<string, unknown> | null;
|
|
2649
1223
|
if (
|
|
2650
|
-
|
|
2651
|
-
|
|
1224
|
+
counts === null ||
|
|
1225
|
+
numberFromSql(counts.surfaces, "surfaces") !== 0 ||
|
|
1226
|
+
numberFromSql(counts.revisions, "revisions") !== 0 ||
|
|
1227
|
+
numberFromSql(counts.turns, "turns") !== 0
|
|
2652
1228
|
) {
|
|
2653
|
-
throw new
|
|
2654
|
-
"
|
|
1229
|
+
throw new SessionError(
|
|
1230
|
+
"SESSION_INTEGRITY_FAILED",
|
|
1231
|
+
"validate_creating_store",
|
|
1232
|
+
"Creating session contains finalized or turn state.",
|
|
1233
|
+
{ sessionId: this.sessionId },
|
|
2655
1234
|
);
|
|
2656
1235
|
}
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
1236
|
+
const view = this.loadProtocolView();
|
|
1237
|
+
this.validator.validate(view, { fullIntegrity: true });
|
|
1238
|
+
if (view.messages.length !== 1 || view.frames.length !== 1) {
|
|
1239
|
+
throw new SessionError(
|
|
1240
|
+
"SESSION_INTEGRITY_FAILED",
|
|
1241
|
+
"validate_creating_store",
|
|
1242
|
+
"Creating session must contain only its creation system frame.",
|
|
1243
|
+
{ sessionId: this.sessionId },
|
|
2662
1244
|
);
|
|
2663
1245
|
}
|
|
2664
|
-
this.touch(now);
|
|
2665
1246
|
}
|
|
2666
1247
|
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
throw new Error(`Iteration ${mutation.iteration.iterationId} is not open.`);
|
|
2674
|
-
}
|
|
2675
|
-
insertFrame(this.database, mutation.frame);
|
|
2676
|
-
insertMessage(this.database, mutation.message);
|
|
1248
|
+
nextTurnNumber(): number {
|
|
1249
|
+
return this.readMeta().nextTurnNumber;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
validateAll(options: { allowOpenTail: boolean }): ProtocolContextView {
|
|
1253
|
+
const meta = this.readMeta();
|
|
2677
1254
|
if (
|
|
2678
|
-
|
|
2679
|
-
|
|
1255
|
+
meta.sessionId !== this.sessionId ||
|
|
1256
|
+
meta.schemaFingerprint !== SESSION_SCHEMA_V10_FINGERPRINT ||
|
|
1257
|
+
meta.initializationState !== "ready" ||
|
|
1258
|
+
meta.activeRevisionId === null
|
|
2680
1259
|
) {
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
"
|
|
1260
|
+
throw new SessionError(
|
|
1261
|
+
"SESSION_SCHEMA_INVALID",
|
|
1262
|
+
"validate_store",
|
|
1263
|
+
"Session metadata identity or schema fingerprint does not match.",
|
|
1264
|
+
{ sessionId: this.sessionId },
|
|
2684
1265
|
);
|
|
2685
|
-
if (expected !== 1) {
|
|
2686
|
-
throw new Error(
|
|
2687
|
-
"Assistant tool calls were already allocated for this iteration.",
|
|
2688
|
-
);
|
|
2689
|
-
}
|
|
2690
|
-
const updated = this.database
|
|
2691
|
-
.query(
|
|
2692
|
-
`UPDATE iterations SET next_tool_call_number = ?
|
|
2693
|
-
WHERE iteration_id = ? AND outcome = 'open' AND next_tool_call_number = 1`,
|
|
2694
|
-
)
|
|
2695
|
-
.run(mutation.message.toolCalls.length + 1, mutation.iteration.iterationId);
|
|
2696
|
-
requireSingleChange(this.database, updated.changes, "advance tool call counter");
|
|
2697
|
-
}
|
|
2698
|
-
this.touch(now);
|
|
2699
|
-
}
|
|
2700
|
-
|
|
2701
|
-
private commitToolCompletions(
|
|
2702
|
-
mutation: Extract<LedgerMutation, { kind: "commit_tool_completions" }>,
|
|
2703
|
-
now: string,
|
|
2704
|
-
): void {
|
|
2705
|
-
const current = this.database
|
|
2706
|
-
.query("SELECT state, last_ordinal FROM protocol_frames WHERE frame_id = ?")
|
|
2707
|
-
.get(mutation.frameBefore.frameId) as {
|
|
2708
|
-
state: string;
|
|
2709
|
-
last_ordinal: unknown;
|
|
2710
|
-
} | null;
|
|
2711
|
-
if (current?.state !== "open" || current.last_ordinal !== null) {
|
|
2712
|
-
throw new Error(`Frame ${mutation.frameBefore.frameId} is not open.`);
|
|
2713
|
-
}
|
|
2714
|
-
for (let index = 0; index < mutation.messages.length; index += 1) {
|
|
2715
|
-
const message = requireItem(mutation.messages, index, "tool message");
|
|
2716
|
-
const result = requireItem(mutation.toolResults, index, "tool result");
|
|
2717
|
-
insertMessage(this.database, message);
|
|
2718
|
-
insertToolResult(this.database, result);
|
|
2719
|
-
insertPendingSkillActivation(this.database, message, result, now);
|
|
2720
|
-
}
|
|
2721
|
-
if (mutation.frameAfter.state === "closed") {
|
|
2722
|
-
const updated = this.database
|
|
2723
|
-
.query(
|
|
2724
|
-
`UPDATE protocol_frames SET state = 'closed', last_ordinal = ?, closed_at = ?
|
|
2725
|
-
WHERE frame_id = ? AND state = 'open' AND last_ordinal IS NULL`,
|
|
2726
|
-
)
|
|
2727
|
-
.run(
|
|
2728
|
-
mutation.frameAfter.lastOrdinal!,
|
|
2729
|
-
mutation.frameAfter.closedAt!,
|
|
2730
|
-
mutation.frameAfter.frameId,
|
|
2731
|
-
);
|
|
2732
|
-
requireSingleChange(this.database, updated.changes, "close tool exchange frame");
|
|
2733
1266
|
}
|
|
2734
|
-
this.touch(now);
|
|
2735
|
-
}
|
|
2736
|
-
|
|
2737
|
-
private commitFinishTurn(
|
|
2738
|
-
mutation: Extract<LedgerMutation, { kind: "finish_turn" }>,
|
|
2739
|
-
now: string,
|
|
2740
|
-
): void {
|
|
2741
|
-
const result = mutation.result;
|
|
2742
|
-
const turnStatus = result.status;
|
|
2743
|
-
const iterationOutcome = result.status;
|
|
2744
|
-
const detail =
|
|
2745
|
-
result.status === "completed"
|
|
2746
|
-
? stableJsonStringify({ version: 1, finalTextLength: result.finalText.length })
|
|
2747
|
-
: result.status === "failed"
|
|
2748
|
-
? stableJsonStringify({ version: 1, error: result.error.slice(0, 2_000) })
|
|
2749
|
-
: stableJsonStringify({ version: 1, cancellation: result.cancellation });
|
|
2750
|
-
this.markTerminalRows(
|
|
2751
|
-
mutation.turn.turnId,
|
|
2752
|
-
result.lastIteration.iterationId,
|
|
2753
|
-
turnStatus,
|
|
2754
|
-
iterationOutcome,
|
|
2755
|
-
mutation.finalMessageId ?? null,
|
|
2756
|
-
detail,
|
|
2757
|
-
now,
|
|
2758
|
-
);
|
|
2759
|
-
}
|
|
2760
|
-
|
|
2761
|
-
private markTerminalRows(
|
|
2762
|
-
turnId: TurnId,
|
|
2763
|
-
iterationId: IterationId,
|
|
2764
|
-
turnStatus: "completed" | "failed" | "cancelled" | "interrupted",
|
|
2765
|
-
iterationOutcome: "completed" | "failed" | "cancelled" | "interrupted",
|
|
2766
|
-
finalMessageId: MessageId | null,
|
|
2767
|
-
terminalDetailJson: string,
|
|
2768
|
-
now: string,
|
|
2769
|
-
): void {
|
|
2770
|
-
const iteration = this.database
|
|
2771
|
-
.query(
|
|
2772
|
-
`UPDATE iterations SET outcome = ?, finished_at = ?
|
|
2773
|
-
WHERE iteration_id = ? AND turn_id = ? AND outcome = 'open'`,
|
|
2774
|
-
)
|
|
2775
|
-
.run(iterationOutcome, now, iterationId, turnId);
|
|
2776
|
-
requireSingleChange(this.database, iteration.changes, "finish iteration");
|
|
2777
|
-
const turn = this.database
|
|
2778
|
-
.query(
|
|
2779
|
-
`UPDATE turns SET status = ?, last_iteration_id = ?, final_message_id = ?,
|
|
2780
|
-
terminal_detail_json = ?, finished_at = ?
|
|
2781
|
-
WHERE turn_id = ? AND status = 'open'`,
|
|
2782
|
-
)
|
|
2783
|
-
.run(turnStatus, iterationId, finalMessageId, terminalDetailJson, now, turnId);
|
|
2784
|
-
requireSingleChange(this.database, turn.changes, "finish turn");
|
|
2785
|
-
this.touch(now);
|
|
2786
|
-
}
|
|
2787
|
-
|
|
2788
|
-
private markOpenTurnInterrupted(
|
|
2789
|
-
turnId: TurnId,
|
|
2790
|
-
iterationId: IterationId | undefined,
|
|
2791
|
-
): void {
|
|
2792
|
-
const now = this.clock();
|
|
2793
1267
|
try {
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
WHERE iteration_id = ? AND outcome = 'open'`,
|
|
2800
|
-
)
|
|
2801
|
-
.run(now, iterationId);
|
|
2802
|
-
requireSingleChange(this.database, iteration.changes, "interrupt iteration");
|
|
2803
|
-
}
|
|
2804
|
-
const turn = this.database
|
|
2805
|
-
.query(
|
|
2806
|
-
`UPDATE turns SET status = 'interrupted', finished_at = ?,
|
|
2807
|
-
terminal_detail_json = ?
|
|
2808
|
-
WHERE turn_id = ? AND status = 'open'`,
|
|
2809
|
-
)
|
|
2810
|
-
.run(
|
|
2811
|
-
now,
|
|
2812
|
-
stableJsonStringify({ version: 1, reason: "process_interrupted" }),
|
|
2813
|
-
turnId,
|
|
2814
|
-
);
|
|
2815
|
-
requireSingleChange(this.database, turn.changes, "interrupt turn");
|
|
2816
|
-
this.touch(now);
|
|
1268
|
+
this.readCreationSystemPrompt();
|
|
1269
|
+
const view = this.loadProtocolView();
|
|
1270
|
+
this.validator.validate(view, {
|
|
1271
|
+
allowOpenTail: options.allowOpenTail,
|
|
1272
|
+
fullIntegrity: true,
|
|
2817
1273
|
});
|
|
1274
|
+
this.validation.loadValidatedContextSnapshot(meta, view);
|
|
1275
|
+
this.validation.validateCounters(meta, view);
|
|
1276
|
+
return view;
|
|
2818
1277
|
} catch (error) {
|
|
1278
|
+
if (error instanceof SessionError) {
|
|
1279
|
+
throw error;
|
|
1280
|
+
}
|
|
1281
|
+
if (error instanceof ContextProtocolError) {
|
|
1282
|
+
throw new SessionError(
|
|
1283
|
+
"SESSION_PROTOCOL_INVALID",
|
|
1284
|
+
"validate_store",
|
|
1285
|
+
error.message,
|
|
1286
|
+
{
|
|
1287
|
+
sessionId: this.sessionId,
|
|
1288
|
+
frameId: error.frameId,
|
|
1289
|
+
messageId: error.messageId,
|
|
1290
|
+
toolCallId: error.toolCallId,
|
|
1291
|
+
cause: error,
|
|
1292
|
+
},
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
2819
1295
|
throw new SessionError(
|
|
2820
|
-
"
|
|
2821
|
-
"
|
|
2822
|
-
`
|
|
1296
|
+
"SESSION_INTEGRITY_FAILED",
|
|
1297
|
+
"validate_store",
|
|
1298
|
+
`Session record validation failed: ${errorMessage(error)}.`,
|
|
2823
1299
|
{ sessionId: this.sessionId, cause: error },
|
|
2824
1300
|
);
|
|
2825
1301
|
}
|
|
2826
1302
|
}
|
|
2827
1303
|
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
const activeRevisionId = requireActiveRevisionId(meta);
|
|
2833
|
-
const surfaces = this.database
|
|
2834
|
-
.query("SELECT * FROM context_surfaces ORDER BY rowid")
|
|
2835
|
-
.all()
|
|
2836
|
-
.map(decodeContextSurface);
|
|
2837
|
-
const surfacesById = new Map<ContextSurfaceId, StoredContextSurfaceV8>();
|
|
2838
|
-
for (const surface of surfaces) {
|
|
2839
|
-
validateStoredContextSurface(surface);
|
|
2840
|
-
if (surface.sessionId !== this.sessionId || surfacesById.has(surface.surfaceId)) {
|
|
2841
|
-
throw new Error("Context surface identity is invalid or duplicated.");
|
|
2842
|
-
}
|
|
2843
|
-
surfacesById.set(surface.surfaceId, surface);
|
|
1304
|
+
async cloneTo(input: CloneSessionStoreInput): Promise<void> {
|
|
1305
|
+
this.requireOpen();
|
|
1306
|
+
if (input.targetSessionId === this.sessionId) {
|
|
1307
|
+
throw new Error("Session clone target must differ from the source session.");
|
|
2844
1308
|
}
|
|
2845
1309
|
|
|
2846
|
-
const
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
}
|
|
1310
|
+
const sourceView = this.validateAll({ allowOpenTail: false });
|
|
1311
|
+
const sourceMeta = this.readMeta();
|
|
1312
|
+
const sessionsRoot = path.dirname(this.sessionDirectory);
|
|
1313
|
+
await validateSessionsRoot(sessionsRoot, this.sessionId);
|
|
1314
|
+
const targetDirectory = safeSessionDirectory(sessionsRoot, input.targetSessionId);
|
|
1315
|
+
await assertPathMissing(targetDirectory, input.targetSessionId);
|
|
2853
1316
|
|
|
2854
|
-
const
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
const surface = surfacesById.get(revision.surfaceId);
|
|
2859
|
-
if (
|
|
2860
|
-
revision.sessionId !== this.sessionId ||
|
|
2861
|
-
revision.revisionNumber !== index + 1 ||
|
|
2862
|
-
surface === undefined ||
|
|
2863
|
-
surface.surfaceSha256 !== revision.surfaceSha256 ||
|
|
2864
|
-
(index === 0
|
|
2865
|
-
? revision.kind !== "initial_full" || revision.parentRevisionId !== null
|
|
2866
|
-
: revision.kind === "initial_full" ||
|
|
2867
|
-
revision.parentRevisionId !== previous?.revisionId) ||
|
|
2868
|
-
((revision.kind === "swap_only" || revision.kind === "prefix_retirement") &&
|
|
2869
|
-
revision.surfaceId !== previous?.surfaceId) ||
|
|
2870
|
-
(revision.kind === "surface_refresh" &&
|
|
2871
|
-
revision.surfaceId === previous?.surfaceId) ||
|
|
2872
|
-
(previous !== undefined &&
|
|
2873
|
-
(revision.keepFromOrdinal < previous.keepFromOrdinal ||
|
|
2874
|
-
((revision.kind === "swap_only" ||
|
|
2875
|
-
revision.kind === "surface_refresh" ||
|
|
2876
|
-
revision.kind === "skills_update") &&
|
|
2877
|
-
revision.keepFromOrdinal !== previous.keepFromOrdinal) ||
|
|
2878
|
-
(revision.kind === "prefix_retirement" &&
|
|
2879
|
-
revision.keepFromOrdinal <= previous.keepFromOrdinal)))
|
|
2880
|
-
) {
|
|
2881
|
-
throw new Error("Context revision chain is not linear and contiguous.");
|
|
2882
|
-
}
|
|
2883
|
-
const boundary = canonical.frames.find(
|
|
2884
|
-
(frame) => frame.lastOrdinal === revision.sourceThroughOrdinal,
|
|
2885
|
-
);
|
|
2886
|
-
if (
|
|
2887
|
-
revision.sourceThroughOrdinal > canonical.messages.length ||
|
|
2888
|
-
boundary?.state !== "closed"
|
|
2889
|
-
) {
|
|
2890
|
-
throw new Error(
|
|
2891
|
-
`Context revision ${revision.revisionId} has an invalid source boundary.`,
|
|
2892
|
-
);
|
|
2893
|
-
}
|
|
2894
|
-
revisionNumberById.set(revision.revisionId, revision.revisionNumber);
|
|
2895
|
-
}
|
|
2896
|
-
const introducedSurfaceIds = new Set(
|
|
2897
|
-
revisions.flatMap((revision, index) => {
|
|
2898
|
-
const previous = revisions[index - 1];
|
|
2899
|
-
return revision.kind === "initial_full" ||
|
|
2900
|
-
revision.kind === "surface_refresh" ||
|
|
2901
|
-
(revision.kind === "skills_update" &&
|
|
2902
|
-
revision.surfaceId !== previous?.surfaceId)
|
|
2903
|
-
? [revision.surfaceId]
|
|
2904
|
-
: [];
|
|
2905
|
-
}),
|
|
2906
|
-
);
|
|
2907
|
-
if (
|
|
2908
|
-
introducedSurfaceIds.size !== surfaces.length ||
|
|
2909
|
-
surfaces.some((surface) => !introducedSurfaceIds.has(surface.surfaceId))
|
|
2910
|
-
) {
|
|
2911
|
-
throw new Error("Context surface chain contains an orphan or duplicate surface.");
|
|
2912
|
-
}
|
|
1317
|
+
const stagingDirectory = path.join(sessionsRoot, `.cloning-${randomUUID()}`);
|
|
1318
|
+
const stagingDatabasePath = path.join(stagingDirectory, "session.sqlite");
|
|
1319
|
+
let stagingDatabase: Database | undefined;
|
|
1320
|
+
let published = false;
|
|
2913
1321
|
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
"
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
}
|
|
2922
|
-
const activeSurface = surfacesById.get(activeRevision.surfaceId);
|
|
2923
|
-
if (activeSurface === undefined) {
|
|
2924
|
-
throw new Error("Active context revision surface is missing.");
|
|
2925
|
-
}
|
|
1322
|
+
try {
|
|
1323
|
+
await mkdir(stagingDirectory, { mode: 0o700 });
|
|
1324
|
+
await chmod(stagingDirectory, 0o700);
|
|
1325
|
+
input.faultInjector?.("after_staging_mkdir");
|
|
1326
|
+
this.database.query("VACUUM INTO ?").run(stagingDatabasePath);
|
|
1327
|
+
await chmod(stagingDatabasePath, 0o600);
|
|
1328
|
+
input.faultInjector?.("after_snapshot");
|
|
2926
1329
|
|
|
2927
|
-
|
|
2928
|
-
.
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
)
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
for (const revision of revisions) {
|
|
2940
|
-
const surface = surfacesById.get(revision.surfaceId);
|
|
2941
|
-
if (surface === undefined) {
|
|
2942
|
-
throw new Error(`Context revision ${revision.revisionId} has no surface.`);
|
|
2943
|
-
}
|
|
2944
|
-
const activeOverrides = overrides.filter(
|
|
2945
|
-
(override) =>
|
|
2946
|
-
(revisionNumberById.get(override.introducedRevisionId) ??
|
|
2947
|
-
Number.POSITIVE_INFINITY) <= revision.revisionNumber &&
|
|
2948
|
-
override.ordinal >= revision.keepFromOrdinal,
|
|
2949
|
-
);
|
|
2950
|
-
const introducedCount = overrides.filter(
|
|
2951
|
-
(override) => override.introducedRevisionId === revision.revisionId,
|
|
2952
|
-
).length;
|
|
2953
|
-
if (
|
|
2954
|
-
introducedCount !== revision.addedOverrideCount ||
|
|
2955
|
-
activeOverrides.length !== revision.activeOverrideCount ||
|
|
2956
|
-
activeOverrideManifestHash(activeOverrides) !==
|
|
2957
|
-
revision.activeOverrideManifestSha256
|
|
2958
|
-
) {
|
|
2959
|
-
throw new Error(
|
|
2960
|
-
`Context revision ${revision.revisionId} override manifest is invalid.`,
|
|
2961
|
-
);
|
|
2962
|
-
}
|
|
2963
|
-
if (
|
|
2964
|
-
revision.kind === "surface_refresh" &&
|
|
2965
|
-
previousRevision(revisions, revision)?.activeOverrideManifestSha256 !==
|
|
2966
|
-
revision.activeOverrideManifestSha256
|
|
2967
|
-
) {
|
|
2968
|
-
throw new Error(
|
|
2969
|
-
`Context surface revision ${revision.revisionId} changed overrides.`,
|
|
2970
|
-
);
|
|
2971
|
-
}
|
|
2972
|
-
if (revision.kind === "skills_update") {
|
|
2973
|
-
const parent = previousRevision(revisions, revision);
|
|
2974
|
-
const parentSurface =
|
|
2975
|
-
parent === undefined ? undefined : surfacesById.get(parent.surfaceId);
|
|
2976
|
-
const settlements = this.loadSkillActivations().filter(
|
|
2977
|
-
(activation) => activation.settledRevisionId === revision.revisionId,
|
|
2978
|
-
);
|
|
2979
|
-
if (
|
|
2980
|
-
parentSurface === undefined ||
|
|
2981
|
-
contextSurfaceChangeManifestHash(
|
|
2982
|
-
contextSurfaceChanges(parentSurface, surface),
|
|
2983
|
-
) !== revision.changeManifestSha256 ||
|
|
2984
|
-
settlements.length !== revision.addedOverrideCount ||
|
|
2985
|
-
skillActivationManifestSha256(
|
|
2986
|
-
settlements.map((activation) => ({
|
|
2987
|
-
activationMessageId: activation.activationMessageId,
|
|
2988
|
-
name: activation.name,
|
|
2989
|
-
state: activation.state === "promoted" ? "promoted" : "rejected",
|
|
2990
|
-
...(activation.rejectionReason === undefined
|
|
2991
|
-
? {}
|
|
2992
|
-
: { rejectionReason: activation.rejectionReason }),
|
|
2993
|
-
})),
|
|
2994
|
-
) !== revision.activationManifestSha256
|
|
2995
|
-
) {
|
|
2996
|
-
throw new Error(
|
|
2997
|
-
`Agent Skills revision ${revision.revisionId} manifest is invalid.`,
|
|
2998
|
-
);
|
|
2999
|
-
}
|
|
3000
|
-
}
|
|
3001
|
-
if (revision.kind === "prefix_retirement") {
|
|
3002
|
-
const parent = previousRevision(revisions, revision);
|
|
3003
|
-
if (parent === undefined) {
|
|
3004
|
-
throw new Error("Prefix retirement revision has no parent.");
|
|
3005
|
-
}
|
|
3006
|
-
const retiredStart = Math.max(parent.keepFromOrdinal, 2);
|
|
3007
|
-
const retiredMessages = canonical.messages.filter(
|
|
3008
|
-
(message) =>
|
|
3009
|
-
message.ordinal >= retiredStart &&
|
|
3010
|
-
message.ordinal < revision.keepFromOrdinal,
|
|
3011
|
-
);
|
|
3012
|
-
const retiredFrames = canonical.frames.filter(
|
|
3013
|
-
(frame) =>
|
|
3014
|
-
frame.firstOrdinal >= retiredStart &&
|
|
3015
|
-
(frame.lastOrdinal ?? Number.POSITIVE_INFINITY) < revision.keepFromOrdinal,
|
|
3016
|
-
);
|
|
3017
|
-
const retiredTurns = new Set(
|
|
3018
|
-
retiredMessages.flatMap((message) =>
|
|
3019
|
-
message.role === "system" ? [] : [message.turnId],
|
|
3020
|
-
),
|
|
3021
|
-
);
|
|
3022
|
-
if (
|
|
3023
|
-
revision.retiredThroughOrdinal !== revision.keepFromOrdinal - 1 ||
|
|
3024
|
-
revision.retiredMessageCount !== retiredMessages.length ||
|
|
3025
|
-
revision.retiredFrameCount !== retiredFrames.length ||
|
|
3026
|
-
revision.retiredTurnCount !== retiredTurns.size
|
|
3027
|
-
) {
|
|
3028
|
-
throw new Error(
|
|
3029
|
-
`Context retirement revision ${revision.revisionId} counts are invalid.`,
|
|
3030
|
-
);
|
|
1330
|
+
stagingDatabase = openWritableDatabase(stagingDatabasePath);
|
|
1331
|
+
verifySessionSchema(stagingDatabase, this.sessionId);
|
|
1332
|
+
dropSessionCloneTriggers(stagingDatabase);
|
|
1333
|
+
input.faultInjector?.("after_trigger_drop");
|
|
1334
|
+
|
|
1335
|
+
const targetCanonical = rekeyProtocolView(sourceView, input.targetSessionId);
|
|
1336
|
+
runTransaction(stagingDatabase, () => {
|
|
1337
|
+
stagingDatabase!.exec("PRAGMA defer_foreign_keys = ON");
|
|
1338
|
+
for (const table of SESSION_SCOPED_TABLES) {
|
|
1339
|
+
stagingDatabase!
|
|
1340
|
+
.query(`UPDATE ${table} SET session_id = ? WHERE session_id = ?`)
|
|
1341
|
+
.run(input.targetSessionId, this.sessionId);
|
|
3031
1342
|
}
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
contextSurfaceChanges(parentSurface, surface),
|
|
3041
|
-
) !== revision.changeManifestSha256
|
|
3042
|
-
) {
|
|
3043
|
-
throw new Error(
|
|
3044
|
-
`Context surface revision ${revision.revisionId} change manifest is invalid.`,
|
|
3045
|
-
);
|
|
1343
|
+
rekeyStoredToolCalls(stagingDatabase!, input.targetSessionId);
|
|
1344
|
+
input.faultInjector?.("after_identity_update");
|
|
1345
|
+
|
|
1346
|
+
rewriteCloneRevisionHashes(stagingDatabase!, targetCanonical);
|
|
1347
|
+
input.faultInjector?.("after_revision_hash_rewrite");
|
|
1348
|
+
|
|
1349
|
+
if (stagingDatabase!.query("PRAGMA foreign_key_check").all().length !== 0) {
|
|
1350
|
+
throw new Error("Cloned session identity re-key broke foreign keys.");
|
|
3046
1351
|
}
|
|
3047
|
-
}
|
|
3048
|
-
const prefix = protocolPrefixView(canonical, revision.sourceThroughOrdinal);
|
|
3049
|
-
this.revisionCompiler.compileActive({
|
|
3050
|
-
meta: Object.freeze({
|
|
3051
|
-
sessionId: this.sessionId,
|
|
3052
|
-
activeRevisionId: revision.revisionId,
|
|
3053
|
-
}),
|
|
3054
|
-
revision,
|
|
3055
|
-
surface,
|
|
3056
|
-
activeOverrides,
|
|
3057
|
-
canonical: prefix,
|
|
3058
1352
|
});
|
|
3059
|
-
}
|
|
3060
1353
|
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
1354
|
+
reinstallSessionCloneTriggers(stagingDatabase);
|
|
1355
|
+
input.faultInjector?.("after_trigger_reinstall");
|
|
1356
|
+
verifySessionSchema(stagingDatabase, input.targetSessionId);
|
|
1357
|
+
verifySqliteIntegrity(stagingDatabase, input.targetSessionId);
|
|
1358
|
+
verifyRecallIndex(stagingDatabase, input.targetSessionId);
|
|
1359
|
+
input.faultInjector?.("after_recall_validation");
|
|
1360
|
+
const clonedStore = new SessionStore(stagingDatabase, this.lease, {
|
|
1361
|
+
sessionId: input.targetSessionId,
|
|
1362
|
+
workspaceRoot: this.workspaceRoot,
|
|
1363
|
+
sessionDirectory: stagingDirectory,
|
|
1364
|
+
databasePath: stagingDatabasePath,
|
|
1365
|
+
clock: this.clock,
|
|
1366
|
+
...(this.homeRoot === undefined ? {} : { homeRoot: this.homeRoot }),
|
|
1367
|
+
});
|
|
1368
|
+
clonedStore.validateAll({ allowOpenTail: false });
|
|
1369
|
+
await clonedStore.verifyImageAssetFiles();
|
|
3068
1370
|
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
overrides.filter(
|
|
3078
|
-
(override) =>
|
|
3079
|
-
(revisionNumberById.get(override.introducedRevisionId) ??
|
|
3080
|
-
Number.POSITIVE_INFINITY) <= activeRevision.revisionNumber &&
|
|
3081
|
-
override.ordinal >= activeRevision.keepFromOrdinal,
|
|
3082
|
-
),
|
|
3083
|
-
),
|
|
3084
|
-
canonical,
|
|
3085
|
-
});
|
|
3086
|
-
}
|
|
1371
|
+
await cloneDiagnosticFiles({
|
|
1372
|
+
sourceDirectory: this.sessionDirectory,
|
|
1373
|
+
stagingDirectory,
|
|
1374
|
+
sourceSessionId: this.sessionId,
|
|
1375
|
+
targetSessionId: input.targetSessionId,
|
|
1376
|
+
nextEventSequence: sourceMeta.nextEventSequence,
|
|
1377
|
+
faultInjector: input.faultInjector,
|
|
1378
|
+
});
|
|
3087
1379
|
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
override.ordinal > revision.sourceThroughOrdinal ||
|
|
3122
|
-
seenMessages.has(override.messageId) ||
|
|
3123
|
-
message?.role !== "tool" ||
|
|
3124
|
-
message.frameId !== override.frameId ||
|
|
3125
|
-
message.ordinal !== override.ordinal ||
|
|
3126
|
-
frame?.kind !== "tool_exchange" ||
|
|
3127
|
-
frame.state !== "closed" ||
|
|
3128
|
-
result === undefined
|
|
3129
|
-
) {
|
|
3130
|
-
throw new Error("Stored context override canonical identity is invalid.");
|
|
1380
|
+
stagingDatabase.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
1381
|
+
const standaloneJournal = stagingDatabase
|
|
1382
|
+
.query("PRAGMA journal_mode = DELETE")
|
|
1383
|
+
.get() as Record<string, unknown> | null;
|
|
1384
|
+
if (String(standaloneJournal?.journal_mode).toLowerCase() !== "delete") {
|
|
1385
|
+
throw new Error("Cloned session database did not leave WAL mode.");
|
|
1386
|
+
}
|
|
1387
|
+
stagingDatabase.close();
|
|
1388
|
+
stagingDatabase = undefined;
|
|
1389
|
+
await unlinkIfExists(`${stagingDatabasePath}-wal`);
|
|
1390
|
+
await unlinkIfExists(`${stagingDatabasePath}-shm`);
|
|
1391
|
+
await validateSecureDirectory(stagingDirectory, input.targetSessionId);
|
|
1392
|
+
await validateSecureFile(stagingDatabasePath, input.targetSessionId);
|
|
1393
|
+
await validateSecureOptionalFile(
|
|
1394
|
+
path.join(stagingDirectory, "events.jsonl"),
|
|
1395
|
+
input.targetSessionId,
|
|
1396
|
+
);
|
|
1397
|
+
await validateSecureOptionalFile(
|
|
1398
|
+
path.join(stagingDirectory, "observations.md"),
|
|
1399
|
+
input.targetSessionId,
|
|
1400
|
+
);
|
|
1401
|
+
input.faultInjector?.("after_artifact_validation");
|
|
1402
|
+
await assertPathMissing(targetDirectory, input.targetSessionId);
|
|
1403
|
+
input.faultInjector?.("before_publish_rename");
|
|
1404
|
+
await rename(stagingDirectory, targetDirectory);
|
|
1405
|
+
published = true;
|
|
1406
|
+
} finally {
|
|
1407
|
+
if (stagingDatabase !== undefined) {
|
|
1408
|
+
try {
|
|
1409
|
+
stagingDatabase.close();
|
|
1410
|
+
} catch {
|
|
1411
|
+
// Preserve the clone failure.
|
|
1412
|
+
}
|
|
3131
1413
|
}
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
override.rendererFormat === SWAP_TOOL_IMAGE_FORMAT
|
|
3135
|
-
? this.swapRenderer.render({ message, result })
|
|
3136
|
-
: this.renderStoredSkillReceipt(message, result, revision.revisionId);
|
|
3137
|
-
if (
|
|
3138
|
-
stableJsonStringify(rendered) !==
|
|
3139
|
-
stableJsonStringify(stripStoredOverride(override))
|
|
3140
|
-
) {
|
|
3141
|
-
throw new Error(
|
|
3142
|
-
`Stored context override ${override.messageId} does not match deterministic rendering.`,
|
|
3143
|
-
);
|
|
1414
|
+
if (!published) {
|
|
1415
|
+
await removeKnownInitializationFiles(stagingDirectory).catch(() => undefined);
|
|
3144
1416
|
}
|
|
3145
|
-
seenMessages.add(override.messageId);
|
|
3146
1417
|
}
|
|
3147
1418
|
}
|
|
3148
1419
|
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
overrides: readonly StoredContextOverrideV8[],
|
|
3153
|
-
surfaces: readonly StoredContextSurfaceV8[],
|
|
3154
|
-
): void {
|
|
3155
|
-
const activations = this.loadSkillActivations();
|
|
3156
|
-
const activationByMessage = new Map(
|
|
3157
|
-
activations.map((activation) => [activation.activationMessageId, activation]),
|
|
3158
|
-
);
|
|
3159
|
-
const messages = new Map(
|
|
3160
|
-
canonical.messages.map((message) => [message.messageId, message]),
|
|
3161
|
-
);
|
|
3162
|
-
const results = new Map(
|
|
3163
|
-
canonical.toolResults.map((result) => [result.toolMessageId, result]),
|
|
3164
|
-
);
|
|
3165
|
-
const loadedResults = canonical.toolResults.filter(
|
|
3166
|
-
(result) =>
|
|
3167
|
-
result.completion.kind === "returned" &&
|
|
3168
|
-
result.completion.raw.kind === "skill" &&
|
|
3169
|
-
result.completion.raw.ok &&
|
|
3170
|
-
result.completion.raw.status === "loaded",
|
|
3171
|
-
);
|
|
3172
|
-
if (loadedResults.length !== activations.length) {
|
|
3173
|
-
throw new Error("Loaded Agent Skill results and activation rows differ.");
|
|
1420
|
+
async close(reason: SessionCloseReason): Promise<void> {
|
|
1421
|
+
if (this.closed) {
|
|
1422
|
+
return;
|
|
3174
1423
|
}
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
message.name !== "Skill" ||
|
|
3190
|
-
message.toolCallId !== activation.toolCallId ||
|
|
3191
|
-
raw?.kind !== "skill" ||
|
|
3192
|
-
!raw.ok ||
|
|
3193
|
-
raw.status !== "loaded" ||
|
|
3194
|
-
raw.name !== activation.name ||
|
|
3195
|
-
raw.scope !== activation.scope ||
|
|
3196
|
-
raw.sha256 !== activation.skillFileSha256
|
|
3197
|
-
) {
|
|
3198
|
-
throw new Error("Agent Skill activation canonical identity is invalid.");
|
|
3199
|
-
}
|
|
3200
|
-
if (activation.settledRevisionId !== undefined) {
|
|
3201
|
-
const revision = revisionsById.get(activation.settledRevisionId);
|
|
3202
|
-
const override = overridesByMessage.get(activation.activationMessageId);
|
|
3203
|
-
if (
|
|
3204
|
-
revision?.kind !== "skills_update" ||
|
|
3205
|
-
override?.introducedRevisionId !== revision.revisionId ||
|
|
3206
|
-
override.rendererFormat !== SKILL_ACTIVATION_RECEIPT_FORMAT
|
|
3207
|
-
) {
|
|
3208
|
-
throw new Error("Settled Agent Skill activation receipt is invalid.");
|
|
3209
|
-
}
|
|
3210
|
-
}
|
|
1424
|
+
let primaryError: unknown;
|
|
1425
|
+
const now = this.clock();
|
|
1426
|
+
try {
|
|
1427
|
+
runTransaction(this.database, () => {
|
|
1428
|
+
const updated = this.database
|
|
1429
|
+
.query(
|
|
1430
|
+
`UPDATE session_meta SET last_closed_at = ?, last_close_reason = ?, updated_at = ?
|
|
1431
|
+
WHERE singleton = 1`,
|
|
1432
|
+
)
|
|
1433
|
+
.run(now, reason, now);
|
|
1434
|
+
requireSingleChange(this.database, updated.changes, "close session activation");
|
|
1435
|
+
});
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
primaryError = sessionWriteError("close_session", this.sessionId, error);
|
|
3211
1438
|
}
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
throw new Error(
|
|
3217
|
-
"Context surface references an invalid Agent Skill activation.",
|
|
3218
|
-
);
|
|
3219
|
-
}
|
|
3220
|
-
}
|
|
1439
|
+
try {
|
|
1440
|
+
this.database.close();
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
primaryError ??= error;
|
|
3221
1443
|
}
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
revisionId: ContextRevisionId,
|
|
3228
|
-
): SwapOverride {
|
|
3229
|
-
if (
|
|
3230
|
-
message.name !== "Skill" ||
|
|
3231
|
-
result.completion.kind !== "returned" ||
|
|
3232
|
-
result.completion.raw.kind !== "skill" ||
|
|
3233
|
-
!result.completion.raw.ok ||
|
|
3234
|
-
result.completion.raw.status !== "loaded"
|
|
3235
|
-
) {
|
|
3236
|
-
throw new Error("Agent Skill receipt does not target a loaded Skill result.");
|
|
1444
|
+
this.closed = true;
|
|
1445
|
+
try {
|
|
1446
|
+
await this.lease.release();
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
primaryError ??= error;
|
|
3237
1449
|
}
|
|
3238
|
-
|
|
3239
|
-
(
|
|
3240
|
-
);
|
|
3241
|
-
if (
|
|
3242
|
-
activation === undefined ||
|
|
3243
|
-
activation.settledRevisionId !== revisionId ||
|
|
3244
|
-
(activation.state !== "promoted" && activation.state !== "rejected")
|
|
3245
|
-
) {
|
|
3246
|
-
throw new Error("Agent Skill receipt has no matching settled activation.");
|
|
1450
|
+
if (primaryError !== undefined) {
|
|
1451
|
+
throw asError(primaryError);
|
|
3247
1452
|
}
|
|
3248
|
-
return renderSkillActivationReceipt({
|
|
3249
|
-
message: {
|
|
3250
|
-
messageId: message.messageId,
|
|
3251
|
-
frameId: message.frameId,
|
|
3252
|
-
ordinal: message.ordinal,
|
|
3253
|
-
content: message.displayText,
|
|
3254
|
-
contentSha256: message.contentSha256,
|
|
3255
|
-
},
|
|
3256
|
-
name: activation.name,
|
|
3257
|
-
outcome:
|
|
3258
|
-
activation.state === "promoted"
|
|
3259
|
-
? "promoted"
|
|
3260
|
-
: activation.rejectionReason === "unavailable"
|
|
3261
|
-
? "unavailable"
|
|
3262
|
-
: "rejected",
|
|
3263
|
-
});
|
|
3264
1453
|
}
|
|
3265
1454
|
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
);
|
|
1455
|
+
async abandon(): Promise<void> {
|
|
1456
|
+
if (this.closed) {
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
try {
|
|
1460
|
+
this.database.close();
|
|
1461
|
+
} catch {
|
|
1462
|
+
// A failed delete path may already have closed the connection.
|
|
3272
1463
|
}
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
? undefined
|
|
3276
|
-
: decodeMeasuredContextState(row, this.sessionId);
|
|
1464
|
+
this.closed = true;
|
|
1465
|
+
await this.lease.release();
|
|
3277
1466
|
}
|
|
3278
1467
|
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
.
|
|
3285
|
-
.
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
"
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
.query("SELECT * FROM iterations WHERE turn_id = ? ORDER BY iteration_number")
|
|
3299
|
-
.all(turnId) as Array<Record<string, unknown>>;
|
|
3300
|
-
const storedLastIterationId = nullableStringFromSql(
|
|
3301
|
-
turn.last_iteration_id,
|
|
3302
|
-
"last_iteration_id",
|
|
3303
|
-
);
|
|
3304
|
-
const actualLastIterationId =
|
|
3305
|
-
iterations.length === 0
|
|
3306
|
-
? null
|
|
3307
|
-
: stringFromSql(iterations.at(-1)!.iteration_id, "iteration_id");
|
|
3308
|
-
if (storedLastIterationId !== actualLastIterationId) {
|
|
3309
|
-
throw new Error(`Last iteration identity is invalid in turn ${turnId}.`);
|
|
3310
|
-
}
|
|
3311
|
-
for (
|
|
3312
|
-
let iterationIndex = 0;
|
|
3313
|
-
iterationIndex < iterations.length;
|
|
3314
|
-
iterationIndex += 1
|
|
3315
|
-
) {
|
|
3316
|
-
const iteration = iterations[iterationIndex];
|
|
3317
|
-
if (
|
|
3318
|
-
numberFromSql(iteration.iteration_number, "iteration_number") !==
|
|
3319
|
-
iterationIndex + 1
|
|
3320
|
-
) {
|
|
3321
|
-
throw new Error(`Iteration number sequence has a gap in turn ${turnId}.`);
|
|
3322
|
-
}
|
|
3323
|
-
const iterationId = stringFromSql(iteration.iteration_id, "iteration_id");
|
|
3324
|
-
const outcome = enumFromSql(
|
|
3325
|
-
iteration.outcome,
|
|
3326
|
-
[
|
|
3327
|
-
"open",
|
|
3328
|
-
"continue",
|
|
3329
|
-
"completed",
|
|
3330
|
-
"failed",
|
|
3331
|
-
"cancelled",
|
|
3332
|
-
"interrupted",
|
|
3333
|
-
] as const,
|
|
3334
|
-
"iteration outcome",
|
|
3335
|
-
);
|
|
3336
|
-
if (iterationIndex < iterations.length - 1 && outcome !== "continue") {
|
|
3337
|
-
throw new Error(
|
|
3338
|
-
`Non-final iteration ${iterationId} must have continue outcome.`,
|
|
3339
|
-
);
|
|
3340
|
-
}
|
|
3341
|
-
const toolCalls = view.messages.flatMap((message) =>
|
|
3342
|
-
message.role === "assistant" && message.iterationId === iterationId
|
|
3343
|
-
? (message.toolCalls ?? [])
|
|
3344
|
-
: [],
|
|
3345
|
-
);
|
|
3346
|
-
if (
|
|
3347
|
-
numberFromSql(iteration.next_tool_call_number, "next_tool_call_number") !==
|
|
3348
|
-
toolCalls.length + 1
|
|
3349
|
-
) {
|
|
3350
|
-
throw new Error(`Tool call counter is invalid in iteration ${iterationId}.`);
|
|
3351
|
-
}
|
|
3352
|
-
}
|
|
3353
|
-
if (
|
|
3354
|
-
numberFromSql(turn.next_iteration_number, "next_iteration_number") !==
|
|
3355
|
-
iterations.length + 1
|
|
3356
|
-
) {
|
|
3357
|
-
throw new Error(`Iteration counter is invalid in turn ${turnId}.`);
|
|
3358
|
-
}
|
|
3359
|
-
const openIterationCount = iterations.filter(
|
|
3360
|
-
(iteration) => iteration.outcome === "open",
|
|
3361
|
-
).length;
|
|
3362
|
-
if (turnStatus === "open" && openIterationCount > 1) {
|
|
3363
|
-
throw new Error(`Open turn ${turnId} has multiple open iterations.`);
|
|
3364
|
-
}
|
|
3365
|
-
if (turnStatus !== "open" && openIterationCount !== 0) {
|
|
3366
|
-
throw new Error(`Terminal turn ${turnId} still has an open iteration.`);
|
|
3367
|
-
}
|
|
3368
|
-
const lastOutcome = iterations.at(-1)?.outcome;
|
|
3369
|
-
if (
|
|
3370
|
-
turnStatus !== "open" &&
|
|
3371
|
-
iterations.length > 0 &&
|
|
3372
|
-
lastOutcome !== turnStatus &&
|
|
3373
|
-
!(turnStatus === "interrupted" && lastOutcome === "continue")
|
|
3374
|
-
) {
|
|
3375
|
-
throw new Error(
|
|
3376
|
-
`Terminal turn ${turnId} does not match its last iteration outcome.`,
|
|
3377
|
-
);
|
|
3378
|
-
}
|
|
3379
|
-
const finalMessageId = nullableStringFromSql(
|
|
3380
|
-
turn.final_message_id,
|
|
3381
|
-
"final_message_id",
|
|
1468
|
+
async deleteFromDisk(): Promise<void> {
|
|
1469
|
+
this.requireOpen();
|
|
1470
|
+
const known = new Set([
|
|
1471
|
+
"session.sqlite",
|
|
1472
|
+
"session.sqlite-wal",
|
|
1473
|
+
"session.sqlite-shm",
|
|
1474
|
+
"events.jsonl",
|
|
1475
|
+
"observations.md",
|
|
1476
|
+
"active.lock",
|
|
1477
|
+
"active.lock.reclaim",
|
|
1478
|
+
]);
|
|
1479
|
+
const entries = await readdir(this.sessionDirectory);
|
|
1480
|
+
const unknown = entries.filter((entry) => !known.has(entry));
|
|
1481
|
+
if (unknown.length > 0) {
|
|
1482
|
+
throw new SessionError(
|
|
1483
|
+
"SESSION_DELETE_BLOCKED",
|
|
1484
|
+
"delete_session",
|
|
1485
|
+
`Session directory contains unknown files: ${unknown.join(", ")}.`,
|
|
1486
|
+
{ sessionId: this.sessionId },
|
|
3382
1487
|
);
|
|
3383
|
-
if (turnStatus === "completed") {
|
|
3384
|
-
const finalMessage = view.messages.find(
|
|
3385
|
-
(message) => message.messageId === finalMessageId,
|
|
3386
|
-
);
|
|
3387
|
-
const lastTurnMessage = [...view.messages]
|
|
3388
|
-
.reverse()
|
|
3389
|
-
.find((message) => "turnId" in message && message.turnId === turnId);
|
|
3390
|
-
if (
|
|
3391
|
-
finalMessage?.role !== "assistant" ||
|
|
3392
|
-
finalMessage.turnId !== turnId ||
|
|
3393
|
-
(finalMessage.toolCalls?.length ?? 0) !== 0 ||
|
|
3394
|
-
lastTurnMessage?.messageId !== finalMessage.messageId
|
|
3395
|
-
) {
|
|
3396
|
-
throw new Error(`Final message identity is invalid in turn ${turnId}.`);
|
|
3397
|
-
}
|
|
3398
|
-
} else if (finalMessageId !== null) {
|
|
3399
|
-
throw new Error(`Non-completed turn ${turnId} has a final message.`);
|
|
3400
|
-
}
|
|
3401
1488
|
}
|
|
3402
|
-
if (meta.nextTurnNumber !== turns.length + 1) {
|
|
3403
|
-
throw new Error("Session turn counter is invalid.");
|
|
3404
|
-
}
|
|
3405
|
-
}
|
|
3406
1489
|
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
1490
|
+
this.database.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
1491
|
+
this.database.close();
|
|
1492
|
+
const tombstone = `${this.sessionDirectory}.deleting-${randomUUID()}`;
|
|
1493
|
+
try {
|
|
1494
|
+
await rename(this.sessionDirectory, tombstone);
|
|
1495
|
+
} catch (error) {
|
|
1496
|
+
this.closed = true;
|
|
1497
|
+
await this.lease.release().catch(() => undefined);
|
|
1498
|
+
throw error;
|
|
3413
1499
|
}
|
|
3414
|
-
|
|
3415
|
-
|
|
1500
|
+
this.lease.relocate(tombstone);
|
|
1501
|
+
await this.lease.release();
|
|
1502
|
+
this.closed = true;
|
|
3416
1503
|
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
1504
|
+
try {
|
|
1505
|
+
for (const name of known) {
|
|
1506
|
+
await unlinkIfExists(path.join(tombstone, name));
|
|
1507
|
+
}
|
|
1508
|
+
await rmdir(tombstone);
|
|
1509
|
+
} catch (error) {
|
|
1510
|
+
throw new SessionError(
|
|
1511
|
+
"SESSION_DELETE_BLOCKED",
|
|
1512
|
+
"delete_session_cleanup",
|
|
1513
|
+
`Session was removed from the catalog, but tombstone cleanup failed: ${tombstone}.`,
|
|
1514
|
+
{ sessionId: this.sessionId, cause: error },
|
|
1515
|
+
);
|
|
3423
1516
|
}
|
|
3424
|
-
return row;
|
|
3425
|
-
}
|
|
3426
|
-
|
|
3427
|
-
private touch(timestamp: string): void {
|
|
3428
|
-
const updated = this.database
|
|
3429
|
-
.query("UPDATE session_meta SET updated_at = ? WHERE singleton = 1")
|
|
3430
|
-
.run(timestamp);
|
|
3431
|
-
requireSingleChange(this.database, updated.changes, "touch session");
|
|
3432
|
-
}
|
|
3433
|
-
|
|
3434
|
-
private recoveryError(message: string): SessionError {
|
|
3435
|
-
return new SessionError("SESSION_RECOVERY_FAILED", "recover_session", message, {
|
|
3436
|
-
sessionId: this.sessionId,
|
|
3437
|
-
});
|
|
3438
1517
|
}
|
|
3439
1518
|
|
|
3440
1519
|
private requireOpen(): void {
|
|
@@ -3450,45 +1529,6 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
3450
1529
|
}
|
|
3451
1530
|
}
|
|
3452
1531
|
|
|
3453
|
-
function insertPendingSkillActivation(
|
|
3454
|
-
database: Database,
|
|
3455
|
-
message: CanonicalMessageRecord,
|
|
3456
|
-
result: ToolResultRecord,
|
|
3457
|
-
now: string,
|
|
3458
|
-
): void {
|
|
3459
|
-
if (
|
|
3460
|
-
message.role !== "tool" ||
|
|
3461
|
-
result.completion.kind !== "returned" ||
|
|
3462
|
-
result.completion.raw.kind !== "skill" ||
|
|
3463
|
-
!result.completion.raw.ok ||
|
|
3464
|
-
result.completion.raw.status !== "loaded"
|
|
3465
|
-
) {
|
|
3466
|
-
return;
|
|
3467
|
-
}
|
|
3468
|
-
const raw = result.completion.raw;
|
|
3469
|
-
if (message.name !== "Skill" || message.messageId !== result.toolMessageId) {
|
|
3470
|
-
throw new Error("Loaded Agent Skill completion has invalid tool identity.");
|
|
3471
|
-
}
|
|
3472
|
-
database
|
|
3473
|
-
.query(
|
|
3474
|
-
`INSERT INTO skill_activations (
|
|
3475
|
-
activation_message_id, tool_call_id, session_id, name, scope,
|
|
3476
|
-
skill_file_sha256, state, dispatched_iteration_id, settled_revision_id,
|
|
3477
|
-
rejection_reason, created_at, updated_at
|
|
3478
|
-
) VALUES (?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, NULL, ?, ?)`,
|
|
3479
|
-
)
|
|
3480
|
-
.run(
|
|
3481
|
-
message.messageId,
|
|
3482
|
-
result.toolCallId,
|
|
3483
|
-
result.sessionId,
|
|
3484
|
-
raw.name,
|
|
3485
|
-
raw.scope,
|
|
3486
|
-
raw.sha256,
|
|
3487
|
-
now,
|
|
3488
|
-
now,
|
|
3489
|
-
);
|
|
3490
|
-
}
|
|
3491
|
-
|
|
3492
1532
|
export async function resolveSessionDatabasePath(
|
|
3493
1533
|
workspaceRoot: string,
|
|
3494
1534
|
sessionId: SessionId,
|
|
@@ -3502,437 +1542,6 @@ export async function resolveSessionDatabasePath(
|
|
|
3502
1542
|
);
|
|
3503
1543
|
}
|
|
3504
1544
|
|
|
3505
|
-
function insertFrame(database: Database, frame: ProtocolFrame): void {
|
|
3506
|
-
database
|
|
3507
|
-
.query(
|
|
3508
|
-
`INSERT INTO protocol_frames (
|
|
3509
|
-
frame_id, session_id, turn_id, iteration_id, kind, state,
|
|
3510
|
-
first_ordinal, last_ordinal, created_at, closed_at
|
|
3511
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3512
|
-
)
|
|
3513
|
-
.run(
|
|
3514
|
-
frame.frameId,
|
|
3515
|
-
frame.sessionId,
|
|
3516
|
-
frame.turnId ?? null,
|
|
3517
|
-
frame.iterationId ?? null,
|
|
3518
|
-
frame.kind,
|
|
3519
|
-
frame.state,
|
|
3520
|
-
frame.firstOrdinal,
|
|
3521
|
-
frame.lastOrdinal ?? null,
|
|
3522
|
-
frame.createdAt,
|
|
3523
|
-
frame.closedAt ?? null,
|
|
3524
|
-
);
|
|
3525
|
-
}
|
|
3526
|
-
|
|
3527
|
-
function insertMessage(database: Database, message: CanonicalMessageRecord): void {
|
|
3528
|
-
const assistant = message.role === "assistant" ? message : undefined;
|
|
3529
|
-
const tool = message.role === "tool" ? message : undefined;
|
|
3530
|
-
const turnId = "turnId" in message ? message.turnId : null;
|
|
3531
|
-
const iterationId = "iterationId" in message ? message.iterationId : null;
|
|
3532
|
-
const reasoningPresent =
|
|
3533
|
-
assistant !== undefined && assistant.reasoningContent !== undefined ? 1 : 0;
|
|
3534
|
-
database
|
|
3535
|
-
.query(
|
|
3536
|
-
`INSERT INTO messages (
|
|
3537
|
-
message_id, session_id, frame_id, ordinal, role, turn_id, iteration_id,
|
|
3538
|
-
content, content_sha256, reasoning_content, reasoning_content_present,
|
|
3539
|
-
tool_calls_json, provider, model, tool_call_id, provider_tool_call_id,
|
|
3540
|
-
name, origin, created_at
|
|
3541
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3542
|
-
)
|
|
3543
|
-
.run(
|
|
3544
|
-
message.messageId,
|
|
3545
|
-
message.sessionId,
|
|
3546
|
-
message.frameId,
|
|
3547
|
-
message.ordinal,
|
|
3548
|
-
message.role,
|
|
3549
|
-
turnId,
|
|
3550
|
-
iterationId,
|
|
3551
|
-
message.role === "tool" ? message.displayText : message.content,
|
|
3552
|
-
message.contentSha256,
|
|
3553
|
-
assistant?.reasoningContent ?? null,
|
|
3554
|
-
reasoningPresent,
|
|
3555
|
-
assistant?.toolCalls === undefined
|
|
3556
|
-
? null
|
|
3557
|
-
: stableJsonStringify(assistant.toolCalls),
|
|
3558
|
-
assistant?.provider ?? null,
|
|
3559
|
-
assistant?.model ?? null,
|
|
3560
|
-
tool?.toolCallId ?? null,
|
|
3561
|
-
tool?.providerToolCallId ?? null,
|
|
3562
|
-
tool?.name ?? null,
|
|
3563
|
-
message.origin,
|
|
3564
|
-
message.createdAt,
|
|
3565
|
-
);
|
|
3566
|
-
if (message.role === "user" && message.attachments !== undefined) {
|
|
3567
|
-
insertMessageImageAttachments(database, message);
|
|
3568
|
-
}
|
|
3569
|
-
if (message.role === "tool") {
|
|
3570
|
-
insertToolMessageContentBlocks(database, message);
|
|
3571
|
-
}
|
|
3572
|
-
}
|
|
3573
|
-
|
|
3574
|
-
function insertMessageImageAttachments(
|
|
3575
|
-
database: Database,
|
|
3576
|
-
message: Extract<CanonicalMessageRecord, { role: "user" }>,
|
|
3577
|
-
): void {
|
|
3578
|
-
const userMessage = {
|
|
3579
|
-
role: "user" as const,
|
|
3580
|
-
content: message.content,
|
|
3581
|
-
attachments: message.attachments,
|
|
3582
|
-
};
|
|
3583
|
-
validateUserMessage(userMessage);
|
|
3584
|
-
if (userMessageHash(userMessage) !== message.contentSha256) {
|
|
3585
|
-
throw new Error("User image attachment hash does not match the message hash.");
|
|
3586
|
-
}
|
|
3587
|
-
for (let position = 0; position < message.attachments!.length; position += 1) {
|
|
3588
|
-
const attachment = requireItem(message.attachments!, position, "image attachment");
|
|
3589
|
-
ensureImageAsset(
|
|
3590
|
-
database,
|
|
3591
|
-
imageAssetRefFromAttachment(attachment),
|
|
3592
|
-
message.createdAt,
|
|
3593
|
-
);
|
|
3594
|
-
database
|
|
3595
|
-
.query(
|
|
3596
|
-
`INSERT INTO message_image_attachments (
|
|
3597
|
-
message_id, attachment_id, asset_id, position, label,
|
|
3598
|
-
range_start, range_end, original_name
|
|
3599
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3600
|
-
)
|
|
3601
|
-
.run(
|
|
3602
|
-
message.messageId,
|
|
3603
|
-
attachment.attachmentId,
|
|
3604
|
-
attachment.assetId,
|
|
3605
|
-
position,
|
|
3606
|
-
attachment.label,
|
|
3607
|
-
attachment.range.start,
|
|
3608
|
-
attachment.range.end,
|
|
3609
|
-
attachment.originalName,
|
|
3610
|
-
);
|
|
3611
|
-
}
|
|
3612
|
-
}
|
|
3613
|
-
|
|
3614
|
-
function insertToolMessageContentBlocks(
|
|
3615
|
-
database: Database,
|
|
3616
|
-
message: Extract<CanonicalMessageRecord, { role: "tool" }>,
|
|
3617
|
-
): void {
|
|
3618
|
-
validateToolResultContent(message.content);
|
|
3619
|
-
if (
|
|
3620
|
-
canonicalToolResultContentHash(message.content) !== message.contentSha256 ||
|
|
3621
|
-
toolResultDisplayText(message.content) !== message.displayText
|
|
3622
|
-
) {
|
|
3623
|
-
throw new Error("Tool content blocks do not match canonical message metadata.");
|
|
3624
|
-
}
|
|
3625
|
-
for (let position = 0; position < message.content.length; position += 1) {
|
|
3626
|
-
const block = requireItem(message.content, position, "tool content block");
|
|
3627
|
-
if (block.type === "image") {
|
|
3628
|
-
ensureImageAsset(database, block.asset, message.createdAt);
|
|
3629
|
-
}
|
|
3630
|
-
database
|
|
3631
|
-
.query(
|
|
3632
|
-
`INSERT INTO tool_message_content_blocks (
|
|
3633
|
-
message_id, position, kind, text_content, asset_id
|
|
3634
|
-
) VALUES (?, ?, ?, ?, ?)`,
|
|
3635
|
-
)
|
|
3636
|
-
.run(
|
|
3637
|
-
message.messageId,
|
|
3638
|
-
position,
|
|
3639
|
-
block.type,
|
|
3640
|
-
block.type === "text" ? block.text : null,
|
|
3641
|
-
block.type === "image" ? block.asset.assetId : null,
|
|
3642
|
-
);
|
|
3643
|
-
}
|
|
3644
|
-
}
|
|
3645
|
-
|
|
3646
|
-
function ensureImageAsset(
|
|
3647
|
-
database: Database,
|
|
3648
|
-
asset: ImageAssetRef,
|
|
3649
|
-
createdAt: string,
|
|
3650
|
-
): void {
|
|
3651
|
-
const existing = database
|
|
3652
|
-
.query(
|
|
3653
|
-
`SELECT mime_type, byte_length, width, height, created_at
|
|
3654
|
-
FROM image_assets WHERE asset_id = ?`,
|
|
3655
|
-
)
|
|
3656
|
-
.get(asset.assetId) as {
|
|
3657
|
-
mime_type: unknown;
|
|
3658
|
-
byte_length: unknown;
|
|
3659
|
-
width: unknown;
|
|
3660
|
-
height: unknown;
|
|
3661
|
-
created_at: unknown;
|
|
3662
|
-
} | null;
|
|
3663
|
-
if (existing === null) {
|
|
3664
|
-
database
|
|
3665
|
-
.query(
|
|
3666
|
-
`INSERT INTO image_assets (
|
|
3667
|
-
asset_id, mime_type, byte_length, width, height, created_at
|
|
3668
|
-
) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
3669
|
-
)
|
|
3670
|
-
.run(
|
|
3671
|
-
asset.assetId,
|
|
3672
|
-
asset.mimeType,
|
|
3673
|
-
asset.byteLength,
|
|
3674
|
-
asset.width,
|
|
3675
|
-
asset.height,
|
|
3676
|
-
createdAt,
|
|
3677
|
-
);
|
|
3678
|
-
return;
|
|
3679
|
-
}
|
|
3680
|
-
timestampFromSql(existing.created_at, "image asset created_at");
|
|
3681
|
-
if (
|
|
3682
|
-
existing.mime_type !== asset.mimeType ||
|
|
3683
|
-
numberFromSql(existing.byte_length, "image asset byte_length") !==
|
|
3684
|
-
asset.byteLength ||
|
|
3685
|
-
numberFromSql(existing.width, "image asset width") !== asset.width ||
|
|
3686
|
-
numberFromSql(existing.height, "image asset height") !== asset.height
|
|
3687
|
-
) {
|
|
3688
|
-
throw new Error(`Image asset metadata conflicts for ${asset.assetId}.`);
|
|
3689
|
-
}
|
|
3690
|
-
}
|
|
3691
|
-
|
|
3692
|
-
function insertToolResult(database: Database, result: ToolResultRecord): void {
|
|
3693
|
-
const returned = result.completion.kind === "returned" ? result.completion : null;
|
|
3694
|
-
const synthetic = result.completion.kind === "synthetic" ? result.completion : null;
|
|
3695
|
-
database
|
|
3696
|
-
.query(
|
|
3697
|
-
`INSERT INTO tool_results (
|
|
3698
|
-
tool_call_id, session_id, frame_id, tool_message_id, completion_kind,
|
|
3699
|
-
raw_json, raw_sha256, observation_format, synthetic_reason,
|
|
3700
|
-
synthetic_detail, observation_sha256, created_at
|
|
3701
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3702
|
-
)
|
|
3703
|
-
.run(
|
|
3704
|
-
result.toolCallId,
|
|
3705
|
-
result.sessionId,
|
|
3706
|
-
result.frameId,
|
|
3707
|
-
result.toolMessageId,
|
|
3708
|
-
result.completion.kind,
|
|
3709
|
-
returned === null ? null : stableJsonStringify(returned.raw),
|
|
3710
|
-
returned?.rawSha256 ?? null,
|
|
3711
|
-
returned?.observationFormat ?? null,
|
|
3712
|
-
synthetic?.reason ?? null,
|
|
3713
|
-
synthetic?.detail ?? null,
|
|
3714
|
-
result.observationSha256,
|
|
3715
|
-
result.createdAt,
|
|
3716
|
-
);
|
|
3717
|
-
}
|
|
3718
|
-
|
|
3719
|
-
function insertContextSurface(
|
|
3720
|
-
database: Database,
|
|
3721
|
-
surface: StoredContextSurfaceV8,
|
|
3722
|
-
): void {
|
|
3723
|
-
validateStoredContextSurface(surface);
|
|
3724
|
-
database
|
|
3725
|
-
.query(
|
|
3726
|
-
`INSERT INTO context_surfaces (
|
|
3727
|
-
surface_id, session_id, system_prompt, system_prompt_sha256,
|
|
3728
|
-
recall_contract_version,
|
|
3729
|
-
project_instruction_json, skill_catalog_json, skill_catalog_sha256,
|
|
3730
|
-
active_skills_json, active_skills_sha256, tool_definitions_json,
|
|
3731
|
-
tool_definitions_sha256, tool_schema_sha256, request_config_sha256,
|
|
3732
|
-
request_max_output_tokens, surface_sha256, created_at
|
|
3733
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3734
|
-
)
|
|
3735
|
-
.run(
|
|
3736
|
-
surface.surfaceId,
|
|
3737
|
-
surface.sessionId,
|
|
3738
|
-
surface.systemPrompt,
|
|
3739
|
-
surface.systemPromptSha256,
|
|
3740
|
-
surface.recallContractVersion,
|
|
3741
|
-
surface.projectInstruction === undefined
|
|
3742
|
-
? null
|
|
3743
|
-
: stableJsonStringify(surface.projectInstruction),
|
|
3744
|
-
stableJsonStringify(surface.skillCatalog),
|
|
3745
|
-
surface.skillCatalogSha256,
|
|
3746
|
-
stableJsonStringify(surface.activeSkills),
|
|
3747
|
-
surface.activeSkillsSha256,
|
|
3748
|
-
stableJsonStringify(surface.toolDefinitions),
|
|
3749
|
-
surface.toolDefinitionsSha256,
|
|
3750
|
-
surface.toolSchemaSha256,
|
|
3751
|
-
surface.requestConfigSha256,
|
|
3752
|
-
surface.requestMaxOutputTokens,
|
|
3753
|
-
surface.surfaceSha256,
|
|
3754
|
-
surface.createdAt,
|
|
3755
|
-
);
|
|
3756
|
-
}
|
|
3757
|
-
|
|
3758
|
-
function decodeMeasuredContextState(
|
|
3759
|
-
value: unknown,
|
|
3760
|
-
expectedSessionId: SessionId,
|
|
3761
|
-
): StoredMeasuredContextState {
|
|
3762
|
-
const row = recordFromSql(value, "context measurement state");
|
|
3763
|
-
const sessionId = stringFromSql(row.session_id, "session_id") as SessionId;
|
|
3764
|
-
if (sessionId !== expectedSessionId) {
|
|
3765
|
-
throw new Error(
|
|
3766
|
-
`Context measurement session ID ${sessionId} does not match store.`,
|
|
3767
|
-
);
|
|
3768
|
-
}
|
|
3769
|
-
const promptTokens = numberFromSql(row.prompt_tokens, "prompt_tokens");
|
|
3770
|
-
const completionTokens = numberFromSql(row.completion_tokens, "completion_tokens");
|
|
3771
|
-
const totalTokens = numberFromSql(row.total_tokens, "total_tokens");
|
|
3772
|
-
if (totalTokens !== promptTokens + completionTokens) {
|
|
3773
|
-
throw new Error(
|
|
3774
|
-
"Context measurement total_tokens must equal prompt_tokens + completion_tokens.",
|
|
3775
|
-
);
|
|
3776
|
-
}
|
|
3777
|
-
timestampFromSql(row.updated_at, "updated_at");
|
|
3778
|
-
return Object.freeze({
|
|
3779
|
-
revisionId: stringFromSql(row.revision_id, "revision_id") as ContextRevisionId,
|
|
3780
|
-
anchor: Object.freeze({
|
|
3781
|
-
totalTokens,
|
|
3782
|
-
promptTokens,
|
|
3783
|
-
completionTokens,
|
|
3784
|
-
segmentCount: numberFromSql(row.segment_count, "segment_count"),
|
|
3785
|
-
prefixHash: sha256FromSql(row.prefix_hash, "prefix_hash"),
|
|
3786
|
-
requestConfigHash: sha256FromSql(row.request_config_hash, "request_config_hash"),
|
|
3787
|
-
toolSchemaHash: sha256FromSql(row.tool_schema_hash, "tool_schema_hash"),
|
|
3788
|
-
}),
|
|
3789
|
-
});
|
|
3790
|
-
}
|
|
3791
|
-
|
|
3792
|
-
function assertCommitSwapRevisionInput(input: CommitSwapRevisionInput): void {
|
|
3793
|
-
if (
|
|
3794
|
-
input.revisionId.trim() === "" ||
|
|
3795
|
-
input.expectedBaseRevisionId.trim() === "" ||
|
|
3796
|
-
!Number.isSafeInteger(input.expectedBaseRevisionNumber) ||
|
|
3797
|
-
input.expectedBaseRevisionNumber < 1 ||
|
|
3798
|
-
!Number.isSafeInteger(input.expectedCanonicalThroughOrdinal) ||
|
|
3799
|
-
input.expectedCanonicalThroughOrdinal < 1 ||
|
|
3800
|
-
input.addedOverrides.length < 1 ||
|
|
3801
|
-
input.policyVersion !== "swap-only-v1" ||
|
|
3802
|
-
input.rendererFormat !== SWAP_OBSERVATION_FORMAT
|
|
3803
|
-
) {
|
|
3804
|
-
throw new Error("Commit swap revision input is invalid.");
|
|
3805
|
-
}
|
|
3806
|
-
for (const [name, hash] of [
|
|
3807
|
-
[
|
|
3808
|
-
"expectedBaseActiveOverrideManifestSha256",
|
|
3809
|
-
input.expectedBaseActiveOverrideManifestSha256,
|
|
3810
|
-
],
|
|
3811
|
-
["planHash", input.planHash],
|
|
3812
|
-
["nextActiveOverrideManifestSha256", input.nextActiveOverrideManifestSha256],
|
|
3813
|
-
["canonicalSequenceSha256", input.canonicalSequenceSha256],
|
|
3814
|
-
["renderedMessageSha256", input.renderedMessageSha256],
|
|
3815
|
-
] as const) {
|
|
3816
|
-
if (!/^[0-9a-f]{64}$/.test(hash)) {
|
|
3817
|
-
throw new Error(`Commit swap revision ${name} must be a SHA-256 hash.`);
|
|
3818
|
-
}
|
|
3819
|
-
}
|
|
3820
|
-
}
|
|
3821
|
-
|
|
3822
|
-
function assertCommitSurfaceRefreshInput(input: CommitSurfaceRefreshInput): void {
|
|
3823
|
-
if (
|
|
3824
|
-
input.revisionId.trim() === "" ||
|
|
3825
|
-
input.expectedBaseRevisionId.trim() === "" ||
|
|
3826
|
-
!Number.isSafeInteger(input.expectedBaseRevisionNumber) ||
|
|
3827
|
-
input.expectedBaseRevisionNumber < 1 ||
|
|
3828
|
-
!Number.isSafeInteger(input.expectedCanonicalThroughOrdinal) ||
|
|
3829
|
-
input.expectedCanonicalThroughOrdinal < 1
|
|
3830
|
-
) {
|
|
3831
|
-
throw new Error("Commit surface refresh input is invalid.");
|
|
3832
|
-
}
|
|
3833
|
-
for (const [name, hash] of [
|
|
3834
|
-
[
|
|
3835
|
-
"expectedBaseActiveOverrideManifestSha256",
|
|
3836
|
-
input.expectedBaseActiveOverrideManifestSha256,
|
|
3837
|
-
],
|
|
3838
|
-
["changeManifestSha256", input.changeManifestSha256],
|
|
3839
|
-
["canonicalSequenceSha256", input.canonicalSequenceSha256],
|
|
3840
|
-
["renderedMessageSha256", input.renderedMessageSha256],
|
|
3841
|
-
] as const) {
|
|
3842
|
-
if (!/^[0-9a-f]{64}$/.test(hash)) {
|
|
3843
|
-
throw new Error(`Commit surface refresh ${name} must be a SHA-256 hash.`);
|
|
3844
|
-
}
|
|
3845
|
-
}
|
|
3846
|
-
}
|
|
3847
|
-
|
|
3848
|
-
function assertCommitSkillsUpdateInput(input: CommitSkillsUpdateInput): void {
|
|
3849
|
-
if (
|
|
3850
|
-
input.revisionId.trim() === "" ||
|
|
3851
|
-
input.expectedBaseRevisionId.trim() === "" ||
|
|
3852
|
-
!Number.isSafeInteger(input.expectedBaseRevisionNumber) ||
|
|
3853
|
-
input.expectedBaseRevisionNumber < 1 ||
|
|
3854
|
-
!Number.isSafeInteger(input.expectedCanonicalThroughOrdinal) ||
|
|
3855
|
-
input.expectedCanonicalThroughOrdinal < 1 ||
|
|
3856
|
-
input.addedOverrides.length < 1 ||
|
|
3857
|
-
input.settlements.length !== input.addedOverrides.length
|
|
3858
|
-
) {
|
|
3859
|
-
throw new Error("Commit Agent Skills update input is invalid.");
|
|
3860
|
-
}
|
|
3861
|
-
for (const [name, hash] of [
|
|
3862
|
-
[
|
|
3863
|
-
"expectedBaseActiveOverrideManifestSha256",
|
|
3864
|
-
input.expectedBaseActiveOverrideManifestSha256,
|
|
3865
|
-
],
|
|
3866
|
-
["changeManifestSha256", input.changeManifestSha256],
|
|
3867
|
-
["activationManifestSha256", input.activationManifestSha256],
|
|
3868
|
-
["nextActiveOverrideManifestSha256", input.nextActiveOverrideManifestSha256],
|
|
3869
|
-
["canonicalSequenceSha256", input.canonicalSequenceSha256],
|
|
3870
|
-
["renderedMessageSha256", input.renderedMessageSha256],
|
|
3871
|
-
] as const) {
|
|
3872
|
-
if (!/^[0-9a-f]{64}$/.test(hash)) {
|
|
3873
|
-
throw new Error(`Commit Agent Skills update ${name} must be a SHA-256 hash.`);
|
|
3874
|
-
}
|
|
3875
|
-
}
|
|
3876
|
-
}
|
|
3877
|
-
|
|
3878
|
-
function assertCommitPrefixRetirementRevisionInput(
|
|
3879
|
-
input: CommitPrefixRetirementRevisionInput,
|
|
3880
|
-
): void {
|
|
3881
|
-
if (
|
|
3882
|
-
input.revisionId.trim() === "" ||
|
|
3883
|
-
input.expectedBaseRevisionId.trim() === "" ||
|
|
3884
|
-
input.policyVersion !== "recall-first-retirement-v1" ||
|
|
3885
|
-
!Number.isSafeInteger(input.expectedBaseRevisionNumber) ||
|
|
3886
|
-
input.expectedBaseRevisionNumber < 1 ||
|
|
3887
|
-
!Number.isSafeInteger(input.expectedBaseKeepFromOrdinal) ||
|
|
3888
|
-
input.expectedBaseKeepFromOrdinal < 1 ||
|
|
3889
|
-
!Number.isSafeInteger(input.expectedCanonicalThroughOrdinal) ||
|
|
3890
|
-
input.expectedCanonicalThroughOrdinal < 1 ||
|
|
3891
|
-
!Number.isSafeInteger(input.nextKeepFromOrdinal) ||
|
|
3892
|
-
input.nextKeepFromOrdinal <= input.expectedBaseKeepFromOrdinal ||
|
|
3893
|
-
input.retiredThroughOrdinal !== input.nextKeepFromOrdinal - 1 ||
|
|
3894
|
-
!Number.isSafeInteger(input.retiredTurnCount) ||
|
|
3895
|
-
input.retiredTurnCount < 1 ||
|
|
3896
|
-
!Number.isSafeInteger(input.retiredFrameCount) ||
|
|
3897
|
-
input.retiredFrameCount < 1 ||
|
|
3898
|
-
!Number.isSafeInteger(input.retiredMessageCount) ||
|
|
3899
|
-
input.retiredMessageCount < 1 ||
|
|
3900
|
-
!Number.isSafeInteger(input.nextActiveOverrideCount) ||
|
|
3901
|
-
input.nextActiveOverrideCount < 0
|
|
3902
|
-
) {
|
|
3903
|
-
throw new Error("Commit prefix retirement input is invalid.");
|
|
3904
|
-
}
|
|
3905
|
-
for (const [name, hash] of [
|
|
3906
|
-
["expectedSurfaceSha256", input.expectedSurfaceSha256],
|
|
3907
|
-
[
|
|
3908
|
-
"expectedBaseActiveOverrideManifestSha256",
|
|
3909
|
-
input.expectedBaseActiveOverrideManifestSha256,
|
|
3910
|
-
],
|
|
3911
|
-
["planHash", input.planHash],
|
|
3912
|
-
["nextActiveOverrideManifestSha256", input.nextActiveOverrideManifestSha256],
|
|
3913
|
-
["canonicalSequenceSha256", input.canonicalSequenceSha256],
|
|
3914
|
-
["renderedMessageSha256", input.renderedMessageSha256],
|
|
3915
|
-
] as const) {
|
|
3916
|
-
if (!/^[0-9a-f]{64}$/.test(hash)) {
|
|
3917
|
-
throw new Error(`Commit prefix retirement ${name} must be a SHA-256 hash.`);
|
|
3918
|
-
}
|
|
3919
|
-
}
|
|
3920
|
-
}
|
|
3921
|
-
|
|
3922
|
-
function requireActiveRevisionId(meta: StoredSessionMetaV10): ContextRevisionId {
|
|
3923
|
-
if (meta.initializationState !== "ready" || meta.activeRevisionId === null) {
|
|
3924
|
-
throw new Error("Session has no active context revision.");
|
|
3925
|
-
}
|
|
3926
|
-
return meta.activeRevisionId;
|
|
3927
|
-
}
|
|
3928
|
-
|
|
3929
|
-
function previousRevision(
|
|
3930
|
-
revisions: readonly StoredContextRevisionV8[],
|
|
3931
|
-
revision: StoredContextRevisionV8,
|
|
3932
|
-
): StoredContextRevisionV8 | undefined {
|
|
3933
|
-
return revisions[revision.revisionNumber - 2];
|
|
3934
|
-
}
|
|
3935
|
-
|
|
3936
1545
|
function openWritableDatabase(databasePath: string): Database {
|
|
3937
1546
|
const database = new Database(databasePath, {
|
|
3938
1547
|
create: false,
|
|
@@ -3944,45 +1553,6 @@ function openWritableDatabase(databasePath: string): Database {
|
|
|
3944
1553
|
return database;
|
|
3945
1554
|
}
|
|
3946
1555
|
|
|
3947
|
-
function runTransaction<T>(database: Database, operation: () => T): T {
|
|
3948
|
-
database.exec("BEGIN IMMEDIATE");
|
|
3949
|
-
try {
|
|
3950
|
-
const result = operation();
|
|
3951
|
-
database.exec("COMMIT");
|
|
3952
|
-
return result;
|
|
3953
|
-
} catch (error) {
|
|
3954
|
-
try {
|
|
3955
|
-
database.exec("ROLLBACK");
|
|
3956
|
-
} catch {
|
|
3957
|
-
// Preserve the mutation error; the session will fault and close the database.
|
|
3958
|
-
}
|
|
3959
|
-
throw error;
|
|
3960
|
-
}
|
|
3961
|
-
}
|
|
3962
|
-
|
|
3963
|
-
function requireSingleChange(
|
|
3964
|
-
database: Database,
|
|
3965
|
-
reportedChanges: number | bigint,
|
|
3966
|
-
operation: string,
|
|
3967
|
-
): void {
|
|
3968
|
-
const row = database.query("SELECT changes() AS changes").get() as {
|
|
3969
|
-
changes: number | bigint;
|
|
3970
|
-
};
|
|
3971
|
-
if (Number(row.changes) !== 1) {
|
|
3972
|
-
throw new Error(
|
|
3973
|
-
`${operation} must change exactly one row; changed ${row.changes} (driver reported ${reportedChanges}).`,
|
|
3974
|
-
);
|
|
3975
|
-
}
|
|
3976
|
-
}
|
|
3977
|
-
|
|
3978
|
-
function requireItem<T>(items: readonly T[], index: number, name: string): T {
|
|
3979
|
-
const item = items[index];
|
|
3980
|
-
if (item === undefined) {
|
|
3981
|
-
throw new Error(`Missing ${name} at index ${index}.`);
|
|
3982
|
-
}
|
|
3983
|
-
return item;
|
|
3984
|
-
}
|
|
3985
|
-
|
|
3986
1556
|
function errorMessage(error: unknown): string {
|
|
3987
1557
|
return error instanceof Error ? error.message : String(error);
|
|
3988
1558
|
}
|