@sigil0/looking-glass 0.1.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.
Files changed (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +572 -0
  3. package/dist/agents/coordinator.d.ts +21 -0
  4. package/dist/agents/coordinator.d.ts.map +1 -0
  5. package/dist/agents/coordinator.js +171 -0
  6. package/dist/agents/coordinator.js.map +1 -0
  7. package/dist/app.d.ts +44 -0
  8. package/dist/app.d.ts.map +1 -0
  9. package/dist/app.js +215 -0
  10. package/dist/app.js.map +1 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +419 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/config.d.ts +4 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/config.js +132 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/engine/context.d.ts +6 -0
  20. package/dist/engine/context.d.ts.map +1 -0
  21. package/dist/engine/context.js +65 -0
  22. package/dist/engine/context.js.map +1 -0
  23. package/dist/engine/engine.d.ts +74 -0
  24. package/dist/engine/engine.d.ts.map +1 -0
  25. package/dist/engine/engine.js +573 -0
  26. package/dist/engine/engine.js.map +1 -0
  27. package/dist/engine/types.d.ts +35 -0
  28. package/dist/engine/types.d.ts.map +1 -0
  29. package/dist/engine/types.js +2 -0
  30. package/dist/engine/types.js.map +1 -0
  31. package/dist/instructions.d.ts +8 -0
  32. package/dist/instructions.d.ts.map +1 -0
  33. package/dist/instructions.js +60 -0
  34. package/dist/instructions.js.map +1 -0
  35. package/dist/model/codex-lb.d.ts +141 -0
  36. package/dist/model/codex-lb.d.ts.map +1 -0
  37. package/dist/model/codex-lb.js +471 -0
  38. package/dist/model/codex-lb.js.map +1 -0
  39. package/dist/paths.d.ts +7 -0
  40. package/dist/paths.d.ts.map +1 -0
  41. package/dist/paths.js +41 -0
  42. package/dist/paths.js.map +1 -0
  43. package/dist/scheduler/daemon.d.ts +40 -0
  44. package/dist/scheduler/daemon.d.ts.map +1 -0
  45. package/dist/scheduler/daemon.js +189 -0
  46. package/dist/scheduler/daemon.js.map +1 -0
  47. package/dist/scheduler/index.d.ts +8 -0
  48. package/dist/scheduler/index.d.ts.map +1 -0
  49. package/dist/scheduler/index.js +6 -0
  50. package/dist/scheduler/index.js.map +1 -0
  51. package/dist/scheduler/runner.d.ts +18 -0
  52. package/dist/scheduler/runner.d.ts.map +1 -0
  53. package/dist/scheduler/runner.js +173 -0
  54. package/dist/scheduler/runner.js.map +1 -0
  55. package/dist/scheduler/schedule.d.ts +7 -0
  56. package/dist/scheduler/schedule.d.ts.map +1 -0
  57. package/dist/scheduler/schedule.js +94 -0
  58. package/dist/scheduler/schedule.js.map +1 -0
  59. package/dist/scheduler/store.d.ts +55 -0
  60. package/dist/scheduler/store.d.ts.map +1 -0
  61. package/dist/scheduler/store.js +1203 -0
  62. package/dist/scheduler/store.js.map +1 -0
  63. package/dist/scheduler/systemd.d.ts +7 -0
  64. package/dist/scheduler/systemd.d.ts.map +1 -0
  65. package/dist/scheduler/systemd.js +97 -0
  66. package/dist/scheduler/systemd.js.map +1 -0
  67. package/dist/scheduler/types.d.ts +145 -0
  68. package/dist/scheduler/types.d.ts.map +1 -0
  69. package/dist/scheduler/types.js +7 -0
  70. package/dist/scheduler/types.js.map +1 -0
  71. package/dist/security.d.ts +6 -0
  72. package/dist/security.d.ts.map +1 -0
  73. package/dist/security.js +27 -0
  74. package/dist/security.js.map +1 -0
  75. package/dist/storage/artifact-store.d.ts +19 -0
  76. package/dist/storage/artifact-store.d.ts.map +1 -0
  77. package/dist/storage/artifact-store.js +47 -0
  78. package/dist/storage/artifact-store.js.map +1 -0
  79. package/dist/storage/database.d.ts +5 -0
  80. package/dist/storage/database.d.ts.map +1 -0
  81. package/dist/storage/database.js +395 -0
  82. package/dist/storage/database.js.map +1 -0
  83. package/dist/storage/session-store.d.ts +114 -0
  84. package/dist/storage/session-store.d.ts.map +1 -0
  85. package/dist/storage/session-store.js +731 -0
  86. package/dist/storage/session-store.js.map +1 -0
  87. package/dist/tools/agents.d.ts +14 -0
  88. package/dist/tools/agents.d.ts.map +1 -0
  89. package/dist/tools/agents.js +35 -0
  90. package/dist/tools/agents.js.map +1 -0
  91. package/dist/tools/apply-patch.d.ts +7 -0
  92. package/dist/tools/apply-patch.d.ts.map +1 -0
  93. package/dist/tools/apply-patch.js +429 -0
  94. package/dist/tools/apply-patch.js.map +1 -0
  95. package/dist/tools/ask.d.ts +8 -0
  96. package/dist/tools/ask.d.ts.map +1 -0
  97. package/dist/tools/ask.js +27 -0
  98. package/dist/tools/ask.js.map +1 -0
  99. package/dist/tools/bash.d.ts +9 -0
  100. package/dist/tools/bash.d.ts.map +1 -0
  101. package/dist/tools/bash.js +98 -0
  102. package/dist/tools/bash.js.map +1 -0
  103. package/dist/tools/index.d.ts +9 -0
  104. package/dist/tools/index.d.ts.map +1 -0
  105. package/dist/tools/index.js +34 -0
  106. package/dist/tools/index.js.map +1 -0
  107. package/dist/tools/paths.d.ts +3 -0
  108. package/dist/tools/paths.d.ts.map +1 -0
  109. package/dist/tools/paths.js +34 -0
  110. package/dist/tools/paths.js.map +1 -0
  111. package/dist/tools/process.d.ts +21 -0
  112. package/dist/tools/process.d.ts.map +1 -0
  113. package/dist/tools/process.js +104 -0
  114. package/dist/tools/process.js.map +1 -0
  115. package/dist/tools/read.d.ts +9 -0
  116. package/dist/tools/read.d.ts.map +1 -0
  117. package/dist/tools/read.js +95 -0
  118. package/dist/tools/read.js.map +1 -0
  119. package/dist/tools/registry.d.ts +19 -0
  120. package/dist/tools/registry.d.ts.map +1 -0
  121. package/dist/tools/registry.js +188 -0
  122. package/dist/tools/registry.js.map +1 -0
  123. package/dist/tools/safety.d.ts +9 -0
  124. package/dist/tools/safety.d.ts.map +1 -0
  125. package/dist/tools/safety.js +199 -0
  126. package/dist/tools/safety.js.map +1 -0
  127. package/dist/tools/schedule.d.ts +4 -0
  128. package/dist/tools/schedule.d.ts.map +1 -0
  129. package/dist/tools/schedule.js +191 -0
  130. package/dist/tools/schedule.js.map +1 -0
  131. package/dist/tools/search.d.ts +16 -0
  132. package/dist/tools/search.d.ts.map +1 -0
  133. package/dist/tools/search.js +75 -0
  134. package/dist/tools/search.js.map +1 -0
  135. package/dist/tools/types.d.ts +54 -0
  136. package/dist/tools/types.d.ts.map +1 -0
  137. package/dist/tools/types.js +10 -0
  138. package/dist/tools/types.js.map +1 -0
  139. package/dist/types.d.ts +90 -0
  140. package/dist/types.d.ts.map +1 -0
  141. package/dist/types.js +2 -0
  142. package/dist/types.js.map +1 -0
  143. package/dist/ui/stdio.d.ts +5 -0
  144. package/dist/ui/stdio.d.ts.map +1 -0
  145. package/dist/ui/stdio.js +65 -0
  146. package/dist/ui/stdio.js.map +1 -0
  147. package/dist/ui/tui.d.ts +93 -0
  148. package/dist/ui/tui.d.ts.map +1 -0
  149. package/dist/ui/tui.js +2246 -0
  150. package/dist/ui/tui.js.map +1 -0
  151. package/package.json +53 -0
@@ -0,0 +1,731 @@
1
+ import { randomUUID } from "node:crypto";
2
+ function sessionFromRow(row) {
3
+ return {
4
+ id: row.id,
5
+ workspace: row.workspace,
6
+ provider: row.provider,
7
+ agentProvider: row.agent_provider ?? row.provider,
8
+ title: row.title,
9
+ model: row.model,
10
+ agentModel: row.agent_model ?? row.model,
11
+ reasoningEffort: row.reasoning_effort,
12
+ agentReasoningEffort: row.agent_reasoning_effort ?? row.reasoning_effort,
13
+ agentsEnabled: row.agents_enabled === 1,
14
+ verbosity: row.verbosity,
15
+ fast: row.fast === 1,
16
+ approvalMode: row.approval_mode,
17
+ showReasoning: row.show_reasoning === 1,
18
+ persistent: row.persistent === 1,
19
+ promptCacheKey: row.prompt_cache_key,
20
+ lastResponseId: row.last_response_id,
21
+ kind: row.session_kind,
22
+ parentSessionId: row.parent_session_id,
23
+ createdAt: row.created_at,
24
+ updatedAt: row.updated_at,
25
+ };
26
+ }
27
+ function eventFromRow(row) {
28
+ return {
29
+ id: row.id,
30
+ sessionId: row.session_id,
31
+ sequence: row.sequence,
32
+ kind: row.kind,
33
+ payload: JSON.parse(row.payload_json),
34
+ createdAt: row.created_at,
35
+ };
36
+ }
37
+ function toolCallFromRow(row) {
38
+ return {
39
+ sessionId: row.session_id,
40
+ callId: row.call_id,
41
+ name: row.name,
42
+ arguments: JSON.parse(row.arguments_json),
43
+ state: row.state,
44
+ output: row.output_text,
45
+ error: row.error_text,
46
+ startedAt: row.started_at,
47
+ finishedAt: row.finished_at,
48
+ executionToken: row.execution_token,
49
+ };
50
+ }
51
+ export class SessionStore {
52
+ db;
53
+ constructor(db) {
54
+ this.db = db;
55
+ }
56
+ create(input) {
57
+ const now = Date.now();
58
+ const session = {
59
+ id: randomUUID(),
60
+ workspace: input.workspace,
61
+ provider: input.provider ?? "codex-lb",
62
+ agentProvider: input.agentProvider ?? input.provider ?? "codex-lb",
63
+ title: "New session",
64
+ model: input.model,
65
+ agentModel: input.agentModel ?? input.model,
66
+ reasoningEffort: input.reasoningEffort,
67
+ agentReasoningEffort: input.agentReasoningEffort ?? input.reasoningEffort,
68
+ agentsEnabled: input.agentsEnabled ?? true,
69
+ verbosity: input.verbosity,
70
+ fast: input.fast,
71
+ approvalMode: input.approvalMode ?? "code",
72
+ showReasoning: false,
73
+ persistent: false,
74
+ promptCacheKey: randomUUID(),
75
+ lastResponseId: null,
76
+ kind: input.kind ?? "interactive",
77
+ parentSessionId: input.parentSessionId ?? null,
78
+ createdAt: now,
79
+ updatedAt: now,
80
+ };
81
+ this.db.prepare(`
82
+ INSERT INTO sessions(
83
+ id, workspace, provider, agent_provider, title, model, agent_model,
84
+ reasoning_effort, agent_reasoning_effort, agents_enabled, verbosity, fast, approval_mode,
85
+ prompt_cache_key, last_response_id, session_kind, parent_session_id, created_at, updated_at
86
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?)
87
+ `).run(session.id, session.workspace, session.provider, session.agentProvider, session.title, session.model, session.agentModel, session.reasoningEffort, session.agentReasoningEffort, session.agentsEnabled ? 1 : 0, session.verbosity, session.fast ? 1 : 0, session.approvalMode, session.promptCacheKey, session.kind, session.parentSessionId, now, now);
88
+ return session;
89
+ }
90
+ get(id) {
91
+ const row = this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
92
+ return row ? sessionFromRow(row) : null;
93
+ }
94
+ list(workspace, limit = 50) {
95
+ const rows = workspace
96
+ ? this.db.prepare("SELECT * FROM sessions WHERE workspace = ? AND session_kind = 'interactive' ORDER BY updated_at DESC LIMIT ?").all(workspace, limit)
97
+ : this.db.prepare("SELECT * FROM sessions WHERE session_kind = 'interactive' ORDER BY updated_at DESC LIMIT ?").all(limit);
98
+ return rows.map(sessionFromRow);
99
+ }
100
+ listWithMessages(workspace, limit = 50) {
101
+ const rows = workspace
102
+ ? this.db.prepare(`
103
+ SELECT s.* FROM sessions s
104
+ WHERE s.workspace = ? AND s.session_kind = 'interactive'
105
+ AND EXISTS (
106
+ SELECT 1 FROM session_events e
107
+ WHERE e.session_id = s.id AND e.kind IN ('user', 'response')
108
+ )
109
+ ORDER BY s.updated_at DESC LIMIT ?
110
+ `).all(workspace, limit)
111
+ : this.db.prepare(`
112
+ SELECT s.* FROM sessions s
113
+ WHERE s.session_kind = 'interactive'
114
+ AND EXISTS (
115
+ SELECT 1 FROM session_events e
116
+ WHERE e.session_id = s.id AND e.kind IN ('user', 'response')
117
+ )
118
+ ORDER BY s.updated_at DESC LIMIT ?
119
+ `).all(limit);
120
+ return rows.map(sessionFromRow);
121
+ }
122
+ deleteIfEmpty(id) {
123
+ return this.db.prepare(`
124
+ DELETE FROM sessions
125
+ WHERE id = ? AND session_kind = 'interactive'
126
+ AND NOT EXISTS (SELECT 1 FROM session_events WHERE session_id = sessions.id)
127
+ AND NOT EXISTS (SELECT 1 FROM sessions child WHERE child.parent_session_id = sessions.id)
128
+ AND NOT EXISTS (SELECT 1 FROM scheduler_jobs WHERE session_id = sessions.id)
129
+ AND NOT EXISTS (SELECT 1 FROM session_operation_leases WHERE session_id = sessions.id)
130
+ `).run(id).changes === 1;
131
+ }
132
+ updateSettings(id, changes) {
133
+ const update = this.db.transaction(() => {
134
+ const now = Date.now();
135
+ const lease = this.db.prepare("SELECT 1 FROM session_operation_leases WHERE session_id = ? AND expires_at > ?").get(id, now);
136
+ if (lease)
137
+ throw new Error("Session is busy with another operation");
138
+ const current = this.require(id);
139
+ const resetContinuity = changes.model !== undefined || changes.provider !== undefined;
140
+ const next = {
141
+ ...current,
142
+ ...changes,
143
+ promptCacheKey: resetContinuity ? randomUUID() : current.promptCacheKey,
144
+ lastResponseId: resetContinuity ? null : current.lastResponseId,
145
+ };
146
+ this.db.prepare(`
147
+ UPDATE sessions
148
+ SET provider = ?, model = ?, reasoning_effort = ?,
149
+ agent_provider = ?, agent_model = ?, agent_reasoning_effort = ?, agents_enabled = ?, verbosity = ?, fast = ?,
150
+ approval_mode = ?, show_reasoning = ?, persistent = ?,
151
+ prompt_cache_key = ?, last_response_id = ?, updated_at = ?
152
+ WHERE id = ?
153
+ `).run(next.provider, next.model, next.reasoningEffort, next.agentProvider, next.agentModel, next.agentReasoningEffort, next.agentsEnabled ? 1 : 0, next.verbosity, next.fast ? 1 : 0, next.approvalMode, next.showReasoning ? 1 : 0, next.persistent ? 1 : 0, next.promptCacheKey, next.lastResponseId, now, id);
154
+ if (changes.provider !== undefined && changes.provider !== current.provider) {
155
+ this.db.prepare("DELETE FROM context_checkpoints WHERE session_id = ?").run(id);
156
+ }
157
+ return this.require(id);
158
+ });
159
+ return update.immediate();
160
+ }
161
+ setLastResponseId(id, responseId) {
162
+ this.db.prepare("UPDATE sessions SET last_response_id = ?, updated_at = ? WHERE id = ?")
163
+ .run(responseId, Date.now(), id);
164
+ }
165
+ resetContinuity(id) {
166
+ this.db.prepare(`
167
+ UPDATE sessions
168
+ SET prompt_cache_key = ?, last_response_id = NULL, updated_at = ?
169
+ WHERE id = ?
170
+ `).run(randomUUID(), Date.now(), id);
171
+ return this.require(id);
172
+ }
173
+ setTitleFromPrompt(id, prompt) {
174
+ const current = this.require(id);
175
+ if (current.title !== "New session")
176
+ return;
177
+ const title = prompt.replace(/\s+/g, " ").trim().slice(0, 72) || "New session";
178
+ this.db.prepare("UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?").run(title, Date.now(), id);
179
+ }
180
+ rename(id, title) {
181
+ const normalized = title.replace(/\s+/g, " ").trim().slice(0, 120);
182
+ if (!normalized)
183
+ throw new Error("Session title must not be empty");
184
+ this.db.prepare("UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?")
185
+ .run(normalized, Date.now(), id);
186
+ return this.require(id);
187
+ }
188
+ hasCommandApproval(sessionId, signature) {
189
+ if (!signature)
190
+ return false;
191
+ return Boolean(this.db.prepare(`
192
+ SELECT 1 FROM session_command_approvals WHERE session_id = ? AND signature = ?
193
+ `).get(sessionId, signature));
194
+ }
195
+ registerCommandApproval(sessionId, signature, now = Date.now()) {
196
+ if (!signature)
197
+ throw new Error("Command approval signature must not be empty");
198
+ this.require(sessionId);
199
+ const result = this.db.prepare(`
200
+ INSERT OR IGNORE INTO session_command_approvals(session_id, signature, approved_at)
201
+ VALUES (?, ?, ?)
202
+ `).run(sessionId, signature, now);
203
+ return result.changes === 1;
204
+ }
205
+ listCommandApprovals(sessionId) {
206
+ this.require(sessionId);
207
+ const rows = this.db.prepare(`
208
+ SELECT signature, approved_at FROM session_command_approvals
209
+ WHERE session_id = ? ORDER BY approved_at DESC
210
+ `).all(sessionId);
211
+ return rows.map((row) => ({ signature: row.signature, approvedAt: row.approved_at }));
212
+ }
213
+ revokeCommandApproval(sessionId, signature) {
214
+ return this.db.prepare(`
215
+ DELETE FROM session_command_approvals WHERE session_id = ? AND signature = ?
216
+ `).run(sessionId, signature).changes === 1;
217
+ }
218
+ deletionSummary(id) {
219
+ this.require(id);
220
+ return this.readDeletionSummary(id);
221
+ }
222
+ delete(id, expected) {
223
+ const remove = this.db.transaction(() => {
224
+ this.require(id);
225
+ const now = Date.now();
226
+ const activeLease = this.db.prepare(`
227
+ WITH RECURSIVE targets(id) AS (
228
+ SELECT ? UNION ALL
229
+ SELECT s.id FROM sessions s JOIN targets t ON s.parent_session_id = t.id
230
+ )
231
+ SELECT kind FROM session_operation_leases
232
+ WHERE session_id IN (SELECT id FROM targets) AND expires_at > ?
233
+ `).get(id, now);
234
+ if (activeLease)
235
+ throw new Error(`Session is busy with an active ${activeLease.kind}`);
236
+ const activeSchedule = this.db.prepare(`
237
+ WITH RECURSIVE targets(id) AS (
238
+ SELECT ? UNION ALL
239
+ SELECT s.id FROM sessions s JOIN targets t ON s.parent_session_id = t.id
240
+ )
241
+ SELECT o.state FROM scheduler_occurrences o
242
+ JOIN scheduler_jobs j ON j.id = o.job_id
243
+ WHERE j.session_id IN (SELECT id FROM targets) AND o.state IN ('claimed', 'running')
244
+ LIMIT 1
245
+ `).get(id);
246
+ if (activeSchedule)
247
+ throw new Error(`Session has ${activeSchedule.state} scheduled work`);
248
+ const summary = this.readDeletionSummary(id);
249
+ if (expected && JSON.stringify(summary) !== JSON.stringify(expected)) {
250
+ throw new Error("Session changed after deletion was confirmed; review it again");
251
+ }
252
+ this.db.prepare(`
253
+ WITH RECURSIVE targets(id) AS (
254
+ SELECT ? UNION ALL
255
+ SELECT s.id FROM sessions s JOIN targets t ON s.parent_session_id = t.id
256
+ )
257
+ DELETE FROM scheduler_inbox
258
+ WHERE job_id IN (SELECT id FROM scheduler_jobs WHERE session_id IN (SELECT id FROM targets))
259
+ `).run(id);
260
+ this.db.prepare(`
261
+ WITH RECURSIVE targets(id) AS (
262
+ SELECT ? UNION ALL
263
+ SELECT s.id FROM sessions s JOIN targets t ON s.parent_session_id = t.id
264
+ )
265
+ DELETE FROM scheduler_occurrences
266
+ WHERE job_id IN (SELECT id FROM scheduler_jobs WHERE session_id IN (SELECT id FROM targets))
267
+ `).run(id);
268
+ this.db.prepare(`
269
+ WITH RECURSIVE targets(id) AS (
270
+ SELECT ? UNION ALL
271
+ SELECT s.id FROM sessions s JOIN targets t ON s.parent_session_id = t.id
272
+ )
273
+ DELETE FROM scheduler_jobs WHERE session_id IN (SELECT id FROM targets)
274
+ `).run(id);
275
+ this.db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
276
+ return summary;
277
+ });
278
+ return remove.immediate();
279
+ }
280
+ appendEvent(sessionId, kind, payload) {
281
+ const append = this.db.transaction(() => {
282
+ return this.appendEventRow(sessionId, kind, payload, Date.now());
283
+ });
284
+ return append.immediate();
285
+ }
286
+ appendResponseAndSetContinuity(sessionId, payload, responseId) {
287
+ const append = this.db.transaction(() => {
288
+ const now = Date.now();
289
+ const event = this.appendEventRow(sessionId, "response", payload, now);
290
+ this.db.prepare("UPDATE sessions SET last_response_id = ?, updated_at = ? WHERE id = ?")
291
+ .run(responseId, now, sessionId);
292
+ return event;
293
+ });
294
+ return append.immediate();
295
+ }
296
+ appendResponseAndSetContinuityFenced(sessionId, executionToken, payload, responseId) {
297
+ const append = this.db.transaction(() => {
298
+ const now = Date.now();
299
+ if (!this.hasOperationLeaseRow(sessionId, executionToken, now))
300
+ return null;
301
+ const event = this.appendEventRow(sessionId, "response", payload, now);
302
+ this.db.prepare("UPDATE sessions SET last_response_id = ?, updated_at = ? WHERE id = ?")
303
+ .run(responseId, now, sessionId);
304
+ return event;
305
+ });
306
+ return append.immediate();
307
+ }
308
+ appendUserAndSetTitle(sessionId, prompt, payload) {
309
+ const append = this.db.transaction(() => {
310
+ const now = Date.now();
311
+ const current = this.require(sessionId);
312
+ if (current.title === "New session") {
313
+ const title = prompt.replace(/\s+/g, " ").trim().slice(0, 72) || "New session";
314
+ this.db.prepare("UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?")
315
+ .run(title, now, sessionId);
316
+ }
317
+ return this.appendEventRow(sessionId, "user", payload, now);
318
+ });
319
+ return append.immediate();
320
+ }
321
+ appendUserAndSetTitleFenced(sessionId, executionToken, prompt, payload) {
322
+ const append = this.db.transaction(() => {
323
+ const now = Date.now();
324
+ if (!this.hasOperationLeaseRow(sessionId, executionToken, now))
325
+ return null;
326
+ const current = this.require(sessionId);
327
+ if (current.title === "New session") {
328
+ const title = prompt.replace(/\s+/g, " ").trim().slice(0, 72) || "New session";
329
+ this.db.prepare("UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?")
330
+ .run(title, now, sessionId);
331
+ }
332
+ return this.appendEventRow(sessionId, "user", payload, now);
333
+ });
334
+ return append.immediate();
335
+ }
336
+ appendEventFenced(sessionId, executionToken, kind, payload) {
337
+ const append = this.db.transaction(() => {
338
+ const now = Date.now();
339
+ if (!this.hasOperationLeaseRow(sessionId, executionToken, now))
340
+ return null;
341
+ return this.appendEventRow(sessionId, kind, payload, now);
342
+ });
343
+ return append.immediate();
344
+ }
345
+ events(sessionId, afterSequence = 0) {
346
+ const rows = this.db.prepare(`
347
+ SELECT * FROM session_events
348
+ WHERE session_id = ? AND sequence > ?
349
+ ORDER BY sequence ASC
350
+ `).all(sessionId, afterSequence);
351
+ return rows.map(eventFromRow);
352
+ }
353
+ latestResponseUsage(sessionId) {
354
+ const row = this.db.prepare(`
355
+ SELECT sequence, json_extract(payload_json, '$.response.usage.input_tokens') AS input_tokens
356
+ FROM session_events
357
+ WHERE session_id = ? AND kind = 'response'
358
+ AND json_type(payload_json, '$.response.usage.input_tokens') IN ('integer', 'real')
359
+ ORDER BY sequence DESC LIMIT 1
360
+ `).get(sessionId);
361
+ return row ? { sequence: row.sequence, inputTokens: row.input_tokens } : null;
362
+ }
363
+ semanticEventCount(sessionId, afterSequence = 0) {
364
+ const row = this.db.prepare(`
365
+ SELECT COUNT(*) AS count FROM session_events
366
+ WHERE session_id = ? AND sequence > ?
367
+ AND kind IN ('user', 'response', 'tool_result', 'tool_denied')
368
+ `).get(sessionId, afterSequence);
369
+ return row.count;
370
+ }
371
+ latestSequence(sessionId) {
372
+ const row = this.db.prepare("SELECT COALESCE(MAX(sequence), 0) AS sequence FROM session_events WHERE session_id = ?").get(sessionId);
373
+ return row.sequence;
374
+ }
375
+ saveCheckpoint(sessionId, throughSequence, compact, inputTokens) {
376
+ const createdAt = Date.now();
377
+ const result = this.db.prepare(`
378
+ INSERT INTO context_checkpoints(
379
+ session_id, through_sequence, compact_json, input_tokens, created_at
380
+ ) VALUES (?, ?, ?, ?, ?)
381
+ ON CONFLICT(session_id, through_sequence) DO UPDATE SET
382
+ compact_json = excluded.compact_json,
383
+ input_tokens = excluded.input_tokens,
384
+ created_at = excluded.created_at
385
+ RETURNING id
386
+ `).get(sessionId, throughSequence, JSON.stringify(compact), inputTokens, createdAt);
387
+ return { id: result.id, sessionId, throughSequence, compact, inputTokens, createdAt };
388
+ }
389
+ saveCheckpointAndResetContinuity(sessionId, throughSequence, compact, inputTokens) {
390
+ const save = this.db.transaction(() => {
391
+ const createdAt = Date.now();
392
+ const result = this.db.prepare(`
393
+ INSERT INTO context_checkpoints(
394
+ session_id, through_sequence, compact_json, input_tokens, created_at
395
+ ) VALUES (?, ?, ?, ?, ?)
396
+ ON CONFLICT(session_id, through_sequence) DO UPDATE SET
397
+ compact_json = excluded.compact_json,
398
+ input_tokens = excluded.input_tokens,
399
+ created_at = excluded.created_at
400
+ RETURNING id
401
+ `).get(sessionId, throughSequence, JSON.stringify(compact), inputTokens, createdAt);
402
+ this.db.prepare("UPDATE sessions SET last_response_id = NULL, updated_at = ? WHERE id = ?")
403
+ .run(createdAt, sessionId);
404
+ return { id: result.id, sessionId, throughSequence, compact, inputTokens, createdAt };
405
+ });
406
+ return save.immediate();
407
+ }
408
+ saveCheckpointAndResetContinuityFenced(sessionId, executionToken, throughSequence, compact, inputTokens) {
409
+ const save = this.db.transaction(() => {
410
+ const createdAt = Date.now();
411
+ if (!this.hasOperationLeaseRow(sessionId, executionToken, createdAt))
412
+ return null;
413
+ const result = this.db.prepare(`
414
+ INSERT INTO context_checkpoints(
415
+ session_id, through_sequence, compact_json, input_tokens, created_at
416
+ ) VALUES (?, ?, ?, ?, ?)
417
+ ON CONFLICT(session_id, through_sequence) DO UPDATE SET
418
+ compact_json = excluded.compact_json,
419
+ input_tokens = excluded.input_tokens,
420
+ created_at = excluded.created_at
421
+ RETURNING id
422
+ `).get(sessionId, throughSequence, JSON.stringify(compact), inputTokens, createdAt);
423
+ this.db.prepare("UPDATE sessions SET last_response_id = NULL, updated_at = ? WHERE id = ?")
424
+ .run(createdAt, sessionId);
425
+ return { id: result.id, sessionId, throughSequence, compact, inputTokens, createdAt };
426
+ });
427
+ return save.immediate();
428
+ }
429
+ latestCheckpoint(sessionId) {
430
+ const row = this.db.prepare(`
431
+ SELECT * FROM context_checkpoints
432
+ WHERE session_id = ?
433
+ ORDER BY through_sequence DESC
434
+ LIMIT 1
435
+ `).get(sessionId);
436
+ if (!row)
437
+ return null;
438
+ return {
439
+ id: row.id,
440
+ sessionId: row.session_id,
441
+ throughSequence: row.through_sequence,
442
+ compact: JSON.parse(row.compact_json),
443
+ inputTokens: row.input_tokens,
444
+ createdAt: row.created_at,
445
+ };
446
+ }
447
+ beginToolCall(sessionId, callId, name, args, executionToken) {
448
+ const begin = this.db.transaction(() => {
449
+ const now = Date.now();
450
+ if (!this.hasOperationLeaseRow(sessionId, executionToken, now)) {
451
+ throw new Error("Session operation lease was lost before tool claim");
452
+ }
453
+ const result = this.db.prepare(`
454
+ INSERT OR IGNORE INTO tool_calls(
455
+ session_id, call_id, name, arguments_json, state, started_at, execution_token
456
+ ) VALUES (?, ?, ?, ?, 'started', ?, ?)
457
+ `).run(sessionId, callId, name, JSON.stringify(args), now, executionToken);
458
+ if (result.changes === 1) {
459
+ this.appendEventRow(sessionId, "tool_started", { callId, name, arguments: args }, now);
460
+ }
461
+ return { record: this.requireToolCall(sessionId, callId), acquired: result.changes === 1 };
462
+ });
463
+ return begin.immediate();
464
+ }
465
+ finishToolCall(sessionId, callId, state, output, error) {
466
+ this.db.prepare(`
467
+ UPDATE tool_calls
468
+ SET state = ?, output_text = ?, error_text = ?, finished_at = ?
469
+ WHERE session_id = ? AND call_id = ?
470
+ `).run(state, output, error, Date.now(), sessionId, callId);
471
+ return this.requireToolCall(sessionId, callId);
472
+ }
473
+ finishToolCallWithEvent(sessionId, callId, executionToken, state, output, error, kind, payload) {
474
+ const finish = this.db.transaction(() => {
475
+ const now = Date.now();
476
+ if (!this.hasOperationLeaseRow(sessionId, executionToken, now)) {
477
+ throw new Error("Session operation lease was lost before tool completion");
478
+ }
479
+ const result = this.db.prepare(`
480
+ UPDATE tool_calls
481
+ SET state = ?, output_text = ?, error_text = ?, finished_at = ?
482
+ WHERE session_id = ? AND call_id = ? AND state = 'started' AND execution_token = ?
483
+ `).run(state, output, error, now, sessionId, callId, executionToken);
484
+ if (result.changes !== 1)
485
+ throw new Error(`Tool call ownership was lost: ${callId}`);
486
+ this.appendEventRow(sessionId, kind, payload, now);
487
+ return this.requireToolCall(sessionId, callId);
488
+ });
489
+ return finish.immediate();
490
+ }
491
+ markToolCallUnknownWithEvent(sessionId, callId, executionToken, payload) {
492
+ const mark = this.db.transaction(() => {
493
+ const now = Date.now();
494
+ if (!this.hasOperationLeaseRow(sessionId, executionToken, now)) {
495
+ throw new Error("Session operation lease was lost before recording an uncertain tool");
496
+ }
497
+ const message = "Tool execution was interrupted; its side effects may have occurred.";
498
+ const result = this.db.prepare(`
499
+ UPDATE tool_calls
500
+ SET state = 'unknown', output_text = ?, error_text = ?, finished_at = ?
501
+ WHERE session_id = ? AND call_id = ? AND state = 'started' AND execution_token = ?
502
+ `).run(message, message, now, sessionId, callId, executionToken);
503
+ if (result.changes !== 1)
504
+ throw new Error(`Tool call ownership was lost: ${callId}`);
505
+ this.appendEventRow(sessionId, "tool_denied", payload, now);
506
+ return this.requireToolCall(sessionId, callId);
507
+ });
508
+ return mark.immediate();
509
+ }
510
+ resetUnknownToolCall(sessionId, callId, executionToken) {
511
+ const reset = this.db.transaction(() => {
512
+ const now = Date.now();
513
+ if (!this.hasOperationLeaseRow(sessionId, executionToken, now)) {
514
+ throw new Error("Session operation lease was lost before retrying an uncertain tool");
515
+ }
516
+ const result = this.db.prepare(`
517
+ UPDATE tool_calls
518
+ SET state = 'started', output_text = NULL, error_text = NULL,
519
+ started_at = ?, finished_at = NULL, execution_token = ?
520
+ WHERE session_id = ? AND call_id = ? AND state = 'unknown'
521
+ `).run(now, executionToken, sessionId, callId);
522
+ return { record: this.requireToolCall(sessionId, callId), acquired: result.changes === 1 };
523
+ });
524
+ return reset.immediate();
525
+ }
526
+ markInterruptedToolCallsUnknown() {
527
+ const result = this.db.prepare(`
528
+ UPDATE tool_calls
529
+ SET state = 'unknown', error_text = 'Looking Glass stopped before recording the tool result', finished_at = ?
530
+ WHERE state = 'started'
531
+ `).run(Date.now());
532
+ return result.changes;
533
+ }
534
+ acquireOperationLease(sessionId, owner, token, kind, now = Date.now(), leaseMs = 30_000) {
535
+ const acquire = this.db.transaction(() => {
536
+ const current = this.db.prepare("SELECT token, expires_at FROM session_operation_leases WHERE session_id = ?").get(sessionId);
537
+ if (current && current.expires_at > now)
538
+ return false;
539
+ this.db.prepare(`
540
+ UPDATE tool_calls
541
+ SET state = 'unknown', output_text = COALESCE(output_text, ?),
542
+ error_text = COALESCE(error_text, ?), finished_at = ?
543
+ WHERE session_id = ? AND state = 'started'
544
+ `).run("Previous Looking Glass process stopped before recording the tool result.", "Previous Looking Glass process stopped before recording the tool result.", now, sessionId);
545
+ this.db.prepare(`
546
+ INSERT INTO session_operation_leases(
547
+ session_id, owner, token, kind, acquired_at, renewed_at, expires_at
548
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
549
+ ON CONFLICT(session_id) DO UPDATE SET
550
+ owner = excluded.owner, token = excluded.token, kind = excluded.kind,
551
+ acquired_at = excluded.acquired_at, renewed_at = excluded.renewed_at,
552
+ expires_at = excluded.expires_at
553
+ `).run(sessionId, owner, token, kind, now, now, now + leaseMs);
554
+ return true;
555
+ });
556
+ return acquire.immediate();
557
+ }
558
+ renewOperationLease(sessionId, owner, token, now = Date.now(), leaseMs = 30_000) {
559
+ return this.db.prepare(`
560
+ UPDATE session_operation_leases SET renewed_at = ?, expires_at = ?
561
+ WHERE session_id = ? AND owner = ? AND token = ? AND expires_at > ?
562
+ `).run(now, now + leaseMs, sessionId, owner, token, now).changes === 1;
563
+ }
564
+ assertOperationLease(sessionId, owner, token, now = Date.now()) {
565
+ return Boolean(this.db.prepare(`
566
+ SELECT 1 FROM session_operation_leases
567
+ WHERE session_id = ? AND owner = ? AND token = ? AND expires_at > ?
568
+ `).get(sessionId, owner, token, now));
569
+ }
570
+ assertOperationToken(sessionId, token, now = Date.now()) {
571
+ return Boolean(this.db.prepare(`
572
+ SELECT 1 FROM session_operation_leases
573
+ WHERE session_id = ? AND token = ? AND expires_at > ?
574
+ `).get(sessionId, token, now));
575
+ }
576
+ releaseOperationLease(sessionId, owner, token) {
577
+ return this.db.prepare(`
578
+ DELETE FROM session_operation_leases WHERE session_id = ? AND owner = ? AND token = ?
579
+ `).run(sessionId, owner, token).changes === 1;
580
+ }
581
+ reconcileToolCallEvents(sessionId, executionToken) {
582
+ const reconcile = this.db.transaction(() => {
583
+ const now = Date.now();
584
+ if (!this.hasOperationLeaseRow(sessionId, executionToken, now)) {
585
+ throw new Error("Session operation lease was lost during tool reconciliation");
586
+ }
587
+ const responses = this.db.prepare(`
588
+ SELECT payload_json FROM session_events WHERE session_id = ? AND kind = 'response'
589
+ `).all(sessionId);
590
+ for (const row of responses) {
591
+ const payload = JSON.parse(row.payload_json);
592
+ for (const value of payload.response?.output ?? []) {
593
+ if (!value || typeof value !== "object" || Array.isArray(value))
594
+ continue;
595
+ const item = value;
596
+ if (item.type !== "function_call" || typeof item.call_id !== "string" || typeof item.name !== "string")
597
+ continue;
598
+ const rawArguments = typeof item.arguments === "string" ? item.arguments : "{}";
599
+ let args;
600
+ try {
601
+ args = JSON.parse(rawArguments);
602
+ }
603
+ catch {
604
+ args = { raw_arguments: rawArguments };
605
+ }
606
+ this.db.prepare(`
607
+ INSERT OR IGNORE INTO tool_calls(
608
+ session_id, call_id, name, arguments_json, state, output_text,
609
+ error_text, started_at, finished_at, execution_token
610
+ ) VALUES (?, ?, ?, ?, 'unknown', ?, ?, ?, ?, NULL)
611
+ `).run(sessionId, item.call_id, item.name, JSON.stringify(args), "No durable tool result was recorded.", "No durable tool result was recorded.", now, now);
612
+ }
613
+ }
614
+ const missing = this.db.prepare(`
615
+ SELECT tc.* FROM tool_calls tc
616
+ WHERE tc.session_id = ? AND tc.call_id NOT IN (
617
+ SELECT json_extract(e.payload_json, '$.callId')
618
+ FROM session_events e
619
+ WHERE e.session_id = ? AND e.kind IN ('tool_result', 'tool_denied')
620
+ AND json_extract(e.payload_json, '$.callId') IS NOT NULL
621
+ )
622
+ `).all(sessionId, sessionId);
623
+ for (const row of missing) {
624
+ const call = toolCallFromRow(row);
625
+ const output = call.output ?? call.error ?? "Tool result is unavailable.";
626
+ const payload = {
627
+ name: call.name,
628
+ callId: call.callId,
629
+ item: { type: "function_call_output", call_id: call.callId, output },
630
+ output,
631
+ };
632
+ this.appendEventRow(sessionId, call.state === "completed" ? "tool_result" : "tool_denied", payload, now);
633
+ }
634
+ return missing.length;
635
+ });
636
+ return reconcile.immediate();
637
+ }
638
+ hasUnanchoredContext(sessionId) {
639
+ const latestResponse = this.db.prepare(`
640
+ SELECT COALESCE(MAX(sequence), 0) AS sequence
641
+ FROM session_events WHERE session_id = ? AND kind = 'response'
642
+ `).get(sessionId);
643
+ return Boolean(this.db.prepare(`
644
+ SELECT 1 FROM session_events
645
+ WHERE session_id = ? AND sequence > ? AND kind IN ('user', 'tool_result', 'tool_denied')
646
+ LIMIT 1
647
+ `).get(sessionId, latestResponse.sequence));
648
+ }
649
+ resetContinuityFenced(sessionId, executionToken) {
650
+ const reset = this.db.transaction(() => {
651
+ const now = Date.now();
652
+ if (!this.hasOperationLeaseRow(sessionId, executionToken, now))
653
+ return null;
654
+ this.db.prepare(`
655
+ UPDATE sessions
656
+ SET prompt_cache_key = ?, last_response_id = NULL, updated_at = ?
657
+ WHERE id = ?
658
+ `).run(randomUUID(), now, sessionId);
659
+ return this.require(sessionId);
660
+ });
661
+ return reset.immediate();
662
+ }
663
+ getToolCall(sessionId, callId) {
664
+ const row = this.db.prepare("SELECT * FROM tool_calls WHERE session_id = ? AND call_id = ?").get(sessionId, callId);
665
+ return row ? toolCallFromRow(row) : null;
666
+ }
667
+ readDeletionSummary(id) {
668
+ const row = this.db.prepare(`
669
+ WITH RECURSIVE targets(id) AS (
670
+ SELECT ? UNION ALL
671
+ SELECT s.id FROM sessions s JOIN targets t ON s.parent_session_id = t.id
672
+ )
673
+ SELECT
674
+ (SELECT COUNT(*) FROM session_events WHERE session_id IN (SELECT id FROM targets)) AS events,
675
+ (SELECT COUNT(*) FROM context_checkpoints WHERE session_id IN (SELECT id FROM targets)) AS checkpoints,
676
+ (SELECT COUNT(*) FROM tool_calls WHERE session_id IN (SELECT id FROM targets)) AS tool_calls,
677
+ (SELECT COUNT(*) FROM scheduler_jobs WHERE session_id IN (SELECT id FROM targets)) AS schedules,
678
+ (SELECT COUNT(*) FROM scheduler_occurrences
679
+ WHERE job_id IN (SELECT id FROM scheduler_jobs WHERE session_id IN (SELECT id FROM targets))) AS occurrences,
680
+ (SELECT COUNT(*) FROM scheduler_inbox
681
+ WHERE job_id IN (SELECT id FROM scheduler_jobs WHERE session_id IN (SELECT id FROM targets))) AS inbox_items,
682
+ (SELECT COUNT(*) FROM session_command_approvals WHERE session_id IN (SELECT id FROM targets)) AS command_approvals,
683
+ (SELECT COUNT(*) FROM artifacts WHERE session_id IN (SELECT id FROM targets)) AS retained_artifacts
684
+ `).get(id);
685
+ return {
686
+ events: row.events,
687
+ checkpoints: row.checkpoints,
688
+ toolCalls: row.tool_calls,
689
+ schedules: row.schedules,
690
+ occurrences: row.occurrences,
691
+ inboxItems: row.inbox_items,
692
+ commandApprovals: row.command_approvals,
693
+ retainedArtifacts: row.retained_artifacts,
694
+ };
695
+ }
696
+ require(id) {
697
+ const session = this.get(id);
698
+ if (!session)
699
+ throw new Error(`Session not found: ${id}`);
700
+ return session;
701
+ }
702
+ requireToolCall(sessionId, callId) {
703
+ const call = this.getToolCall(sessionId, callId);
704
+ if (!call)
705
+ throw new Error(`Tool call not found: ${callId}`);
706
+ return call;
707
+ }
708
+ appendEventRow(sessionId, kind, payload, createdAt) {
709
+ const row = this.db.prepare("SELECT COALESCE(MAX(sequence), 0) + 1 AS sequence FROM session_events WHERE session_id = ?").get(sessionId);
710
+ const result = this.db.prepare(`
711
+ INSERT INTO session_events(session_id, sequence, kind, payload_json, created_at)
712
+ VALUES (?, ?, ?, ?, ?)
713
+ `).run(sessionId, row.sequence, kind, JSON.stringify(payload), createdAt);
714
+ this.db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(createdAt, sessionId);
715
+ return {
716
+ id: Number(result.lastInsertRowid),
717
+ sessionId,
718
+ sequence: row.sequence,
719
+ kind,
720
+ payload,
721
+ createdAt,
722
+ };
723
+ }
724
+ hasOperationLeaseRow(sessionId, executionToken, now) {
725
+ return Boolean(this.db.prepare(`
726
+ SELECT 1 FROM session_operation_leases
727
+ WHERE session_id = ? AND token = ? AND expires_at > ?
728
+ `).get(sessionId, executionToken, now));
729
+ }
730
+ }
731
+ //# sourceMappingURL=session-store.js.map