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.
Files changed (110) hide show
  1. package/README.md +173 -0
  2. package/package.json +78 -0
  3. package/patches/markdansi@0.3.2.patch +37 -0
  4. package/src/agent/context-builder.ts +43 -0
  5. package/src/agent/context-meter.ts +310 -0
  6. package/src/agent/loop.ts +525 -0
  7. package/src/agent/runtime-session.ts +1212 -0
  8. package/src/agent/session-ledger.ts +828 -0
  9. package/src/agent/turn-cancellation.ts +44 -0
  10. package/src/agent/types.ts +77 -0
  11. package/src/cli/config.ts +283 -0
  12. package/src/cli/index.ts +29 -0
  13. package/src/cli/model-profiles.ts +289 -0
  14. package/src/cli/run-runner.ts +107 -0
  15. package/src/cli/tui-runner.tsx +290 -0
  16. package/src/context/compiled-context-hash.ts +138 -0
  17. package/src/context/compiled-context-validator.ts +209 -0
  18. package/src/context/context-manager.ts +362 -0
  19. package/src/context/context-policy.ts +8 -0
  20. package/src/context/context-protocol-validator.ts +463 -0
  21. package/src/context/context-revision-compiler.ts +281 -0
  22. package/src/context/context-revision.ts +111 -0
  23. package/src/context/context-source.ts +30 -0
  24. package/src/context/context-swap-renderer.ts +272 -0
  25. package/src/context/protocol-frame.ts +240 -0
  26. package/src/context/swap-planner.ts +725 -0
  27. package/src/events/append-private-file.ts +16 -0
  28. package/src/events/bash-result-detail.ts +70 -0
  29. package/src/events/composite-event-sink.ts +82 -0
  30. package/src/events/event-sink.ts +16 -0
  31. package/src/events/jsonl-event-log.ts +13 -0
  32. package/src/events/observation-text-log.ts +195 -0
  33. package/src/events/stdout-event-printer.ts +396 -0
  34. package/src/events/types.ts +263 -0
  35. package/src/ids/runtime-id.ts +68 -0
  36. package/src/ids/uuid-v7.ts +5 -0
  37. package/src/instructions/project-instructions.ts +242 -0
  38. package/src/mcp/mcp-config.ts +144 -0
  39. package/src/mcp/mcp-manager.ts +216 -0
  40. package/src/mcp/mcp-tool-executor.ts +178 -0
  41. package/src/model/committed-prefix-auditor.ts +68 -0
  42. package/src/model/fake-model-client.ts +280 -0
  43. package/src/model/model-client.ts +64 -0
  44. package/src/model/model-context-profile.ts +134 -0
  45. package/src/model/model-request-preflight.ts +120 -0
  46. package/src/model/openai-chat-mapping.ts +444 -0
  47. package/src/model/openai-chat-model-client.ts +190 -0
  48. package/src/model/prompt-prefix-hash.ts +47 -0
  49. package/src/model/token-estimator.ts +148 -0
  50. package/src/observation/observation-builder.ts +481 -0
  51. package/src/session/resume-projection.ts +616 -0
  52. package/src/session/session-catalog.ts +270 -0
  53. package/src/session/session-errors.ts +121 -0
  54. package/src/session/session-history-reader.ts +535 -0
  55. package/src/session/session-lock.ts +291 -0
  56. package/src/session/session-schema.ts +741 -0
  57. package/src/session/session-store.ts +3067 -0
  58. package/src/session/sqlite-session-ledger.ts +153 -0
  59. package/src/tools/bash-task.ts +617 -0
  60. package/src/tools/bash.ts +450 -0
  61. package/src/tools/cwd-state.ts +22 -0
  62. package/src/tools/edit.ts +428 -0
  63. package/src/tools/file-diff.ts +116 -0
  64. package/src/tools/glob.ts +202 -0
  65. package/src/tools/grep.ts +550 -0
  66. package/src/tools/hash.ts +9 -0
  67. package/src/tools/path-safety.ts +33 -0
  68. package/src/tools/read.ts +319 -0
  69. package/src/tools/recall.ts +400 -0
  70. package/src/tools/registry.ts +213 -0
  71. package/src/tools/ripgrep.ts +220 -0
  72. package/src/tools/task-list.ts +59 -0
  73. package/src/tools/task-output-snapshot.ts +47 -0
  74. package/src/tools/task-output-tool.ts +62 -0
  75. package/src/tools/task-output.ts +159 -0
  76. package/src/tools/task-stop.ts +59 -0
  77. package/src/tools/task-tool-args.ts +29 -0
  78. package/src/tools/types.ts +330 -0
  79. package/src/tools/web-fetch/backend.ts +27 -0
  80. package/src/tools/web-fetch/browser-backend.ts +126 -0
  81. package/src/tools/web-fetch/exa-backend.ts +172 -0
  82. package/src/tools/web-fetch/index.ts +298 -0
  83. package/src/tools/web-fetch/local-backend.ts +267 -0
  84. package/src/tools/web-fetch/refiner.ts +78 -0
  85. package/src/tools/web-fetch/route.ts +95 -0
  86. package/src/tools/web-search.ts +300 -0
  87. package/src/tools/write.ts +244 -0
  88. package/src/tui/app.tsx +497 -0
  89. package/src/tui/components/assistant-markdown.tsx +47 -0
  90. package/src/tui/components/background-tasks.tsx +92 -0
  91. package/src/tui/components/bash-result-view.tsx +47 -0
  92. package/src/tui/components/context-status.tsx +127 -0
  93. package/src/tui/components/diff-view.tsx +151 -0
  94. package/src/tui/components/file-viewer.tsx +212 -0
  95. package/src/tui/components/footer.tsx +60 -0
  96. package/src/tui/components/header.tsx +21 -0
  97. package/src/tui/components/model-picker.tsx +142 -0
  98. package/src/tui/components/prompt-input.tsx +432 -0
  99. package/src/tui/components/resume-session-picker.tsx +273 -0
  100. package/src/tui/components/timeline.tsx +121 -0
  101. package/src/tui/context-format.ts +24 -0
  102. package/src/tui/event-store.ts +865 -0
  103. package/src/tui/git-branch.ts +23 -0
  104. package/src/tui/line-editor.ts +157 -0
  105. package/src/tui/prompt-history.ts +94 -0
  106. package/src/tui/slash-commands.ts +126 -0
  107. package/src/tui/tui-projection-policy.ts +35 -0
  108. package/src/tui/tui-projection-store.ts +123 -0
  109. package/src/tui/tui-session-controller.ts +170 -0
  110. 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
+ }