tinker-agent 1.0.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -0
- package/package.json +78 -0
- package/patches/markdansi@0.3.2.patch +37 -0
- package/src/agent/context-builder.ts +43 -0
- package/src/agent/context-meter.ts +310 -0
- package/src/agent/loop.ts +525 -0
- package/src/agent/runtime-session.ts +1212 -0
- package/src/agent/session-ledger.ts +828 -0
- package/src/agent/turn-cancellation.ts +44 -0
- package/src/agent/types.ts +77 -0
- package/src/cli/config.ts +283 -0
- package/src/cli/index.ts +29 -0
- package/src/cli/model-profiles.ts +289 -0
- package/src/cli/run-runner.ts +107 -0
- package/src/cli/tui-runner.tsx +290 -0
- package/src/context/compiled-context-hash.ts +138 -0
- package/src/context/compiled-context-validator.ts +209 -0
- package/src/context/context-manager.ts +362 -0
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +463 -0
- package/src/context/context-revision-compiler.ts +281 -0
- package/src/context/context-revision.ts +111 -0
- package/src/context/context-source.ts +30 -0
- package/src/context/context-swap-renderer.ts +272 -0
- package/src/context/protocol-frame.ts +240 -0
- package/src/context/swap-planner.ts +725 -0
- package/src/events/append-private-file.ts +16 -0
- package/src/events/bash-result-detail.ts +70 -0
- package/src/events/composite-event-sink.ts +82 -0
- package/src/events/event-sink.ts +16 -0
- package/src/events/jsonl-event-log.ts +13 -0
- package/src/events/observation-text-log.ts +195 -0
- package/src/events/stdout-event-printer.ts +396 -0
- package/src/events/types.ts +263 -0
- package/src/ids/runtime-id.ts +68 -0
- package/src/ids/uuid-v7.ts +5 -0
- package/src/instructions/project-instructions.ts +242 -0
- package/src/mcp/mcp-config.ts +144 -0
- package/src/mcp/mcp-manager.ts +216 -0
- package/src/mcp/mcp-tool-executor.ts +178 -0
- package/src/model/committed-prefix-auditor.ts +68 -0
- package/src/model/fake-model-client.ts +280 -0
- package/src/model/model-client.ts +64 -0
- package/src/model/model-context-profile.ts +134 -0
- package/src/model/model-request-preflight.ts +120 -0
- package/src/model/openai-chat-mapping.ts +444 -0
- package/src/model/openai-chat-model-client.ts +190 -0
- package/src/model/prompt-prefix-hash.ts +47 -0
- package/src/model/token-estimator.ts +148 -0
- package/src/observation/observation-builder.ts +481 -0
- package/src/session/resume-projection.ts +616 -0
- package/src/session/session-catalog.ts +270 -0
- package/src/session/session-errors.ts +121 -0
- package/src/session/session-history-reader.ts +535 -0
- package/src/session/session-lock.ts +291 -0
- package/src/session/session-schema.ts +741 -0
- package/src/session/session-store.ts +3067 -0
- package/src/session/sqlite-session-ledger.ts +153 -0
- package/src/tools/bash-task.ts +617 -0
- package/src/tools/bash.ts +450 -0
- package/src/tools/cwd-state.ts +22 -0
- package/src/tools/edit.ts +428 -0
- package/src/tools/file-diff.ts +116 -0
- package/src/tools/glob.ts +202 -0
- package/src/tools/grep.ts +550 -0
- package/src/tools/hash.ts +9 -0
- package/src/tools/path-safety.ts +33 -0
- package/src/tools/read.ts +319 -0
- package/src/tools/recall.ts +400 -0
- package/src/tools/registry.ts +213 -0
- package/src/tools/ripgrep.ts +220 -0
- package/src/tools/task-list.ts +59 -0
- package/src/tools/task-output-snapshot.ts +47 -0
- package/src/tools/task-output-tool.ts +62 -0
- package/src/tools/task-output.ts +159 -0
- package/src/tools/task-stop.ts +59 -0
- package/src/tools/task-tool-args.ts +29 -0
- package/src/tools/types.ts +330 -0
- package/src/tools/web-fetch/backend.ts +27 -0
- package/src/tools/web-fetch/browser-backend.ts +126 -0
- package/src/tools/web-fetch/exa-backend.ts +172 -0
- package/src/tools/web-fetch/index.ts +298 -0
- package/src/tools/web-fetch/local-backend.ts +267 -0
- package/src/tools/web-fetch/refiner.ts +78 -0
- package/src/tools/web-fetch/route.ts +95 -0
- package/src/tools/web-search.ts +300 -0
- package/src/tools/write.ts +244 -0
- package/src/tui/app.tsx +497 -0
- package/src/tui/components/assistant-markdown.tsx +47 -0
- package/src/tui/components/background-tasks.tsx +92 -0
- package/src/tui/components/bash-result-view.tsx +47 -0
- package/src/tui/components/context-status.tsx +127 -0
- package/src/tui/components/diff-view.tsx +151 -0
- package/src/tui/components/file-viewer.tsx +212 -0
- package/src/tui/components/footer.tsx +60 -0
- package/src/tui/components/header.tsx +21 -0
- package/src/tui/components/model-picker.tsx +142 -0
- package/src/tui/components/prompt-input.tsx +432 -0
- package/src/tui/components/resume-session-picker.tsx +273 -0
- package/src/tui/components/timeline.tsx +121 -0
- package/src/tui/context-format.ts +24 -0
- package/src/tui/event-store.ts +865 -0
- package/src/tui/git-branch.ts +23 -0
- package/src/tui/line-editor.ts +157 -0
- package/src/tui/prompt-history.ts +94 -0
- package/src/tui/slash-commands.ts +126 -0
- package/src/tui/tui-projection-policy.ts +35 -0
- package/src/tui/tui-projection-store.ts +123 -0
- package/src/tui/tui-session-controller.ts +170 -0
- package/src/tui/view-file.ts +122 -0
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { sha256, stableJsonStringify } from "../model/model-request-preflight";
|
|
3
|
+
import type { SessionId } from "../ids/runtime-id";
|
|
4
|
+
import { SessionError } from "./session-errors";
|
|
5
|
+
|
|
6
|
+
export const SESSION_APPLICATION_ID = 0x544b5231;
|
|
7
|
+
export const SESSION_SCHEMA_VERSION = 5;
|
|
8
|
+
|
|
9
|
+
type SchemaDefinition = {
|
|
10
|
+
type: "table" | "index" | "trigger" | "view";
|
|
11
|
+
name: string;
|
|
12
|
+
sql: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const RECALL_FTS_SHADOW_OBJECTS = [
|
|
16
|
+
{ type: "table", name: "message_fts_config" },
|
|
17
|
+
{ type: "table", name: "message_fts_data" },
|
|
18
|
+
{ type: "table", name: "message_fts_docsize" },
|
|
19
|
+
{ type: "table", name: "message_fts_idx" },
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
const RECALL_FTS_CONFIG = [{ key: "version", value: 4 }] as const;
|
|
23
|
+
|
|
24
|
+
const schemaDefinitions: readonly SchemaDefinition[] = [
|
|
25
|
+
{
|
|
26
|
+
type: "table",
|
|
27
|
+
name: "session_meta",
|
|
28
|
+
sql: `CREATE TABLE session_meta (
|
|
29
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
30
|
+
schema_version INTEGER NOT NULL CHECK (schema_version = 5),
|
|
31
|
+
schema_fingerprint TEXT NOT NULL,
|
|
32
|
+
initialization_state TEXT NOT NULL CHECK (initialization_state IN ('creating', 'ready')),
|
|
33
|
+
session_id TEXT NOT NULL UNIQUE,
|
|
34
|
+
workspace_root TEXT NOT NULL,
|
|
35
|
+
model_name TEXT NOT NULL,
|
|
36
|
+
system_prompt_sha256 TEXT NOT NULL,
|
|
37
|
+
project_instruction_file TEXT CHECK (project_instruction_file IS NULL OR project_instruction_file IN ('AGENTS.md', 'CLAUDE.md')),
|
|
38
|
+
project_instruction_byte_length INTEGER CHECK (project_instruction_byte_length IS NULL OR project_instruction_byte_length >= 1),
|
|
39
|
+
project_instruction_sha256 TEXT CHECK (project_instruction_sha256 IS NULL OR length(project_instruction_sha256) = 64),
|
|
40
|
+
tool_schema_sha256 TEXT,
|
|
41
|
+
runtime_contract_json TEXT,
|
|
42
|
+
runtime_contract_sha256 TEXT,
|
|
43
|
+
active_revision_id TEXT NOT NULL,
|
|
44
|
+
next_turn_number INTEGER NOT NULL CHECK (next_turn_number >= 1),
|
|
45
|
+
next_event_sequence INTEGER NOT NULL CHECK (next_event_sequence >= 1),
|
|
46
|
+
open_count INTEGER NOT NULL CHECK (open_count >= 1),
|
|
47
|
+
created_at TEXT NOT NULL,
|
|
48
|
+
updated_at TEXT NOT NULL,
|
|
49
|
+
last_opened_at TEXT NOT NULL,
|
|
50
|
+
last_closed_at TEXT,
|
|
51
|
+
last_close_reason TEXT CHECK (last_close_reason IS NULL OR last_close_reason IN (
|
|
52
|
+
'oneshot_complete', 'tui_exit', 'session_switch', 'runner_failed', 'initialization_failed'
|
|
53
|
+
)),
|
|
54
|
+
CHECK ((runtime_contract_json IS NULL) = (runtime_contract_sha256 IS NULL)),
|
|
55
|
+
CHECK ((project_instruction_file IS NULL) = (project_instruction_byte_length IS NULL)),
|
|
56
|
+
CHECK ((project_instruction_file IS NULL) = (project_instruction_sha256 IS NULL)),
|
|
57
|
+
CHECK ((initialization_state = 'creating') OR runtime_contract_json IS NOT NULL)
|
|
58
|
+
) STRICT`,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: "table",
|
|
62
|
+
name: "turns",
|
|
63
|
+
sql: `CREATE TABLE turns (
|
|
64
|
+
session_id TEXT NOT NULL,
|
|
65
|
+
turn_id TEXT PRIMARY KEY,
|
|
66
|
+
turn_number INTEGER NOT NULL CHECK (turn_number >= 1),
|
|
67
|
+
status TEXT NOT NULL CHECK (status IN ('open', 'completed', 'failed', 'cancelled', 'interrupted')),
|
|
68
|
+
next_iteration_number INTEGER NOT NULL CHECK (next_iteration_number >= 1),
|
|
69
|
+
last_iteration_id TEXT,
|
|
70
|
+
final_message_id TEXT,
|
|
71
|
+
terminal_detail_json TEXT,
|
|
72
|
+
started_at TEXT NOT NULL,
|
|
73
|
+
finished_at TEXT,
|
|
74
|
+
UNIQUE (session_id, turn_number),
|
|
75
|
+
FOREIGN KEY (session_id) REFERENCES session_meta(session_id),
|
|
76
|
+
FOREIGN KEY (final_message_id) REFERENCES messages(message_id),
|
|
77
|
+
CHECK ((status = 'open') = (finished_at IS NULL))
|
|
78
|
+
) STRICT`,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: "table",
|
|
82
|
+
name: "iterations",
|
|
83
|
+
sql: `CREATE TABLE iterations (
|
|
84
|
+
session_id TEXT NOT NULL,
|
|
85
|
+
turn_id TEXT NOT NULL,
|
|
86
|
+
iteration_id TEXT PRIMARY KEY,
|
|
87
|
+
iteration_number INTEGER NOT NULL CHECK (iteration_number >= 1),
|
|
88
|
+
outcome TEXT NOT NULL CHECK (outcome IN ('open', 'continue', 'completed', 'failed', 'cancelled', 'interrupted')),
|
|
89
|
+
next_tool_call_number INTEGER NOT NULL CHECK (next_tool_call_number >= 1),
|
|
90
|
+
started_at TEXT NOT NULL,
|
|
91
|
+
finished_at TEXT,
|
|
92
|
+
UNIQUE (turn_id, iteration_number),
|
|
93
|
+
FOREIGN KEY (session_id) REFERENCES session_meta(session_id),
|
|
94
|
+
FOREIGN KEY (turn_id) REFERENCES turns(turn_id),
|
|
95
|
+
CHECK ((outcome = 'open') = (finished_at IS NULL))
|
|
96
|
+
) STRICT`,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: "table",
|
|
100
|
+
name: "protocol_frames",
|
|
101
|
+
sql: `CREATE TABLE protocol_frames (
|
|
102
|
+
frame_id TEXT PRIMARY KEY,
|
|
103
|
+
session_id TEXT NOT NULL,
|
|
104
|
+
turn_id TEXT,
|
|
105
|
+
iteration_id TEXT,
|
|
106
|
+
kind TEXT NOT NULL CHECK (kind IN ('system', 'user', 'assistant_text', 'tool_exchange')),
|
|
107
|
+
state TEXT NOT NULL CHECK (state IN ('open', 'closed')),
|
|
108
|
+
first_ordinal INTEGER NOT NULL CHECK (first_ordinal >= 1),
|
|
109
|
+
last_ordinal INTEGER,
|
|
110
|
+
created_at TEXT NOT NULL,
|
|
111
|
+
closed_at TEXT,
|
|
112
|
+
FOREIGN KEY (session_id) REFERENCES session_meta(session_id),
|
|
113
|
+
FOREIGN KEY (turn_id) REFERENCES turns(turn_id),
|
|
114
|
+
FOREIGN KEY (iteration_id) REFERENCES iterations(iteration_id),
|
|
115
|
+
CHECK ((state = 'open' AND last_ordinal IS NULL AND closed_at IS NULL) OR
|
|
116
|
+
(state = 'closed' AND last_ordinal IS NOT NULL AND closed_at IS NOT NULL)),
|
|
117
|
+
CHECK (last_ordinal IS NULL OR last_ordinal >= first_ordinal)
|
|
118
|
+
) STRICT`,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
type: "table",
|
|
122
|
+
name: "messages",
|
|
123
|
+
sql: `CREATE TABLE messages (
|
|
124
|
+
message_id TEXT PRIMARY KEY,
|
|
125
|
+
session_id TEXT NOT NULL,
|
|
126
|
+
frame_id TEXT NOT NULL,
|
|
127
|
+
ordinal INTEGER NOT NULL CHECK (ordinal >= 1),
|
|
128
|
+
role TEXT NOT NULL CHECK (role IN ('system', 'user', 'assistant', 'tool')),
|
|
129
|
+
turn_id TEXT,
|
|
130
|
+
iteration_id TEXT,
|
|
131
|
+
content TEXT,
|
|
132
|
+
content_sha256 TEXT NOT NULL,
|
|
133
|
+
reasoning_content TEXT,
|
|
134
|
+
reasoning_content_present INTEGER NOT NULL DEFAULT 0 CHECK (reasoning_content_present IN (0, 1)),
|
|
135
|
+
tool_calls_json TEXT,
|
|
136
|
+
provider TEXT,
|
|
137
|
+
model TEXT,
|
|
138
|
+
tool_call_id TEXT,
|
|
139
|
+
provider_tool_call_id TEXT,
|
|
140
|
+
name TEXT,
|
|
141
|
+
origin TEXT NOT NULL CHECK (origin IN ('runtime', 'user', 'model', 'tool')),
|
|
142
|
+
created_at TEXT NOT NULL,
|
|
143
|
+
UNIQUE (session_id, ordinal),
|
|
144
|
+
FOREIGN KEY (session_id) REFERENCES session_meta(session_id),
|
|
145
|
+
FOREIGN KEY (frame_id) REFERENCES protocol_frames(frame_id),
|
|
146
|
+
FOREIGN KEY (turn_id) REFERENCES turns(turn_id),
|
|
147
|
+
FOREIGN KEY (iteration_id) REFERENCES iterations(iteration_id),
|
|
148
|
+
CHECK (tool_calls_json IS NULL OR json_valid(tool_calls_json)),
|
|
149
|
+
CHECK (
|
|
150
|
+
(role = 'system' AND turn_id IS NULL AND iteration_id IS NULL AND content IS NOT NULL AND
|
|
151
|
+
reasoning_content IS NULL AND reasoning_content_present = 0 AND tool_calls_json IS NULL AND provider IS NULL AND model IS NULL AND
|
|
152
|
+
tool_call_id IS NULL AND provider_tool_call_id IS NULL AND name IS NULL AND origin = 'runtime') OR
|
|
153
|
+
(role = 'user' AND turn_id IS NOT NULL AND iteration_id IS NULL AND content IS NOT NULL AND
|
|
154
|
+
reasoning_content IS NULL AND reasoning_content_present = 0 AND tool_calls_json IS NULL AND provider IS NULL AND model IS NULL AND
|
|
155
|
+
tool_call_id IS NULL AND provider_tool_call_id IS NULL AND name IS NULL AND origin = 'user') OR
|
|
156
|
+
(role = 'assistant' AND turn_id IS NOT NULL AND iteration_id IS NOT NULL AND
|
|
157
|
+
provider IS NOT NULL AND model IS NOT NULL AND tool_call_id IS NULL AND
|
|
158
|
+
provider_tool_call_id IS NULL AND name IS NULL AND origin = 'model') OR
|
|
159
|
+
(role = 'tool' AND turn_id IS NOT NULL AND iteration_id IS NOT NULL AND content IS NOT NULL AND
|
|
160
|
+
reasoning_content IS NULL AND reasoning_content_present = 0 AND tool_calls_json IS NULL AND provider IS NULL AND model IS NULL AND
|
|
161
|
+
tool_call_id IS NOT NULL AND provider_tool_call_id IS NOT NULL AND name IS NOT NULL AND
|
|
162
|
+
origin IN ('tool', 'runtime'))
|
|
163
|
+
)
|
|
164
|
+
) STRICT`,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
type: "table",
|
|
168
|
+
name: "tool_results",
|
|
169
|
+
sql: `CREATE TABLE tool_results (
|
|
170
|
+
tool_call_id TEXT PRIMARY KEY,
|
|
171
|
+
session_id TEXT NOT NULL,
|
|
172
|
+
frame_id TEXT NOT NULL,
|
|
173
|
+
tool_message_id TEXT NOT NULL UNIQUE,
|
|
174
|
+
completion_kind TEXT NOT NULL CHECK (completion_kind IN ('returned', 'synthetic')),
|
|
175
|
+
raw_json TEXT,
|
|
176
|
+
raw_sha256 TEXT,
|
|
177
|
+
observation_format TEXT,
|
|
178
|
+
synthetic_reason TEXT CHECK (synthetic_reason IS NULL OR synthetic_reason IN (
|
|
179
|
+
'cancelled_active', 'skipped_after_cancel', 'failed_active', 'skipped_after_failure',
|
|
180
|
+
'interrupted_active', 'skipped_after_interruption'
|
|
181
|
+
)),
|
|
182
|
+
synthetic_detail TEXT,
|
|
183
|
+
observation_sha256 TEXT NOT NULL,
|
|
184
|
+
created_at TEXT NOT NULL,
|
|
185
|
+
FOREIGN KEY (session_id) REFERENCES session_meta(session_id),
|
|
186
|
+
FOREIGN KEY (frame_id) REFERENCES protocol_frames(frame_id),
|
|
187
|
+
FOREIGN KEY (tool_message_id) REFERENCES messages(message_id),
|
|
188
|
+
CHECK (raw_json IS NULL OR json_valid(raw_json)),
|
|
189
|
+
CHECK (
|
|
190
|
+
(completion_kind = 'returned' AND raw_json IS NOT NULL AND raw_sha256 IS NOT NULL AND
|
|
191
|
+
observation_format = 'tool-observation-v2' AND synthetic_reason IS NULL AND synthetic_detail IS NULL) OR
|
|
192
|
+
(completion_kind = 'synthetic' AND raw_json IS NULL AND raw_sha256 IS NULL AND
|
|
193
|
+
observation_format IS NULL AND synthetic_reason IS NOT NULL)
|
|
194
|
+
),
|
|
195
|
+
CHECK ((synthetic_reason = 'failed_active') = (synthetic_detail IS NOT NULL))
|
|
196
|
+
) STRICT`,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
type: "table",
|
|
200
|
+
name: "context_revisions",
|
|
201
|
+
sql: `CREATE TABLE context_revisions (
|
|
202
|
+
revision_id TEXT PRIMARY KEY,
|
|
203
|
+
session_id TEXT NOT NULL,
|
|
204
|
+
revision_number INTEGER NOT NULL CHECK (revision_number >= 1),
|
|
205
|
+
parent_revision_id TEXT,
|
|
206
|
+
kind TEXT NOT NULL CHECK (kind IN ('initial_full', 'swap_only')),
|
|
207
|
+
keep_from_ordinal INTEGER NOT NULL CHECK (keep_from_ordinal = 1),
|
|
208
|
+
source_through_ordinal INTEGER NOT NULL CHECK (source_through_ordinal >= 1),
|
|
209
|
+
added_override_count INTEGER NOT NULL CHECK (added_override_count >= 0),
|
|
210
|
+
total_override_count INTEGER NOT NULL CHECK (total_override_count >= 0),
|
|
211
|
+
override_manifest_sha256 TEXT NOT NULL CHECK (length(override_manifest_sha256) = 64),
|
|
212
|
+
canonical_sequence_sha256 TEXT NOT NULL CHECK (length(canonical_sequence_sha256) = 64),
|
|
213
|
+
rendered_message_sha256 TEXT NOT NULL CHECK (length(rendered_message_sha256) = 64),
|
|
214
|
+
policy_version TEXT,
|
|
215
|
+
renderer_format TEXT,
|
|
216
|
+
plan_sha256 TEXT CHECK (plan_sha256 IS NULL OR length(plan_sha256) = 64),
|
|
217
|
+
created_at TEXT NOT NULL,
|
|
218
|
+
UNIQUE (session_id, revision_number),
|
|
219
|
+
FOREIGN KEY (session_id) REFERENCES session_meta(session_id),
|
|
220
|
+
FOREIGN KEY (parent_revision_id) REFERENCES context_revisions(revision_id),
|
|
221
|
+
CHECK (
|
|
222
|
+
(kind = 'initial_full' AND revision_number = 1 AND parent_revision_id IS NULL AND
|
|
223
|
+
source_through_ordinal = 1 AND added_override_count = 0 AND
|
|
224
|
+
total_override_count = 0 AND policy_version IS NULL AND
|
|
225
|
+
renderer_format IS NULL AND plan_sha256 IS NULL) OR
|
|
226
|
+
(kind = 'swap_only' AND revision_number >= 2 AND parent_revision_id IS NOT NULL AND
|
|
227
|
+
added_override_count >= 1 AND total_override_count >= added_override_count AND
|
|
228
|
+
policy_version = 'swap-only-v1' AND
|
|
229
|
+
renderer_format = 'swap-observation-v1' AND plan_sha256 IS NOT NULL)
|
|
230
|
+
)
|
|
231
|
+
) STRICT`,
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
type: "table",
|
|
235
|
+
name: "context_overrides",
|
|
236
|
+
sql: `CREATE TABLE context_overrides (
|
|
237
|
+
introduced_revision_id TEXT NOT NULL,
|
|
238
|
+
session_id TEXT NOT NULL,
|
|
239
|
+
message_id TEXT NOT NULL,
|
|
240
|
+
frame_id TEXT NOT NULL,
|
|
241
|
+
ordinal INTEGER NOT NULL CHECK (ordinal >= 1),
|
|
242
|
+
representation TEXT NOT NULL CHECK (representation = 'swapped'),
|
|
243
|
+
renderer_format TEXT NOT NULL CHECK (renderer_format = 'swap-observation-v1'),
|
|
244
|
+
source TEXT NOT NULL,
|
|
245
|
+
original_content_sha256 TEXT NOT NULL CHECK (length(original_content_sha256) = 64),
|
|
246
|
+
rendered_content TEXT NOT NULL CHECK (length(rendered_content) > 0),
|
|
247
|
+
rendered_content_sha256 TEXT NOT NULL CHECK (length(rendered_content_sha256) = 64),
|
|
248
|
+
original_bytes INTEGER NOT NULL CHECK (original_bytes > 0),
|
|
249
|
+
rendered_bytes INTEGER NOT NULL CHECK (rendered_bytes > 0),
|
|
250
|
+
byte_savings INTEGER NOT NULL CHECK (byte_savings > 0),
|
|
251
|
+
created_at TEXT NOT NULL,
|
|
252
|
+
PRIMARY KEY (introduced_revision_id, message_id),
|
|
253
|
+
UNIQUE (session_id, message_id),
|
|
254
|
+
UNIQUE (introduced_revision_id, ordinal),
|
|
255
|
+
FOREIGN KEY (introduced_revision_id) REFERENCES context_revisions(revision_id),
|
|
256
|
+
FOREIGN KEY (session_id) REFERENCES session_meta(session_id),
|
|
257
|
+
FOREIGN KEY (message_id) REFERENCES messages(message_id),
|
|
258
|
+
FOREIGN KEY (frame_id) REFERENCES protocol_frames(frame_id),
|
|
259
|
+
CHECK (original_bytes = rendered_bytes + byte_savings)
|
|
260
|
+
) STRICT`,
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
type: "table",
|
|
264
|
+
name: "context_measurement_state",
|
|
265
|
+
sql: `CREATE TABLE context_measurement_state (
|
|
266
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
267
|
+
session_id TEXT NOT NULL UNIQUE,
|
|
268
|
+
revision_id TEXT NOT NULL,
|
|
269
|
+
total_tokens INTEGER NOT NULL CHECK (total_tokens >= 0),
|
|
270
|
+
prompt_tokens INTEGER NOT NULL CHECK (prompt_tokens >= 0),
|
|
271
|
+
completion_tokens INTEGER NOT NULL CHECK (completion_tokens >= 0),
|
|
272
|
+
segment_count INTEGER NOT NULL CHECK (segment_count >= 0),
|
|
273
|
+
prefix_hash TEXT NOT NULL CHECK (length(prefix_hash) = 64),
|
|
274
|
+
request_config_hash TEXT NOT NULL CHECK (length(request_config_hash) = 64),
|
|
275
|
+
tool_schema_hash TEXT NOT NULL CHECK (length(tool_schema_hash) = 64),
|
|
276
|
+
updated_at TEXT NOT NULL,
|
|
277
|
+
FOREIGN KEY (session_id) REFERENCES session_meta(session_id),
|
|
278
|
+
FOREIGN KEY (revision_id) REFERENCES context_revisions(revision_id),
|
|
279
|
+
CHECK (total_tokens = prompt_tokens + completion_tokens)
|
|
280
|
+
) STRICT`,
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
type: "index",
|
|
284
|
+
name: "idx_context_revisions_session_number",
|
|
285
|
+
sql: "CREATE INDEX idx_context_revisions_session_number ON context_revisions(session_id, revision_number)",
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
type: "index",
|
|
289
|
+
name: "idx_context_overrides_revision_ordinal",
|
|
290
|
+
sql: "CREATE INDEX idx_context_overrides_revision_ordinal ON context_overrides(introduced_revision_id, ordinal)",
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
type: "index",
|
|
294
|
+
name: "idx_frames_session_ordinal",
|
|
295
|
+
sql: "CREATE INDEX idx_frames_session_ordinal ON protocol_frames(session_id, first_ordinal)",
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
type: "index",
|
|
299
|
+
name: "idx_messages_frame_ordinal",
|
|
300
|
+
sql: "CREATE INDEX idx_messages_frame_ordinal ON messages(frame_id, ordinal)",
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
type: "index",
|
|
304
|
+
name: "idx_turns_session_recent",
|
|
305
|
+
sql: "CREATE INDEX idx_turns_session_recent ON turns(session_id, turn_number DESC)",
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
type: "view",
|
|
309
|
+
name: "recall_documents",
|
|
310
|
+
sql: `CREATE VIEW recall_documents AS
|
|
311
|
+
SELECT rowid AS docid, content
|
|
312
|
+
FROM messages
|
|
313
|
+
WHERE role IN ('user', 'assistant', 'tool')
|
|
314
|
+
AND content IS NOT NULL
|
|
315
|
+
AND length(content) > 0
|
|
316
|
+
AND NOT (role = 'tool' AND name = 'Recall')`,
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
type: "table",
|
|
320
|
+
name: "message_fts",
|
|
321
|
+
sql: `CREATE VIRTUAL TABLE message_fts USING fts5(
|
|
322
|
+
content,
|
|
323
|
+
content='recall_documents',
|
|
324
|
+
content_rowid='docid',
|
|
325
|
+
tokenize='trigram'
|
|
326
|
+
)`,
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
type: "trigger",
|
|
330
|
+
name: "messages_recall_index",
|
|
331
|
+
sql: `CREATE TRIGGER messages_recall_index
|
|
332
|
+
AFTER INSERT ON messages
|
|
333
|
+
WHEN NEW.role IN ('user', 'assistant', 'tool')
|
|
334
|
+
AND NEW.content IS NOT NULL
|
|
335
|
+
AND length(NEW.content) > 0
|
|
336
|
+
AND NOT (NEW.role = 'tool' AND NEW.name = 'Recall')
|
|
337
|
+
BEGIN
|
|
338
|
+
INSERT INTO message_fts(rowid, content)
|
|
339
|
+
VALUES (NEW.rowid, NEW.content);
|
|
340
|
+
END`,
|
|
341
|
+
},
|
|
342
|
+
...immutableTriggers("messages"),
|
|
343
|
+
...immutableTriggers("tool_results"),
|
|
344
|
+
...immutableTriggers("context_revisions"),
|
|
345
|
+
...immutableTriggers("context_overrides"),
|
|
346
|
+
{
|
|
347
|
+
type: "trigger",
|
|
348
|
+
name: "context_revisions_validate_insert",
|
|
349
|
+
sql: `CREATE TRIGGER context_revisions_validate_insert
|
|
350
|
+
BEFORE INSERT ON context_revisions
|
|
351
|
+
WHEN NOT (
|
|
352
|
+
NEW.session_id = (SELECT session_id FROM session_meta WHERE singleton = 1) AND
|
|
353
|
+
((NEW.kind = 'initial_full' AND
|
|
354
|
+
NEW.revision_id = (SELECT active_revision_id FROM session_meta WHERE singleton = 1) AND
|
|
355
|
+
NOT EXISTS (SELECT 1 FROM context_revisions WHERE session_id = NEW.session_id)) OR
|
|
356
|
+
(NEW.kind = 'swap_only' AND
|
|
357
|
+
NEW.parent_revision_id = (SELECT active_revision_id FROM session_meta WHERE singleton = 1) AND
|
|
358
|
+
NEW.revision_number = 1 + COALESCE((
|
|
359
|
+
SELECT revision_number FROM context_revisions
|
|
360
|
+
WHERE revision_id = NEW.parent_revision_id AND session_id = NEW.session_id
|
|
361
|
+
), 0) AND
|
|
362
|
+
NEW.source_through_ordinal = COALESCE((SELECT MAX(ordinal) FROM messages), 0)))
|
|
363
|
+
)
|
|
364
|
+
BEGIN SELECT RAISE(ABORT, 'invalid context revision insert'); END`,
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
type: "trigger",
|
|
368
|
+
name: "context_overrides_validate_insert",
|
|
369
|
+
sql: `CREATE TRIGGER context_overrides_validate_insert
|
|
370
|
+
BEFORE INSERT ON context_overrides
|
|
371
|
+
WHEN NOT (
|
|
372
|
+
EXISTS (
|
|
373
|
+
SELECT 1 FROM context_revisions cr
|
|
374
|
+
WHERE cr.revision_id = NEW.introduced_revision_id
|
|
375
|
+
AND cr.session_id = NEW.session_id
|
|
376
|
+
AND cr.kind = 'swap_only'
|
|
377
|
+
AND NEW.ordinal <= cr.source_through_ordinal
|
|
378
|
+
) AND
|
|
379
|
+
EXISTS (
|
|
380
|
+
SELECT 1
|
|
381
|
+
FROM messages m
|
|
382
|
+
JOIN protocol_frames pf ON pf.frame_id = m.frame_id
|
|
383
|
+
JOIN tool_results tr ON tr.tool_message_id = m.message_id
|
|
384
|
+
WHERE m.message_id = NEW.message_id
|
|
385
|
+
AND m.session_id = NEW.session_id
|
|
386
|
+
AND m.frame_id = NEW.frame_id
|
|
387
|
+
AND m.ordinal = NEW.ordinal
|
|
388
|
+
AND m.role = 'tool'
|
|
389
|
+
AND m.content_sha256 = NEW.original_content_sha256
|
|
390
|
+
AND length(CAST(m.content AS BLOB)) = NEW.original_bytes
|
|
391
|
+
AND pf.session_id = NEW.session_id
|
|
392
|
+
AND pf.kind = 'tool_exchange'
|
|
393
|
+
AND pf.state = 'closed'
|
|
394
|
+
AND tr.session_id = NEW.session_id
|
|
395
|
+
AND tr.frame_id = NEW.frame_id
|
|
396
|
+
AND tr.observation_sha256 = NEW.original_content_sha256
|
|
397
|
+
) AND
|
|
398
|
+
NEW.source = 'ctx://message/' || NEW.message_id AND
|
|
399
|
+
length(CAST(NEW.rendered_content AS BLOB)) = NEW.rendered_bytes AND
|
|
400
|
+
NEW.rendered_bytes < NEW.original_bytes
|
|
401
|
+
)
|
|
402
|
+
BEGIN SELECT RAISE(ABORT, 'invalid context override insert'); END`,
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
type: "trigger",
|
|
406
|
+
name: "context_measurement_active_insert",
|
|
407
|
+
sql: `CREATE TRIGGER context_measurement_active_insert
|
|
408
|
+
BEFORE INSERT ON context_measurement_state
|
|
409
|
+
WHEN NOT EXISTS (
|
|
410
|
+
SELECT 1 FROM session_meta sm
|
|
411
|
+
WHERE sm.singleton = 1 AND sm.session_id = NEW.session_id
|
|
412
|
+
AND sm.active_revision_id = NEW.revision_id
|
|
413
|
+
)
|
|
414
|
+
BEGIN SELECT RAISE(ABORT, 'context measurement revision is not active'); END`,
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
type: "trigger",
|
|
418
|
+
name: "context_measurement_active_update",
|
|
419
|
+
sql: `CREATE TRIGGER context_measurement_active_update
|
|
420
|
+
BEFORE UPDATE ON context_measurement_state
|
|
421
|
+
WHEN NOT EXISTS (
|
|
422
|
+
SELECT 1 FROM session_meta sm
|
|
423
|
+
WHERE sm.singleton = 1 AND sm.session_id = NEW.session_id
|
|
424
|
+
AND sm.active_revision_id = NEW.revision_id
|
|
425
|
+
)
|
|
426
|
+
BEGIN SELECT RAISE(ABORT, 'context measurement revision is not active'); END`,
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
type: "trigger",
|
|
430
|
+
name: "protocol_frames_no_delete",
|
|
431
|
+
sql: `CREATE TRIGGER protocol_frames_no_delete BEFORE DELETE ON protocol_frames
|
|
432
|
+
BEGIN SELECT RAISE(ABORT, 'protocol_frames are immutable'); END`,
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
type: "trigger",
|
|
436
|
+
name: "protocol_frames_monotonic_update",
|
|
437
|
+
sql: `CREATE TRIGGER protocol_frames_monotonic_update BEFORE UPDATE ON protocol_frames
|
|
438
|
+
WHEN NOT (
|
|
439
|
+
OLD.state = 'open' AND NEW.state = 'closed' AND
|
|
440
|
+
OLD.frame_id = NEW.frame_id AND OLD.session_id = NEW.session_id AND
|
|
441
|
+
OLD.turn_id IS NEW.turn_id AND OLD.iteration_id IS NEW.iteration_id AND
|
|
442
|
+
OLD.kind = NEW.kind AND OLD.first_ordinal = NEW.first_ordinal AND
|
|
443
|
+
OLD.created_at = NEW.created_at AND OLD.last_ordinal IS NULL AND
|
|
444
|
+
NEW.last_ordinal IS NOT NULL AND OLD.closed_at IS NULL AND NEW.closed_at IS NOT NULL
|
|
445
|
+
)
|
|
446
|
+
BEGIN SELECT RAISE(ABORT, 'invalid protocol frame transition'); END`,
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
type: "trigger",
|
|
450
|
+
name: "turns_no_delete",
|
|
451
|
+
sql: `CREATE TRIGGER turns_no_delete BEFORE DELETE ON turns
|
|
452
|
+
BEGIN SELECT RAISE(ABORT, 'turns cannot be deleted'); END`,
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
type: "trigger",
|
|
456
|
+
name: "turns_monotonic_update",
|
|
457
|
+
sql: `CREATE TRIGGER turns_monotonic_update BEFORE UPDATE ON turns
|
|
458
|
+
WHEN NOT (
|
|
459
|
+
OLD.session_id = NEW.session_id AND OLD.turn_id = NEW.turn_id AND
|
|
460
|
+
OLD.turn_number = NEW.turn_number AND OLD.started_at = NEW.started_at AND
|
|
461
|
+
((OLD.status = 'open' AND
|
|
462
|
+
NEW.status IN ('open', 'completed', 'failed', 'cancelled', 'interrupted') AND
|
|
463
|
+
NEW.next_iteration_number >= OLD.next_iteration_number AND
|
|
464
|
+
(OLD.last_iteration_id IS NULL OR NEW.last_iteration_id = OLD.last_iteration_id OR NEW.status = 'open') AND
|
|
465
|
+
OLD.final_message_id IS NULL AND OLD.terminal_detail_json IS NULL AND OLD.finished_at IS NULL) OR
|
|
466
|
+
(OLD.status <> 'open' AND NEW.status = OLD.status AND
|
|
467
|
+
NEW.next_iteration_number = OLD.next_iteration_number AND
|
|
468
|
+
NEW.last_iteration_id IS OLD.last_iteration_id AND
|
|
469
|
+
NEW.final_message_id IS OLD.final_message_id AND
|
|
470
|
+
NEW.terminal_detail_json IS OLD.terminal_detail_json AND
|
|
471
|
+
NEW.finished_at = OLD.finished_at))
|
|
472
|
+
)
|
|
473
|
+
BEGIN SELECT RAISE(ABORT, 'invalid turn transition'); END`,
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
type: "trigger",
|
|
477
|
+
name: "iterations_no_delete",
|
|
478
|
+
sql: `CREATE TRIGGER iterations_no_delete BEFORE DELETE ON iterations
|
|
479
|
+
BEGIN SELECT RAISE(ABORT, 'iterations cannot be deleted'); END`,
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
type: "trigger",
|
|
483
|
+
name: "iterations_monotonic_update",
|
|
484
|
+
sql: `CREATE TRIGGER iterations_monotonic_update BEFORE UPDATE ON iterations
|
|
485
|
+
WHEN NOT (
|
|
486
|
+
OLD.session_id = NEW.session_id AND OLD.turn_id = NEW.turn_id AND
|
|
487
|
+
OLD.iteration_id = NEW.iteration_id AND
|
|
488
|
+
OLD.iteration_number = NEW.iteration_number AND OLD.started_at = NEW.started_at AND
|
|
489
|
+
((OLD.outcome = 'open' AND
|
|
490
|
+
NEW.outcome IN ('open', 'continue', 'completed', 'failed', 'cancelled', 'interrupted') AND
|
|
491
|
+
NEW.next_tool_call_number >= OLD.next_tool_call_number AND OLD.finished_at IS NULL) OR
|
|
492
|
+
(OLD.outcome <> 'open' AND NEW.outcome = OLD.outcome AND
|
|
493
|
+
NEW.next_tool_call_number = OLD.next_tool_call_number AND
|
|
494
|
+
NEW.finished_at = OLD.finished_at))
|
|
495
|
+
)
|
|
496
|
+
BEGIN SELECT RAISE(ABORT, 'invalid iteration transition'); END`,
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
type: "trigger",
|
|
500
|
+
name: "session_meta_no_delete",
|
|
501
|
+
sql: `CREATE TRIGGER session_meta_no_delete BEFORE DELETE ON session_meta
|
|
502
|
+
BEGIN SELECT RAISE(ABORT, 'session metadata cannot be deleted'); END`,
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
type: "trigger",
|
|
506
|
+
name: "session_meta_monotonic_update",
|
|
507
|
+
sql: `CREATE TRIGGER session_meta_monotonic_update BEFORE UPDATE ON session_meta
|
|
508
|
+
WHEN NOT (
|
|
509
|
+
OLD.singleton = NEW.singleton AND OLD.schema_version = NEW.schema_version AND
|
|
510
|
+
OLD.schema_fingerprint = NEW.schema_fingerprint AND OLD.session_id = NEW.session_id AND
|
|
511
|
+
OLD.workspace_root = NEW.workspace_root AND OLD.model_name = NEW.model_name AND
|
|
512
|
+
OLD.system_prompt_sha256 = NEW.system_prompt_sha256 AND
|
|
513
|
+
OLD.project_instruction_file IS NEW.project_instruction_file AND
|
|
514
|
+
OLD.project_instruction_byte_length IS NEW.project_instruction_byte_length AND
|
|
515
|
+
OLD.project_instruction_sha256 IS NEW.project_instruction_sha256 AND
|
|
516
|
+
OLD.created_at = NEW.created_at AND
|
|
517
|
+
NEW.next_turn_number >= OLD.next_turn_number AND
|
|
518
|
+
NEW.next_event_sequence >= OLD.next_event_sequence AND
|
|
519
|
+
NEW.open_count >= OLD.open_count AND
|
|
520
|
+
((OLD.initialization_state = 'creating' AND NEW.initialization_state IN ('creating', 'ready')) OR
|
|
521
|
+
(OLD.initialization_state = 'ready' AND NEW.initialization_state = 'ready')) AND
|
|
522
|
+
(OLD.tool_schema_sha256 IS NULL OR NEW.tool_schema_sha256 = OLD.tool_schema_sha256) AND
|
|
523
|
+
(OLD.runtime_contract_json IS NULL OR NEW.runtime_contract_json = OLD.runtime_contract_json) AND
|
|
524
|
+
(OLD.runtime_contract_sha256 IS NULL OR NEW.runtime_contract_sha256 = OLD.runtime_contract_sha256) AND
|
|
525
|
+
(OLD.active_revision_id = NEW.active_revision_id OR EXISTS (
|
|
526
|
+
SELECT 1
|
|
527
|
+
FROM context_revisions previous
|
|
528
|
+
JOIN context_revisions next
|
|
529
|
+
ON next.parent_revision_id = previous.revision_id
|
|
530
|
+
AND next.session_id = previous.session_id
|
|
531
|
+
AND next.revision_number = previous.revision_number + 1
|
|
532
|
+
WHERE previous.revision_id = OLD.active_revision_id
|
|
533
|
+
AND next.revision_id = NEW.active_revision_id
|
|
534
|
+
AND next.session_id = OLD.session_id
|
|
535
|
+
AND next.kind = 'swap_only'
|
|
536
|
+
AND next.added_override_count = (
|
|
537
|
+
SELECT COUNT(*) FROM context_overrides
|
|
538
|
+
WHERE introduced_revision_id = next.revision_id
|
|
539
|
+
)
|
|
540
|
+
AND next.total_override_count = previous.total_override_count + next.added_override_count
|
|
541
|
+
AND NOT EXISTS (SELECT 1 FROM context_measurement_state)
|
|
542
|
+
))
|
|
543
|
+
)
|
|
544
|
+
BEGIN SELECT RAISE(ABORT, 'invalid session metadata transition'); END`,
|
|
545
|
+
},
|
|
546
|
+
];
|
|
547
|
+
|
|
548
|
+
export const SESSION_SCHEMA_V5_FINGERPRINT = sha256(
|
|
549
|
+
stableJsonStringify({
|
|
550
|
+
definitions: schemaDefinitions.map((definition) => ({
|
|
551
|
+
type: definition.type,
|
|
552
|
+
name: definition.name,
|
|
553
|
+
sql: normalizeSql(definition.sql),
|
|
554
|
+
})),
|
|
555
|
+
recallFts: {
|
|
556
|
+
config: RECALL_FTS_CONFIG,
|
|
557
|
+
shadowObjects: RECALL_FTS_SHADOW_OBJECTS,
|
|
558
|
+
},
|
|
559
|
+
}),
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
export function configureWritableDatabase(database: Database): void {
|
|
563
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
564
|
+
const journal = singlePragmaValue(database, "PRAGMA journal_mode = WAL");
|
|
565
|
+
if (String(journal).toLowerCase() !== "wal") {
|
|
566
|
+
throw new Error(`SQLite did not enable WAL mode; received ${String(journal)}.`);
|
|
567
|
+
}
|
|
568
|
+
database.exec("PRAGMA synchronous = FULL");
|
|
569
|
+
database.exec("PRAGMA busy_timeout = 0");
|
|
570
|
+
database.exec("PRAGMA trusted_schema = OFF");
|
|
571
|
+
database.exec("PRAGMA wal_autocheckpoint = 1000");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export function createSessionSchema(database: Database): void {
|
|
575
|
+
database.exec(`PRAGMA application_id = ${SESSION_APPLICATION_ID}`);
|
|
576
|
+
database.exec(`PRAGMA user_version = ${SESSION_SCHEMA_VERSION}`);
|
|
577
|
+
for (const definition of schemaDefinitions) {
|
|
578
|
+
database.exec(definition.sql);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export function verifySessionSchema(database: Database, sessionId?: SessionId): void {
|
|
583
|
+
const applicationId = Number(singlePragmaValue(database, "PRAGMA application_id"));
|
|
584
|
+
const userVersion = Number(singlePragmaValue(database, "PRAGMA user_version"));
|
|
585
|
+
if (
|
|
586
|
+
applicationId !== SESSION_APPLICATION_ID ||
|
|
587
|
+
userVersion !== SESSION_SCHEMA_VERSION
|
|
588
|
+
) {
|
|
589
|
+
throw new SessionError(
|
|
590
|
+
"SESSION_SCHEMA_UNSUPPORTED",
|
|
591
|
+
"verify_schema",
|
|
592
|
+
`Unsupported session schema identity application_id=${applicationId} user_version=${userVersion}.`,
|
|
593
|
+
{ sessionId },
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const actual = database
|
|
598
|
+
.query(
|
|
599
|
+
`SELECT type, name, sql FROM sqlite_schema
|
|
600
|
+
WHERE name NOT LIKE 'sqlite_%' AND sql IS NOT NULL
|
|
601
|
+
ORDER BY type, name`,
|
|
602
|
+
)
|
|
603
|
+
.all() as Array<{ type: string; name: string; sql: string }>;
|
|
604
|
+
const actualByName = new Map(
|
|
605
|
+
actual.map((entry) => [`${entry.type}:${entry.name}`, normalizeSql(entry.sql)]),
|
|
606
|
+
);
|
|
607
|
+
for (const expected of schemaDefinitions) {
|
|
608
|
+
const actualSql = actualByName.get(`${expected.type}:${expected.name}`);
|
|
609
|
+
if (actualSql !== normalizeSql(expected.sql)) {
|
|
610
|
+
throw new SessionError(
|
|
611
|
+
"SESSION_SCHEMA_INVALID",
|
|
612
|
+
"verify_schema",
|
|
613
|
+
`Session schema definition does not match: ${expected.type} ${expected.name}.`,
|
|
614
|
+
{ sessionId },
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
actualByName.delete(`${expected.type}:${expected.name}`);
|
|
618
|
+
}
|
|
619
|
+
for (const shadow of RECALL_FTS_SHADOW_OBJECTS) {
|
|
620
|
+
const key = `${shadow.type}:${shadow.name}`;
|
|
621
|
+
if (!actualByName.has(key)) {
|
|
622
|
+
throw new SessionError(
|
|
623
|
+
"SESSION_SCHEMA_INVALID",
|
|
624
|
+
"verify_schema",
|
|
625
|
+
`Session schema is missing FTS shadow object: ${key}.`,
|
|
626
|
+
{ sessionId },
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
actualByName.delete(key);
|
|
630
|
+
}
|
|
631
|
+
if (actualByName.size > 0) {
|
|
632
|
+
throw new SessionError(
|
|
633
|
+
"SESSION_SCHEMA_INVALID",
|
|
634
|
+
"verify_schema",
|
|
635
|
+
`Session schema contains unexpected definitions: ${[...actualByName.keys()].join(", ")}.`,
|
|
636
|
+
{ sessionId },
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const actualFtsConfig = database
|
|
641
|
+
.query("SELECT k, v FROM message_fts_config ORDER BY k")
|
|
642
|
+
.all()
|
|
643
|
+
.map((entry) => {
|
|
644
|
+
const row = entry as { k: unknown; v: unknown };
|
|
645
|
+
return { key: String(row.k), value: Number(row.v) };
|
|
646
|
+
});
|
|
647
|
+
if (stableJsonStringify(actualFtsConfig) !== stableJsonStringify(RECALL_FTS_CONFIG)) {
|
|
648
|
+
throw new SessionError(
|
|
649
|
+
"SESSION_SCHEMA_INVALID",
|
|
650
|
+
"verify_schema",
|
|
651
|
+
"Session FTS configuration does not match schema v5.",
|
|
652
|
+
{ sessionId },
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export function verifyRecallIndex(database: Database, sessionId?: SessionId): void {
|
|
658
|
+
try {
|
|
659
|
+
database
|
|
660
|
+
.query(
|
|
661
|
+
`INSERT INTO message_fts(message_fts, rank)
|
|
662
|
+
VALUES ('integrity-check', 1)`,
|
|
663
|
+
)
|
|
664
|
+
.run();
|
|
665
|
+
} catch (error) {
|
|
666
|
+
throw new SessionError(
|
|
667
|
+
"SESSION_RECALL_INDEX_INVALID",
|
|
668
|
+
"verify_recall_index",
|
|
669
|
+
"Session Recall index failed its external-content integrity check.",
|
|
670
|
+
{ sessionId, cause: error },
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export function rebuildRecallIndex(database: Database, sessionId?: SessionId): void {
|
|
676
|
+
try {
|
|
677
|
+
database.query("INSERT INTO message_fts(message_fts) VALUES ('rebuild')").run();
|
|
678
|
+
} catch (error) {
|
|
679
|
+
throw new SessionError(
|
|
680
|
+
"SESSION_RECALL_INDEX_INVALID",
|
|
681
|
+
"rebuild_recall_index",
|
|
682
|
+
"Session Recall index could not be rebuilt from canonical history.",
|
|
683
|
+
{ sessionId, cause: error },
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export function verifySqliteIntegrity(database: Database, sessionId?: SessionId): void {
|
|
689
|
+
const quick = database.query("PRAGMA quick_check").all() as Array<
|
|
690
|
+
Record<string, unknown>
|
|
691
|
+
>;
|
|
692
|
+
if (
|
|
693
|
+
quick.length !== 1 ||
|
|
694
|
+
String(Object.values(quick[0] ?? {})[0]).toLowerCase() !== "ok"
|
|
695
|
+
) {
|
|
696
|
+
throw new SessionError(
|
|
697
|
+
"SESSION_INTEGRITY_FAILED",
|
|
698
|
+
"quick_check",
|
|
699
|
+
"SQLite quick_check did not return ok.",
|
|
700
|
+
{ sessionId },
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
const foreignKeys = database.query("PRAGMA foreign_key_check").all();
|
|
704
|
+
if (foreignKeys.length !== 0) {
|
|
705
|
+
throw new SessionError(
|
|
706
|
+
"SESSION_INTEGRITY_FAILED",
|
|
707
|
+
"foreign_key_check",
|
|
708
|
+
"SQLite foreign_key_check found invalid references.",
|
|
709
|
+
{ sessionId },
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function immutableTriggers(table: string): SchemaDefinition[] {
|
|
715
|
+
return [
|
|
716
|
+
{
|
|
717
|
+
type: "trigger",
|
|
718
|
+
name: `${table}_no_update`,
|
|
719
|
+
sql: `CREATE TRIGGER ${table}_no_update BEFORE UPDATE ON ${table}
|
|
720
|
+
BEGIN SELECT RAISE(ABORT, '${table} are immutable'); END`,
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
type: "trigger",
|
|
724
|
+
name: `${table}_no_delete`,
|
|
725
|
+
sql: `CREATE TRIGGER ${table}_no_delete BEFORE DELETE ON ${table}
|
|
726
|
+
BEGIN SELECT RAISE(ABORT, '${table} are immutable'); END`,
|
|
727
|
+
},
|
|
728
|
+
];
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function singlePragmaValue(database: Database, sql: string): unknown {
|
|
732
|
+
const row = database.query(sql).get() as Record<string, unknown> | null;
|
|
733
|
+
if (row === null) {
|
|
734
|
+
throw new Error(`SQLite pragma returned no row: ${sql}.`);
|
|
735
|
+
}
|
|
736
|
+
return Object.values(row)[0];
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function normalizeSql(sql: string): string {
|
|
740
|
+
return sql.replace(/\s+/g, " ").trim().replace(/;$/, "").toLowerCase();
|
|
741
|
+
}
|