zidane 5.5.5 → 5.6.2

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 (71) hide show
  1. package/README.md +7 -1
  2. package/dist/{agent-CMAklak7.d.ts → agent-Dtnvs5ee.d.ts} +91 -2
  3. package/dist/agent-Dtnvs5ee.d.ts.map +1 -0
  4. package/dist/chat.d.ts +204 -15
  5. package/dist/chat.d.ts.map +1 -1
  6. package/dist/chat.js +3 -3
  7. package/dist/{errors-C5VSakmT.js → errors-DdZXnyXE.js} +38 -2
  8. package/dist/errors-DdZXnyXE.js.map +1 -0
  9. package/dist/{index-CF5QwBiz.d.ts → index-DHeHe04L.d.ts} +2 -2
  10. package/dist/{index-CF5QwBiz.d.ts.map → index-DHeHe04L.d.ts.map} +1 -1
  11. package/dist/{index-kroGomhj.d.ts → index-DX8De0nl.d.ts} +23 -2
  12. package/dist/index-DX8De0nl.d.ts.map +1 -0
  13. package/dist/index.d.ts +4 -4
  14. package/dist/index.js +10 -10
  15. package/dist/{interpolate-Cvjy8gpk.js → interpolate-j5V-wcAQ.js} +2 -2
  16. package/dist/{interpolate-Cvjy8gpk.js.map → interpolate-j5V-wcAQ.js.map} +1 -1
  17. package/dist/{login-B_kfoGMP.js → login-BOj03nVe.js} +5 -4
  18. package/dist/login-BOj03nVe.js.map +1 -0
  19. package/dist/{mcp-BE43Viwi.js → mcp-ngMS0S6N.js} +2 -2
  20. package/dist/{mcp-BE43Viwi.js.map → mcp-ngMS0S6N.js.map} +1 -1
  21. package/dist/mcp.d.ts +1 -1
  22. package/dist/mcp.js +1 -1
  23. package/dist/{messages-BBWakTN6.js → messages-B5k4DAXy.js} +2 -2
  24. package/dist/{messages-BBWakTN6.js.map → messages-B5k4DAXy.js.map} +1 -1
  25. package/dist/{presets-BDvBZuYI.js → presets-CTSij3yV.js} +2 -2
  26. package/dist/{presets-BDvBZuYI.js.map → presets-CTSij3yV.js.map} +1 -1
  27. package/dist/presets.d.ts +2 -2
  28. package/dist/presets.js +1 -1
  29. package/dist/{providers-CsUyN_FJ.js → providers-CaJE2ToS.js} +3 -3
  30. package/dist/{providers-CsUyN_FJ.js.map → providers-CaJE2ToS.js.map} +1 -1
  31. package/dist/providers.d.ts +1 -1
  32. package/dist/providers.js +2 -2
  33. package/dist/restate.d.ts +1 -1
  34. package/dist/session/sqlite.d.ts +1 -1
  35. package/dist/session/sqlite.d.ts.map +1 -1
  36. package/dist/session/sqlite.js +226 -51
  37. package/dist/session/sqlite.js.map +1 -1
  38. package/dist/{session-DzfRacU_.js → session-BoEW_wCR.js} +2 -2
  39. package/dist/{session-DzfRacU_.js.map → session-BoEW_wCR.js.map} +1 -1
  40. package/dist/session.d.ts +1 -1
  41. package/dist/session.js +2 -2
  42. package/dist/skills.d.ts +2 -2
  43. package/dist/skills.js +1 -1
  44. package/dist/{tools-Bbd0Ivwn.js → tools-CslsHpKb.js} +156 -16
  45. package/dist/tools-CslsHpKb.js.map +1 -0
  46. package/dist/tools.d.ts +2 -2
  47. package/dist/tools.js +1 -1
  48. package/dist/{transcript-anchors-C7CtKPPo.d.ts → transcript-anchors-CwoKNW6Y.d.ts} +74 -5
  49. package/dist/transcript-anchors-CwoKNW6Y.d.ts.map +1 -0
  50. package/dist/tui.d.ts +24 -5
  51. package/dist/tui.d.ts.map +1 -1
  52. package/dist/tui.js +1280 -333
  53. package/dist/tui.js.map +1 -1
  54. package/dist/{turn-operations-rYyU2Qyq.js → turn-operations-B8ySajUl.js} +687 -86
  55. package/dist/turn-operations-B8ySajUl.js.map +1 -0
  56. package/dist/types-oKPBdCmL.js.map +1 -1
  57. package/dist/types.d.ts +3 -3
  58. package/dist/types.js +2 -2
  59. package/docs/ARCHITECTURE.md +5 -2
  60. package/docs/CHAT.md +10 -3
  61. package/docs/RESTATE.md +190 -0
  62. package/docs/SKILL.md +27 -2
  63. package/docs/TUI.md +4 -3
  64. package/package.json +2 -1
  65. package/dist/agent-CMAklak7.d.ts.map +0 -1
  66. package/dist/errors-C5VSakmT.js.map +0 -1
  67. package/dist/index-kroGomhj.d.ts.map +0 -1
  68. package/dist/login-B_kfoGMP.js.map +0 -1
  69. package/dist/tools-Bbd0Ivwn.js.map +0 -1
  70. package/dist/transcript-anchors-C7CtKPPo.d.ts.map +0 -1
  71. package/dist/turn-operations-rYyU2Qyq.js.map +0 -1
@@ -1,86 +1,124 @@
1
- import { s as errorMessage } from "../errors-C5VSakmT.js";
1
+ import { c as errorMessage } from "../errors-DdZXnyXE.js";
2
2
  import { dirname } from "node:path";
3
- import { existsSync, mkdirSync } from "node:fs";
3
+ import { copyFileSync, existsSync, mkdirSync } from "node:fs";
4
4
  import { Database } from "bun:sqlite";
5
5
  //#region src/session/sqlite.ts
