tinker-agent 2.5.0 → 2.7.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 +42 -1
- package/README.md +30 -7
- package/package.json +1 -1
- package/src/agent/loop.ts +22 -0
- package/src/agent/runtime-session.ts +158 -0
- package/src/cli/main.ts +2 -0
- package/src/cli/public-config-contract.ts +1 -1
- package/src/cli/tui-runner.tsx +16 -1
- package/src/events/stdout-event-printer.ts +14 -0
- package/src/events/types.ts +9 -0
- package/src/memory/contracts.ts +46 -0
- package/src/memory/memory-coordinator.ts +445 -4
- package/src/memory/memory-create-tool.ts +117 -0
- package/src/memory/memory-delete-tool.ts +88 -0
- package/src/memory/memory-store.ts +239 -0
- package/src/memory/memory-update-tool.ts +142 -0
- package/src/observation/observation-builder.ts +70 -0
- package/src/session/session-clone-helpers.ts +249 -0
- package/src/session/session-compatibility-codec.ts +401 -0
- package/src/session/session-store-contracts.ts +304 -0
- package/src/session/session-store-filesystem.ts +245 -0
- package/src/session/session-store-record-codecs.ts +1064 -0
- package/src/session/session-store-value-codecs.ts +156 -0
- package/src/session/session-store.ts +234 -3023
- package/src/session/session-tool-result-codec.ts +594 -0
- package/src/tools/ask-user.ts +100 -0
- package/src/tools/grep.ts +1 -3
- package/src/tools/registry.ts +30 -0
- package/src/tools/types.ts +71 -0
- package/src/tui/app.tsx +35 -5
- package/src/tui/components/ask-user.tsx +61 -0
- package/src/tui/components/footer.tsx +14 -1
- package/src/tui/components/prompt-input.tsx +4 -0
- package/src/tui/components/resume-session-picker.tsx +118 -37
- package/src/tui/event-store.ts +57 -0
- package/src/tui/tui-session-controller.ts +8 -0
|
@@ -1,39 +1,16 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
chmod,
|
|
4
|
-
lstat,
|
|
5
|
-
mkdir,
|
|
6
|
-
open,
|
|
7
|
-
readFile,
|
|
8
|
-
readdir,
|
|
9
|
-
realpath,
|
|
10
|
-
rename,
|
|
11
|
-
rmdir,
|
|
12
|
-
unlink,
|
|
13
|
-
} from "node:fs/promises";
|
|
2
|
+
import { chmod, mkdir, open, readdir, rename, rmdir } from "node:fs/promises";
|
|
14
3
|
import { randomUUID } from "node:crypto";
|
|
15
4
|
import { Database } from "bun:sqlite";
|
|
16
|
-
import { parseMessageId } from "../ids/runtime-id";
|
|
17
5
|
import type {
|
|
18
6
|
ContextRevisionId,
|
|
19
7
|
ContextSurfaceId,
|
|
20
8
|
IterationId,
|
|
21
9
|
MessageId,
|
|
22
|
-
ProtocolFrameId,
|
|
23
10
|
RuntimeIdFactory,
|
|
24
11
|
SessionId,
|
|
25
|
-
ToolCallId,
|
|
26
12
|
TurnId,
|
|
27
13
|
} from "../ids/runtime-id";
|
|
28
|
-
import {
|
|
29
|
-
createModelContextProfile,
|
|
30
|
-
type ModelContextProfile,
|
|
31
|
-
} from "../model/model-context-profile";
|
|
32
|
-
import {
|
|
33
|
-
MODEL_MESSAGE_PROTOCOL_ADAPTERS,
|
|
34
|
-
type ModelMessageProtocol,
|
|
35
|
-
} from "../model/model-client";
|
|
36
|
-
import type { ToolDefinition, ToolRawResult } from "../tools/types";
|
|
37
14
|
import { sha256, stableJsonStringify } from "../model/model-request-preflight";
|
|
38
15
|
import {
|
|
39
16
|
canonicalHomeRoot,
|
|
@@ -59,9 +36,7 @@ import {
|
|
|
59
36
|
SWAP_TOOL_IMAGE_FORMAT,
|
|
60
37
|
} from "../context/context-swap-renderer";
|
|
61
38
|
import {
|
|
62
|
-
SUPPORTED_TOOL_OBSERVATION_FORMATS,
|
|
63
39
|
contentHash,
|
|
64
|
-
immutableCanonicalClone,
|
|
65
40
|
immutableRecord,
|
|
66
41
|
interruptedCompletionInputs,
|
|
67
42
|
observationForCompletion,
|
|
@@ -76,7 +51,6 @@ import {
|
|
|
76
51
|
contextSurfaceChangeManifestHash,
|
|
77
52
|
contextSurfaceChanges,
|
|
78
53
|
validateStoredContextSurface,
|
|
79
|
-
type ContextSurfaceChanges,
|
|
80
54
|
type StoredContextSurfaceV8,
|
|
81
55
|
} from "../context/context-surface";
|
|
82
56
|
import type {
|
|
@@ -85,25 +59,16 @@ import type {
|
|
|
85
59
|
StoredContextOverrideV8,
|
|
86
60
|
SwapOverride,
|
|
87
61
|
} from "../context/context-revision";
|
|
88
|
-
import type { IterationIdentity
|
|
62
|
+
import type { IterationIdentity } from "../agent/types";
|
|
89
63
|
import {
|
|
90
64
|
canonicalToolResultContentHash,
|
|
91
65
|
toolResultDisplayText,
|
|
92
66
|
validateToolResultContent,
|
|
93
67
|
} from "../agent/tool-result-content";
|
|
94
|
-
import {
|
|
95
|
-
parseImageAssetId,
|
|
96
|
-
parseImageAttachmentId,
|
|
97
|
-
validateImageAssetRef,
|
|
98
|
-
validateOriginalImageName,
|
|
99
|
-
validateUserMessage,
|
|
100
|
-
type ImageAssetRef,
|
|
101
|
-
type UserImageAttachment,
|
|
102
|
-
} from "../image/image-types";
|
|
68
|
+
import { validateUserMessage, type ImageAssetRef } from "../image/image-types";
|
|
103
69
|
import { ImageAssetStore } from "../image/image-asset-store";
|
|
104
70
|
import type { MeasuredContextAnchor } from "../agent/context-meter";
|
|
105
71
|
import type { ProjectInstructionManifest } from "../instructions/project-instructions";
|
|
106
|
-
import { SUPPORTED_RECALL_RETIREMENT_CONTRACT_VERSIONS } from "../context/recall-retirement-contract";
|
|
107
72
|
import type {
|
|
108
73
|
ActiveTurnBoundary,
|
|
109
74
|
ClosedTurnBoundary,
|
|
@@ -135,305 +100,87 @@ import {
|
|
|
135
100
|
verifySessionSchema,
|
|
136
101
|
verifySqliteIntegrity,
|
|
137
102
|
} from "./session-schema";
|
|
138
|
-
import type { AgentEvent } from "../events/types";
|
|
139
|
-
import { renderObservationLogEvent } from "../events/observation-text-log";
|
|
140
|
-
import {
|
|
141
|
-
SKILL_FILE_MAX_BYTES,
|
|
142
|
-
SKILL_RESOURCE_MAX_DEPTH,
|
|
143
|
-
SKILL_RESOURCE_MAX_ENTRIES,
|
|
144
|
-
type SkillScope,
|
|
145
|
-
} from "../skills/skill-loader";
|
|
146
|
-
import type {
|
|
147
|
-
ActiveSkillManifestEntry,
|
|
148
|
-
SkillCatalogManifestEntry,
|
|
149
|
-
} from "../skills/skill-catalog";
|
|
150
103
|
import {
|
|
151
104
|
SKILL_ACTIVATION_RECEIPT_FORMAT,
|
|
152
105
|
SKILL_POLICY_VERSION,
|
|
153
106
|
renderSkillActivationReceipt,
|
|
154
107
|
} from "../skills/skill-context";
|
|
155
108
|
import {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
recoveredFrameId?: ProtocolFrameId;
|
|
232
|
-
syntheticCompletionCount: number;
|
|
233
|
-
recallIndexRebuilt: boolean;
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
type StoredMeasuredContextState = {
|
|
237
|
-
revisionId: ContextRevisionId;
|
|
238
|
-
anchor: MeasuredContextAnchor;
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
export type CommitSwapRevisionInput = {
|
|
242
|
-
revisionId: ContextRevisionId;
|
|
243
|
-
expectedBaseRevisionId: ContextRevisionId;
|
|
244
|
-
expectedBaseRevisionNumber: number;
|
|
245
|
-
expectedCanonicalThroughOrdinal: number;
|
|
246
|
-
expectedBaseActiveOverrideManifestSha256: string;
|
|
247
|
-
policyVersion: "swap-only-v1";
|
|
248
|
-
rendererFormat: typeof SWAP_OBSERVATION_FORMAT;
|
|
249
|
-
planHash: string;
|
|
250
|
-
addedOverrides: readonly SwapOverride[];
|
|
251
|
-
nextActiveOverrideManifestSha256: string;
|
|
252
|
-
canonicalSequenceSha256: string;
|
|
253
|
-
renderedMessageSha256: string;
|
|
254
|
-
activeTurnId?: TurnId;
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
export type CommitSwapRevisionFaultStage =
|
|
258
|
-
| "before_revision_insert"
|
|
259
|
-
| "after_revision_insert"
|
|
260
|
-
| "after_first_override_insert"
|
|
261
|
-
| "after_overrides_insert"
|
|
262
|
-
| "after_measurement_delete"
|
|
263
|
-
| "after_active_update";
|
|
264
|
-
|
|
265
|
-
export type CommitSwapRevisionOptions = {
|
|
266
|
-
faultInjector?: (stage: CommitSwapRevisionFaultStage) => void;
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
export type CommitSurfaceRefreshInput = {
|
|
270
|
-
revisionId: ContextRevisionId;
|
|
271
|
-
expectedBaseRevisionId: ContextRevisionId;
|
|
272
|
-
expectedBaseRevisionNumber: number;
|
|
273
|
-
expectedCanonicalThroughOrdinal: number;
|
|
274
|
-
expectedBaseActiveOverrideManifestSha256: string;
|
|
275
|
-
surface: StoredContextSurfaceV8;
|
|
276
|
-
changes: ContextSurfaceChanges;
|
|
277
|
-
changeManifestSha256: string;
|
|
278
|
-
canonicalSequenceSha256: string;
|
|
279
|
-
renderedMessageSha256: string;
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
export type CommitSurfaceRefreshFaultStage =
|
|
283
|
-
| "before_surface_insert"
|
|
284
|
-
| "after_surface_insert"
|
|
285
|
-
| "after_revision_insert"
|
|
286
|
-
| "after_measurement_delete"
|
|
287
|
-
| "after_active_update";
|
|
288
|
-
|
|
289
|
-
export type CommitSurfaceRefreshOptions = {
|
|
290
|
-
faultInjector?: (stage: CommitSurfaceRefreshFaultStage) => void;
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
export type CommitPrefixRetirementRevisionInput = {
|
|
294
|
-
revisionId: ContextRevisionId;
|
|
295
|
-
expectedBaseRevisionId: ContextRevisionId;
|
|
296
|
-
expectedBaseRevisionNumber: number;
|
|
297
|
-
expectedBaseKeepFromOrdinal: number;
|
|
298
|
-
expectedCanonicalThroughOrdinal: number;
|
|
299
|
-
expectedSurfaceSha256: string;
|
|
300
|
-
expectedBaseActiveOverrideManifestSha256: string;
|
|
301
|
-
policyVersion: "recall-first-retirement-v1";
|
|
302
|
-
planHash: string;
|
|
303
|
-
nextKeepFromOrdinal: number;
|
|
304
|
-
retiredThroughOrdinal: number;
|
|
305
|
-
retiredTurnCount: number;
|
|
306
|
-
retiredFrameCount: number;
|
|
307
|
-
retiredMessageCount: number;
|
|
308
|
-
nextActiveOverrideCount: number;
|
|
309
|
-
nextActiveOverrideManifestSha256: string;
|
|
310
|
-
canonicalSequenceSha256: string;
|
|
311
|
-
renderedMessageSha256: string;
|
|
312
|
-
activeTurnId?: TurnId;
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
export type CommitPrefixRetirementRevisionFaultStage =
|
|
316
|
-
| "before_revision_insert"
|
|
317
|
-
| "after_revision_insert"
|
|
318
|
-
| "after_override_readback"
|
|
319
|
-
| "after_measurement_delete"
|
|
320
|
-
| "after_active_update"
|
|
321
|
-
| "after_snapshot_readback";
|
|
322
|
-
|
|
323
|
-
export type CommitPrefixRetirementRevisionOptions = {
|
|
324
|
-
faultInjector?: (stage: CommitPrefixRetirementRevisionFaultStage) => void;
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
export type StoredSkillActivation = {
|
|
328
|
-
readonly activationMessageId: MessageId;
|
|
329
|
-
readonly toolCallId: ToolCallId;
|
|
330
|
-
readonly sessionId: SessionId;
|
|
331
|
-
readonly name: string;
|
|
332
|
-
readonly scope: SkillScope;
|
|
333
|
-
readonly skillFileSha256: string;
|
|
334
|
-
readonly state: "pending" | "dispatched" | "promoted" | "rejected";
|
|
335
|
-
readonly dispatchedIterationId?: IterationId;
|
|
336
|
-
readonly settledRevisionId?: ContextRevisionId;
|
|
337
|
-
readonly rejectionReason?: string;
|
|
338
|
-
readonly createdAt: string;
|
|
339
|
-
readonly updatedAt: string;
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
export type CommitSkillsUpdateInput = {
|
|
343
|
-
revisionId: ContextRevisionId;
|
|
344
|
-
expectedBaseRevisionId: ContextRevisionId;
|
|
345
|
-
expectedBaseRevisionNumber: number;
|
|
346
|
-
expectedCanonicalThroughOrdinal: number;
|
|
347
|
-
expectedBaseActiveOverrideManifestSha256: string;
|
|
348
|
-
surface: StoredContextSurfaceV8;
|
|
349
|
-
changes: ContextSurfaceChanges;
|
|
350
|
-
changeManifestSha256: string;
|
|
351
|
-
activationManifestSha256: string;
|
|
352
|
-
addedOverrides: readonly SwapOverride[];
|
|
353
|
-
nextActiveOverrideManifestSha256: string;
|
|
354
|
-
settlements: readonly {
|
|
355
|
-
activationMessageId: MessageId;
|
|
356
|
-
name: string;
|
|
357
|
-
state: "promoted" | "rejected";
|
|
358
|
-
rejectionReason?: string;
|
|
359
|
-
}[];
|
|
360
|
-
canonicalSequenceSha256: string;
|
|
361
|
-
renderedMessageSha256: string;
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
export type CommitSkillsUpdateFaultStage =
|
|
365
|
-
| "before_surface_insert"
|
|
366
|
-
| "after_surface_insert"
|
|
367
|
-
| "after_revision_insert"
|
|
368
|
-
| "after_first_override_insert"
|
|
369
|
-
| "after_overrides_insert"
|
|
370
|
-
| "after_activations_update"
|
|
371
|
-
| "after_measurement_delete"
|
|
372
|
-
| "after_active_update";
|
|
373
|
-
|
|
374
|
-
export type CommitSkillsUpdateOptions = {
|
|
375
|
-
faultInjector?: (stage: CommitSkillsUpdateFaultStage) => void;
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
export type CloneSessionFaultStage =
|
|
379
|
-
| "after_staging_mkdir"
|
|
380
|
-
| "after_snapshot"
|
|
381
|
-
| "after_trigger_drop"
|
|
382
|
-
| "after_identity_update"
|
|
383
|
-
| "after_revision_hash_rewrite"
|
|
384
|
-
| "after_trigger_reinstall"
|
|
385
|
-
| "after_recall_validation"
|
|
386
|
-
| "after_event_rewrite"
|
|
387
|
-
| "after_observation_render"
|
|
388
|
-
| "after_artifact_validation"
|
|
389
|
-
| "before_publish_rename";
|
|
390
|
-
|
|
391
|
-
export type CloneSessionStoreInput = {
|
|
392
|
-
targetSessionId: SessionId;
|
|
393
|
-
faultInjector?: (stage: CloneSessionFaultStage) => void;
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
export function skillActivationManifestSha256(
|
|
397
|
-
settlements: CommitSkillsUpdateInput["settlements"],
|
|
398
|
-
): string {
|
|
399
|
-
return sha256(
|
|
400
|
-
stableJsonStringify(
|
|
401
|
-
[...settlements]
|
|
402
|
-
.sort(
|
|
403
|
-
(left, right) =>
|
|
404
|
-
compareCanonicalText(left.name, right.name) ||
|
|
405
|
-
compareCanonicalText(left.activationMessageId, right.activationMessageId),
|
|
406
|
-
)
|
|
407
|
-
.map((settlement) => ({
|
|
408
|
-
activationMessageId: settlement.activationMessageId,
|
|
409
|
-
name: settlement.name,
|
|
410
|
-
state: settlement.state,
|
|
411
|
-
...(settlement.rejectionReason === undefined
|
|
412
|
-
? {}
|
|
413
|
-
: { rejectionReason: settlement.rejectionReason }),
|
|
414
|
-
})),
|
|
415
|
-
),
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
export type CreateNewSessionStoreInput = {
|
|
420
|
-
workspaceRoot: string;
|
|
421
|
-
sessionId: SessionId;
|
|
422
|
-
modelName: string;
|
|
423
|
-
systemPrompt: string;
|
|
424
|
-
projectInstruction?: ProjectInstructionManifest;
|
|
425
|
-
idFactory: RuntimeIdFactory;
|
|
426
|
-
clock?: () => string;
|
|
427
|
-
homeRoot?: string;
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
export type OpenSessionStoreInput = {
|
|
431
|
-
workspaceRoot: string;
|
|
432
|
-
sessionId: SessionId;
|
|
433
|
-
clock?: () => string;
|
|
434
|
-
allowIncomplete?: boolean;
|
|
435
|
-
homeRoot?: string;
|
|
436
|
-
};
|
|
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";
|
|
140
|
+
import {
|
|
141
|
+
skillActivationManifestSha256,
|
|
142
|
+
type CloneSessionStoreInput,
|
|
143
|
+
type CommitPrefixRetirementRevisionInput,
|
|
144
|
+
type CommitPrefixRetirementRevisionOptions,
|
|
145
|
+
type CommitSkillsUpdateInput,
|
|
146
|
+
type CommitSkillsUpdateOptions,
|
|
147
|
+
type CommitSurfaceRefreshInput,
|
|
148
|
+
type CommitSurfaceRefreshOptions,
|
|
149
|
+
type CommitSwapRevisionInput,
|
|
150
|
+
type CommitSwapRevisionOptions,
|
|
151
|
+
type CompletedTurnMessageSnapshot,
|
|
152
|
+
type CompletedTurnSnapshot,
|
|
153
|
+
type CreateNewSessionStoreInput,
|
|
154
|
+
type OpenSessionStoreInput,
|
|
155
|
+
type SessionCloseReason,
|
|
156
|
+
type SessionCompatibilityContract,
|
|
157
|
+
type SessionRecoveryResult,
|
|
158
|
+
type StoredMeasuredContextState,
|
|
159
|
+
type StoredSessionMetaV10,
|
|
160
|
+
type StoredSkillActivation,
|
|
161
|
+
} from "./session-store-contracts";
|
|
162
|
+
export * from "./session-store-contracts";
|
|
163
|
+
export { createSessionCompatibilityContract } from "./session-compatibility-codec";
|
|
164
|
+
import {
|
|
165
|
+
compatibilityContractDifferences,
|
|
166
|
+
decodeMeta,
|
|
167
|
+
normalizeSessionCompatibilityContract,
|
|
168
|
+
} from "./session-compatibility-codec";
|
|
169
|
+
export { decodeStoredToolCalls } from "./session-store-record-codecs";
|
|
170
|
+
import {
|
|
171
|
+
decodeContextRevision,
|
|
172
|
+
decodeContextSurface,
|
|
173
|
+
decodeFrame,
|
|
174
|
+
decodeMessage,
|
|
175
|
+
decodeSkillActivation,
|
|
176
|
+
decodeStoredSwapOverride,
|
|
177
|
+
imageAssetRefFromAttachment,
|
|
178
|
+
decodeToolResult,
|
|
179
|
+
loadMessageImageAttachments,
|
|
180
|
+
loadToolMessageContentBlocks,
|
|
181
|
+
protocolPrefixView,
|
|
182
|
+
stripStoredOverride,
|
|
183
|
+
} from "./session-store-record-codecs";
|
|
437
184
|
|
|
438
185
|
export class SessionStore implements SessionLedgerCommitter {
|
|
439
186
|
readonly sessionId: SessionId;
|
|
@@ -3742,87 +3489,6 @@ function insertPendingSkillActivation(
|
|
|
3742
3489
|
);
|
|
3743
3490
|
}
|
|
3744
3491
|
|
|
3745
|
-
export function createSessionCompatibilityContract(input: {
|
|
3746
|
-
modelName: string;
|
|
3747
|
-
profileName?: string;
|
|
3748
|
-
includeReasoningContent: boolean;
|
|
3749
|
-
contextProfile: ModelContextProfile;
|
|
3750
|
-
messageProtocol: ModelMessageProtocol;
|
|
3751
|
-
inputModalities: readonly ("text" | "image")[];
|
|
3752
|
-
toolResultModalities: readonly ("text" | "image")[];
|
|
3753
|
-
}): SessionCompatibilityContract {
|
|
3754
|
-
if (input.modelName.trim() === "") {
|
|
3755
|
-
throw new Error("Session compatibility model name must not be empty.");
|
|
3756
|
-
}
|
|
3757
|
-
if (input.profileName !== undefined && input.profileName.trim() === "") {
|
|
3758
|
-
throw new Error("Session compatibility profile name must not be empty.");
|
|
3759
|
-
}
|
|
3760
|
-
if (typeof input.includeReasoningContent !== "boolean") {
|
|
3761
|
-
throw new Error("Session compatibility reasoning replay flag must be boolean.");
|
|
3762
|
-
}
|
|
3763
|
-
if (
|
|
3764
|
-
!MODEL_MESSAGE_PROTOCOL_ADAPTERS.includes(input.messageProtocol.adapter) ||
|
|
3765
|
-
input.messageProtocol.serializationVersion.trim() === ""
|
|
3766
|
-
) {
|
|
3767
|
-
throw new Error("Session compatibility message protocol is invalid.");
|
|
3768
|
-
}
|
|
3769
|
-
const inputModalities = normalizeInputModalities(input.inputModalities);
|
|
3770
|
-
const toolResultModalities = normalizeInputModalities(input.toolResultModalities);
|
|
3771
|
-
if (toolResultModalities.includes("image") && !inputModalities.includes("image")) {
|
|
3772
|
-
throw new Error("Session compatibility image tool results require image input.");
|
|
3773
|
-
}
|
|
3774
|
-
return Object.freeze({
|
|
3775
|
-
modelName: input.modelName,
|
|
3776
|
-
...(input.profileName === undefined ? {} : { profileName: input.profileName }),
|
|
3777
|
-
includeReasoningContent: input.includeReasoningContent,
|
|
3778
|
-
contextProfile: Object.freeze(createModelContextProfile(input.contextProfile)),
|
|
3779
|
-
messageProtocol: immutableCanonicalClone(input.messageProtocol),
|
|
3780
|
-
media: Object.freeze({
|
|
3781
|
-
policyVersion: IMAGE_INPUT_POLICY_VERSION,
|
|
3782
|
-
policySha256: sha256(
|
|
3783
|
-
stableJsonStringify({
|
|
3784
|
-
version: IMAGE_INPUT_POLICY_VERSION,
|
|
3785
|
-
...IMAGE_INPUT_POLICY,
|
|
3786
|
-
}),
|
|
3787
|
-
),
|
|
3788
|
-
inputModalities,
|
|
3789
|
-
toolResultModalities,
|
|
3790
|
-
}),
|
|
3791
|
-
});
|
|
3792
|
-
}
|
|
3793
|
-
|
|
3794
|
-
function normalizeSessionCompatibilityContract(
|
|
3795
|
-
contract: SessionCompatibilityContract,
|
|
3796
|
-
): SessionCompatibilityContract {
|
|
3797
|
-
return createSessionCompatibilityContract({
|
|
3798
|
-
modelName: contract.modelName,
|
|
3799
|
-
...(contract.profileName === undefined
|
|
3800
|
-
? {}
|
|
3801
|
-
: { profileName: contract.profileName }),
|
|
3802
|
-
includeReasoningContent: contract.includeReasoningContent,
|
|
3803
|
-
contextProfile: contract.contextProfile,
|
|
3804
|
-
messageProtocol: contract.messageProtocol,
|
|
3805
|
-
inputModalities: contract.media.inputModalities,
|
|
3806
|
-
toolResultModalities: contract.media.toolResultModalities,
|
|
3807
|
-
});
|
|
3808
|
-
}
|
|
3809
|
-
|
|
3810
|
-
function normalizeInputModalities(
|
|
3811
|
-
modalities: readonly ("text" | "image")[],
|
|
3812
|
-
): readonly ("text" | "image")[] {
|
|
3813
|
-
if (
|
|
3814
|
-
modalities.length === 0 ||
|
|
3815
|
-
modalities.some((value) => value !== "text" && value !== "image") ||
|
|
3816
|
-
new Set(modalities).size !== modalities.length ||
|
|
3817
|
-
!modalities.includes("text")
|
|
3818
|
-
) {
|
|
3819
|
-
throw new Error("Session compatibility input modalities are invalid.");
|
|
3820
|
-
}
|
|
3821
|
-
return Object.freeze(
|
|
3822
|
-
modalities.includes("image") ? (["text", "image"] as const) : (["text"] as const),
|
|
3823
|
-
);
|
|
3824
|
-
}
|
|
3825
|
-
|
|
3826
3492
|
export async function resolveSessionDatabasePath(
|
|
3827
3493
|
workspaceRoot: string,
|
|
3828
3494
|
sessionId: SessionId,
|
|
@@ -4089,2019 +3755,182 @@ function insertContextSurface(
|
|
|
4089
3755
|
);
|
|
4090
3756
|
}
|
|
4091
3757
|
|
|
4092
|
-
function
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
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
|
+
}),
|
|
4119
3789
|
});
|
|
4120
3790
|
}
|
|
4121
3791
|
|
|
4122
|
-
function
|
|
4123
|
-
|
|
4124
|
-
)
|
|
4125
|
-
|
|
4126
|
-
.
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
.
|
|
4135
|
-
|
|
4136
|
-
for (const
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
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.`);
|
|
4145
3818
|
}
|
|
4146
|
-
timestampFromSql(row.asset_created_at, "image asset created_at");
|
|
4147
|
-
const attachment = immutableRecord<UserImageAttachment>({
|
|
4148
|
-
attachmentId: parseImageAttachmentId(
|
|
4149
|
-
stringFromSql(row.attachment_id, "attachment_id"),
|
|
4150
|
-
),
|
|
4151
|
-
assetId: parseImageAssetId(stringFromSql(row.asset_id, "asset_id")),
|
|
4152
|
-
mimeType: enumFromSql(
|
|
4153
|
-
row.mime_type,
|
|
4154
|
-
["image/png", "image/jpeg", "image/webp"] as const,
|
|
4155
|
-
"image mime_type",
|
|
4156
|
-
),
|
|
4157
|
-
byteLength: numberFromSql(row.byte_length, "image byte_length"),
|
|
4158
|
-
width: numberFromSql(row.width, "image width"),
|
|
4159
|
-
height: numberFromSql(row.height, "image height"),
|
|
4160
|
-
label: stringFromSql(row.label, "attachment label"),
|
|
4161
|
-
range: immutableRecord({
|
|
4162
|
-
start: numberFromSql(row.range_start, "attachment range_start"),
|
|
4163
|
-
end: numberFromSql(row.range_end, "attachment range_end"),
|
|
4164
|
-
}),
|
|
4165
|
-
originalName: stringFromSql(row.original_name, "attachment original_name"),
|
|
4166
|
-
});
|
|
4167
|
-
validateOriginalImageName(attachment.originalName);
|
|
4168
|
-
attachments.push(attachment);
|
|
4169
|
-
mutable.set(messageId, attachments);
|
|
4170
3819
|
}
|
|
4171
|
-
return new Map(
|
|
4172
|
-
[...mutable].map(([messageId, attachments]) => [
|
|
4173
|
-
messageId,
|
|
4174
|
-
Object.freeze(attachments),
|
|
4175
|
-
]),
|
|
4176
|
-
);
|
|
4177
3820
|
}
|
|
4178
3821
|
|
|
4179
|
-
function
|
|
4180
|
-
|
|
4181
|
-
)
|
|
4182
|
-
|
|
4183
|
-
.
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
);
|
|
4202
|
-
}
|
|
4203
|
-
const kind = enumFromSql(row.kind, ["text", "image"] as const, "tool block kind");
|
|
4204
|
-
const block: ToolResultContent =
|
|
4205
|
-
kind === "text"
|
|
4206
|
-
? immutableRecord({
|
|
4207
|
-
type: "text",
|
|
4208
|
-
text: stringFromSql(row.text_content, "tool block text_content"),
|
|
4209
|
-
})
|
|
4210
|
-
: immutableRecord({
|
|
4211
|
-
type: "image",
|
|
4212
|
-
asset: immutableRecord({
|
|
4213
|
-
assetId: parseImageAssetId(
|
|
4214
|
-
stringFromSql(row.asset_id, "tool block asset_id"),
|
|
4215
|
-
),
|
|
4216
|
-
mimeType: enumFromSql(
|
|
4217
|
-
row.mime_type,
|
|
4218
|
-
["image/png", "image/jpeg", "image/webp"] as const,
|
|
4219
|
-
"tool block image mime_type",
|
|
4220
|
-
),
|
|
4221
|
-
byteLength: numberFromSql(
|
|
4222
|
-
row.byte_length,
|
|
4223
|
-
"tool block image byte_length",
|
|
4224
|
-
),
|
|
4225
|
-
width: numberFromSql(row.width, "tool block image width"),
|
|
4226
|
-
height: numberFromSql(row.height, "tool block image height"),
|
|
4227
|
-
}),
|
|
4228
|
-
});
|
|
4229
|
-
if (kind === "image") {
|
|
4230
|
-
timestampFromSql(row.asset_created_at, "tool block image asset created_at");
|
|
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.`);
|
|
4231
3844
|
}
|
|
4232
|
-
blocks.push(block);
|
|
4233
|
-
mutable.set(messageId, blocks);
|
|
4234
3845
|
}
|
|
4235
|
-
return new Map(
|
|
4236
|
-
[...mutable].map(([messageId, blocks]) => {
|
|
4237
|
-
validateToolResultContent(blocks);
|
|
4238
|
-
return [messageId, Object.freeze(blocks)] as const;
|
|
4239
|
-
}),
|
|
4240
|
-
);
|
|
4241
3846
|
}
|
|
4242
3847
|
|
|
4243
|
-
function
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
)
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
role,
|
|
4270
|
-
content: stringFromSql(row.content, "content"),
|
|
4271
|
-
origin: "runtime",
|
|
4272
|
-
});
|
|
4273
|
-
case "user": {
|
|
4274
|
-
if (toolContentBlocks !== undefined) {
|
|
4275
|
-
throw new Error("User messages cannot have tool content blocks.");
|
|
4276
|
-
}
|
|
4277
|
-
const message = {
|
|
4278
|
-
...base,
|
|
4279
|
-
role,
|
|
4280
|
-
turnId: stringFromSql(row.turn_id, "turn_id") as TurnId,
|
|
4281
|
-
content: stringFromSql(row.content, "content"),
|
|
4282
|
-
...(attachments === undefined ? {} : { attachments }),
|
|
4283
|
-
origin: "user",
|
|
4284
|
-
} as const;
|
|
4285
|
-
validateUserMessage({
|
|
4286
|
-
role: "user",
|
|
4287
|
-
content: message.content,
|
|
4288
|
-
...(message.attachments === undefined
|
|
4289
|
-
? {}
|
|
4290
|
-
: { attachments: message.attachments }),
|
|
4291
|
-
});
|
|
4292
|
-
return immutableRecord(message);
|
|
4293
|
-
}
|
|
4294
|
-
case "assistant": {
|
|
4295
|
-
if (attachments !== undefined || toolContentBlocks !== undefined) {
|
|
4296
|
-
throw new Error("Assistant messages cannot have media relation rows.");
|
|
4297
|
-
}
|
|
4298
|
-
const content = nullableTextFromSql(row.content, "content");
|
|
4299
|
-
const reasoningPresent = numberFromSql(
|
|
4300
|
-
row.reasoning_content_present,
|
|
4301
|
-
"reasoning_content_present",
|
|
4302
|
-
);
|
|
4303
|
-
if (reasoningPresent !== 0 && reasoningPresent !== 1) {
|
|
4304
|
-
throw new Error("reasoning_content_present must be 0 or 1.");
|
|
4305
|
-
}
|
|
4306
|
-
const toolCalls =
|
|
4307
|
-
row.tool_calls_json === null
|
|
4308
|
-
? undefined
|
|
4309
|
-
: decodeStoredToolCalls(
|
|
4310
|
-
stringFromSql(row.tool_calls_json, "tool_calls_json"),
|
|
4311
|
-
);
|
|
4312
|
-
return immutableRecord({
|
|
4313
|
-
...base,
|
|
4314
|
-
role,
|
|
4315
|
-
turnId: stringFromSql(row.turn_id, "turn_id") as TurnId,
|
|
4316
|
-
iterationId: stringFromSql(row.iteration_id, "iteration_id") as IterationId,
|
|
4317
|
-
content,
|
|
4318
|
-
...(reasoningPresent === 0
|
|
4319
|
-
? {}
|
|
4320
|
-
: {
|
|
4321
|
-
reasoningContent: nullableTextFromSql(
|
|
4322
|
-
row.reasoning_content,
|
|
4323
|
-
"reasoning_content",
|
|
4324
|
-
),
|
|
4325
|
-
}),
|
|
4326
|
-
...(toolCalls === undefined ? {} : { toolCalls }),
|
|
4327
|
-
provider: stringFromSql(row.provider, "provider"),
|
|
4328
|
-
model: stringFromSql(row.model, "model"),
|
|
4329
|
-
origin: "model",
|
|
4330
|
-
});
|
|
4331
|
-
}
|
|
4332
|
-
case "tool": {
|
|
4333
|
-
if (attachments !== undefined) {
|
|
4334
|
-
throw new Error("Tool messages cannot have image attachments.");
|
|
4335
|
-
}
|
|
4336
|
-
if (toolContentBlocks === undefined) {
|
|
4337
|
-
throw new Error("Tool messages must have persisted content blocks.");
|
|
4338
|
-
}
|
|
4339
|
-
validateToolResultContent(toolContentBlocks);
|
|
4340
|
-
const displayText = stringFromSql(row.content, "content");
|
|
4341
|
-
if (toolResultDisplayText(toolContentBlocks) !== displayText) {
|
|
4342
|
-
throw new Error("Tool message display projection does not match its blocks.");
|
|
4343
|
-
}
|
|
4344
|
-
return immutableRecord({
|
|
4345
|
-
...base,
|
|
4346
|
-
role,
|
|
4347
|
-
turnId: stringFromSql(row.turn_id, "turn_id") as TurnId,
|
|
4348
|
-
iterationId: stringFromSql(row.iteration_id, "iteration_id") as IterationId,
|
|
4349
|
-
toolCallId: stringFromSql(row.tool_call_id, "tool_call_id") as ToolCallId,
|
|
4350
|
-
providerToolCallId: stringFromSql(
|
|
4351
|
-
row.provider_tool_call_id,
|
|
4352
|
-
"provider_tool_call_id",
|
|
4353
|
-
),
|
|
4354
|
-
name: stringFromSql(row.name, "name"),
|
|
4355
|
-
content: toolContentBlocks,
|
|
4356
|
-
displayText,
|
|
4357
|
-
origin: enumFromSql(row.origin, ["tool", "runtime"] as const, "tool origin"),
|
|
4358
|
-
});
|
|
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.`);
|
|
4359
3874
|
}
|
|
4360
3875
|
}
|
|
4361
3876
|
}
|
|
4362
3877
|
|
|
4363
|
-
function
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
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
|
+
}
|
|
4371
3920
|
}
|
|
4372
3921
|
|
|
4373
|
-
function
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
row.completion_kind,
|
|
4377
|
-
["returned", "synthetic"] as const,
|
|
4378
|
-
"completion kind",
|
|
4379
|
-
);
|
|
4380
|
-
let completion: ToolCompletion;
|
|
4381
|
-
if (kind === "returned") {
|
|
4382
|
-
completion = immutableRecord({
|
|
4383
|
-
kind,
|
|
4384
|
-
raw: decodeStoredToolRawResult(
|
|
4385
|
-
parseJson(stringFromSql(row.raw_json, "raw_json"), "raw_json"),
|
|
4386
|
-
),
|
|
4387
|
-
rawSha256: stringFromSql(row.raw_sha256, "raw_sha256"),
|
|
4388
|
-
observationFormat: enumFromSql(
|
|
4389
|
-
row.observation_format,
|
|
4390
|
-
SUPPORTED_TOOL_OBSERVATION_FORMATS,
|
|
4391
|
-
"observation format",
|
|
4392
|
-
),
|
|
4393
|
-
});
|
|
4394
|
-
} else {
|
|
4395
|
-
const reason = enumFromSql(
|
|
4396
|
-
row.synthetic_reason,
|
|
4397
|
-
[
|
|
4398
|
-
"cancelled_active",
|
|
4399
|
-
"skipped_after_cancel",
|
|
4400
|
-
"failed_active",
|
|
4401
|
-
"skipped_after_failure",
|
|
4402
|
-
"interrupted_active",
|
|
4403
|
-
"skipped_after_interruption",
|
|
4404
|
-
] as const,
|
|
4405
|
-
"synthetic reason",
|
|
4406
|
-
);
|
|
4407
|
-
completion = immutableRecord({
|
|
4408
|
-
kind,
|
|
4409
|
-
reason,
|
|
4410
|
-
...(row.synthetic_detail === null
|
|
4411
|
-
? {}
|
|
4412
|
-
: {
|
|
4413
|
-
detail: stringFromSql(row.synthetic_detail, "synthetic_detail"),
|
|
4414
|
-
}),
|
|
4415
|
-
});
|
|
3922
|
+
function requireActiveRevisionId(meta: StoredSessionMetaV10): ContextRevisionId {
|
|
3923
|
+
if (meta.initializationState !== "ready" || meta.activeRevisionId === null) {
|
|
3924
|
+
throw new Error("Session has no active context revision.");
|
|
4416
3925
|
}
|
|
4417
|
-
return
|
|
4418
|
-
sessionId: stringFromSql(row.session_id, "session_id") as SessionId,
|
|
4419
|
-
frameId: stringFromSql(row.frame_id, "frame_id") as ProtocolFrameId,
|
|
4420
|
-
toolCallId: stringFromSql(row.tool_call_id, "tool_call_id") as ToolCallId,
|
|
4421
|
-
toolMessageId: stringFromSql(row.tool_message_id, "tool_message_id") as MessageId,
|
|
4422
|
-
completion,
|
|
4423
|
-
observationSha256: stringFromSql(row.observation_sha256, "observation_sha256"),
|
|
4424
|
-
createdAt: timestampFromSql(row.created_at, "created_at"),
|
|
4425
|
-
});
|
|
3926
|
+
return meta.activeRevisionId;
|
|
4426
3927
|
}
|
|
4427
3928
|
|
|
4428
|
-
function
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
: decodeProjectInstructionManifest(
|
|
4434
|
-
parseJson(
|
|
4435
|
-
stringFromSql(row.project_instruction_json, "project_instruction_json"),
|
|
4436
|
-
"project_instruction_json",
|
|
4437
|
-
),
|
|
4438
|
-
);
|
|
4439
|
-
const toolDefinitions = decodeToolDefinitions(
|
|
4440
|
-
parseJson(
|
|
4441
|
-
stringFromSql(row.tool_definitions_json, "tool_definitions_json"),
|
|
4442
|
-
"tool_definitions_json",
|
|
4443
|
-
),
|
|
4444
|
-
);
|
|
4445
|
-
const skillCatalog = decodeSkillCatalogManifest(
|
|
4446
|
-
parseJson(
|
|
4447
|
-
stringFromSql(row.skill_catalog_json, "skill_catalog_json"),
|
|
4448
|
-
"skill_catalog_json",
|
|
4449
|
-
),
|
|
4450
|
-
);
|
|
4451
|
-
const activeSkills = decodeActiveSkillsManifest(
|
|
4452
|
-
parseJson(
|
|
4453
|
-
stringFromSql(row.active_skills_json, "active_skills_json"),
|
|
4454
|
-
"active_skills_json",
|
|
4455
|
-
),
|
|
4456
|
-
);
|
|
4457
|
-
const surface = Object.freeze({
|
|
4458
|
-
surfaceId: stringFromSql(row.surface_id, "surface_id") as ContextSurfaceId,
|
|
4459
|
-
sessionId: stringFromSql(row.session_id, "session_id") as SessionId,
|
|
4460
|
-
systemPrompt: stringFromSql(row.system_prompt, "system_prompt"),
|
|
4461
|
-
systemPromptSha256: sha256FromSql(row.system_prompt_sha256, "system_prompt_sha256"),
|
|
4462
|
-
recallContractVersion: enumFromSql(
|
|
4463
|
-
row.recall_contract_version,
|
|
4464
|
-
SUPPORTED_RECALL_RETIREMENT_CONTRACT_VERSIONS,
|
|
4465
|
-
"recall_contract_version",
|
|
4466
|
-
),
|
|
4467
|
-
...(projectInstruction === undefined ? {} : { projectInstruction }),
|
|
4468
|
-
skillCatalog,
|
|
4469
|
-
skillCatalogSha256: sha256FromSql(row.skill_catalog_sha256, "skill_catalog_sha256"),
|
|
4470
|
-
activeSkills,
|
|
4471
|
-
activeSkillsSha256: sha256FromSql(row.active_skills_sha256, "active_skills_sha256"),
|
|
4472
|
-
toolDefinitions,
|
|
4473
|
-
toolDefinitionsSha256: sha256FromSql(
|
|
4474
|
-
row.tool_definitions_sha256,
|
|
4475
|
-
"tool_definitions_sha256",
|
|
4476
|
-
),
|
|
4477
|
-
toolSchemaSha256: sha256FromSql(row.tool_schema_sha256, "tool_schema_sha256"),
|
|
4478
|
-
requestConfigSha256: sha256FromSql(
|
|
4479
|
-
row.request_config_sha256,
|
|
4480
|
-
"request_config_sha256",
|
|
4481
|
-
),
|
|
4482
|
-
requestMaxOutputTokens: numberFromSql(
|
|
4483
|
-
row.request_max_output_tokens,
|
|
4484
|
-
"request_max_output_tokens",
|
|
4485
|
-
),
|
|
4486
|
-
surfaceSha256: sha256FromSql(row.surface_sha256, "surface_sha256"),
|
|
4487
|
-
createdAt: timestampFromSql(row.created_at, "created_at"),
|
|
4488
|
-
});
|
|
4489
|
-
validateStoredContextSurface(surface);
|
|
4490
|
-
return surface;
|
|
4491
|
-
}
|
|
4492
|
-
|
|
4493
|
-
function decodeContextRevision(rowValue: unknown): StoredContextRevisionV8 {
|
|
4494
|
-
const row = recordFromSql(rowValue, "context revision");
|
|
4495
|
-
const kind = enumFromSql(
|
|
4496
|
-
row.kind,
|
|
4497
|
-
[
|
|
4498
|
-
"initial_full",
|
|
4499
|
-
"swap_only",
|
|
4500
|
-
"surface_refresh",
|
|
4501
|
-
"prefix_retirement",
|
|
4502
|
-
"skills_update",
|
|
4503
|
-
] as const,
|
|
4504
|
-
"context revision kind",
|
|
4505
|
-
);
|
|
4506
|
-
const common = {
|
|
4507
|
-
revisionId: stringFromSql(row.revision_id, "revision_id") as ContextRevisionId,
|
|
4508
|
-
sessionId: stringFromSql(row.session_id, "session_id") as SessionId,
|
|
4509
|
-
surfaceId: stringFromSql(row.surface_id, "surface_id") as ContextSurfaceId,
|
|
4510
|
-
surfaceSha256: sha256FromSql(row.surface_sha256, "surface_sha256"),
|
|
4511
|
-
keepFromOrdinal: numberFromSql(row.keep_from_ordinal, "keep_from_ordinal"),
|
|
4512
|
-
sourceThroughOrdinal: numberFromSql(
|
|
4513
|
-
row.source_through_ordinal,
|
|
4514
|
-
"source_through_ordinal",
|
|
4515
|
-
),
|
|
4516
|
-
addedOverrideCount: numberFromSql(row.added_override_count, "added_override_count"),
|
|
4517
|
-
activeOverrideCount: numberFromSql(
|
|
4518
|
-
row.active_override_count,
|
|
4519
|
-
"active_override_count",
|
|
4520
|
-
),
|
|
4521
|
-
activeOverrideManifestSha256: sha256FromSql(
|
|
4522
|
-
row.active_override_manifest_sha256,
|
|
4523
|
-
"active_override_manifest_sha256",
|
|
4524
|
-
),
|
|
4525
|
-
canonicalSequenceSha256: sha256FromSql(
|
|
4526
|
-
row.canonical_sequence_sha256,
|
|
4527
|
-
"canonical_sequence_sha256",
|
|
4528
|
-
),
|
|
4529
|
-
renderedMessageSha256: sha256FromSql(
|
|
4530
|
-
row.rendered_message_sha256,
|
|
4531
|
-
"rendered_message_sha256",
|
|
4532
|
-
),
|
|
4533
|
-
createdAt: timestampFromSql(row.created_at, "created_at"),
|
|
4534
|
-
};
|
|
4535
|
-
if (common.keepFromOrdinal < 1) {
|
|
4536
|
-
throw new Error("Context revision keep_from_ordinal must be positive.");
|
|
4537
|
-
}
|
|
4538
|
-
const revisionNumber = numberFromSql(row.revision_number, "revision_number");
|
|
4539
|
-
const parentRevisionId = nullableStringFromSql(
|
|
4540
|
-
row.parent_revision_id,
|
|
4541
|
-
"parent_revision_id",
|
|
4542
|
-
) as ContextRevisionId | null;
|
|
4543
|
-
if (kind === "initial_full") {
|
|
4544
|
-
if (
|
|
4545
|
-
revisionNumber !== 1 ||
|
|
4546
|
-
parentRevisionId !== null ||
|
|
4547
|
-
common.sourceThroughOrdinal !== 1 ||
|
|
4548
|
-
common.addedOverrideCount !== 0 ||
|
|
4549
|
-
common.activeOverrideCount !== 0 ||
|
|
4550
|
-
common.keepFromOrdinal !== 1 ||
|
|
4551
|
-
row.policy_version !== null ||
|
|
4552
|
-
row.renderer_format !== null ||
|
|
4553
|
-
row.plan_sha256 !== null ||
|
|
4554
|
-
row.change_manifest_sha256 !== null ||
|
|
4555
|
-
row.activation_manifest_sha256 !== null ||
|
|
4556
|
-
!hasNoRetirementFields(row)
|
|
4557
|
-
) {
|
|
4558
|
-
throw new Error("Initial context revision row is invalid.");
|
|
4559
|
-
}
|
|
4560
|
-
return Object.freeze({
|
|
4561
|
-
...common,
|
|
4562
|
-
revisionNumber: 1,
|
|
4563
|
-
parentRevisionId: null,
|
|
4564
|
-
kind,
|
|
4565
|
-
keepFromOrdinal: 1,
|
|
4566
|
-
sourceThroughOrdinal: 1,
|
|
4567
|
-
addedOverrideCount: 0,
|
|
4568
|
-
activeOverrideCount: 0,
|
|
4569
|
-
});
|
|
4570
|
-
}
|
|
4571
|
-
if (
|
|
4572
|
-
kind === "swap_only" &&
|
|
4573
|
-
(revisionNumber < 2 ||
|
|
4574
|
-
parentRevisionId === null ||
|
|
4575
|
-
common.addedOverrideCount < 1 ||
|
|
4576
|
-
common.activeOverrideCount < common.addedOverrideCount)
|
|
4577
|
-
) {
|
|
4578
|
-
throw new Error("Swap context revision row is invalid.");
|
|
4579
|
-
}
|
|
4580
|
-
if (kind === "swap_only") {
|
|
4581
|
-
if (
|
|
4582
|
-
row.change_manifest_sha256 !== null ||
|
|
4583
|
-
row.activation_manifest_sha256 !== null ||
|
|
4584
|
-
!hasNoRetirementFields(row)
|
|
4585
|
-
) {
|
|
4586
|
-
throw new Error("Swap context revision has a change manifest.");
|
|
4587
|
-
}
|
|
4588
|
-
return Object.freeze({
|
|
4589
|
-
...common,
|
|
4590
|
-
revisionNumber,
|
|
4591
|
-
parentRevisionId: parentRevisionId!,
|
|
4592
|
-
kind,
|
|
4593
|
-
policyVersion: enumFromSql(
|
|
4594
|
-
row.policy_version,
|
|
4595
|
-
["swap-only-v1"] as const,
|
|
4596
|
-
"context revision policy",
|
|
4597
|
-
),
|
|
4598
|
-
rendererFormat: enumFromSql(
|
|
4599
|
-
row.renderer_format,
|
|
4600
|
-
[SWAP_OBSERVATION_FORMAT] as const,
|
|
4601
|
-
"context revision renderer format",
|
|
4602
|
-
),
|
|
4603
|
-
planSha256: sha256FromSql(row.plan_sha256, "plan_sha256"),
|
|
4604
|
-
});
|
|
4605
|
-
}
|
|
4606
|
-
if (kind === "skills_update") {
|
|
4607
|
-
if (
|
|
4608
|
-
revisionNumber < 2 ||
|
|
4609
|
-
parentRevisionId === null ||
|
|
4610
|
-
common.addedOverrideCount < 1 ||
|
|
4611
|
-
common.activeOverrideCount < common.addedOverrideCount ||
|
|
4612
|
-
row.plan_sha256 !== null ||
|
|
4613
|
-
!hasNoRetirementFields(row)
|
|
4614
|
-
) {
|
|
4615
|
-
throw new Error("Agent Skills context revision row is invalid.");
|
|
4616
|
-
}
|
|
4617
|
-
return Object.freeze({
|
|
4618
|
-
...common,
|
|
4619
|
-
revisionNumber,
|
|
4620
|
-
parentRevisionId,
|
|
4621
|
-
kind,
|
|
4622
|
-
policyVersion: enumFromSql(
|
|
4623
|
-
row.policy_version,
|
|
4624
|
-
[SKILL_POLICY_VERSION] as const,
|
|
4625
|
-
"Agent Skills context revision policy",
|
|
4626
|
-
),
|
|
4627
|
-
rendererFormat: enumFromSql(
|
|
4628
|
-
row.renderer_format,
|
|
4629
|
-
[SKILL_ACTIVATION_RECEIPT_FORMAT] as const,
|
|
4630
|
-
"Agent Skills context revision renderer format",
|
|
4631
|
-
),
|
|
4632
|
-
changeManifestSha256: sha256FromSql(
|
|
4633
|
-
row.change_manifest_sha256,
|
|
4634
|
-
"change_manifest_sha256",
|
|
4635
|
-
),
|
|
4636
|
-
activationManifestSha256: sha256FromSql(
|
|
4637
|
-
row.activation_manifest_sha256,
|
|
4638
|
-
"activation_manifest_sha256",
|
|
4639
|
-
),
|
|
4640
|
-
});
|
|
4641
|
-
}
|
|
4642
|
-
if (kind === "prefix_retirement") {
|
|
4643
|
-
const retiredThroughOrdinal = numberFromSql(
|
|
4644
|
-
row.retired_through_ordinal,
|
|
4645
|
-
"retired_through_ordinal",
|
|
4646
|
-
);
|
|
4647
|
-
const retiredTurnCount = numberFromSql(
|
|
4648
|
-
row.retired_turn_count,
|
|
4649
|
-
"retired_turn_count",
|
|
4650
|
-
);
|
|
4651
|
-
const retiredFrameCount = numberFromSql(
|
|
4652
|
-
row.retired_frame_count,
|
|
4653
|
-
"retired_frame_count",
|
|
4654
|
-
);
|
|
4655
|
-
const retiredMessageCount = numberFromSql(
|
|
4656
|
-
row.retired_message_count,
|
|
4657
|
-
"retired_message_count",
|
|
4658
|
-
);
|
|
4659
|
-
if (
|
|
4660
|
-
revisionNumber < 2 ||
|
|
4661
|
-
parentRevisionId === null ||
|
|
4662
|
-
common.keepFromOrdinal <= 1 ||
|
|
4663
|
-
common.addedOverrideCount !== 0 ||
|
|
4664
|
-
retiredThroughOrdinal !== common.keepFromOrdinal - 1 ||
|
|
4665
|
-
retiredTurnCount < 1 ||
|
|
4666
|
-
retiredFrameCount < 1 ||
|
|
4667
|
-
retiredMessageCount < 1 ||
|
|
4668
|
-
row.renderer_format !== null ||
|
|
4669
|
-
row.change_manifest_sha256 !== null ||
|
|
4670
|
-
row.activation_manifest_sha256 !== null
|
|
4671
|
-
) {
|
|
4672
|
-
throw new Error("Prefix retirement revision row is invalid.");
|
|
4673
|
-
}
|
|
4674
|
-
return Object.freeze({
|
|
4675
|
-
...common,
|
|
4676
|
-
revisionNumber,
|
|
4677
|
-
parentRevisionId,
|
|
4678
|
-
kind,
|
|
4679
|
-
addedOverrideCount: 0,
|
|
4680
|
-
policyVersion: enumFromSql(
|
|
4681
|
-
row.policy_version,
|
|
4682
|
-
["recall-first-retirement-v1"] as const,
|
|
4683
|
-
"context revision policy",
|
|
4684
|
-
),
|
|
4685
|
-
planSha256: sha256FromSql(row.plan_sha256, "plan_sha256"),
|
|
4686
|
-
retiredThroughOrdinal,
|
|
4687
|
-
retiredTurnCount,
|
|
4688
|
-
retiredFrameCount,
|
|
4689
|
-
retiredMessageCount,
|
|
4690
|
-
});
|
|
4691
|
-
}
|
|
4692
|
-
if (
|
|
4693
|
-
revisionNumber < 2 ||
|
|
4694
|
-
parentRevisionId === null ||
|
|
4695
|
-
common.addedOverrideCount !== 0 ||
|
|
4696
|
-
row.policy_version !== null ||
|
|
4697
|
-
row.renderer_format !== null ||
|
|
4698
|
-
row.plan_sha256 !== null ||
|
|
4699
|
-
row.activation_manifest_sha256 !== null ||
|
|
4700
|
-
!hasNoRetirementFields(row)
|
|
4701
|
-
) {
|
|
4702
|
-
throw new Error("Surface context revision row is invalid.");
|
|
4703
|
-
}
|
|
4704
|
-
return Object.freeze({
|
|
4705
|
-
...common,
|
|
4706
|
-
revisionNumber,
|
|
4707
|
-
parentRevisionId,
|
|
4708
|
-
kind,
|
|
4709
|
-
addedOverrideCount: 0,
|
|
4710
|
-
changeManifestSha256: sha256FromSql(
|
|
4711
|
-
row.change_manifest_sha256,
|
|
4712
|
-
"change_manifest_sha256",
|
|
4713
|
-
),
|
|
4714
|
-
});
|
|
4715
|
-
}
|
|
4716
|
-
|
|
4717
|
-
function hasNoRetirementFields(row: Record<string, unknown>): boolean {
|
|
4718
|
-
return (
|
|
4719
|
-
row.retired_through_ordinal === null &&
|
|
4720
|
-
row.retired_turn_count === null &&
|
|
4721
|
-
row.retired_frame_count === null &&
|
|
4722
|
-
row.retired_message_count === null
|
|
4723
|
-
);
|
|
4724
|
-
}
|
|
4725
|
-
|
|
4726
|
-
function decodeStoredSwapOverride(rowValue: unknown): StoredContextOverrideV8 {
|
|
4727
|
-
const row = recordFromSql(rowValue, "context override");
|
|
4728
|
-
if (row.representation !== "swapped") {
|
|
4729
|
-
throw new Error("Context override representation must be swapped.");
|
|
4730
|
-
}
|
|
4731
|
-
return Object.freeze({
|
|
4732
|
-
introducedRevisionId: stringFromSql(
|
|
4733
|
-
row.introduced_revision_id,
|
|
4734
|
-
"introduced_revision_id",
|
|
4735
|
-
) as ContextRevisionId,
|
|
4736
|
-
frameId: stringFromSql(row.frame_id, "frame_id") as ProtocolFrameId,
|
|
4737
|
-
messageId: stringFromSql(row.message_id, "message_id") as MessageId,
|
|
4738
|
-
ordinal: numberFromSql(row.ordinal, "ordinal"),
|
|
4739
|
-
rendererFormat: enumFromSql(
|
|
4740
|
-
row.renderer_format,
|
|
4741
|
-
[
|
|
4742
|
-
SWAP_OBSERVATION_FORMAT,
|
|
4743
|
-
SWAP_TOOL_IMAGE_FORMAT,
|
|
4744
|
-
SKILL_ACTIVATION_RECEIPT_FORMAT,
|
|
4745
|
-
] as const,
|
|
4746
|
-
"context override renderer format",
|
|
4747
|
-
),
|
|
4748
|
-
source: stringFromSql(row.source, "source") as StoredContextOverrideV8["source"],
|
|
4749
|
-
originalContentSha256: sha256FromSql(
|
|
4750
|
-
row.original_content_sha256,
|
|
4751
|
-
"original_content_sha256",
|
|
4752
|
-
),
|
|
4753
|
-
renderedContent: stringFromSql(row.rendered_content, "rendered_content"),
|
|
4754
|
-
renderedContentSha256: sha256FromSql(
|
|
4755
|
-
row.rendered_content_sha256,
|
|
4756
|
-
"rendered_content_sha256",
|
|
4757
|
-
),
|
|
4758
|
-
originalBytes: numberFromSql(row.original_bytes, "original_bytes"),
|
|
4759
|
-
renderedBytes: numberFromSql(row.rendered_bytes, "rendered_bytes"),
|
|
4760
|
-
byteSavings: numberFromSql(row.byte_savings, "byte_savings"),
|
|
4761
|
-
createdAt: timestampFromSql(row.created_at, "created_at"),
|
|
4762
|
-
});
|
|
4763
|
-
}
|
|
4764
|
-
|
|
4765
|
-
function stripStoredOverride(override: StoredContextOverrideV8): SwapOverride {
|
|
4766
|
-
return Object.freeze({
|
|
4767
|
-
frameId: override.frameId,
|
|
4768
|
-
messageId: override.messageId,
|
|
4769
|
-
ordinal: override.ordinal,
|
|
4770
|
-
source: override.source,
|
|
4771
|
-
originalContentSha256: override.originalContentSha256,
|
|
4772
|
-
renderedContent: override.renderedContent,
|
|
4773
|
-
renderedContentSha256: override.renderedContentSha256,
|
|
4774
|
-
originalBytes: override.originalBytes,
|
|
4775
|
-
renderedBytes: override.renderedBytes,
|
|
4776
|
-
byteSavings: override.byteSavings,
|
|
4777
|
-
...(override.rendererFormat === SWAP_OBSERVATION_FORMAT
|
|
4778
|
-
? {}
|
|
4779
|
-
: { rendererFormat: override.rendererFormat }),
|
|
4780
|
-
});
|
|
4781
|
-
}
|
|
4782
|
-
|
|
4783
|
-
function decodeSkillActivation(rowValue: unknown): StoredSkillActivation {
|
|
4784
|
-
const row = recordFromSql(rowValue, "skill activation");
|
|
4785
|
-
const state = enumFromSql(
|
|
4786
|
-
row.state,
|
|
4787
|
-
["pending", "dispatched", "promoted", "rejected"] as const,
|
|
4788
|
-
"skill activation state",
|
|
4789
|
-
);
|
|
4790
|
-
const dispatchedIterationId = nullableStringFromSql(
|
|
4791
|
-
row.dispatched_iteration_id,
|
|
4792
|
-
"dispatched_iteration_id",
|
|
4793
|
-
) as IterationId | null;
|
|
4794
|
-
const settledRevisionId = nullableStringFromSql(
|
|
4795
|
-
row.settled_revision_id,
|
|
4796
|
-
"settled_revision_id",
|
|
4797
|
-
) as ContextRevisionId | null;
|
|
4798
|
-
const rejectionReason = nullableStringFromSql(
|
|
4799
|
-
row.rejection_reason,
|
|
4800
|
-
"rejection_reason",
|
|
4801
|
-
);
|
|
4802
|
-
if (
|
|
4803
|
-
(state === "pending" &&
|
|
4804
|
-
(dispatchedIterationId !== null ||
|
|
4805
|
-
settledRevisionId !== null ||
|
|
4806
|
-
rejectionReason !== null)) ||
|
|
4807
|
-
(state === "dispatched" &&
|
|
4808
|
-
(dispatchedIterationId === null ||
|
|
4809
|
-
settledRevisionId !== null ||
|
|
4810
|
-
rejectionReason !== null)) ||
|
|
4811
|
-
(state === "promoted" &&
|
|
4812
|
-
(dispatchedIterationId === null ||
|
|
4813
|
-
settledRevisionId === null ||
|
|
4814
|
-
rejectionReason !== null)) ||
|
|
4815
|
-
(state === "rejected" &&
|
|
4816
|
-
(settledRevisionId === null ||
|
|
4817
|
-
rejectionReason === null ||
|
|
4818
|
-
rejectionReason.trim() === ""))
|
|
4819
|
-
) {
|
|
4820
|
-
throw new Error("Skill activation lifecycle fields are invalid.");
|
|
4821
|
-
}
|
|
4822
|
-
return Object.freeze({
|
|
4823
|
-
activationMessageId: stringFromSql(
|
|
4824
|
-
row.activation_message_id,
|
|
4825
|
-
"activation_message_id",
|
|
4826
|
-
) as MessageId,
|
|
4827
|
-
toolCallId: stringFromSql(row.tool_call_id, "tool_call_id") as ToolCallId,
|
|
4828
|
-
sessionId: stringFromSql(row.session_id, "session_id") as SessionId,
|
|
4829
|
-
name: stringFromSql(row.name, "skill activation name"),
|
|
4830
|
-
scope: enumFromSql(
|
|
4831
|
-
row.scope,
|
|
4832
|
-
["project", "user"] as const,
|
|
4833
|
-
"skill activation scope",
|
|
4834
|
-
),
|
|
4835
|
-
skillFileSha256: sha256FromSql(row.skill_file_sha256, "skill_file_sha256"),
|
|
4836
|
-
state,
|
|
4837
|
-
...(dispatchedIterationId === null ? {} : { dispatchedIterationId }),
|
|
4838
|
-
...(settledRevisionId === null ? {} : { settledRevisionId }),
|
|
4839
|
-
...(rejectionReason === null ? {} : { rejectionReason }),
|
|
4840
|
-
createdAt: timestampFromSql(row.created_at, "created_at"),
|
|
4841
|
-
updatedAt: timestampFromSql(row.updated_at, "updated_at"),
|
|
4842
|
-
});
|
|
4843
|
-
}
|
|
4844
|
-
|
|
4845
|
-
function protocolPrefixView(
|
|
4846
|
-
canonical: ProtocolContextView,
|
|
4847
|
-
throughOrdinal: number,
|
|
4848
|
-
): ProtocolContextView {
|
|
4849
|
-
const messages = canonical.messages.filter(
|
|
4850
|
-
(message) => message.ordinal <= throughOrdinal,
|
|
4851
|
-
);
|
|
4852
|
-
const messageIds = new Set(messages.map((message) => message.messageId));
|
|
4853
|
-
return Object.freeze({
|
|
4854
|
-
sessionId: canonical.sessionId,
|
|
4855
|
-
faulted: false,
|
|
4856
|
-
frames: Object.freeze(
|
|
4857
|
-
canonical.frames.filter(
|
|
4858
|
-
(frame) =>
|
|
4859
|
-
frame.state === "closed" &&
|
|
4860
|
-
frame.lastOrdinal !== undefined &&
|
|
4861
|
-
frame.lastOrdinal <= throughOrdinal,
|
|
4862
|
-
),
|
|
4863
|
-
),
|
|
4864
|
-
messages: Object.freeze(messages),
|
|
4865
|
-
toolResults: Object.freeze(
|
|
4866
|
-
canonical.toolResults.filter((result) => messageIds.has(result.toolMessageId)),
|
|
4867
|
-
),
|
|
4868
|
-
});
|
|
4869
|
-
}
|
|
4870
|
-
|
|
4871
|
-
export function decodeStoredToolCalls(json: string): readonly ToolCall[] {
|
|
4872
|
-
const value = parseJson(json, "tool_calls_json");
|
|
4873
|
-
if (!Array.isArray(value) || value.length === 0) {
|
|
4874
|
-
throw new Error("tool_calls_json must contain a non-empty array.");
|
|
4875
|
-
}
|
|
4876
|
-
const calls = value.map((entry, index): ToolCall => {
|
|
4877
|
-
const call = recordFromSql(entry, `tool call ${index}`);
|
|
4878
|
-
assertObjectKeys(
|
|
4879
|
-
call,
|
|
4880
|
-
[
|
|
4881
|
-
"args",
|
|
4882
|
-
"argsParseError",
|
|
4883
|
-
"iterationId",
|
|
4884
|
-
"iterationNumber",
|
|
4885
|
-
"name",
|
|
4886
|
-
"providerToolCallId",
|
|
4887
|
-
"rawArgs",
|
|
4888
|
-
"sessionId",
|
|
4889
|
-
"toolCallId",
|
|
4890
|
-
"toolCallNumber",
|
|
4891
|
-
"turnId",
|
|
4892
|
-
"turnNumber",
|
|
4893
|
-
],
|
|
4894
|
-
[
|
|
4895
|
-
"args",
|
|
4896
|
-
"iterationId",
|
|
4897
|
-
"iterationNumber",
|
|
4898
|
-
"name",
|
|
4899
|
-
"providerToolCallId",
|
|
4900
|
-
"sessionId",
|
|
4901
|
-
"toolCallId",
|
|
4902
|
-
"toolCallNumber",
|
|
4903
|
-
"turnId",
|
|
4904
|
-
"turnNumber",
|
|
4905
|
-
],
|
|
4906
|
-
`tool call ${index}`,
|
|
4907
|
-
);
|
|
4908
|
-
const toolCallNumber = numberFromJson(call.toolCallNumber, "toolCallNumber");
|
|
4909
|
-
return {
|
|
4910
|
-
sessionId: stringFromSql(call.sessionId, "sessionId") as SessionId,
|
|
4911
|
-
turnId: stringFromSql(call.turnId, "turnId") as TurnId,
|
|
4912
|
-
turnNumber: numberFromJson(call.turnNumber, "turnNumber"),
|
|
4913
|
-
iterationId: stringFromSql(call.iterationId, "iterationId") as IterationId,
|
|
4914
|
-
iterationNumber: numberFromJson(call.iterationNumber, "iterationNumber"),
|
|
4915
|
-
toolCallId: stringFromSql(call.toolCallId, "toolCallId") as ToolCallId,
|
|
4916
|
-
toolCallNumber,
|
|
4917
|
-
providerToolCallId: stringFromSql(call.providerToolCallId, "providerToolCallId"),
|
|
4918
|
-
name: stringFromSql(call.name, "name"),
|
|
4919
|
-
args: immutableCanonicalClone(call.args),
|
|
4920
|
-
...(call.rawArgs === undefined
|
|
4921
|
-
? {}
|
|
4922
|
-
: { rawArgs: stringFromSql(call.rawArgs, "rawArgs") }),
|
|
4923
|
-
...(call.argsParseError === undefined
|
|
4924
|
-
? {}
|
|
4925
|
-
: {
|
|
4926
|
-
argsParseError: stringFromSql(call.argsParseError, "argsParseError"),
|
|
4927
|
-
}),
|
|
4928
|
-
};
|
|
4929
|
-
});
|
|
4930
|
-
return Object.freeze(calls);
|
|
4931
|
-
}
|
|
4932
|
-
|
|
4933
|
-
function decodeProjectInstructionManifest(value: unknown): ProjectInstructionManifest {
|
|
4934
|
-
const record = recordFromSql(value, "project instruction manifest");
|
|
4935
|
-
assertObjectKeys(
|
|
4936
|
-
record,
|
|
4937
|
-
["path", "byteLength", "sha256"],
|
|
4938
|
-
["path", "byteLength", "sha256"],
|
|
4939
|
-
"project instruction manifest",
|
|
4940
|
-
);
|
|
4941
|
-
return Object.freeze({
|
|
4942
|
-
path: enumFromSql(
|
|
4943
|
-
record.path,
|
|
4944
|
-
["AGENTS.md", "CLAUDE.md"] as const,
|
|
4945
|
-
"project instruction path",
|
|
4946
|
-
),
|
|
4947
|
-
byteLength: numberFromJson(record.byteLength, "project instruction byteLength"),
|
|
4948
|
-
sha256: sha256FromSql(record.sha256, "project instruction sha256"),
|
|
4949
|
-
});
|
|
4950
|
-
}
|
|
4951
|
-
|
|
4952
|
-
function decodeSkillCatalogManifest(
|
|
4953
|
-
value: unknown,
|
|
4954
|
-
): readonly SkillCatalogManifestEntry[] {
|
|
4955
|
-
if (!Array.isArray(value)) {
|
|
4956
|
-
throw new Error("skill_catalog_json must contain an array.");
|
|
4957
|
-
}
|
|
4958
|
-
return Object.freeze(
|
|
4959
|
-
value.map((entry, index) => {
|
|
4960
|
-
const record = recordFromSql(entry, `skill catalog entry ${index}`);
|
|
4961
|
-
assertObjectKeys(
|
|
4962
|
-
record,
|
|
4963
|
-
[
|
|
4964
|
-
"name",
|
|
4965
|
-
"scope",
|
|
4966
|
-
"directorySha256",
|
|
4967
|
-
"descriptionSha256",
|
|
4968
|
-
"skillFileSha256",
|
|
4969
|
-
"byteLength",
|
|
4970
|
-
],
|
|
4971
|
-
[
|
|
4972
|
-
"name",
|
|
4973
|
-
"scope",
|
|
4974
|
-
"directorySha256",
|
|
4975
|
-
"descriptionSha256",
|
|
4976
|
-
"skillFileSha256",
|
|
4977
|
-
"byteLength",
|
|
4978
|
-
],
|
|
4979
|
-
`skill catalog entry ${index}`,
|
|
4980
|
-
);
|
|
4981
|
-
return Object.freeze({
|
|
4982
|
-
name: stringFromSql(record.name, "skill name"),
|
|
4983
|
-
scope: enumFromSql(record.scope, ["project", "user"] as const, "skill scope"),
|
|
4984
|
-
directorySha256: sha256FromSql(record.directorySha256, "skill directorySha256"),
|
|
4985
|
-
descriptionSha256: sha256FromSql(
|
|
4986
|
-
record.descriptionSha256,
|
|
4987
|
-
"skill descriptionSha256",
|
|
4988
|
-
),
|
|
4989
|
-
skillFileSha256: sha256FromSql(record.skillFileSha256, "skill skillFileSha256"),
|
|
4990
|
-
byteLength: numberFromJson(record.byteLength, "skill byteLength"),
|
|
4991
|
-
});
|
|
4992
|
-
}),
|
|
4993
|
-
);
|
|
4994
|
-
}
|
|
4995
|
-
|
|
4996
|
-
function decodeActiveSkillsManifest(
|
|
4997
|
-
value: unknown,
|
|
4998
|
-
): readonly ActiveSkillManifestEntry[] {
|
|
4999
|
-
if (!Array.isArray(value)) {
|
|
5000
|
-
throw new Error("active_skills_json must contain an array.");
|
|
5001
|
-
}
|
|
5002
|
-
return Object.freeze(
|
|
5003
|
-
value.map((entry, index) => {
|
|
5004
|
-
const record = recordFromSql(entry, `active skill entry ${index}`);
|
|
5005
|
-
assertObjectKeys(
|
|
5006
|
-
record,
|
|
5007
|
-
[
|
|
5008
|
-
"name",
|
|
5009
|
-
"scope",
|
|
5010
|
-
"directorySha256",
|
|
5011
|
-
"descriptionSha256",
|
|
5012
|
-
"skillFileSha256",
|
|
5013
|
-
"byteLength",
|
|
5014
|
-
"activationMessageId",
|
|
5015
|
-
],
|
|
5016
|
-
[
|
|
5017
|
-
"name",
|
|
5018
|
-
"scope",
|
|
5019
|
-
"directorySha256",
|
|
5020
|
-
"descriptionSha256",
|
|
5021
|
-
"skillFileSha256",
|
|
5022
|
-
"byteLength",
|
|
5023
|
-
"activationMessageId",
|
|
5024
|
-
],
|
|
5025
|
-
`active skill entry ${index}`,
|
|
5026
|
-
);
|
|
5027
|
-
const [catalogEntry] = decodeSkillCatalogManifest([
|
|
5028
|
-
{
|
|
5029
|
-
name: record.name,
|
|
5030
|
-
scope: record.scope,
|
|
5031
|
-
directorySha256: record.directorySha256,
|
|
5032
|
-
descriptionSha256: record.descriptionSha256,
|
|
5033
|
-
skillFileSha256: record.skillFileSha256,
|
|
5034
|
-
byteLength: record.byteLength,
|
|
5035
|
-
},
|
|
5036
|
-
]);
|
|
5037
|
-
if (catalogEntry === undefined) {
|
|
5038
|
-
throw new Error(`Active skill entry ${index} is missing.`);
|
|
5039
|
-
}
|
|
5040
|
-
return Object.freeze({
|
|
5041
|
-
...catalogEntry,
|
|
5042
|
-
activationMessageId: stringFromSql(
|
|
5043
|
-
record.activationMessageId,
|
|
5044
|
-
"skill activationMessageId",
|
|
5045
|
-
) as MessageId,
|
|
5046
|
-
});
|
|
5047
|
-
}),
|
|
5048
|
-
);
|
|
5049
|
-
}
|
|
5050
|
-
|
|
5051
|
-
function decodeToolDefinitions(value: unknown): readonly ToolDefinition[] {
|
|
5052
|
-
if (!Array.isArray(value)) {
|
|
5053
|
-
throw new Error("tool_definitions_json must contain an array.");
|
|
5054
|
-
}
|
|
5055
|
-
const definitions = value.map((entry, index): ToolDefinition => {
|
|
5056
|
-
const record = recordFromSql(entry, `tool definition ${index}`);
|
|
5057
|
-
assertObjectKeys(
|
|
5058
|
-
record,
|
|
5059
|
-
["name", "description", "parameters"],
|
|
5060
|
-
["name", "description", "parameters"],
|
|
5061
|
-
`tool definition ${index}`,
|
|
5062
|
-
);
|
|
5063
|
-
const parameters = recordFromSql(
|
|
5064
|
-
record.parameters,
|
|
5065
|
-
`tool definition ${index} parameters`,
|
|
5066
|
-
);
|
|
5067
|
-
return Object.freeze({
|
|
5068
|
-
name: stringFromSql(record.name, `tool definition ${index} name`),
|
|
5069
|
-
description: stringFromSql(
|
|
5070
|
-
record.description,
|
|
5071
|
-
`tool definition ${index} description`,
|
|
5072
|
-
),
|
|
5073
|
-
parameters: immutableCanonicalClone(parameters),
|
|
5074
|
-
});
|
|
5075
|
-
});
|
|
5076
|
-
return Object.freeze(definitions);
|
|
5077
|
-
}
|
|
5078
|
-
|
|
5079
|
-
export function decodeStoredToolRawResult(value: unknown): ToolRawResult {
|
|
5080
|
-
const raw = recordFromSql(value, "tool raw result");
|
|
5081
|
-
const kind = enumFromSql(
|
|
5082
|
-
raw.kind,
|
|
5083
|
-
[
|
|
5084
|
-
"read",
|
|
5085
|
-
"view_image",
|
|
5086
|
-
"write",
|
|
5087
|
-
"edit",
|
|
5088
|
-
"delete",
|
|
5089
|
-
"glob",
|
|
5090
|
-
"grep",
|
|
5091
|
-
"bash",
|
|
5092
|
-
"update_plan",
|
|
5093
|
-
"task_list",
|
|
5094
|
-
"task_output",
|
|
5095
|
-
"task_input",
|
|
5096
|
-
"task_stop",
|
|
5097
|
-
"web_search",
|
|
5098
|
-
"web_fetch",
|
|
5099
|
-
"recall",
|
|
5100
|
-
"context_maintenance",
|
|
5101
|
-
"memory_search",
|
|
5102
|
-
"memory_get",
|
|
5103
|
-
"wait",
|
|
5104
|
-
"skill",
|
|
5105
|
-
"mcp",
|
|
5106
|
-
"generic",
|
|
5107
|
-
] as const,
|
|
5108
|
-
"tool raw result kind",
|
|
5109
|
-
);
|
|
5110
|
-
if (typeof raw.ok !== "boolean") {
|
|
5111
|
-
throw new Error("tool raw result ok must be a boolean.");
|
|
5112
|
-
}
|
|
5113
|
-
if (kind === "skill") {
|
|
5114
|
-
return decodeStoredSkillRawResult(raw);
|
|
5115
|
-
}
|
|
5116
|
-
if (kind === "view_image") {
|
|
5117
|
-
return decodeStoredViewImageRawResult(raw);
|
|
5118
|
-
}
|
|
5119
|
-
if (kind === "context_maintenance") {
|
|
5120
|
-
return decodeStoredContextMaintenanceRawResult(raw);
|
|
5121
|
-
}
|
|
5122
|
-
return immutableCanonicalClone(raw) as ToolRawResult;
|
|
5123
|
-
}
|
|
5124
|
-
|
|
5125
|
-
function decodeStoredContextMaintenanceRawResult(
|
|
5126
|
-
raw: Record<string, unknown>,
|
|
5127
|
-
): Extract<ToolRawResult, { kind: "context_maintenance" }> {
|
|
5128
|
-
const operation = enumFromSql(
|
|
5129
|
-
raw.operation,
|
|
5130
|
-
["status", "candidates", "swap"] as const,
|
|
5131
|
-
"context maintenance operation",
|
|
5132
|
-
);
|
|
5133
|
-
if (raw.ok === false) {
|
|
5134
|
-
if (operation !== "swap") {
|
|
5135
|
-
assertObjectKeys(
|
|
5136
|
-
raw,
|
|
5137
|
-
["kind", "ok", "operation", "error"],
|
|
5138
|
-
["kind", "ok", "operation", "error"],
|
|
5139
|
-
`failed context ${operation} result`,
|
|
5140
|
-
);
|
|
5141
|
-
return immutableRecord({
|
|
5142
|
-
kind: "context_maintenance" as const,
|
|
5143
|
-
ok: false as const,
|
|
5144
|
-
operation,
|
|
5145
|
-
error: nonEmptyStringFromJson(raw.error, `context ${operation} error`),
|
|
5146
|
-
});
|
|
5147
|
-
}
|
|
5148
|
-
assertObjectKeys(
|
|
5149
|
-
raw,
|
|
5150
|
-
["kind", "ok", "operation", "scheduled", "rejected", "error"],
|
|
5151
|
-
["kind", "ok", "operation", "scheduled", "rejected"],
|
|
5152
|
-
"failed context swap result",
|
|
5153
|
-
);
|
|
5154
|
-
if (!Array.isArray(raw.scheduled) || raw.scheduled.length !== 0) {
|
|
5155
|
-
throw new Error("Failed context swap result must schedule no candidates.");
|
|
5156
|
-
}
|
|
5157
|
-
const rejected = decodeContextSwapRejected(raw.rejected);
|
|
5158
|
-
const error =
|
|
5159
|
-
raw.error === undefined
|
|
5160
|
-
? undefined
|
|
5161
|
-
: nonEmptyStringFromJson(raw.error, "context swap error");
|
|
5162
|
-
if (rejected.length === 0 && error === undefined) {
|
|
5163
|
-
throw new Error("Failed context swap result must explain its failure.");
|
|
5164
|
-
}
|
|
5165
|
-
return immutableRecord({
|
|
5166
|
-
kind: "context_maintenance" as const,
|
|
5167
|
-
ok: false as const,
|
|
5168
|
-
operation,
|
|
5169
|
-
scheduled: Object.freeze([]),
|
|
5170
|
-
rejected,
|
|
5171
|
-
...(error === undefined ? {} : { error }),
|
|
5172
|
-
});
|
|
5173
|
-
}
|
|
5174
|
-
if (raw.ok !== true) {
|
|
5175
|
-
throw new Error("Context maintenance raw result ok must be a boolean.");
|
|
5176
|
-
}
|
|
5177
|
-
if (operation === "status") {
|
|
5178
|
-
assertObjectKeys(
|
|
5179
|
-
raw,
|
|
5180
|
-
[
|
|
5181
|
-
"kind",
|
|
5182
|
-
"ok",
|
|
5183
|
-
"operation",
|
|
5184
|
-
"usedInputTokens",
|
|
5185
|
-
"inputBudgetTokens",
|
|
5186
|
-
"pressure",
|
|
5187
|
-
"triggerTokens",
|
|
5188
|
-
"source",
|
|
5189
|
-
],
|
|
5190
|
-
[
|
|
5191
|
-
"kind",
|
|
5192
|
-
"ok",
|
|
5193
|
-
"operation",
|
|
5194
|
-
"usedInputTokens",
|
|
5195
|
-
"inputBudgetTokens",
|
|
5196
|
-
"pressure",
|
|
5197
|
-
"triggerTokens",
|
|
5198
|
-
"source",
|
|
5199
|
-
],
|
|
5200
|
-
"context status result",
|
|
5201
|
-
);
|
|
5202
|
-
return immutableRecord({
|
|
5203
|
-
kind: "context_maintenance" as const,
|
|
5204
|
-
ok: true as const,
|
|
5205
|
-
operation,
|
|
5206
|
-
usedInputTokens: nonNegativeJsonInteger(
|
|
5207
|
-
raw.usedInputTokens,
|
|
5208
|
-
"context status usedInputTokens",
|
|
5209
|
-
),
|
|
5210
|
-
inputBudgetTokens: positiveJsonInteger(
|
|
5211
|
-
raw.inputBudgetTokens,
|
|
5212
|
-
"context status inputBudgetTokens",
|
|
5213
|
-
),
|
|
5214
|
-
pressure: enumFromSql(
|
|
5215
|
-
raw.pressure,
|
|
5216
|
-
["normal", "high", "critical"] as const,
|
|
5217
|
-
"context status pressure",
|
|
5218
|
-
),
|
|
5219
|
-
triggerTokens: positiveJsonInteger(
|
|
5220
|
-
raw.triggerTokens,
|
|
5221
|
-
"context status triggerTokens",
|
|
5222
|
-
),
|
|
5223
|
-
source: enumFromSql(
|
|
5224
|
-
raw.source,
|
|
5225
|
-
[
|
|
5226
|
-
"estimated_full",
|
|
5227
|
-
"provider_measured",
|
|
5228
|
-
"measured_plus_estimated_delta",
|
|
5229
|
-
] as const,
|
|
5230
|
-
"context status source",
|
|
5231
|
-
),
|
|
5232
|
-
});
|
|
5233
|
-
}
|
|
5234
|
-
if (operation === "candidates") {
|
|
5235
|
-
assertObjectKeys(
|
|
5236
|
-
raw,
|
|
5237
|
-
["kind", "ok", "operation", "total", "candidates"],
|
|
5238
|
-
["kind", "ok", "operation", "total", "candidates"],
|
|
5239
|
-
"context swap candidates result",
|
|
5240
|
-
);
|
|
5241
|
-
if (!Array.isArray(raw.candidates) || raw.candidates.length > 50) {
|
|
5242
|
-
throw new Error("Context swap candidates result has an invalid page.");
|
|
5243
|
-
}
|
|
5244
|
-
const candidates = raw.candidates.map((value, index) => {
|
|
5245
|
-
const candidate = recordFromSql(value, `context candidate ${index}`);
|
|
5246
|
-
assertObjectKeys(
|
|
5247
|
-
candidate,
|
|
5248
|
-
["candidateId", "label", "ordinal", "savingsBytes"],
|
|
5249
|
-
["candidateId", "label", "ordinal", "savingsBytes"],
|
|
5250
|
-
`context candidate ${index}`,
|
|
5251
|
-
);
|
|
5252
|
-
const label = stringFromSql(candidate.label, `context candidate ${index} label`);
|
|
5253
|
-
if (
|
|
5254
|
-
label === "" ||
|
|
5255
|
-
label !== label.replace(/[\p{Cc}\p{Cf}\s]+/gu, " ").trim() ||
|
|
5256
|
-
Buffer.byteLength(label, "utf8") > 80
|
|
5257
|
-
) {
|
|
5258
|
-
throw new Error(`Context candidate ${index} label is invalid or too large.`);
|
|
5259
|
-
}
|
|
5260
|
-
return immutableRecord({
|
|
5261
|
-
candidateId: parseMessageId(
|
|
5262
|
-
stringFromSql(candidate.candidateId, `context candidate ${index} ID`),
|
|
5263
|
-
),
|
|
5264
|
-
label,
|
|
5265
|
-
ordinal: positiveJsonInteger(
|
|
5266
|
-
candidate.ordinal,
|
|
5267
|
-
`context candidate ${index} ordinal`,
|
|
5268
|
-
),
|
|
5269
|
-
savingsBytes: positiveJsonInteger(
|
|
5270
|
-
candidate.savingsBytes,
|
|
5271
|
-
`context candidate ${index} savingsBytes`,
|
|
5272
|
-
),
|
|
5273
|
-
});
|
|
5274
|
-
});
|
|
5275
|
-
if (
|
|
5276
|
-
new Set(candidates.map((candidate) => candidate.candidateId)).size !==
|
|
5277
|
-
candidates.length ||
|
|
5278
|
-
candidates.some(
|
|
5279
|
-
(candidate, index) =>
|
|
5280
|
-
index > 0 &&
|
|
5281
|
-
candidate.ordinal <= (candidates[index - 1]?.ordinal ?? candidate.ordinal),
|
|
5282
|
-
)
|
|
5283
|
-
) {
|
|
5284
|
-
throw new Error(
|
|
5285
|
-
"Context swap candidates must have unique IDs and ascending ordinals.",
|
|
5286
|
-
);
|
|
5287
|
-
}
|
|
5288
|
-
const total = nonNegativeJsonInteger(raw.total, "context candidates total");
|
|
5289
|
-
if (total < candidates.length) {
|
|
5290
|
-
throw new Error("Context candidates total is smaller than its page.");
|
|
5291
|
-
}
|
|
5292
|
-
return immutableRecord({
|
|
5293
|
-
kind: "context_maintenance" as const,
|
|
5294
|
-
ok: true as const,
|
|
5295
|
-
operation,
|
|
5296
|
-
total,
|
|
5297
|
-
candidates: Object.freeze(candidates),
|
|
5298
|
-
});
|
|
5299
|
-
}
|
|
5300
|
-
|
|
5301
|
-
assertObjectKeys(
|
|
5302
|
-
raw,
|
|
5303
|
-
["kind", "ok", "operation", "scheduled", "rejected", "note"],
|
|
5304
|
-
["kind", "ok", "operation", "scheduled", "rejected", "note"],
|
|
5305
|
-
"context swap result",
|
|
5306
|
-
);
|
|
5307
|
-
if (!Array.isArray(raw.scheduled) || raw.scheduled.length < 1) {
|
|
5308
|
-
throw new Error("Successful context swap result must schedule candidates.");
|
|
5309
|
-
}
|
|
5310
|
-
const scheduled = raw.scheduled.map((value, index) => {
|
|
5311
|
-
const candidate = recordFromSql(value, `scheduled context candidate ${index}`);
|
|
5312
|
-
assertObjectKeys(
|
|
5313
|
-
candidate,
|
|
5314
|
-
["candidateId", "savingsBytes"],
|
|
5315
|
-
["candidateId", "savingsBytes"],
|
|
5316
|
-
`scheduled context candidate ${index}`,
|
|
5317
|
-
);
|
|
5318
|
-
return immutableRecord({
|
|
5319
|
-
candidateId: parseMessageId(
|
|
5320
|
-
stringFromSql(candidate.candidateId, `scheduled candidate ${index} ID`),
|
|
5321
|
-
),
|
|
5322
|
-
savingsBytes: positiveJsonInteger(
|
|
5323
|
-
candidate.savingsBytes,
|
|
5324
|
-
`scheduled candidate ${index} savingsBytes`,
|
|
5325
|
-
),
|
|
5326
|
-
});
|
|
5327
|
-
});
|
|
5328
|
-
if (
|
|
5329
|
-
scheduled.length > 16 ||
|
|
5330
|
-
new Set(scheduled.map((candidate) => candidate.candidateId)).size !==
|
|
5331
|
-
scheduled.length
|
|
5332
|
-
) {
|
|
5333
|
-
throw new Error("Successful context swap result has invalid scheduled IDs.");
|
|
5334
|
-
}
|
|
5335
|
-
const rejected = decodeContextSwapRejected(raw.rejected);
|
|
5336
|
-
if (
|
|
5337
|
-
scheduled.length + rejected.length > 16 ||
|
|
5338
|
-
scheduled.some((scheduledCandidate) =>
|
|
5339
|
-
rejected.some(
|
|
5340
|
-
(rejectedCandidate) =>
|
|
5341
|
-
rejectedCandidate.candidateId === scheduledCandidate.candidateId,
|
|
5342
|
-
),
|
|
5343
|
-
)
|
|
5344
|
-
) {
|
|
5345
|
-
throw new Error("Context swap result candidate partitions are invalid.");
|
|
5346
|
-
}
|
|
5347
|
-
const note = stringFromSql(raw.note, "context swap note");
|
|
5348
|
-
return immutableRecord({
|
|
5349
|
-
kind: "context_maintenance" as const,
|
|
5350
|
-
ok: true as const,
|
|
5351
|
-
operation,
|
|
5352
|
-
scheduled: Object.freeze(scheduled),
|
|
5353
|
-
rejected,
|
|
5354
|
-
note,
|
|
5355
|
-
});
|
|
5356
|
-
}
|
|
5357
|
-
|
|
5358
|
-
function decodeContextSwapRejected(value: unknown): readonly {
|
|
5359
|
-
readonly candidateId: MessageId;
|
|
5360
|
-
readonly reason: string;
|
|
5361
|
-
}[] {
|
|
5362
|
-
if (!Array.isArray(value) || value.length > 16) {
|
|
5363
|
-
throw new Error("Context swap rejected candidates must be an array of at most 16.");
|
|
5364
|
-
}
|
|
5365
|
-
const rejected = value.map((entry, index) => {
|
|
5366
|
-
const candidate = recordFromSql(entry, `rejected context candidate ${index}`);
|
|
5367
|
-
assertObjectKeys(
|
|
5368
|
-
candidate,
|
|
5369
|
-
["candidateId", "reason"],
|
|
5370
|
-
["candidateId", "reason"],
|
|
5371
|
-
`rejected context candidate ${index}`,
|
|
5372
|
-
);
|
|
5373
|
-
const reason = stringFromSql(
|
|
5374
|
-
candidate.reason,
|
|
5375
|
-
`rejected context candidate ${index} reason`,
|
|
5376
|
-
);
|
|
5377
|
-
if (!/^[a-z][a-z0-9_]{0,79}$/.test(reason)) {
|
|
5378
|
-
throw new Error(`Rejected context candidate ${index} reason is invalid.`);
|
|
5379
|
-
}
|
|
5380
|
-
return immutableRecord({
|
|
5381
|
-
candidateId: parseMessageId(
|
|
5382
|
-
stringFromSql(candidate.candidateId, `rejected candidate ${index} ID`),
|
|
5383
|
-
),
|
|
5384
|
-
reason,
|
|
5385
|
-
});
|
|
5386
|
-
});
|
|
5387
|
-
if (
|
|
5388
|
-
new Set(rejected.map((candidate) => candidate.candidateId)).size !== rejected.length
|
|
5389
|
-
) {
|
|
5390
|
-
throw new Error("Context swap rejected candidate IDs must be unique.");
|
|
5391
|
-
}
|
|
5392
|
-
return Object.freeze(rejected);
|
|
5393
|
-
}
|
|
5394
|
-
|
|
5395
|
-
function decodeStoredViewImageRawResult(
|
|
5396
|
-
raw: Record<string, unknown>,
|
|
5397
|
-
): Extract<ToolRawResult, { kind: "view_image" }> {
|
|
5398
|
-
assertObjectKeys(
|
|
5399
|
-
raw,
|
|
5400
|
-
["kind", "ok", "filePath", "originalName", "asset", "error"],
|
|
5401
|
-
["kind", "ok", "filePath"],
|
|
5402
|
-
"ViewImage raw result",
|
|
5403
|
-
);
|
|
5404
|
-
const filePath = stringFromSql(raw.filePath, "ViewImage filePath");
|
|
5405
|
-
if (raw.ok === false) {
|
|
5406
|
-
if (raw.originalName !== undefined || raw.asset !== undefined) {
|
|
5407
|
-
throw new Error("Failed ViewImage raw result cannot contain image metadata.");
|
|
5408
|
-
}
|
|
5409
|
-
const error = stringFromSql(raw.error, "ViewImage error");
|
|
5410
|
-
if (error.trim() === "") {
|
|
5411
|
-
throw new Error("Failed ViewImage raw result error must not be empty.");
|
|
5412
|
-
}
|
|
5413
|
-
return immutableRecord({ kind: "view_image", ok: false, filePath, error });
|
|
5414
|
-
}
|
|
5415
|
-
if (raw.ok !== true || raw.error !== undefined || filePath.trim() === "") {
|
|
5416
|
-
throw new Error("Successful ViewImage raw result is invalid.");
|
|
5417
|
-
}
|
|
5418
|
-
const originalName = stringFromSql(raw.originalName, "ViewImage originalName");
|
|
5419
|
-
validateOriginalImageName(originalName);
|
|
5420
|
-
const storedAsset = recordFromSql(raw.asset, "ViewImage asset");
|
|
5421
|
-
assertObjectKeys(
|
|
5422
|
-
storedAsset,
|
|
5423
|
-
["assetId", "mimeType", "byteLength", "width", "height"],
|
|
5424
|
-
["assetId", "mimeType", "byteLength", "width", "height"],
|
|
5425
|
-
"ViewImage asset",
|
|
5426
|
-
);
|
|
5427
|
-
const asset: ImageAssetRef = immutableRecord({
|
|
5428
|
-
assetId: parseImageAssetId(stringFromSql(storedAsset.assetId, "assetId")),
|
|
5429
|
-
mimeType: enumFromSql(
|
|
5430
|
-
storedAsset.mimeType,
|
|
5431
|
-
["image/png", "image/jpeg", "image/webp"] as const,
|
|
5432
|
-
"ViewImage asset mimeType",
|
|
5433
|
-
),
|
|
5434
|
-
byteLength: numberFromJson(storedAsset.byteLength, "ViewImage asset byteLength"),
|
|
5435
|
-
width: numberFromJson(storedAsset.width, "ViewImage asset width"),
|
|
5436
|
-
height: numberFromJson(storedAsset.height, "ViewImage asset height"),
|
|
5437
|
-
});
|
|
5438
|
-
validateImageAssetRef(asset);
|
|
5439
|
-
return immutableRecord({
|
|
5440
|
-
kind: "view_image",
|
|
5441
|
-
ok: true,
|
|
5442
|
-
filePath,
|
|
5443
|
-
originalName,
|
|
5444
|
-
asset,
|
|
5445
|
-
});
|
|
5446
|
-
}
|
|
5447
|
-
|
|
5448
|
-
function decodeStoredSkillRawResult(
|
|
5449
|
-
raw: Record<string, unknown>,
|
|
5450
|
-
): Extract<ToolRawResult, { kind: "skill" }> {
|
|
5451
|
-
const status = enumFromSql(
|
|
5452
|
-
raw.status,
|
|
5453
|
-
["loaded", "already_loaded", "already_active", "failed"] as const,
|
|
5454
|
-
"Skill raw result status",
|
|
5455
|
-
);
|
|
5456
|
-
const name = stringFromSql(raw.name, "Skill raw result name");
|
|
5457
|
-
if (status === "failed") {
|
|
5458
|
-
if (raw.ok !== false) {
|
|
5459
|
-
throw new Error("Failed Skill raw result must have ok=false.");
|
|
5460
|
-
}
|
|
5461
|
-
assertObjectKeys(
|
|
5462
|
-
raw,
|
|
5463
|
-
["kind", "ok", "status", "name", "errorCode", "error"],
|
|
5464
|
-
["kind", "ok", "status", "name", "errorCode", "error"],
|
|
5465
|
-
"failed Skill raw result",
|
|
5466
|
-
);
|
|
5467
|
-
const errorCode = stringFromSql(raw.errorCode, "Skill errorCode");
|
|
5468
|
-
const error = stringFromSql(raw.error, "Skill error");
|
|
5469
|
-
if (
|
|
5470
|
-
(name !== "" && !isValidSkillName(name)) ||
|
|
5471
|
-
!/^[A-Z][A-Z0-9_]{0,79}$/.test(errorCode) ||
|
|
5472
|
-
error.trim() === ""
|
|
5473
|
-
) {
|
|
5474
|
-
throw new Error("Failed Skill raw result fields are invalid.");
|
|
5475
|
-
}
|
|
5476
|
-
return immutableRecord({
|
|
5477
|
-
kind: "skill" as const,
|
|
5478
|
-
ok: false as const,
|
|
5479
|
-
status,
|
|
5480
|
-
name,
|
|
5481
|
-
errorCode,
|
|
5482
|
-
error,
|
|
5483
|
-
});
|
|
5484
|
-
}
|
|
5485
|
-
if (raw.ok !== true) {
|
|
5486
|
-
throw new Error("Successful Skill raw result must have ok=true.");
|
|
5487
|
-
}
|
|
5488
|
-
const scope = enumFromSql(
|
|
5489
|
-
raw.scope,
|
|
5490
|
-
["project", "user"] as const,
|
|
5491
|
-
"Skill raw result scope",
|
|
5492
|
-
);
|
|
5493
|
-
const skillFileSha256 = sha256FromSql(raw.sha256, "Skill raw result sha256");
|
|
5494
|
-
if (!isValidSkillName(name)) {
|
|
5495
|
-
throw new Error("Successful Skill raw result name is invalid.");
|
|
5496
|
-
}
|
|
5497
|
-
if (status === "already_loaded") {
|
|
5498
|
-
assertObjectKeys(
|
|
5499
|
-
raw,
|
|
5500
|
-
["kind", "ok", "status", "name", "scope", "lifecycle", "sha256"],
|
|
5501
|
-
["kind", "ok", "status", "name", "scope", "lifecycle", "sha256"],
|
|
5502
|
-
"already loaded Skill raw result",
|
|
5503
|
-
);
|
|
5504
|
-
return immutableRecord({
|
|
5505
|
-
kind: "skill" as const,
|
|
5506
|
-
ok: true as const,
|
|
5507
|
-
status,
|
|
5508
|
-
name,
|
|
5509
|
-
scope,
|
|
5510
|
-
lifecycle: enumFromSql(
|
|
5511
|
-
raw.lifecycle,
|
|
5512
|
-
["pending", "dispatched"] as const,
|
|
5513
|
-
"Skill lifecycle",
|
|
5514
|
-
),
|
|
5515
|
-
sha256: skillFileSha256,
|
|
5516
|
-
});
|
|
5517
|
-
}
|
|
5518
|
-
if (status === "already_active") {
|
|
5519
|
-
assertObjectKeys(
|
|
5520
|
-
raw,
|
|
5521
|
-
["kind", "ok", "status", "name", "scope", "sha256"],
|
|
5522
|
-
["kind", "ok", "status", "name", "scope", "sha256"],
|
|
5523
|
-
"already active Skill raw result",
|
|
5524
|
-
);
|
|
5525
|
-
return immutableRecord({
|
|
5526
|
-
kind: "skill" as const,
|
|
5527
|
-
ok: true as const,
|
|
5528
|
-
status,
|
|
5529
|
-
name,
|
|
5530
|
-
scope,
|
|
5531
|
-
sha256: skillFileSha256,
|
|
5532
|
-
});
|
|
5533
|
-
}
|
|
5534
|
-
assertObjectKeys(
|
|
5535
|
-
raw,
|
|
5536
|
-
[
|
|
5537
|
-
"kind",
|
|
5538
|
-
"ok",
|
|
5539
|
-
"status",
|
|
5540
|
-
"name",
|
|
5541
|
-
"scope",
|
|
5542
|
-
"directory",
|
|
5543
|
-
"skillFilePath",
|
|
5544
|
-
"content",
|
|
5545
|
-
"byteLength",
|
|
5546
|
-
"sha256",
|
|
5547
|
-
"resources",
|
|
5548
|
-
"resourcesTruncated",
|
|
5549
|
-
],
|
|
5550
|
-
[
|
|
5551
|
-
"kind",
|
|
5552
|
-
"ok",
|
|
5553
|
-
"status",
|
|
5554
|
-
"name",
|
|
5555
|
-
"scope",
|
|
5556
|
-
"directory",
|
|
5557
|
-
"skillFilePath",
|
|
5558
|
-
"content",
|
|
5559
|
-
"byteLength",
|
|
5560
|
-
"sha256",
|
|
5561
|
-
"resources",
|
|
5562
|
-
"resourcesTruncated",
|
|
5563
|
-
],
|
|
5564
|
-
"loaded Skill raw result",
|
|
5565
|
-
);
|
|
5566
|
-
if (
|
|
5567
|
-
!Array.isArray(raw.resources) ||
|
|
5568
|
-
raw.resources.some((entry) => typeof entry !== "string") ||
|
|
5569
|
-
raw.resources.length > SKILL_RESOURCE_MAX_ENTRIES ||
|
|
5570
|
-
typeof raw.resourcesTruncated !== "boolean"
|
|
5571
|
-
) {
|
|
5572
|
-
throw new Error("Loaded Skill resource manifest is invalid.");
|
|
5573
|
-
}
|
|
5574
|
-
const directory = stringFromSql(raw.directory, "Skill directory");
|
|
5575
|
-
const skillFilePath = stringFromSql(raw.skillFilePath, "Skill file path");
|
|
5576
|
-
const content = stringFromSql(raw.content, "Skill content");
|
|
5577
|
-
const byteLength = numberFromJson(raw.byteLength, "Skill byteLength");
|
|
5578
|
-
const resources = raw.resources as string[];
|
|
5579
|
-
if (
|
|
5580
|
-
!path.isAbsolute(directory) ||
|
|
5581
|
-
!path.isAbsolute(skillFilePath) ||
|
|
5582
|
-
!isPathWithin(directory, skillFilePath) ||
|
|
5583
|
-
byteLength < 1 ||
|
|
5584
|
-
byteLength > SKILL_FILE_MAX_BYTES ||
|
|
5585
|
-
Buffer.byteLength(content, "utf8") !== byteLength ||
|
|
5586
|
-
sha256(content) !== skillFileSha256 ||
|
|
5587
|
-
!isValidResourceManifest(resources)
|
|
5588
|
-
) {
|
|
5589
|
-
throw new Error("Loaded Skill raw result snapshot is invalid.");
|
|
5590
|
-
}
|
|
5591
|
-
return immutableRecord({
|
|
5592
|
-
kind: "skill" as const,
|
|
5593
|
-
ok: true as const,
|
|
5594
|
-
status,
|
|
5595
|
-
name,
|
|
5596
|
-
scope,
|
|
5597
|
-
directory,
|
|
5598
|
-
skillFilePath,
|
|
5599
|
-
content,
|
|
5600
|
-
byteLength,
|
|
5601
|
-
sha256: skillFileSha256,
|
|
5602
|
-
resources: Object.freeze([...resources]),
|
|
5603
|
-
resourcesTruncated: raw.resourcesTruncated,
|
|
5604
|
-
});
|
|
5605
|
-
}
|
|
5606
|
-
|
|
5607
|
-
function isValidSkillName(value: string): boolean {
|
|
5608
|
-
return value.length <= 64 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
|
|
5609
|
-
}
|
|
5610
|
-
|
|
5611
|
-
function isPathWithin(root: string, candidate: string): boolean {
|
|
5612
|
-
const relative = path.relative(root, candidate);
|
|
5613
|
-
return (
|
|
5614
|
-
relative !== "" &&
|
|
5615
|
-
relative !== ".." &&
|
|
5616
|
-
!relative.startsWith(`..${path.sep}`) &&
|
|
5617
|
-
!path.isAbsolute(relative)
|
|
5618
|
-
);
|
|
5619
|
-
}
|
|
5620
|
-
|
|
5621
|
-
function isValidResourceManifest(resources: readonly string[]): boolean {
|
|
5622
|
-
let previous: string | undefined;
|
|
5623
|
-
for (const resource of resources) {
|
|
5624
|
-
const parts = resource.split("/");
|
|
5625
|
-
if (
|
|
5626
|
-
resource === "" ||
|
|
5627
|
-
resource.includes("\\") ||
|
|
5628
|
-
path.posix.isAbsolute(resource) ||
|
|
5629
|
-
path.posix.normalize(resource) !== resource ||
|
|
5630
|
-
!["assets", "references", "scripts"].includes(parts[0] ?? "") ||
|
|
5631
|
-
parts.length < 2 ||
|
|
5632
|
-
parts.length > SKILL_RESOURCE_MAX_DEPTH + 1 ||
|
|
5633
|
-
(previous !== undefined && previous >= resource)
|
|
5634
|
-
) {
|
|
5635
|
-
return false;
|
|
5636
|
-
}
|
|
5637
|
-
previous = resource;
|
|
5638
|
-
}
|
|
5639
|
-
return true;
|
|
5640
|
-
}
|
|
5641
|
-
|
|
5642
|
-
function decodeMeasuredContextState(
|
|
5643
|
-
value: unknown,
|
|
5644
|
-
expectedSessionId: SessionId,
|
|
5645
|
-
): StoredMeasuredContextState {
|
|
5646
|
-
const row = recordFromSql(value, "context measurement state");
|
|
5647
|
-
const sessionId = stringFromSql(row.session_id, "session_id") as SessionId;
|
|
5648
|
-
if (sessionId !== expectedSessionId) {
|
|
5649
|
-
throw new Error(
|
|
5650
|
-
`Context measurement session ID ${sessionId} does not match store.`,
|
|
5651
|
-
);
|
|
5652
|
-
}
|
|
5653
|
-
const promptTokens = numberFromSql(row.prompt_tokens, "prompt_tokens");
|
|
5654
|
-
const completionTokens = numberFromSql(row.completion_tokens, "completion_tokens");
|
|
5655
|
-
const totalTokens = numberFromSql(row.total_tokens, "total_tokens");
|
|
5656
|
-
if (totalTokens !== promptTokens + completionTokens) {
|
|
5657
|
-
throw new Error(
|
|
5658
|
-
"Context measurement total_tokens must equal prompt_tokens + completion_tokens.",
|
|
5659
|
-
);
|
|
5660
|
-
}
|
|
5661
|
-
timestampFromSql(row.updated_at, "updated_at");
|
|
5662
|
-
return Object.freeze({
|
|
5663
|
-
revisionId: stringFromSql(row.revision_id, "revision_id") as ContextRevisionId,
|
|
5664
|
-
anchor: Object.freeze({
|
|
5665
|
-
totalTokens,
|
|
5666
|
-
promptTokens,
|
|
5667
|
-
completionTokens,
|
|
5668
|
-
segmentCount: numberFromSql(row.segment_count, "segment_count"),
|
|
5669
|
-
prefixHash: sha256FromSql(row.prefix_hash, "prefix_hash"),
|
|
5670
|
-
requestConfigHash: sha256FromSql(row.request_config_hash, "request_config_hash"),
|
|
5671
|
-
toolSchemaHash: sha256FromSql(row.tool_schema_hash, "tool_schema_hash"),
|
|
5672
|
-
}),
|
|
5673
|
-
});
|
|
5674
|
-
}
|
|
5675
|
-
|
|
5676
|
-
function assertCommitSwapRevisionInput(input: CommitSwapRevisionInput): void {
|
|
5677
|
-
if (
|
|
5678
|
-
input.revisionId.trim() === "" ||
|
|
5679
|
-
input.expectedBaseRevisionId.trim() === "" ||
|
|
5680
|
-
!Number.isSafeInteger(input.expectedBaseRevisionNumber) ||
|
|
5681
|
-
input.expectedBaseRevisionNumber < 1 ||
|
|
5682
|
-
!Number.isSafeInteger(input.expectedCanonicalThroughOrdinal) ||
|
|
5683
|
-
input.expectedCanonicalThroughOrdinal < 1 ||
|
|
5684
|
-
input.addedOverrides.length < 1 ||
|
|
5685
|
-
input.policyVersion !== "swap-only-v1" ||
|
|
5686
|
-
input.rendererFormat !== SWAP_OBSERVATION_FORMAT
|
|
5687
|
-
) {
|
|
5688
|
-
throw new Error("Commit swap revision input is invalid.");
|
|
5689
|
-
}
|
|
5690
|
-
for (const [name, hash] of [
|
|
5691
|
-
[
|
|
5692
|
-
"expectedBaseActiveOverrideManifestSha256",
|
|
5693
|
-
input.expectedBaseActiveOverrideManifestSha256,
|
|
5694
|
-
],
|
|
5695
|
-
["planHash", input.planHash],
|
|
5696
|
-
["nextActiveOverrideManifestSha256", input.nextActiveOverrideManifestSha256],
|
|
5697
|
-
["canonicalSequenceSha256", input.canonicalSequenceSha256],
|
|
5698
|
-
["renderedMessageSha256", input.renderedMessageSha256],
|
|
5699
|
-
] as const) {
|
|
5700
|
-
if (!/^[0-9a-f]{64}$/.test(hash)) {
|
|
5701
|
-
throw new Error(`Commit swap revision ${name} must be a SHA-256 hash.`);
|
|
5702
|
-
}
|
|
5703
|
-
}
|
|
5704
|
-
}
|
|
5705
|
-
|
|
5706
|
-
function assertCommitSurfaceRefreshInput(input: CommitSurfaceRefreshInput): void {
|
|
5707
|
-
if (
|
|
5708
|
-
input.revisionId.trim() === "" ||
|
|
5709
|
-
input.expectedBaseRevisionId.trim() === "" ||
|
|
5710
|
-
!Number.isSafeInteger(input.expectedBaseRevisionNumber) ||
|
|
5711
|
-
input.expectedBaseRevisionNumber < 1 ||
|
|
5712
|
-
!Number.isSafeInteger(input.expectedCanonicalThroughOrdinal) ||
|
|
5713
|
-
input.expectedCanonicalThroughOrdinal < 1
|
|
5714
|
-
) {
|
|
5715
|
-
throw new Error("Commit surface refresh input is invalid.");
|
|
5716
|
-
}
|
|
5717
|
-
for (const [name, hash] of [
|
|
5718
|
-
[
|
|
5719
|
-
"expectedBaseActiveOverrideManifestSha256",
|
|
5720
|
-
input.expectedBaseActiveOverrideManifestSha256,
|
|
5721
|
-
],
|
|
5722
|
-
["changeManifestSha256", input.changeManifestSha256],
|
|
5723
|
-
["canonicalSequenceSha256", input.canonicalSequenceSha256],
|
|
5724
|
-
["renderedMessageSha256", input.renderedMessageSha256],
|
|
5725
|
-
] as const) {
|
|
5726
|
-
if (!/^[0-9a-f]{64}$/.test(hash)) {
|
|
5727
|
-
throw new Error(`Commit surface refresh ${name} must be a SHA-256 hash.`);
|
|
5728
|
-
}
|
|
5729
|
-
}
|
|
5730
|
-
}
|
|
5731
|
-
|
|
5732
|
-
function assertCommitSkillsUpdateInput(input: CommitSkillsUpdateInput): void {
|
|
5733
|
-
if (
|
|
5734
|
-
input.revisionId.trim() === "" ||
|
|
5735
|
-
input.expectedBaseRevisionId.trim() === "" ||
|
|
5736
|
-
!Number.isSafeInteger(input.expectedBaseRevisionNumber) ||
|
|
5737
|
-
input.expectedBaseRevisionNumber < 1 ||
|
|
5738
|
-
!Number.isSafeInteger(input.expectedCanonicalThroughOrdinal) ||
|
|
5739
|
-
input.expectedCanonicalThroughOrdinal < 1 ||
|
|
5740
|
-
input.addedOverrides.length < 1 ||
|
|
5741
|
-
input.settlements.length !== input.addedOverrides.length
|
|
5742
|
-
) {
|
|
5743
|
-
throw new Error("Commit Agent Skills update input is invalid.");
|
|
5744
|
-
}
|
|
5745
|
-
for (const [name, hash] of [
|
|
5746
|
-
[
|
|
5747
|
-
"expectedBaseActiveOverrideManifestSha256",
|
|
5748
|
-
input.expectedBaseActiveOverrideManifestSha256,
|
|
5749
|
-
],
|
|
5750
|
-
["changeManifestSha256", input.changeManifestSha256],
|
|
5751
|
-
["activationManifestSha256", input.activationManifestSha256],
|
|
5752
|
-
["nextActiveOverrideManifestSha256", input.nextActiveOverrideManifestSha256],
|
|
5753
|
-
["canonicalSequenceSha256", input.canonicalSequenceSha256],
|
|
5754
|
-
["renderedMessageSha256", input.renderedMessageSha256],
|
|
5755
|
-
] as const) {
|
|
5756
|
-
if (!/^[0-9a-f]{64}$/.test(hash)) {
|
|
5757
|
-
throw new Error(`Commit Agent Skills update ${name} must be a SHA-256 hash.`);
|
|
5758
|
-
}
|
|
5759
|
-
}
|
|
5760
|
-
}
|
|
5761
|
-
|
|
5762
|
-
function assertCommitPrefixRetirementRevisionInput(
|
|
5763
|
-
input: CommitPrefixRetirementRevisionInput,
|
|
5764
|
-
): void {
|
|
5765
|
-
if (
|
|
5766
|
-
input.revisionId.trim() === "" ||
|
|
5767
|
-
input.expectedBaseRevisionId.trim() === "" ||
|
|
5768
|
-
input.policyVersion !== "recall-first-retirement-v1" ||
|
|
5769
|
-
!Number.isSafeInteger(input.expectedBaseRevisionNumber) ||
|
|
5770
|
-
input.expectedBaseRevisionNumber < 1 ||
|
|
5771
|
-
!Number.isSafeInteger(input.expectedBaseKeepFromOrdinal) ||
|
|
5772
|
-
input.expectedBaseKeepFromOrdinal < 1 ||
|
|
5773
|
-
!Number.isSafeInteger(input.expectedCanonicalThroughOrdinal) ||
|
|
5774
|
-
input.expectedCanonicalThroughOrdinal < 1 ||
|
|
5775
|
-
!Number.isSafeInteger(input.nextKeepFromOrdinal) ||
|
|
5776
|
-
input.nextKeepFromOrdinal <= input.expectedBaseKeepFromOrdinal ||
|
|
5777
|
-
input.retiredThroughOrdinal !== input.nextKeepFromOrdinal - 1 ||
|
|
5778
|
-
!Number.isSafeInteger(input.retiredTurnCount) ||
|
|
5779
|
-
input.retiredTurnCount < 1 ||
|
|
5780
|
-
!Number.isSafeInteger(input.retiredFrameCount) ||
|
|
5781
|
-
input.retiredFrameCount < 1 ||
|
|
5782
|
-
!Number.isSafeInteger(input.retiredMessageCount) ||
|
|
5783
|
-
input.retiredMessageCount < 1 ||
|
|
5784
|
-
!Number.isSafeInteger(input.nextActiveOverrideCount) ||
|
|
5785
|
-
input.nextActiveOverrideCount < 0
|
|
5786
|
-
) {
|
|
5787
|
-
throw new Error("Commit prefix retirement input is invalid.");
|
|
5788
|
-
}
|
|
5789
|
-
for (const [name, hash] of [
|
|
5790
|
-
["expectedSurfaceSha256", input.expectedSurfaceSha256],
|
|
5791
|
-
[
|
|
5792
|
-
"expectedBaseActiveOverrideManifestSha256",
|
|
5793
|
-
input.expectedBaseActiveOverrideManifestSha256,
|
|
5794
|
-
],
|
|
5795
|
-
["planHash", input.planHash],
|
|
5796
|
-
["nextActiveOverrideManifestSha256", input.nextActiveOverrideManifestSha256],
|
|
5797
|
-
["canonicalSequenceSha256", input.canonicalSequenceSha256],
|
|
5798
|
-
["renderedMessageSha256", input.renderedMessageSha256],
|
|
5799
|
-
] as const) {
|
|
5800
|
-
if (!/^[0-9a-f]{64}$/.test(hash)) {
|
|
5801
|
-
throw new Error(`Commit prefix retirement ${name} must be a SHA-256 hash.`);
|
|
5802
|
-
}
|
|
5803
|
-
}
|
|
5804
|
-
}
|
|
5805
|
-
|
|
5806
|
-
function requireActiveRevisionId(meta: StoredSessionMetaV10): ContextRevisionId {
|
|
5807
|
-
if (meta.initializationState !== "ready" || meta.activeRevisionId === null) {
|
|
5808
|
-
throw new Error("Session has no active context revision.");
|
|
5809
|
-
}
|
|
5810
|
-
return meta.activeRevisionId;
|
|
5811
|
-
}
|
|
5812
|
-
|
|
5813
|
-
function previousRevision(
|
|
5814
|
-
revisions: readonly StoredContextRevisionV8[],
|
|
5815
|
-
revision: StoredContextRevisionV8,
|
|
5816
|
-
): StoredContextRevisionV8 | undefined {
|
|
5817
|
-
return revisions[revision.revisionNumber - 2];
|
|
5818
|
-
}
|
|
5819
|
-
|
|
5820
|
-
function decodeMeta(
|
|
5821
|
-
value: unknown,
|
|
5822
|
-
expectedSessionId: SessionId,
|
|
5823
|
-
): StoredSessionMetaV10 {
|
|
5824
|
-
const row = recordFromSql(value, "session metadata");
|
|
5825
|
-
const sessionId = stringFromSql(row.session_id, "session_id") as SessionId;
|
|
5826
|
-
if (sessionId !== expectedSessionId) {
|
|
5827
|
-
throw new Error(`Metadata session ID ${sessionId} does not match directory.`);
|
|
5828
|
-
}
|
|
5829
|
-
const schemaVersion = numberFromSql(row.schema_version, "schema_version");
|
|
5830
|
-
if (schemaVersion !== 10) {
|
|
5831
|
-
throw new Error(
|
|
5832
|
-
`Session metadata schema version must be 10; received ${schemaVersion}.`,
|
|
5833
|
-
);
|
|
5834
|
-
}
|
|
5835
|
-
const projectInstructionFile = nullableStringFromSql(
|
|
5836
|
-
row.project_instruction_file,
|
|
5837
|
-
"project_instruction_file",
|
|
5838
|
-
);
|
|
5839
|
-
const projectInstructionByteLength = nullableNumberFromSql(
|
|
5840
|
-
row.project_instruction_byte_length,
|
|
5841
|
-
"project_instruction_byte_length",
|
|
5842
|
-
);
|
|
5843
|
-
const projectInstructionSha256 = nullableStringFromSql(
|
|
5844
|
-
row.project_instruction_sha256,
|
|
5845
|
-
"project_instruction_sha256",
|
|
5846
|
-
);
|
|
5847
|
-
if (
|
|
5848
|
-
(projectInstructionFile === null) !== (projectInstructionByteLength === null) ||
|
|
5849
|
-
(projectInstructionFile === null) !== (projectInstructionSha256 === null)
|
|
5850
|
-
) {
|
|
5851
|
-
throw new Error("Project instruction metadata must be entirely set or null.");
|
|
5852
|
-
}
|
|
5853
|
-
if (
|
|
5854
|
-
projectInstructionFile !== null &&
|
|
5855
|
-
projectInstructionFile !== "AGENTS.md" &&
|
|
5856
|
-
projectInstructionFile !== "CLAUDE.md"
|
|
5857
|
-
) {
|
|
5858
|
-
throw new Error(`Invalid project instruction file ${projectInstructionFile}.`);
|
|
5859
|
-
}
|
|
5860
|
-
const projectInstruction: ProjectInstructionManifest | undefined =
|
|
5861
|
-
projectInstructionFile === null ||
|
|
5862
|
-
projectInstructionByteLength === null ||
|
|
5863
|
-
projectInstructionSha256 === null
|
|
5864
|
-
? undefined
|
|
5865
|
-
: {
|
|
5866
|
-
path: projectInstructionFile === "AGENTS.md" ? "AGENTS.md" : "CLAUDE.md",
|
|
5867
|
-
byteLength: projectInstructionByteLength,
|
|
5868
|
-
sha256: sha256FromSql(projectInstructionSha256, "project_instruction_sha256"),
|
|
5869
|
-
};
|
|
5870
|
-
const initializationState = enumFromSql(
|
|
5871
|
-
row.initialization_state,
|
|
5872
|
-
["creating", "ready"] as const,
|
|
5873
|
-
"initialization_state",
|
|
5874
|
-
);
|
|
5875
|
-
const sessionCompatibilityJson = nullableStringFromSql(
|
|
5876
|
-
row.session_compatibility_json,
|
|
5877
|
-
"session_compatibility_json",
|
|
5878
|
-
);
|
|
5879
|
-
const sessionCompatibilitySha256 = nullableStringFromSql(
|
|
5880
|
-
row.session_compatibility_sha256,
|
|
5881
|
-
"session_compatibility_sha256",
|
|
5882
|
-
);
|
|
5883
|
-
const activeRevisionId = nullableStringFromSql(
|
|
5884
|
-
row.active_revision_id,
|
|
5885
|
-
"active_revision_id",
|
|
5886
|
-
) as ContextRevisionId | null;
|
|
5887
|
-
const modelName = stringFromSql(row.model_name, "model_name");
|
|
5888
|
-
const storedContract =
|
|
5889
|
-
sessionCompatibilityJson === null
|
|
5890
|
-
? undefined
|
|
5891
|
-
: decodeSessionCompatibilityContract(sessionCompatibilityJson);
|
|
5892
|
-
if (
|
|
5893
|
-
(sessionCompatibilityJson === null) !== (sessionCompatibilitySha256 === null) ||
|
|
5894
|
-
(sessionCompatibilityJson !== null &&
|
|
5895
|
-
sha256(sessionCompatibilityJson) !== sessionCompatibilitySha256) ||
|
|
5896
|
-
(initializationState === "creating") !== (activeRevisionId === null) ||
|
|
5897
|
-
(initializationState === "creating") !== (sessionCompatibilityJson === null) ||
|
|
5898
|
-
(storedContract !== undefined &&
|
|
5899
|
-
(storedContract.modelName !== modelName ||
|
|
5900
|
-
stableJsonStringify(storedContract) !== sessionCompatibilityJson))
|
|
5901
|
-
) {
|
|
5902
|
-
throw new Error("Session compatibility or initialization metadata is invalid.");
|
|
5903
|
-
}
|
|
5904
|
-
return {
|
|
5905
|
-
schemaVersion,
|
|
5906
|
-
schemaFingerprint: stringFromSql(row.schema_fingerprint, "schema_fingerprint"),
|
|
5907
|
-
initializationState,
|
|
5908
|
-
sessionId,
|
|
5909
|
-
workspaceRoot: stringFromSql(row.workspace_root, "workspace_root"),
|
|
5910
|
-
modelName,
|
|
5911
|
-
systemPromptSha256: stringFromSql(row.system_prompt_sha256, "system_prompt_sha256"),
|
|
5912
|
-
...(projectInstruction === undefined ? {} : { projectInstruction }),
|
|
5913
|
-
sessionCompatibilityJson,
|
|
5914
|
-
sessionCompatibilitySha256,
|
|
5915
|
-
activeRevisionId,
|
|
5916
|
-
nextTurnNumber: numberFromSql(row.next_turn_number, "next_turn_number"),
|
|
5917
|
-
nextEventSequence: numberFromSql(row.next_event_sequence, "next_event_sequence"),
|
|
5918
|
-
openCount: numberFromSql(row.open_count, "open_count"),
|
|
5919
|
-
createdAt: timestampFromSql(row.created_at, "created_at"),
|
|
5920
|
-
updatedAt: timestampFromSql(row.updated_at, "updated_at"),
|
|
5921
|
-
lastOpenedAt: timestampFromSql(row.last_opened_at, "last_opened_at"),
|
|
5922
|
-
lastClosedAt:
|
|
5923
|
-
row.last_closed_at === null
|
|
5924
|
-
? null
|
|
5925
|
-
: timestampFromSql(row.last_closed_at, "last_closed_at"),
|
|
5926
|
-
lastCloseReason:
|
|
5927
|
-
row.last_close_reason === null
|
|
5928
|
-
? null
|
|
5929
|
-
: enumFromSql(
|
|
5930
|
-
row.last_close_reason,
|
|
5931
|
-
[
|
|
5932
|
-
"oneshot_complete",
|
|
5933
|
-
"tui_exit",
|
|
5934
|
-
"session_switch",
|
|
5935
|
-
"runner_failed",
|
|
5936
|
-
"initialization_failed",
|
|
5937
|
-
] as const,
|
|
5938
|
-
"last_close_reason",
|
|
5939
|
-
),
|
|
5940
|
-
};
|
|
5941
|
-
}
|
|
5942
|
-
|
|
5943
|
-
function compatibilityContractDifferences(
|
|
5944
|
-
storedJson: string | null,
|
|
5945
|
-
current: SessionCompatibilityContract,
|
|
5946
|
-
): string[] {
|
|
5947
|
-
if (storedJson === null) {
|
|
5948
|
-
return ["sessionCompatibility"];
|
|
5949
|
-
}
|
|
5950
|
-
const stored = parseJson(storedJson, "session_compatibility_json");
|
|
5951
|
-
if (typeof stored !== "object" || stored === null || Array.isArray(stored)) {
|
|
5952
|
-
return ["sessionCompatibility"];
|
|
5953
|
-
}
|
|
5954
|
-
const record = stored as Record<string, unknown>;
|
|
5955
|
-
const fields: readonly (keyof SessionCompatibilityContract)[] = [
|
|
5956
|
-
"modelName",
|
|
5957
|
-
"profileName",
|
|
5958
|
-
"includeReasoningContent",
|
|
5959
|
-
"contextProfile",
|
|
5960
|
-
"messageProtocol",
|
|
5961
|
-
"media",
|
|
5962
|
-
];
|
|
5963
|
-
return fields.filter((key) => {
|
|
5964
|
-
const storedValue = record[key];
|
|
5965
|
-
const currentValue = current[key];
|
|
5966
|
-
if (storedValue === undefined || currentValue === undefined) {
|
|
5967
|
-
return storedValue !== currentValue;
|
|
5968
|
-
}
|
|
5969
|
-
return stableJsonStringify(storedValue) !== stableJsonStringify(currentValue);
|
|
5970
|
-
});
|
|
5971
|
-
}
|
|
5972
|
-
|
|
5973
|
-
function decodeSessionCompatibilityContract(
|
|
5974
|
-
json: string,
|
|
5975
|
-
): SessionCompatibilityContract {
|
|
5976
|
-
const record = recordFromSql(
|
|
5977
|
-
parseJson(json, "session_compatibility_json"),
|
|
5978
|
-
"session compatibility contract",
|
|
5979
|
-
);
|
|
5980
|
-
assertObjectKeys(
|
|
5981
|
-
record,
|
|
5982
|
-
[
|
|
5983
|
-
"modelName",
|
|
5984
|
-
"profileName",
|
|
5985
|
-
"includeReasoningContent",
|
|
5986
|
-
"contextProfile",
|
|
5987
|
-
"messageProtocol",
|
|
5988
|
-
"media",
|
|
5989
|
-
],
|
|
5990
|
-
[
|
|
5991
|
-
"modelName",
|
|
5992
|
-
"includeReasoningContent",
|
|
5993
|
-
"contextProfile",
|
|
5994
|
-
"messageProtocol",
|
|
5995
|
-
"media",
|
|
5996
|
-
],
|
|
5997
|
-
"session compatibility contract",
|
|
5998
|
-
);
|
|
5999
|
-
const contextProfile = recordFromSql(
|
|
6000
|
-
record.contextProfile,
|
|
6001
|
-
"session compatibility context profile",
|
|
6002
|
-
);
|
|
6003
|
-
assertObjectKeys(
|
|
6004
|
-
contextProfile,
|
|
6005
|
-
["contextWindowTokens", "maxSupportedOutputTokens"],
|
|
6006
|
-
["contextWindowTokens", "maxSupportedOutputTokens"],
|
|
6007
|
-
"session compatibility context profile",
|
|
6008
|
-
);
|
|
6009
|
-
const messageProtocol = recordFromSql(
|
|
6010
|
-
record.messageProtocol,
|
|
6011
|
-
"session compatibility message protocol",
|
|
6012
|
-
);
|
|
6013
|
-
assertObjectKeys(
|
|
6014
|
-
messageProtocol,
|
|
6015
|
-
["adapter", "serializationVersion"],
|
|
6016
|
-
["adapter", "serializationVersion"],
|
|
6017
|
-
"session compatibility message protocol",
|
|
6018
|
-
);
|
|
6019
|
-
const media = recordFromSql(record.media, "session compatibility media");
|
|
6020
|
-
assertObjectKeys(
|
|
6021
|
-
media,
|
|
6022
|
-
["policyVersion", "policySha256", "inputModalities", "toolResultModalities"],
|
|
6023
|
-
["policyVersion", "policySha256", "inputModalities", "toolResultModalities"],
|
|
6024
|
-
"session compatibility media",
|
|
6025
|
-
);
|
|
6026
|
-
const inputModalities = decodeCompatibilityModalities(media.inputModalities, "input");
|
|
6027
|
-
const toolResultModalities = decodeCompatibilityModalities(
|
|
6028
|
-
media.toolResultModalities,
|
|
6029
|
-
"tool result",
|
|
6030
|
-
);
|
|
6031
|
-
if (toolResultModalities.includes("image") && !inputModalities.includes("image")) {
|
|
6032
|
-
throw new Error("Session compatibility image tool results require image input.");
|
|
6033
|
-
}
|
|
6034
|
-
if (typeof record.includeReasoningContent !== "boolean") {
|
|
6035
|
-
throw new Error("Session compatibility reasoning replay flag must be boolean.");
|
|
6036
|
-
}
|
|
6037
|
-
const modelName = stringFromSql(record.modelName, "compatibility modelName");
|
|
6038
|
-
const profileName =
|
|
6039
|
-
record.profileName === undefined
|
|
6040
|
-
? undefined
|
|
6041
|
-
: stringFromSql(record.profileName, "compatibility profileName");
|
|
6042
|
-
const context = createModelContextProfile({
|
|
6043
|
-
contextWindowTokens: numberFromJson(
|
|
6044
|
-
contextProfile.contextWindowTokens,
|
|
6045
|
-
"compatibility contextWindowTokens",
|
|
6046
|
-
),
|
|
6047
|
-
maxSupportedOutputTokens: numberFromJson(
|
|
6048
|
-
contextProfile.maxSupportedOutputTokens,
|
|
6049
|
-
"compatibility maxSupportedOutputTokens",
|
|
6050
|
-
),
|
|
6051
|
-
});
|
|
6052
|
-
const protocol: ModelMessageProtocol = Object.freeze({
|
|
6053
|
-
adapter: enumFromSql(
|
|
6054
|
-
messageProtocol.adapter,
|
|
6055
|
-
MODEL_MESSAGE_PROTOCOL_ADAPTERS,
|
|
6056
|
-
"compatibility message adapter",
|
|
6057
|
-
),
|
|
6058
|
-
serializationVersion: stringFromSql(
|
|
6059
|
-
messageProtocol.serializationVersion,
|
|
6060
|
-
"compatibility serializationVersion",
|
|
6061
|
-
),
|
|
6062
|
-
});
|
|
6063
|
-
const policyVersion = stringFromSql(
|
|
6064
|
-
media.policyVersion,
|
|
6065
|
-
"compatibility image policyVersion",
|
|
6066
|
-
);
|
|
6067
|
-
const policySha256 = stringFromSql(
|
|
6068
|
-
media.policySha256,
|
|
6069
|
-
"compatibility image policySha256",
|
|
6070
|
-
);
|
|
6071
|
-
if (!/^[0-9a-f]{64}$/.test(policySha256)) {
|
|
6072
|
-
throw new Error("Session image policy hash is invalid.");
|
|
6073
|
-
}
|
|
6074
|
-
return Object.freeze({
|
|
6075
|
-
modelName,
|
|
6076
|
-
...(record.profileName === undefined ? {} : { profileName: profileName! }),
|
|
6077
|
-
includeReasoningContent: record.includeReasoningContent,
|
|
6078
|
-
contextProfile: Object.freeze(context),
|
|
6079
|
-
messageProtocol: protocol,
|
|
6080
|
-
media: Object.freeze({
|
|
6081
|
-
policyVersion,
|
|
6082
|
-
policySha256,
|
|
6083
|
-
inputModalities,
|
|
6084
|
-
toolResultModalities,
|
|
6085
|
-
}),
|
|
6086
|
-
});
|
|
6087
|
-
}
|
|
6088
|
-
|
|
6089
|
-
function decodeCompatibilityModalities(
|
|
6090
|
-
value: unknown,
|
|
6091
|
-
label: string,
|
|
6092
|
-
): readonly ("text" | "image")[] {
|
|
6093
|
-
if (!Array.isArray(value)) {
|
|
6094
|
-
throw new Error(`Session compatibility ${label} modalities must be an array.`);
|
|
6095
|
-
}
|
|
6096
|
-
return normalizeInputModalities(
|
|
6097
|
-
value.map((modality) =>
|
|
6098
|
-
enumFromSql(
|
|
6099
|
-
modality,
|
|
6100
|
-
["text", "image"] as const,
|
|
6101
|
-
`compatibility ${label} modality`,
|
|
6102
|
-
),
|
|
6103
|
-
),
|
|
6104
|
-
);
|
|
3929
|
+
function previousRevision(
|
|
3930
|
+
revisions: readonly StoredContextRevisionV8[],
|
|
3931
|
+
revision: StoredContextRevisionV8,
|
|
3932
|
+
): StoredContextRevisionV8 | undefined {
|
|
3933
|
+
return revisions[revision.revisionNumber - 2];
|
|
6105
3934
|
}
|
|
6106
3935
|
|
|
6107
3936
|
function openWritableDatabase(databasePath: string): Database {
|
|
@@ -6131,465 +3960,6 @@ function runTransaction<T>(database: Database, operation: () => T): T {
|
|
|
6131
3960
|
}
|
|
6132
3961
|
}
|
|
6133
3962
|
|
|
6134
|
-
const SESSION_SCOPED_TABLES = [
|
|
6135
|
-
"session_meta",
|
|
6136
|
-
"turns",
|
|
6137
|
-
"iterations",
|
|
6138
|
-
"protocol_frames",
|
|
6139
|
-
"messages",
|
|
6140
|
-
"tool_results",
|
|
6141
|
-
"context_surfaces",
|
|
6142
|
-
"context_revisions",
|
|
6143
|
-
"context_overrides",
|
|
6144
|
-
"skill_activations",
|
|
6145
|
-
"context_measurement_state",
|
|
6146
|
-
] as const;
|
|
6147
|
-
|
|
6148
|
-
function rekeyStoredToolCalls(database: Database, targetSessionId: SessionId): void {
|
|
6149
|
-
const rows = database
|
|
6150
|
-
.query(
|
|
6151
|
-
`SELECT message_id, tool_calls_json FROM messages
|
|
6152
|
-
WHERE tool_calls_json IS NOT NULL ORDER BY ordinal`,
|
|
6153
|
-
)
|
|
6154
|
-
.all() as Array<{ message_id: string; tool_calls_json: string }>;
|
|
6155
|
-
for (const row of rows) {
|
|
6156
|
-
const calls = decodeStoredToolCalls(row.tool_calls_json).map((call) => ({
|
|
6157
|
-
...call,
|
|
6158
|
-
sessionId: targetSessionId,
|
|
6159
|
-
}));
|
|
6160
|
-
database
|
|
6161
|
-
.query("UPDATE messages SET tool_calls_json = ? WHERE message_id = ?")
|
|
6162
|
-
.run(stableJsonStringify(calls), row.message_id);
|
|
6163
|
-
}
|
|
6164
|
-
}
|
|
6165
|
-
|
|
6166
|
-
function rekeyProtocolView(
|
|
6167
|
-
source: ProtocolContextView,
|
|
6168
|
-
targetSessionId: SessionId,
|
|
6169
|
-
): ProtocolContextView {
|
|
6170
|
-
return {
|
|
6171
|
-
sessionId: targetSessionId,
|
|
6172
|
-
faulted: source.faulted,
|
|
6173
|
-
frames: source.frames.map((frame) => ({
|
|
6174
|
-
...frame,
|
|
6175
|
-
sessionId: targetSessionId,
|
|
6176
|
-
})),
|
|
6177
|
-
messages: source.messages.map((message) => ({
|
|
6178
|
-
...message,
|
|
6179
|
-
sessionId: targetSessionId,
|
|
6180
|
-
...(message.role === "assistant" && message.toolCalls !== undefined
|
|
6181
|
-
? {
|
|
6182
|
-
toolCalls: message.toolCalls.map((call) => ({
|
|
6183
|
-
...call,
|
|
6184
|
-
sessionId: targetSessionId,
|
|
6185
|
-
})),
|
|
6186
|
-
}
|
|
6187
|
-
: {}),
|
|
6188
|
-
})),
|
|
6189
|
-
toolResults: source.toolResults.map((result) => ({
|
|
6190
|
-
...result,
|
|
6191
|
-
sessionId: targetSessionId,
|
|
6192
|
-
})),
|
|
6193
|
-
};
|
|
6194
|
-
}
|
|
6195
|
-
|
|
6196
|
-
function rewriteCloneRevisionHashes(
|
|
6197
|
-
database: Database,
|
|
6198
|
-
canonical: ProtocolContextView,
|
|
6199
|
-
): void {
|
|
6200
|
-
const surfaces = database
|
|
6201
|
-
.query("SELECT * FROM context_surfaces")
|
|
6202
|
-
.all()
|
|
6203
|
-
.map(decodeContextSurface);
|
|
6204
|
-
const surfacesById = new Map(surfaces.map((surface) => [surface.surfaceId, surface]));
|
|
6205
|
-
const revisions = database
|
|
6206
|
-
.query("SELECT * FROM context_revisions ORDER BY revision_number")
|
|
6207
|
-
.all()
|
|
6208
|
-
.map(decodeContextRevision);
|
|
6209
|
-
const revisionNumberById = new Map(
|
|
6210
|
-
revisions.map((revision) => [revision.revisionId, revision.revisionNumber]),
|
|
6211
|
-
);
|
|
6212
|
-
const overrides = database
|
|
6213
|
-
.query(
|
|
6214
|
-
`SELECT co.* FROM context_overrides co
|
|
6215
|
-
JOIN context_revisions cr ON cr.revision_id = co.introduced_revision_id
|
|
6216
|
-
ORDER BY cr.revision_number, co.ordinal`,
|
|
6217
|
-
)
|
|
6218
|
-
.all()
|
|
6219
|
-
.map(decodeStoredSwapOverride);
|
|
6220
|
-
const compiler = new ContextRevisionCompiler();
|
|
6221
|
-
for (const revision of revisions) {
|
|
6222
|
-
const surface = surfacesById.get(revision.surfaceId);
|
|
6223
|
-
if (surface === undefined) {
|
|
6224
|
-
throw new Error(`Cloned revision ${revision.revisionId} has no surface.`);
|
|
6225
|
-
}
|
|
6226
|
-
const activeOverrides = overrides.filter(
|
|
6227
|
-
(override) =>
|
|
6228
|
-
(revisionNumberById.get(override.introducedRevisionId) ??
|
|
6229
|
-
Number.POSITIVE_INFINITY) <= revision.revisionNumber &&
|
|
6230
|
-
override.ordinal >= revision.keepFromOrdinal,
|
|
6231
|
-
);
|
|
6232
|
-
const prefix = protocolPrefixView(canonical, revision.sourceThroughOrdinal);
|
|
6233
|
-
const compiled = compiler.compileForIdentityRekey({
|
|
6234
|
-
canonical: prefix,
|
|
6235
|
-
revisionId: revision.revisionId,
|
|
6236
|
-
activeOverrides,
|
|
6237
|
-
keepFromOrdinal: revision.keepFromOrdinal,
|
|
6238
|
-
surface,
|
|
6239
|
-
});
|
|
6240
|
-
database
|
|
6241
|
-
.query(
|
|
6242
|
-
`UPDATE context_revisions
|
|
6243
|
-
SET canonical_sequence_sha256 = ?, rendered_message_sha256 = ?
|
|
6244
|
-
WHERE revision_id = ?`,
|
|
6245
|
-
)
|
|
6246
|
-
.run(
|
|
6247
|
-
canonicalSequenceHash(canonical, revision.sourceThroughOrdinal),
|
|
6248
|
-
renderedMessageHash(compiled.entries, revision.sourceThroughOrdinal),
|
|
6249
|
-
revision.revisionId,
|
|
6250
|
-
);
|
|
6251
|
-
}
|
|
6252
|
-
}
|
|
6253
|
-
|
|
6254
|
-
async function cloneDiagnosticFiles(input: {
|
|
6255
|
-
sourceDirectory: string;
|
|
6256
|
-
stagingDirectory: string;
|
|
6257
|
-
sourceSessionId: SessionId;
|
|
6258
|
-
targetSessionId: SessionId;
|
|
6259
|
-
nextEventSequence: number;
|
|
6260
|
-
faultInjector?: (stage: CloneSessionFaultStage) => void;
|
|
6261
|
-
}): Promise<void> {
|
|
6262
|
-
const sourcePath = path.join(input.sourceDirectory, "events.jsonl");
|
|
6263
|
-
try {
|
|
6264
|
-
await lstat(sourcePath);
|
|
6265
|
-
} catch (error) {
|
|
6266
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
6267
|
-
return;
|
|
6268
|
-
}
|
|
6269
|
-
throw error;
|
|
6270
|
-
}
|
|
6271
|
-
await validateSecureFile(sourcePath, input.sourceSessionId);
|
|
6272
|
-
|
|
6273
|
-
const bytes = await readFile(sourcePath);
|
|
6274
|
-
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
6275
|
-
const rawLines = text.split("\n");
|
|
6276
|
-
if (rawLines.at(-1) === "") {
|
|
6277
|
-
rawLines.pop();
|
|
6278
|
-
}
|
|
6279
|
-
const events: AgentEvent[] = [];
|
|
6280
|
-
let previousSequence = 0;
|
|
6281
|
-
for (const [index, line] of rawLines.entries()) {
|
|
6282
|
-
if (line === "") {
|
|
6283
|
-
throw new Error(`Session event log contains an empty line at ${index + 1}.`);
|
|
6284
|
-
}
|
|
6285
|
-
let value: unknown;
|
|
6286
|
-
try {
|
|
6287
|
-
value = JSON.parse(line);
|
|
6288
|
-
} catch (error) {
|
|
6289
|
-
throw new Error(`Session event log has invalid JSON at line ${index + 1}.`, {
|
|
6290
|
-
cause: error,
|
|
6291
|
-
});
|
|
6292
|
-
}
|
|
6293
|
-
if (!isEventEnvelope(value)) {
|
|
6294
|
-
throw new Error(
|
|
6295
|
-
`Session event log has an invalid envelope at line ${index + 1}.`,
|
|
6296
|
-
);
|
|
6297
|
-
}
|
|
6298
|
-
if (value.sessionId !== input.sourceSessionId) {
|
|
6299
|
-
throw new Error(`Session event log identity changed at line ${index + 1}.`);
|
|
6300
|
-
}
|
|
6301
|
-
if (value.eventSequence <= previousSequence) {
|
|
6302
|
-
throw new Error(
|
|
6303
|
-
`Session event sequence is not strictly increasing at line ${index + 1}.`,
|
|
6304
|
-
);
|
|
6305
|
-
}
|
|
6306
|
-
if (value.eventSequence >= input.nextEventSequence) {
|
|
6307
|
-
throw new Error(
|
|
6308
|
-
`Session event sequence exceeds the canonical next counter at line ${index + 1}.`,
|
|
6309
|
-
);
|
|
6310
|
-
}
|
|
6311
|
-
previousSequence = value.eventSequence;
|
|
6312
|
-
events.push({ ...value, sessionId: input.targetSessionId });
|
|
6313
|
-
}
|
|
6314
|
-
|
|
6315
|
-
const eventText = events.map((event) => JSON.stringify(event)).join("\n");
|
|
6316
|
-
await writePrivateNewFile(
|
|
6317
|
-
path.join(input.stagingDirectory, "events.jsonl"),
|
|
6318
|
-
eventText === "" ? "" : `${eventText}\n`,
|
|
6319
|
-
);
|
|
6320
|
-
input.faultInjector?.("after_event_rewrite");
|
|
6321
|
-
const observationText = events
|
|
6322
|
-
.map((event) => renderObservationLogEvent(event))
|
|
6323
|
-
.filter((block): block is string => block !== undefined)
|
|
6324
|
-
.join("");
|
|
6325
|
-
await writePrivateNewFile(
|
|
6326
|
-
path.join(input.stagingDirectory, "observations.md"),
|
|
6327
|
-
observationText,
|
|
6328
|
-
);
|
|
6329
|
-
input.faultInjector?.("after_observation_render");
|
|
6330
|
-
}
|
|
6331
|
-
|
|
6332
|
-
function isEventEnvelope(value: unknown): value is AgentEvent {
|
|
6333
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
6334
|
-
return false;
|
|
6335
|
-
}
|
|
6336
|
-
const record = value as Record<string, unknown>;
|
|
6337
|
-
return (
|
|
6338
|
-
typeof record.sessionId === "string" &&
|
|
6339
|
-
Number.isSafeInteger(record.eventSequence) &&
|
|
6340
|
-
Number(record.eventSequence) >= 1 &&
|
|
6341
|
-
typeof record.timestamp === "string" &&
|
|
6342
|
-
typeof record.type === "string" &&
|
|
6343
|
-
record.data !== null &&
|
|
6344
|
-
typeof record.data === "object"
|
|
6345
|
-
);
|
|
6346
|
-
}
|
|
6347
|
-
|
|
6348
|
-
async function writePrivateNewFile(filePath: string, content: string): Promise<void> {
|
|
6349
|
-
const handle = await open(filePath, "wx", 0o600);
|
|
6350
|
-
try {
|
|
6351
|
-
await handle.writeFile(content, "utf8");
|
|
6352
|
-
} finally {
|
|
6353
|
-
await handle.close();
|
|
6354
|
-
}
|
|
6355
|
-
await chmod(filePath, 0o600);
|
|
6356
|
-
}
|
|
6357
|
-
|
|
6358
|
-
async function assertPathMissing(
|
|
6359
|
-
filePath: string,
|
|
6360
|
-
sessionId: SessionId,
|
|
6361
|
-
): Promise<void> {
|
|
6362
|
-
try {
|
|
6363
|
-
await lstat(filePath);
|
|
6364
|
-
} catch (error) {
|
|
6365
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
6366
|
-
return;
|
|
6367
|
-
}
|
|
6368
|
-
throw error;
|
|
6369
|
-
}
|
|
6370
|
-
throw new SessionError(
|
|
6371
|
-
"SESSION_ALREADY_EXISTS",
|
|
6372
|
-
"clone_session",
|
|
6373
|
-
`Session directory already exists: ${filePath}.`,
|
|
6374
|
-
{ sessionId },
|
|
6375
|
-
);
|
|
6376
|
-
}
|
|
6377
|
-
|
|
6378
|
-
async function canonicalWorkspaceRoot(workspaceRoot: string): Promise<string> {
|
|
6379
|
-
if (!path.isAbsolute(workspaceRoot)) {
|
|
6380
|
-
throw new Error("Session workspace root must be absolute.");
|
|
6381
|
-
}
|
|
6382
|
-
return realpath(workspaceRoot);
|
|
6383
|
-
}
|
|
6384
|
-
|
|
6385
|
-
async function ensureSessionsRoot(
|
|
6386
|
-
workspaceRoot: string,
|
|
6387
|
-
homeRoot?: string,
|
|
6388
|
-
): Promise<string> {
|
|
6389
|
-
const tinkerRoot = workspaceStorageRoot(
|
|
6390
|
-
workspaceRoot,
|
|
6391
|
-
await canonicalHomeRoot(homeRoot),
|
|
6392
|
-
);
|
|
6393
|
-
const sessionsRoot = path.join(tinkerRoot, "sessions");
|
|
6394
|
-
await mkdir(sessionsRoot, { recursive: true, mode: 0o700 });
|
|
6395
|
-
await validateSessionsRoot(sessionsRoot);
|
|
6396
|
-
await chmod(tinkerRoot, 0o700);
|
|
6397
|
-
await chmod(sessionsRoot, 0o700);
|
|
6398
|
-
return sessionsRoot;
|
|
6399
|
-
}
|
|
6400
|
-
|
|
6401
|
-
async function validateSessionsRoot(
|
|
6402
|
-
sessionsRoot: string,
|
|
6403
|
-
sessionId?: SessionId,
|
|
6404
|
-
): Promise<void> {
|
|
6405
|
-
const tinkerRoot = path.dirname(sessionsRoot);
|
|
6406
|
-
for (const directory of [tinkerRoot, sessionsRoot]) {
|
|
6407
|
-
let stats;
|
|
6408
|
-
try {
|
|
6409
|
-
stats = await lstat(directory);
|
|
6410
|
-
} catch (error) {
|
|
6411
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
6412
|
-
throw new SessionError(
|
|
6413
|
-
"SESSION_STORE_NOT_FOUND",
|
|
6414
|
-
"validate_session_root",
|
|
6415
|
-
`Session root does not exist: ${directory}.`,
|
|
6416
|
-
{ sessionId, cause: error },
|
|
6417
|
-
);
|
|
6418
|
-
}
|
|
6419
|
-
throw error;
|
|
6420
|
-
}
|
|
6421
|
-
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
6422
|
-
throw new SessionError(
|
|
6423
|
-
"SESSION_PERMISSION_INVALID",
|
|
6424
|
-
"validate_session_root",
|
|
6425
|
-
`Session root must not be a symlink: ${directory}.`,
|
|
6426
|
-
{ sessionId },
|
|
6427
|
-
);
|
|
6428
|
-
}
|
|
6429
|
-
}
|
|
6430
|
-
if ((await realpath(sessionsRoot)) !== sessionsRoot) {
|
|
6431
|
-
throw new SessionError(
|
|
6432
|
-
"SESSION_PERMISSION_INVALID",
|
|
6433
|
-
"validate_session_root",
|
|
6434
|
-
`Session root resolves outside its canonical path: ${sessionsRoot}.`,
|
|
6435
|
-
{ sessionId },
|
|
6436
|
-
);
|
|
6437
|
-
}
|
|
6438
|
-
}
|
|
6439
|
-
|
|
6440
|
-
function safeSessionDirectory(sessionsRoot: string, sessionId: SessionId): string {
|
|
6441
|
-
const value = String(sessionId);
|
|
6442
|
-
if (
|
|
6443
|
-
value.trim() === "" ||
|
|
6444
|
-
value !== value.trim() ||
|
|
6445
|
-
value.includes("/") ||
|
|
6446
|
-
value.includes("\\") ||
|
|
6447
|
-
value === "." ||
|
|
6448
|
-
value === ".."
|
|
6449
|
-
) {
|
|
6450
|
-
throw new SessionError(
|
|
6451
|
-
"SESSION_ID_INVALID",
|
|
6452
|
-
"resolve_session_path",
|
|
6453
|
-
`Unsafe session ID: ${JSON.stringify(value)}.`,
|
|
6454
|
-
{ sessionId },
|
|
6455
|
-
);
|
|
6456
|
-
}
|
|
6457
|
-
const directory = path.join(sessionsRoot, value);
|
|
6458
|
-
if (path.dirname(directory) !== sessionsRoot) {
|
|
6459
|
-
throw new SessionError(
|
|
6460
|
-
"SESSION_ID_INVALID",
|
|
6461
|
-
"resolve_session_path",
|
|
6462
|
-
`Session ID escapes the sessions directory: ${JSON.stringify(value)}.`,
|
|
6463
|
-
{ sessionId },
|
|
6464
|
-
);
|
|
6465
|
-
}
|
|
6466
|
-
return directory;
|
|
6467
|
-
}
|
|
6468
|
-
|
|
6469
|
-
async function validateSecureDirectory(
|
|
6470
|
-
directory: string,
|
|
6471
|
-
sessionId: SessionId,
|
|
6472
|
-
): Promise<void> {
|
|
6473
|
-
let stats;
|
|
6474
|
-
try {
|
|
6475
|
-
stats = await lstat(directory);
|
|
6476
|
-
} catch (error) {
|
|
6477
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
6478
|
-
throw new SessionError(
|
|
6479
|
-
"SESSION_STORE_NOT_FOUND",
|
|
6480
|
-
"open_session",
|
|
6481
|
-
`Session directory does not exist: ${directory}.`,
|
|
6482
|
-
{ sessionId, cause: error },
|
|
6483
|
-
);
|
|
6484
|
-
}
|
|
6485
|
-
throw error;
|
|
6486
|
-
}
|
|
6487
|
-
if (
|
|
6488
|
-
!stats.isDirectory() ||
|
|
6489
|
-
stats.isSymbolicLink() ||
|
|
6490
|
-
(stats.mode & 0o077) !== 0 ||
|
|
6491
|
-
(typeof process.getuid === "function" && stats.uid !== process.getuid())
|
|
6492
|
-
) {
|
|
6493
|
-
throw new SessionError(
|
|
6494
|
-
"SESSION_PERMISSION_INVALID",
|
|
6495
|
-
"open_session",
|
|
6496
|
-
`Session directory must be an owner-only real directory: ${directory}.`,
|
|
6497
|
-
{ sessionId },
|
|
6498
|
-
);
|
|
6499
|
-
}
|
|
6500
|
-
}
|
|
6501
|
-
|
|
6502
|
-
async function validateSecureFile(
|
|
6503
|
-
filePath: string,
|
|
6504
|
-
sessionId: SessionId,
|
|
6505
|
-
): Promise<void> {
|
|
6506
|
-
let stats;
|
|
6507
|
-
try {
|
|
6508
|
-
stats = await lstat(filePath);
|
|
6509
|
-
} catch (error) {
|
|
6510
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
6511
|
-
throw new SessionError(
|
|
6512
|
-
"SESSION_STORE_NOT_FOUND",
|
|
6513
|
-
"open_session",
|
|
6514
|
-
`Session database does not exist: ${filePath}.`,
|
|
6515
|
-
{ sessionId, cause: error },
|
|
6516
|
-
);
|
|
6517
|
-
}
|
|
6518
|
-
throw error;
|
|
6519
|
-
}
|
|
6520
|
-
if (
|
|
6521
|
-
!stats.isFile() ||
|
|
6522
|
-
stats.isSymbolicLink() ||
|
|
6523
|
-
(stats.mode & 0o077) !== 0 ||
|
|
6524
|
-
(typeof process.getuid === "function" && stats.uid !== process.getuid())
|
|
6525
|
-
) {
|
|
6526
|
-
throw new SessionError(
|
|
6527
|
-
"SESSION_PERMISSION_INVALID",
|
|
6528
|
-
"open_session",
|
|
6529
|
-
`Session database must be an owner-only regular file: ${filePath}.`,
|
|
6530
|
-
{ sessionId },
|
|
6531
|
-
);
|
|
6532
|
-
}
|
|
6533
|
-
}
|
|
6534
|
-
|
|
6535
|
-
async function validateSecureOptionalFile(
|
|
6536
|
-
filePath: string,
|
|
6537
|
-
sessionId: SessionId,
|
|
6538
|
-
): Promise<void> {
|
|
6539
|
-
try {
|
|
6540
|
-
await lstat(filePath);
|
|
6541
|
-
} catch (error) {
|
|
6542
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
6543
|
-
return;
|
|
6544
|
-
}
|
|
6545
|
-
throw error;
|
|
6546
|
-
}
|
|
6547
|
-
await validateSecureFile(filePath, sessionId);
|
|
6548
|
-
}
|
|
6549
|
-
|
|
6550
|
-
async function removeKnownInitializationFiles(sessionDirectory: string): Promise<void> {
|
|
6551
|
-
for (const name of [
|
|
6552
|
-
"session.sqlite-wal",
|
|
6553
|
-
"session.sqlite-shm",
|
|
6554
|
-
"session.sqlite",
|
|
6555
|
-
"events.jsonl",
|
|
6556
|
-
"observations.md",
|
|
6557
|
-
"active.lock.reclaim",
|
|
6558
|
-
"active.lock",
|
|
6559
|
-
]) {
|
|
6560
|
-
await unlinkIfExists(path.join(sessionDirectory, name));
|
|
6561
|
-
}
|
|
6562
|
-
try {
|
|
6563
|
-
await rmdir(sessionDirectory);
|
|
6564
|
-
} catch (error) {
|
|
6565
|
-
if (
|
|
6566
|
-
!new Set(["ENOENT", "ENOTEMPTY"]).has((error as NodeJS.ErrnoException).code ?? "")
|
|
6567
|
-
) {
|
|
6568
|
-
throw error;
|
|
6569
|
-
}
|
|
6570
|
-
}
|
|
6571
|
-
}
|
|
6572
|
-
|
|
6573
|
-
async function chmodIfExists(filePath: string, mode: number): Promise<void> {
|
|
6574
|
-
try {
|
|
6575
|
-
await chmod(filePath, mode);
|
|
6576
|
-
} catch (error) {
|
|
6577
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
6578
|
-
throw error;
|
|
6579
|
-
}
|
|
6580
|
-
}
|
|
6581
|
-
}
|
|
6582
|
-
|
|
6583
|
-
async function unlinkIfExists(filePath: string): Promise<void> {
|
|
6584
|
-
try {
|
|
6585
|
-
await unlink(filePath);
|
|
6586
|
-
} catch (error) {
|
|
6587
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
6588
|
-
throw error;
|
|
6589
|
-
}
|
|
6590
|
-
}
|
|
6591
|
-
}
|
|
6592
|
-
|
|
6593
3963
|
function requireSingleChange(
|
|
6594
3964
|
database: Database,
|
|
6595
3965
|
reportedChanges: number | bigint,
|
|
@@ -6605,161 +3975,6 @@ function requireSingleChange(
|
|
|
6605
3975
|
}
|
|
6606
3976
|
}
|
|
6607
3977
|
|
|
6608
|
-
function recordFromSql(value: unknown, name: string): Record<string, unknown> {
|
|
6609
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
6610
|
-
throw new Error(`${name} must be an object.`);
|
|
6611
|
-
}
|
|
6612
|
-
return value as Record<string, unknown>;
|
|
6613
|
-
}
|
|
6614
|
-
|
|
6615
|
-
function assertObjectKeys(
|
|
6616
|
-
record: Record<string, unknown>,
|
|
6617
|
-
allowed: readonly string[],
|
|
6618
|
-
required: readonly string[],
|
|
6619
|
-
name: string,
|
|
6620
|
-
): void {
|
|
6621
|
-
const allowedSet = new Set(allowed);
|
|
6622
|
-
const unknown = Object.keys(record).filter((key) => !allowedSet.has(key));
|
|
6623
|
-
const missing = required.filter((key) => !(key in record));
|
|
6624
|
-
if (unknown.length > 0 || missing.length > 0) {
|
|
6625
|
-
throw new Error(
|
|
6626
|
-
`${name} has invalid keys; unknown=${unknown.join(",") || "none"} missing=${missing.join(",") || "none"}.`,
|
|
6627
|
-
);
|
|
6628
|
-
}
|
|
6629
|
-
}
|
|
6630
|
-
|
|
6631
|
-
function stringFromSql(value: unknown, name: string): string {
|
|
6632
|
-
if (typeof value !== "string" || value === "") {
|
|
6633
|
-
throw new Error(`${name} must be a non-empty string.`);
|
|
6634
|
-
}
|
|
6635
|
-
return value;
|
|
6636
|
-
}
|
|
6637
|
-
|
|
6638
|
-
function sha256FromSql(value: unknown, name: string): string {
|
|
6639
|
-
const hash = stringFromSql(value, name);
|
|
6640
|
-
if (!/^[0-9a-f]{64}$/.test(hash)) {
|
|
6641
|
-
throw new Error(`${name} must be a lowercase SHA-256 digest.`);
|
|
6642
|
-
}
|
|
6643
|
-
return hash;
|
|
6644
|
-
}
|
|
6645
|
-
|
|
6646
|
-
function assertMeasuredContextAnchor(anchor: MeasuredContextAnchor): void {
|
|
6647
|
-
for (const [name, value] of [
|
|
6648
|
-
["promptTokens", anchor.promptTokens],
|
|
6649
|
-
["completionTokens", anchor.completionTokens],
|
|
6650
|
-
["totalTokens", anchor.totalTokens],
|
|
6651
|
-
["segmentCount", anchor.segmentCount],
|
|
6652
|
-
] as const) {
|
|
6653
|
-
if (!Number.isSafeInteger(value) || value < 0) {
|
|
6654
|
-
throw new Error(
|
|
6655
|
-
`Measured context anchor ${name} must be a non-negative safe integer; received ${value}.`,
|
|
6656
|
-
);
|
|
6657
|
-
}
|
|
6658
|
-
}
|
|
6659
|
-
if (anchor.totalTokens !== anchor.promptTokens + anchor.completionTokens) {
|
|
6660
|
-
throw new Error(
|
|
6661
|
-
"Measured context anchor totalTokens must equal promptTokens + completionTokens.",
|
|
6662
|
-
);
|
|
6663
|
-
}
|
|
6664
|
-
for (const [name, value] of [
|
|
6665
|
-
["prefixHash", anchor.prefixHash],
|
|
6666
|
-
["requestConfigHash", anchor.requestConfigHash],
|
|
6667
|
-
["toolSchemaHash", anchor.toolSchemaHash],
|
|
6668
|
-
] as const) {
|
|
6669
|
-
if (!/^[0-9a-f]{64}$/.test(value)) {
|
|
6670
|
-
throw new Error(`Measured context anchor ${name} must be a SHA-256 digest.`);
|
|
6671
|
-
}
|
|
6672
|
-
}
|
|
6673
|
-
}
|
|
6674
|
-
|
|
6675
|
-
function nullableStringFromSql(value: unknown, name: string): string | null {
|
|
6676
|
-
return value === null ? null : stringFromSql(value, name);
|
|
6677
|
-
}
|
|
6678
|
-
|
|
6679
|
-
function nullableTextFromSql(value: unknown, name: string): string | null {
|
|
6680
|
-
if (value === null) {
|
|
6681
|
-
return null;
|
|
6682
|
-
}
|
|
6683
|
-
if (typeof value !== "string") {
|
|
6684
|
-
throw new Error(`${name} must be a string or null.`);
|
|
6685
|
-
}
|
|
6686
|
-
return value;
|
|
6687
|
-
}
|
|
6688
|
-
|
|
6689
|
-
function numberFromSql(value: unknown, name: string): number {
|
|
6690
|
-
const number = typeof value === "bigint" ? Number(value) : value;
|
|
6691
|
-
if (typeof number !== "number" || !Number.isSafeInteger(number) || number < 0) {
|
|
6692
|
-
throw new Error(`${name} must be a safe non-negative integer.`);
|
|
6693
|
-
}
|
|
6694
|
-
return number;
|
|
6695
|
-
}
|
|
6696
|
-
|
|
6697
|
-
function nullableNumberFromSql(value: unknown, name: string): number | null {
|
|
6698
|
-
return value === null ? null : numberFromSql(value, name);
|
|
6699
|
-
}
|
|
6700
|
-
|
|
6701
|
-
function numberFromJson(value: unknown, name: string): number {
|
|
6702
|
-
if (!Number.isSafeInteger(value) || (value as number) < 1) {
|
|
6703
|
-
throw new Error(`${name} must be a positive safe integer.`);
|
|
6704
|
-
}
|
|
6705
|
-
return value as number;
|
|
6706
|
-
}
|
|
6707
|
-
|
|
6708
|
-
function safeJsonInteger(value: unknown, name: string): number {
|
|
6709
|
-
if (!Number.isSafeInteger(value)) {
|
|
6710
|
-
throw new Error(`${name} must be a safe integer.`);
|
|
6711
|
-
}
|
|
6712
|
-
return value as number;
|
|
6713
|
-
}
|
|
6714
|
-
|
|
6715
|
-
function nonNegativeJsonInteger(value: unknown, name: string): number {
|
|
6716
|
-
const number = safeJsonInteger(value, name);
|
|
6717
|
-
if (number < 0) throw new Error(`${name} must be non-negative.`);
|
|
6718
|
-
return number;
|
|
6719
|
-
}
|
|
6720
|
-
|
|
6721
|
-
function positiveJsonInteger(value: unknown, name: string): number {
|
|
6722
|
-
const number = safeJsonInteger(value, name);
|
|
6723
|
-
if (number < 1) throw new Error(`${name} must be positive.`);
|
|
6724
|
-
return number;
|
|
6725
|
-
}
|
|
6726
|
-
|
|
6727
|
-
function nonEmptyStringFromJson(value: unknown, name: string): string {
|
|
6728
|
-
const text = stringFromSql(value, name);
|
|
6729
|
-
if (text.trim() === "") throw new Error(`${name} must not be empty.`);
|
|
6730
|
-
return text;
|
|
6731
|
-
}
|
|
6732
|
-
|
|
6733
|
-
function enumFromSql<const T extends readonly string[]>(
|
|
6734
|
-
value: unknown,
|
|
6735
|
-
values: T,
|
|
6736
|
-
name: string,
|
|
6737
|
-
): T[number] {
|
|
6738
|
-
if (typeof value !== "string" || !values.includes(value)) {
|
|
6739
|
-
throw new Error(`${name} has unsupported value ${JSON.stringify(value)}.`);
|
|
6740
|
-
}
|
|
6741
|
-
return value;
|
|
6742
|
-
}
|
|
6743
|
-
|
|
6744
|
-
function timestampFromSql(value: unknown, name: string): string {
|
|
6745
|
-
return timestampValue(stringFromSql(value, name), name);
|
|
6746
|
-
}
|
|
6747
|
-
|
|
6748
|
-
function timestampValue(value: string, name: string): string {
|
|
6749
|
-
if (Number.isNaN(Date.parse(value)) || !value.endsWith("Z")) {
|
|
6750
|
-
throw new Error(`${name} must be a UTC ISO-8601 timestamp.`);
|
|
6751
|
-
}
|
|
6752
|
-
return value;
|
|
6753
|
-
}
|
|
6754
|
-
|
|
6755
|
-
function parseJson(value: string, name: string): unknown {
|
|
6756
|
-
try {
|
|
6757
|
-
return JSON.parse(value);
|
|
6758
|
-
} catch (error) {
|
|
6759
|
-
throw new Error(`${name} is not valid JSON.`, { cause: error });
|
|
6760
|
-
}
|
|
6761
|
-
}
|
|
6762
|
-
|
|
6763
3978
|
function requireItem<T>(items: readonly T[], index: number, name: string): T {
|
|
6764
3979
|
const item = items[index];
|
|
6765
3980
|
if (item === undefined) {
|
|
@@ -6768,10 +3983,6 @@ function requireItem<T>(items: readonly T[], index: number, name: string): T {
|
|
|
6768
3983
|
return item;
|
|
6769
3984
|
}
|
|
6770
3985
|
|
|
6771
|
-
function compareCanonicalText(left: string, right: string): number {
|
|
6772
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
6773
|
-
}
|
|
6774
|
-
|
|
6775
3986
|
function errorMessage(error: unknown): string {
|
|
6776
3987
|
return error instanceof Error ? error.message : String(error);
|
|
6777
3988
|
}
|