tinker-agent 1.0.65 → 1.2.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 +36 -0
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +90 -9
- package/bin/tinker.js +39 -0
- package/node_modules/ansi-regex/index.d.ts +33 -0
- package/node_modules/ansi-regex/index.js +14 -0
- package/node_modules/ansi-regex/license +9 -0
- package/node_modules/ansi-regex/package.json +61 -0
- package/node_modules/ansi-regex/readme.md +66 -0
- package/node_modules/ansi-styles/index.d.ts +236 -0
- package/node_modules/ansi-styles/index.js +223 -0
- package/node_modules/ansi-styles/license +9 -0
- package/node_modules/ansi-styles/package.json +54 -0
- package/node_modules/ansi-styles/readme.md +173 -0
- package/node_modules/chalk/license +9 -0
- package/node_modules/chalk/package.json +83 -0
- package/node_modules/chalk/readme.md +297 -0
- package/node_modules/chalk/source/index.d.ts +325 -0
- package/node_modules/chalk/source/index.js +225 -0
- package/node_modules/chalk/source/utilities.js +33 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/node_modules/character-entities/index.d.ts +6 -0
- package/node_modules/character-entities/index.js +2132 -0
- package/node_modules/character-entities/license +22 -0
- package/node_modules/character-entities/package.json +78 -0
- package/node_modules/character-entities/readme.md +152 -0
- package/node_modules/decode-named-character-reference/index.d.ts +13 -0
- package/node_modules/decode-named-character-reference/index.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts +6 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.js +32 -0
- package/node_modules/decode-named-character-reference/index.js +19 -0
- package/node_modules/decode-named-character-reference/license +22 -0
- package/node_modules/decode-named-character-reference/package.json +90 -0
- package/node_modules/decode-named-character-reference/readme.md +136 -0
- package/node_modules/get-east-asian-width/index.d.ts +60 -0
- package/node_modules/get-east-asian-width/index.js +30 -0
- package/node_modules/get-east-asian-width/license +9 -0
- package/node_modules/get-east-asian-width/lookup-data.js +21 -0
- package/node_modules/get-east-asian-width/lookup.js +138 -0
- package/node_modules/get-east-asian-width/package.json +71 -0
- package/node_modules/get-east-asian-width/readme.md +65 -0
- package/node_modules/get-east-asian-width/utilities.js +24 -0
- package/node_modules/has-flag/index.d.ts +39 -0
- package/node_modules/has-flag/index.js +8 -0
- package/node_modules/has-flag/license +9 -0
- package/node_modules/has-flag/package.json +49 -0
- package/node_modules/has-flag/readme.md +74 -0
- package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
- package/node_modules/is-fullwidth-code-point/index.js +12 -0
- package/node_modules/is-fullwidth-code-point/license +9 -0
- package/node_modules/is-fullwidth-code-point/package.json +53 -0
- package/node_modules/is-fullwidth-code-point/readme.md +31 -0
- package/node_modules/markdansi/LICENSE +21 -0
- package/node_modules/markdansi/README.md +167 -0
- package/node_modules/markdansi/dist/ast.d.ts +99 -0
- package/node_modules/markdansi/dist/ast.js +1 -0
- package/node_modules/markdansi/dist/cli.d.ts +17 -0
- package/node_modules/markdansi/dist/cli.js +208 -0
- package/node_modules/markdansi/dist/hyperlink.d.ts +9 -0
- package/node_modules/markdansi/dist/hyperlink.js +26 -0
- package/node_modules/markdansi/dist/index.d.ts +10 -0
- package/node_modules/markdansi/dist/index.js +16 -0
- package/node_modules/markdansi/dist/parser.d.ts +2 -0
- package/node_modules/markdansi/dist/parser.js +205 -0
- package/node_modules/markdansi/dist/render.d.ts +9 -0
- package/node_modules/markdansi/dist/render.js +680 -0
- package/node_modules/markdansi/dist/stream.d.ts +39 -0
- package/node_modules/markdansi/dist/stream.js +192 -0
- package/node_modules/markdansi/dist/theme.d.ts +18 -0
- package/node_modules/markdansi/dist/theme.js +105 -0
- package/node_modules/markdansi/dist/types.d.ts +58 -0
- package/node_modules/markdansi/dist/types.js +1 -0
- package/node_modules/markdansi/dist/wrap.d.ts +10 -0
- package/node_modules/markdansi/dist/wrap.js +73 -0
- package/node_modules/markdansi/docs/spec.md +110 -0
- package/node_modules/markdansi/package.json +78 -0
- package/node_modules/markdansi/tsconfig.json +18 -0
- package/node_modules/marked/LICENSE +44 -0
- package/node_modules/marked/README.md +115 -0
- package/node_modules/marked/bin/main.js +284 -0
- package/node_modules/marked/bin/marked.js +16 -0
- package/node_modules/marked/lib/marked.d.ts +760 -0
- package/node_modules/marked/lib/marked.esm.js +77 -0
- package/node_modules/marked/lib/marked.esm.js.map +7 -0
- package/node_modules/marked/lib/marked.umd.js +79 -0
- package/node_modules/marked/lib/marked.umd.js.map +7 -0
- package/node_modules/marked/man/marked.1 +113 -0
- package/node_modules/marked/man/marked.1.md +93 -0
- package/node_modules/marked/package.json +103 -0
- package/node_modules/slice-ansi/index.d.ts +19 -0
- package/node_modules/slice-ansi/index.js +317 -0
- package/node_modules/slice-ansi/license +10 -0
- package/node_modules/slice-ansi/package.json +59 -0
- package/node_modules/slice-ansi/readme.md +55 -0
- package/node_modules/slice-ansi/tokenize-ansi.js +752 -0
- package/node_modules/string-width/index.d.ts +39 -0
- package/node_modules/string-width/index.js +203 -0
- package/node_modules/string-width/license +9 -0
- package/node_modules/string-width/package.json +65 -0
- package/node_modules/string-width/readme.md +66 -0
- package/node_modules/strip-ansi/index.d.ts +15 -0
- package/node_modules/strip-ansi/index.js +19 -0
- package/node_modules/strip-ansi/license +9 -0
- package/node_modules/strip-ansi/package.json +59 -0
- package/node_modules/strip-ansi/readme.md +37 -0
- package/node_modules/supports-color/browser.d.ts +1 -0
- package/node_modules/supports-color/browser.js +35 -0
- package/node_modules/supports-color/index.d.ts +55 -0
- package/node_modules/supports-color/index.js +202 -0
- package/node_modules/supports-color/license +9 -0
- package/node_modules/supports-color/package.json +64 -0
- package/node_modules/supports-color/readme.md +75 -0
- package/node_modules/supports-hyperlinks/browser.js +10 -0
- package/node_modules/supports-hyperlinks/index.d.ts +25 -0
- package/node_modules/supports-hyperlinks/index.js +163 -0
- package/node_modules/supports-hyperlinks/license +10 -0
- package/node_modules/supports-hyperlinks/package.json +47 -0
- package/node_modules/supports-hyperlinks/readme.md +45 -0
- package/package.json +41 -10
- package/patches/markdansi@0.3.2.patch +22 -2
- package/src/agent/context-builder.ts +24 -10
- package/src/agent/context-meter.ts +89 -5
- package/src/agent/loop.ts +107 -10
- package/src/agent/runtime-session.ts +1437 -82
- package/src/agent/session-ledger.ts +169 -29
- package/src/agent/types.ts +12 -1
- package/src/agent/user-prompt-projection.ts +62 -0
- package/src/cli/config.ts +57 -14
- package/src/cli/model-profiles.ts +124 -0
- package/src/cli/run-runner.ts +4 -1
- package/src/cli/tui-runner.tsx +68 -30
- package/src/context/compiled-context-hash.ts +1 -1
- package/src/context/compiled-context-validator.ts +92 -15
- package/src/context/context-automation-policy.ts +121 -0
- package/src/context/context-manager.ts +390 -11
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +11 -1
- package/src/context/context-revision-compiler.ts +190 -27
- package/src/context/context-revision.ts +105 -19
- package/src/context/context-surface.ts +329 -0
- package/src/context/prefix-retirement-planner.ts +570 -0
- package/src/context/protocol-frame.ts +30 -3
- package/src/context/recall-retirement-contract.ts +27 -0
- package/src/context/swap-planner.ts +34 -10
- package/src/events/observation-text-log.ts +3 -3
- package/src/events/stdout-event-printer.ts +39 -3
- package/src/events/types.ts +172 -35
- package/src/ids/runtime-id.ts +6 -0
- package/src/ids/uuid-v7.ts +7 -0
- package/src/image/image-asset-store.ts +319 -0
- package/src/image/image-input-policy.ts +14 -0
- package/src/image/image-probe.ts +233 -0
- package/src/image/image-types.ts +262 -0
- package/src/instructions/project-instructions.ts +4 -1
- package/src/mcp/mcp-manager.ts +83 -19
- package/src/model/fake-model-client.ts +9 -1
- package/src/model/input-token-estimator.ts +25 -0
- package/src/model/model-client.ts +60 -0
- package/src/model/model-request-preflight.ts +1 -0
- package/src/model/moonshot-input-token-estimator.ts +107 -0
- package/src/model/openai-chat-mapping.ts +77 -1
- package/src/model/openai-chat-model-client.ts +389 -59
- package/src/model/openai-chat-stream.ts +272 -0
- package/src/model/prompt-prefix-hash.ts +2 -4
- package/src/model/token-estimator.ts +4 -0
- package/src/observation/observation-builder.ts +29 -0
- package/src/session/resume-projection.ts +94 -34
- package/src/session/session-catalog.ts +4 -4
- package/src/session/session-errors.ts +1 -1
- package/src/session/session-history-reader.ts +151 -4
- package/src/session/session-last-response-reader.ts +189 -0
- package/src/session/session-schema.ts +349 -35
- package/src/session/session-store.ts +3266 -255
- package/src/session/sqlite-session-ledger.ts +9 -4
- package/src/skills/skill-catalog.ts +182 -0
- package/src/skills/skill-context.ts +364 -0
- package/src/skills/skill-loader.ts +779 -0
- package/src/skills/skill-tool.ts +297 -0
- package/src/tools/edit.ts +51 -64
- package/src/tools/ensure-parent-directory.ts +6 -0
- package/src/tools/read.ts +2 -2
- package/src/tools/recall.ts +43 -40
- package/src/tools/registry.ts +24 -3
- package/src/tools/ripgrep.ts +3 -2
- package/src/tools/types.ts +43 -4
- package/src/tools/write.ts +20 -31
- package/src/tui/app.tsx +333 -96
- package/src/tui/clipboard.ts +5 -0
- package/src/tui/components/mcp-panel.tsx +28 -0
- package/src/tui/components/prompt-input.tsx +790 -257
- package/src/tui/components/skills-panel.tsx +31 -0
- package/src/tui/components/timeline.tsx +30 -1
- package/src/tui/event-store.ts +85 -2
- package/src/tui/file-mention.ts +234 -0
- package/src/tui/line-editor.ts +1 -0
- package/src/tui/project-slash-commands.ts +229 -0
- package/src/tui/prompt-draft.ts +510 -0
- package/src/tui/prompt-history.ts +294 -22
- package/src/tui/slash-commands.ts +46 -2
- package/src/tui/tui-session-controller.ts +110 -48
- package/src/tui/workspace-file-search.ts +94 -0
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
lstat,
|
|
5
5
|
mkdir,
|
|
6
6
|
open,
|
|
7
|
+
readFile,
|
|
7
8
|
readdir,
|
|
8
9
|
realpath,
|
|
9
10
|
rename,
|
|
@@ -14,6 +15,7 @@ import { randomUUID } from "node:crypto";
|
|
|
14
15
|
import { Database } from "bun:sqlite";
|
|
15
16
|
import type {
|
|
16
17
|
ContextRevisionId,
|
|
18
|
+
ContextSurfaceId,
|
|
17
19
|
IterationId,
|
|
18
20
|
MessageId,
|
|
19
21
|
ProtocolFrameId,
|
|
@@ -22,11 +24,13 @@ import type {
|
|
|
22
24
|
ToolCallId,
|
|
23
25
|
TurnId,
|
|
24
26
|
} from "../ids/runtime-id";
|
|
25
|
-
import
|
|
26
|
-
|
|
27
|
-
ModelContextProfile,
|
|
27
|
+
import {
|
|
28
|
+
createModelContextProfile,
|
|
29
|
+
type ModelContextProfile,
|
|
28
30
|
} from "../model/model-context-profile";
|
|
29
|
-
import type {
|
|
31
|
+
import type { ModelMessageProtocol } from "../model/model-client";
|
|
32
|
+
import type { InputTokenEstimatorCompatibility } from "../model/input-token-estimator";
|
|
33
|
+
import type { ToolDefinition, ToolRawResult } from "../tools/types";
|
|
30
34
|
import { sha256, stableJsonStringify } from "../model/model-request-preflight";
|
|
31
35
|
import {
|
|
32
36
|
ContextProtocolError,
|
|
@@ -41,33 +45,54 @@ import {
|
|
|
41
45
|
ContextRevisionCompiler,
|
|
42
46
|
createInitialContextRevision,
|
|
43
47
|
} from "../context/context-revision-compiler";
|
|
48
|
+
import { recallFirstRetirementPolicyV1 } from "../context/context-policy";
|
|
44
49
|
import {
|
|
45
50
|
ContextSwapRenderer,
|
|
46
51
|
SWAP_OBSERVATION_FORMAT,
|
|
47
52
|
} from "../context/context-swap-renderer";
|
|
48
53
|
import {
|
|
49
|
-
|
|
54
|
+
SUPPORTED_TOOL_OBSERVATION_FORMATS,
|
|
50
55
|
contentHash,
|
|
51
56
|
immutableCanonicalClone,
|
|
52
57
|
immutableRecord,
|
|
53
58
|
interruptedCompletionInputs,
|
|
54
59
|
observationForCompletion,
|
|
60
|
+
userMessageHash,
|
|
55
61
|
type CanonicalMessageRecord,
|
|
56
62
|
type ProtocolContextView,
|
|
57
63
|
type ProtocolFrame,
|
|
58
64
|
type ToolCompletion,
|
|
59
65
|
type ToolResultRecord,
|
|
60
66
|
} from "../context/protocol-frame";
|
|
67
|
+
import {
|
|
68
|
+
contextSurfaceChangeManifestHash,
|
|
69
|
+
contextSurfaceChanges,
|
|
70
|
+
validateStoredContextSurface,
|
|
71
|
+
type ContextSurfaceChanges,
|
|
72
|
+
type StoredContextSurfaceV8,
|
|
73
|
+
} from "../context/context-surface";
|
|
61
74
|
import type {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
75
|
+
StoredContextRevisionV8,
|
|
76
|
+
StoredContextSnapshotV8,
|
|
77
|
+
StoredContextOverrideV8,
|
|
65
78
|
SwapOverride,
|
|
66
79
|
} from "../context/context-revision";
|
|
67
80
|
import type { IterationIdentity, ToolCall } from "../agent/types";
|
|
81
|
+
import {
|
|
82
|
+
parseImageAssetId,
|
|
83
|
+
parseImageAttachmentId,
|
|
84
|
+
validateOriginalImageName,
|
|
85
|
+
validateUserMessage,
|
|
86
|
+
type ImageAssetRef,
|
|
87
|
+
type UserImageAttachment,
|
|
88
|
+
} from "../image/image-types";
|
|
89
|
+
import { ImageAssetStore } from "../image/image-asset-store";
|
|
68
90
|
import type { MeasuredContextAnchor } from "../agent/context-meter";
|
|
69
91
|
import type { ProjectInstructionManifest } from "../instructions/project-instructions";
|
|
92
|
+
import { SUPPORTED_RECALL_RETIREMENT_CONTRACT_VERSIONS } from "../context/recall-retirement-contract";
|
|
93
|
+
import type { ClosedTurnBoundary } from "../context/prefix-retirement-planner";
|
|
70
94
|
import {
|
|
95
|
+
AdmissionStaleError,
|
|
71
96
|
InMemorySessionLedger,
|
|
72
97
|
type LedgerMutation,
|
|
73
98
|
type SessionLedgerCommitter,
|
|
@@ -79,31 +104,57 @@ import {
|
|
|
79
104
|
} from "./session-history-reader";
|
|
80
105
|
import { SessionLease } from "./session-lock";
|
|
81
106
|
import {
|
|
82
|
-
|
|
107
|
+
SESSION_SCHEMA_V9_FINGERPRINT,
|
|
83
108
|
SESSION_SCHEMA_VERSION,
|
|
84
109
|
configureWritableDatabase,
|
|
85
110
|
createSessionSchema,
|
|
111
|
+
dropSessionCloneTriggers,
|
|
86
112
|
rebuildRecallIndex,
|
|
113
|
+
reinstallSessionCloneTriggers,
|
|
87
114
|
verifyRecallIndex,
|
|
88
115
|
verifySessionSchema,
|
|
89
116
|
verifySqliteIntegrity,
|
|
90
117
|
} from "./session-schema";
|
|
118
|
+
import type { AgentEvent } from "../events/types";
|
|
119
|
+
import { renderObservationLogEvent } from "../events/observation-text-log";
|
|
120
|
+
import {
|
|
121
|
+
SKILL_FILE_MAX_BYTES,
|
|
122
|
+
SKILL_RESOURCE_MAX_DEPTH,
|
|
123
|
+
SKILL_RESOURCE_MAX_ENTRIES,
|
|
124
|
+
type SkillScope,
|
|
125
|
+
} from "../skills/skill-loader";
|
|
126
|
+
import type {
|
|
127
|
+
ActiveSkillManifestEntry,
|
|
128
|
+
SkillCatalogManifestEntry,
|
|
129
|
+
} from "../skills/skill-catalog";
|
|
130
|
+
import {
|
|
131
|
+
SKILL_ACTIVATION_RECEIPT_FORMAT,
|
|
132
|
+
SKILL_POLICY_VERSION,
|
|
133
|
+
renderSkillActivationReceipt,
|
|
134
|
+
} from "../skills/skill-context";
|
|
135
|
+
import {
|
|
136
|
+
IMAGE_INPUT_POLICY,
|
|
137
|
+
IMAGE_INPUT_POLICY_VERSION,
|
|
138
|
+
} from "../image/image-input-policy";
|
|
91
139
|
|
|
92
|
-
export type
|
|
93
|
-
|
|
140
|
+
export type SessionImageInputCompatibility = {
|
|
141
|
+
readonly policyVersion: string;
|
|
142
|
+
readonly policySha256: string;
|
|
143
|
+
readonly inputModalities: readonly ("text" | "image")[];
|
|
144
|
+
readonly tokenEstimator?: InputTokenEstimatorCompatibility;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export type SessionCompatibilityContract = {
|
|
94
148
|
modelName: string;
|
|
95
149
|
profileName?: string;
|
|
96
150
|
includeReasoningContent: boolean;
|
|
97
151
|
contextProfile: ModelContextProfile;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
toolSchemaSha256: string;
|
|
101
|
-
requestConfigSha256: string;
|
|
102
|
-
observationFormat: typeof TOOL_OBSERVATION_FORMAT;
|
|
152
|
+
messageProtocol: ModelMessageProtocol;
|
|
153
|
+
imageInput: SessionImageInputCompatibility;
|
|
103
154
|
};
|
|
104
155
|
|
|
105
|
-
export type
|
|
106
|
-
schemaVersion:
|
|
156
|
+
export type StoredSessionMetaV9 = {
|
|
157
|
+
schemaVersion: 9;
|
|
107
158
|
schemaFingerprint: string;
|
|
108
159
|
initializationState: "creating" | "ready";
|
|
109
160
|
sessionId: SessionId;
|
|
@@ -111,10 +162,9 @@ export type StoredSessionMetaV5 = {
|
|
|
111
162
|
modelName: string;
|
|
112
163
|
systemPromptSha256: string;
|
|
113
164
|
projectInstruction?: ProjectInstructionManifest;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
activeRevisionId: ContextRevisionId;
|
|
165
|
+
sessionCompatibilityJson: string | null;
|
|
166
|
+
sessionCompatibilitySha256: string | null;
|
|
167
|
+
activeRevisionId: ContextRevisionId | null;
|
|
118
168
|
nextTurnNumber: number;
|
|
119
169
|
nextEventSequence: number;
|
|
120
170
|
openCount: number;
|
|
@@ -131,7 +181,7 @@ export type StoredSessionMetaV5 = {
|
|
|
131
181
|
| null;
|
|
132
182
|
};
|
|
133
183
|
|
|
134
|
-
export type SessionCloseReason = NonNullable<
|
|
184
|
+
export type SessionCloseReason = NonNullable<StoredSessionMetaV9["lastCloseReason"]>;
|
|
135
185
|
|
|
136
186
|
export type SessionRecoveryResult = {
|
|
137
187
|
recoveredTurnId?: TurnId;
|
|
@@ -150,12 +200,12 @@ export type CommitSwapRevisionInput = {
|
|
|
150
200
|
expectedBaseRevisionId: ContextRevisionId;
|
|
151
201
|
expectedBaseRevisionNumber: number;
|
|
152
202
|
expectedCanonicalThroughOrdinal: number;
|
|
153
|
-
|
|
203
|
+
expectedBaseActiveOverrideManifestSha256: string;
|
|
154
204
|
policyVersion: "swap-only-v1";
|
|
155
205
|
rendererFormat: typeof SWAP_OBSERVATION_FORMAT;
|
|
156
206
|
planHash: string;
|
|
157
207
|
addedOverrides: readonly SwapOverride[];
|
|
158
|
-
|
|
208
|
+
nextActiveOverrideManifestSha256: string;
|
|
159
209
|
canonicalSequenceSha256: string;
|
|
160
210
|
renderedMessageSha256: string;
|
|
161
211
|
};
|
|
@@ -172,6 +222,155 @@ export type CommitSwapRevisionOptions = {
|
|
|
172
222
|
faultInjector?: (stage: CommitSwapRevisionFaultStage) => void;
|
|
173
223
|
};
|
|
174
224
|
|
|
225
|
+
export type CommitSurfaceRefreshInput = {
|
|
226
|
+
revisionId: ContextRevisionId;
|
|
227
|
+
expectedBaseRevisionId: ContextRevisionId;
|
|
228
|
+
expectedBaseRevisionNumber: number;
|
|
229
|
+
expectedCanonicalThroughOrdinal: number;
|
|
230
|
+
expectedBaseActiveOverrideManifestSha256: string;
|
|
231
|
+
surface: StoredContextSurfaceV8;
|
|
232
|
+
changes: ContextSurfaceChanges;
|
|
233
|
+
changeManifestSha256: string;
|
|
234
|
+
canonicalSequenceSha256: string;
|
|
235
|
+
renderedMessageSha256: string;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export type CommitSurfaceRefreshFaultStage =
|
|
239
|
+
| "before_surface_insert"
|
|
240
|
+
| "after_surface_insert"
|
|
241
|
+
| "after_revision_insert"
|
|
242
|
+
| "after_measurement_delete"
|
|
243
|
+
| "after_active_update";
|
|
244
|
+
|
|
245
|
+
export type CommitSurfaceRefreshOptions = {
|
|
246
|
+
faultInjector?: (stage: CommitSurfaceRefreshFaultStage) => void;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
export type CommitPrefixRetirementRevisionInput = {
|
|
250
|
+
revisionId: ContextRevisionId;
|
|
251
|
+
expectedBaseRevisionId: ContextRevisionId;
|
|
252
|
+
expectedBaseRevisionNumber: number;
|
|
253
|
+
expectedBaseKeepFromOrdinal: number;
|
|
254
|
+
expectedCanonicalThroughOrdinal: number;
|
|
255
|
+
expectedSurfaceSha256: string;
|
|
256
|
+
expectedBaseActiveOverrideManifestSha256: string;
|
|
257
|
+
policyVersion: "recall-first-retirement-v1";
|
|
258
|
+
planHash: string;
|
|
259
|
+
nextKeepFromOrdinal: number;
|
|
260
|
+
retiredThroughOrdinal: number;
|
|
261
|
+
retiredTurnCount: number;
|
|
262
|
+
retiredFrameCount: number;
|
|
263
|
+
retiredMessageCount: number;
|
|
264
|
+
nextActiveOverrideCount: number;
|
|
265
|
+
nextActiveOverrideManifestSha256: string;
|
|
266
|
+
canonicalSequenceSha256: string;
|
|
267
|
+
renderedMessageSha256: string;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export type CommitPrefixRetirementRevisionFaultStage =
|
|
271
|
+
| "before_revision_insert"
|
|
272
|
+
| "after_revision_insert"
|
|
273
|
+
| "after_override_readback"
|
|
274
|
+
| "after_measurement_delete"
|
|
275
|
+
| "after_active_update"
|
|
276
|
+
| "after_snapshot_readback";
|
|
277
|
+
|
|
278
|
+
export type CommitPrefixRetirementRevisionOptions = {
|
|
279
|
+
faultInjector?: (stage: CommitPrefixRetirementRevisionFaultStage) => void;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
export type StoredSkillActivation = {
|
|
283
|
+
readonly activationMessageId: MessageId;
|
|
284
|
+
readonly toolCallId: ToolCallId;
|
|
285
|
+
readonly sessionId: SessionId;
|
|
286
|
+
readonly name: string;
|
|
287
|
+
readonly scope: SkillScope;
|
|
288
|
+
readonly skillFileSha256: string;
|
|
289
|
+
readonly state: "pending" | "dispatched" | "promoted" | "rejected";
|
|
290
|
+
readonly dispatchedIterationId?: IterationId;
|
|
291
|
+
readonly settledRevisionId?: ContextRevisionId;
|
|
292
|
+
readonly rejectionReason?: string;
|
|
293
|
+
readonly createdAt: string;
|
|
294
|
+
readonly updatedAt: string;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
export type CommitSkillsUpdateInput = {
|
|
298
|
+
revisionId: ContextRevisionId;
|
|
299
|
+
expectedBaseRevisionId: ContextRevisionId;
|
|
300
|
+
expectedBaseRevisionNumber: number;
|
|
301
|
+
expectedCanonicalThroughOrdinal: number;
|
|
302
|
+
expectedBaseActiveOverrideManifestSha256: string;
|
|
303
|
+
surface: StoredContextSurfaceV8;
|
|
304
|
+
changes: ContextSurfaceChanges;
|
|
305
|
+
changeManifestSha256: string;
|
|
306
|
+
activationManifestSha256: string;
|
|
307
|
+
addedOverrides: readonly SwapOverride[];
|
|
308
|
+
nextActiveOverrideManifestSha256: string;
|
|
309
|
+
settlements: readonly {
|
|
310
|
+
activationMessageId: MessageId;
|
|
311
|
+
name: string;
|
|
312
|
+
state: "promoted" | "rejected";
|
|
313
|
+
rejectionReason?: string;
|
|
314
|
+
}[];
|
|
315
|
+
canonicalSequenceSha256: string;
|
|
316
|
+
renderedMessageSha256: string;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
export type CommitSkillsUpdateFaultStage =
|
|
320
|
+
| "before_surface_insert"
|
|
321
|
+
| "after_surface_insert"
|
|
322
|
+
| "after_revision_insert"
|
|
323
|
+
| "after_first_override_insert"
|
|
324
|
+
| "after_overrides_insert"
|
|
325
|
+
| "after_activations_update"
|
|
326
|
+
| "after_measurement_delete"
|
|
327
|
+
| "after_active_update";
|
|
328
|
+
|
|
329
|
+
export type CommitSkillsUpdateOptions = {
|
|
330
|
+
faultInjector?: (stage: CommitSkillsUpdateFaultStage) => void;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
export type CloneSessionFaultStage =
|
|
334
|
+
| "after_staging_mkdir"
|
|
335
|
+
| "after_snapshot"
|
|
336
|
+
| "after_trigger_drop"
|
|
337
|
+
| "after_identity_update"
|
|
338
|
+
| "after_revision_hash_rewrite"
|
|
339
|
+
| "after_trigger_reinstall"
|
|
340
|
+
| "after_recall_validation"
|
|
341
|
+
| "after_event_rewrite"
|
|
342
|
+
| "after_observation_render"
|
|
343
|
+
| "after_artifact_validation"
|
|
344
|
+
| "before_publish_rename";
|
|
345
|
+
|
|
346
|
+
export type CloneSessionStoreInput = {
|
|
347
|
+
targetSessionId: SessionId;
|
|
348
|
+
faultInjector?: (stage: CloneSessionFaultStage) => void;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
export function skillActivationManifestSha256(
|
|
352
|
+
settlements: CommitSkillsUpdateInput["settlements"],
|
|
353
|
+
): string {
|
|
354
|
+
return sha256(
|
|
355
|
+
stableJsonStringify(
|
|
356
|
+
[...settlements]
|
|
357
|
+
.sort(
|
|
358
|
+
(left, right) =>
|
|
359
|
+
compareCanonicalText(left.name, right.name) ||
|
|
360
|
+
compareCanonicalText(left.activationMessageId, right.activationMessageId),
|
|
361
|
+
)
|
|
362
|
+
.map((settlement) => ({
|
|
363
|
+
activationMessageId: settlement.activationMessageId,
|
|
364
|
+
name: settlement.name,
|
|
365
|
+
state: settlement.state,
|
|
366
|
+
...(settlement.rejectionReason === undefined
|
|
367
|
+
? {}
|
|
368
|
+
: { rejectionReason: settlement.rejectionReason }),
|
|
369
|
+
})),
|
|
370
|
+
),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
175
374
|
export type CreateNewSessionStoreInput = {
|
|
176
375
|
workspaceRoot: string;
|
|
177
376
|
sessionId: SessionId;
|
|
@@ -254,21 +453,14 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
254
453
|
createSessionSchema(database);
|
|
255
454
|
verifySessionSchema(database, input.sessionId);
|
|
256
455
|
|
|
257
|
-
const revisionId = input.idFactory.createContextRevisionId();
|
|
258
456
|
const initialLedger = new InMemorySessionLedger({
|
|
259
457
|
sessionId: input.sessionId,
|
|
260
458
|
systemPrompt: input.systemPrompt,
|
|
261
459
|
idFactory: input.idFactory,
|
|
262
|
-
initialRevisionId: revisionId,
|
|
263
460
|
clock,
|
|
264
461
|
});
|
|
265
462
|
const initialView = initialLedger.snapshot({ fullIntegrity: true });
|
|
266
463
|
const createdAt = clock();
|
|
267
|
-
const initialRevision = createInitialContextRevision({
|
|
268
|
-
revisionId,
|
|
269
|
-
canonical: initialView,
|
|
270
|
-
createdAt,
|
|
271
|
-
});
|
|
272
464
|
runTransaction(database, () => {
|
|
273
465
|
database!
|
|
274
466
|
.query(
|
|
@@ -277,14 +469,14 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
277
469
|
session_id, workspace_root, model_name, system_prompt_sha256,
|
|
278
470
|
project_instruction_file, project_instruction_byte_length,
|
|
279
471
|
project_instruction_sha256,
|
|
280
|
-
|
|
472
|
+
session_compatibility_json, session_compatibility_sha256,
|
|
281
473
|
active_revision_id, next_turn_number, next_event_sequence, open_count,
|
|
282
474
|
created_at, updated_at, last_opened_at, last_closed_at, last_close_reason
|
|
283
|
-
) VALUES (1, ?, ?, 'creating', ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL,
|
|
475
|
+
) VALUES (1, ?, ?, 'creating', ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, 1, 1, 1, ?, ?, ?, NULL, NULL)`,
|
|
284
476
|
)
|
|
285
477
|
.run(
|
|
286
478
|
SESSION_SCHEMA_VERSION,
|
|
287
|
-
|
|
479
|
+
SESSION_SCHEMA_V9_FINGERPRINT,
|
|
288
480
|
input.sessionId,
|
|
289
481
|
workspaceRoot,
|
|
290
482
|
input.modelName,
|
|
@@ -292,7 +484,6 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
292
484
|
input.projectInstruction?.path ?? null,
|
|
293
485
|
input.projectInstruction?.byteLength ?? null,
|
|
294
486
|
input.projectInstruction?.sha256 ?? null,
|
|
295
|
-
revisionId,
|
|
296
487
|
createdAt,
|
|
297
488
|
createdAt,
|
|
298
489
|
createdAt,
|
|
@@ -302,24 +493,6 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
302
493
|
database!,
|
|
303
494
|
requireItem(initialView.messages, 0, "system message"),
|
|
304
495
|
);
|
|
305
|
-
database!
|
|
306
|
-
.query(
|
|
307
|
-
`INSERT INTO context_revisions (
|
|
308
|
-
revision_id, session_id, revision_number, parent_revision_id, kind,
|
|
309
|
-
keep_from_ordinal, source_through_ordinal, added_override_count,
|
|
310
|
-
total_override_count, override_manifest_sha256,
|
|
311
|
-
canonical_sequence_sha256, rendered_message_sha256, policy_version,
|
|
312
|
-
renderer_format, plan_sha256, created_at
|
|
313
|
-
) VALUES (?, ?, 1, NULL, 'initial_full', 1, 1, 0, 0, ?, ?, ?, NULL, NULL, NULL, ?)`,
|
|
314
|
-
)
|
|
315
|
-
.run(
|
|
316
|
-
revisionId,
|
|
317
|
-
input.sessionId,
|
|
318
|
-
initialRevision.overrideManifestSha256,
|
|
319
|
-
initialRevision.canonicalSequenceSha256,
|
|
320
|
-
initialRevision.renderedMessageSha256,
|
|
321
|
-
createdAt,
|
|
322
|
-
);
|
|
323
496
|
});
|
|
324
497
|
|
|
325
498
|
const store = new SessionStore(database, lease, {
|
|
@@ -330,7 +503,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
330
503
|
clock,
|
|
331
504
|
});
|
|
332
505
|
await store.correctDatabaseModes();
|
|
333
|
-
store.
|
|
506
|
+
store.validateCreatingState();
|
|
334
507
|
verifyRecallIndex(database, input.sessionId);
|
|
335
508
|
return store;
|
|
336
509
|
} catch (error) {
|
|
@@ -394,7 +567,12 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
394
567
|
{ sessionId: input.sessionId },
|
|
395
568
|
);
|
|
396
569
|
}
|
|
397
|
-
|
|
570
|
+
if (meta.initializationState === "creating") {
|
|
571
|
+
store.validateCreatingState();
|
|
572
|
+
} else {
|
|
573
|
+
store.validateAll({ allowOpenTail: true });
|
|
574
|
+
await store.verifyImageAssetFiles();
|
|
575
|
+
}
|
|
398
576
|
try {
|
|
399
577
|
verifyRecallIndex(database, input.sessionId);
|
|
400
578
|
} catch (error) {
|
|
@@ -455,6 +633,9 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
455
633
|
}
|
|
456
634
|
});
|
|
457
635
|
} catch (error) {
|
|
636
|
+
if (error instanceof AdmissionStaleError) {
|
|
637
|
+
throw error;
|
|
638
|
+
}
|
|
458
639
|
throw sessionWriteError(mutation.kind, this.sessionId, error);
|
|
459
640
|
}
|
|
460
641
|
}
|
|
@@ -554,46 +735,106 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
554
735
|
}
|
|
555
736
|
}
|
|
556
737
|
|
|
557
|
-
|
|
738
|
+
finalizeInitialization(input: {
|
|
739
|
+
contract: SessionCompatibilityContract;
|
|
740
|
+
surface: StoredContextSurfaceV8;
|
|
741
|
+
revisionId: ContextRevisionId;
|
|
742
|
+
}): void {
|
|
558
743
|
this.requireOpen();
|
|
744
|
+
const contract = normalizeSessionCompatibilityContract(input.contract);
|
|
559
745
|
const json = stableJsonStringify(contract);
|
|
560
746
|
const contractSha256 = sha256(json);
|
|
561
747
|
const now = this.clock();
|
|
562
748
|
try {
|
|
563
749
|
runTransaction(this.database, () => {
|
|
750
|
+
const meta = this.readMeta();
|
|
751
|
+
if (
|
|
752
|
+
meta.initializationState !== "creating" ||
|
|
753
|
+
meta.activeRevisionId !== null ||
|
|
754
|
+
input.surface.sessionId !== this.sessionId
|
|
755
|
+
) {
|
|
756
|
+
throw new Error("Session initialization base is invalid.");
|
|
757
|
+
}
|
|
758
|
+
validateStoredContextSurface(input.surface);
|
|
759
|
+
const canonical = this.loadProtocolView();
|
|
760
|
+
const creationPrompt = this.readCreationSystemPrompt();
|
|
761
|
+
if (input.surface.systemPrompt !== creationPrompt) {
|
|
762
|
+
throw new Error(
|
|
763
|
+
"Initial context surface must match the creation system prompt.",
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
const revision = createInitialContextRevision({
|
|
767
|
+
revisionId: input.revisionId,
|
|
768
|
+
canonical,
|
|
769
|
+
surface: input.surface,
|
|
770
|
+
createdAt: now,
|
|
771
|
+
});
|
|
772
|
+
insertContextSurface(this.database, input.surface);
|
|
773
|
+
this.database
|
|
774
|
+
.query(
|
|
775
|
+
`INSERT INTO context_revisions (
|
|
776
|
+
revision_id, session_id, revision_number, parent_revision_id, kind,
|
|
777
|
+
surface_id, surface_sha256, keep_from_ordinal,
|
|
778
|
+
source_through_ordinal, added_override_count, active_override_count,
|
|
779
|
+
active_override_manifest_sha256, canonical_sequence_sha256,
|
|
780
|
+
rendered_message_sha256, policy_version, renderer_format,
|
|
781
|
+
plan_sha256, change_manifest_sha256, created_at
|
|
782
|
+
) VALUES (?, ?, 1, NULL, 'initial_full', ?, ?, 1, 1, 0, 0, ?, ?, ?, NULL, NULL, NULL, NULL, ?)`,
|
|
783
|
+
)
|
|
784
|
+
.run(
|
|
785
|
+
revision.revisionId,
|
|
786
|
+
this.sessionId,
|
|
787
|
+
revision.surfaceId,
|
|
788
|
+
revision.surfaceSha256,
|
|
789
|
+
revision.activeOverrideManifestSha256,
|
|
790
|
+
revision.canonicalSequenceSha256,
|
|
791
|
+
revision.renderedMessageSha256,
|
|
792
|
+
revision.createdAt,
|
|
793
|
+
);
|
|
564
794
|
const updated = this.database
|
|
565
795
|
.query(
|
|
566
796
|
`UPDATE session_meta
|
|
567
|
-
SET initialization_state = 'ready',
|
|
568
|
-
|
|
797
|
+
SET initialization_state = 'ready', session_compatibility_json = ?,
|
|
798
|
+
session_compatibility_sha256 = ?, active_revision_id = ?, updated_at = ?
|
|
569
799
|
WHERE singleton = 1 AND initialization_state = 'creating'
|
|
570
|
-
AND
|
|
800
|
+
AND session_compatibility_json IS NULL AND active_revision_id IS NULL`,
|
|
571
801
|
)
|
|
572
|
-
.run(
|
|
802
|
+
.run(json, contractSha256, revision.revisionId, now);
|
|
573
803
|
requireSingleChange(
|
|
574
804
|
this.database,
|
|
575
805
|
updated.changes,
|
|
576
|
-
"finalize
|
|
806
|
+
"finalize session initialization",
|
|
577
807
|
);
|
|
808
|
+
const readback = this.loadContextSnapshot();
|
|
809
|
+
if (
|
|
810
|
+
readback.revision.revisionId !== revision.revisionId ||
|
|
811
|
+
readback.surface.surfaceId !== input.surface.surfaceId
|
|
812
|
+
) {
|
|
813
|
+
throw new Error("Finalized session initialization readback failed.");
|
|
814
|
+
}
|
|
578
815
|
});
|
|
579
816
|
} catch (error) {
|
|
580
|
-
throw sessionWriteError("
|
|
817
|
+
throw sessionWriteError("finalize_session_initialization", this.sessionId, error);
|
|
581
818
|
}
|
|
582
819
|
}
|
|
583
820
|
|
|
584
|
-
|
|
821
|
+
assertSessionCompatibility(contract: SessionCompatibilityContract): void {
|
|
585
822
|
const meta = this.readMeta();
|
|
586
|
-
const
|
|
823
|
+
const currentContract = normalizeSessionCompatibilityContract(contract);
|
|
824
|
+
const current = stableJsonStringify(currentContract);
|
|
587
825
|
const currentHash = sha256(current);
|
|
588
826
|
if (
|
|
589
|
-
meta.
|
|
590
|
-
meta.
|
|
827
|
+
meta.sessionCompatibilityJson !== current ||
|
|
828
|
+
meta.sessionCompatibilitySha256 !== currentHash
|
|
591
829
|
) {
|
|
592
|
-
const changed =
|
|
830
|
+
const changed = compatibilityContractDifferences(
|
|
831
|
+
meta.sessionCompatibilityJson,
|
|
832
|
+
currentContract,
|
|
833
|
+
);
|
|
593
834
|
throw new SessionError(
|
|
594
|
-
"
|
|
595
|
-
"
|
|
596
|
-
`Session
|
|
835
|
+
"SESSION_COMPATIBILITY_MISMATCH",
|
|
836
|
+
"compare_session_compatibility",
|
|
837
|
+
`Session compatibility contract changed: ${changed.join(", ") || "stored contract is invalid"}.`,
|
|
597
838
|
{ sessionId: this.sessionId },
|
|
598
839
|
);
|
|
599
840
|
}
|
|
@@ -602,7 +843,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
602
843
|
writeMeasuredContextAnchor(anchor: MeasuredContextAnchor): void {
|
|
603
844
|
this.requireOpen();
|
|
604
845
|
assertMeasuredContextAnchor(anchor);
|
|
605
|
-
const revisionId = this.readMeta()
|
|
846
|
+
const revisionId = requireActiveRevisionId(this.readMeta());
|
|
606
847
|
const now = this.clock();
|
|
607
848
|
try {
|
|
608
849
|
runTransaction(this.database, () => {
|
|
@@ -655,7 +896,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
655
896
|
if (state === undefined) {
|
|
656
897
|
return undefined;
|
|
657
898
|
}
|
|
658
|
-
if (state.revisionId !== this.readMeta()
|
|
899
|
+
if (state.revisionId !== requireActiveRevisionId(this.readMeta())) {
|
|
659
900
|
return undefined;
|
|
660
901
|
}
|
|
661
902
|
return state.anchor;
|
|
@@ -686,10 +927,82 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
686
927
|
}
|
|
687
928
|
}
|
|
688
929
|
|
|
930
|
+
loadClosedTurnBoundaries(): readonly ClosedTurnBoundary[] {
|
|
931
|
+
this.requireOpen();
|
|
932
|
+
this.assertContextRevisionIdle();
|
|
933
|
+
const canonical = this.loadProtocolView();
|
|
934
|
+
this.validator.validate(canonical, { fullIntegrity: true });
|
|
935
|
+
return this.readClosedTurnBoundaries(canonical);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
private readClosedTurnBoundaries(
|
|
939
|
+
canonical: ProtocolContextView,
|
|
940
|
+
): readonly ClosedTurnBoundary[] {
|
|
941
|
+
const rows = this.database
|
|
942
|
+
.query("SELECT * FROM turns ORDER BY turn_number")
|
|
943
|
+
.all() as Array<Record<string, unknown>>;
|
|
944
|
+
const boundaries: ClosedTurnBoundary[] = [];
|
|
945
|
+
let expectedOrdinal = 2;
|
|
946
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
947
|
+
const row = requireItem(rows, index, "turn row");
|
|
948
|
+
const turnId = stringFromSql(row.turn_id, "turn_id") as TurnId;
|
|
949
|
+
const turnNumber = numberFromSql(row.turn_number, "turn_number");
|
|
950
|
+
const status = enumFromSql(
|
|
951
|
+
row.status,
|
|
952
|
+
["completed", "failed", "cancelled", "interrupted"] as const,
|
|
953
|
+
"closed turn status",
|
|
954
|
+
);
|
|
955
|
+
const frames = canonical.frames.filter((frame) => frame.turnId === turnId);
|
|
956
|
+
const messages = canonical.messages.filter(
|
|
957
|
+
(message) => message.role !== "system" && message.turnId === turnId,
|
|
958
|
+
);
|
|
959
|
+
const firstMessage = messages[0];
|
|
960
|
+
const lastMessage = messages.at(-1);
|
|
961
|
+
let nextFrameOrdinal = expectedOrdinal;
|
|
962
|
+
for (const frame of frames) {
|
|
963
|
+
if (
|
|
964
|
+
frame.state !== "closed" ||
|
|
965
|
+
frame.firstOrdinal !== nextFrameOrdinal ||
|
|
966
|
+
frame.lastOrdinal === undefined
|
|
967
|
+
) {
|
|
968
|
+
throw new Error(`Turn ${turnId} has an invalid closed frame boundary.`);
|
|
969
|
+
}
|
|
970
|
+
nextFrameOrdinal = frame.lastOrdinal + 1;
|
|
971
|
+
}
|
|
972
|
+
if (
|
|
973
|
+
turnNumber !== index + 1 ||
|
|
974
|
+
frames.length < 1 ||
|
|
975
|
+
messages.length < 1 ||
|
|
976
|
+
firstMessage?.role !== "user" ||
|
|
977
|
+
firstMessage.ordinal !== expectedOrdinal ||
|
|
978
|
+
lastMessage === undefined ||
|
|
979
|
+
nextFrameOrdinal !== lastMessage.ordinal + 1
|
|
980
|
+
) {
|
|
981
|
+
throw new Error(`Turn ${turnId} has an invalid canonical boundary.`);
|
|
982
|
+
}
|
|
983
|
+
boundaries.push(
|
|
984
|
+
Object.freeze({
|
|
985
|
+
turnId,
|
|
986
|
+
turnNumber,
|
|
987
|
+
status,
|
|
988
|
+
firstOrdinal: expectedOrdinal,
|
|
989
|
+
lastOrdinal: lastMessage.ordinal,
|
|
990
|
+
frameCount: frames.length,
|
|
991
|
+
messageCount: messages.length,
|
|
992
|
+
}),
|
|
993
|
+
);
|
|
994
|
+
expectedOrdinal = lastMessage.ordinal + 1;
|
|
995
|
+
}
|
|
996
|
+
if (expectedOrdinal !== canonical.messages.length + 1) {
|
|
997
|
+
throw new Error("Closed turn boundaries do not cover canonical history.");
|
|
998
|
+
}
|
|
999
|
+
return Object.freeze(boundaries);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
689
1002
|
commitSwapRevision(
|
|
690
1003
|
input: CommitSwapRevisionInput,
|
|
691
1004
|
options: CommitSwapRevisionOptions = {},
|
|
692
|
-
): Extract<
|
|
1005
|
+
): Extract<StoredContextRevisionV8, { kind: "swap_only" }> {
|
|
693
1006
|
this.requireOpen();
|
|
694
1007
|
assertCommitSwapRevisionInput(input);
|
|
695
1008
|
const now = this.clock();
|
|
@@ -702,8 +1015,8 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
702
1015
|
baseRevision.revisionNumber !== input.expectedBaseRevisionNumber ||
|
|
703
1016
|
snapshot.canonical.messages.length !==
|
|
704
1017
|
input.expectedCanonicalThroughOrdinal ||
|
|
705
|
-
baseRevision.
|
|
706
|
-
input.
|
|
1018
|
+
baseRevision.activeOverrideManifestSha256 !==
|
|
1019
|
+
input.expectedBaseActiveOverrideManifestSha256
|
|
707
1020
|
) {
|
|
708
1021
|
throw new Error("Context revision commit base is stale.");
|
|
709
1022
|
}
|
|
@@ -718,7 +1031,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
718
1031
|
new Set(candidateOverrides.map((override) => override.messageId)).size !==
|
|
719
1032
|
candidateOverrides.length ||
|
|
720
1033
|
activeOverrideManifestHash(candidateOverrides) !==
|
|
721
|
-
input.
|
|
1034
|
+
input.nextActiveOverrideManifestSha256
|
|
722
1035
|
) {
|
|
723
1036
|
throw new Error("Candidate override manifest is invalid.");
|
|
724
1037
|
}
|
|
@@ -727,6 +1040,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
727
1040
|
canonical: snapshot.canonical,
|
|
728
1041
|
activeOverrides: snapshot.activeOverrides,
|
|
729
1042
|
addedOverrides: input.addedOverrides,
|
|
1043
|
+
activeSurface: snapshot.surface,
|
|
730
1044
|
});
|
|
731
1045
|
if (
|
|
732
1046
|
canonicalSequenceHash(
|
|
@@ -743,28 +1057,32 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
743
1057
|
this.validateAddedOverrides(input.addedOverrides, snapshot.canonical);
|
|
744
1058
|
|
|
745
1059
|
const revisionNumber = baseRevision.revisionNumber + 1;
|
|
746
|
-
const
|
|
747
|
-
baseRevision.
|
|
1060
|
+
const activeOverrideCount =
|
|
1061
|
+
baseRevision.activeOverrideCount + input.addedOverrides.length;
|
|
748
1062
|
options.faultInjector?.("before_revision_insert");
|
|
749
1063
|
this.database
|
|
750
1064
|
.query(
|
|
751
1065
|
`INSERT INTO context_revisions (
|
|
752
1066
|
revision_id, session_id, revision_number, parent_revision_id, kind,
|
|
753
|
-
|
|
754
|
-
|
|
1067
|
+
surface_id, surface_sha256, keep_from_ordinal,
|
|
1068
|
+
source_through_ordinal, added_override_count,
|
|
1069
|
+
active_override_count, active_override_manifest_sha256,
|
|
755
1070
|
canonical_sequence_sha256, rendered_message_sha256, policy_version,
|
|
756
|
-
renderer_format, plan_sha256, created_at
|
|
757
|
-
) VALUES (?, ?, ?, ?, 'swap_only',
|
|
1071
|
+
renderer_format, plan_sha256, change_manifest_sha256, created_at
|
|
1072
|
+
) VALUES (?, ?, ?, ?, 'swap_only', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?)`,
|
|
758
1073
|
)
|
|
759
1074
|
.run(
|
|
760
1075
|
input.revisionId,
|
|
761
1076
|
this.sessionId,
|
|
762
1077
|
revisionNumber,
|
|
763
1078
|
baseRevision.revisionId,
|
|
1079
|
+
baseRevision.surfaceId,
|
|
1080
|
+
baseRevision.surfaceSha256,
|
|
1081
|
+
baseRevision.keepFromOrdinal,
|
|
764
1082
|
input.expectedCanonicalThroughOrdinal,
|
|
765
1083
|
input.addedOverrides.length,
|
|
766
|
-
|
|
767
|
-
input.
|
|
1084
|
+
activeOverrideCount,
|
|
1085
|
+
input.nextActiveOverrideManifestSha256,
|
|
768
1086
|
input.canonicalSequenceSha256,
|
|
769
1087
|
input.renderedMessageSha256,
|
|
770
1088
|
input.policyVersion,
|
|
@@ -816,15 +1134,15 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
816
1134
|
`SELECT co.* FROM context_overrides co
|
|
817
1135
|
JOIN context_revisions cr
|
|
818
1136
|
ON cr.revision_id = co.introduced_revision_id
|
|
819
|
-
WHERE cr.revision_number <= ?
|
|
1137
|
+
WHERE cr.revision_number <= ? AND co.ordinal >= ?
|
|
820
1138
|
ORDER BY co.ordinal`,
|
|
821
1139
|
)
|
|
822
|
-
.all(revisionNumber)
|
|
1140
|
+
.all(revisionNumber, baseRevision.keepFromOrdinal)
|
|
823
1141
|
.map(decodeStoredSwapOverride);
|
|
824
1142
|
if (
|
|
825
|
-
storedCandidateOverrides.length !==
|
|
1143
|
+
storedCandidateOverrides.length !== activeOverrideCount ||
|
|
826
1144
|
activeOverrideManifestHash(storedCandidateOverrides) !==
|
|
827
|
-
input.
|
|
1145
|
+
input.nextActiveOverrideManifestSha256
|
|
828
1146
|
) {
|
|
829
1147
|
throw new Error("Stored candidate override readback is invalid.");
|
|
830
1148
|
}
|
|
@@ -856,7 +1174,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
856
1174
|
return readback.revision;
|
|
857
1175
|
});
|
|
858
1176
|
} catch (error) {
|
|
859
|
-
if (this.readMeta()
|
|
1177
|
+
if (requireActiveRevisionId(this.readMeta()) !== input.expectedBaseRevisionId) {
|
|
860
1178
|
throw new Error("Failed context revision transaction changed active state.", {
|
|
861
1179
|
cause: error,
|
|
862
1180
|
});
|
|
@@ -865,59 +1183,734 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
865
1183
|
}
|
|
866
1184
|
}
|
|
867
1185
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
):
|
|
872
|
-
const messages = new Map(
|
|
873
|
-
canonical.messages.map((message) => [message.messageId, message] as const),
|
|
874
|
-
);
|
|
875
|
-
const results = new Map(
|
|
876
|
-
canonical.toolResults.map((result) => [result.toolMessageId, result] as const),
|
|
877
|
-
);
|
|
878
|
-
for (const override of overrides) {
|
|
879
|
-
const message = messages.get(override.messageId);
|
|
880
|
-
const result = results.get(override.messageId);
|
|
881
|
-
if (message?.role !== "tool" || result === undefined) {
|
|
882
|
-
throw new Error("Added context override does not target a tool result.");
|
|
883
|
-
}
|
|
884
|
-
const expected = this.swapRenderer.render({ message, result });
|
|
885
|
-
if (stableJsonStringify(expected) !== stableJsonStringify(override)) {
|
|
886
|
-
throw new Error("Added context override is not deterministic.");
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
markResumed(): number {
|
|
1186
|
+
commitPrefixRetirementRevision(
|
|
1187
|
+
input: CommitPrefixRetirementRevisionInput,
|
|
1188
|
+
options: CommitPrefixRetirementRevisionOptions = {},
|
|
1189
|
+
): Extract<StoredContextRevisionV8, { kind: "prefix_retirement" }> {
|
|
892
1190
|
this.requireOpen();
|
|
1191
|
+
assertCommitPrefixRetirementRevisionInput(input);
|
|
893
1192
|
const now = this.clock();
|
|
894
1193
|
try {
|
|
895
1194
|
return runTransaction(this.database, () => {
|
|
896
|
-
const
|
|
897
|
-
const
|
|
898
|
-
|
|
1195
|
+
const snapshot = this.loadContextSnapshot();
|
|
1196
|
+
const baseRevision = snapshot.revision;
|
|
1197
|
+
if (
|
|
1198
|
+
baseRevision.revisionId !== input.expectedBaseRevisionId ||
|
|
1199
|
+
baseRevision.revisionNumber !== input.expectedBaseRevisionNumber ||
|
|
1200
|
+
baseRevision.keepFromOrdinal !== input.expectedBaseKeepFromOrdinal ||
|
|
1201
|
+
snapshot.canonical.messages.length !==
|
|
1202
|
+
input.expectedCanonicalThroughOrdinal ||
|
|
1203
|
+
snapshot.surface.surfaceSha256 !== input.expectedSurfaceSha256 ||
|
|
1204
|
+
baseRevision.activeOverrideManifestSha256 !==
|
|
1205
|
+
input.expectedBaseActiveOverrideManifestSha256
|
|
1206
|
+
) {
|
|
1207
|
+
throw new Error("Prefix retirement commit base is stale.");
|
|
1208
|
+
}
|
|
1209
|
+
this.assertContextRevisionIdle();
|
|
1210
|
+
const closedTurns = this.readClosedTurnBoundaries(snapshot.canonical);
|
|
1211
|
+
const activeTurns = closedTurns.filter(
|
|
1212
|
+
(turn) => turn.firstOrdinal >= baseRevision.keepFromOrdinal,
|
|
1213
|
+
);
|
|
1214
|
+
const nextBoundary = activeTurns.find(
|
|
1215
|
+
(turn) => turn.firstOrdinal === input.nextKeepFromOrdinal,
|
|
1216
|
+
);
|
|
1217
|
+
const retainedTurns = activeTurns.filter(
|
|
1218
|
+
(turn) => turn.firstOrdinal >= input.nextKeepFromOrdinal,
|
|
1219
|
+
);
|
|
1220
|
+
const retiredTurns = activeTurns.filter(
|
|
1221
|
+
(turn) => turn.lastOrdinal < input.nextKeepFromOrdinal,
|
|
1222
|
+
);
|
|
1223
|
+
if (
|
|
1224
|
+
nextBoundary === undefined ||
|
|
1225
|
+
input.nextKeepFromOrdinal <= baseRevision.keepFromOrdinal ||
|
|
1226
|
+
retainedTurns.length <
|
|
1227
|
+
recallFirstRetirementPolicyV1.protectedRecentTurnCount ||
|
|
1228
|
+
retiredTurns.length !== input.retiredTurnCount ||
|
|
1229
|
+
retiredTurns.reduce((total, turn) => total + turn.frameCount, 0) !==
|
|
1230
|
+
input.retiredFrameCount ||
|
|
1231
|
+
retiredTurns.reduce((total, turn) => total + turn.messageCount, 0) !==
|
|
1232
|
+
input.retiredMessageCount
|
|
1233
|
+
) {
|
|
1234
|
+
throw new Error("Prefix retirement turn boundary is invalid.");
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const active = this.revisionCompiler.compileActive(snapshot);
|
|
1238
|
+
const nextActiveOverrides = snapshot.activeOverrides.filter(
|
|
1239
|
+
(override) => override.ordinal >= input.nextKeepFromOrdinal,
|
|
1240
|
+
);
|
|
1241
|
+
const candidate = this.revisionCompiler.compileProspective({
|
|
1242
|
+
active,
|
|
1243
|
+
canonical: snapshot.canonical,
|
|
1244
|
+
activeOverrides: snapshot.activeOverrides,
|
|
1245
|
+
addedOverrides: [],
|
|
1246
|
+
activeSurface: snapshot.surface,
|
|
1247
|
+
keepFromOrdinal: input.nextKeepFromOrdinal,
|
|
1248
|
+
});
|
|
1249
|
+
if (
|
|
1250
|
+
input.retiredThroughOrdinal !== input.nextKeepFromOrdinal - 1 ||
|
|
1251
|
+
nextActiveOverrides.length !== input.nextActiveOverrideCount ||
|
|
1252
|
+
activeOverrideManifestHash(nextActiveOverrides) !==
|
|
1253
|
+
input.nextActiveOverrideManifestSha256 ||
|
|
1254
|
+
canonicalSequenceHash(
|
|
1255
|
+
snapshot.canonical,
|
|
1256
|
+
input.expectedCanonicalThroughOrdinal,
|
|
1257
|
+
) !== input.canonicalSequenceSha256 ||
|
|
1258
|
+
renderedMessageHash(
|
|
1259
|
+
candidate.entries,
|
|
1260
|
+
input.expectedCanonicalThroughOrdinal,
|
|
1261
|
+
) !== input.renderedMessageSha256
|
|
1262
|
+
) {
|
|
1263
|
+
throw new Error("Prefix retirement candidate is invalid.");
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const revisionNumber = baseRevision.revisionNumber + 1;
|
|
1267
|
+
options.faultInjector?.("before_revision_insert");
|
|
1268
|
+
this.database
|
|
899
1269
|
.query(
|
|
900
|
-
`
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1270
|
+
`INSERT INTO context_revisions (
|
|
1271
|
+
revision_id, session_id, revision_number, parent_revision_id, kind,
|
|
1272
|
+
surface_id, surface_sha256, keep_from_ordinal,
|
|
1273
|
+
source_through_ordinal, added_override_count, active_override_count,
|
|
1274
|
+
active_override_manifest_sha256, canonical_sequence_sha256,
|
|
1275
|
+
rendered_message_sha256, policy_version, renderer_format,
|
|
1276
|
+
plan_sha256, change_manifest_sha256, retired_through_ordinal,
|
|
1277
|
+
retired_turn_count, retired_frame_count, retired_message_count,
|
|
1278
|
+
created_at
|
|
1279
|
+
) VALUES (?, ?, ?, ?, 'prefix_retirement', ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, NULL, ?, NULL, ?, ?, ?, ?, ?)`,
|
|
904
1280
|
)
|
|
905
|
-
.run(
|
|
906
|
-
|
|
907
|
-
|
|
1281
|
+
.run(
|
|
1282
|
+
input.revisionId,
|
|
1283
|
+
this.sessionId,
|
|
1284
|
+
revisionNumber,
|
|
1285
|
+
baseRevision.revisionId,
|
|
1286
|
+
baseRevision.surfaceId,
|
|
1287
|
+
baseRevision.surfaceSha256,
|
|
1288
|
+
input.nextKeepFromOrdinal,
|
|
1289
|
+
input.expectedCanonicalThroughOrdinal,
|
|
1290
|
+
input.nextActiveOverrideCount,
|
|
1291
|
+
input.nextActiveOverrideManifestSha256,
|
|
1292
|
+
input.canonicalSequenceSha256,
|
|
1293
|
+
input.renderedMessageSha256,
|
|
1294
|
+
input.policyVersion,
|
|
1295
|
+
input.planHash,
|
|
1296
|
+
input.retiredThroughOrdinal,
|
|
1297
|
+
input.retiredTurnCount,
|
|
1298
|
+
input.retiredFrameCount,
|
|
1299
|
+
input.retiredMessageCount,
|
|
1300
|
+
now,
|
|
1301
|
+
);
|
|
1302
|
+
options.faultInjector?.("after_revision_insert");
|
|
1303
|
+
|
|
1304
|
+
const storedActiveOverrides = this.database
|
|
1305
|
+
.query(
|
|
1306
|
+
`SELECT co.* FROM context_overrides co
|
|
1307
|
+
JOIN context_revisions introduced
|
|
1308
|
+
ON introduced.revision_id = co.introduced_revision_id
|
|
1309
|
+
WHERE introduced.revision_number <= ? AND co.ordinal >= ?
|
|
1310
|
+
ORDER BY co.ordinal`,
|
|
1311
|
+
)
|
|
1312
|
+
.all(revisionNumber, input.nextKeepFromOrdinal)
|
|
1313
|
+
.map(decodeStoredSwapOverride);
|
|
1314
|
+
if (
|
|
1315
|
+
storedActiveOverrides.length !== input.nextActiveOverrideCount ||
|
|
1316
|
+
activeOverrideManifestHash(storedActiveOverrides) !==
|
|
1317
|
+
input.nextActiveOverrideManifestSha256
|
|
1318
|
+
) {
|
|
1319
|
+
throw new Error("Prefix retirement override readback is invalid.");
|
|
1320
|
+
}
|
|
1321
|
+
options.faultInjector?.("after_override_readback");
|
|
1322
|
+
|
|
1323
|
+
this.database.query("DELETE FROM context_measurement_state").run();
|
|
1324
|
+
options.faultInjector?.("after_measurement_delete");
|
|
1325
|
+
const switched = this.database
|
|
1326
|
+
.query(
|
|
1327
|
+
`UPDATE session_meta SET active_revision_id = ?, updated_at = ?
|
|
1328
|
+
WHERE singleton = 1 AND active_revision_id = ?`,
|
|
1329
|
+
)
|
|
1330
|
+
.run(input.revisionId, now, baseRevision.revisionId);
|
|
1331
|
+
requireSingleChange(
|
|
1332
|
+
this.database,
|
|
1333
|
+
switched.changes,
|
|
1334
|
+
"activate prefix retirement revision",
|
|
1335
|
+
);
|
|
1336
|
+
options.faultInjector?.("after_active_update");
|
|
1337
|
+
|
|
1338
|
+
const readback = this.loadContextSnapshot();
|
|
1339
|
+
if (
|
|
1340
|
+
readback.revision.kind !== "prefix_retirement" ||
|
|
1341
|
+
readback.revision.revisionId !== input.revisionId ||
|
|
1342
|
+
readback.revision.keepFromOrdinal !== input.nextKeepFromOrdinal ||
|
|
1343
|
+
this.loadMeasuredContextState() !== undefined
|
|
1344
|
+
) {
|
|
1345
|
+
throw new Error("Committed prefix retirement readback failed.");
|
|
1346
|
+
}
|
|
1347
|
+
options.faultInjector?.("after_snapshot_readback");
|
|
1348
|
+
return readback.revision;
|
|
908
1349
|
});
|
|
909
1350
|
} catch (error) {
|
|
910
|
-
|
|
1351
|
+
if (requireActiveRevisionId(this.readMeta()) !== input.expectedBaseRevisionId) {
|
|
1352
|
+
throw new Error("Failed prefix retirement changed active state.", {
|
|
1353
|
+
cause: error,
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
throw sessionWriteError("commit_prefix_retirement", this.sessionId, error);
|
|
911
1357
|
}
|
|
912
1358
|
}
|
|
913
1359
|
|
|
914
|
-
|
|
1360
|
+
commitSurfaceRefresh(
|
|
1361
|
+
input: CommitSurfaceRefreshInput,
|
|
1362
|
+
options: CommitSurfaceRefreshOptions = {},
|
|
1363
|
+
): Extract<StoredContextRevisionV8, { kind: "surface_refresh" }> {
|
|
915
1364
|
this.requireOpen();
|
|
916
|
-
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
.
|
|
920
|
-
|
|
1365
|
+
assertCommitSurfaceRefreshInput(input);
|
|
1366
|
+
const now = this.clock();
|
|
1367
|
+
try {
|
|
1368
|
+
return runTransaction(this.database, () => {
|
|
1369
|
+
const snapshot = this.loadContextSnapshot();
|
|
1370
|
+
const baseRevision = snapshot.revision;
|
|
1371
|
+
if (
|
|
1372
|
+
baseRevision.revisionId !== input.expectedBaseRevisionId ||
|
|
1373
|
+
baseRevision.revisionNumber !== input.expectedBaseRevisionNumber ||
|
|
1374
|
+
snapshot.canonical.messages.length !==
|
|
1375
|
+
input.expectedCanonicalThroughOrdinal ||
|
|
1376
|
+
baseRevision.activeOverrideManifestSha256 !==
|
|
1377
|
+
input.expectedBaseActiveOverrideManifestSha256
|
|
1378
|
+
) {
|
|
1379
|
+
throw new Error("Context surface refresh base is stale.");
|
|
1380
|
+
}
|
|
1381
|
+
this.assertContextRevisionIdle();
|
|
1382
|
+
validateStoredContextSurface(input.surface);
|
|
1383
|
+
if (
|
|
1384
|
+
input.surface.sessionId !== this.sessionId ||
|
|
1385
|
+
input.surface.surfaceId === snapshot.surface.surfaceId ||
|
|
1386
|
+
input.surface.surfaceSha256 === snapshot.surface.surfaceSha256
|
|
1387
|
+
) {
|
|
1388
|
+
throw new Error("Context surface refresh does not introduce a new surface.");
|
|
1389
|
+
}
|
|
1390
|
+
const actualChanges = contextSurfaceChanges(snapshot.surface, input.surface);
|
|
1391
|
+
if (
|
|
1392
|
+
stableJsonStringify(actualChanges) !== stableJsonStringify(input.changes) ||
|
|
1393
|
+
contextSurfaceChangeManifestHash(actualChanges) !==
|
|
1394
|
+
input.changeManifestSha256 ||
|
|
1395
|
+
!Object.values(actualChanges).some(Boolean)
|
|
1396
|
+
) {
|
|
1397
|
+
throw new Error("Context surface refresh change manifest is invalid.");
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
const active = this.revisionCompiler.compileActive(snapshot);
|
|
1401
|
+
const candidate = this.revisionCompiler.compileProspective({
|
|
1402
|
+
active,
|
|
1403
|
+
canonical: snapshot.canonical,
|
|
1404
|
+
activeOverrides: snapshot.activeOverrides,
|
|
1405
|
+
addedOverrides: [],
|
|
1406
|
+
activeSurface: snapshot.surface,
|
|
1407
|
+
surface: input.surface,
|
|
1408
|
+
});
|
|
1409
|
+
if (
|
|
1410
|
+
canonicalSequenceHash(
|
|
1411
|
+
snapshot.canonical,
|
|
1412
|
+
input.expectedCanonicalThroughOrdinal,
|
|
1413
|
+
) !== input.canonicalSequenceSha256 ||
|
|
1414
|
+
renderedMessageHash(
|
|
1415
|
+
candidate.entries,
|
|
1416
|
+
input.expectedCanonicalThroughOrdinal,
|
|
1417
|
+
) !== input.renderedMessageSha256
|
|
1418
|
+
) {
|
|
1419
|
+
throw new Error("Candidate context surface prefix hash is invalid.");
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
const revisionNumber = baseRevision.revisionNumber + 1;
|
|
1423
|
+
options.faultInjector?.("before_surface_insert");
|
|
1424
|
+
insertContextSurface(this.database, input.surface);
|
|
1425
|
+
options.faultInjector?.("after_surface_insert");
|
|
1426
|
+
this.database
|
|
1427
|
+
.query(
|
|
1428
|
+
`INSERT INTO context_revisions (
|
|
1429
|
+
revision_id, session_id, revision_number, parent_revision_id, kind,
|
|
1430
|
+
surface_id, surface_sha256, keep_from_ordinal,
|
|
1431
|
+
source_through_ordinal, added_override_count, active_override_count,
|
|
1432
|
+
active_override_manifest_sha256, canonical_sequence_sha256,
|
|
1433
|
+
rendered_message_sha256, policy_version, renderer_format,
|
|
1434
|
+
plan_sha256, change_manifest_sha256, created_at
|
|
1435
|
+
) VALUES (?, ?, ?, ?, 'surface_refresh', ?, ?, ?, ?, 0, ?, ?, ?, ?, NULL, NULL, NULL, ?, ?)`,
|
|
1436
|
+
)
|
|
1437
|
+
.run(
|
|
1438
|
+
input.revisionId,
|
|
1439
|
+
this.sessionId,
|
|
1440
|
+
revisionNumber,
|
|
1441
|
+
baseRevision.revisionId,
|
|
1442
|
+
input.surface.surfaceId,
|
|
1443
|
+
input.surface.surfaceSha256,
|
|
1444
|
+
baseRevision.keepFromOrdinal,
|
|
1445
|
+
input.expectedCanonicalThroughOrdinal,
|
|
1446
|
+
baseRevision.activeOverrideCount,
|
|
1447
|
+
baseRevision.activeOverrideManifestSha256,
|
|
1448
|
+
input.canonicalSequenceSha256,
|
|
1449
|
+
input.renderedMessageSha256,
|
|
1450
|
+
input.changeManifestSha256,
|
|
1451
|
+
now,
|
|
1452
|
+
);
|
|
1453
|
+
options.faultInjector?.("after_revision_insert");
|
|
1454
|
+
|
|
1455
|
+
this.database.query("DELETE FROM context_measurement_state").run();
|
|
1456
|
+
options.faultInjector?.("after_measurement_delete");
|
|
1457
|
+
const switched = this.database
|
|
1458
|
+
.query(
|
|
1459
|
+
`UPDATE session_meta SET active_revision_id = ?, updated_at = ?
|
|
1460
|
+
WHERE singleton = 1 AND active_revision_id = ?`,
|
|
1461
|
+
)
|
|
1462
|
+
.run(input.revisionId, now, baseRevision.revisionId);
|
|
1463
|
+
requireSingleChange(
|
|
1464
|
+
this.database,
|
|
1465
|
+
switched.changes,
|
|
1466
|
+
"activate context surface revision",
|
|
1467
|
+
);
|
|
1468
|
+
options.faultInjector?.("after_active_update");
|
|
1469
|
+
|
|
1470
|
+
const readback = this.loadContextSnapshot();
|
|
1471
|
+
if (
|
|
1472
|
+
readback.revision.kind !== "surface_refresh" ||
|
|
1473
|
+
readback.revision.revisionId !== input.revisionId ||
|
|
1474
|
+
readback.surface.surfaceId !== input.surface.surfaceId ||
|
|
1475
|
+
this.loadMeasuredContextState() !== undefined
|
|
1476
|
+
) {
|
|
1477
|
+
throw new Error("Committed context surface revision readback failed.");
|
|
1478
|
+
}
|
|
1479
|
+
return readback.revision;
|
|
1480
|
+
});
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
if (requireActiveRevisionId(this.readMeta()) !== input.expectedBaseRevisionId) {
|
|
1483
|
+
throw new Error("Failed context surface transaction changed active state.", {
|
|
1484
|
+
cause: error,
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
throw sessionWriteError("commit_context_surface", this.sessionId, error);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
commitSkillsUpdate(
|
|
1492
|
+
input: CommitSkillsUpdateInput,
|
|
1493
|
+
options: CommitSkillsUpdateOptions = {},
|
|
1494
|
+
): Extract<StoredContextRevisionV8, { kind: "skills_update" }> {
|
|
1495
|
+
this.requireOpen();
|
|
1496
|
+
assertCommitSkillsUpdateInput(input);
|
|
1497
|
+
const now = this.clock();
|
|
1498
|
+
try {
|
|
1499
|
+
return runTransaction(this.database, () => {
|
|
1500
|
+
const snapshot = this.loadContextSnapshot();
|
|
1501
|
+
const baseRevision = snapshot.revision;
|
|
1502
|
+
if (
|
|
1503
|
+
baseRevision.revisionId !== input.expectedBaseRevisionId ||
|
|
1504
|
+
baseRevision.revisionNumber !== input.expectedBaseRevisionNumber ||
|
|
1505
|
+
snapshot.canonical.messages.length !==
|
|
1506
|
+
input.expectedCanonicalThroughOrdinal ||
|
|
1507
|
+
baseRevision.activeOverrideManifestSha256 !==
|
|
1508
|
+
input.expectedBaseActiveOverrideManifestSha256
|
|
1509
|
+
) {
|
|
1510
|
+
throw new Error("Agent Skills update base is stale.");
|
|
1511
|
+
}
|
|
1512
|
+
this.assertContextRevisionIdle();
|
|
1513
|
+
validateStoredContextSurface(input.surface);
|
|
1514
|
+
|
|
1515
|
+
const surfaceChanged =
|
|
1516
|
+
input.surface.surfaceSha256 !== snapshot.surface.surfaceSha256;
|
|
1517
|
+
const actualChanges = contextSurfaceChanges(snapshot.surface, input.surface);
|
|
1518
|
+
if (
|
|
1519
|
+
input.surface.sessionId !== this.sessionId ||
|
|
1520
|
+
stableJsonStringify(actualChanges) !== stableJsonStringify(input.changes) ||
|
|
1521
|
+
contextSurfaceChangeManifestHash(actualChanges) !==
|
|
1522
|
+
input.changeManifestSha256 ||
|
|
1523
|
+
(surfaceChanged && input.surface.surfaceId === snapshot.surface.surfaceId) ||
|
|
1524
|
+
(!surfaceChanged && input.surface.surfaceId !== snapshot.surface.surfaceId) ||
|
|
1525
|
+
(!surfaceChanged && Object.values(actualChanges).some(Boolean))
|
|
1526
|
+
) {
|
|
1527
|
+
throw new Error("Agent Skills surface change manifest is invalid.");
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
const unresolved = new Map(
|
|
1531
|
+
this.loadSkillActivations(["pending", "dispatched"]).map((entry) => [
|
|
1532
|
+
entry.activationMessageId,
|
|
1533
|
+
entry,
|
|
1534
|
+
]),
|
|
1535
|
+
);
|
|
1536
|
+
if (
|
|
1537
|
+
input.settlements.length !== input.addedOverrides.length ||
|
|
1538
|
+
input.settlements.length !== unresolved.size ||
|
|
1539
|
+
new Set(input.settlements.map((entry) => entry.activationMessageId)).size !==
|
|
1540
|
+
input.settlements.length ||
|
|
1541
|
+
skillActivationManifestSha256(input.settlements) !==
|
|
1542
|
+
input.activationManifestSha256
|
|
1543
|
+
) {
|
|
1544
|
+
throw new Error("Agent Skills settlement manifest is invalid.");
|
|
1545
|
+
}
|
|
1546
|
+
const canonicalMessages = new Map(
|
|
1547
|
+
snapshot.canonical.messages.map((message) => [message.messageId, message]),
|
|
1548
|
+
);
|
|
1549
|
+
const providedOverrides = new Map(
|
|
1550
|
+
input.addedOverrides.map((override) => [override.messageId, override]),
|
|
1551
|
+
);
|
|
1552
|
+
for (const settlement of input.settlements) {
|
|
1553
|
+
const activation = unresolved.get(settlement.activationMessageId);
|
|
1554
|
+
const message = canonicalMessages.get(settlement.activationMessageId);
|
|
1555
|
+
const override = providedOverrides.get(settlement.activationMessageId);
|
|
1556
|
+
if (
|
|
1557
|
+
activation === undefined ||
|
|
1558
|
+
message?.role !== "tool" ||
|
|
1559
|
+
message.name !== "Skill" ||
|
|
1560
|
+
override === undefined ||
|
|
1561
|
+
settlement.name !== activation.name ||
|
|
1562
|
+
(settlement.state === "promoted" &&
|
|
1563
|
+
(activation.state !== "dispatched" ||
|
|
1564
|
+
settlement.rejectionReason !== undefined)) ||
|
|
1565
|
+
(settlement.state === "rejected" &&
|
|
1566
|
+
(settlement.rejectionReason === undefined ||
|
|
1567
|
+
settlement.rejectionReason.trim() === "" ||
|
|
1568
|
+
settlement.rejectionReason.length > 256))
|
|
1569
|
+
) {
|
|
1570
|
+
throw new Error(
|
|
1571
|
+
`Agent Skill activation ${settlement.activationMessageId} cannot be settled.`,
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
const activeManifest = input.surface.activeSkills.find(
|
|
1575
|
+
(entry) =>
|
|
1576
|
+
entry.name === activation.name &&
|
|
1577
|
+
entry.activationMessageId === activation.activationMessageId,
|
|
1578
|
+
);
|
|
1579
|
+
if (
|
|
1580
|
+
(settlement.state === "promoted" && activeManifest === undefined) ||
|
|
1581
|
+
(settlement.state === "rejected" && activeManifest !== undefined)
|
|
1582
|
+
) {
|
|
1583
|
+
throw new Error("Agent Skills active manifest does not match settlements.");
|
|
1584
|
+
}
|
|
1585
|
+
const expected = renderSkillActivationReceipt({
|
|
1586
|
+
message: {
|
|
1587
|
+
messageId: message.messageId,
|
|
1588
|
+
frameId: message.frameId,
|
|
1589
|
+
ordinal: message.ordinal,
|
|
1590
|
+
content: message.content,
|
|
1591
|
+
contentSha256: message.contentSha256,
|
|
1592
|
+
},
|
|
1593
|
+
name: activation.name,
|
|
1594
|
+
outcome:
|
|
1595
|
+
settlement.state === "promoted"
|
|
1596
|
+
? "promoted"
|
|
1597
|
+
: settlement.rejectionReason === "unavailable"
|
|
1598
|
+
? "unavailable"
|
|
1599
|
+
: "rejected",
|
|
1600
|
+
});
|
|
1601
|
+
if (stableJsonStringify(expected) !== stableJsonStringify(override)) {
|
|
1602
|
+
throw new Error("Agent Skill activation receipt is not deterministic.");
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
const active = this.revisionCompiler.compileActive(snapshot);
|
|
1607
|
+
const candidateOverrides = [
|
|
1608
|
+
...snapshot.activeOverrides,
|
|
1609
|
+
...input.addedOverrides,
|
|
1610
|
+
];
|
|
1611
|
+
if (
|
|
1612
|
+
new Set(candidateOverrides.map((override) => override.messageId)).size !==
|
|
1613
|
+
candidateOverrides.length ||
|
|
1614
|
+
activeOverrideManifestHash(candidateOverrides) !==
|
|
1615
|
+
input.nextActiveOverrideManifestSha256
|
|
1616
|
+
) {
|
|
1617
|
+
throw new Error("Agent Skills override manifest is invalid.");
|
|
1618
|
+
}
|
|
1619
|
+
const candidate = this.revisionCompiler.compileProspective({
|
|
1620
|
+
active,
|
|
1621
|
+
canonical: snapshot.canonical,
|
|
1622
|
+
activeOverrides: snapshot.activeOverrides,
|
|
1623
|
+
addedOverrides: input.addedOverrides,
|
|
1624
|
+
activeSurface: snapshot.surface,
|
|
1625
|
+
...(surfaceChanged ? { surface: input.surface } : {}),
|
|
1626
|
+
allowCombinedSurfaceAndOverrides: true,
|
|
1627
|
+
});
|
|
1628
|
+
if (
|
|
1629
|
+
canonicalSequenceHash(
|
|
1630
|
+
snapshot.canonical,
|
|
1631
|
+
input.expectedCanonicalThroughOrdinal,
|
|
1632
|
+
) !== input.canonicalSequenceSha256 ||
|
|
1633
|
+
renderedMessageHash(
|
|
1634
|
+
candidate.entries,
|
|
1635
|
+
input.expectedCanonicalThroughOrdinal,
|
|
1636
|
+
) !== input.renderedMessageSha256
|
|
1637
|
+
) {
|
|
1638
|
+
throw new Error("Agent Skills compiled context hashes are invalid.");
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
options.faultInjector?.("before_surface_insert");
|
|
1642
|
+
if (surfaceChanged) {
|
|
1643
|
+
insertContextSurface(this.database, input.surface);
|
|
1644
|
+
}
|
|
1645
|
+
options.faultInjector?.("after_surface_insert");
|
|
1646
|
+
const revisionNumber = baseRevision.revisionNumber + 1;
|
|
1647
|
+
const activeOverrideCount =
|
|
1648
|
+
baseRevision.activeOverrideCount + input.addedOverrides.length;
|
|
1649
|
+
this.database
|
|
1650
|
+
.query(
|
|
1651
|
+
`INSERT INTO context_revisions (
|
|
1652
|
+
revision_id, session_id, revision_number, parent_revision_id, kind,
|
|
1653
|
+
surface_id, surface_sha256, keep_from_ordinal,
|
|
1654
|
+
source_through_ordinal, added_override_count, active_override_count,
|
|
1655
|
+
active_override_manifest_sha256, canonical_sequence_sha256,
|
|
1656
|
+
rendered_message_sha256, policy_version, renderer_format,
|
|
1657
|
+
plan_sha256, change_manifest_sha256, activation_manifest_sha256,
|
|
1658
|
+
retired_through_ordinal, retired_turn_count, retired_frame_count,
|
|
1659
|
+
retired_message_count, created_at
|
|
1660
|
+
) VALUES (?, ?, ?, ?, 'skills_update', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, NULL, NULL, NULL, NULL, ?)`,
|
|
1661
|
+
)
|
|
1662
|
+
.run(
|
|
1663
|
+
input.revisionId,
|
|
1664
|
+
this.sessionId,
|
|
1665
|
+
revisionNumber,
|
|
1666
|
+
baseRevision.revisionId,
|
|
1667
|
+
input.surface.surfaceId,
|
|
1668
|
+
input.surface.surfaceSha256,
|
|
1669
|
+
baseRevision.keepFromOrdinal,
|
|
1670
|
+
input.expectedCanonicalThroughOrdinal,
|
|
1671
|
+
input.addedOverrides.length,
|
|
1672
|
+
activeOverrideCount,
|
|
1673
|
+
input.nextActiveOverrideManifestSha256,
|
|
1674
|
+
input.canonicalSequenceSha256,
|
|
1675
|
+
input.renderedMessageSha256,
|
|
1676
|
+
SKILL_POLICY_VERSION,
|
|
1677
|
+
SKILL_ACTIVATION_RECEIPT_FORMAT,
|
|
1678
|
+
input.changeManifestSha256,
|
|
1679
|
+
input.activationManifestSha256,
|
|
1680
|
+
now,
|
|
1681
|
+
);
|
|
1682
|
+
options.faultInjector?.("after_revision_insert");
|
|
1683
|
+
|
|
1684
|
+
for (let index = 0; index < input.addedOverrides.length; index += 1) {
|
|
1685
|
+
const override = requireItem(
|
|
1686
|
+
input.addedOverrides,
|
|
1687
|
+
index,
|
|
1688
|
+
"Agent Skill receipt override",
|
|
1689
|
+
);
|
|
1690
|
+
this.database
|
|
1691
|
+
.query(
|
|
1692
|
+
`INSERT INTO context_overrides (
|
|
1693
|
+
introduced_revision_id, session_id, message_id, frame_id, ordinal,
|
|
1694
|
+
representation, renderer_format, source, original_content_sha256,
|
|
1695
|
+
rendered_content, rendered_content_sha256, original_bytes,
|
|
1696
|
+
rendered_bytes, byte_savings, created_at
|
|
1697
|
+
) VALUES (?, ?, ?, ?, ?, 'swapped', ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1698
|
+
)
|
|
1699
|
+
.run(
|
|
1700
|
+
input.revisionId,
|
|
1701
|
+
this.sessionId,
|
|
1702
|
+
override.messageId,
|
|
1703
|
+
override.frameId,
|
|
1704
|
+
override.ordinal,
|
|
1705
|
+
SKILL_ACTIVATION_RECEIPT_FORMAT,
|
|
1706
|
+
override.source,
|
|
1707
|
+
override.originalContentSha256,
|
|
1708
|
+
override.renderedContent,
|
|
1709
|
+
override.renderedContentSha256,
|
|
1710
|
+
override.originalBytes,
|
|
1711
|
+
override.renderedBytes,
|
|
1712
|
+
override.byteSavings,
|
|
1713
|
+
now,
|
|
1714
|
+
);
|
|
1715
|
+
if (index === 0) {
|
|
1716
|
+
options.faultInjector?.("after_first_override_insert");
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
options.faultInjector?.("after_overrides_insert");
|
|
1720
|
+
|
|
1721
|
+
for (const settlement of input.settlements) {
|
|
1722
|
+
const activation = unresolved.get(settlement.activationMessageId)!;
|
|
1723
|
+
const updated =
|
|
1724
|
+
settlement.state === "promoted"
|
|
1725
|
+
? this.database
|
|
1726
|
+
.query(
|
|
1727
|
+
`UPDATE skill_activations
|
|
1728
|
+
SET state = 'promoted', settled_revision_id = ?, updated_at = ?
|
|
1729
|
+
WHERE activation_message_id = ? AND state = 'dispatched'`,
|
|
1730
|
+
)
|
|
1731
|
+
.run(input.revisionId, now, settlement.activationMessageId)
|
|
1732
|
+
: this.database
|
|
1733
|
+
.query(
|
|
1734
|
+
`UPDATE skill_activations
|
|
1735
|
+
SET state = 'rejected', settled_revision_id = ?, rejection_reason = ?, updated_at = ?
|
|
1736
|
+
WHERE activation_message_id = ? AND state = ?`,
|
|
1737
|
+
)
|
|
1738
|
+
.run(
|
|
1739
|
+
input.revisionId,
|
|
1740
|
+
settlement.rejectionReason!,
|
|
1741
|
+
now,
|
|
1742
|
+
settlement.activationMessageId,
|
|
1743
|
+
activation.state,
|
|
1744
|
+
);
|
|
1745
|
+
requireSingleChange(
|
|
1746
|
+
this.database,
|
|
1747
|
+
updated.changes,
|
|
1748
|
+
`settle Agent Skill activation ${settlement.activationMessageId}`,
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
options.faultInjector?.("after_activations_update");
|
|
1752
|
+
|
|
1753
|
+
this.database.query("DELETE FROM context_measurement_state").run();
|
|
1754
|
+
options.faultInjector?.("after_measurement_delete");
|
|
1755
|
+
const switched = this.database
|
|
1756
|
+
.query(
|
|
1757
|
+
`UPDATE session_meta SET active_revision_id = ?, updated_at = ?
|
|
1758
|
+
WHERE singleton = 1 AND active_revision_id = ?`,
|
|
1759
|
+
)
|
|
1760
|
+
.run(input.revisionId, now, baseRevision.revisionId);
|
|
1761
|
+
requireSingleChange(
|
|
1762
|
+
this.database,
|
|
1763
|
+
switched.changes,
|
|
1764
|
+
"activate Agent Skills context revision",
|
|
1765
|
+
);
|
|
1766
|
+
options.faultInjector?.("after_active_update");
|
|
1767
|
+
|
|
1768
|
+
const readback = this.loadContextSnapshot();
|
|
1769
|
+
if (
|
|
1770
|
+
readback.revision.kind !== "skills_update" ||
|
|
1771
|
+
readback.revision.revisionId !== input.revisionId ||
|
|
1772
|
+
readback.surface.surfaceId !== input.surface.surfaceId ||
|
|
1773
|
+
this.loadMeasuredContextState() !== undefined ||
|
|
1774
|
+
this.loadSkillActivations(["pending", "dispatched"]).some((entry) =>
|
|
1775
|
+
input.settlements.some(
|
|
1776
|
+
(settlement) =>
|
|
1777
|
+
settlement.activationMessageId === entry.activationMessageId,
|
|
1778
|
+
),
|
|
1779
|
+
)
|
|
1780
|
+
) {
|
|
1781
|
+
throw new Error("Committed Agent Skills update readback failed.");
|
|
1782
|
+
}
|
|
1783
|
+
return readback.revision;
|
|
1784
|
+
});
|
|
1785
|
+
} catch (error) {
|
|
1786
|
+
if (requireActiveRevisionId(this.readMeta()) !== input.expectedBaseRevisionId) {
|
|
1787
|
+
throw new Error("Failed Agent Skills transaction changed active state.", {
|
|
1788
|
+
cause: error,
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
throw sessionWriteError("commit_skills_update", this.sessionId, error);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
private validateAddedOverrides(
|
|
1796
|
+
overrides: readonly SwapOverride[],
|
|
1797
|
+
canonical: ProtocolContextView,
|
|
1798
|
+
): void {
|
|
1799
|
+
const messages = new Map(
|
|
1800
|
+
canonical.messages.map((message) => [message.messageId, message] as const),
|
|
1801
|
+
);
|
|
1802
|
+
const results = new Map(
|
|
1803
|
+
canonical.toolResults.map((result) => [result.toolMessageId, result] as const),
|
|
1804
|
+
);
|
|
1805
|
+
for (const override of overrides) {
|
|
1806
|
+
const message = messages.get(override.messageId);
|
|
1807
|
+
const result = results.get(override.messageId);
|
|
1808
|
+
if (message?.role !== "tool" || result === undefined) {
|
|
1809
|
+
throw new Error("Added context override does not target a tool result.");
|
|
1810
|
+
}
|
|
1811
|
+
const expected = this.swapRenderer.render({ message, result });
|
|
1812
|
+
if (stableJsonStringify(expected) !== stableJsonStringify(override)) {
|
|
1813
|
+
throw new Error("Added context override is not deterministic.");
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
loadSkillActivations(
|
|
1819
|
+
states?: readonly StoredSkillActivation["state"][],
|
|
1820
|
+
): readonly StoredSkillActivation[] {
|
|
1821
|
+
this.requireOpen();
|
|
1822
|
+
const rows = this.database
|
|
1823
|
+
.query(
|
|
1824
|
+
`SELECT sa.*, m.ordinal AS activation_ordinal
|
|
1825
|
+
FROM skill_activations sa
|
|
1826
|
+
JOIN messages m ON m.message_id = sa.activation_message_id
|
|
1827
|
+
ORDER BY m.ordinal`,
|
|
1828
|
+
)
|
|
1829
|
+
.all()
|
|
1830
|
+
.map(decodeSkillActivation);
|
|
1831
|
+
const filtered =
|
|
1832
|
+
states === undefined ? rows : rows.filter((row) => states.includes(row.state));
|
|
1833
|
+
return Object.freeze(filtered);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
markSkillActivationsDispatched(input: {
|
|
1837
|
+
iterationId: IterationId;
|
|
1838
|
+
activationMessageIds: readonly MessageId[];
|
|
1839
|
+
}): readonly StoredSkillActivation[] {
|
|
1840
|
+
this.requireOpen();
|
|
1841
|
+
if (input.activationMessageIds.length === 0) {
|
|
1842
|
+
return Object.freeze([]);
|
|
1843
|
+
}
|
|
1844
|
+
if (
|
|
1845
|
+
new Set(input.activationMessageIds).size !== input.activationMessageIds.length
|
|
1846
|
+
) {
|
|
1847
|
+
throw new Error("Agent Skill dispatch contains duplicate activation messages.");
|
|
1848
|
+
}
|
|
1849
|
+
const now = this.clock();
|
|
1850
|
+
try {
|
|
1851
|
+
return runTransaction(this.database, () => {
|
|
1852
|
+
const iteration = this.requireIterationRow(input.iterationId);
|
|
1853
|
+
if (iteration.outcome !== "open") {
|
|
1854
|
+
throw new Error(`Iteration ${input.iterationId} is not open for dispatch.`);
|
|
1855
|
+
}
|
|
1856
|
+
for (const messageId of input.activationMessageIds) {
|
|
1857
|
+
const updated = this.database
|
|
1858
|
+
.query(
|
|
1859
|
+
`UPDATE skill_activations
|
|
1860
|
+
SET state = 'dispatched', dispatched_iteration_id = ?, updated_at = ?
|
|
1861
|
+
WHERE activation_message_id = ? AND session_id = ? AND state = 'pending'`,
|
|
1862
|
+
)
|
|
1863
|
+
.run(input.iterationId, now, messageId, this.sessionId);
|
|
1864
|
+
requireSingleChange(
|
|
1865
|
+
this.database,
|
|
1866
|
+
updated.changes,
|
|
1867
|
+
`dispatch Agent Skill activation ${messageId}`,
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
this.touch(now);
|
|
1871
|
+
const dispatched = this.loadSkillActivations(["dispatched"]).filter((row) =>
|
|
1872
|
+
input.activationMessageIds.includes(row.activationMessageId),
|
|
1873
|
+
);
|
|
1874
|
+
if (dispatched.length !== input.activationMessageIds.length) {
|
|
1875
|
+
throw new Error("Agent Skill dispatch readback failed.");
|
|
1876
|
+
}
|
|
1877
|
+
return Object.freeze(dispatched);
|
|
1878
|
+
});
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
throw sessionWriteError("dispatch_skill_activations", this.sessionId, error);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
markResumed(): number {
|
|
1885
|
+
this.requireOpen();
|
|
1886
|
+
const now = this.clock();
|
|
1887
|
+
try {
|
|
1888
|
+
return runTransaction(this.database, () => {
|
|
1889
|
+
const meta = this.readMeta();
|
|
1890
|
+
const next = meta.openCount + 1;
|
|
1891
|
+
const updated = this.database
|
|
1892
|
+
.query(
|
|
1893
|
+
`UPDATE session_meta
|
|
1894
|
+
SET open_count = ?, last_opened_at = ?, updated_at = ?,
|
|
1895
|
+
last_closed_at = NULL, last_close_reason = NULL
|
|
1896
|
+
WHERE singleton = 1 AND open_count = ?`,
|
|
1897
|
+
)
|
|
1898
|
+
.run(next, now, now, meta.openCount);
|
|
1899
|
+
requireSingleChange(this.database, updated.changes, "increment open count");
|
|
1900
|
+
return next;
|
|
1901
|
+
});
|
|
1902
|
+
} catch (error) {
|
|
1903
|
+
throw sessionWriteError("mark_resumed", this.sessionId, error);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
recoverInterruptedState(idFactory: RuntimeIdFactory): SessionRecoveryResult {
|
|
1908
|
+
this.requireOpen();
|
|
1909
|
+
const view = this.loadProtocolView();
|
|
1910
|
+
const openTurns = this.database
|
|
1911
|
+
.query("SELECT turn_id FROM turns WHERE status = 'open' ORDER BY turn_number")
|
|
1912
|
+
.all() as Array<{ turn_id: string }>;
|
|
1913
|
+
const openFrames = view.frames.filter((frame) => frame.state === "open");
|
|
921
1914
|
if (openTurns.length === 0 && openFrames.length === 0) {
|
|
922
1915
|
return {
|
|
923
1916
|
syntheticCompletionCount: 0,
|
|
@@ -1091,6 +2084,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1091
2084
|
|
|
1092
2085
|
loadProtocolView(): ProtocolContextView {
|
|
1093
2086
|
this.requireOpen();
|
|
2087
|
+
const imageAttachments = loadMessageImageAttachments(this.database);
|
|
1094
2088
|
const frames = this.database
|
|
1095
2089
|
.query("SELECT * FROM protocol_frames ORDER BY first_ordinal")
|
|
1096
2090
|
.all()
|
|
@@ -1098,7 +2092,16 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1098
2092
|
const messages = this.database
|
|
1099
2093
|
.query("SELECT * FROM messages ORDER BY ordinal")
|
|
1100
2094
|
.all()
|
|
1101
|
-
.map(
|
|
2095
|
+
.map((row) => {
|
|
2096
|
+
const record = recordFromSql(row, "message");
|
|
2097
|
+
const messageId = stringFromSql(record.message_id, "message_id");
|
|
2098
|
+
const message = decodeMessage(row, imageAttachments.get(messageId));
|
|
2099
|
+
imageAttachments.delete(messageId);
|
|
2100
|
+
return message;
|
|
2101
|
+
});
|
|
2102
|
+
if (imageAttachments.size > 0) {
|
|
2103
|
+
throw new Error("Image attachment rows reference unknown messages.");
|
|
2104
|
+
}
|
|
1102
2105
|
const toolResults = this.database
|
|
1103
2106
|
.query(
|
|
1104
2107
|
`SELECT tr.* FROM tool_results tr
|
|
@@ -1116,13 +2119,43 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1116
2119
|
});
|
|
1117
2120
|
}
|
|
1118
2121
|
|
|
1119
|
-
|
|
2122
|
+
async verifyImageAssetFiles(): Promise<void> {
|
|
2123
|
+
this.requireOpen();
|
|
2124
|
+
const distinct = new Map<string, ImageAssetRef>();
|
|
2125
|
+
for (const message of this.loadProtocolView().messages) {
|
|
2126
|
+
if (message.role !== "user") {
|
|
2127
|
+
continue;
|
|
2128
|
+
}
|
|
2129
|
+
for (const attachment of message.attachments ?? []) {
|
|
2130
|
+
const previous = distinct.get(attachment.assetId);
|
|
2131
|
+
const asset = imageAssetRefFromAttachment(attachment);
|
|
2132
|
+
if (
|
|
2133
|
+
previous !== undefined &&
|
|
2134
|
+
stableJsonStringify(previous) !== stableJsonStringify(asset)
|
|
2135
|
+
) {
|
|
2136
|
+
throw new Error(
|
|
2137
|
+
`Conflicting metadata for image asset ${attachment.assetId}.`,
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
distinct.set(attachment.assetId, asset);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
if (distinct.size === 0) {
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
const store = await ImageAssetStore.open({ workspaceRoot: this.workspaceRoot });
|
|
2147
|
+
for (const asset of distinct.values()) {
|
|
2148
|
+
await store.verify(asset);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
loadContextSnapshot(): StoredContextSnapshotV8 {
|
|
1120
2153
|
this.requireOpen();
|
|
1121
2154
|
const meta = this.readMeta();
|
|
1122
2155
|
try {
|
|
1123
2156
|
if (
|
|
1124
2157
|
meta.sessionId !== this.sessionId ||
|
|
1125
|
-
meta.schemaFingerprint !==
|
|
2158
|
+
meta.schemaFingerprint !== SESSION_SCHEMA_V9_FINGERPRINT
|
|
1126
2159
|
) {
|
|
1127
2160
|
throw new Error("Session metadata identity or schema fingerprint changed.");
|
|
1128
2161
|
}
|
|
@@ -1168,7 +2201,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1168
2201
|
}
|
|
1169
2202
|
}
|
|
1170
2203
|
|
|
1171
|
-
readMeta():
|
|
2204
|
+
readMeta(): StoredSessionMetaV9 {
|
|
1172
2205
|
this.requireOpen();
|
|
1173
2206
|
const rows = this.database.query("SELECT * FROM session_meta").all();
|
|
1174
2207
|
if (rows.length !== 1) {
|
|
@@ -1182,7 +2215,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1182
2215
|
return decodeMeta(rows[0], this.sessionId);
|
|
1183
2216
|
}
|
|
1184
2217
|
|
|
1185
|
-
|
|
2218
|
+
readCreationSystemPrompt(): string {
|
|
1186
2219
|
this.requireOpen();
|
|
1187
2220
|
try {
|
|
1188
2221
|
const frames = this.database
|
|
@@ -1235,8 +2268,8 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1235
2268
|
}
|
|
1236
2269
|
throw new SessionError(
|
|
1237
2270
|
"SESSION_RECOVERY_FAILED",
|
|
1238
|
-
"
|
|
1239
|
-
"
|
|
2271
|
+
"read_creation_system_prompt",
|
|
2272
|
+
"Creation system prompt is missing or invalid.",
|
|
1240
2273
|
{ sessionId: this.sessionId, cause: error },
|
|
1241
2274
|
);
|
|
1242
2275
|
}
|
|
@@ -1246,6 +2279,56 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1246
2279
|
return this.readMeta().projectInstruction;
|
|
1247
2280
|
}
|
|
1248
2281
|
|
|
2282
|
+
validateCreatingState(): void {
|
|
2283
|
+
this.requireOpen();
|
|
2284
|
+
const meta = this.readMeta();
|
|
2285
|
+
if (
|
|
2286
|
+
meta.initializationState !== "creating" ||
|
|
2287
|
+
meta.activeRevisionId !== null ||
|
|
2288
|
+
meta.sessionCompatibilityJson !== null ||
|
|
2289
|
+
meta.sessionCompatibilitySha256 !== null
|
|
2290
|
+
) {
|
|
2291
|
+
throw new SessionError(
|
|
2292
|
+
"SESSION_INTEGRITY_FAILED",
|
|
2293
|
+
"validate_creating_store",
|
|
2294
|
+
"Creating session metadata is invalid.",
|
|
2295
|
+
{ sessionId: this.sessionId },
|
|
2296
|
+
);
|
|
2297
|
+
}
|
|
2298
|
+
this.readCreationSystemPrompt();
|
|
2299
|
+
const counts = this.database
|
|
2300
|
+
.query(
|
|
2301
|
+
`SELECT
|
|
2302
|
+
(SELECT COUNT(*) FROM context_surfaces) AS surfaces,
|
|
2303
|
+
(SELECT COUNT(*) FROM context_revisions) AS revisions,
|
|
2304
|
+
(SELECT COUNT(*) FROM turns) AS turns`,
|
|
2305
|
+
)
|
|
2306
|
+
.get() as Record<string, unknown> | null;
|
|
2307
|
+
if (
|
|
2308
|
+
counts === null ||
|
|
2309
|
+
numberFromSql(counts.surfaces, "surfaces") !== 0 ||
|
|
2310
|
+
numberFromSql(counts.revisions, "revisions") !== 0 ||
|
|
2311
|
+
numberFromSql(counts.turns, "turns") !== 0
|
|
2312
|
+
) {
|
|
2313
|
+
throw new SessionError(
|
|
2314
|
+
"SESSION_INTEGRITY_FAILED",
|
|
2315
|
+
"validate_creating_store",
|
|
2316
|
+
"Creating session contains finalized or turn state.",
|
|
2317
|
+
{ sessionId: this.sessionId },
|
|
2318
|
+
);
|
|
2319
|
+
}
|
|
2320
|
+
const view = this.loadProtocolView();
|
|
2321
|
+
this.validator.validate(view, { fullIntegrity: true });
|
|
2322
|
+
if (view.messages.length !== 1 || view.frames.length !== 1) {
|
|
2323
|
+
throw new SessionError(
|
|
2324
|
+
"SESSION_INTEGRITY_FAILED",
|
|
2325
|
+
"validate_creating_store",
|
|
2326
|
+
"Creating session must contain only its creation system frame.",
|
|
2327
|
+
{ sessionId: this.sessionId },
|
|
2328
|
+
);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
1249
2332
|
nextTurnNumber(): number {
|
|
1250
2333
|
return this.readMeta().nextTurnNumber;
|
|
1251
2334
|
}
|
|
@@ -1254,7 +2337,9 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1254
2337
|
const meta = this.readMeta();
|
|
1255
2338
|
if (
|
|
1256
2339
|
meta.sessionId !== this.sessionId ||
|
|
1257
|
-
meta.schemaFingerprint !==
|
|
2340
|
+
meta.schemaFingerprint !== SESSION_SCHEMA_V9_FINGERPRINT ||
|
|
2341
|
+
meta.initializationState !== "ready" ||
|
|
2342
|
+
meta.activeRevisionId === null
|
|
1258
2343
|
) {
|
|
1259
2344
|
throw new SessionError(
|
|
1260
2345
|
"SESSION_SCHEMA_INVALID",
|
|
@@ -1263,15 +2348,16 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1263
2348
|
{ sessionId: this.sessionId },
|
|
1264
2349
|
);
|
|
1265
2350
|
}
|
|
1266
|
-
this.readStoredSystemPrompt();
|
|
1267
|
-
const view = this.loadProtocolView();
|
|
1268
2351
|
try {
|
|
2352
|
+
this.readCreationSystemPrompt();
|
|
2353
|
+
const view = this.loadProtocolView();
|
|
1269
2354
|
this.validator.validate(view, {
|
|
1270
2355
|
allowOpenTail: options.allowOpenTail,
|
|
1271
2356
|
fullIntegrity: true,
|
|
1272
2357
|
});
|
|
1273
2358
|
this.loadValidatedContextSnapshot(meta, view);
|
|
1274
2359
|
this.validateCounters(meta, view);
|
|
2360
|
+
return view;
|
|
1275
2361
|
} catch (error) {
|
|
1276
2362
|
if (error instanceof SessionError) {
|
|
1277
2363
|
throw error;
|
|
@@ -1297,7 +2383,121 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1297
2383
|
{ sessionId: this.sessionId, cause: error },
|
|
1298
2384
|
);
|
|
1299
2385
|
}
|
|
1300
|
-
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
async cloneTo(input: CloneSessionStoreInput): Promise<void> {
|
|
2389
|
+
this.requireOpen();
|
|
2390
|
+
if (input.targetSessionId === this.sessionId) {
|
|
2391
|
+
throw new Error("Session clone target must differ from the source session.");
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
const sourceView = this.validateAll({ allowOpenTail: false });
|
|
2395
|
+
const sourceMeta = this.readMeta();
|
|
2396
|
+
const sessionsRoot = path.dirname(this.sessionDirectory);
|
|
2397
|
+
await validateSessionsRoot(sessionsRoot, this.sessionId);
|
|
2398
|
+
const targetDirectory = safeSessionDirectory(sessionsRoot, input.targetSessionId);
|
|
2399
|
+
await assertPathMissing(targetDirectory, input.targetSessionId);
|
|
2400
|
+
|
|
2401
|
+
const stagingDirectory = path.join(sessionsRoot, `.cloning-${randomUUID()}`);
|
|
2402
|
+
const stagingDatabasePath = path.join(stagingDirectory, "session.sqlite");
|
|
2403
|
+
let stagingDatabase: Database | undefined;
|
|
2404
|
+
let published = false;
|
|
2405
|
+
|
|
2406
|
+
try {
|
|
2407
|
+
await mkdir(stagingDirectory, { mode: 0o700 });
|
|
2408
|
+
await chmod(stagingDirectory, 0o700);
|
|
2409
|
+
input.faultInjector?.("after_staging_mkdir");
|
|
2410
|
+
this.database.query("VACUUM INTO ?").run(stagingDatabasePath);
|
|
2411
|
+
await chmod(stagingDatabasePath, 0o600);
|
|
2412
|
+
input.faultInjector?.("after_snapshot");
|
|
2413
|
+
|
|
2414
|
+
stagingDatabase = openWritableDatabase(stagingDatabasePath);
|
|
2415
|
+
verifySessionSchema(stagingDatabase, this.sessionId);
|
|
2416
|
+
dropSessionCloneTriggers(stagingDatabase);
|
|
2417
|
+
input.faultInjector?.("after_trigger_drop");
|
|
2418
|
+
|
|
2419
|
+
const targetCanonical = rekeyProtocolView(sourceView, input.targetSessionId);
|
|
2420
|
+
runTransaction(stagingDatabase, () => {
|
|
2421
|
+
stagingDatabase!.exec("PRAGMA defer_foreign_keys = ON");
|
|
2422
|
+
for (const table of SESSION_SCOPED_TABLES) {
|
|
2423
|
+
stagingDatabase!
|
|
2424
|
+
.query(`UPDATE ${table} SET session_id = ? WHERE session_id = ?`)
|
|
2425
|
+
.run(input.targetSessionId, this.sessionId);
|
|
2426
|
+
}
|
|
2427
|
+
rekeyStoredToolCalls(stagingDatabase!, input.targetSessionId);
|
|
2428
|
+
input.faultInjector?.("after_identity_update");
|
|
2429
|
+
|
|
2430
|
+
rewriteCloneRevisionHashes(stagingDatabase!, targetCanonical);
|
|
2431
|
+
input.faultInjector?.("after_revision_hash_rewrite");
|
|
2432
|
+
|
|
2433
|
+
if (stagingDatabase!.query("PRAGMA foreign_key_check").all().length !== 0) {
|
|
2434
|
+
throw new Error("Cloned session identity re-key broke foreign keys.");
|
|
2435
|
+
}
|
|
2436
|
+
});
|
|
2437
|
+
|
|
2438
|
+
reinstallSessionCloneTriggers(stagingDatabase);
|
|
2439
|
+
input.faultInjector?.("after_trigger_reinstall");
|
|
2440
|
+
verifySessionSchema(stagingDatabase, input.targetSessionId);
|
|
2441
|
+
verifySqliteIntegrity(stagingDatabase, input.targetSessionId);
|
|
2442
|
+
verifyRecallIndex(stagingDatabase, input.targetSessionId);
|
|
2443
|
+
input.faultInjector?.("after_recall_validation");
|
|
2444
|
+
const clonedStore = new SessionStore(stagingDatabase, this.lease, {
|
|
2445
|
+
sessionId: input.targetSessionId,
|
|
2446
|
+
workspaceRoot: this.workspaceRoot,
|
|
2447
|
+
sessionDirectory: stagingDirectory,
|
|
2448
|
+
databasePath: stagingDatabasePath,
|
|
2449
|
+
clock: this.clock,
|
|
2450
|
+
});
|
|
2451
|
+
clonedStore.validateAll({ allowOpenTail: false });
|
|
2452
|
+
await clonedStore.verifyImageAssetFiles();
|
|
2453
|
+
|
|
2454
|
+
await cloneDiagnosticFiles({
|
|
2455
|
+
sourceDirectory: this.sessionDirectory,
|
|
2456
|
+
stagingDirectory,
|
|
2457
|
+
sourceSessionId: this.sessionId,
|
|
2458
|
+
targetSessionId: input.targetSessionId,
|
|
2459
|
+
nextEventSequence: sourceMeta.nextEventSequence,
|
|
2460
|
+
faultInjector: input.faultInjector,
|
|
2461
|
+
});
|
|
2462
|
+
|
|
2463
|
+
stagingDatabase.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
2464
|
+
const standaloneJournal = stagingDatabase
|
|
2465
|
+
.query("PRAGMA journal_mode = DELETE")
|
|
2466
|
+
.get() as Record<string, unknown> | null;
|
|
2467
|
+
if (String(standaloneJournal?.journal_mode).toLowerCase() !== "delete") {
|
|
2468
|
+
throw new Error("Cloned session database did not leave WAL mode.");
|
|
2469
|
+
}
|
|
2470
|
+
stagingDatabase.close();
|
|
2471
|
+
stagingDatabase = undefined;
|
|
2472
|
+
await unlinkIfExists(`${stagingDatabasePath}-wal`);
|
|
2473
|
+
await unlinkIfExists(`${stagingDatabasePath}-shm`);
|
|
2474
|
+
await validateSecureDirectory(stagingDirectory, input.targetSessionId);
|
|
2475
|
+
await validateSecureFile(stagingDatabasePath, input.targetSessionId);
|
|
2476
|
+
await validateSecureOptionalFile(
|
|
2477
|
+
path.join(stagingDirectory, "events.jsonl"),
|
|
2478
|
+
input.targetSessionId,
|
|
2479
|
+
);
|
|
2480
|
+
await validateSecureOptionalFile(
|
|
2481
|
+
path.join(stagingDirectory, "observations.md"),
|
|
2482
|
+
input.targetSessionId,
|
|
2483
|
+
);
|
|
2484
|
+
input.faultInjector?.("after_artifact_validation");
|
|
2485
|
+
await assertPathMissing(targetDirectory, input.targetSessionId);
|
|
2486
|
+
input.faultInjector?.("before_publish_rename");
|
|
2487
|
+
await rename(stagingDirectory, targetDirectory);
|
|
2488
|
+
published = true;
|
|
2489
|
+
} finally {
|
|
2490
|
+
if (stagingDatabase !== undefined) {
|
|
2491
|
+
try {
|
|
2492
|
+
stagingDatabase.close();
|
|
2493
|
+
} catch {
|
|
2494
|
+
// Preserve the clone failure.
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
if (!published) {
|
|
2498
|
+
await removeKnownInitializationFiles(stagingDirectory).catch(() => undefined);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
1301
2501
|
}
|
|
1302
2502
|
|
|
1303
2503
|
async close(reason: SessionCloseReason): Promise<void> {
|
|
@@ -1404,6 +2604,24 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1404
2604
|
now: string,
|
|
1405
2605
|
): void {
|
|
1406
2606
|
const meta = this.readMeta();
|
|
2607
|
+
if (mutation.admissionBase !== undefined) {
|
|
2608
|
+
const snapshot = this.loadContextSnapshot();
|
|
2609
|
+
const head = snapshot.canonical.messages.at(-1);
|
|
2610
|
+
const base = mutation.admissionBase;
|
|
2611
|
+
if (
|
|
2612
|
+
head === undefined ||
|
|
2613
|
+
base.canonicalMessageCount !== snapshot.canonical.messages.length ||
|
|
2614
|
+
base.canonicalHeadMessageId !== head.messageId ||
|
|
2615
|
+
base.canonicalHeadContentSha256 !== head.contentSha256 ||
|
|
2616
|
+
base.activeRevisionId !== snapshot.revision.revisionId ||
|
|
2617
|
+
base.activeRevisionNumber !== snapshot.revision.revisionNumber ||
|
|
2618
|
+
base.surfaceSha256 !== snapshot.surface.surfaceSha256 ||
|
|
2619
|
+
base.sessionCompatibilitySha256 !== meta.sessionCompatibilitySha256 ||
|
|
2620
|
+
base.nextTurnNumber !== meta.nextTurnNumber
|
|
2621
|
+
) {
|
|
2622
|
+
throw new AdmissionStaleError();
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
1407
2625
|
if (
|
|
1408
2626
|
meta.initializationState !== "ready" ||
|
|
1409
2627
|
meta.nextTurnNumber !== mutation.turn.turnNumber
|
|
@@ -1477,14 +2695,11 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1477
2695
|
throw new Error(`Frame ${mutation.frameBefore.frameId} is not open.`);
|
|
1478
2696
|
}
|
|
1479
2697
|
for (let index = 0; index < mutation.messages.length; index += 1) {
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
);
|
|
1484
|
-
|
|
1485
|
-
this.database,
|
|
1486
|
-
requireItem(mutation.toolResults, index, "tool result"),
|
|
1487
|
-
);
|
|
2698
|
+
const message = requireItem(mutation.messages, index, "tool message");
|
|
2699
|
+
const result = requireItem(mutation.toolResults, index, "tool result");
|
|
2700
|
+
insertMessage(this.database, message);
|
|
2701
|
+
insertToolResult(this.database, result);
|
|
2702
|
+
insertPendingSkillActivation(this.database, message, result, now);
|
|
1488
2703
|
}
|
|
1489
2704
|
if (mutation.frameAfter.state === "closed") {
|
|
1490
2705
|
const updated = this.database
|
|
@@ -1594,9 +2809,23 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1594
2809
|
}
|
|
1595
2810
|
|
|
1596
2811
|
private loadValidatedContextSnapshot(
|
|
1597
|
-
meta:
|
|
2812
|
+
meta: StoredSessionMetaV9,
|
|
1598
2813
|
canonical: ProtocolContextView,
|
|
1599
|
-
):
|
|
2814
|
+
): StoredContextSnapshotV8 {
|
|
2815
|
+
const activeRevisionId = requireActiveRevisionId(meta);
|
|
2816
|
+
const surfaces = this.database
|
|
2817
|
+
.query("SELECT * FROM context_surfaces ORDER BY rowid")
|
|
2818
|
+
.all()
|
|
2819
|
+
.map(decodeContextSurface);
|
|
2820
|
+
const surfacesById = new Map<ContextSurfaceId, StoredContextSurfaceV8>();
|
|
2821
|
+
for (const surface of surfaces) {
|
|
2822
|
+
validateStoredContextSurface(surface);
|
|
2823
|
+
if (surface.sessionId !== this.sessionId || surfacesById.has(surface.surfaceId)) {
|
|
2824
|
+
throw new Error("Context surface identity is invalid or duplicated.");
|
|
2825
|
+
}
|
|
2826
|
+
surfacesById.set(surface.surfaceId, surface);
|
|
2827
|
+
}
|
|
2828
|
+
|
|
1600
2829
|
const revisions = this.database
|
|
1601
2830
|
.query("SELECT * FROM context_revisions ORDER BY revision_number")
|
|
1602
2831
|
.all()
|
|
@@ -1609,13 +2838,28 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1609
2838
|
for (let index = 0; index < revisions.length; index += 1) {
|
|
1610
2839
|
const revision = requireItem(revisions, index, "context revision");
|
|
1611
2840
|
const previous = revisions[index - 1];
|
|
2841
|
+
const surface = surfacesById.get(revision.surfaceId);
|
|
1612
2842
|
if (
|
|
1613
2843
|
revision.sessionId !== this.sessionId ||
|
|
1614
2844
|
revision.revisionNumber !== index + 1 ||
|
|
2845
|
+
surface === undefined ||
|
|
2846
|
+
surface.surfaceSha256 !== revision.surfaceSha256 ||
|
|
1615
2847
|
(index === 0
|
|
1616
2848
|
? revision.kind !== "initial_full" || revision.parentRevisionId !== null
|
|
1617
|
-
: revision.kind
|
|
1618
|
-
revision.parentRevisionId !== previous?.revisionId)
|
|
2849
|
+
: revision.kind === "initial_full" ||
|
|
2850
|
+
revision.parentRevisionId !== previous?.revisionId) ||
|
|
2851
|
+
((revision.kind === "swap_only" || revision.kind === "prefix_retirement") &&
|
|
2852
|
+
revision.surfaceId !== previous?.surfaceId) ||
|
|
2853
|
+
(revision.kind === "surface_refresh" &&
|
|
2854
|
+
revision.surfaceId === previous?.surfaceId) ||
|
|
2855
|
+
(previous !== undefined &&
|
|
2856
|
+
(revision.keepFromOrdinal < previous.keepFromOrdinal ||
|
|
2857
|
+
((revision.kind === "swap_only" ||
|
|
2858
|
+
revision.kind === "surface_refresh" ||
|
|
2859
|
+
revision.kind === "skills_update") &&
|
|
2860
|
+
revision.keepFromOrdinal !== previous.keepFromOrdinal) ||
|
|
2861
|
+
(revision.kind === "prefix_retirement" &&
|
|
2862
|
+
revision.keepFromOrdinal <= previous.keepFromOrdinal)))
|
|
1619
2863
|
) {
|
|
1620
2864
|
throw new Error("Context revision chain is not linear and contiguous.");
|
|
1621
2865
|
}
|
|
@@ -1632,15 +2876,36 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1632
2876
|
}
|
|
1633
2877
|
revisionNumberById.set(revision.revisionId, revision.revisionNumber);
|
|
1634
2878
|
}
|
|
2879
|
+
const introducedSurfaceIds = new Set(
|
|
2880
|
+
revisions.flatMap((revision, index) => {
|
|
2881
|
+
const previous = revisions[index - 1];
|
|
2882
|
+
return revision.kind === "initial_full" ||
|
|
2883
|
+
revision.kind === "surface_refresh" ||
|
|
2884
|
+
(revision.kind === "skills_update" &&
|
|
2885
|
+
revision.surfaceId !== previous?.surfaceId)
|
|
2886
|
+
? [revision.surfaceId]
|
|
2887
|
+
: [];
|
|
2888
|
+
}),
|
|
2889
|
+
);
|
|
2890
|
+
if (
|
|
2891
|
+
introducedSurfaceIds.size !== surfaces.length ||
|
|
2892
|
+
surfaces.some((surface) => !introducedSurfaceIds.has(surface.surfaceId))
|
|
2893
|
+
) {
|
|
2894
|
+
throw new Error("Context surface chain contains an orphan or duplicate surface.");
|
|
2895
|
+
}
|
|
1635
2896
|
|
|
1636
2897
|
const activeRevision = requireItem(
|
|
1637
2898
|
revisions,
|
|
1638
2899
|
revisions.length - 1,
|
|
1639
2900
|
"active context revision",
|
|
1640
2901
|
);
|
|
1641
|
-
if (activeRevision.revisionId !==
|
|
2902
|
+
if (activeRevision.revisionId !== activeRevisionId) {
|
|
1642
2903
|
throw new Error("Active context revision is not the latest committed revision.");
|
|
1643
2904
|
}
|
|
2905
|
+
const activeSurface = surfacesById.get(activeRevision.surfaceId);
|
|
2906
|
+
if (activeSurface === undefined) {
|
|
2907
|
+
throw new Error("Active context revision surface is missing.");
|
|
2908
|
+
}
|
|
1644
2909
|
|
|
1645
2910
|
const overrides = this.database
|
|
1646
2911
|
.query(
|
|
@@ -1652,27 +2917,117 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1652
2917
|
.all()
|
|
1653
2918
|
.map(decodeStoredSwapOverride);
|
|
1654
2919
|
this.validateStoredOverrides(overrides, canonical, revisions, revisionNumberById);
|
|
2920
|
+
this.validateSkillActivationRows(canonical, revisions, overrides, surfaces);
|
|
1655
2921
|
|
|
1656
|
-
let cumulativeCount = 0;
|
|
1657
2922
|
for (const revision of revisions) {
|
|
2923
|
+
const surface = surfacesById.get(revision.surfaceId);
|
|
2924
|
+
if (surface === undefined) {
|
|
2925
|
+
throw new Error(`Context revision ${revision.revisionId} has no surface.`);
|
|
2926
|
+
}
|
|
1658
2927
|
const activeOverrides = overrides.filter(
|
|
1659
2928
|
(override) =>
|
|
1660
2929
|
(revisionNumberById.get(override.introducedRevisionId) ??
|
|
1661
|
-
Number.POSITIVE_INFINITY) <= revision.revisionNumber
|
|
2930
|
+
Number.POSITIVE_INFINITY) <= revision.revisionNumber &&
|
|
2931
|
+
override.ordinal >= revision.keepFromOrdinal,
|
|
1662
2932
|
);
|
|
1663
2933
|
const introducedCount = overrides.filter(
|
|
1664
2934
|
(override) => override.introducedRevisionId === revision.revisionId,
|
|
1665
2935
|
).length;
|
|
1666
|
-
cumulativeCount += introducedCount;
|
|
1667
2936
|
if (
|
|
1668
2937
|
introducedCount !== revision.addedOverrideCount ||
|
|
1669
|
-
|
|
1670
|
-
activeOverrideManifestHash(activeOverrides) !==
|
|
2938
|
+
activeOverrides.length !== revision.activeOverrideCount ||
|
|
2939
|
+
activeOverrideManifestHash(activeOverrides) !==
|
|
2940
|
+
revision.activeOverrideManifestSha256
|
|
1671
2941
|
) {
|
|
1672
2942
|
throw new Error(
|
|
1673
2943
|
`Context revision ${revision.revisionId} override manifest is invalid.`,
|
|
1674
2944
|
);
|
|
1675
2945
|
}
|
|
2946
|
+
if (
|
|
2947
|
+
revision.kind === "surface_refresh" &&
|
|
2948
|
+
previousRevision(revisions, revision)?.activeOverrideManifestSha256 !==
|
|
2949
|
+
revision.activeOverrideManifestSha256
|
|
2950
|
+
) {
|
|
2951
|
+
throw new Error(
|
|
2952
|
+
`Context surface revision ${revision.revisionId} changed overrides.`,
|
|
2953
|
+
);
|
|
2954
|
+
}
|
|
2955
|
+
if (revision.kind === "skills_update") {
|
|
2956
|
+
const parent = previousRevision(revisions, revision);
|
|
2957
|
+
const parentSurface =
|
|
2958
|
+
parent === undefined ? undefined : surfacesById.get(parent.surfaceId);
|
|
2959
|
+
const settlements = this.loadSkillActivations().filter(
|
|
2960
|
+
(activation) => activation.settledRevisionId === revision.revisionId,
|
|
2961
|
+
);
|
|
2962
|
+
if (
|
|
2963
|
+
parentSurface === undefined ||
|
|
2964
|
+
contextSurfaceChangeManifestHash(
|
|
2965
|
+
contextSurfaceChanges(parentSurface, surface),
|
|
2966
|
+
) !== revision.changeManifestSha256 ||
|
|
2967
|
+
settlements.length !== revision.addedOverrideCount ||
|
|
2968
|
+
skillActivationManifestSha256(
|
|
2969
|
+
settlements.map((activation) => ({
|
|
2970
|
+
activationMessageId: activation.activationMessageId,
|
|
2971
|
+
name: activation.name,
|
|
2972
|
+
state: activation.state === "promoted" ? "promoted" : "rejected",
|
|
2973
|
+
...(activation.rejectionReason === undefined
|
|
2974
|
+
? {}
|
|
2975
|
+
: { rejectionReason: activation.rejectionReason }),
|
|
2976
|
+
})),
|
|
2977
|
+
) !== revision.activationManifestSha256
|
|
2978
|
+
) {
|
|
2979
|
+
throw new Error(
|
|
2980
|
+
`Agent Skills revision ${revision.revisionId} manifest is invalid.`,
|
|
2981
|
+
);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
if (revision.kind === "prefix_retirement") {
|
|
2985
|
+
const parent = previousRevision(revisions, revision);
|
|
2986
|
+
if (parent === undefined) {
|
|
2987
|
+
throw new Error("Prefix retirement revision has no parent.");
|
|
2988
|
+
}
|
|
2989
|
+
const retiredStart = Math.max(parent.keepFromOrdinal, 2);
|
|
2990
|
+
const retiredMessages = canonical.messages.filter(
|
|
2991
|
+
(message) =>
|
|
2992
|
+
message.ordinal >= retiredStart &&
|
|
2993
|
+
message.ordinal < revision.keepFromOrdinal,
|
|
2994
|
+
);
|
|
2995
|
+
const retiredFrames = canonical.frames.filter(
|
|
2996
|
+
(frame) =>
|
|
2997
|
+
frame.firstOrdinal >= retiredStart &&
|
|
2998
|
+
(frame.lastOrdinal ?? Number.POSITIVE_INFINITY) < revision.keepFromOrdinal,
|
|
2999
|
+
);
|
|
3000
|
+
const retiredTurns = new Set(
|
|
3001
|
+
retiredMessages.flatMap((message) =>
|
|
3002
|
+
message.role === "system" ? [] : [message.turnId],
|
|
3003
|
+
),
|
|
3004
|
+
);
|
|
3005
|
+
if (
|
|
3006
|
+
revision.retiredThroughOrdinal !== revision.keepFromOrdinal - 1 ||
|
|
3007
|
+
revision.retiredMessageCount !== retiredMessages.length ||
|
|
3008
|
+
revision.retiredFrameCount !== retiredFrames.length ||
|
|
3009
|
+
revision.retiredTurnCount !== retiredTurns.size
|
|
3010
|
+
) {
|
|
3011
|
+
throw new Error(
|
|
3012
|
+
`Context retirement revision ${revision.revisionId} counts are invalid.`,
|
|
3013
|
+
);
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
if (revision.kind === "surface_refresh") {
|
|
3017
|
+
const parent = previousRevision(revisions, revision);
|
|
3018
|
+
const parentSurface =
|
|
3019
|
+
parent === undefined ? undefined : surfacesById.get(parent.surfaceId);
|
|
3020
|
+
if (
|
|
3021
|
+
parentSurface === undefined ||
|
|
3022
|
+
contextSurfaceChangeManifestHash(
|
|
3023
|
+
contextSurfaceChanges(parentSurface, surface),
|
|
3024
|
+
) !== revision.changeManifestSha256
|
|
3025
|
+
) {
|
|
3026
|
+
throw new Error(
|
|
3027
|
+
`Context surface revision ${revision.revisionId} change manifest is invalid.`,
|
|
3028
|
+
);
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
1676
3031
|
const prefix = protocolPrefixView(canonical, revision.sourceThroughOrdinal);
|
|
1677
3032
|
this.revisionCompiler.compileActive({
|
|
1678
3033
|
meta: Object.freeze({
|
|
@@ -1680,6 +3035,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1680
3035
|
activeRevisionId: revision.revisionId,
|
|
1681
3036
|
}),
|
|
1682
3037
|
revision,
|
|
3038
|
+
surface,
|
|
1683
3039
|
activeOverrides,
|
|
1684
3040
|
canonical: prefix,
|
|
1685
3041
|
});
|
|
@@ -1696,18 +3052,26 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1696
3052
|
return Object.freeze({
|
|
1697
3053
|
meta: Object.freeze({
|
|
1698
3054
|
sessionId: meta.sessionId,
|
|
1699
|
-
activeRevisionId
|
|
3055
|
+
activeRevisionId,
|
|
1700
3056
|
}),
|
|
1701
3057
|
revision: activeRevision,
|
|
1702
|
-
|
|
3058
|
+
surface: activeSurface,
|
|
3059
|
+
activeOverrides: Object.freeze(
|
|
3060
|
+
overrides.filter(
|
|
3061
|
+
(override) =>
|
|
3062
|
+
(revisionNumberById.get(override.introducedRevisionId) ??
|
|
3063
|
+
Number.POSITIVE_INFINITY) <= activeRevision.revisionNumber &&
|
|
3064
|
+
override.ordinal >= activeRevision.keepFromOrdinal,
|
|
3065
|
+
),
|
|
3066
|
+
),
|
|
1703
3067
|
canonical,
|
|
1704
3068
|
});
|
|
1705
3069
|
}
|
|
1706
3070
|
|
|
1707
3071
|
private validateStoredOverrides(
|
|
1708
|
-
overrides: readonly
|
|
3072
|
+
overrides: readonly StoredContextOverrideV8[],
|
|
1709
3073
|
canonical: ProtocolContextView,
|
|
1710
|
-
revisions: readonly
|
|
3074
|
+
revisions: readonly StoredContextRevisionV8[],
|
|
1711
3075
|
revisionNumberById: ReadonlyMap<ContextRevisionId, number>,
|
|
1712
3076
|
): void {
|
|
1713
3077
|
const messages = new Map(
|
|
@@ -1729,8 +3093,13 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1729
3093
|
const frame = frames.get(override.frameId);
|
|
1730
3094
|
const result = results.get(override.messageId);
|
|
1731
3095
|
if (
|
|
1732
|
-
revision?.kind !== "swap_only" ||
|
|
3096
|
+
(revision?.kind !== "swap_only" && revision?.kind !== "skills_update") ||
|
|
3097
|
+
(revision.kind === "swap_only" &&
|
|
3098
|
+
override.rendererFormat !== SWAP_OBSERVATION_FORMAT) ||
|
|
3099
|
+
(revision.kind === "skills_update" &&
|
|
3100
|
+
override.rendererFormat !== SKILL_ACTIVATION_RECEIPT_FORMAT) ||
|
|
1733
3101
|
revisionNumberById.get(revision.revisionId) === undefined ||
|
|
3102
|
+
override.ordinal < revision.keepFromOrdinal ||
|
|
1734
3103
|
override.ordinal > revision.sourceThroughOrdinal ||
|
|
1735
3104
|
seenMessages.has(override.messageId) ||
|
|
1736
3105
|
message?.role !== "tool" ||
|
|
@@ -1742,17 +3111,137 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1742
3111
|
) {
|
|
1743
3112
|
throw new Error("Stored context override canonical identity is invalid.");
|
|
1744
3113
|
}
|
|
1745
|
-
const rendered =
|
|
3114
|
+
const rendered =
|
|
3115
|
+
override.rendererFormat === SWAP_OBSERVATION_FORMAT
|
|
3116
|
+
? this.swapRenderer.render({ message, result })
|
|
3117
|
+
: this.renderStoredSkillReceipt(message, result, revision.revisionId);
|
|
1746
3118
|
if (
|
|
1747
3119
|
stableJsonStringify(rendered) !==
|
|
1748
3120
|
stableJsonStringify(stripStoredOverride(override))
|
|
1749
3121
|
) {
|
|
1750
|
-
throw new Error(
|
|
1751
|
-
`Stored context override ${override.messageId} does not match deterministic rendering.`,
|
|
1752
|
-
);
|
|
3122
|
+
throw new Error(
|
|
3123
|
+
`Stored context override ${override.messageId} does not match deterministic rendering.`,
|
|
3124
|
+
);
|
|
3125
|
+
}
|
|
3126
|
+
seenMessages.add(override.messageId);
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
private validateSkillActivationRows(
|
|
3131
|
+
canonical: ProtocolContextView,
|
|
3132
|
+
revisions: readonly StoredContextRevisionV8[],
|
|
3133
|
+
overrides: readonly StoredContextOverrideV8[],
|
|
3134
|
+
surfaces: readonly StoredContextSurfaceV8[],
|
|
3135
|
+
): void {
|
|
3136
|
+
const activations = this.loadSkillActivations();
|
|
3137
|
+
const activationByMessage = new Map(
|
|
3138
|
+
activations.map((activation) => [activation.activationMessageId, activation]),
|
|
3139
|
+
);
|
|
3140
|
+
const messages = new Map(
|
|
3141
|
+
canonical.messages.map((message) => [message.messageId, message]),
|
|
3142
|
+
);
|
|
3143
|
+
const results = new Map(
|
|
3144
|
+
canonical.toolResults.map((result) => [result.toolMessageId, result]),
|
|
3145
|
+
);
|
|
3146
|
+
const loadedResults = canonical.toolResults.filter(
|
|
3147
|
+
(result) =>
|
|
3148
|
+
result.completion.kind === "returned" &&
|
|
3149
|
+
result.completion.raw.kind === "skill" &&
|
|
3150
|
+
result.completion.raw.ok &&
|
|
3151
|
+
result.completion.raw.status === "loaded",
|
|
3152
|
+
);
|
|
3153
|
+
if (loadedResults.length !== activations.length) {
|
|
3154
|
+
throw new Error("Loaded Agent Skill results and activation rows differ.");
|
|
3155
|
+
}
|
|
3156
|
+
const revisionsById = new Map(
|
|
3157
|
+
revisions.map((revision) => [revision.revisionId, revision]),
|
|
3158
|
+
);
|
|
3159
|
+
const overridesByMessage = new Map(
|
|
3160
|
+
overrides.map((override) => [override.messageId, override]),
|
|
3161
|
+
);
|
|
3162
|
+
for (const activation of activations) {
|
|
3163
|
+
const message = messages.get(activation.activationMessageId);
|
|
3164
|
+
const result = results.get(activation.activationMessageId);
|
|
3165
|
+
const raw =
|
|
3166
|
+
result?.completion.kind === "returned" ? result.completion.raw : undefined;
|
|
3167
|
+
if (
|
|
3168
|
+
activation.sessionId !== this.sessionId ||
|
|
3169
|
+
message?.role !== "tool" ||
|
|
3170
|
+
message.name !== "Skill" ||
|
|
3171
|
+
message.toolCallId !== activation.toolCallId ||
|
|
3172
|
+
raw?.kind !== "skill" ||
|
|
3173
|
+
!raw.ok ||
|
|
3174
|
+
raw.status !== "loaded" ||
|
|
3175
|
+
raw.name !== activation.name ||
|
|
3176
|
+
raw.scope !== activation.scope ||
|
|
3177
|
+
raw.sha256 !== activation.skillFileSha256
|
|
3178
|
+
) {
|
|
3179
|
+
throw new Error("Agent Skill activation canonical identity is invalid.");
|
|
1753
3180
|
}
|
|
1754
|
-
|
|
3181
|
+
if (activation.settledRevisionId !== undefined) {
|
|
3182
|
+
const revision = revisionsById.get(activation.settledRevisionId);
|
|
3183
|
+
const override = overridesByMessage.get(activation.activationMessageId);
|
|
3184
|
+
if (
|
|
3185
|
+
revision?.kind !== "skills_update" ||
|
|
3186
|
+
override?.introducedRevisionId !== revision.revisionId ||
|
|
3187
|
+
override.rendererFormat !== SKILL_ACTIVATION_RECEIPT_FORMAT
|
|
3188
|
+
) {
|
|
3189
|
+
throw new Error("Settled Agent Skill activation receipt is invalid.");
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
for (const surface of surfaces) {
|
|
3194
|
+
for (const active of surface.activeSkills) {
|
|
3195
|
+
const activation = activationByMessage.get(active.activationMessageId);
|
|
3196
|
+
if (activation?.state !== "promoted" || activation.name !== active.name) {
|
|
3197
|
+
throw new Error(
|
|
3198
|
+
"Context surface references an invalid Agent Skill activation.",
|
|
3199
|
+
);
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
private renderStoredSkillReceipt(
|
|
3206
|
+
message: Extract<CanonicalMessageRecord, { role: "tool" }>,
|
|
3207
|
+
result: ToolResultRecord,
|
|
3208
|
+
revisionId: ContextRevisionId,
|
|
3209
|
+
): SwapOverride {
|
|
3210
|
+
if (
|
|
3211
|
+
message.name !== "Skill" ||
|
|
3212
|
+
result.completion.kind !== "returned" ||
|
|
3213
|
+
result.completion.raw.kind !== "skill" ||
|
|
3214
|
+
!result.completion.raw.ok ||
|
|
3215
|
+
result.completion.raw.status !== "loaded"
|
|
3216
|
+
) {
|
|
3217
|
+
throw new Error("Agent Skill receipt does not target a loaded Skill result.");
|
|
3218
|
+
}
|
|
3219
|
+
const activation = this.loadSkillActivations().find(
|
|
3220
|
+
(entry) => entry.activationMessageId === message.messageId,
|
|
3221
|
+
);
|
|
3222
|
+
if (
|
|
3223
|
+
activation === undefined ||
|
|
3224
|
+
activation.settledRevisionId !== revisionId ||
|
|
3225
|
+
(activation.state !== "promoted" && activation.state !== "rejected")
|
|
3226
|
+
) {
|
|
3227
|
+
throw new Error("Agent Skill receipt has no matching settled activation.");
|
|
1755
3228
|
}
|
|
3229
|
+
return renderSkillActivationReceipt({
|
|
3230
|
+
message: {
|
|
3231
|
+
messageId: message.messageId,
|
|
3232
|
+
frameId: message.frameId,
|
|
3233
|
+
ordinal: message.ordinal,
|
|
3234
|
+
content: message.content,
|
|
3235
|
+
contentSha256: message.contentSha256,
|
|
3236
|
+
},
|
|
3237
|
+
name: activation.name,
|
|
3238
|
+
outcome:
|
|
3239
|
+
activation.state === "promoted"
|
|
3240
|
+
? "promoted"
|
|
3241
|
+
: activation.rejectionReason === "unavailable"
|
|
3242
|
+
? "unavailable"
|
|
3243
|
+
: "rejected",
|
|
3244
|
+
});
|
|
1756
3245
|
}
|
|
1757
3246
|
|
|
1758
3247
|
private loadMeasuredContextState(): StoredMeasuredContextState | undefined {
|
|
@@ -1768,7 +3257,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1768
3257
|
: decodeMeasuredContextState(row, this.sessionId);
|
|
1769
3258
|
}
|
|
1770
3259
|
|
|
1771
|
-
private validateCounters(meta:
|
|
3260
|
+
private validateCounters(meta: StoredSessionMetaV9, view: ProtocolContextView): void {
|
|
1772
3261
|
const turns = this.database
|
|
1773
3262
|
.query("SELECT * FROM turns ORDER BY turn_number")
|
|
1774
3263
|
.all() as Array<Record<string, unknown>>;
|
|
@@ -1939,30 +3428,168 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1939
3428
|
}
|
|
1940
3429
|
}
|
|
1941
3430
|
|
|
1942
|
-
|
|
3431
|
+
function insertPendingSkillActivation(
|
|
3432
|
+
database: Database,
|
|
3433
|
+
message: CanonicalMessageRecord,
|
|
3434
|
+
result: ToolResultRecord,
|
|
3435
|
+
now: string,
|
|
3436
|
+
): void {
|
|
3437
|
+
if (
|
|
3438
|
+
message.role !== "tool" ||
|
|
3439
|
+
result.completion.kind !== "returned" ||
|
|
3440
|
+
result.completion.raw.kind !== "skill" ||
|
|
3441
|
+
!result.completion.raw.ok ||
|
|
3442
|
+
result.completion.raw.status !== "loaded"
|
|
3443
|
+
) {
|
|
3444
|
+
return;
|
|
3445
|
+
}
|
|
3446
|
+
const raw = result.completion.raw;
|
|
3447
|
+
if (message.name !== "Skill" || message.messageId !== result.toolMessageId) {
|
|
3448
|
+
throw new Error("Loaded Agent Skill completion has invalid tool identity.");
|
|
3449
|
+
}
|
|
3450
|
+
database
|
|
3451
|
+
.query(
|
|
3452
|
+
`INSERT INTO skill_activations (
|
|
3453
|
+
activation_message_id, tool_call_id, session_id, name, scope,
|
|
3454
|
+
skill_file_sha256, state, dispatched_iteration_id, settled_revision_id,
|
|
3455
|
+
rejection_reason, created_at, updated_at
|
|
3456
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, NULL, ?, ?)`,
|
|
3457
|
+
)
|
|
3458
|
+
.run(
|
|
3459
|
+
message.messageId,
|
|
3460
|
+
result.toolCallId,
|
|
3461
|
+
result.sessionId,
|
|
3462
|
+
raw.name,
|
|
3463
|
+
raw.scope,
|
|
3464
|
+
raw.sha256,
|
|
3465
|
+
now,
|
|
3466
|
+
now,
|
|
3467
|
+
);
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
export function createSessionCompatibilityContract(input: {
|
|
1943
3471
|
modelName: string;
|
|
1944
3472
|
profileName?: string;
|
|
1945
3473
|
includeReasoningContent: boolean;
|
|
1946
3474
|
contextProfile: ModelContextProfile;
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
3475
|
+
messageProtocol: ModelMessageProtocol;
|
|
3476
|
+
inputModalities?: readonly ("text" | "image")[];
|
|
3477
|
+
tokenEstimator?: InputTokenEstimatorCompatibility;
|
|
3478
|
+
}): SessionCompatibilityContract {
|
|
3479
|
+
if (input.modelName.trim() === "") {
|
|
3480
|
+
throw new Error("Session compatibility model name must not be empty.");
|
|
3481
|
+
}
|
|
3482
|
+
if (input.profileName !== undefined && input.profileName.trim() === "") {
|
|
3483
|
+
throw new Error("Session compatibility profile name must not be empty.");
|
|
3484
|
+
}
|
|
3485
|
+
if (typeof input.includeReasoningContent !== "boolean") {
|
|
3486
|
+
throw new Error("Session compatibility reasoning replay flag must be boolean.");
|
|
3487
|
+
}
|
|
3488
|
+
if (
|
|
3489
|
+
!["openai-chat", "fake"].includes(input.messageProtocol.adapter) ||
|
|
3490
|
+
input.messageProtocol.serializationVersion.trim() === ""
|
|
3491
|
+
) {
|
|
3492
|
+
throw new Error("Session compatibility message protocol is invalid.");
|
|
3493
|
+
}
|
|
3494
|
+
const inputModalities = normalizeInputModalities(input.inputModalities ?? ["text"]);
|
|
3495
|
+
if (inputModalities.includes("image") && input.tokenEstimator === undefined) {
|
|
3496
|
+
throw new Error(
|
|
3497
|
+
"Session compatibility image input requires a token estimator identity.",
|
|
3498
|
+
);
|
|
3499
|
+
}
|
|
3500
|
+
if (input.tokenEstimator !== undefined) {
|
|
3501
|
+
validateTokenEstimatorCompatibility(input.tokenEstimator);
|
|
3502
|
+
}
|
|
1952
3503
|
return Object.freeze({
|
|
1953
|
-
version: 1,
|
|
1954
3504
|
modelName: input.modelName,
|
|
1955
3505
|
...(input.profileName === undefined ? {} : { profileName: input.profileName }),
|
|
1956
3506
|
includeReasoningContent: input.includeReasoningContent,
|
|
1957
|
-
contextProfile:
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
3507
|
+
contextProfile: Object.freeze(createModelContextProfile(input.contextProfile)),
|
|
3508
|
+
messageProtocol: immutableCanonicalClone(input.messageProtocol),
|
|
3509
|
+
imageInput: Object.freeze({
|
|
3510
|
+
policyVersion: IMAGE_INPUT_POLICY_VERSION,
|
|
3511
|
+
policySha256: sha256(
|
|
3512
|
+
stableJsonStringify({
|
|
3513
|
+
version: IMAGE_INPUT_POLICY_VERSION,
|
|
3514
|
+
...IMAGE_INPUT_POLICY,
|
|
3515
|
+
}),
|
|
3516
|
+
),
|
|
3517
|
+
inputModalities,
|
|
3518
|
+
...(input.tokenEstimator === undefined
|
|
3519
|
+
? {}
|
|
3520
|
+
: { tokenEstimator: immutableCanonicalClone(input.tokenEstimator) }),
|
|
3521
|
+
}),
|
|
3522
|
+
});
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
function normalizeSessionCompatibilityContract(
|
|
3526
|
+
contract: SessionCompatibilityContract,
|
|
3527
|
+
): SessionCompatibilityContract {
|
|
3528
|
+
return createSessionCompatibilityContract({
|
|
3529
|
+
modelName: contract.modelName,
|
|
3530
|
+
...(contract.profileName === undefined
|
|
3531
|
+
? {}
|
|
3532
|
+
: { profileName: contract.profileName }),
|
|
3533
|
+
includeReasoningContent: contract.includeReasoningContent,
|
|
3534
|
+
contextProfile: contract.contextProfile,
|
|
3535
|
+
messageProtocol: contract.messageProtocol,
|
|
3536
|
+
inputModalities: contract.imageInput.inputModalities,
|
|
3537
|
+
...(contract.imageInput.tokenEstimator === undefined
|
|
3538
|
+
? {}
|
|
3539
|
+
: { tokenEstimator: contract.imageInput.tokenEstimator }),
|
|
1963
3540
|
});
|
|
1964
3541
|
}
|
|
1965
3542
|
|
|
3543
|
+
function normalizeInputModalities(
|
|
3544
|
+
modalities: readonly ("text" | "image")[],
|
|
3545
|
+
): readonly ("text" | "image")[] {
|
|
3546
|
+
if (
|
|
3547
|
+
modalities.length === 0 ||
|
|
3548
|
+
modalities.some((value) => value !== "text" && value !== "image") ||
|
|
3549
|
+
new Set(modalities).size !== modalities.length ||
|
|
3550
|
+
!modalities.includes("text")
|
|
3551
|
+
) {
|
|
3552
|
+
throw new Error("Session compatibility input modalities are invalid.");
|
|
3553
|
+
}
|
|
3554
|
+
return Object.freeze(
|
|
3555
|
+
modalities.includes("image") ? (["text", "image"] as const) : (["text"] as const),
|
|
3556
|
+
);
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
function validateTokenEstimatorCompatibility(
|
|
3560
|
+
estimator: InputTokenEstimatorCompatibility,
|
|
3561
|
+
): void {
|
|
3562
|
+
if (
|
|
3563
|
+
estimator.kind !== "moonshot-estimate-token-count-v1" ||
|
|
3564
|
+
estimator.coverageVersion !== "full-request-v1" ||
|
|
3565
|
+
estimator.model.trim() === "" ||
|
|
3566
|
+
!isNormalizedEstimatorEndpoint(estimator.endpoint) ||
|
|
3567
|
+
!Number.isSafeInteger(estimator.timeoutMs) ||
|
|
3568
|
+
estimator.timeoutMs < 1_000 ||
|
|
3569
|
+
estimator.timeoutMs > 60_000 ||
|
|
3570
|
+
estimator.maxRetries !== 0
|
|
3571
|
+
) {
|
|
3572
|
+
throw new Error("Session compatibility token estimator identity is invalid.");
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
function isNormalizedEstimatorEndpoint(value: string): boolean {
|
|
3577
|
+
try {
|
|
3578
|
+
const url = new URL(value);
|
|
3579
|
+
return (
|
|
3580
|
+
(url.protocol === "https:" || url.protocol === "http:") &&
|
|
3581
|
+
url.username === "" &&
|
|
3582
|
+
url.password === "" &&
|
|
3583
|
+
url.search === "" &&
|
|
3584
|
+
url.hash === "" &&
|
|
3585
|
+
url.pathname.endsWith("/tokenizers/estimate-token-count") &&
|
|
3586
|
+
url.toString() === value
|
|
3587
|
+
);
|
|
3588
|
+
} catch {
|
|
3589
|
+
return false;
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
|
|
1966
3593
|
export function sessionDatabasePath(
|
|
1967
3594
|
workspaceRoot: string,
|
|
1968
3595
|
sessionId: SessionId,
|
|
@@ -2031,6 +3658,83 @@ function insertMessage(database: Database, message: CanonicalMessageRecord): voi
|
|
|
2031
3658
|
message.origin,
|
|
2032
3659
|
message.createdAt,
|
|
2033
3660
|
);
|
|
3661
|
+
if (message.role === "user" && message.attachments !== undefined) {
|
|
3662
|
+
insertMessageImageAttachments(database, message);
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
function insertMessageImageAttachments(
|
|
3667
|
+
database: Database,
|
|
3668
|
+
message: Extract<CanonicalMessageRecord, { role: "user" }>,
|
|
3669
|
+
): void {
|
|
3670
|
+
const userMessage = {
|
|
3671
|
+
role: "user" as const,
|
|
3672
|
+
content: message.content,
|
|
3673
|
+
attachments: message.attachments,
|
|
3674
|
+
};
|
|
3675
|
+
validateUserMessage(userMessage);
|
|
3676
|
+
if (userMessageHash(userMessage) !== message.contentSha256) {
|
|
3677
|
+
throw new Error("User image attachment hash does not match the message hash.");
|
|
3678
|
+
}
|
|
3679
|
+
for (let position = 0; position < message.attachments!.length; position += 1) {
|
|
3680
|
+
const attachment = requireItem(message.attachments!, position, "image attachment");
|
|
3681
|
+
const existing = database
|
|
3682
|
+
.query(
|
|
3683
|
+
`SELECT mime_type, byte_length, width, height, created_at
|
|
3684
|
+
FROM image_assets WHERE asset_id = ?`,
|
|
3685
|
+
)
|
|
3686
|
+
.get(attachment.assetId) as {
|
|
3687
|
+
mime_type: unknown;
|
|
3688
|
+
byte_length: unknown;
|
|
3689
|
+
width: unknown;
|
|
3690
|
+
height: unknown;
|
|
3691
|
+
created_at: unknown;
|
|
3692
|
+
} | null;
|
|
3693
|
+
if (existing === null) {
|
|
3694
|
+
database
|
|
3695
|
+
.query(
|
|
3696
|
+
`INSERT INTO image_assets (
|
|
3697
|
+
asset_id, mime_type, byte_length, width, height, created_at
|
|
3698
|
+
) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
3699
|
+
)
|
|
3700
|
+
.run(
|
|
3701
|
+
attachment.assetId,
|
|
3702
|
+
attachment.mimeType,
|
|
3703
|
+
attachment.byteLength,
|
|
3704
|
+
attachment.width,
|
|
3705
|
+
attachment.height,
|
|
3706
|
+
message.createdAt,
|
|
3707
|
+
);
|
|
3708
|
+
} else {
|
|
3709
|
+
timestampFromSql(existing.created_at, "image asset created_at");
|
|
3710
|
+
if (
|
|
3711
|
+
existing.mime_type !== attachment.mimeType ||
|
|
3712
|
+
numberFromSql(existing.byte_length, "image asset byte_length") !==
|
|
3713
|
+
attachment.byteLength ||
|
|
3714
|
+
numberFromSql(existing.width, "image asset width") !== attachment.width ||
|
|
3715
|
+
numberFromSql(existing.height, "image asset height") !== attachment.height
|
|
3716
|
+
) {
|
|
3717
|
+
throw new Error(`Image asset metadata conflicts for ${attachment.assetId}.`);
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
database
|
|
3721
|
+
.query(
|
|
3722
|
+
`INSERT INTO message_image_attachments (
|
|
3723
|
+
message_id, attachment_id, asset_id, position, label,
|
|
3724
|
+
range_start, range_end, original_name
|
|
3725
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3726
|
+
)
|
|
3727
|
+
.run(
|
|
3728
|
+
message.messageId,
|
|
3729
|
+
attachment.attachmentId,
|
|
3730
|
+
attachment.assetId,
|
|
3731
|
+
position,
|
|
3732
|
+
attachment.label,
|
|
3733
|
+
attachment.range.start,
|
|
3734
|
+
attachment.range.end,
|
|
3735
|
+
attachment.originalName,
|
|
3736
|
+
);
|
|
3737
|
+
}
|
|
2034
3738
|
}
|
|
2035
3739
|
|
|
2036
3740
|
function insertToolResult(database: Database, result: ToolResultRecord): void {
|
|
@@ -2060,6 +3764,45 @@ function insertToolResult(database: Database, result: ToolResultRecord): void {
|
|
|
2060
3764
|
);
|
|
2061
3765
|
}
|
|
2062
3766
|
|
|
3767
|
+
function insertContextSurface(
|
|
3768
|
+
database: Database,
|
|
3769
|
+
surface: StoredContextSurfaceV8,
|
|
3770
|
+
): void {
|
|
3771
|
+
validateStoredContextSurface(surface);
|
|
3772
|
+
database
|
|
3773
|
+
.query(
|
|
3774
|
+
`INSERT INTO context_surfaces (
|
|
3775
|
+
surface_id, session_id, system_prompt, system_prompt_sha256,
|
|
3776
|
+
recall_contract_version,
|
|
3777
|
+
project_instruction_json, skill_catalog_json, skill_catalog_sha256,
|
|
3778
|
+
active_skills_json, active_skills_sha256, tool_definitions_json,
|
|
3779
|
+
tool_definitions_sha256, tool_schema_sha256, request_config_sha256,
|
|
3780
|
+
request_max_output_tokens, surface_sha256, created_at
|
|
3781
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3782
|
+
)
|
|
3783
|
+
.run(
|
|
3784
|
+
surface.surfaceId,
|
|
3785
|
+
surface.sessionId,
|
|
3786
|
+
surface.systemPrompt,
|
|
3787
|
+
surface.systemPromptSha256,
|
|
3788
|
+
surface.recallContractVersion,
|
|
3789
|
+
surface.projectInstruction === undefined
|
|
3790
|
+
? null
|
|
3791
|
+
: stableJsonStringify(surface.projectInstruction),
|
|
3792
|
+
stableJsonStringify(surface.skillCatalog),
|
|
3793
|
+
surface.skillCatalogSha256,
|
|
3794
|
+
stableJsonStringify(surface.activeSkills),
|
|
3795
|
+
surface.activeSkillsSha256,
|
|
3796
|
+
stableJsonStringify(surface.toolDefinitions),
|
|
3797
|
+
surface.toolDefinitionsSha256,
|
|
3798
|
+
surface.toolSchemaSha256,
|
|
3799
|
+
surface.requestConfigSha256,
|
|
3800
|
+
surface.requestMaxOutputTokens,
|
|
3801
|
+
surface.surfaceSha256,
|
|
3802
|
+
surface.createdAt,
|
|
3803
|
+
);
|
|
3804
|
+
}
|
|
3805
|
+
|
|
2063
3806
|
function decodeFrame(rowValue: unknown): ProtocolFrame {
|
|
2064
3807
|
const row = recordFromSql(rowValue, "protocol frame");
|
|
2065
3808
|
const state = enumFromSql(row.state, ["open", "closed"] as const, "frame state");
|
|
@@ -2090,7 +3833,67 @@ function decodeFrame(rowValue: unknown): ProtocolFrame {
|
|
|
2090
3833
|
});
|
|
2091
3834
|
}
|
|
2092
3835
|
|
|
2093
|
-
function
|
|
3836
|
+
function loadMessageImageAttachments(
|
|
3837
|
+
database: Database,
|
|
3838
|
+
): Map<string, readonly UserImageAttachment[]> {
|
|
3839
|
+
const rows = database
|
|
3840
|
+
.query(
|
|
3841
|
+
`SELECT mia.*, ia.mime_type, ia.byte_length, ia.width, ia.height,
|
|
3842
|
+
ia.created_at AS asset_created_at
|
|
3843
|
+
FROM message_image_attachments mia
|
|
3844
|
+
JOIN image_assets ia ON ia.asset_id = mia.asset_id
|
|
3845
|
+
JOIN messages m ON m.message_id = mia.message_id
|
|
3846
|
+
ORDER BY m.ordinal, mia.position`,
|
|
3847
|
+
)
|
|
3848
|
+
.all();
|
|
3849
|
+
const mutable = new Map<string, UserImageAttachment[]>();
|
|
3850
|
+
for (const rowValue of rows) {
|
|
3851
|
+
const row = recordFromSql(rowValue, "message image attachment");
|
|
3852
|
+
const messageId = stringFromSql(row.message_id, "attachment message_id");
|
|
3853
|
+
const attachments = mutable.get(messageId) ?? [];
|
|
3854
|
+
const position = numberFromSql(row.position, "attachment position");
|
|
3855
|
+
if (position !== attachments.length) {
|
|
3856
|
+
throw new Error(
|
|
3857
|
+
`Image attachment positions for message ${messageId} are not continuous.`,
|
|
3858
|
+
);
|
|
3859
|
+
}
|
|
3860
|
+
timestampFromSql(row.asset_created_at, "image asset created_at");
|
|
3861
|
+
const attachment = immutableRecord<UserImageAttachment>({
|
|
3862
|
+
attachmentId: parseImageAttachmentId(
|
|
3863
|
+
stringFromSql(row.attachment_id, "attachment_id"),
|
|
3864
|
+
),
|
|
3865
|
+
assetId: parseImageAssetId(stringFromSql(row.asset_id, "asset_id")),
|
|
3866
|
+
mimeType: enumFromSql(
|
|
3867
|
+
row.mime_type,
|
|
3868
|
+
["image/png", "image/jpeg", "image/webp"] as const,
|
|
3869
|
+
"image mime_type",
|
|
3870
|
+
),
|
|
3871
|
+
byteLength: numberFromSql(row.byte_length, "image byte_length"),
|
|
3872
|
+
width: numberFromSql(row.width, "image width"),
|
|
3873
|
+
height: numberFromSql(row.height, "image height"),
|
|
3874
|
+
label: stringFromSql(row.label, "attachment label"),
|
|
3875
|
+
range: immutableRecord({
|
|
3876
|
+
start: numberFromSql(row.range_start, "attachment range_start"),
|
|
3877
|
+
end: numberFromSql(row.range_end, "attachment range_end"),
|
|
3878
|
+
}),
|
|
3879
|
+
originalName: stringFromSql(row.original_name, "attachment original_name"),
|
|
3880
|
+
});
|
|
3881
|
+
validateOriginalImageName(attachment.originalName);
|
|
3882
|
+
attachments.push(attachment);
|
|
3883
|
+
mutable.set(messageId, attachments);
|
|
3884
|
+
}
|
|
3885
|
+
return new Map(
|
|
3886
|
+
[...mutable].map(([messageId, attachments]) => [
|
|
3887
|
+
messageId,
|
|
3888
|
+
Object.freeze(attachments),
|
|
3889
|
+
]),
|
|
3890
|
+
);
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3893
|
+
function decodeMessage(
|
|
3894
|
+
rowValue: unknown,
|
|
3895
|
+
attachments?: readonly UserImageAttachment[],
|
|
3896
|
+
): CanonicalMessageRecord {
|
|
2094
3897
|
const row = recordFromSql(rowValue, "message");
|
|
2095
3898
|
const base = {
|
|
2096
3899
|
messageId: stringFromSql(row.message_id, "message_id") as MessageId,
|
|
@@ -2107,21 +3910,37 @@ function decodeMessage(rowValue: unknown): CanonicalMessageRecord {
|
|
|
2107
3910
|
);
|
|
2108
3911
|
switch (role) {
|
|
2109
3912
|
case "system":
|
|
3913
|
+
if (attachments !== undefined) {
|
|
3914
|
+
throw new Error("System messages cannot have image attachments.");
|
|
3915
|
+
}
|
|
2110
3916
|
return immutableRecord({
|
|
2111
3917
|
...base,
|
|
2112
3918
|
role,
|
|
2113
3919
|
content: stringFromSql(row.content, "content"),
|
|
2114
3920
|
origin: "runtime",
|
|
2115
3921
|
});
|
|
2116
|
-
case "user":
|
|
2117
|
-
|
|
3922
|
+
case "user": {
|
|
3923
|
+
const message = {
|
|
2118
3924
|
...base,
|
|
2119
3925
|
role,
|
|
2120
3926
|
turnId: stringFromSql(row.turn_id, "turn_id") as TurnId,
|
|
2121
3927
|
content: stringFromSql(row.content, "content"),
|
|
3928
|
+
...(attachments === undefined ? {} : { attachments }),
|
|
2122
3929
|
origin: "user",
|
|
3930
|
+
} as const;
|
|
3931
|
+
validateUserMessage({
|
|
3932
|
+
role: "user",
|
|
3933
|
+
content: message.content,
|
|
3934
|
+
...(message.attachments === undefined
|
|
3935
|
+
? {}
|
|
3936
|
+
: { attachments: message.attachments }),
|
|
2123
3937
|
});
|
|
3938
|
+
return immutableRecord(message);
|
|
3939
|
+
}
|
|
2124
3940
|
case "assistant": {
|
|
3941
|
+
if (attachments !== undefined) {
|
|
3942
|
+
throw new Error("Assistant messages cannot have image attachments.");
|
|
3943
|
+
}
|
|
2125
3944
|
const content = nullableTextFromSql(row.content, "content");
|
|
2126
3945
|
const reasoningPresent = numberFromSql(
|
|
2127
3946
|
row.reasoning_content_present,
|
|
@@ -2157,6 +3976,9 @@ function decodeMessage(rowValue: unknown): CanonicalMessageRecord {
|
|
|
2157
3976
|
});
|
|
2158
3977
|
}
|
|
2159
3978
|
case "tool":
|
|
3979
|
+
if (attachments !== undefined) {
|
|
3980
|
+
throw new Error("Tool messages cannot have image attachments.");
|
|
3981
|
+
}
|
|
2160
3982
|
return immutableRecord({
|
|
2161
3983
|
...base,
|
|
2162
3984
|
role,
|
|
@@ -2174,6 +3996,16 @@ function decodeMessage(rowValue: unknown): CanonicalMessageRecord {
|
|
|
2174
3996
|
}
|
|
2175
3997
|
}
|
|
2176
3998
|
|
|
3999
|
+
function imageAssetRefFromAttachment(attachment: UserImageAttachment): ImageAssetRef {
|
|
4000
|
+
return Object.freeze({
|
|
4001
|
+
assetId: attachment.assetId,
|
|
4002
|
+
mimeType: attachment.mimeType,
|
|
4003
|
+
byteLength: attachment.byteLength,
|
|
4004
|
+
width: attachment.width,
|
|
4005
|
+
height: attachment.height,
|
|
4006
|
+
});
|
|
4007
|
+
}
|
|
4008
|
+
|
|
2177
4009
|
function decodeToolResult(rowValue: unknown): ToolResultRecord {
|
|
2178
4010
|
const row = recordFromSql(rowValue, "tool result");
|
|
2179
4011
|
const kind = enumFromSql(
|
|
@@ -2191,7 +4023,7 @@ function decodeToolResult(rowValue: unknown): ToolResultRecord {
|
|
|
2191
4023
|
rawSha256: stringFromSql(row.raw_sha256, "raw_sha256"),
|
|
2192
4024
|
observationFormat: enumFromSql(
|
|
2193
4025
|
row.observation_format,
|
|
2194
|
-
|
|
4026
|
+
SUPPORTED_TOOL_OBSERVATION_FORMATS,
|
|
2195
4027
|
"observation format",
|
|
2196
4028
|
),
|
|
2197
4029
|
});
|
|
@@ -2229,26 +4061,102 @@ function decodeToolResult(rowValue: unknown): ToolResultRecord {
|
|
|
2229
4061
|
});
|
|
2230
4062
|
}
|
|
2231
4063
|
|
|
2232
|
-
function
|
|
4064
|
+
function decodeContextSurface(rowValue: unknown): StoredContextSurfaceV8 {
|
|
4065
|
+
const row = recordFromSql(rowValue, "context surface");
|
|
4066
|
+
const projectInstruction =
|
|
4067
|
+
row.project_instruction_json === null
|
|
4068
|
+
? undefined
|
|
4069
|
+
: decodeProjectInstructionManifest(
|
|
4070
|
+
parseJson(
|
|
4071
|
+
stringFromSql(row.project_instruction_json, "project_instruction_json"),
|
|
4072
|
+
"project_instruction_json",
|
|
4073
|
+
),
|
|
4074
|
+
);
|
|
4075
|
+
const toolDefinitions = decodeToolDefinitions(
|
|
4076
|
+
parseJson(
|
|
4077
|
+
stringFromSql(row.tool_definitions_json, "tool_definitions_json"),
|
|
4078
|
+
"tool_definitions_json",
|
|
4079
|
+
),
|
|
4080
|
+
);
|
|
4081
|
+
const skillCatalog = decodeSkillCatalogManifest(
|
|
4082
|
+
parseJson(
|
|
4083
|
+
stringFromSql(row.skill_catalog_json, "skill_catalog_json"),
|
|
4084
|
+
"skill_catalog_json",
|
|
4085
|
+
),
|
|
4086
|
+
);
|
|
4087
|
+
const activeSkills = decodeActiveSkillsManifest(
|
|
4088
|
+
parseJson(
|
|
4089
|
+
stringFromSql(row.active_skills_json, "active_skills_json"),
|
|
4090
|
+
"active_skills_json",
|
|
4091
|
+
),
|
|
4092
|
+
);
|
|
4093
|
+
const surface = Object.freeze({
|
|
4094
|
+
surfaceId: stringFromSql(row.surface_id, "surface_id") as ContextSurfaceId,
|
|
4095
|
+
sessionId: stringFromSql(row.session_id, "session_id") as SessionId,
|
|
4096
|
+
systemPrompt: stringFromSql(row.system_prompt, "system_prompt"),
|
|
4097
|
+
systemPromptSha256: sha256FromSql(row.system_prompt_sha256, "system_prompt_sha256"),
|
|
4098
|
+
recallContractVersion: enumFromSql(
|
|
4099
|
+
row.recall_contract_version,
|
|
4100
|
+
SUPPORTED_RECALL_RETIREMENT_CONTRACT_VERSIONS,
|
|
4101
|
+
"recall_contract_version",
|
|
4102
|
+
),
|
|
4103
|
+
...(projectInstruction === undefined ? {} : { projectInstruction }),
|
|
4104
|
+
skillCatalog,
|
|
4105
|
+
skillCatalogSha256: sha256FromSql(row.skill_catalog_sha256, "skill_catalog_sha256"),
|
|
4106
|
+
activeSkills,
|
|
4107
|
+
activeSkillsSha256: sha256FromSql(row.active_skills_sha256, "active_skills_sha256"),
|
|
4108
|
+
toolDefinitions,
|
|
4109
|
+
toolDefinitionsSha256: sha256FromSql(
|
|
4110
|
+
row.tool_definitions_sha256,
|
|
4111
|
+
"tool_definitions_sha256",
|
|
4112
|
+
),
|
|
4113
|
+
toolSchemaSha256: sha256FromSql(row.tool_schema_sha256, "tool_schema_sha256"),
|
|
4114
|
+
requestConfigSha256: sha256FromSql(
|
|
4115
|
+
row.request_config_sha256,
|
|
4116
|
+
"request_config_sha256",
|
|
4117
|
+
),
|
|
4118
|
+
requestMaxOutputTokens: numberFromSql(
|
|
4119
|
+
row.request_max_output_tokens,
|
|
4120
|
+
"request_max_output_tokens",
|
|
4121
|
+
),
|
|
4122
|
+
surfaceSha256: sha256FromSql(row.surface_sha256, "surface_sha256"),
|
|
4123
|
+
createdAt: timestampFromSql(row.created_at, "created_at"),
|
|
4124
|
+
});
|
|
4125
|
+
validateStoredContextSurface(surface);
|
|
4126
|
+
return surface;
|
|
4127
|
+
}
|
|
4128
|
+
|
|
4129
|
+
function decodeContextRevision(rowValue: unknown): StoredContextRevisionV8 {
|
|
2233
4130
|
const row = recordFromSql(rowValue, "context revision");
|
|
2234
4131
|
const kind = enumFromSql(
|
|
2235
4132
|
row.kind,
|
|
2236
|
-
[
|
|
4133
|
+
[
|
|
4134
|
+
"initial_full",
|
|
4135
|
+
"swap_only",
|
|
4136
|
+
"surface_refresh",
|
|
4137
|
+
"prefix_retirement",
|
|
4138
|
+
"skills_update",
|
|
4139
|
+
] as const,
|
|
2237
4140
|
"context revision kind",
|
|
2238
4141
|
);
|
|
2239
4142
|
const common = {
|
|
2240
4143
|
revisionId: stringFromSql(row.revision_id, "revision_id") as ContextRevisionId,
|
|
2241
4144
|
sessionId: stringFromSql(row.session_id, "session_id") as SessionId,
|
|
4145
|
+
surfaceId: stringFromSql(row.surface_id, "surface_id") as ContextSurfaceId,
|
|
4146
|
+
surfaceSha256: sha256FromSql(row.surface_sha256, "surface_sha256"),
|
|
2242
4147
|
keepFromOrdinal: numberFromSql(row.keep_from_ordinal, "keep_from_ordinal"),
|
|
2243
4148
|
sourceThroughOrdinal: numberFromSql(
|
|
2244
4149
|
row.source_through_ordinal,
|
|
2245
4150
|
"source_through_ordinal",
|
|
2246
4151
|
),
|
|
2247
4152
|
addedOverrideCount: numberFromSql(row.added_override_count, "added_override_count"),
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
4153
|
+
activeOverrideCount: numberFromSql(
|
|
4154
|
+
row.active_override_count,
|
|
4155
|
+
"active_override_count",
|
|
4156
|
+
),
|
|
4157
|
+
activeOverrideManifestSha256: sha256FromSql(
|
|
4158
|
+
row.active_override_manifest_sha256,
|
|
4159
|
+
"active_override_manifest_sha256",
|
|
2252
4160
|
),
|
|
2253
4161
|
canonicalSequenceSha256: sha256FromSql(
|
|
2254
4162
|
row.canonical_sequence_sha256,
|
|
@@ -2260,8 +4168,8 @@ function decodeContextRevision(rowValue: unknown): StoredContextRevisionV5 {
|
|
|
2260
4168
|
),
|
|
2261
4169
|
createdAt: timestampFromSql(row.created_at, "created_at"),
|
|
2262
4170
|
};
|
|
2263
|
-
if (common.keepFromOrdinal
|
|
2264
|
-
throw new Error("Context revision keep_from_ordinal must be
|
|
4171
|
+
if (common.keepFromOrdinal < 1) {
|
|
4172
|
+
throw new Error("Context revision keep_from_ordinal must be positive.");
|
|
2265
4173
|
}
|
|
2266
4174
|
const revisionNumber = numberFromSql(row.revision_number, "revision_number");
|
|
2267
4175
|
const parentRevisionId = nullableStringFromSql(
|
|
@@ -2274,10 +4182,14 @@ function decodeContextRevision(rowValue: unknown): StoredContextRevisionV5 {
|
|
|
2274
4182
|
parentRevisionId !== null ||
|
|
2275
4183
|
common.sourceThroughOrdinal !== 1 ||
|
|
2276
4184
|
common.addedOverrideCount !== 0 ||
|
|
2277
|
-
common.
|
|
4185
|
+
common.activeOverrideCount !== 0 ||
|
|
4186
|
+
common.keepFromOrdinal !== 1 ||
|
|
2278
4187
|
row.policy_version !== null ||
|
|
2279
4188
|
row.renderer_format !== null ||
|
|
2280
|
-
row.plan_sha256 !== null
|
|
4189
|
+
row.plan_sha256 !== null ||
|
|
4190
|
+
row.change_manifest_sha256 !== null ||
|
|
4191
|
+
row.activation_manifest_sha256 !== null ||
|
|
4192
|
+
!hasNoRetirementFields(row)
|
|
2281
4193
|
) {
|
|
2282
4194
|
throw new Error("Initial context revision row is invalid.");
|
|
2283
4195
|
}
|
|
@@ -2289,38 +4201,165 @@ function decodeContextRevision(rowValue: unknown): StoredContextRevisionV5 {
|
|
|
2289
4201
|
keepFromOrdinal: 1,
|
|
2290
4202
|
sourceThroughOrdinal: 1,
|
|
2291
4203
|
addedOverrideCount: 0,
|
|
2292
|
-
|
|
4204
|
+
activeOverrideCount: 0,
|
|
4205
|
+
});
|
|
4206
|
+
}
|
|
4207
|
+
if (
|
|
4208
|
+
kind === "swap_only" &&
|
|
4209
|
+
(revisionNumber < 2 ||
|
|
4210
|
+
parentRevisionId === null ||
|
|
4211
|
+
common.addedOverrideCount < 1 ||
|
|
4212
|
+
common.activeOverrideCount < common.addedOverrideCount)
|
|
4213
|
+
) {
|
|
4214
|
+
throw new Error("Swap context revision row is invalid.");
|
|
4215
|
+
}
|
|
4216
|
+
if (kind === "swap_only") {
|
|
4217
|
+
if (
|
|
4218
|
+
row.change_manifest_sha256 !== null ||
|
|
4219
|
+
row.activation_manifest_sha256 !== null ||
|
|
4220
|
+
!hasNoRetirementFields(row)
|
|
4221
|
+
) {
|
|
4222
|
+
throw new Error("Swap context revision has a change manifest.");
|
|
4223
|
+
}
|
|
4224
|
+
return Object.freeze({
|
|
4225
|
+
...common,
|
|
4226
|
+
revisionNumber,
|
|
4227
|
+
parentRevisionId: parentRevisionId!,
|
|
4228
|
+
kind,
|
|
4229
|
+
policyVersion: enumFromSql(
|
|
4230
|
+
row.policy_version,
|
|
4231
|
+
["swap-only-v1"] as const,
|
|
4232
|
+
"context revision policy",
|
|
4233
|
+
),
|
|
4234
|
+
rendererFormat: enumFromSql(
|
|
4235
|
+
row.renderer_format,
|
|
4236
|
+
[SWAP_OBSERVATION_FORMAT] as const,
|
|
4237
|
+
"context revision renderer format",
|
|
4238
|
+
),
|
|
4239
|
+
planSha256: sha256FromSql(row.plan_sha256, "plan_sha256"),
|
|
4240
|
+
});
|
|
4241
|
+
}
|
|
4242
|
+
if (kind === "skills_update") {
|
|
4243
|
+
if (
|
|
4244
|
+
revisionNumber < 2 ||
|
|
4245
|
+
parentRevisionId === null ||
|
|
4246
|
+
common.addedOverrideCount < 1 ||
|
|
4247
|
+
common.activeOverrideCount < common.addedOverrideCount ||
|
|
4248
|
+
row.plan_sha256 !== null ||
|
|
4249
|
+
!hasNoRetirementFields(row)
|
|
4250
|
+
) {
|
|
4251
|
+
throw new Error("Agent Skills context revision row is invalid.");
|
|
4252
|
+
}
|
|
4253
|
+
return Object.freeze({
|
|
4254
|
+
...common,
|
|
4255
|
+
revisionNumber,
|
|
4256
|
+
parentRevisionId,
|
|
4257
|
+
kind,
|
|
4258
|
+
policyVersion: enumFromSql(
|
|
4259
|
+
row.policy_version,
|
|
4260
|
+
[SKILL_POLICY_VERSION] as const,
|
|
4261
|
+
"Agent Skills context revision policy",
|
|
4262
|
+
),
|
|
4263
|
+
rendererFormat: enumFromSql(
|
|
4264
|
+
row.renderer_format,
|
|
4265
|
+
[SKILL_ACTIVATION_RECEIPT_FORMAT] as const,
|
|
4266
|
+
"Agent Skills context revision renderer format",
|
|
4267
|
+
),
|
|
4268
|
+
changeManifestSha256: sha256FromSql(
|
|
4269
|
+
row.change_manifest_sha256,
|
|
4270
|
+
"change_manifest_sha256",
|
|
4271
|
+
),
|
|
4272
|
+
activationManifestSha256: sha256FromSql(
|
|
4273
|
+
row.activation_manifest_sha256,
|
|
4274
|
+
"activation_manifest_sha256",
|
|
4275
|
+
),
|
|
4276
|
+
});
|
|
4277
|
+
}
|
|
4278
|
+
if (kind === "prefix_retirement") {
|
|
4279
|
+
const retiredThroughOrdinal = numberFromSql(
|
|
4280
|
+
row.retired_through_ordinal,
|
|
4281
|
+
"retired_through_ordinal",
|
|
4282
|
+
);
|
|
4283
|
+
const retiredTurnCount = numberFromSql(
|
|
4284
|
+
row.retired_turn_count,
|
|
4285
|
+
"retired_turn_count",
|
|
4286
|
+
);
|
|
4287
|
+
const retiredFrameCount = numberFromSql(
|
|
4288
|
+
row.retired_frame_count,
|
|
4289
|
+
"retired_frame_count",
|
|
4290
|
+
);
|
|
4291
|
+
const retiredMessageCount = numberFromSql(
|
|
4292
|
+
row.retired_message_count,
|
|
4293
|
+
"retired_message_count",
|
|
4294
|
+
);
|
|
4295
|
+
if (
|
|
4296
|
+
revisionNumber < 2 ||
|
|
4297
|
+
parentRevisionId === null ||
|
|
4298
|
+
common.keepFromOrdinal <= 1 ||
|
|
4299
|
+
common.addedOverrideCount !== 0 ||
|
|
4300
|
+
retiredThroughOrdinal !== common.keepFromOrdinal - 1 ||
|
|
4301
|
+
retiredTurnCount < 1 ||
|
|
4302
|
+
retiredFrameCount < 1 ||
|
|
4303
|
+
retiredMessageCount < 1 ||
|
|
4304
|
+
row.renderer_format !== null ||
|
|
4305
|
+
row.change_manifest_sha256 !== null ||
|
|
4306
|
+
row.activation_manifest_sha256 !== null
|
|
4307
|
+
) {
|
|
4308
|
+
throw new Error("Prefix retirement revision row is invalid.");
|
|
4309
|
+
}
|
|
4310
|
+
return Object.freeze({
|
|
4311
|
+
...common,
|
|
4312
|
+
revisionNumber,
|
|
4313
|
+
parentRevisionId,
|
|
4314
|
+
kind,
|
|
4315
|
+
addedOverrideCount: 0,
|
|
4316
|
+
policyVersion: enumFromSql(
|
|
4317
|
+
row.policy_version,
|
|
4318
|
+
["recall-first-retirement-v1"] as const,
|
|
4319
|
+
"context revision policy",
|
|
4320
|
+
),
|
|
4321
|
+
planSha256: sha256FromSql(row.plan_sha256, "plan_sha256"),
|
|
4322
|
+
retiredThroughOrdinal,
|
|
4323
|
+
retiredTurnCount,
|
|
4324
|
+
retiredFrameCount,
|
|
4325
|
+
retiredMessageCount,
|
|
2293
4326
|
});
|
|
2294
4327
|
}
|
|
2295
4328
|
if (
|
|
2296
4329
|
revisionNumber < 2 ||
|
|
2297
4330
|
parentRevisionId === null ||
|
|
2298
|
-
common.addedOverrideCount
|
|
2299
|
-
|
|
4331
|
+
common.addedOverrideCount !== 0 ||
|
|
4332
|
+
row.policy_version !== null ||
|
|
4333
|
+
row.renderer_format !== null ||
|
|
4334
|
+
row.plan_sha256 !== null ||
|
|
4335
|
+
row.activation_manifest_sha256 !== null ||
|
|
4336
|
+
!hasNoRetirementFields(row)
|
|
2300
4337
|
) {
|
|
2301
|
-
throw new Error("
|
|
4338
|
+
throw new Error("Surface context revision row is invalid.");
|
|
2302
4339
|
}
|
|
2303
4340
|
return Object.freeze({
|
|
2304
4341
|
...common,
|
|
2305
4342
|
revisionNumber,
|
|
2306
4343
|
parentRevisionId,
|
|
2307
4344
|
kind,
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
row.
|
|
2311
|
-
|
|
2312
|
-
"context revision policy",
|
|
2313
|
-
),
|
|
2314
|
-
rendererFormat: enumFromSql(
|
|
2315
|
-
row.renderer_format,
|
|
2316
|
-
[SWAP_OBSERVATION_FORMAT] as const,
|
|
2317
|
-
"context revision renderer format",
|
|
4345
|
+
addedOverrideCount: 0,
|
|
4346
|
+
changeManifestSha256: sha256FromSql(
|
|
4347
|
+
row.change_manifest_sha256,
|
|
4348
|
+
"change_manifest_sha256",
|
|
2318
4349
|
),
|
|
2319
|
-
planSha256: sha256FromSql(row.plan_sha256, "plan_sha256"),
|
|
2320
4350
|
});
|
|
2321
4351
|
}
|
|
2322
4352
|
|
|
2323
|
-
function
|
|
4353
|
+
function hasNoRetirementFields(row: Record<string, unknown>): boolean {
|
|
4354
|
+
return (
|
|
4355
|
+
row.retired_through_ordinal === null &&
|
|
4356
|
+
row.retired_turn_count === null &&
|
|
4357
|
+
row.retired_frame_count === null &&
|
|
4358
|
+
row.retired_message_count === null
|
|
4359
|
+
);
|
|
4360
|
+
}
|
|
4361
|
+
|
|
4362
|
+
function decodeStoredSwapOverride(rowValue: unknown): StoredContextOverrideV8 {
|
|
2324
4363
|
const row = recordFromSql(rowValue, "context override");
|
|
2325
4364
|
if (row.representation !== "swapped") {
|
|
2326
4365
|
throw new Error("Context override representation must be swapped.");
|
|
@@ -2335,10 +4374,10 @@ function decodeStoredSwapOverride(rowValue: unknown): StoredSwapOverrideV5 {
|
|
|
2335
4374
|
ordinal: numberFromSql(row.ordinal, "ordinal"),
|
|
2336
4375
|
rendererFormat: enumFromSql(
|
|
2337
4376
|
row.renderer_format,
|
|
2338
|
-
[SWAP_OBSERVATION_FORMAT] as const,
|
|
4377
|
+
[SWAP_OBSERVATION_FORMAT, SKILL_ACTIVATION_RECEIPT_FORMAT] as const,
|
|
2339
4378
|
"context override renderer format",
|
|
2340
4379
|
),
|
|
2341
|
-
source: stringFromSql(row.source, "source") as
|
|
4380
|
+
source: stringFromSql(row.source, "source") as StoredContextOverrideV8["source"],
|
|
2342
4381
|
originalContentSha256: sha256FromSql(
|
|
2343
4382
|
row.original_content_sha256,
|
|
2344
4383
|
"original_content_sha256",
|
|
@@ -2355,7 +4394,7 @@ function decodeStoredSwapOverride(rowValue: unknown): StoredSwapOverrideV5 {
|
|
|
2355
4394
|
});
|
|
2356
4395
|
}
|
|
2357
4396
|
|
|
2358
|
-
function stripStoredOverride(override:
|
|
4397
|
+
function stripStoredOverride(override: StoredContextOverrideV8): SwapOverride {
|
|
2359
4398
|
return Object.freeze({
|
|
2360
4399
|
frameId: override.frameId,
|
|
2361
4400
|
messageId: override.messageId,
|
|
@@ -2367,6 +4406,71 @@ function stripStoredOverride(override: StoredSwapOverrideV5): SwapOverride {
|
|
|
2367
4406
|
originalBytes: override.originalBytes,
|
|
2368
4407
|
renderedBytes: override.renderedBytes,
|
|
2369
4408
|
byteSavings: override.byteSavings,
|
|
4409
|
+
...(override.rendererFormat === SWAP_OBSERVATION_FORMAT
|
|
4410
|
+
? {}
|
|
4411
|
+
: { rendererFormat: override.rendererFormat }),
|
|
4412
|
+
});
|
|
4413
|
+
}
|
|
4414
|
+
|
|
4415
|
+
function decodeSkillActivation(rowValue: unknown): StoredSkillActivation {
|
|
4416
|
+
const row = recordFromSql(rowValue, "skill activation");
|
|
4417
|
+
const state = enumFromSql(
|
|
4418
|
+
row.state,
|
|
4419
|
+
["pending", "dispatched", "promoted", "rejected"] as const,
|
|
4420
|
+
"skill activation state",
|
|
4421
|
+
);
|
|
4422
|
+
const dispatchedIterationId = nullableStringFromSql(
|
|
4423
|
+
row.dispatched_iteration_id,
|
|
4424
|
+
"dispatched_iteration_id",
|
|
4425
|
+
) as IterationId | null;
|
|
4426
|
+
const settledRevisionId = nullableStringFromSql(
|
|
4427
|
+
row.settled_revision_id,
|
|
4428
|
+
"settled_revision_id",
|
|
4429
|
+
) as ContextRevisionId | null;
|
|
4430
|
+
const rejectionReason = nullableStringFromSql(
|
|
4431
|
+
row.rejection_reason,
|
|
4432
|
+
"rejection_reason",
|
|
4433
|
+
);
|
|
4434
|
+
if (
|
|
4435
|
+
(state === "pending" &&
|
|
4436
|
+
(dispatchedIterationId !== null ||
|
|
4437
|
+
settledRevisionId !== null ||
|
|
4438
|
+
rejectionReason !== null)) ||
|
|
4439
|
+
(state === "dispatched" &&
|
|
4440
|
+
(dispatchedIterationId === null ||
|
|
4441
|
+
settledRevisionId !== null ||
|
|
4442
|
+
rejectionReason !== null)) ||
|
|
4443
|
+
(state === "promoted" &&
|
|
4444
|
+
(dispatchedIterationId === null ||
|
|
4445
|
+
settledRevisionId === null ||
|
|
4446
|
+
rejectionReason !== null)) ||
|
|
4447
|
+
(state === "rejected" &&
|
|
4448
|
+
(settledRevisionId === null ||
|
|
4449
|
+
rejectionReason === null ||
|
|
4450
|
+
rejectionReason.trim() === ""))
|
|
4451
|
+
) {
|
|
4452
|
+
throw new Error("Skill activation lifecycle fields are invalid.");
|
|
4453
|
+
}
|
|
4454
|
+
return Object.freeze({
|
|
4455
|
+
activationMessageId: stringFromSql(
|
|
4456
|
+
row.activation_message_id,
|
|
4457
|
+
"activation_message_id",
|
|
4458
|
+
) as MessageId,
|
|
4459
|
+
toolCallId: stringFromSql(row.tool_call_id, "tool_call_id") as ToolCallId,
|
|
4460
|
+
sessionId: stringFromSql(row.session_id, "session_id") as SessionId,
|
|
4461
|
+
name: stringFromSql(row.name, "skill activation name"),
|
|
4462
|
+
scope: enumFromSql(
|
|
4463
|
+
row.scope,
|
|
4464
|
+
["project", "user"] as const,
|
|
4465
|
+
"skill activation scope",
|
|
4466
|
+
),
|
|
4467
|
+
skillFileSha256: sha256FromSql(row.skill_file_sha256, "skill_file_sha256"),
|
|
4468
|
+
state,
|
|
4469
|
+
...(dispatchedIterationId === null ? {} : { dispatchedIterationId }),
|
|
4470
|
+
...(settledRevisionId === null ? {} : { settledRevisionId }),
|
|
4471
|
+
...(rejectionReason === null ? {} : { rejectionReason }),
|
|
4472
|
+
createdAt: timestampFromSql(row.created_at, "created_at"),
|
|
4473
|
+
updatedAt: timestampFromSql(row.updated_at, "updated_at"),
|
|
2370
4474
|
});
|
|
2371
4475
|
}
|
|
2372
4476
|
|
|
@@ -2455,12 +4559,158 @@ export function decodeStoredToolCalls(json: string): readonly ToolCall[] {
|
|
|
2455
4559
|
}),
|
|
2456
4560
|
};
|
|
2457
4561
|
});
|
|
2458
|
-
return Object.freeze(calls);
|
|
4562
|
+
return Object.freeze(calls);
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4565
|
+
function decodeProjectInstructionManifest(value: unknown): ProjectInstructionManifest {
|
|
4566
|
+
const record = recordFromSql(value, "project instruction manifest");
|
|
4567
|
+
assertObjectKeys(
|
|
4568
|
+
record,
|
|
4569
|
+
["path", "byteLength", "sha256"],
|
|
4570
|
+
["path", "byteLength", "sha256"],
|
|
4571
|
+
"project instruction manifest",
|
|
4572
|
+
);
|
|
4573
|
+
return Object.freeze({
|
|
4574
|
+
path: enumFromSql(
|
|
4575
|
+
record.path,
|
|
4576
|
+
["AGENTS.md", "CLAUDE.md"] as const,
|
|
4577
|
+
"project instruction path",
|
|
4578
|
+
),
|
|
4579
|
+
byteLength: numberFromJson(record.byteLength, "project instruction byteLength"),
|
|
4580
|
+
sha256: sha256FromSql(record.sha256, "project instruction sha256"),
|
|
4581
|
+
});
|
|
4582
|
+
}
|
|
4583
|
+
|
|
4584
|
+
function decodeSkillCatalogManifest(
|
|
4585
|
+
value: unknown,
|
|
4586
|
+
): readonly SkillCatalogManifestEntry[] {
|
|
4587
|
+
if (!Array.isArray(value)) {
|
|
4588
|
+
throw new Error("skill_catalog_json must contain an array.");
|
|
4589
|
+
}
|
|
4590
|
+
return Object.freeze(
|
|
4591
|
+
value.map((entry, index) => {
|
|
4592
|
+
const record = recordFromSql(entry, `skill catalog entry ${index}`);
|
|
4593
|
+
assertObjectKeys(
|
|
4594
|
+
record,
|
|
4595
|
+
[
|
|
4596
|
+
"name",
|
|
4597
|
+
"scope",
|
|
4598
|
+
"directorySha256",
|
|
4599
|
+
"descriptionSha256",
|
|
4600
|
+
"skillFileSha256",
|
|
4601
|
+
"byteLength",
|
|
4602
|
+
],
|
|
4603
|
+
[
|
|
4604
|
+
"name",
|
|
4605
|
+
"scope",
|
|
4606
|
+
"directorySha256",
|
|
4607
|
+
"descriptionSha256",
|
|
4608
|
+
"skillFileSha256",
|
|
4609
|
+
"byteLength",
|
|
4610
|
+
],
|
|
4611
|
+
`skill catalog entry ${index}`,
|
|
4612
|
+
);
|
|
4613
|
+
return Object.freeze({
|
|
4614
|
+
name: stringFromSql(record.name, "skill name"),
|
|
4615
|
+
scope: enumFromSql(record.scope, ["project", "user"] as const, "skill scope"),
|
|
4616
|
+
directorySha256: sha256FromSql(record.directorySha256, "skill directorySha256"),
|
|
4617
|
+
descriptionSha256: sha256FromSql(
|
|
4618
|
+
record.descriptionSha256,
|
|
4619
|
+
"skill descriptionSha256",
|
|
4620
|
+
),
|
|
4621
|
+
skillFileSha256: sha256FromSql(record.skillFileSha256, "skill skillFileSha256"),
|
|
4622
|
+
byteLength: numberFromJson(record.byteLength, "skill byteLength"),
|
|
4623
|
+
});
|
|
4624
|
+
}),
|
|
4625
|
+
);
|
|
4626
|
+
}
|
|
4627
|
+
|
|
4628
|
+
function decodeActiveSkillsManifest(
|
|
4629
|
+
value: unknown,
|
|
4630
|
+
): readonly ActiveSkillManifestEntry[] {
|
|
4631
|
+
if (!Array.isArray(value)) {
|
|
4632
|
+
throw new Error("active_skills_json must contain an array.");
|
|
4633
|
+
}
|
|
4634
|
+
return Object.freeze(
|
|
4635
|
+
value.map((entry, index) => {
|
|
4636
|
+
const record = recordFromSql(entry, `active skill entry ${index}`);
|
|
4637
|
+
assertObjectKeys(
|
|
4638
|
+
record,
|
|
4639
|
+
[
|
|
4640
|
+
"name",
|
|
4641
|
+
"scope",
|
|
4642
|
+
"directorySha256",
|
|
4643
|
+
"descriptionSha256",
|
|
4644
|
+
"skillFileSha256",
|
|
4645
|
+
"byteLength",
|
|
4646
|
+
"activationMessageId",
|
|
4647
|
+
],
|
|
4648
|
+
[
|
|
4649
|
+
"name",
|
|
4650
|
+
"scope",
|
|
4651
|
+
"directorySha256",
|
|
4652
|
+
"descriptionSha256",
|
|
4653
|
+
"skillFileSha256",
|
|
4654
|
+
"byteLength",
|
|
4655
|
+
"activationMessageId",
|
|
4656
|
+
],
|
|
4657
|
+
`active skill entry ${index}`,
|
|
4658
|
+
);
|
|
4659
|
+
const [catalogEntry] = decodeSkillCatalogManifest([
|
|
4660
|
+
{
|
|
4661
|
+
name: record.name,
|
|
4662
|
+
scope: record.scope,
|
|
4663
|
+
directorySha256: record.directorySha256,
|
|
4664
|
+
descriptionSha256: record.descriptionSha256,
|
|
4665
|
+
skillFileSha256: record.skillFileSha256,
|
|
4666
|
+
byteLength: record.byteLength,
|
|
4667
|
+
},
|
|
4668
|
+
]);
|
|
4669
|
+
if (catalogEntry === undefined) {
|
|
4670
|
+
throw new Error(`Active skill entry ${index} is missing.`);
|
|
4671
|
+
}
|
|
4672
|
+
return Object.freeze({
|
|
4673
|
+
...catalogEntry,
|
|
4674
|
+
activationMessageId: stringFromSql(
|
|
4675
|
+
record.activationMessageId,
|
|
4676
|
+
"skill activationMessageId",
|
|
4677
|
+
) as MessageId,
|
|
4678
|
+
});
|
|
4679
|
+
}),
|
|
4680
|
+
);
|
|
4681
|
+
}
|
|
4682
|
+
|
|
4683
|
+
function decodeToolDefinitions(value: unknown): readonly ToolDefinition[] {
|
|
4684
|
+
if (!Array.isArray(value)) {
|
|
4685
|
+
throw new Error("tool_definitions_json must contain an array.");
|
|
4686
|
+
}
|
|
4687
|
+
const definitions = value.map((entry, index): ToolDefinition => {
|
|
4688
|
+
const record = recordFromSql(entry, `tool definition ${index}`);
|
|
4689
|
+
assertObjectKeys(
|
|
4690
|
+
record,
|
|
4691
|
+
["name", "description", "parameters"],
|
|
4692
|
+
["name", "description", "parameters"],
|
|
4693
|
+
`tool definition ${index}`,
|
|
4694
|
+
);
|
|
4695
|
+
const parameters = recordFromSql(
|
|
4696
|
+
record.parameters,
|
|
4697
|
+
`tool definition ${index} parameters`,
|
|
4698
|
+
);
|
|
4699
|
+
return Object.freeze({
|
|
4700
|
+
name: stringFromSql(record.name, `tool definition ${index} name`),
|
|
4701
|
+
description: stringFromSql(
|
|
4702
|
+
record.description,
|
|
4703
|
+
`tool definition ${index} description`,
|
|
4704
|
+
),
|
|
4705
|
+
parameters: immutableCanonicalClone(parameters),
|
|
4706
|
+
});
|
|
4707
|
+
});
|
|
4708
|
+
return Object.freeze(definitions);
|
|
2459
4709
|
}
|
|
2460
4710
|
|
|
2461
4711
|
export function decodeStoredToolRawResult(value: unknown): ToolRawResult {
|
|
2462
4712
|
const raw = recordFromSql(value, "tool raw result");
|
|
2463
|
-
enumFromSql(
|
|
4713
|
+
const kind = enumFromSql(
|
|
2464
4714
|
raw.kind,
|
|
2465
4715
|
[
|
|
2466
4716
|
"read",
|
|
@@ -2475,6 +4725,7 @@ export function decodeStoredToolRawResult(value: unknown): ToolRawResult {
|
|
|
2475
4725
|
"web_search",
|
|
2476
4726
|
"web_fetch",
|
|
2477
4727
|
"recall",
|
|
4728
|
+
"skill",
|
|
2478
4729
|
"mcp",
|
|
2479
4730
|
"generic",
|
|
2480
4731
|
] as const,
|
|
@@ -2483,9 +4734,206 @@ export function decodeStoredToolRawResult(value: unknown): ToolRawResult {
|
|
|
2483
4734
|
if (typeof raw.ok !== "boolean") {
|
|
2484
4735
|
throw new Error("tool raw result ok must be a boolean.");
|
|
2485
4736
|
}
|
|
4737
|
+
if (kind === "skill") {
|
|
4738
|
+
return decodeStoredSkillRawResult(raw);
|
|
4739
|
+
}
|
|
2486
4740
|
return immutableCanonicalClone(raw) as ToolRawResult;
|
|
2487
4741
|
}
|
|
2488
4742
|
|
|
4743
|
+
function decodeStoredSkillRawResult(
|
|
4744
|
+
raw: Record<string, unknown>,
|
|
4745
|
+
): Extract<ToolRawResult, { kind: "skill" }> {
|
|
4746
|
+
const status = enumFromSql(
|
|
4747
|
+
raw.status,
|
|
4748
|
+
["loaded", "already_loaded", "already_active", "failed"] as const,
|
|
4749
|
+
"Skill raw result status",
|
|
4750
|
+
);
|
|
4751
|
+
const name = stringFromSql(raw.name, "Skill raw result name");
|
|
4752
|
+
if (status === "failed") {
|
|
4753
|
+
if (raw.ok !== false) {
|
|
4754
|
+
throw new Error("Failed Skill raw result must have ok=false.");
|
|
4755
|
+
}
|
|
4756
|
+
assertObjectKeys(
|
|
4757
|
+
raw,
|
|
4758
|
+
["kind", "ok", "status", "name", "errorCode", "error"],
|
|
4759
|
+
["kind", "ok", "status", "name", "errorCode", "error"],
|
|
4760
|
+
"failed Skill raw result",
|
|
4761
|
+
);
|
|
4762
|
+
const errorCode = stringFromSql(raw.errorCode, "Skill errorCode");
|
|
4763
|
+
const error = stringFromSql(raw.error, "Skill error");
|
|
4764
|
+
if (
|
|
4765
|
+
(name !== "" && !isValidSkillName(name)) ||
|
|
4766
|
+
!/^[A-Z][A-Z0-9_]{0,79}$/.test(errorCode) ||
|
|
4767
|
+
error.trim() === ""
|
|
4768
|
+
) {
|
|
4769
|
+
throw new Error("Failed Skill raw result fields are invalid.");
|
|
4770
|
+
}
|
|
4771
|
+
return immutableRecord({
|
|
4772
|
+
kind: "skill" as const,
|
|
4773
|
+
ok: false as const,
|
|
4774
|
+
status,
|
|
4775
|
+
name,
|
|
4776
|
+
errorCode,
|
|
4777
|
+
error,
|
|
4778
|
+
});
|
|
4779
|
+
}
|
|
4780
|
+
if (raw.ok !== true) {
|
|
4781
|
+
throw new Error("Successful Skill raw result must have ok=true.");
|
|
4782
|
+
}
|
|
4783
|
+
const scope = enumFromSql(
|
|
4784
|
+
raw.scope,
|
|
4785
|
+
["project", "user"] as const,
|
|
4786
|
+
"Skill raw result scope",
|
|
4787
|
+
);
|
|
4788
|
+
const skillFileSha256 = sha256FromSql(raw.sha256, "Skill raw result sha256");
|
|
4789
|
+
if (!isValidSkillName(name)) {
|
|
4790
|
+
throw new Error("Successful Skill raw result name is invalid.");
|
|
4791
|
+
}
|
|
4792
|
+
if (status === "already_loaded") {
|
|
4793
|
+
assertObjectKeys(
|
|
4794
|
+
raw,
|
|
4795
|
+
["kind", "ok", "status", "name", "scope", "lifecycle", "sha256"],
|
|
4796
|
+
["kind", "ok", "status", "name", "scope", "lifecycle", "sha256"],
|
|
4797
|
+
"already loaded Skill raw result",
|
|
4798
|
+
);
|
|
4799
|
+
return immutableRecord({
|
|
4800
|
+
kind: "skill" as const,
|
|
4801
|
+
ok: true as const,
|
|
4802
|
+
status,
|
|
4803
|
+
name,
|
|
4804
|
+
scope,
|
|
4805
|
+
lifecycle: enumFromSql(
|
|
4806
|
+
raw.lifecycle,
|
|
4807
|
+
["pending", "dispatched"] as const,
|
|
4808
|
+
"Skill lifecycle",
|
|
4809
|
+
),
|
|
4810
|
+
sha256: skillFileSha256,
|
|
4811
|
+
});
|
|
4812
|
+
}
|
|
4813
|
+
if (status === "already_active") {
|
|
4814
|
+
assertObjectKeys(
|
|
4815
|
+
raw,
|
|
4816
|
+
["kind", "ok", "status", "name", "scope", "sha256"],
|
|
4817
|
+
["kind", "ok", "status", "name", "scope", "sha256"],
|
|
4818
|
+
"already active Skill raw result",
|
|
4819
|
+
);
|
|
4820
|
+
return immutableRecord({
|
|
4821
|
+
kind: "skill" as const,
|
|
4822
|
+
ok: true as const,
|
|
4823
|
+
status,
|
|
4824
|
+
name,
|
|
4825
|
+
scope,
|
|
4826
|
+
sha256: skillFileSha256,
|
|
4827
|
+
});
|
|
4828
|
+
}
|
|
4829
|
+
assertObjectKeys(
|
|
4830
|
+
raw,
|
|
4831
|
+
[
|
|
4832
|
+
"kind",
|
|
4833
|
+
"ok",
|
|
4834
|
+
"status",
|
|
4835
|
+
"name",
|
|
4836
|
+
"scope",
|
|
4837
|
+
"directory",
|
|
4838
|
+
"skillFilePath",
|
|
4839
|
+
"content",
|
|
4840
|
+
"byteLength",
|
|
4841
|
+
"sha256",
|
|
4842
|
+
"resources",
|
|
4843
|
+
"resourcesTruncated",
|
|
4844
|
+
],
|
|
4845
|
+
[
|
|
4846
|
+
"kind",
|
|
4847
|
+
"ok",
|
|
4848
|
+
"status",
|
|
4849
|
+
"name",
|
|
4850
|
+
"scope",
|
|
4851
|
+
"directory",
|
|
4852
|
+
"skillFilePath",
|
|
4853
|
+
"content",
|
|
4854
|
+
"byteLength",
|
|
4855
|
+
"sha256",
|
|
4856
|
+
"resources",
|
|
4857
|
+
"resourcesTruncated",
|
|
4858
|
+
],
|
|
4859
|
+
"loaded Skill raw result",
|
|
4860
|
+
);
|
|
4861
|
+
if (
|
|
4862
|
+
!Array.isArray(raw.resources) ||
|
|
4863
|
+
raw.resources.some((entry) => typeof entry !== "string") ||
|
|
4864
|
+
raw.resources.length > SKILL_RESOURCE_MAX_ENTRIES ||
|
|
4865
|
+
typeof raw.resourcesTruncated !== "boolean"
|
|
4866
|
+
) {
|
|
4867
|
+
throw new Error("Loaded Skill resource manifest is invalid.");
|
|
4868
|
+
}
|
|
4869
|
+
const directory = stringFromSql(raw.directory, "Skill directory");
|
|
4870
|
+
const skillFilePath = stringFromSql(raw.skillFilePath, "Skill file path");
|
|
4871
|
+
const content = stringFromSql(raw.content, "Skill content");
|
|
4872
|
+
const byteLength = numberFromJson(raw.byteLength, "Skill byteLength");
|
|
4873
|
+
const resources = raw.resources as string[];
|
|
4874
|
+
if (
|
|
4875
|
+
!path.isAbsolute(directory) ||
|
|
4876
|
+
!path.isAbsolute(skillFilePath) ||
|
|
4877
|
+
!isPathWithin(directory, skillFilePath) ||
|
|
4878
|
+
byteLength < 1 ||
|
|
4879
|
+
byteLength > SKILL_FILE_MAX_BYTES ||
|
|
4880
|
+
Buffer.byteLength(content, "utf8") !== byteLength ||
|
|
4881
|
+
sha256(content) !== skillFileSha256 ||
|
|
4882
|
+
!isValidResourceManifest(resources)
|
|
4883
|
+
) {
|
|
4884
|
+
throw new Error("Loaded Skill raw result snapshot is invalid.");
|
|
4885
|
+
}
|
|
4886
|
+
return immutableRecord({
|
|
4887
|
+
kind: "skill" as const,
|
|
4888
|
+
ok: true as const,
|
|
4889
|
+
status,
|
|
4890
|
+
name,
|
|
4891
|
+
scope,
|
|
4892
|
+
directory,
|
|
4893
|
+
skillFilePath,
|
|
4894
|
+
content,
|
|
4895
|
+
byteLength,
|
|
4896
|
+
sha256: skillFileSha256,
|
|
4897
|
+
resources: Object.freeze([...resources]),
|
|
4898
|
+
resourcesTruncated: raw.resourcesTruncated,
|
|
4899
|
+
});
|
|
4900
|
+
}
|
|
4901
|
+
|
|
4902
|
+
function isValidSkillName(value: string): boolean {
|
|
4903
|
+
return value.length <= 64 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
|
|
4904
|
+
}
|
|
4905
|
+
|
|
4906
|
+
function isPathWithin(root: string, candidate: string): boolean {
|
|
4907
|
+
const relative = path.relative(root, candidate);
|
|
4908
|
+
return (
|
|
4909
|
+
relative !== "" &&
|
|
4910
|
+
relative !== ".." &&
|
|
4911
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
4912
|
+
!path.isAbsolute(relative)
|
|
4913
|
+
);
|
|
4914
|
+
}
|
|
4915
|
+
|
|
4916
|
+
function isValidResourceManifest(resources: readonly string[]): boolean {
|
|
4917
|
+
let previous: string | undefined;
|
|
4918
|
+
for (const resource of resources) {
|
|
4919
|
+
const parts = resource.split("/");
|
|
4920
|
+
if (
|
|
4921
|
+
resource === "" ||
|
|
4922
|
+
resource.includes("\\") ||
|
|
4923
|
+
path.posix.isAbsolute(resource) ||
|
|
4924
|
+
path.posix.normalize(resource) !== resource ||
|
|
4925
|
+
!["assets", "references", "scripts"].includes(parts[0] ?? "") ||
|
|
4926
|
+
parts.length < 2 ||
|
|
4927
|
+
parts.length > SKILL_RESOURCE_MAX_DEPTH + 1 ||
|
|
4928
|
+
(previous !== undefined && previous >= resource)
|
|
4929
|
+
) {
|
|
4930
|
+
return false;
|
|
4931
|
+
}
|
|
4932
|
+
previous = resource;
|
|
4933
|
+
}
|
|
4934
|
+
return true;
|
|
4935
|
+
}
|
|
4936
|
+
|
|
2489
4937
|
function decodeMeasuredContextState(
|
|
2490
4938
|
value: unknown,
|
|
2491
4939
|
expectedSessionId: SessionId,
|
|
@@ -2535,9 +4983,12 @@ function assertCommitSwapRevisionInput(input: CommitSwapRevisionInput): void {
|
|
|
2535
4983
|
throw new Error("Commit swap revision input is invalid.");
|
|
2536
4984
|
}
|
|
2537
4985
|
for (const [name, hash] of [
|
|
2538
|
-
[
|
|
4986
|
+
[
|
|
4987
|
+
"expectedBaseActiveOverrideManifestSha256",
|
|
4988
|
+
input.expectedBaseActiveOverrideManifestSha256,
|
|
4989
|
+
],
|
|
2539
4990
|
["planHash", input.planHash],
|
|
2540
|
-
["
|
|
4991
|
+
["nextActiveOverrideManifestSha256", input.nextActiveOverrideManifestSha256],
|
|
2541
4992
|
["canonicalSequenceSha256", input.canonicalSequenceSha256],
|
|
2542
4993
|
["renderedMessageSha256", input.renderedMessageSha256],
|
|
2543
4994
|
] as const) {
|
|
@@ -2547,16 +4998,130 @@ function assertCommitSwapRevisionInput(input: CommitSwapRevisionInput): void {
|
|
|
2547
4998
|
}
|
|
2548
4999
|
}
|
|
2549
5000
|
|
|
2550
|
-
function
|
|
5001
|
+
function assertCommitSurfaceRefreshInput(input: CommitSurfaceRefreshInput): void {
|
|
5002
|
+
if (
|
|
5003
|
+
input.revisionId.trim() === "" ||
|
|
5004
|
+
input.expectedBaseRevisionId.trim() === "" ||
|
|
5005
|
+
!Number.isSafeInteger(input.expectedBaseRevisionNumber) ||
|
|
5006
|
+
input.expectedBaseRevisionNumber < 1 ||
|
|
5007
|
+
!Number.isSafeInteger(input.expectedCanonicalThroughOrdinal) ||
|
|
5008
|
+
input.expectedCanonicalThroughOrdinal < 1
|
|
5009
|
+
) {
|
|
5010
|
+
throw new Error("Commit surface refresh input is invalid.");
|
|
5011
|
+
}
|
|
5012
|
+
for (const [name, hash] of [
|
|
5013
|
+
[
|
|
5014
|
+
"expectedBaseActiveOverrideManifestSha256",
|
|
5015
|
+
input.expectedBaseActiveOverrideManifestSha256,
|
|
5016
|
+
],
|
|
5017
|
+
["changeManifestSha256", input.changeManifestSha256],
|
|
5018
|
+
["canonicalSequenceSha256", input.canonicalSequenceSha256],
|
|
5019
|
+
["renderedMessageSha256", input.renderedMessageSha256],
|
|
5020
|
+
] as const) {
|
|
5021
|
+
if (!/^[0-9a-f]{64}$/.test(hash)) {
|
|
5022
|
+
throw new Error(`Commit surface refresh ${name} must be a SHA-256 hash.`);
|
|
5023
|
+
}
|
|
5024
|
+
}
|
|
5025
|
+
}
|
|
5026
|
+
|
|
5027
|
+
function assertCommitSkillsUpdateInput(input: CommitSkillsUpdateInput): void {
|
|
5028
|
+
if (
|
|
5029
|
+
input.revisionId.trim() === "" ||
|
|
5030
|
+
input.expectedBaseRevisionId.trim() === "" ||
|
|
5031
|
+
!Number.isSafeInteger(input.expectedBaseRevisionNumber) ||
|
|
5032
|
+
input.expectedBaseRevisionNumber < 1 ||
|
|
5033
|
+
!Number.isSafeInteger(input.expectedCanonicalThroughOrdinal) ||
|
|
5034
|
+
input.expectedCanonicalThroughOrdinal < 1 ||
|
|
5035
|
+
input.addedOverrides.length < 1 ||
|
|
5036
|
+
input.settlements.length !== input.addedOverrides.length
|
|
5037
|
+
) {
|
|
5038
|
+
throw new Error("Commit Agent Skills update input is invalid.");
|
|
5039
|
+
}
|
|
5040
|
+
for (const [name, hash] of [
|
|
5041
|
+
[
|
|
5042
|
+
"expectedBaseActiveOverrideManifestSha256",
|
|
5043
|
+
input.expectedBaseActiveOverrideManifestSha256,
|
|
5044
|
+
],
|
|
5045
|
+
["changeManifestSha256", input.changeManifestSha256],
|
|
5046
|
+
["activationManifestSha256", input.activationManifestSha256],
|
|
5047
|
+
["nextActiveOverrideManifestSha256", input.nextActiveOverrideManifestSha256],
|
|
5048
|
+
["canonicalSequenceSha256", input.canonicalSequenceSha256],
|
|
5049
|
+
["renderedMessageSha256", input.renderedMessageSha256],
|
|
5050
|
+
] as const) {
|
|
5051
|
+
if (!/^[0-9a-f]{64}$/.test(hash)) {
|
|
5052
|
+
throw new Error(`Commit Agent Skills update ${name} must be a SHA-256 hash.`);
|
|
5053
|
+
}
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
5056
|
+
|
|
5057
|
+
function assertCommitPrefixRetirementRevisionInput(
|
|
5058
|
+
input: CommitPrefixRetirementRevisionInput,
|
|
5059
|
+
): void {
|
|
5060
|
+
if (
|
|
5061
|
+
input.revisionId.trim() === "" ||
|
|
5062
|
+
input.expectedBaseRevisionId.trim() === "" ||
|
|
5063
|
+
input.policyVersion !== "recall-first-retirement-v1" ||
|
|
5064
|
+
!Number.isSafeInteger(input.expectedBaseRevisionNumber) ||
|
|
5065
|
+
input.expectedBaseRevisionNumber < 1 ||
|
|
5066
|
+
!Number.isSafeInteger(input.expectedBaseKeepFromOrdinal) ||
|
|
5067
|
+
input.expectedBaseKeepFromOrdinal < 1 ||
|
|
5068
|
+
!Number.isSafeInteger(input.expectedCanonicalThroughOrdinal) ||
|
|
5069
|
+
input.expectedCanonicalThroughOrdinal < 1 ||
|
|
5070
|
+
!Number.isSafeInteger(input.nextKeepFromOrdinal) ||
|
|
5071
|
+
input.nextKeepFromOrdinal <= input.expectedBaseKeepFromOrdinal ||
|
|
5072
|
+
input.retiredThroughOrdinal !== input.nextKeepFromOrdinal - 1 ||
|
|
5073
|
+
!Number.isSafeInteger(input.retiredTurnCount) ||
|
|
5074
|
+
input.retiredTurnCount < 1 ||
|
|
5075
|
+
!Number.isSafeInteger(input.retiredFrameCount) ||
|
|
5076
|
+
input.retiredFrameCount < 1 ||
|
|
5077
|
+
!Number.isSafeInteger(input.retiredMessageCount) ||
|
|
5078
|
+
input.retiredMessageCount < 1 ||
|
|
5079
|
+
!Number.isSafeInteger(input.nextActiveOverrideCount) ||
|
|
5080
|
+
input.nextActiveOverrideCount < 0
|
|
5081
|
+
) {
|
|
5082
|
+
throw new Error("Commit prefix retirement input is invalid.");
|
|
5083
|
+
}
|
|
5084
|
+
for (const [name, hash] of [
|
|
5085
|
+
["expectedSurfaceSha256", input.expectedSurfaceSha256],
|
|
5086
|
+
[
|
|
5087
|
+
"expectedBaseActiveOverrideManifestSha256",
|
|
5088
|
+
input.expectedBaseActiveOverrideManifestSha256,
|
|
5089
|
+
],
|
|
5090
|
+
["planHash", input.planHash],
|
|
5091
|
+
["nextActiveOverrideManifestSha256", input.nextActiveOverrideManifestSha256],
|
|
5092
|
+
["canonicalSequenceSha256", input.canonicalSequenceSha256],
|
|
5093
|
+
["renderedMessageSha256", input.renderedMessageSha256],
|
|
5094
|
+
] as const) {
|
|
5095
|
+
if (!/^[0-9a-f]{64}$/.test(hash)) {
|
|
5096
|
+
throw new Error(`Commit prefix retirement ${name} must be a SHA-256 hash.`);
|
|
5097
|
+
}
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
|
|
5101
|
+
function requireActiveRevisionId(meta: StoredSessionMetaV9): ContextRevisionId {
|
|
5102
|
+
if (meta.initializationState !== "ready" || meta.activeRevisionId === null) {
|
|
5103
|
+
throw new Error("Session has no active context revision.");
|
|
5104
|
+
}
|
|
5105
|
+
return meta.activeRevisionId;
|
|
5106
|
+
}
|
|
5107
|
+
|
|
5108
|
+
function previousRevision(
|
|
5109
|
+
revisions: readonly StoredContextRevisionV8[],
|
|
5110
|
+
revision: StoredContextRevisionV8,
|
|
5111
|
+
): StoredContextRevisionV8 | undefined {
|
|
5112
|
+
return revisions[revision.revisionNumber - 2];
|
|
5113
|
+
}
|
|
5114
|
+
|
|
5115
|
+
function decodeMeta(value: unknown, expectedSessionId: SessionId): StoredSessionMetaV9 {
|
|
2551
5116
|
const row = recordFromSql(value, "session metadata");
|
|
2552
5117
|
const sessionId = stringFromSql(row.session_id, "session_id") as SessionId;
|
|
2553
5118
|
if (sessionId !== expectedSessionId) {
|
|
2554
5119
|
throw new Error(`Metadata session ID ${sessionId} does not match directory.`);
|
|
2555
5120
|
}
|
|
2556
5121
|
const schemaVersion = numberFromSql(row.schema_version, "schema_version");
|
|
2557
|
-
if (schemaVersion !==
|
|
5122
|
+
if (schemaVersion !== 9) {
|
|
2558
5123
|
throw new Error(
|
|
2559
|
-
`Session metadata schema version must be
|
|
5124
|
+
`Session metadata schema version must be 9; received ${schemaVersion}.`,
|
|
2560
5125
|
);
|
|
2561
5126
|
}
|
|
2562
5127
|
const projectInstructionFile = nullableStringFromSql(
|
|
@@ -2594,35 +5159,52 @@ function decodeMeta(value: unknown, expectedSessionId: SessionId): StoredSession
|
|
|
2594
5159
|
byteLength: projectInstructionByteLength,
|
|
2595
5160
|
sha256: sha256FromSql(projectInstructionSha256, "project_instruction_sha256"),
|
|
2596
5161
|
};
|
|
5162
|
+
const initializationState = enumFromSql(
|
|
5163
|
+
row.initialization_state,
|
|
5164
|
+
["creating", "ready"] as const,
|
|
5165
|
+
"initialization_state",
|
|
5166
|
+
);
|
|
5167
|
+
const sessionCompatibilityJson = nullableStringFromSql(
|
|
5168
|
+
row.session_compatibility_json,
|
|
5169
|
+
"session_compatibility_json",
|
|
5170
|
+
);
|
|
5171
|
+
const sessionCompatibilitySha256 = nullableStringFromSql(
|
|
5172
|
+
row.session_compatibility_sha256,
|
|
5173
|
+
"session_compatibility_sha256",
|
|
5174
|
+
);
|
|
5175
|
+
const activeRevisionId = nullableStringFromSql(
|
|
5176
|
+
row.active_revision_id,
|
|
5177
|
+
"active_revision_id",
|
|
5178
|
+
) as ContextRevisionId | null;
|
|
5179
|
+
const modelName = stringFromSql(row.model_name, "model_name");
|
|
5180
|
+
const storedContract =
|
|
5181
|
+
sessionCompatibilityJson === null
|
|
5182
|
+
? undefined
|
|
5183
|
+
: decodeSessionCompatibilityContract(sessionCompatibilityJson);
|
|
5184
|
+
if (
|
|
5185
|
+
(sessionCompatibilityJson === null) !== (sessionCompatibilitySha256 === null) ||
|
|
5186
|
+
(sessionCompatibilityJson !== null &&
|
|
5187
|
+
sha256(sessionCompatibilityJson) !== sessionCompatibilitySha256) ||
|
|
5188
|
+
(initializationState === "creating") !== (activeRevisionId === null) ||
|
|
5189
|
+
(initializationState === "creating") !== (sessionCompatibilityJson === null) ||
|
|
5190
|
+
(storedContract !== undefined &&
|
|
5191
|
+
(storedContract.modelName !== modelName ||
|
|
5192
|
+
stableJsonStringify(storedContract) !== sessionCompatibilityJson))
|
|
5193
|
+
) {
|
|
5194
|
+
throw new Error("Session compatibility or initialization metadata is invalid.");
|
|
5195
|
+
}
|
|
2597
5196
|
return {
|
|
2598
5197
|
schemaVersion,
|
|
2599
5198
|
schemaFingerprint: stringFromSql(row.schema_fingerprint, "schema_fingerprint"),
|
|
2600
|
-
initializationState
|
|
2601
|
-
row.initialization_state,
|
|
2602
|
-
["creating", "ready"] as const,
|
|
2603
|
-
"initialization_state",
|
|
2604
|
-
),
|
|
5199
|
+
initializationState,
|
|
2605
5200
|
sessionId,
|
|
2606
5201
|
workspaceRoot: stringFromSql(row.workspace_root, "workspace_root"),
|
|
2607
|
-
modelName
|
|
5202
|
+
modelName,
|
|
2608
5203
|
systemPromptSha256: stringFromSql(row.system_prompt_sha256, "system_prompt_sha256"),
|
|
2609
5204
|
...(projectInstruction === undefined ? {} : { projectInstruction }),
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
),
|
|
2614
|
-
runtimeContractJson: nullableStringFromSql(
|
|
2615
|
-
row.runtime_contract_json,
|
|
2616
|
-
"runtime_contract_json",
|
|
2617
|
-
),
|
|
2618
|
-
runtimeContractSha256: nullableStringFromSql(
|
|
2619
|
-
row.runtime_contract_sha256,
|
|
2620
|
-
"runtime_contract_sha256",
|
|
2621
|
-
),
|
|
2622
|
-
activeRevisionId: stringFromSql(
|
|
2623
|
-
row.active_revision_id,
|
|
2624
|
-
"active_revision_id",
|
|
2625
|
-
) as ContextRevisionId,
|
|
5205
|
+
sessionCompatibilityJson,
|
|
5206
|
+
sessionCompatibilitySha256,
|
|
5207
|
+
activeRevisionId,
|
|
2626
5208
|
nextTurnNumber: numberFromSql(row.next_turn_number, "next_turn_number"),
|
|
2627
5209
|
nextEventSequence: numberFromSql(row.next_event_sequence, "next_event_sequence"),
|
|
2628
5210
|
openCount: numberFromSql(row.open_count, "open_count"),
|
|
@@ -2650,23 +5232,197 @@ function decodeMeta(value: unknown, expectedSessionId: SessionId): StoredSession
|
|
|
2650
5232
|
};
|
|
2651
5233
|
}
|
|
2652
5234
|
|
|
2653
|
-
function
|
|
5235
|
+
function compatibilityContractDifferences(
|
|
2654
5236
|
storedJson: string | null,
|
|
2655
|
-
current:
|
|
5237
|
+
current: SessionCompatibilityContract,
|
|
2656
5238
|
): string[] {
|
|
2657
5239
|
if (storedJson === null) {
|
|
2658
|
-
return ["
|
|
5240
|
+
return ["sessionCompatibility"];
|
|
2659
5241
|
}
|
|
2660
|
-
const stored = parseJson(storedJson, "
|
|
5242
|
+
const stored = parseJson(storedJson, "session_compatibility_json");
|
|
2661
5243
|
if (typeof stored !== "object" || stored === null || Array.isArray(stored)) {
|
|
2662
|
-
return ["
|
|
5244
|
+
return ["sessionCompatibility"];
|
|
2663
5245
|
}
|
|
2664
5246
|
const record = stored as Record<string, unknown>;
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
5247
|
+
const fields: readonly (keyof SessionCompatibilityContract)[] = [
|
|
5248
|
+
"modelName",
|
|
5249
|
+
"profileName",
|
|
5250
|
+
"includeReasoningContent",
|
|
5251
|
+
"contextProfile",
|
|
5252
|
+
"messageProtocol",
|
|
5253
|
+
"imageInput",
|
|
5254
|
+
];
|
|
5255
|
+
return fields.filter((key) => {
|
|
5256
|
+
const storedValue = record[key];
|
|
5257
|
+
const currentValue = current[key];
|
|
5258
|
+
if (storedValue === undefined || currentValue === undefined) {
|
|
5259
|
+
return storedValue !== currentValue;
|
|
5260
|
+
}
|
|
5261
|
+
return stableJsonStringify(storedValue) !== stableJsonStringify(currentValue);
|
|
5262
|
+
});
|
|
5263
|
+
}
|
|
5264
|
+
|
|
5265
|
+
function decodeSessionCompatibilityContract(
|
|
5266
|
+
json: string,
|
|
5267
|
+
): SessionCompatibilityContract {
|
|
5268
|
+
const record = recordFromSql(
|
|
5269
|
+
parseJson(json, "session_compatibility_json"),
|
|
5270
|
+
"session compatibility contract",
|
|
5271
|
+
);
|
|
5272
|
+
assertObjectKeys(
|
|
5273
|
+
record,
|
|
5274
|
+
[
|
|
5275
|
+
"modelName",
|
|
5276
|
+
"profileName",
|
|
5277
|
+
"includeReasoningContent",
|
|
5278
|
+
"contextProfile",
|
|
5279
|
+
"messageProtocol",
|
|
5280
|
+
"imageInput",
|
|
5281
|
+
],
|
|
5282
|
+
[
|
|
5283
|
+
"modelName",
|
|
5284
|
+
"includeReasoningContent",
|
|
5285
|
+
"contextProfile",
|
|
5286
|
+
"messageProtocol",
|
|
5287
|
+
"imageInput",
|
|
5288
|
+
],
|
|
5289
|
+
"session compatibility contract",
|
|
5290
|
+
);
|
|
5291
|
+
const contextProfile = recordFromSql(
|
|
5292
|
+
record.contextProfile,
|
|
5293
|
+
"session compatibility context profile",
|
|
5294
|
+
);
|
|
5295
|
+
assertObjectKeys(
|
|
5296
|
+
contextProfile,
|
|
5297
|
+
["contextWindowTokens", "maxSupportedOutputTokens"],
|
|
5298
|
+
["contextWindowTokens", "maxSupportedOutputTokens"],
|
|
5299
|
+
"session compatibility context profile",
|
|
5300
|
+
);
|
|
5301
|
+
const messageProtocol = recordFromSql(
|
|
5302
|
+
record.messageProtocol,
|
|
5303
|
+
"session compatibility message protocol",
|
|
5304
|
+
);
|
|
5305
|
+
assertObjectKeys(
|
|
5306
|
+
messageProtocol,
|
|
5307
|
+
["adapter", "serializationVersion"],
|
|
5308
|
+
["adapter", "serializationVersion"],
|
|
5309
|
+
"session compatibility message protocol",
|
|
5310
|
+
);
|
|
5311
|
+
const imageInput = recordFromSql(
|
|
5312
|
+
record.imageInput,
|
|
5313
|
+
"session compatibility image input",
|
|
5314
|
+
);
|
|
5315
|
+
assertObjectKeys(
|
|
5316
|
+
imageInput,
|
|
5317
|
+
["policyVersion", "policySha256", "inputModalities", "tokenEstimator"],
|
|
5318
|
+
["policyVersion", "policySha256", "inputModalities"],
|
|
5319
|
+
"session compatibility image input",
|
|
5320
|
+
);
|
|
5321
|
+
const inputModalities = imageInput.inputModalities;
|
|
5322
|
+
if (!Array.isArray(inputModalities)) {
|
|
5323
|
+
throw new Error("Session compatibility input modalities must be an array.");
|
|
5324
|
+
}
|
|
5325
|
+
const tokenEstimator =
|
|
5326
|
+
imageInput.tokenEstimator === undefined
|
|
5327
|
+
? undefined
|
|
5328
|
+
: decodeTokenEstimatorCompatibility(imageInput.tokenEstimator);
|
|
5329
|
+
if (typeof record.includeReasoningContent !== "boolean") {
|
|
5330
|
+
throw new Error("Session compatibility reasoning replay flag must be boolean.");
|
|
5331
|
+
}
|
|
5332
|
+
const modelName = stringFromSql(record.modelName, "compatibility modelName");
|
|
5333
|
+
const profileName =
|
|
5334
|
+
record.profileName === undefined
|
|
5335
|
+
? undefined
|
|
5336
|
+
: stringFromSql(record.profileName, "compatibility profileName");
|
|
5337
|
+
const context = createModelContextProfile({
|
|
5338
|
+
contextWindowTokens: numberFromJson(
|
|
5339
|
+
contextProfile.contextWindowTokens,
|
|
5340
|
+
"compatibility contextWindowTokens",
|
|
5341
|
+
),
|
|
5342
|
+
maxSupportedOutputTokens: numberFromJson(
|
|
5343
|
+
contextProfile.maxSupportedOutputTokens,
|
|
5344
|
+
"compatibility maxSupportedOutputTokens",
|
|
5345
|
+
),
|
|
5346
|
+
});
|
|
5347
|
+
const protocol: ModelMessageProtocol = Object.freeze({
|
|
5348
|
+
adapter: enumFromSql(
|
|
5349
|
+
messageProtocol.adapter,
|
|
5350
|
+
["openai-chat", "fake"] as const,
|
|
5351
|
+
"compatibility message adapter",
|
|
5352
|
+
),
|
|
5353
|
+
serializationVersion: stringFromSql(
|
|
5354
|
+
messageProtocol.serializationVersion,
|
|
5355
|
+
"compatibility serializationVersion",
|
|
5356
|
+
),
|
|
5357
|
+
});
|
|
5358
|
+
const modalities = normalizeInputModalities(
|
|
5359
|
+
inputModalities.map((value) =>
|
|
5360
|
+
enumFromSql(value, ["text", "image"] as const, "compatibility input modality"),
|
|
5361
|
+
),
|
|
5362
|
+
);
|
|
5363
|
+
if (modalities.includes("image") && tokenEstimator === undefined) {
|
|
5364
|
+
throw new Error("Stored image input compatibility has no token estimator.");
|
|
5365
|
+
}
|
|
5366
|
+
const policyVersion = stringFromSql(
|
|
5367
|
+
imageInput.policyVersion,
|
|
5368
|
+
"compatibility image policyVersion",
|
|
5369
|
+
);
|
|
5370
|
+
const policySha256 = stringFromSql(
|
|
5371
|
+
imageInput.policySha256,
|
|
5372
|
+
"compatibility image policySha256",
|
|
5373
|
+
);
|
|
5374
|
+
if (!/^[0-9a-f]{64}$/.test(policySha256)) {
|
|
5375
|
+
throw new Error("Session image policy hash is invalid.");
|
|
5376
|
+
}
|
|
5377
|
+
return Object.freeze({
|
|
5378
|
+
modelName,
|
|
5379
|
+
...(record.profileName === undefined ? {} : { profileName: profileName! }),
|
|
5380
|
+
includeReasoningContent: record.includeReasoningContent,
|
|
5381
|
+
contextProfile: Object.freeze(context),
|
|
5382
|
+
messageProtocol: protocol,
|
|
5383
|
+
imageInput: Object.freeze({
|
|
5384
|
+
policyVersion,
|
|
5385
|
+
policySha256,
|
|
5386
|
+
inputModalities: modalities,
|
|
5387
|
+
...(tokenEstimator === undefined ? {} : { tokenEstimator }),
|
|
5388
|
+
}),
|
|
5389
|
+
});
|
|
5390
|
+
}
|
|
5391
|
+
|
|
5392
|
+
function decodeTokenEstimatorCompatibility(
|
|
5393
|
+
value: unknown,
|
|
5394
|
+
): InputTokenEstimatorCompatibility {
|
|
5395
|
+
const record = recordFromSql(value, "session compatibility token estimator");
|
|
5396
|
+
assertObjectKeys(
|
|
5397
|
+
record,
|
|
5398
|
+
["kind", "coverageVersion", "model", "endpoint", "timeoutMs", "maxRetries"],
|
|
5399
|
+
["kind", "coverageVersion", "model", "endpoint", "timeoutMs", "maxRetries"],
|
|
5400
|
+
"session compatibility token estimator",
|
|
2669
5401
|
);
|
|
5402
|
+
const estimator: InputTokenEstimatorCompatibility = {
|
|
5403
|
+
kind: enumFromSql(
|
|
5404
|
+
record.kind,
|
|
5405
|
+
["moonshot-estimate-token-count-v1"] as const,
|
|
5406
|
+
"compatibility token estimator kind",
|
|
5407
|
+
),
|
|
5408
|
+
coverageVersion: enumFromSql(
|
|
5409
|
+
record.coverageVersion,
|
|
5410
|
+
["full-request-v1"] as const,
|
|
5411
|
+
"compatibility token estimator coverage",
|
|
5412
|
+
),
|
|
5413
|
+
model: stringFromSql(record.model, "compatibility token estimator model"),
|
|
5414
|
+
endpoint: stringFromSql(record.endpoint, "compatibility token estimator endpoint"),
|
|
5415
|
+
timeoutMs: numberFromJson(
|
|
5416
|
+
record.timeoutMs,
|
|
5417
|
+
"compatibility token estimator timeoutMs",
|
|
5418
|
+
),
|
|
5419
|
+
maxRetries: zeroFromJson(
|
|
5420
|
+
record.maxRetries,
|
|
5421
|
+
"compatibility token estimator maxRetries",
|
|
5422
|
+
),
|
|
5423
|
+
};
|
|
5424
|
+
validateTokenEstimatorCompatibility(estimator);
|
|
5425
|
+
return Object.freeze(estimator);
|
|
2670
5426
|
}
|
|
2671
5427
|
|
|
2672
5428
|
function openWritableDatabase(databasePath: string): Database {
|
|
@@ -2696,6 +5452,250 @@ function runTransaction<T>(database: Database, operation: () => T): T {
|
|
|
2696
5452
|
}
|
|
2697
5453
|
}
|
|
2698
5454
|
|
|
5455
|
+
const SESSION_SCOPED_TABLES = [
|
|
5456
|
+
"session_meta",
|
|
5457
|
+
"turns",
|
|
5458
|
+
"iterations",
|
|
5459
|
+
"protocol_frames",
|
|
5460
|
+
"messages",
|
|
5461
|
+
"tool_results",
|
|
5462
|
+
"context_surfaces",
|
|
5463
|
+
"context_revisions",
|
|
5464
|
+
"context_overrides",
|
|
5465
|
+
"skill_activations",
|
|
5466
|
+
"context_measurement_state",
|
|
5467
|
+
] as const;
|
|
5468
|
+
|
|
5469
|
+
function rekeyStoredToolCalls(database: Database, targetSessionId: SessionId): void {
|
|
5470
|
+
const rows = database
|
|
5471
|
+
.query(
|
|
5472
|
+
`SELECT message_id, tool_calls_json FROM messages
|
|
5473
|
+
WHERE tool_calls_json IS NOT NULL ORDER BY ordinal`,
|
|
5474
|
+
)
|
|
5475
|
+
.all() as Array<{ message_id: string; tool_calls_json: string }>;
|
|
5476
|
+
for (const row of rows) {
|
|
5477
|
+
const calls = decodeStoredToolCalls(row.tool_calls_json).map((call) => ({
|
|
5478
|
+
...call,
|
|
5479
|
+
sessionId: targetSessionId,
|
|
5480
|
+
}));
|
|
5481
|
+
database
|
|
5482
|
+
.query("UPDATE messages SET tool_calls_json = ? WHERE message_id = ?")
|
|
5483
|
+
.run(stableJsonStringify(calls), row.message_id);
|
|
5484
|
+
}
|
|
5485
|
+
}
|
|
5486
|
+
|
|
5487
|
+
function rekeyProtocolView(
|
|
5488
|
+
source: ProtocolContextView,
|
|
5489
|
+
targetSessionId: SessionId,
|
|
5490
|
+
): ProtocolContextView {
|
|
5491
|
+
return {
|
|
5492
|
+
sessionId: targetSessionId,
|
|
5493
|
+
faulted: source.faulted,
|
|
5494
|
+
frames: source.frames.map((frame) => ({
|
|
5495
|
+
...frame,
|
|
5496
|
+
sessionId: targetSessionId,
|
|
5497
|
+
})),
|
|
5498
|
+
messages: source.messages.map((message) => ({
|
|
5499
|
+
...message,
|
|
5500
|
+
sessionId: targetSessionId,
|
|
5501
|
+
...(message.role === "assistant" && message.toolCalls !== undefined
|
|
5502
|
+
? {
|
|
5503
|
+
toolCalls: message.toolCalls.map((call) => ({
|
|
5504
|
+
...call,
|
|
5505
|
+
sessionId: targetSessionId,
|
|
5506
|
+
})),
|
|
5507
|
+
}
|
|
5508
|
+
: {}),
|
|
5509
|
+
})),
|
|
5510
|
+
toolResults: source.toolResults.map((result) => ({
|
|
5511
|
+
...result,
|
|
5512
|
+
sessionId: targetSessionId,
|
|
5513
|
+
})),
|
|
5514
|
+
};
|
|
5515
|
+
}
|
|
5516
|
+
|
|
5517
|
+
function rewriteCloneRevisionHashes(
|
|
5518
|
+
database: Database,
|
|
5519
|
+
canonical: ProtocolContextView,
|
|
5520
|
+
): void {
|
|
5521
|
+
const surfaces = database
|
|
5522
|
+
.query("SELECT * FROM context_surfaces")
|
|
5523
|
+
.all()
|
|
5524
|
+
.map(decodeContextSurface);
|
|
5525
|
+
const surfacesById = new Map(surfaces.map((surface) => [surface.surfaceId, surface]));
|
|
5526
|
+
const revisions = database
|
|
5527
|
+
.query("SELECT * FROM context_revisions ORDER BY revision_number")
|
|
5528
|
+
.all()
|
|
5529
|
+
.map(decodeContextRevision);
|
|
5530
|
+
const revisionNumberById = new Map(
|
|
5531
|
+
revisions.map((revision) => [revision.revisionId, revision.revisionNumber]),
|
|
5532
|
+
);
|
|
5533
|
+
const overrides = database
|
|
5534
|
+
.query(
|
|
5535
|
+
`SELECT co.* FROM context_overrides co
|
|
5536
|
+
JOIN context_revisions cr ON cr.revision_id = co.introduced_revision_id
|
|
5537
|
+
ORDER BY cr.revision_number, co.ordinal`,
|
|
5538
|
+
)
|
|
5539
|
+
.all()
|
|
5540
|
+
.map(decodeStoredSwapOverride);
|
|
5541
|
+
const compiler = new ContextRevisionCompiler();
|
|
5542
|
+
for (const revision of revisions) {
|
|
5543
|
+
const surface = surfacesById.get(revision.surfaceId);
|
|
5544
|
+
if (surface === undefined) {
|
|
5545
|
+
throw new Error(`Cloned revision ${revision.revisionId} has no surface.`);
|
|
5546
|
+
}
|
|
5547
|
+
const activeOverrides = overrides.filter(
|
|
5548
|
+
(override) =>
|
|
5549
|
+
(revisionNumberById.get(override.introducedRevisionId) ??
|
|
5550
|
+
Number.POSITIVE_INFINITY) <= revision.revisionNumber &&
|
|
5551
|
+
override.ordinal >= revision.keepFromOrdinal,
|
|
5552
|
+
);
|
|
5553
|
+
const prefix = protocolPrefixView(canonical, revision.sourceThroughOrdinal);
|
|
5554
|
+
const compiled = compiler.compileForIdentityRekey({
|
|
5555
|
+
canonical: prefix,
|
|
5556
|
+
revisionId: revision.revisionId,
|
|
5557
|
+
activeOverrides,
|
|
5558
|
+
keepFromOrdinal: revision.keepFromOrdinal,
|
|
5559
|
+
surface,
|
|
5560
|
+
});
|
|
5561
|
+
database
|
|
5562
|
+
.query(
|
|
5563
|
+
`UPDATE context_revisions
|
|
5564
|
+
SET canonical_sequence_sha256 = ?, rendered_message_sha256 = ?
|
|
5565
|
+
WHERE revision_id = ?`,
|
|
5566
|
+
)
|
|
5567
|
+
.run(
|
|
5568
|
+
canonicalSequenceHash(canonical, revision.sourceThroughOrdinal),
|
|
5569
|
+
renderedMessageHash(compiled.entries, revision.sourceThroughOrdinal),
|
|
5570
|
+
revision.revisionId,
|
|
5571
|
+
);
|
|
5572
|
+
}
|
|
5573
|
+
}
|
|
5574
|
+
|
|
5575
|
+
async function cloneDiagnosticFiles(input: {
|
|
5576
|
+
sourceDirectory: string;
|
|
5577
|
+
stagingDirectory: string;
|
|
5578
|
+
sourceSessionId: SessionId;
|
|
5579
|
+
targetSessionId: SessionId;
|
|
5580
|
+
nextEventSequence: number;
|
|
5581
|
+
faultInjector?: (stage: CloneSessionFaultStage) => void;
|
|
5582
|
+
}): Promise<void> {
|
|
5583
|
+
const sourcePath = path.join(input.sourceDirectory, "events.jsonl");
|
|
5584
|
+
try {
|
|
5585
|
+
await lstat(sourcePath);
|
|
5586
|
+
} catch (error) {
|
|
5587
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
5588
|
+
return;
|
|
5589
|
+
}
|
|
5590
|
+
throw error;
|
|
5591
|
+
}
|
|
5592
|
+
await validateSecureFile(sourcePath, input.sourceSessionId);
|
|
5593
|
+
|
|
5594
|
+
const bytes = await readFile(sourcePath);
|
|
5595
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
5596
|
+
const rawLines = text.split("\n");
|
|
5597
|
+
if (rawLines.at(-1) === "") {
|
|
5598
|
+
rawLines.pop();
|
|
5599
|
+
}
|
|
5600
|
+
const events: AgentEvent[] = [];
|
|
5601
|
+
let previousSequence = 0;
|
|
5602
|
+
for (const [index, line] of rawLines.entries()) {
|
|
5603
|
+
if (line === "") {
|
|
5604
|
+
throw new Error(`Session event log contains an empty line at ${index + 1}.`);
|
|
5605
|
+
}
|
|
5606
|
+
let value: unknown;
|
|
5607
|
+
try {
|
|
5608
|
+
value = JSON.parse(line);
|
|
5609
|
+
} catch (error) {
|
|
5610
|
+
throw new Error(`Session event log has invalid JSON at line ${index + 1}.`, {
|
|
5611
|
+
cause: error,
|
|
5612
|
+
});
|
|
5613
|
+
}
|
|
5614
|
+
if (!isEventEnvelope(value)) {
|
|
5615
|
+
throw new Error(
|
|
5616
|
+
`Session event log has an invalid envelope at line ${index + 1}.`,
|
|
5617
|
+
);
|
|
5618
|
+
}
|
|
5619
|
+
if (value.sessionId !== input.sourceSessionId) {
|
|
5620
|
+
throw new Error(`Session event log identity changed at line ${index + 1}.`);
|
|
5621
|
+
}
|
|
5622
|
+
if (value.eventSequence <= previousSequence) {
|
|
5623
|
+
throw new Error(
|
|
5624
|
+
`Session event sequence is not strictly increasing at line ${index + 1}.`,
|
|
5625
|
+
);
|
|
5626
|
+
}
|
|
5627
|
+
if (value.eventSequence >= input.nextEventSequence) {
|
|
5628
|
+
throw new Error(
|
|
5629
|
+
`Session event sequence exceeds the canonical next counter at line ${index + 1}.`,
|
|
5630
|
+
);
|
|
5631
|
+
}
|
|
5632
|
+
previousSequence = value.eventSequence;
|
|
5633
|
+
events.push({ ...value, sessionId: input.targetSessionId });
|
|
5634
|
+
}
|
|
5635
|
+
|
|
5636
|
+
const eventText = events.map((event) => JSON.stringify(event)).join("\n");
|
|
5637
|
+
await writePrivateNewFile(
|
|
5638
|
+
path.join(input.stagingDirectory, "events.jsonl"),
|
|
5639
|
+
eventText === "" ? "" : `${eventText}\n`,
|
|
5640
|
+
);
|
|
5641
|
+
input.faultInjector?.("after_event_rewrite");
|
|
5642
|
+
const observationText = events
|
|
5643
|
+
.map((event) => renderObservationLogEvent(event))
|
|
5644
|
+
.filter((block): block is string => block !== undefined)
|
|
5645
|
+
.join("");
|
|
5646
|
+
await writePrivateNewFile(
|
|
5647
|
+
path.join(input.stagingDirectory, "observations.md"),
|
|
5648
|
+
observationText,
|
|
5649
|
+
);
|
|
5650
|
+
input.faultInjector?.("after_observation_render");
|
|
5651
|
+
}
|
|
5652
|
+
|
|
5653
|
+
function isEventEnvelope(value: unknown): value is AgentEvent {
|
|
5654
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
5655
|
+
return false;
|
|
5656
|
+
}
|
|
5657
|
+
const record = value as Record<string, unknown>;
|
|
5658
|
+
return (
|
|
5659
|
+
typeof record.sessionId === "string" &&
|
|
5660
|
+
Number.isSafeInteger(record.eventSequence) &&
|
|
5661
|
+
Number(record.eventSequence) >= 1 &&
|
|
5662
|
+
typeof record.timestamp === "string" &&
|
|
5663
|
+
typeof record.type === "string" &&
|
|
5664
|
+
record.data !== null &&
|
|
5665
|
+
typeof record.data === "object"
|
|
5666
|
+
);
|
|
5667
|
+
}
|
|
5668
|
+
|
|
5669
|
+
async function writePrivateNewFile(filePath: string, content: string): Promise<void> {
|
|
5670
|
+
const handle = await open(filePath, "wx", 0o600);
|
|
5671
|
+
try {
|
|
5672
|
+
await handle.writeFile(content, "utf8");
|
|
5673
|
+
} finally {
|
|
5674
|
+
await handle.close();
|
|
5675
|
+
}
|
|
5676
|
+
await chmod(filePath, 0o600);
|
|
5677
|
+
}
|
|
5678
|
+
|
|
5679
|
+
async function assertPathMissing(
|
|
5680
|
+
filePath: string,
|
|
5681
|
+
sessionId: SessionId,
|
|
5682
|
+
): Promise<void> {
|
|
5683
|
+
try {
|
|
5684
|
+
await lstat(filePath);
|
|
5685
|
+
} catch (error) {
|
|
5686
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
5687
|
+
return;
|
|
5688
|
+
}
|
|
5689
|
+
throw error;
|
|
5690
|
+
}
|
|
5691
|
+
throw new SessionError(
|
|
5692
|
+
"SESSION_ALREADY_EXISTS",
|
|
5693
|
+
"clone_session",
|
|
5694
|
+
`Session directory already exists: ${filePath}.`,
|
|
5695
|
+
{ sessionId },
|
|
5696
|
+
);
|
|
5697
|
+
}
|
|
5698
|
+
|
|
2699
5699
|
async function canonicalWorkspaceRoot(workspaceRoot: string): Promise<string> {
|
|
2700
5700
|
if (!path.isAbsolute(workspaceRoot)) {
|
|
2701
5701
|
throw new Error("Session workspace root must be absolute.");
|
|
@@ -3020,6 +6020,13 @@ function numberFromJson(value: unknown, name: string): number {
|
|
|
3020
6020
|
return value as number;
|
|
3021
6021
|
}
|
|
3022
6022
|
|
|
6023
|
+
function zeroFromJson(value: unknown, name: string): 0 {
|
|
6024
|
+
if (value !== 0) {
|
|
6025
|
+
throw new Error(`${name} must be 0.`);
|
|
6026
|
+
}
|
|
6027
|
+
return 0;
|
|
6028
|
+
}
|
|
6029
|
+
|
|
3023
6030
|
function enumFromSql<const T extends readonly string[]>(
|
|
3024
6031
|
value: unknown,
|
|
3025
6032
|
values: T,
|
|
@@ -3058,6 +6065,10 @@ function requireItem<T>(items: readonly T[], index: number, name: string): T {
|
|
|
3058
6065
|
return item;
|
|
3059
6066
|
}
|
|
3060
6067
|
|
|
6068
|
+
function compareCanonicalText(left: string, right: string): number {
|
|
6069
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
6070
|
+
}
|
|
6071
|
+
|
|
3061
6072
|
function errorMessage(error: unknown): string {
|
|
3062
6073
|
return error instanceof Error ? error.message : String(error);
|
|
3063
6074
|
}
|