6
- /**
7
- * Current on-disk schema revision for the SQLite session store.
8
- *
9
- * 1 — initial schema (sessions table, no version pragma)
10
- * 2 — `parentRunId` / `depth` on runs; cache-aware `totalUsage`
11
- * fields; cache-aware token rendering. Pre-2 rows load via
12
- * defensive `?? 0` fallbacks.
13
- * 3 — `project_root` column + index. Sessions are tagged with the
14
- * project they belong to (git root / cwd) so the TUI can list
15
- * per-project. v2→v3 migration is non-destructive: we add the
16
- * column NULL, existing rows stay readable as "untagged".
17
- */
18
- const SCHEMA_VERSION = 3;
6
+ const SCHEMA_VERSION = 4;
19
7
  function createSqliteStore(options) {
20
8
  const db = new Database(options.path);
9
+ db.run("PRAGMA busy_timeout = 5000");
21
10
  db.run("PRAGMA journal_mode = WAL");
11
+ db.run("PRAGMA foreign_keys = ON");
22
12
  db.run(`
23
13
  CREATE TABLE IF NOT EXISTS sessions (
24
14
  id TEXT PRIMARY KEY,
25
15
  agent_id TEXT,
26
16
  project_root TEXT,
27
- data TEXT NOT NULL,
17
+ status TEXT NOT NULL DEFAULT 'idle',
18
+ metadata TEXT NOT NULL DEFAULT '{}',
28
19
  created_at INTEGER NOT NULL,
29
20
  updated_at INTEGER NOT NULL
30
21
  )
31
22
  `);
32
23
  db.run(`CREATE INDEX IF NOT EXISTS idx_sessions_agent_id ON sessions(agent_id)`);
33
- if (!db.query("PRAGMA table_info(sessions)").all().some((c) => c.name === "project_root")) db.run("ALTER TABLE sessions ADD COLUMN project_root TEXT");
34
- db.run("CREATE INDEX IF NOT EXISTS idx_sessions_project_root ON sessions(project_root)");
35
- if ((db.query("PRAGMA user_version").get()?.user_version ?? 0) < SCHEMA_VERSION) db.run(`PRAGMA user_version = ${SCHEMA_VERSION}`);
36
- const stmtLoad = db.prepare("SELECT data FROM sessions WHERE id = ?");
37
- const stmtUpsert = db.prepare(`
38
- INSERT INTO sessions (id, agent_id, project_root, data, created_at, updated_at)
39
- VALUES (?, ?, ?, ?, ?, ?)
24
+ db.run(`CREATE INDEX IF NOT EXISTS idx_sessions_project_root ON sessions(project_root)`);
25
+ db.run(`
26
+ CREATE TABLE IF NOT EXISTS turns (
27
+ session_id TEXT NOT NULL,
28
+ seq INTEGER NOT NULL,
29
+ data TEXT NOT NULL,
30
+ PRIMARY KEY (session_id, seq),
31
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
32
+ )
33
+ `);
34
+ db.run(`
35
+ CREATE TABLE IF NOT EXISTS runs (
36
+ session_id TEXT NOT NULL,
37
+ run_id TEXT NOT NULL,
38
+ started_at INTEGER NOT NULL,
39
+ data TEXT NOT NULL,
40
+ PRIMARY KEY (session_id, run_id),
41
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
42
+ )
43
+ `);
44
+ migrate(db, options.path);
45
+ const stmtLoadHeader = db.prepare(`
46
+ SELECT id, agent_id, project_root, status, metadata, created_at, updated_at
47
+ FROM sessions WHERE id = ?
48
+ `);
49
+ const stmtLoadTurns = db.prepare("SELECT data FROM turns WHERE session_id = ? ORDER BY seq");
50
+ const stmtLoadTurnsRange = db.prepare("SELECT data FROM turns WHERE session_id = ? ORDER BY seq LIMIT ? OFFSET ?");
51
+ const stmtLoadRuns = db.prepare("SELECT data FROM runs WHERE session_id = ? ORDER BY started_at, run_id");
52
+ const stmtUpsertHeader = db.prepare(`
53
+ INSERT INTO sessions (id, agent_id, project_root, status, metadata, created_at, updated_at)
54
+ VALUES (?, ?, ?, ?, ?, ?, ?)
40
55
  ON CONFLICT(id) DO UPDATE SET
41
56
  agent_id = excluded.agent_id,
42
57
  project_root = excluded.project_root,
43
- data = excluded.data,
58
+ status = excluded.status,
59
+ metadata = excluded.metadata,
44
60
  updated_at = excluded.updated_at
45
61
  `);
46
62
  const stmtDelete = db.prepare("DELETE FROM sessions WHERE id = ?");
63
+ const stmtDeleteTurnsForSession = db.prepare("DELETE FROM turns WHERE session_id = ?");
64
+ const stmtDeleteRunsForSession = db.prepare("DELETE FROM runs WHERE session_id = ?");
65
+ const stmtInsertTurn = db.prepare("INSERT INTO turns (session_id, seq, data) VALUES (?, ?, ?)");
66
+ const stmtUpsertRun = db.prepare(`
67
+ INSERT INTO runs (session_id, run_id, started_at, data) VALUES (?, ?, ?, ?)
68
+ ON CONFLICT(session_id, run_id) DO UPDATE SET
69
+ data = excluded.data,
70
+ started_at = excluded.started_at
71
+ `);
72
+ const stmtMaxSeq = db.prepare("SELECT MAX(seq) as m FROM turns WHERE session_id = ?");
73
+ const stmtTouch = db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?");
74
+ const stmtUpdateStatus = db.prepare("UPDATE sessions SET status = ?, updated_at = ? WHERE id = ?");
47
75
  const loadSync = (sessionId) => {
48
- const row = stmtLoad.get(sessionId);
49
- if (!row) return null;
50
- return JSON.parse(row.data);
51
- };
52
- const saveSync = (session) => {
53
- stmtUpsert.run(session.id, session.agentId ?? null, session.projectRoot ?? null, JSON.stringify(session), session.createdAt, session.updatedAt);
76
+ const header = stmtLoadHeader.get(sessionId);
77
+ if (!header) return null;
78
+ const turnRows = stmtLoadTurns.all(sessionId);
79
+ const runRows = stmtLoadRuns.all(sessionId);
80
+ return {
81
+ id: header.id,
82
+ ...header.agent_id ? { agentId: header.agent_id } : {},
83
+ ...header.project_root ? { projectRoot: header.project_root } : {},
84
+ turns: turnRows.map((r) => JSON.parse(r.data)),
85
+ runs: runRows.map((r) => JSON.parse(r.data)),
86
+ status: header.status,
87
+ metadata: safeParseRecord(header.metadata),
88
+ createdAt: header.created_at,
89
+ updatedAt: header.updated_at
90
+ };
54
91
  };
92
+ const txnSave = db.transaction((session) => {
93
+ stmtUpsertHeader.run(session.id, session.agentId ?? null, session.projectRoot ?? null, session.status, JSON.stringify(session.metadata ?? {}), session.createdAt, session.updatedAt);
94
+ stmtDeleteTurnsForSession.run(session.id);
95
+ stmtDeleteRunsForSession.run(session.id);
96
+ session.turns.forEach((t, i) => stmtInsertTurn.run(session.id, i, JSON.stringify(t)));
97
+ for (const run of session.runs) stmtUpsertRun.run(session.id, run.id, run.startedAt, JSON.stringify(run));
98
+ });
55
99
  const txnAppendTurns = db.transaction((sessionId, turns) => {
56
- const data = loadSync(sessionId);
57
- if (!data) return;
58
- data.turns.push(...turns);
59
- data.updatedAt = Date.now();
60
- saveSync(data);
100
+ if (!stmtLoadHeader.get(sessionId)) return;
101
+ let nextSeq = (stmtMaxSeq.get(sessionId).m ?? -1) + 1;
102
+ for (const turn of turns) {
103
+ stmtInsertTurn.run(sessionId, nextSeq, JSON.stringify(turn));
104
+ nextSeq += 1;
105
+ }
106
+ stmtTouch.run(Date.now(), sessionId);
61
107
  });
62
108
  const txnUpdateRun = db.transaction((sessionId, run) => {
63
- const data = loadSync(sessionId);
64
- if (!data) return;
65
- const idx = data.runs.findIndex((r) => r.id === run.id);
66
- if (idx >= 0) data.runs[idx] = run;
67
- else data.runs.push(run);
68
- data.updatedAt = Date.now();
69
- saveSync(data);
109
+ if (!stmtLoadHeader.get(sessionId)) return;
110
+ stmtUpsertRun.run(sessionId, run.id, run.startedAt, JSON.stringify(run));
111
+ stmtTouch.run(Date.now(), sessionId);
70
112
  });
71
113
  const txnUpdateStatus = db.transaction((sessionId, status) => {
72
- const data = loadSync(sessionId);
73
- if (!data) return;
74
- data.status = status;
75
- data.updatedAt = Date.now();
76
- saveSync(data);
114
+ stmtUpdateStatus.run(status, Date.now(), sessionId);
77
115
  });
78
116
  return {
79
117
  async load(sessionId) {
80
118
  return loadSync(sessionId);
81
119
  },
82
120
  async save(session) {
83
- saveSync(session);
121
+ txnSave.immediate(session);
84
122
  },
85
123
  async delete(sessionId) {
86
124
  stmtDelete.run(sessionId);
@@ -107,9 +145,8 @@ function createSqliteStore(options) {
107
145
  txnAppendTurns.immediate(sessionId, turns);
108
146
  },
109
147
  async getTurns(sessionId, from = 0, limit) {
110
- const data = loadSync(sessionId);
111
- if (!data) return [];
112
- return data.turns.slice(from, limit !== void 0 ? from + limit : void 0);
148
+ const lim = typeof limit === "number" ? limit : -1;
149
+ return stmtLoadTurnsRange.all(sessionId, lim, from).map((r) => JSON.parse(r.data));
113
150
  },
114
151
  async updateRun(sessionId, run) {
115
152
  txnUpdateRun.immediate(sessionId, run);
@@ -120,6 +157,144 @@ function createSqliteStore(options) {
120
157
  };
121
158
  }
122
159
  /**
160
+ * Forward-only schema migration runner. Idempotent — every step
161
+ * feature-detects rather than relying on the `PRAGMA user_version`
162
+ * number alone, so a partially-migrated DB (e.g. interrupted v3→v4
163
+ * backfill) is brought to a consistent v4 state on the next open.
164
+ *
165
+ * The legacy `data` column is the v3 sentinel: when present we treat
166
+ * the DB as v3-or-older and run the backfill. Before any destructive
167
+ * change we copy the entire DB file to `<path>.zidane-pre-v4.bak` (see
168
+ * {@link backupBeforeV3Migration}) so the user has a guaranteed
169
+ * recovery path if anything goes sideways.
170
+ *
171
+ * After backfill we attempt to drop the column (SQLite 3.35+; Bun
172
+ * ships ≥ 3.43 so this always succeeds in practice). If it does fail,
173
+ * we throw rather than limp along — leaving the column with `NOT NULL`
174
+ * while the new write paths omit it would silently break every future
175
+ * `INSERT`. The `.bak` file lets the user roll back and report the bug.
176
+ */
177
+ function migrate(db, dbPath) {
178
+ const sessionCols = db.query("PRAGMA table_info(sessions)").all();
179
+ const hasCol = (n) => sessionCols.some((c) => c.name === n);
180
+ if (!hasCol("project_root")) db.run("ALTER TABLE sessions ADD COLUMN project_root TEXT");
181
+ if (!hasCol("status")) db.run(`ALTER TABLE sessions ADD COLUMN status TEXT NOT NULL DEFAULT 'idle'`);
182
+ if (!hasCol("metadata")) db.run(`ALTER TABLE sessions ADD COLUMN metadata TEXT NOT NULL DEFAULT '{}'`);
183
+ if (hasCol("data")) {
184
+ backupBeforeV3Migration(db, dbPath);
185
+ backfillV3Blobs(db);
186
+ try {
187
+ db.run("ALTER TABLE sessions DROP COLUMN data");
188
+ } catch (err) {
189
+ throw new Error(`[zidane/sqlite] v3→v4 migration could not drop legacy "data" column: ${errorMessage(err)}. Restore from "${dbPath}.zidane-pre-v4.bak" with an older zidane build and report this — your sessions are intact in the backup file.`);
190
+ }
191
+ }
192
+ if ((db.query("PRAGMA user_version").get()?.user_version ?? 0) < SCHEMA_VERSION) db.run(`PRAGMA user_version = ${SCHEMA_VERSION}`);
193
+ }
194
+ /**
195
+ * Pre-migration safety net for v3 → v4: copy the source DB file to
196
+ * `<path>.zidane-pre-v4.bak` so the user can recover by restoring the
197
+ * snapshot if anything in the migration goes sideways.
198
+ *
199
+ * - Skipped for non-filesystem paths (`:memory:`, empty string) — no
200
+ * file to copy.
201
+ * - Skipped when the file is missing (fresh install, never reached
202
+ * migration territory).
203
+ * - Skipped when the backup already exists — re-runs of the migration
204
+ * (idempotent path) reuse the same snapshot rather than overwrite a
205
+ * potentially-earlier good copy with a later partial state.
206
+ * - WAL is checkpointed first so the .db file contains everything that
207
+ * was committed up to this point (otherwise pending WAL frames would
208
+ * be missing from the snapshot).
209
+ * - On copy failure the whole migration is aborted with a thrown error
210
+ * — we refuse to modify the DB without a recovery path. The user
211
+ * sees a clear diagnostic; their sessions stay intact.
212
+ *
213
+ * Surfaces a one-line stderr message on success so the user knows where
214
+ * the snapshot landed. They can delete it once they've confirmed the
215
+ * post-migration TUI behaves correctly.
216
+ */
217
+ function backupBeforeV3Migration(db, dbPath) {
218
+ if (dbPath === "" || dbPath === ":memory:" || dbPath.startsWith("file::memory:")) return;
219
+ if (!existsSync(dbPath)) return;
220
+ const backupPath = `${dbPath}.zidane-pre-v4.bak`;
221
+ if (existsSync(backupPath)) return;
222
+ try {
223
+ db.run("PRAGMA wal_checkpoint(TRUNCATE)");
224
+ } catch {}
225
+ try {
226
+ copyFileSync(dbPath, backupPath);
227
+ process.stderr.write(`[zidane/sqlite] v3→v4 migration: backed up your sessions DB to "${backupPath}".\n To roll back if anything looks wrong:\n 1. quit zidane\n 2. cp "${backupPath}" "${dbPath}" && rm -f "${dbPath}-wal" "${dbPath}-shm"\n 3. pin an older zidane that knows the v3 schema, or upgrade to a fix\n Delete the .bak once every session loads correctly.\n`);
228
+ } catch (err) {
229
+ throw new Error(`[zidane/sqlite] failed to back up "${dbPath}" before v3→v4 migration: ${errorMessage(err)}. Refusing to modify the DB without a recovery path. Move the file aside or fix the permissions and retry.`);
230
+ }
231
+ }
232
+ /**
233
+ * Parse each session's legacy `data` blob and fan its `turns` + `runs`
234
+ * arrays out into the dedicated tables, then overwrite the synthetic
235
+ * `status`/`metadata` defaults with the values from the blob.
236
+ *
237
+ * Skips rows that already have at least one entry in `turns` for them —
238
+ * a previous run of this migration already landed those bytes, and
239
+ * re-inserting them would either duplicate (if seq numbering shifted)
240
+ * or crash on the PK. The "header already exists, turns table populated"
241
+ * combination is the idempotency signal.
242
+ *
243
+ * Per-row failures (corrupt JSON, …) log + skip rather than abort the
244
+ * whole migration: one bad row shouldn't prevent the rest of the DB
245
+ * from coming forward to v4.
246
+ */
247
+ function backfillV3Blobs(db) {
248
+ const rows = db.query("SELECT id, data FROM sessions").all();
249
+ if (rows.length === 0) return;
250
+ const insertTurn = db.prepare("INSERT INTO turns (session_id, seq, data) VALUES (?, ?, ?)");
251
+ const upsertRun = db.prepare(`
252
+ INSERT INTO runs (session_id, run_id, started_at, data) VALUES (?, ?, ?, ?)
253
+ ON CONFLICT(session_id, run_id) DO UPDATE SET
254
+ data = excluded.data,
255
+ started_at = excluded.started_at
256
+ `);
257
+ const updateHeader = db.prepare("UPDATE sessions SET status = ?, metadata = ? WHERE id = ?");
258
+ const countTurns = db.prepare("SELECT COUNT(*) as c FROM turns WHERE session_id = ?");
259
+ for (const row of rows) {
260
+ if (!row.data) continue;
261
+ try {
262
+ const data = JSON.parse(row.data);
263
+ if (countTurns.get(row.id).c > 0) continue;
264
+ const turns = Array.isArray(data.turns) ? data.turns : [];
265
+ const runs = Array.isArray(data.runs) ? data.runs : [];
266
+ db.transaction(() => {
267
+ turns.forEach((t, i) => insertTurn.run(row.id, i, JSON.stringify(t)));
268
+ for (const run of runs) {
269
+ if (!run || typeof run.id !== "string") continue;
270
+ upsertRun.run(row.id, run.id, run.startedAt ?? 0, JSON.stringify(run));
271
+ }
272
+ updateHeader.run(data.status ?? "idle", JSON.stringify(data.metadata ?? {}), row.id);
273
+ }).immediate();
274
+ } catch (err) {
275
+ process.stderr.write(`[zidane/sqlite] v3→v4 migrate: session "${row.id}" skipped: ${errorMessage(err)}\n`);
276
+ }
277
+ }
278
+ }
279
+ /**
280
+ * `JSON.parse` with a fallback to `{}` for the metadata column. v3→v4
281
+ * migration writes well-formed JSON, fresh v4 writes go through
282
+ * `JSON.stringify(metadata ?? {})` — but a hand-edited DB or a botched
283
+ * external migration could leave a row with malformed JSON, and we'd
284
+ * rather surface an empty metadata object than crash the entire load
285
+ * path. Logged under `ZIDANE_DEBUG` so the corruption isn't fully
286
+ * silent.
287
+ */
288
+ function safeParseRecord(text) {
289
+ try {
290
+ const parsed = JSON.parse(text);
291
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
292
+ } catch (err) {
293
+ if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/sqlite] metadata JSON parse failed, defaulting to {}: ${errorMessage(err)}\n`);
294
+ return {};
295
+ }
296
+ }
297
+ /**
123
298
  * Convenience: open a SQLite store at `dbPath`, creating its parent
124
299
  * directory if it doesn't yet exist. Used by the TUI launcher to spin up
125
300
  * the default session store at `~/.zidane/sessions.db`.
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite.js","names":[],"sources":["../../src/session/sqlite.ts"],"sourcesContent":["/**\n * SQLite session store using Bun's built-in bun:sqlite.\n *\n * Concurrency model\n * -----------------\n * Mutating methods (`appendTurns`, `updateRun`, `updateStatus`) read the\n * existing row, mutate the parsed `SessionData`, and write it back. Under\n * `maxConcurrent > 1` spawns with `persist: true` two child runs can fire\n * `tool-results:after` on the same session at the same time — both reading\n * version N, both writing N+1, last writer silently clobbering the other.\n *\n * Every mutation here therefore runs inside a `BEGIN IMMEDIATE` transaction\n * via `bun:sqlite`'s `db.transaction(fn).immediate(args)` API. IMMEDIATE\n * acquires the database-level write lock at `BEGIN` time, so the second\n * writer blocks on `BEGIN IMMEDIATE` until the first commits — guaranteeing\n * its `SELECT` sees the freshly-written row.\n *\n * Schema versioning\n * -----------------\n * `PRAGMA user_version` is the canonical place to store schema version in\n * SQLite. `SCHEMA_VERSION` below is bumped whenever the on-disk shape of\n * `SessionData` changes in a way readers need to know about. The constructor\n * runs forward migrations; older databases get upgraded in place. Readers\n * still defensively fall back to `?? 0` etc. since the JSON blob is the\n * authoritative shape — `user_version` is for observability + future\n * structural migrations.\n */\n\nimport type { SessionData, SessionRun, SessionStore } from '.'\nimport type { SessionTurn } from '../types'\nimport { existsSync, mkdirSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { Database } from 'bun:sqlite'\nimport { errorMessage } from '../errors'\n\n/**\n * Current on-disk schema revision for the SQLite session store.\n *\n * 1 — initial schema (sessions table, no version pragma)\n * 2 — `parentRunId` / `depth` on runs; cache-aware `totalUsage`\n * fields; cache-aware token rendering. Pre-2 rows load via\n * defensive `?? 0` fallbacks.\n * 3 — `project_root` column + index. Sessions are tagged with the\n * project they belong to (git root / cwd) so the TUI can list\n * per-project. v2→v3 migration is non-destructive: we add the\n * column NULL, existing rows stay readable as \"untagged\".\n */\nconst SCHEMA_VERSION = 3\n\nexport interface SqliteStoreOptions {\n /** Path to the SQLite database file */\n path: string\n}\n\nexport function createSqliteStore(options: SqliteStoreOptions): SessionStore {\n const db = new Database(options.path)\n\n // WAL gives concurrent reads while a writer holds the lock — the\n // BEGIN IMMEDIATE serialization below still applies for writes.\n db.run('PRAGMA journal_mode = WAL')\n\n db.run(`\n CREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n agent_id TEXT,\n project_root TEXT,\n data TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n )\n `)\n db.run(`CREATE INDEX IF NOT EXISTS idx_sessions_agent_id ON sessions(agent_id)`)\n\n // Schema migration. Forward-only — we only ever ADD columns / indexes.\n //\n // v2 → v3 : adds `project_root` for per-project session filtering.\n // Existing rows get NULL (untagged); they remain readable\n // and load fine — the TUI just hides them from project-\n // scoped lists unless `showAllProjects` is on.\n //\n // `PRAGMA table_info` is the canonical way to feature-detect columns\n // on bun:sqlite without parsing `sqlite_master` text. Idempotent —\n // running it on a fresh DB (where `CREATE TABLE IF NOT EXISTS` above\n // already includes the column) is a no-op.\n const cols = db.query('PRAGMA table_info(sessions)').all() as { name: string }[]\n if (!cols.some(c => c.name === 'project_root'))\n db.run('ALTER TABLE sessions ADD COLUMN project_root TEXT')\n db.run('CREATE INDEX IF NOT EXISTS idx_sessions_project_root ON sessions(project_root)')\n\n const currentVersionRow = db.query('PRAGMA user_version').get() as { user_version: number } | null\n const currentVersion = currentVersionRow?.user_version ?? 0\n if (currentVersion < SCHEMA_VERSION)\n db.run(`PRAGMA user_version = ${SCHEMA_VERSION}`)\n\n const stmtLoad = db.prepare('SELECT data FROM sessions WHERE id = ?')\n const stmtUpsert = db.prepare(`\n INSERT INTO sessions (id, agent_id, project_root, data, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET\n agent_id = excluded.agent_id,\n project_root = excluded.project_root,\n data = excluded.data,\n updated_at = excluded.updated_at\n `)\n const stmtDelete = db.prepare('DELETE FROM sessions WHERE id = ?')\n\n // --- Synchronous primitives used inside transactions ----------------------\n //\n // bun:sqlite statements are synchronous; `db.transaction(fn).immediate(...)`\n // requires `fn` to be sync. Wrapping these as standalone helpers makes both\n // the transactional and the simple (non-mutating) paths share one\n // implementation.\n const loadSync = (sessionId: string): SessionData | null => {\n const row = stmtLoad.get(sessionId) as { data: string } | null\n if (!row)\n return null\n return JSON.parse(row.data) as SessionData\n }\n const saveSync = (session: SessionData): void => {\n stmtUpsert.run(\n session.id,\n session.agentId ?? null,\n session.projectRoot ?? null,\n JSON.stringify(session),\n session.createdAt,\n session.updatedAt,\n )\n }\n\n // Mutation transactions. `.immediate(...)` acquires the write lock at\n // `BEGIN IMMEDIATE` time so two concurrent writers serialize on the\n // transaction boundary, not on the (already-too-late) save call.\n const txnAppendTurns = db.transaction((sessionId: string, turns: SessionTurn[]) => {\n const data = loadSync(sessionId)\n if (!data)\n return\n data.turns.push(...turns)\n data.updatedAt = Date.now()\n saveSync(data)\n })\n const txnUpdateRun = db.transaction((sessionId: string, run: SessionRun) => {\n const data = loadSync(sessionId)\n if (!data)\n return\n // Upsert semantics: the agent calls `startRun()` to register a new\n // run in-memory and then `updateRun()` at finalize. But the store\n // doesn't see `startRun()` — it only sees `updateRun()`, AND\n // `txnAppendTurns` above re-reads from disk on every call (no new\n // runs visible there either). Without upserting here, the\n // finalize call no-ops for any newly-created run, and the on-disk\n // `runs[]` permanently lacks the run record. The next session\n // reload would then fail `ancestryOf(turn.runId)` for those turns,\n // making `eventsFromTurns` mis-classify subagent turns as\n // depth-0 — which leaks the subagent's prompt text into the TUI's\n // `userPrompts` history. See `eventsFromTurns` in `chat/store.ts`.\n const idx = data.runs.findIndex(r => r.id === run.id)\n if (idx >= 0)\n data.runs[idx] = run\n else\n data.runs.push(run)\n data.updatedAt = Date.now()\n saveSync(data)\n })\n const txnUpdateStatus = db.transaction((sessionId: string, status: SessionData['status']) => {\n const data = loadSync(sessionId)\n if (!data)\n return\n data.status = status\n data.updatedAt = Date.now()\n saveSync(data)\n })\n\n const store: SessionStore = {\n async load(sessionId: string) {\n return loadSync(sessionId)\n },\n\n async save(session: SessionData) {\n saveSync(session)\n },\n\n async delete(sessionId: string) {\n stmtDelete.run(sessionId)\n },\n\n async list(filter) {\n // Build the SQL on the fly so the filter axes compose cleanly —\n // four prepared statements collapsed into one dynamic query. The\n // shape is always `SELECT id FROM sessions [WHERE …] ORDER BY\n // updated_at DESC [LIMIT N]`; only the WHERE varies.\n //\n // `projectRoot: string` → `WHERE project_root = ?` (excludes untagged).\n // `projectRoot: null` → `WHERE project_root IS NULL` (untagged only).\n // `projectRoot: undefined` → axis omitted (untagged + tagged).\n //\n // The `null` case lets a future \"show legacy\" debug toggle query\n // pre-v3 sessions explicitly without leaking them into normal\n // per-project lists.\n const conditions: string[] = []\n const params: string[] = []\n if (filter?.agentId) {\n conditions.push('agent_id = ?')\n params.push(filter.agentId)\n }\n if ('projectRoot' in (filter ?? {})) {\n const v = filter!.projectRoot\n if (v === null) {\n conditions.push('project_root IS NULL')\n }\n else if (typeof v === 'string') {\n conditions.push('project_root = ?')\n params.push(v)\n }\n }\n const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''\n const limit = typeof filter?.limit === 'number' && filter.limit > 0 ? `LIMIT ${filter.limit}` : ''\n const sql = `SELECT id FROM sessions ${where} ORDER BY updated_at DESC ${limit}`.replace(/\\s+/g, ' ').trim()\n const rows = db.query(sql).all(...params) as { id: string }[]\n return rows.map(r => r.id)\n },\n\n async appendTurns(sessionId: string, turns: SessionTurn[]) {\n txnAppendTurns.immediate(sessionId, turns)\n },\n\n async getTurns(sessionId: string, from = 0, limit?: number) {\n const data = loadSync(sessionId)\n if (!data)\n return []\n return data.turns.slice(from, limit !== undefined ? from + limit : undefined)\n },\n\n async updateRun(sessionId: string, run: SessionRun) {\n txnUpdateRun.immediate(sessionId, run)\n },\n\n async updateStatus(sessionId: string, status: SessionData['status']) {\n txnUpdateStatus.immediate(sessionId, status)\n },\n }\n\n return store\n}\n\n/**\n * Convenience: open a SQLite store at `dbPath`, creating its parent\n * directory if it doesn't yet exist. Used by the TUI launcher to spin up\n * the default session store at `~/.zidane/sessions.db`.\n *\n * Lives here (and not in `chat/store.ts`) so the renderer-agnostic\n * `zidane/chat` entry stays free of `bun:sqlite` — non-Bun consumers\n * (a future GUI, an SDK) can import `zidane/chat` without pulling in the\n * Bun-only binding.\n */\nexport function createTuiStore(dbPath: string): SessionStore {\n const dir = dirname(dbPath)\n if (!existsSync(dir)) {\n try {\n mkdirSync(dir, { recursive: true })\n }\n catch (err) {\n throw new Error(\n `Could not create TUI storage directory at \"${dir}\". `\n + `Override the location via \\`runTui({ storageDir, prefix })\\` or the `\n + `\\`ZIDANE_STORAGE_DIR\\` env var. Original error: ${errorMessage(err)}`,\n )\n }\n }\n return createSqliteStore({ path: dbPath })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sqlite.js","names":[],"sources":["../../src/session/sqlite.ts"],"sourcesContent":["/**\n * SQLite session store using Bun's built-in bun:sqlite.\n *\n * Schema v4 — normalized\n * ----------------------\n *\n * Three tables: `sessions` (one row per session, holds the header fields),\n * `turns` (one row per turn, ordered by `(session_id, seq)`), and `runs`\n * (one row per run, keyed by `(session_id, run_id)`). Replaces the v3\n * \"fat blob\" shape where the entire `SessionData` — including every turn\n * and every run — lived in a single TEXT column.\n *\n * The normalization removes a write-amplification anti-pattern that bit\n * long sessions hard: every `appendTurns` previously re-read the full\n * document, mutated the parsed object in memory, and serialized the\n * whole thing back. At turn N the per-call work was O(N) bytes; a 5000-\n * turn session paid multi-MB JSON.stringify on every assistant turn.\n * The new shape makes:\n *\n * - `appendTurns` a pure `INSERT INTO turns` per turn (O(1) per row,\n * plus one `MAX(seq)` lookup at the start of the call).\n * - `updateRun` an `INSERT OR UPDATE ON CONFLICT` against `runs` —\n * the upsert semantic the v3 code carefully simulated in JS is now\n * the row PK's natural behavior.\n * - `updateStatus` a single-column `UPDATE` against the sessions row\n * instead of a full read-mutate-write cycle.\n * - `getTurns(from, limit)` an indexed `LIMIT/OFFSET` range scan\n * instead of \"parse full doc, slice\".\n *\n * Concurrency model\n * -----------------\n * Mutations still run inside `BEGIN IMMEDIATE` transactions via\n * `db.transaction(fn).immediate(...)` — IMMEDIATE acquires the database-\n * level write lock at `BEGIN`, so two concurrent writers serialize on\n * the transaction boundary. WAL mode (set on connection open) lets\n * readers see committed data without blocking the writer.\n *\n * `delete(sessionId)` relies on `PRAGMA foreign_keys = ON` to cascade\n * `sessions` → `turns` / `runs` via ON DELETE CASCADE. The pragma is\n * per-connection; it's set at connection open.\n *\n * Schema versioning\n * -----------------\n * `PRAGMA user_version` tracks the current shape:\n *\n * 1 — initial schema (sessions table, no version pragma)\n * 2 — `parentRunId` / `depth` on runs; cache-aware `totalUsage` fields\n * 3 — `project_root` column + index\n * 4 — normalized layout (this file). `data` blob column dropped,\n * header fields promoted (`status`, `metadata`), turns + runs\n * split into dedicated tables. v3→v4 migration runs in-place at\n * connection open: existing blobs are parsed and fanned out, then\n * the legacy `data` column is dropped (SQLite 3.35+).\n *\n * Safety net for v3 → v4\n * ----------------------\n * Before the first destructive change (column drop, data fan-out), the\n * migration copies the source DB file to `<path>.zidane-pre-v4.bak` and\n * checkpoints WAL so the snapshot is point-in-time consistent. A user\n * who hits a regression can restore by copying that file back over\n * `sessions.db` (with the old `zidane` version) without any data loss.\n * The backup is created once and never overwritten — re-runs of the\n * migration (idempotent path) reuse the existing snapshot.\n *\n * Per-row backfill failures (corrupt JSON inside one session's blob)\n * are logged + skipped rather than aborting the whole DB; the .bak\n * file remains the recovery path for those individual rows.\n *\n * Forward-only migrations: we never roll back. Hosts pinning an older\n * `zidane` against a v4 db would fail to read the (now-missing) `data`\n * column — same forward-compat policy as the rest of the project. The\n * `.zidane-pre-v4.bak` snapshot is the explicit escape hatch for that\n * downgrade case.\n */\n\nimport type { SessionData, SessionRun, SessionStore } from '.'\nimport type { SessionTurn } from '../types'\nimport { copyFileSync, existsSync, mkdirSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { Database } from 'bun:sqlite'\nimport { errorMessage } from '../errors'\n\nconst SCHEMA_VERSION = 4\n\nexport interface SqliteStoreOptions {\n /** Path to the SQLite database file */\n path: string\n}\n\nexport function createSqliteStore(options: SqliteStoreOptions): SessionStore {\n const db = new Database(options.path)\n\n // `busy_timeout` BEFORE any other PRAGMA so the very first DDL /\n // mode-change tolerates a brief lock held by another writer rather\n // than throwing \"database is locked\" instantly. Real scenarios this\n // covers:\n //\n // - `bun --watch` dev loops where the prior process hasn't fully\n // released the WAL / -shm sidecars at the moment the restart\n // re-opens the file.\n // - A second zidane TUI booted before the first one finished\n // shutting down (rare, but `bun:sqlite` won't retry on its own).\n // - The v3→v4 migration's `wal_checkpoint(TRUNCATE)` racing a\n // stale background connection.\n //\n // 5s is generous enough that an actual zombie process is obvious to\n // the operator (they can Ctrl+C) while still erring on the side of\n // \"just work\" for the dev-loop case.\n db.run('PRAGMA busy_timeout = 5000')\n\n // WAL gives concurrent reads while a writer holds the lock; `BEGIN\n // IMMEDIATE` below still serializes writers among themselves.\n db.run('PRAGMA journal_mode = WAL')\n // Required for the ON DELETE CASCADE from `sessions` to `turns` / `runs`.\n // Per-connection setting; cheap idempotent call at open.\n db.run('PRAGMA foreign_keys = ON')\n\n // ------------------------------------------------------------------\n // Schema (v4 shape). `CREATE TABLE IF NOT EXISTS` is a no-op on\n // pre-existing v3 databases; the migrate() call below brings them\n // forward via `ALTER TABLE ADD COLUMN` + data backfill.\n // ------------------------------------------------------------------\n db.run(`\n CREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n agent_id TEXT,\n project_root TEXT,\n status TEXT NOT NULL DEFAULT 'idle',\n metadata TEXT NOT NULL DEFAULT '{}',\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n )\n `)\n db.run(`CREATE INDEX IF NOT EXISTS idx_sessions_agent_id ON sessions(agent_id)`)\n db.run(`CREATE INDEX IF NOT EXISTS idx_sessions_project_root ON sessions(project_root)`)\n\n // Per-turn rows. `(session_id, seq)` PK + `ORDER BY seq` reads give\n // O(log n) ordered access and O(1) appends. Cascade so `delete()` on\n // sessions clears these rows in one statement.\n db.run(`\n CREATE TABLE IF NOT EXISTS turns (\n session_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n data TEXT NOT NULL,\n PRIMARY KEY (session_id, seq),\n FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE\n )\n `)\n\n // Per-run rows. `(session_id, run_id)` PK gives natural upsert via\n // `ON CONFLICT(session_id, run_id) DO UPDATE`. `started_at` lifted to\n // a column so reads can `ORDER BY` it cheaply (the wall-clock order of\n // run creation is the order the agent registered them, which is what\n // `eventsFromTurns` expects when reconstructing the run tree).\n db.run(`\n CREATE TABLE IF NOT EXISTS runs (\n session_id TEXT NOT NULL,\n run_id TEXT NOT NULL,\n started_at INTEGER NOT NULL,\n data TEXT NOT NULL,\n PRIMARY KEY (session_id, run_id),\n FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE\n )\n `)\n\n migrate(db, options.path)\n\n // ------------------------------------------------------------------\n // Prepared statements (hot-path queries).\n // ------------------------------------------------------------------\n const stmtLoadHeader = db.prepare(`\n SELECT id, agent_id, project_root, status, metadata, created_at, updated_at\n FROM sessions WHERE id = ?\n `)\n const stmtLoadTurns = db.prepare('SELECT data FROM turns WHERE session_id = ? ORDER BY seq')\n const stmtLoadTurnsRange = db.prepare('SELECT data FROM turns WHERE session_id = ? ORDER BY seq LIMIT ? OFFSET ?')\n const stmtLoadRuns = db.prepare('SELECT data FROM runs WHERE session_id = ? ORDER BY started_at, run_id')\n\n const stmtUpsertHeader = db.prepare(`\n INSERT INTO sessions (id, agent_id, project_root, status, metadata, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET\n agent_id = excluded.agent_id,\n project_root = excluded.project_root,\n status = excluded.status,\n metadata = excluded.metadata,\n updated_at = excluded.updated_at\n `)\n const stmtDelete = db.prepare('DELETE FROM sessions WHERE id = ?')\n const stmtDeleteTurnsForSession = db.prepare('DELETE FROM turns WHERE session_id = ?')\n const stmtDeleteRunsForSession = db.prepare('DELETE FROM runs WHERE session_id = ?')\n const stmtInsertTurn = db.prepare('INSERT INTO turns (session_id, seq, data) VALUES (?, ?, ?)')\n const stmtUpsertRun = db.prepare(`\n INSERT INTO runs (session_id, run_id, started_at, data) VALUES (?, ?, ?, ?)\n ON CONFLICT(session_id, run_id) DO UPDATE SET\n data = excluded.data,\n started_at = excluded.started_at\n `)\n const stmtMaxSeq = db.prepare('SELECT MAX(seq) as m FROM turns WHERE session_id = ?')\n const stmtTouch = db.prepare('UPDATE sessions SET updated_at = ? WHERE id = ?')\n const stmtUpdateStatus = db.prepare('UPDATE sessions SET status = ?, updated_at = ? WHERE id = ?')\n\n // ------------------------------------------------------------------\n // Sync helpers used inside transactions. bun:sqlite's `db.transaction(fn)`\n // requires `fn` to be sync; wrapping these as standalone helpers keeps\n // the transactional and non-transactional code paths sharing one\n // implementation.\n // ------------------------------------------------------------------\n interface HeaderRow {\n id: string\n agent_id: string | null\n project_root: string | null\n status: string\n metadata: string\n created_at: number\n updated_at: number\n }\n\n const loadSync = (sessionId: string): SessionData | null => {\n const header = stmtLoadHeader.get(sessionId) as HeaderRow | null\n if (!header)\n return null\n\n const turnRows = stmtLoadTurns.all(sessionId) as { data: string }[]\n const runRows = stmtLoadRuns.all(sessionId) as { data: string }[]\n\n return {\n id: header.id,\n ...(header.agent_id ? { agentId: header.agent_id } : {}),\n ...(header.project_root ? { projectRoot: header.project_root } : {}),\n turns: turnRows.map(r => JSON.parse(r.data) as SessionTurn),\n runs: runRows.map(r => JSON.parse(r.data) as SessionRun),\n status: header.status as SessionData['status'],\n metadata: safeParseRecord(header.metadata),\n createdAt: header.created_at,\n updatedAt: header.updated_at,\n }\n }\n\n // ------------------------------------------------------------------\n // Mutation transactions. `.immediate(...)` acquires the write lock at\n // `BEGIN IMMEDIATE` time so two concurrent writers serialize on the\n // transaction boundary, not on the (already-too-late) save call.\n // ------------------------------------------------------------------\n const txnSave = db.transaction((session: SessionData) => {\n // Header upsert. `status` + `metadata` are now first-class columns\n // instead of nested fields in a blob; the JSON.stringify on\n // `metadata` is the only serialization we still do at this level.\n stmtUpsertHeader.run(\n session.id,\n session.agentId ?? null,\n session.projectRoot ?? null,\n session.status,\n JSON.stringify(session.metadata ?? {}),\n session.createdAt,\n session.updatedAt,\n )\n // `save()` is the bulk-overwrite path (used by compact / fork /\n // explicit consumer save). Wipe and re-insert turns + runs so the\n // on-disk state mirrors the caller's view exactly — matches the\n // v3 semantic where the whole `data` blob was replaced.\n stmtDeleteTurnsForSession.run(session.id)\n stmtDeleteRunsForSession.run(session.id)\n session.turns.forEach((t, i) => stmtInsertTurn.run(session.id, i, JSON.stringify(t)))\n for (const run of session.runs) {\n stmtUpsertRun.run(session.id, run.id, run.startedAt, JSON.stringify(run))\n }\n })\n\n const txnAppendTurns = db.transaction((sessionId: string, turns: SessionTurn[]) => {\n // Header must exist; we don't auto-create a session row from an\n // `appendTurns` against an unknown id. Matches v3 semantics: the old\n // code's `loadSync` returned null in that case and the append was\n // a silent no-op. Callers that need ownership of the session row\n // create it via `createSession()` + `session.save()` first.\n const header = stmtLoadHeader.get(sessionId)\n if (!header)\n return\n\n const max = (stmtMaxSeq.get(sessionId) as { m: number | null }).m\n let nextSeq = (max ?? -1) + 1\n for (const turn of turns) {\n stmtInsertTurn.run(sessionId, nextSeq, JSON.stringify(turn))\n nextSeq += 1\n }\n stmtTouch.run(Date.now(), sessionId)\n })\n\n const txnUpdateRun = db.transaction((sessionId: string, run: SessionRun) => {\n const header = stmtLoadHeader.get(sessionId)\n if (!header)\n return\n // True upsert via PK constraint — the v3 code had a careful comment\n // explaining why a find-then-update would silently lose run records;\n // that whole class of bug is structurally impossible here. New ids\n // get inserted, existing ids get their JSON + started_at refreshed.\n stmtUpsertRun.run(sessionId, run.id, run.startedAt, JSON.stringify(run))\n stmtTouch.run(Date.now(), sessionId)\n })\n\n const txnUpdateStatus = db.transaction((sessionId: string, status: SessionData['status']) => {\n stmtUpdateStatus.run(status, Date.now(), sessionId)\n })\n\n const store: SessionStore = {\n async load(sessionId: string) {\n return loadSync(sessionId)\n },\n\n async save(session: SessionData) {\n txnSave.immediate(session)\n },\n\n async delete(sessionId: string) {\n // FK CASCADE on `turns.session_id` / `runs.session_id` clears the\n // dependent rows. Single statement, single round-trip.\n stmtDelete.run(sessionId)\n },\n\n async list(filter) {\n // Build the SQL on the fly so the filter axes compose cleanly —\n // four prepared statements collapsed into one dynamic query. The\n // shape is always `SELECT id FROM sessions [WHERE …] ORDER BY\n // updated_at DESC [LIMIT N]`; only the WHERE varies.\n //\n // `projectRoot: string` → `WHERE project_root = ?` (excludes untagged).\n // `projectRoot: null` → `WHERE project_root IS NULL` (untagged only).\n // `projectRoot: undefined` → axis omitted (untagged + tagged).\n //\n // The `null` case lets a future \"show legacy\" debug toggle query\n // pre-v3 sessions explicitly without leaking them into normal\n // per-project lists.\n const conditions: string[] = []\n const params: string[] = []\n if (filter?.agentId) {\n conditions.push('agent_id = ?')\n params.push(filter.agentId)\n }\n if ('projectRoot' in (filter ?? {})) {\n const v = filter!.projectRoot\n if (v === null) {\n conditions.push('project_root IS NULL')\n }\n else if (typeof v === 'string') {\n conditions.push('project_root = ?')\n params.push(v)\n }\n }\n const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''\n const limit = typeof filter?.limit === 'number' && filter.limit > 0 ? `LIMIT ${filter.limit}` : ''\n const sql = `SELECT id FROM sessions ${where} ORDER BY updated_at DESC ${limit}`.replace(/\\s+/g, ' ').trim()\n const rows = db.query(sql).all(...params) as { id: string }[]\n return rows.map(r => r.id)\n },\n\n async appendTurns(sessionId: string, turns: SessionTurn[]) {\n txnAppendTurns.immediate(sessionId, turns)\n },\n\n async getTurns(sessionId: string, from = 0, limit?: number) {\n // `LIMIT -1` in SQLite means \"no limit\". Use that as the sentinel\n // for `limit === undefined` so the same prepared statement covers\n // both paged and full-range reads without a second SQL.\n const lim = typeof limit === 'number' ? limit : -1\n const rows = stmtLoadTurnsRange.all(sessionId, lim, from) as { data: string }[]\n return rows.map(r => JSON.parse(r.data) as SessionTurn)\n },\n\n async updateRun(sessionId: string, run: SessionRun) {\n txnUpdateRun.immediate(sessionId, run)\n },\n\n async updateStatus(sessionId: string, status: SessionData['status']) {\n txnUpdateStatus.immediate(sessionId, status)\n },\n }\n\n return store\n}\n\n// ---------------------------------------------------------------------------\n// Migrations\n// ---------------------------------------------------------------------------\n\n/**\n * Forward-only schema migration runner. Idempotent — every step\n * feature-detects rather than relying on the `PRAGMA user_version`\n * number alone, so a partially-migrated DB (e.g. interrupted v3→v4\n * backfill) is brought to a consistent v4 state on the next open.\n *\n * The legacy `data` column is the v3 sentinel: when present we treat\n * the DB as v3-or-older and run the backfill. Before any destructive\n * change we copy the entire DB file to `<path>.zidane-pre-v4.bak` (see\n * {@link backupBeforeV3Migration}) so the user has a guaranteed\n * recovery path if anything goes sideways.\n *\n * After backfill we attempt to drop the column (SQLite 3.35+; Bun\n * ships ≥ 3.43 so this always succeeds in practice). If it does fail,\n * we throw rather than limp along — leaving the column with `NOT NULL`\n * while the new write paths omit it would silently break every future\n * `INSERT`. The `.bak` file lets the user roll back and report the bug.\n */\nfunction migrate(db: Database, dbPath: string) {\n const sessionCols = db.query('PRAGMA table_info(sessions)').all() as { name: string }[]\n const hasCol = (n: string) => sessionCols.some(c => c.name === n)\n\n // v1/v2 → v3: project_root column (idempotent on fresh DBs since\n // CREATE TABLE already has it).\n if (!hasCol('project_root'))\n db.run('ALTER TABLE sessions ADD COLUMN project_root TEXT')\n\n // v3 → v4 header promotion. ADD COLUMN ... DEFAULT applies the\n // default to every existing row, so v3 sessions get a synthetic\n // 'idle' status + empty metadata until the backfill loop below\n // overwrites with the real values from the JSON blob.\n if (!hasCol('status'))\n db.run(`ALTER TABLE sessions ADD COLUMN status TEXT NOT NULL DEFAULT 'idle'`)\n if (!hasCol('metadata'))\n db.run(`ALTER TABLE sessions ADD COLUMN metadata TEXT NOT NULL DEFAULT '{}'`)\n\n // v3 → v4 row fan-out. Skip when the legacy column is gone (already\n // migrated, or fresh v4 DB).\n if (hasCol('data')) {\n // Take a point-in-time snapshot BEFORE the destructive changes\n // below. Aborts the whole migration on backup failure so the user's\n // v3 DB is never modified without a recovery path on disk.\n backupBeforeV3Migration(db, dbPath)\n backfillV3Blobs(db)\n // Drop the legacy column. Bun's SQLite is 3.51+ in practice — DROP\n // COLUMN has been supported since 3.35 (2021). If it ever did fail\n // we'd be left with a `NOT NULL data` column that the new INSERT\n // statements don't populate, making the DB unwritable. Throwing\n // here surfaces the failure immediately and points the user at the\n // backup file we just created.\n try {\n db.run('ALTER TABLE sessions DROP COLUMN data')\n }\n catch (err) {\n throw new Error(\n `[zidane/sqlite] v3→v4 migration could not drop legacy \"data\" column: ${errorMessage(err)}. `\n + `Restore from \"${dbPath}.zidane-pre-v4.bak\" with an older zidane build and report this — `\n + `your sessions are intact in the backup file.`,\n )\n }\n }\n\n const currentVersion = (db.query('PRAGMA user_version').get() as { user_version: number } | null)?.user_version ?? 0\n if (currentVersion < SCHEMA_VERSION)\n db.run(`PRAGMA user_version = ${SCHEMA_VERSION}`)\n}\n\n/**\n * Pre-migration safety net for v3 → v4: copy the source DB file to\n * `<path>.zidane-pre-v4.bak` so the user can recover by restoring the\n * snapshot if anything in the migration goes sideways.\n *\n * - Skipped for non-filesystem paths (`:memory:`, empty string) — no\n * file to copy.\n * - Skipped when the file is missing (fresh install, never reached\n * migration territory).\n * - Skipped when the backup already exists — re-runs of the migration\n * (idempotent path) reuse the same snapshot rather than overwrite a\n * potentially-earlier good copy with a later partial state.\n * - WAL is checkpointed first so the .db file contains everything that\n * was committed up to this point (otherwise pending WAL frames would\n * be missing from the snapshot).\n * - On copy failure the whole migration is aborted with a thrown error\n * — we refuse to modify the DB without a recovery path. The user\n * sees a clear diagnostic; their sessions stay intact.\n *\n * Surfaces a one-line stderr message on success so the user knows where\n * the snapshot landed. They can delete it once they've confirmed the\n * post-migration TUI behaves correctly.\n */\nfunction backupBeforeV3Migration(db: Database, dbPath: string) {\n // `:memory:` and similar URI-shaped paths have no filesystem target.\n // bun:sqlite also accepts an empty string as a synonym for in-memory.\n if (dbPath === '' || dbPath === ':memory:' || dbPath.startsWith('file::memory:'))\n return\n\n if (!existsSync(dbPath))\n return\n\n const backupPath = `${dbPath}.zidane-pre-v4.bak`\n // Don't clobber an existing backup. The first interrupted-migration\n // attempt's snapshot is the one closest to the user's pre-zidane-v4\n // state; subsequent retries would only overwrite it with a partial\n // intermediate.\n if (existsSync(backupPath))\n return\n\n // Checkpoint WAL so the .db file contains every committed frame.\n // Without this, a backup of just the .db file would be missing\n // anything sitting in the -wal sidecar. Failure is non-fatal — an\n // empty WAL (the common case for a freshly-opened DB) returns an\n // error from TRUNCATE on some SQLite builds; we still proceed with\n // the file copy.\n try {\n db.run('PRAGMA wal_checkpoint(TRUNCATE)')\n }\n catch {\n // Best-effort; the copy below still captures whatever's on disk.\n }\n\n try {\n copyFileSync(dbPath, backupPath)\n process.stderr.write(\n `[zidane/sqlite] v3→v4 migration: backed up your sessions DB to \"${backupPath}\".\\n`\n + ` To roll back if anything looks wrong:\\n`\n + ` 1. quit zidane\\n`\n + ` 2. cp \"${backupPath}\" \"${dbPath}\" && rm -f \"${dbPath}-wal\" \"${dbPath}-shm\"\\n`\n + ` 3. pin an older zidane that knows the v3 schema, or upgrade to a fix\\n`\n + ` Delete the .bak once every session loads correctly.\\n`,\n )\n }\n catch (err) {\n throw new Error(\n `[zidane/sqlite] failed to back up \"${dbPath}\" before v3→v4 migration: ${errorMessage(err)}. `\n + `Refusing to modify the DB without a recovery path. Move the file aside or fix the permissions and retry.`,\n )\n }\n}\n\n/**\n * Parse each session's legacy `data` blob and fan its `turns` + `runs`\n * arrays out into the dedicated tables, then overwrite the synthetic\n * `status`/`metadata` defaults with the values from the blob.\n *\n * Skips rows that already have at least one entry in `turns` for them —\n * a previous run of this migration already landed those bytes, and\n * re-inserting them would either duplicate (if seq numbering shifted)\n * or crash on the PK. The \"header already exists, turns table populated\"\n * combination is the idempotency signal.\n *\n * Per-row failures (corrupt JSON, …) log + skip rather than abort the\n * whole migration: one bad row shouldn't prevent the rest of the DB\n * from coming forward to v4.\n */\nfunction backfillV3Blobs(db: Database) {\n // Use a raw SELECT (not a prepared statement) so the legacy column\n // reference doesn't survive in the statement cache after the column\n // is dropped at the end of `migrate()`.\n const rows = db.query('SELECT id, data FROM sessions').all() as { id: string, data: string | null }[]\n if (rows.length === 0)\n return\n\n const insertTurn = db.prepare('INSERT INTO turns (session_id, seq, data) VALUES (?, ?, ?)')\n const upsertRun = db.prepare(`\n INSERT INTO runs (session_id, run_id, started_at, data) VALUES (?, ?, ?, ?)\n ON CONFLICT(session_id, run_id) DO UPDATE SET\n data = excluded.data,\n started_at = excluded.started_at\n `)\n const updateHeader = db.prepare('UPDATE sessions SET status = ?, metadata = ? WHERE id = ?')\n const countTurns = db.prepare('SELECT COUNT(*) as c FROM turns WHERE session_id = ?')\n\n for (const row of rows) {\n if (!row.data)\n continue\n try {\n const data = JSON.parse(row.data) as Partial<SessionData>\n // Skip rows whose turns table already has content — a previous\n // partial migration already fanned this row out.\n const existing = (countTurns.get(row.id) as { c: number }).c\n if (existing > 0)\n continue\n\n const turns = Array.isArray(data.turns) ? data.turns : []\n const runs = Array.isArray(data.runs) ? data.runs : []\n const txn = db.transaction(() => {\n turns.forEach((t, i) => insertTurn.run(row.id, i, JSON.stringify(t)))\n for (const run of runs) {\n // Defensive: a corrupt run record without an id can't go into\n // the (session_id, run_id) PK. Skip rather than crash.\n if (!run || typeof run.id !== 'string')\n continue\n upsertRun.run(row.id, run.id, run.startedAt ?? 0, JSON.stringify(run))\n }\n updateHeader.run(data.status ?? 'idle', JSON.stringify(data.metadata ?? {}), row.id)\n })\n txn.immediate()\n }\n catch (err) {\n process.stderr.write(`[zidane/sqlite] v3→v4 migrate: session \"${row.id}\" skipped: ${errorMessage(err)}\\n`)\n }\n }\n}\n\n/**\n * `JSON.parse` with a fallback to `{}` for the metadata column. v3→v4\n * migration writes well-formed JSON, fresh v4 writes go through\n * `JSON.stringify(metadata ?? {})` — but a hand-edited DB or a botched\n * external migration could leave a row with malformed JSON, and we'd\n * rather surface an empty metadata object than crash the entire load\n * path. Logged under `ZIDANE_DEBUG` so the corruption isn't fully\n * silent.\n */\nfunction safeParseRecord(text: string): Record<string, unknown> {\n try {\n const parsed = JSON.parse(text)\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? parsed as Record<string, unknown>\n : {}\n }\n catch (err) {\n if (process.env.ZIDANE_DEBUG)\n process.stderr.write(`[zidane/sqlite] metadata JSON parse failed, defaulting to {}: ${errorMessage(err)}\\n`)\n return {}\n }\n}\n\n/**\n * Convenience: open a SQLite store at `dbPath`, creating its parent\n * directory if it doesn't yet exist. Used by the TUI launcher to spin up\n * the default session store at `~/.zidane/sessions.db`.\n *\n * Lives here (and not in `chat/store.ts`) so the renderer-agnostic\n * `zidane/chat` entry stays free of `bun:sqlite` — non-Bun consumers\n * (a future GUI, an SDK) can import `zidane/chat` without pulling in the\n * Bun-only binding.\n */\nexport function createTuiStore(dbPath: string): SessionStore {\n const dir = dirname(dbPath)\n if (!existsSync(dir)) {\n try {\n mkdirSync(dir, { recursive: true })\n }\n catch (err) {\n throw new Error(\n `Could not create TUI storage directory at \"${dir}\". `\n + `Override the location via \\`runTui({ storageDir, prefix })\\` or the `\n + `\\`ZIDANE_STORAGE_DIR\\` env var. Original error: ${errorMessage(err)}`,\n )\n }\n }\n return createSqliteStore({ path: dbPath })\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import "./messages-BBWakTN6.js";
1
+ import "./messages-B5k4DAXy.js";
2
2
  //#region src/session/file-map.ts
3
3
  function toMeta(data) {
4
4
  return {
@@ -474,4 +474,4 @@ function generateId() {
474
474
  //#endregion
475
475
  export { createFileMapStore as a, createMemoryStore as i, loadSession as n, createRemoteStore as r, createSession as t };
476
476
 
477
- //# sourceMappingURL=session-DzfRacU_.js.map
477
+ //# sourceMappingURL=session-BoEW_wCR.js.map