zidane 5.6.14 → 5.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/{agent-ClkpElCZ.d.ts → agent-BNS2nx_T.d.ts} +535 -15
- package/dist/agent-BNS2nx_T.d.ts.map +1 -0
- package/dist/chat/pure.d.ts +4 -0
- package/dist/chat/pure.js +3 -0
- package/dist/chat.d.ts +31 -661
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +5 -3
- package/dist/chat.js.map +1 -1
- package/dist/contexts/docker.d.ts +1 -1
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js.map +1 -1
- package/dist/{contexts-BOtMvzli.js → contexts-BD2U_xpi.js} +2 -2
- package/dist/{contexts-BOtMvzli.js.map → contexts-BD2U_xpi.js.map} +1 -1
- package/dist/contexts.d.ts +3 -3
- package/dist/contexts.js +1 -1
- package/dist/edit-utils-DnfNoj16.js +574 -0
- package/dist/edit-utils-DnfNoj16.js.map +1 -0
- package/dist/{errors-DdZXnyXE.js → errors-CoQnKRf1.js} +32 -2
- package/dist/{errors-DdZXnyXE.js.map → errors-CoQnKRf1.js.map} +1 -1
- package/dist/fetch-url-CPxfiXDa.js +518 -0
- package/dist/fetch-url-CPxfiXDa.js.map +1 -0
- package/dist/image-sniff-B7uFSNO1.js +90 -0
- package/dist/image-sniff-B7uFSNO1.js.map +1 -0
- package/dist/{index-CbS75MD3.d.ts → index-CZOwAJIX.d.ts} +2 -2
- package/dist/index-CZOwAJIX.d.ts.map +1 -0
- package/dist/{index-CTDMMdIy.d.ts → index-Ck_AWt8P.d.ts} +3 -4
- package/dist/index-Ck_AWt8P.d.ts.map +1 -0
- package/dist/{index-v3Tzobqr.d.ts → index-KiS7w0dC.d.ts} +3 -3
- package/dist/index-KiS7w0dC.d.ts.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +13 -12
- package/dist/index.js.map +1 -1
- package/dist/{interpolate-DM1UcKeQ.js → interpolate-TySiqKzc.js} +23 -23
- package/dist/{interpolate-DM1UcKeQ.js.map → interpolate-TySiqKzc.js.map} +1 -1
- package/dist/{login-7tHcckmX.js → login-BDeqENSe.js} +7 -58
- package/dist/login-BDeqENSe.js.map +1 -0
- package/dist/{mcp-DGeB7-3D.js → mcp-Kqzz-Rs_.js} +8 -6
- package/dist/mcp-Kqzz-Rs_.js.map +1 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +1 -1
- package/dist/{messages-Dym8S_YH.js → messages-CvRQTdbR.js} +118 -39
- package/dist/messages-CvRQTdbR.js.map +1 -0
- package/dist/{presets-w9Px_aAm.js → presets-JuOnSI-i.js} +2 -2
- package/dist/{presets-w9Px_aAm.js.map → presets-JuOnSI-i.js.map} +1 -1
- package/dist/presets.d.ts +3 -3
- package/dist/presets.js +1 -1
- package/dist/{providers-beXyD9W9.js → providers-h4HJPbbv.js} +485 -31
- package/dist/providers-h4HJPbbv.js.map +1 -0
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +3 -3
- package/dist/restate.d.ts +1 -1
- package/dist/restate.d.ts.map +1 -1
- package/dist/restate.js.map +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +1 -1
- package/dist/session/sqlite.js.map +1 -1
- package/dist/{session-BRIsmBSY.js → session-BzLou2_-.js} +2 -2
- package/dist/{session-BRIsmBSY.js.map → session-BzLou2_-.js.map} +1 -1
- package/dist/session.d.ts +2 -2
- package/dist/session.js +2 -2
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +1 -1
- package/dist/skills.js.map +1 -1
- package/dist/{stats-Lc3zL3RM.js → stats-DAKBEKjc.js} +12 -2
- package/dist/stats-DAKBEKjc.js.map +1 -0
- package/dist/{stdio-loader-EVAF5KlU.js → stdio-loader-Ce68wUmM.js} +4 -4
- package/dist/stdio-loader-Ce68wUmM.js.map +1 -0
- package/dist/tool-formatters-CU-j3a3e.d.ts +1471 -0
- package/dist/tool-formatters-CU-j3a3e.d.ts.map +1 -0
- package/dist/tools/fetch-url.d.ts +70 -0
- package/dist/tools/fetch-url.d.ts.map +1 -0
- package/dist/tools/fetch-url.js +2 -0
- package/dist/tools/web-search.d.ts +7 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +190 -0
- package/dist/tools/web-search.js.map +1 -0
- package/dist/{tools-DhrLrOEr.js → tools-BGtJK0vo.js} +1368 -421
- package/dist/tools-BGtJK0vo.js.map +1 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +1 -1
- package/dist/{turn-operations-UAkOjO-u.js → transcript-anchors-BTSZAPVc.js} +147 -2713
- package/dist/transcript-anchors-BTSZAPVc.js.map +1 -0
- package/dist/{transcript-anchors-D0TR6djV.d.ts → transcript-anchors-DX90kXc4.d.ts} +13 -1299
- package/dist/transcript-anchors-DX90kXc4.d.ts.map +1 -0
- package/dist/tui.d.ts +58 -28
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +1349 -422
- package/dist/tui.js.map +1 -1
- package/dist/turn-operations-CCHfR9eC.js +1938 -0
- package/dist/turn-operations-CCHfR9eC.js.map +1 -0
- package/dist/turn-operations-DDIl4YVk.d.ts +658 -0
- package/dist/turn-operations-DDIl4YVk.d.ts.map +1 -0
- package/dist/{types-oKPBdCmL.js → types-BPw_i5vb.js} +1 -1
- package/dist/types-BPw_i5vb.js.map +1 -0
- package/dist/{types-KukEp-mi.d.ts → types-CEAMIUXw.d.ts} +1 -1
- package/dist/types-CEAMIUXw.d.ts.map +1 -0
- package/dist/types.d.ts +4 -4
- package/dist/types.js +3 -3
- package/docs/CHAT.md +53 -6
- package/docs/SKILL.md +3 -0
- package/docs/TUI.md +7 -0
- package/package.json +18 -2
- package/dist/agent-ClkpElCZ.d.ts.map +0 -1
- package/dist/index-CTDMMdIy.d.ts.map +0 -1
- package/dist/index-CbS75MD3.d.ts.map +0 -1
- package/dist/index-v3Tzobqr.d.ts.map +0 -1
- package/dist/login-7tHcckmX.js.map +0 -1
- package/dist/mcp-DGeB7-3D.js.map +0 -1
- package/dist/messages-Dym8S_YH.js.map +0 -1
- package/dist/providers-beXyD9W9.js.map +0 -1
- package/dist/stats-Lc3zL3RM.js.map +0 -1
- package/dist/stdio-loader-EVAF5KlU.js.map +0 -1
- package/dist/tools-DhrLrOEr.js.map +0 -1
- package/dist/transcript-anchors-D0TR6djV.d.ts.map +0 -1
- package/dist/turn-operations-UAkOjO-u.js.map +0 -1
- package/dist/types-KukEp-mi.d.ts.map +0 -1
- package/dist/types-oKPBdCmL.js.map +0 -1
|
@@ -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 * 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
|
+
{"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 { p as toWireMessages } from "./messages-
|
|
1
|
+
import { p as toWireMessages } from "./messages-CvRQTdbR.js";
|
|
2
2
|
//#region src/session/file-map.ts
|
|
3
3
|
function toMeta(data) {
|
|
4
4
|
return {
|
|
@@ -477,4 +477,4 @@ function generateId() {
|
|
|
477
477
|
//#endregion
|
|
478
478
|
export { createFileMapStore as a, createMemoryStore as i, loadSession as n, createRemoteStore as r, createSession as t };
|
|
479
479
|
|
|
480
|
-
//# sourceMappingURL=session-
|
|
480
|
+
//# sourceMappingURL=session-BzLou2_-.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-BRIsmBSY.js","names":[],"sources":["../src/session/file-map.ts","../src/session/memory.ts","../src/session/remote.ts","../src/session/index.ts"],"sourcesContent":["/**\n * File-map session store.\n *\n * Wraps a narrow 3-method adapter (`get` / `save` / `delete`) that exchanges a flat\n * map of filename → string content. Useful for embedding zidane sessions inside\n * host-provided session backends that only speak in file maps (not zidane's native\n * `SessionStore` shape).\n *\n * Serialization format:\n * - `turns.jsonl` — one `SessionTurn` per line.\n * - `meta.json` — session metadata (id, agentId, status, runs, metadata, timestamps).\n *\n * JSONL for turns keeps history inspectable with tools like `jq` and resilient to\n * partial corruption — parse up to the first bad line and you still have a valid\n * prefix. Metadata lives in its own file so large turn logs don't bloat the\n * metadata path.\n *\n * Scope: each `createFileMapStore` handles a **single session** — the adapter's\n * file map holds at most one zidane session at a time. This matches how host SDKs\n * scope their session stores per conversation.\n *\n * Divergences from the built-in memory / sqlite stores:\n * - `appendTurns` / `updateStatus` / `updateRun` auto-create a minimal `SessionData`\n * record on first write, instead of silently no-oping when the session hasn't been\n * explicitly `save()`-ed. This matches the host-SDK integration path where\n * `createSession(...)` → `agent.run(...)` directly without an explicit `save()` call.\n * - `updateRun` inserts the run if not found in the cached record (rather than\n * silently dropping). Run records therefore always reach the adapter.\n */\n\nimport type { SessionData, SessionRun, SessionStore } from '.'\nimport type { SessionTurn } from '../types'\n\n/**\n * Host-provided file-map adapter. Three methods exchanging `Record<string, string>`\n * payloads — the whole persistence surface the wrapper needs.\n */\nexport interface FileMapAdapter {\n /** Load the current file map. Returns an empty `files` record when nothing is persisted. */\n get: () => Promise<{ files: Record<string, string> }>\n /** Replace the persisted file map. Full-rewrite semantics. */\n save: (files: Record<string, string>) => Promise<void>\n /** Delete all persisted state. */\n delete: () => Promise<void>\n}\n\nexport interface FileMapStoreOptions {\n /** Filename for the JSONL turns log. Default: `turns.jsonl`. */\n turnsFile?: string\n /** Filename for the metadata JSON. Default: `meta.json`. */\n metaFile?: string\n}\n\ninterface MetaShape {\n id: string\n agentId?: string\n runs: SessionRun[]\n status: SessionData['status']\n metadata: Record<string, unknown>\n createdAt: number\n updatedAt: number\n}\n\nfunction toMeta(data: SessionData): MetaShape {\n return {\n id: data.id,\n agentId: data.agentId,\n runs: data.runs,\n status: data.status,\n metadata: data.metadata,\n createdAt: data.createdAt,\n updatedAt: data.updatedAt,\n }\n}\n\nfunction toData(meta: MetaShape, turns: SessionTurn[]): SessionData {\n return {\n id: meta.id,\n agentId: meta.agentId,\n turns,\n runs: meta.runs,\n status: meta.status,\n metadata: meta.metadata,\n createdAt: meta.createdAt,\n updatedAt: meta.updatedAt,\n }\n}\n\nfunction parseTurnsJsonl(jsonl: string): SessionTurn[] {\n if (!jsonl)\n return []\n const turns: SessionTurn[] = []\n for (const line of jsonl.split('\\n')) {\n const trimmed = line.trim()\n if (!trimmed)\n continue\n try {\n turns.push(JSON.parse(trimmed) as SessionTurn)\n }\n catch {\n // Skip malformed lines — preserves the valid prefix on partial corruption.\n }\n }\n return turns\n}\n\nfunction serializeTurnsJsonl(turns: SessionTurn[]): string {\n if (turns.length === 0)\n return ''\n return `${turns.map(t => JSON.stringify(t)).join('\\n')}\\n`\n}\n\n/**\n * Create a single-session `SessionStore` backed by a file-map adapter.\n *\n * @example\n * ```ts\n * const session = await createSession({\n * store: createFileMapStore(hostSessionStore),\n * })\n * ```\n */\nexport function createFileMapStore(\n adapter: FileMapAdapter,\n options: FileMapStoreOptions = {},\n): SessionStore {\n const turnsFile = options.turnsFile ?? 'turns.jsonl'\n const metaFile = options.metaFile ?? 'meta.json'\n\n // Cached view of the persisted session. Populated lazily on first access so the\n // factory itself doesn't do I/O.\n let cached: SessionData | null = null\n let hydrated = false\n\n async function hydrate(): Promise<void> {\n if (hydrated)\n return\n const { files } = await adapter.get()\n const metaRaw = files[metaFile]\n if (metaRaw) {\n let meta: MetaShape | null = null\n try {\n meta = JSON.parse(metaRaw) as MetaShape\n }\n catch {\n meta = null\n }\n if (meta) {\n cached = toData(meta, parseTurnsJsonl(files[turnsFile] ?? ''))\n }\n }\n hydrated = true\n }\n\n async function persist(data: SessionData): Promise<void> {\n const meta = toMeta(data)\n await adapter.save({\n [metaFile]: JSON.stringify(meta, null, 2),\n [turnsFile]: serializeTurnsJsonl(data.turns),\n })\n }\n\n // Ensure `cached` exists for `sessionId`, creating a minimal record when first written.\n // Returns false when `cached` already holds a different sessionId (request ignored).\n async function ensureCachedFor(sessionId: string): Promise<boolean> {\n await hydrate()\n if (cached) {\n return cached.id === sessionId\n }\n const now = Date.now()\n cached = {\n id: sessionId,\n turns: [],\n runs: [],\n status: 'idle',\n metadata: {},\n createdAt: now,\n updatedAt: now,\n }\n hydrated = true\n return true\n }\n\n return {\n async load(sessionId: string): Promise<SessionData | null> {\n await hydrate()\n if (!cached || cached.id !== sessionId)\n return null\n return structuredClone(cached)\n },\n\n async save(data: SessionData): Promise<void> {\n cached = structuredClone(data)\n hydrated = true\n await persist(cached)\n },\n\n async delete(sessionId: string): Promise<void> {\n await hydrate()\n if (cached && cached.id !== sessionId)\n return\n cached = null\n await adapter.delete()\n },\n\n async list(filter): Promise<string[]> {\n await hydrate()\n if (!cached)\n return []\n if (filter?.agentId && cached.agentId !== filter.agentId)\n return []\n // file-map stores exactly one session, so the projectRoot filter\n // either keeps it (match / axis off) or drops it entirely.\n if (filter && 'projectRoot' in filter) {\n const v = filter.projectRoot\n if (v === null && cached.projectRoot != null)\n return []\n if (typeof v === 'string' && cached.projectRoot !== v)\n return []\n }\n return [cached.id]\n },\n\n async appendTurns(sessionId: string, turns: SessionTurn[]): Promise<void> {\n const ok = await ensureCachedFor(sessionId)\n if (!ok)\n return\n cached!.turns.push(...structuredClone(turns))\n cached!.updatedAt = Date.now()\n await persist(cached!)\n },\n\n async getTurns(sessionId: string, from = 0, limit?: number): Promise<SessionTurn[]> {\n await hydrate()\n if (!cached || cached.id !== sessionId)\n return []\n const slice = cached.turns.slice(from, limit !== undefined ? from + limit : undefined)\n return structuredClone(slice) as SessionTurn[]\n },\n\n async updateRun(sessionId: string, run: SessionRun): Promise<void> {\n const ok = await ensureCachedFor(sessionId)\n if (!ok)\n return\n const idx = cached!.runs.findIndex(r => r.id === run.id)\n if (idx >= 0)\n cached!.runs[idx] = structuredClone(run)\n else\n cached!.runs.push(structuredClone(run))\n cached!.updatedAt = Date.now()\n await persist(cached!)\n },\n\n async updateStatus(sessionId: string, status: SessionData['status']): Promise<void> {\n const ok = await ensureCachedFor(sessionId)\n if (!ok)\n return\n cached!.status = status\n cached!.updatedAt = Date.now()\n await persist(cached!)\n },\n }\n}\n","/**\n * In-memory session store.\n * Useful for development and testing. Data is lost when the process exits.\n */\n\nimport type { SessionData, SessionRun, SessionStore } from '.'\nimport type { SessionTurn } from '../types'\n\nexport function createMemoryStore(): SessionStore {\n const sessions = new Map<string, SessionData>()\n\n return {\n async load(sessionId: string) {\n const data = sessions.get(sessionId)\n return data ? structuredClone(data) : null\n },\n\n async save(session: SessionData) {\n sessions.set(session.id, structuredClone(session))\n },\n\n async delete(sessionId: string) {\n sessions.delete(sessionId)\n },\n\n async list(filter) {\n let ids = Array.from(sessions.keys())\n if (filter?.agentId) {\n ids = ids.filter(id => sessions.get(id)?.agentId === filter.agentId)\n }\n // `projectRoot` mirrors the SQLite store's tri-state contract:\n // string → only sessions tagged with that root\n // null → only UNTAGGED sessions (legacy / pre-v3 rows)\n // undefined → axis ignored (tagged + untagged both returned)\n if (filter && 'projectRoot' in filter) {\n const v = filter.projectRoot\n if (v === null)\n ids = ids.filter(id => sessions.get(id)?.projectRoot == null)\n else if (typeof v === 'string')\n ids = ids.filter(id => sessions.get(id)?.projectRoot === v)\n }\n if (filter?.limit) {\n ids = ids.slice(0, filter.limit)\n }\n return ids\n },\n\n async appendTurns(sessionId: string, turns: SessionTurn[]) {\n const data = sessions.get(sessionId)\n if (data) {\n data.turns.push(...structuredClone(turns))\n data.updatedAt = Date.now()\n }\n },\n\n async getTurns(sessionId: string, from = 0, limit?: number) {\n const data = sessions.get(sessionId)\n if (!data)\n return []\n const sliced = data.turns.slice(from, limit !== undefined ? from + limit : undefined)\n return structuredClone(sliced) as SessionTurn[]\n },\n\n async updateRun(sessionId: string, run: SessionRun) {\n const data = sessions.get(sessionId)\n if (data) {\n // Upsert — see the matching comment in sqlite.ts's\n // `txnUpdateRun` for the failure mode. tl;dr: the agent's\n // `startRun()` only updates in-memory state; the store first\n // sees the run via `updateRun()` at finalize time, so an\n // update-only impl silently drops every new run.\n const idx = data.runs.findIndex(r => r.id === run.id)\n if (idx >= 0)\n data.runs[idx] = structuredClone(run)\n else\n data.runs.push(structuredClone(run))\n data.updatedAt = Date.now()\n }\n },\n\n async updateStatus(sessionId: string, status: SessionData['status']) {\n const data = sessions.get(sessionId)\n if (data) {\n data.status = status\n data.updatedAt = Date.now()\n }\n },\n }\n}\n","/**\n * Remote session store via HTTP API.\n *\n * Expects a REST API with:\n * GET {url}/sessions/{id} -> SessionData | 404\n * PUT {url}/sessions/{id} -> save SessionData\n * DELETE {url}/sessions/{id} -> delete\n * GET {url}/sessions?agentId=&limit=&projectRoot= -> { ids: string[] }\n * `projectRoot=__null__` is the wire encoding for \"untagged only\".\n * POST {url}/sessions/{id}/turns -> append turns\n * GET {url}/sessions/{id}/turns?from=&limit= -> SessionTurn[]\n * PUT {url}/sessions/{id}/runs/{runId} -> update run\n * PATCH {url}/sessions/{id} -> { status }\n */\n\nimport type { SessionData, SessionRun, SessionStore } from '.'\nimport type { SessionTurn } from '../types'\n\nexport interface RemoteStoreOptions {\n /** Base URL of the session API */\n url: string\n /** Optional headers (e.g. for authentication) */\n headers?: Record<string, string>\n}\n\nconst TRAILING_SLASH = /\\/$/\n\nexport function createRemoteStore(options: RemoteStoreOptions): SessionStore {\n const baseUrl = options.url.replace(TRAILING_SLASH, '')\n const defaultHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...options.headers,\n }\n\n async function request(path: string, init?: RequestInit): Promise<Response> {\n const url = `${baseUrl}${path}`\n const res = await fetch(url, {\n ...init,\n headers: { ...defaultHeaders, ...init?.headers },\n })\n return res\n }\n\n return {\n async load(sessionId: string) {\n const res = await request(`/sessions/${encodeURIComponent(sessionId)}`)\n if (!res.ok) {\n if (res.status === 404)\n return null\n throw new Error(`Remote session load failed: ${res.status} ${res.statusText}`)\n }\n return await res.json() as SessionData\n },\n\n async save(session: SessionData) {\n const res = await request(`/sessions/${encodeURIComponent(session.id)}`, {\n method: 'PUT',\n body: JSON.stringify(session),\n })\n if (!res.ok) {\n throw new Error(`Remote session save failed: ${res.status} ${res.statusText}`)\n }\n },\n\n async delete(sessionId: string) {\n const res = await request(`/sessions/${encodeURIComponent(sessionId)}`, {\n method: 'DELETE',\n })\n if (!res.ok && res.status !== 404) {\n throw new Error(`Remote session delete failed: ${res.status} ${res.statusText}`)\n }\n },\n\n async list(filter) {\n const params = new URLSearchParams()\n if (filter?.agentId)\n params.set('agentId', filter.agentId)\n if (filter?.limit)\n params.set('limit', String(filter.limit))\n if (filter && 'projectRoot' in filter) {\n const v = filter.projectRoot\n if (v === null)\n params.set('projectRoot', '__null__')\n else if (typeof v === 'string')\n params.set('projectRoot', v)\n }\n\n const query = params.toString()\n const path = query ? `/sessions?${query}` : '/sessions'\n const res = await request(path)\n\n if (!res.ok) {\n throw new Error(`Remote session list failed: ${res.status} ${res.statusText}`)\n }\n\n const body = await res.json() as { ids: string[] }\n return body.ids\n },\n\n async appendTurns(sessionId: string, turns: SessionTurn[]) {\n const res = await request(`/sessions/${encodeURIComponent(sessionId)}/turns`, {\n method: 'POST',\n body: JSON.stringify(turns),\n })\n if (!res.ok) {\n throw new Error(`Remote appendTurns failed: ${res.status} ${res.statusText}`)\n }\n },\n\n async getTurns(sessionId: string, from = 0, limit?: number) {\n const params = new URLSearchParams()\n if (from)\n params.set('from', String(from))\n if (limit !== undefined)\n params.set('limit', String(limit))\n\n const query = params.toString()\n const path = `/sessions/${encodeURIComponent(sessionId)}/turns${query ? `?${query}` : ''}`\n const res = await request(path)\n\n if (!res.ok) {\n throw new Error(`Remote getTurns failed: ${res.status} ${res.statusText}`)\n }\n\n return await res.json() as SessionTurn[]\n },\n\n async updateRun(sessionId: string, run: SessionRun) {\n const res = await request(\n `/sessions/${encodeURIComponent(sessionId)}/runs/${encodeURIComponent(run.id)}`,\n {\n method: 'PUT',\n body: JSON.stringify(run),\n },\n )\n if (!res.ok) {\n throw new Error(`Remote updateRun failed: ${res.status} ${res.statusText}`)\n }\n },\n\n async updateStatus(sessionId: string, status: SessionData['status']) {\n const res = await request(`/sessions/${encodeURIComponent(sessionId)}`, {\n method: 'PATCH',\n body: JSON.stringify({ status }),\n })\n if (!res.ok) {\n throw new Error(`Remote updateStatus failed: ${res.status} ${res.statusText}`)\n }\n },\n }\n}\n","/**\n * Session management for agents.\n *\n * A session tracks identity, turn history, and run metadata.\n * Plug in any storage backend by implementing the SessionStore interface,\n * or use one of the built-in stores: memory, sqlite, remote.\n */\n\nimport type { SessionMessage, SessionTurn, TurnUsage } from '../types'\nimport type { ToWireMessagesOptions } from './messages'\nimport { toWireMessages } from './messages'\n\nexport type { SessionContentBlock, SessionMessage, SessionTurn } from '../types'\nexport { createFileMapStore } from './file-map'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SessionRun {\n id: string\n startedAt: number\n endedAt?: number\n prompt: string\n status: 'running' | 'completed' | 'aborted' | 'error'\n turns?: number\n tokensIn?: number\n tokensOut?: number\n error?: string\n /** Per-turn usage breakdown */\n turnUsage?: TurnUsage[]\n /** Total usage across all turns */\n totalUsage?: TurnUsage\n /** Estimated cost in USD */\n cost?: number\n /**\n * The run that spawned this one, when the agent is a subagent sharing its\n * parent's session. Undefined on top-level `agent.run()`. Consumers can walk\n * `runs` by `parentRunId` to reconstruct the subagent tree.\n */\n parentRunId?: string\n /**\n * Zero-based subagent depth. 0 = top-level run, 1 = direct child, …\n * Recorded here so hosts can query/filter by level without walking the tree.\n */\n depth?: number\n}\n\nexport interface SessionData {\n id: string\n agentId?: string\n /**\n * Absolute path of the project this session belongs to — typically\n * the git root resolved from `cwd` at creation time, falling back to\n * `cwd` itself when not in a git repo. Set ONCE on creation and never\n * mutated thereafter (the session \"belongs\" to that project forever).\n *\n * Used by the TUI's sessions list to filter rows by current project,\n * so the user only sees conversations relevant to where they are\n * working — without needing one `.{prefix}/` directory per project.\n *\n * `undefined` on pre-tagging legacy sessions; the chat layer treats\n * those as \"untagged\" and hides them unless `Settings.showAllProjects`\n * is on.\n */\n projectRoot?: string\n turns: SessionTurn[]\n runs: SessionRun[]\n status: 'idle' | 'running' | 'completed' | 'error'\n metadata: Record<string, unknown>\n createdAt: number\n updatedAt: number\n}\n\n// ---------------------------------------------------------------------------\n// SessionStore interface (pluggable backend)\n// ---------------------------------------------------------------------------\n\nexport interface SessionStore {\n /** Optional: generate a session ID server-side (e.g. Supabase UUID). */\n generateSessionId?: () => string | Promise<string>\n\n /** Optional: generate a turn ID server-side. */\n generateTurnId?: () => string | Promise<string>\n\n /** Load a session by ID. Returns null if not found. */\n load: (sessionId: string) => Promise<SessionData | null>\n\n /** Save a session (create or update, full document). */\n save: (session: SessionData) => Promise<void>\n\n /** Delete a session. */\n delete: (sessionId: string) => Promise<void>\n\n /**\n * List session IDs, optionally filtered. `projectRoot` restricts to\n * sessions whose `SessionData.projectRoot` matches exactly — untagged\n * (legacy) sessions are NOT returned under that filter; pass `null`\n * explicitly to ask for untagged ones, or omit the field to ignore\n * the axis entirely. `agentId` filters by recorded agent; the two\n * conditions AND together when both are set.\n */\n list: (filter?: { agentId?: string, limit?: number, projectRoot?: string | null }) => Promise<string[]>\n\n /** Append new turns to a session (incremental, avoids full re-save). */\n appendTurns: (sessionId: string, turns: SessionTurn[]) => Promise<void>\n\n /** Return a slice of turns for a session. */\n getTurns: (sessionId: string, from?: number, limit?: number) => Promise<SessionTurn[]>\n\n /**\n * Persist a run record (called after completeRun / abortRun / errorRun).\n *\n * **Upsert semantics required.** The agent's run lifecycle calls\n * `session.startRun()` to register a new run in IN-MEMORY state and\n * then `updateRun()` at finalize. The store doesn't see the run any\n * earlier — `appendTurns` is the only intermediate call, and it\n * persists turns but not runs. An update-only implementation would\n * silently drop every newly-created run record at finalize, leaving\n * the on-disk `runs[]` missing entries that the persisted turns\n * reference (which `eventsFromTurns` then mis-classifies via\n * `ancestryOf` → broken depth, broken `child-N` labels, broken\n * `userPrompts` history).\n *\n * Implementations MUST insert when the id doesn't exist and update\n * when it does. The bundled `sqlite` / `memory` / `file-map` stores\n * all follow this contract; remote stores using HTTP PUT get upsert\n * for free via REST semantics.\n */\n updateRun: (sessionId: string, run: SessionRun) => Promise<void>\n\n /** Update the top-level status of a session. */\n updateStatus: (sessionId: string, status: SessionData['status']) => Promise<void>\n}\n\n// ---------------------------------------------------------------------------\n// Session (live instance wrapping a SessionData)\n// ---------------------------------------------------------------------------\n\nexport interface Session {\n /** Session ID */\n readonly id: string\n\n /** Agent ID (optional label) */\n readonly agentId?: string\n\n /**\n * Project this session was created under — see {@link SessionData.projectRoot}.\n * Set once on creation; surfaces here for read-only inspection.\n */\n readonly projectRoot?: string\n\n /**\n * Raw persisted turn history — kept pristine for fidelity (the model's\n * reasoning chain stays intact across reloads).\n *\n * **DO NOT pass this directly to a provider request.** Mid-pair\n * persistence (interrupted runs, partial-result writes, compaction\n * boundaries) leaves orphan `tool_use` / `tool_result` blocks here that\n * every provider 400s on (`tool_result must be preceded by a tool_call\n * with the same toolCallId`). The agent's own loop repairs these\n * just-in-time at wire-send time and never writes the repair back to\n * `turns`.\n *\n * For provider requests built outside `agent.run()`, use\n * {@link Session.toMessages} — it applies the same defensive pairing\n * repair and returns a guaranteed-safe `SessionMessage[]`.\n */\n readonly turns: SessionTurn[]\n\n /**\n * True when this session has no turns yet.\n *\n * Use this as a first-prompt signal when setting up a run — e.g. writing initial\n * configuration only on fresh sessions. Equivalent to `turns.length === 0`.\n */\n readonly isEmpty: boolean\n\n /** Top-level session status */\n readonly status: SessionData['status']\n\n /** All runs in this session */\n readonly runs: SessionRun[]\n\n /** Arbitrary metadata */\n readonly metadata: Record<string, unknown>\n\n /**\n * Start tracking a new run. `extras.parentRunId` + `extras.depth` are\n * populated by the spawn tool when a child agent shares its parent's\n * session; regular top-level `agent.run()` calls omit them.\n */\n startRun: (runId: string, prompt?: string, extras?: { parentRunId?: string, depth?: number }) => void\n\n /** Mark a run as completed */\n completeRun: (runId: string, stats: { turns: number, tokensIn: number, tokensOut: number, turnUsage?: TurnUsage[], cost?: number }) => void\n\n /** Mark a run as aborted */\n /**\n * Optional `stats` lets the agent backfill the run's token totals when\n * the abort happened *after* the loop accumulated meaningful usage —\n * common when the user presses esc mid-streaming. Without it, the run\n * record reads `0 in / 0 out` on reload regardless of how much was\n * spent before the abort. Same shape as `completeRun`'s stats so the\n * persisted `totalUsage` aggregate stays consistent across paths.\n */\n abortRun: (runId: string, stats?: { turns: number, tokensIn: number, tokensOut: number, turnUsage?: TurnUsage[], cost?: number }) => void\n\n /** Mark a run as errored */\n /** Optional `stats` — same rationale as `abortRun.stats`. */\n errorRun: (runId: string, error: string, stats?: { turns: number, tokensIn: number, tokensOut: number, turnUsage?: TurnUsage[], cost?: number }) => void\n\n /** Append turns to in-memory history AND persist via store.appendTurns (if store present) */\n appendTurns: (turns: SessionTurn[]) => Promise<void>\n\n /** Replace all turns in-memory (does not persist — use save() for that) */\n setTurns: (turns: SessionTurn[]) => void\n\n /**\n * Replace all runs in-memory (does not persist — use save() for that).\n * Mirrors {@link setTurns} for the fork / restore case: callers that\n * bootstrap a session from an externally-derived snapshot (e.g.\n * `onForkTurn` copying parent runs into a child session) need this so\n * the cloned runs land in `data.runs` before the first `save()`.\n * Production agent runs continue to mutate runs via `startRun` /\n * `completeRun` / `updateRun`; this is the bulk-replace escape hatch.\n */\n setRuns: (runs: SessionRun[]) => void\n\n /** Update the session status in memory AND via store.updateStatus (if store present) */\n updateStatus: (status: SessionData['status']) => Promise<void>\n\n /** Persist an updated run record via store.updateRun (if store present) */\n updateRun: (run: SessionRun) => Promise<void>\n\n /** Generate a turn ID using store.generateTurnId if available, else crypto.randomUUID() */\n generateTurnId: () => string | Promise<string>\n\n /** Set metadata key */\n setMeta: (key: string, value: unknown) => void\n\n /** Persist the full session document to the store */\n save: () => Promise<void>\n\n /**\n * Project the session's persisted turns into a wire-ready\n * `SessionMessage[]` safe to hand to any provider.\n *\n * The canonical answer to \"I need to send these turns to Anthropic /\n * OpenAI from outside `agent.run()`\" (sub-agent classifier, side-channel\n * summarizer, replay worker, …). Runs the same defensive pairing repair\n * the agent loop applies just-in-time at wire-send time\n * ({@link ensureToolResultPairing}), so orphan `tool_use` blocks and\n * dangling `tool_result`s never reach the provider as the 400\n * \"tool_result must be preceded by a tool_call with the same toolCallId\".\n *\n * Pass `provider` to also enforce the user-tail invariant\n * ({@link ensureEndsWithUserMessage}) — required for opus 4.7 / o-series\n * style models that reject assistant-prefill.\n *\n * Pure + idempotent. Does NOT mutate `session.turns`. **Do not write the\n * result back via `setTurns` / `appendTurns`** — synthetic placeholders\n * inserted by the repair will contaminate persisted history.\n */\n toMessages: (options?: ToWireMessagesOptions) => SessionMessage[]\n\n /** Serialize to SessionData */\n toJSON: () => SessionData\n}\n\n// ---------------------------------------------------------------------------\n// createSession\n// ---------------------------------------------------------------------------\n\nexport interface CreateSessionOptions {\n /** Session ID. If omitted and store provides generateSessionId, that is used. */\n id?: string\n /** Agent ID label */\n agentId?: string\n /**\n * Project tag — see {@link SessionData.projectRoot}. Stamped once on\n * creation; ignored when `_data` is set (restoring an existing\n * session preserves whatever was already persisted there). The TUI\n * resolves this from `findGitRoot(cwd) ?? cwd` so sessions started\n * from the same repo (no matter which subdir) share one tag.\n */\n projectRoot?: string\n /** Initial metadata */\n metadata?: Record<string, unknown>\n /** Storage backend (optional, enables save/load) */\n store?: SessionStore\n // @internal: restore from existing data (bypasses id/agentId/metadata options)\n _data?: SessionData\n}\n\n/**\n * Create a new session.\n * Async so stores that generate IDs server-side (e.g. Supabase) can be supported.\n */\nexport async function createSession(options: CreateSessionOptions = {}): Promise<Session> {\n const store = options.store\n const now = Date.now()\n\n let sessionId = options.id\n if (!sessionId && store?.generateSessionId) {\n sessionId = await store.generateSessionId()\n }\n if (!sessionId) {\n sessionId = generateId()\n }\n\n const data: SessionData = options._data ?? {\n id: sessionId,\n agentId: options.agentId,\n // Stamp the project tag at creation only — restored sessions (the\n // `_data` branch above) keep whatever was already persisted, even\n // if `options.projectRoot` differs. A session's project identity\n // is sticky for its lifetime; loading the same session from a\n // different cwd doesn't re-home it.\n ...(options.projectRoot ? { projectRoot: options.projectRoot } : {}),\n turns: [],\n runs: [],\n status: 'idle',\n metadata: options.metadata ?? {},\n createdAt: now,\n updatedAt: now,\n }\n\n function touch() {\n data.updatedAt = Date.now()\n }\n\n function findRun(runId: string): SessionRun | undefined {\n return data.runs.find(r => r.id === runId)\n }\n\n /**\n * Apply per-run usage stats onto a SessionRun. Shared by `completeRun`,\n * `abortRun`, and `errorRun` so the on-disk shape stays consistent across\n * exit paths — historically only `completeRun` filled these in, and\n * aborted/errored runs rendered `0 in / 0 out` on reload regardless of\n * how much was actually consumed.\n */\n function applyRunStats(run: SessionRun, stats: { turns: number, tokensIn: number, tokensOut: number, turnUsage?: TurnUsage[], cost?: number }) {\n run.turns = stats.turns\n run.tokensIn = stats.tokensIn\n run.tokensOut = stats.tokensOut\n if (stats.turnUsage) {\n run.turnUsage = stats.turnUsage\n const total = stats.turnUsage.reduce((acc, t) => ({\n input: acc.input + t.input,\n output: acc.output + t.output,\n cacheCreation: (acc.cacheCreation ?? 0) + (t.cacheCreation ?? 0),\n cacheRead: (acc.cacheRead ?? 0) + (t.cacheRead ?? 0),\n thinking: (acc.thinking ?? 0) + (t.thinking ?? 0),\n }), { input: 0, output: 0, cacheCreation: 0, cacheRead: 0, thinking: 0 })\n run.totalUsage = {\n input: total.input,\n output: total.output,\n ...(total.cacheCreation ? { cacheCreation: total.cacheCreation } : {}),\n ...(total.cacheRead ? { cacheRead: total.cacheRead } : {}),\n ...(total.thinking ? { thinking: total.thinking } : {}),\n }\n }\n if (stats.cost !== undefined)\n run.cost = stats.cost\n }\n\n const session: Session = {\n get id() { return data.id },\n get agentId() { return data.agentId },\n get projectRoot() { return data.projectRoot },\n get turns() { return data.turns },\n get isEmpty() { return data.turns.length === 0 },\n get status() { return data.status },\n get runs() { return data.runs },\n get metadata() { return data.metadata },\n\n startRun(runId: string, prompt?: string, extras?: { parentRunId?: string, depth?: number }) {\n data.runs.push({\n id: runId,\n startedAt: Date.now(),\n prompt: prompt ?? '',\n status: 'running',\n ...(extras?.parentRunId ? { parentRunId: extras.parentRunId } : {}),\n ...(typeof extras?.depth === 'number' ? { depth: extras.depth } : {}),\n })\n touch()\n },\n\n completeRun(runId: string, stats: { turns: number, tokensIn: number, tokensOut: number, turnUsage?: TurnUsage[], cost?: number }) {\n const run = findRun(runId)\n if (run) {\n run.status = 'completed'\n run.endedAt = Date.now()\n applyRunStats(run, stats)\n }\n touch()\n },\n\n abortRun(runId: string, stats?: { turns: number, tokensIn: number, tokensOut: number, turnUsage?: TurnUsage[], cost?: number }) {\n const run = findRun(runId)\n if (run) {\n run.status = 'aborted'\n run.endedAt = Date.now()\n // Backfill tokens when available so an aborted run's session\n // ledger reflects actual consumption rather than `0 in / 0 out`.\n if (stats)\n applyRunStats(run, stats)\n }\n touch()\n },\n\n errorRun(runId: string, error: string, stats?: { turns: number, tokensIn: number, tokensOut: number, turnUsage?: TurnUsage[], cost?: number }) {\n const run = findRun(runId)\n if (run) {\n run.status = 'error'\n run.endedAt = Date.now()\n run.error = error\n if (stats)\n applyRunStats(run, stats)\n }\n touch()\n },\n\n async appendTurns(turns: SessionTurn[]) {\n data.turns.push(...turns)\n touch()\n if (store) {\n await store.appendTurns(data.id, turns)\n }\n },\n\n setTurns(turns: SessionTurn[]) {\n data.turns = turns\n touch()\n },\n\n setRuns(runs: SessionRun[]) {\n data.runs = runs\n touch()\n },\n\n async updateStatus(status: SessionData['status']) {\n data.status = status\n touch()\n if (store) {\n await store.updateStatus(data.id, status)\n }\n },\n\n async updateRun(run: SessionRun) {\n if (store) {\n await store.updateRun(data.id, run)\n }\n },\n\n generateTurnId() {\n if (store?.generateTurnId) {\n return store.generateTurnId()\n }\n return crypto.randomUUID()\n },\n\n setMeta(key: string, value: unknown) {\n data.metadata[key] = value\n touch()\n },\n\n async save() {\n if (!store) {\n throw new Error('No SessionStore configured. Pass a store to createSession() to enable persistence.')\n }\n await store.save(data)\n },\n\n toMessages(options?: ToWireMessagesOptions) {\n return toWireMessages(data.turns, options)\n },\n\n toJSON() {\n return structuredClone(data)\n },\n }\n\n return session\n}\n\n/**\n * Load an existing session from a store.\n */\nexport async function loadSession(store: SessionStore, sessionId: string): Promise<Session | null> {\n const loaded = await store.load(sessionId)\n if (!loaded)\n return null\n\n return createSession({ store, _data: loaded })\n}\n\n// ---------------------------------------------------------------------------\n// Re-export stores\n// ---------------------------------------------------------------------------\n\nexport type { FileMapAdapter, FileMapStoreOptions } from './file-map'\nexport { createMemoryStore } from './memory'\nexport { autoDetectAndConvert, fromAnthropic, fromOpenAI, toAnthropic, toOpenAI, toWireMessages } from './messages'\nexport type { ToWireMessagesOptions } from './messages'\nexport { createRemoteStore } from './remote'\nexport type { RemoteStoreOptions } from './remote'\n\n// NOTE: `createSqliteStore` is intentionally NOT re-exported here. It lives behind\n// the dedicated `zidane/session/sqlite` subpath so that non-Bun consumers don't\n// transitively evaluate `bun:sqlite` when they import from `zidane/session`.\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction generateId(): string {\n return `ses_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"session-BzLou2_-.js","names":[],"sources":["../src/session/file-map.ts","../src/session/memory.ts","../src/session/remote.ts","../src/session/index.ts"],"sourcesContent":["/**\n * File-map session store.\n *\n * Wraps a narrow 3-method adapter (`get` / `save` / `delete`) that exchanges a flat\n * map of filename → string content. Useful for embedding zidane sessions inside\n * host-provided session backends that only speak in file maps (not zidane's native\n * `SessionStore` shape).\n *\n * Serialization format:\n * - `turns.jsonl` — one `SessionTurn` per line.\n * - `meta.json` — session metadata (id, agentId, status, runs, metadata, timestamps).\n *\n * JSONL for turns keeps history inspectable with tools like `jq` and resilient to\n * partial corruption — parse up to the first bad line and you still have a valid\n * prefix. Metadata lives in its own file so large turn logs don't bloat the\n * metadata path.\n *\n * Scope: each `createFileMapStore` handles a **single session** — the adapter's\n * file map holds at most one zidane session at a time. This matches how host SDKs\n * scope their session stores per conversation.\n *\n * Divergences from the built-in memory / sqlite stores:\n * - `appendTurns` / `updateStatus` / `updateRun` auto-create a minimal `SessionData`\n * record on first write, instead of silently no-oping when the session hasn't been\n * explicitly `save()`-ed. This matches the host-SDK integration path where\n * `createSession(...)` → `agent.run(...)` directly without an explicit `save()` call.\n * - `updateRun` inserts the run if not found in the cached record (rather than\n * silently dropping). Run records therefore always reach the adapter.\n */\n\nimport type { SessionData, SessionRun, SessionStore } from '.'\nimport type { SessionTurn } from '../types'\n\n/**\n * Host-provided file-map adapter. Three methods exchanging `Record<string, string>`\n * payloads — the whole persistence surface the wrapper needs.\n */\nexport interface FileMapAdapter {\n /** Load the current file map. Returns an empty `files` record when nothing is persisted. */\n get: () => Promise<{ files: Record<string, string> }>\n /** Replace the persisted file map. Full-rewrite semantics. */\n save: (files: Record<string, string>) => Promise<void>\n /** Delete all persisted state. */\n delete: () => Promise<void>\n}\n\nexport interface FileMapStoreOptions {\n /** Filename for the JSONL turns log. Default: `turns.jsonl`. */\n turnsFile?: string\n /** Filename for the metadata JSON. Default: `meta.json`. */\n metaFile?: string\n}\n\ninterface MetaShape {\n id: string\n agentId?: string\n runs: SessionRun[]\n status: SessionData['status']\n metadata: Record<string, unknown>\n createdAt: number\n updatedAt: number\n}\n\nfunction toMeta(data: SessionData): MetaShape {\n return {\n id: data.id,\n agentId: data.agentId,\n runs: data.runs,\n status: data.status,\n metadata: data.metadata,\n createdAt: data.createdAt,\n updatedAt: data.updatedAt,\n }\n}\n\nfunction toData(meta: MetaShape, turns: SessionTurn[]): SessionData {\n return {\n id: meta.id,\n agentId: meta.agentId,\n turns,\n runs: meta.runs,\n status: meta.status,\n metadata: meta.metadata,\n createdAt: meta.createdAt,\n updatedAt: meta.updatedAt,\n }\n}\n\nfunction parseTurnsJsonl(jsonl: string): SessionTurn[] {\n if (!jsonl)\n return []\n const turns: SessionTurn[] = []\n for (const line of jsonl.split('\\n')) {\n const trimmed = line.trim()\n if (!trimmed)\n continue\n try {\n turns.push(JSON.parse(trimmed) as SessionTurn)\n }\n catch {\n // Skip malformed lines — preserves the valid prefix on partial corruption.\n }\n }\n return turns\n}\n\nfunction serializeTurnsJsonl(turns: SessionTurn[]): string {\n if (turns.length === 0)\n return ''\n return `${turns.map(t => JSON.stringify(t)).join('\\n')}\\n`\n}\n\n/**\n * Create a single-session `SessionStore` backed by a file-map adapter.\n *\n * @example\n * ```ts\n * const session = await createSession({\n * store: createFileMapStore(hostSessionStore),\n * })\n * ```\n */\nexport function createFileMapStore(\n adapter: FileMapAdapter,\n options: FileMapStoreOptions = {},\n): SessionStore {\n const turnsFile = options.turnsFile ?? 'turns.jsonl'\n const metaFile = options.metaFile ?? 'meta.json'\n\n // Cached view of the persisted session. Populated lazily on first access so the\n // factory itself doesn't do I/O.\n let cached: SessionData | null = null\n let hydrated = false\n\n async function hydrate(): Promise<void> {\n if (hydrated)\n return\n const { files } = await adapter.get()\n const metaRaw = files[metaFile]\n if (metaRaw) {\n let meta: MetaShape | null = null\n try {\n meta = JSON.parse(metaRaw) as MetaShape\n }\n catch {\n meta = null\n }\n if (meta) {\n cached = toData(meta, parseTurnsJsonl(files[turnsFile] ?? ''))\n }\n }\n hydrated = true\n }\n\n async function persist(data: SessionData): Promise<void> {\n const meta = toMeta(data)\n await adapter.save({\n [metaFile]: JSON.stringify(meta, null, 2),\n [turnsFile]: serializeTurnsJsonl(data.turns),\n })\n }\n\n // Ensure `cached` exists for `sessionId`, creating a minimal record when first written.\n // Returns false when `cached` already holds a different sessionId (request ignored).\n async function ensureCachedFor(sessionId: string): Promise<boolean> {\n await hydrate()\n if (cached) {\n return cached.id === sessionId\n }\n const now = Date.now()\n cached = {\n id: sessionId,\n turns: [],\n runs: [],\n status: 'idle',\n metadata: {},\n createdAt: now,\n updatedAt: now,\n }\n hydrated = true\n return true\n }\n\n return {\n async load(sessionId: string): Promise<SessionData | null> {\n await hydrate()\n if (!cached || cached.id !== sessionId)\n return null\n return structuredClone(cached)\n },\n\n async save(data: SessionData): Promise<void> {\n cached = structuredClone(data)\n hydrated = true\n await persist(cached)\n },\n\n async delete(sessionId: string): Promise<void> {\n await hydrate()\n if (cached && cached.id !== sessionId)\n return\n cached = null\n await adapter.delete()\n },\n\n async list(filter): Promise<string[]> {\n await hydrate()\n if (!cached)\n return []\n if (filter?.agentId && cached.agentId !== filter.agentId)\n return []\n // file-map stores exactly one session, so the projectRoot filter\n // either keeps it (match / axis off) or drops it entirely.\n if (filter && 'projectRoot' in filter) {\n const v = filter.projectRoot\n if (v === null && cached.projectRoot != null)\n return []\n if (typeof v === 'string' && cached.projectRoot !== v)\n return []\n }\n return [cached.id]\n },\n\n async appendTurns(sessionId: string, turns: SessionTurn[]): Promise<void> {\n const ok = await ensureCachedFor(sessionId)\n if (!ok)\n return\n cached!.turns.push(...structuredClone(turns))\n cached!.updatedAt = Date.now()\n await persist(cached!)\n },\n\n async getTurns(sessionId: string, from = 0, limit?: number): Promise<SessionTurn[]> {\n await hydrate()\n if (!cached || cached.id !== sessionId)\n return []\n const slice = cached.turns.slice(from, limit !== undefined ? from + limit : undefined)\n return structuredClone(slice) as SessionTurn[]\n },\n\n async updateRun(sessionId: string, run: SessionRun): Promise<void> {\n const ok = await ensureCachedFor(sessionId)\n if (!ok)\n return\n const idx = cached!.runs.findIndex(r => r.id === run.id)\n if (idx >= 0)\n cached!.runs[idx] = structuredClone(run)\n else\n cached!.runs.push(structuredClone(run))\n cached!.updatedAt = Date.now()\n await persist(cached!)\n },\n\n async updateStatus(sessionId: string, status: SessionData['status']): Promise<void> {\n const ok = await ensureCachedFor(sessionId)\n if (!ok)\n return\n cached!.status = status\n cached!.updatedAt = Date.now()\n await persist(cached!)\n },\n }\n}\n","/**\n * In-memory session store.\n * Useful for development and testing. Data is lost when the process exits.\n */\n\nimport type { SessionData, SessionRun, SessionStore } from '.'\nimport type { SessionTurn } from '../types'\n\nexport function createMemoryStore(): SessionStore {\n const sessions = new Map<string, SessionData>()\n\n return {\n async load(sessionId: string) {\n const data = sessions.get(sessionId)\n return data ? structuredClone(data) : null\n },\n\n async save(session: SessionData) {\n sessions.set(session.id, structuredClone(session))\n },\n\n async delete(sessionId: string) {\n sessions.delete(sessionId)\n },\n\n async list(filter) {\n let ids = Array.from(sessions.keys())\n if (filter?.agentId) {\n ids = ids.filter(id => sessions.get(id)?.agentId === filter.agentId)\n }\n // `projectRoot` mirrors the SQLite store's tri-state contract:\n // string → only sessions tagged with that root\n // null → only UNTAGGED sessions (legacy / pre-v3 rows)\n // undefined → axis ignored (tagged + untagged both returned)\n if (filter && 'projectRoot' in filter) {\n const v = filter.projectRoot\n if (v === null)\n ids = ids.filter(id => sessions.get(id)?.projectRoot == null)\n else if (typeof v === 'string')\n ids = ids.filter(id => sessions.get(id)?.projectRoot === v)\n }\n if (filter?.limit) {\n ids = ids.slice(0, filter.limit)\n }\n return ids\n },\n\n async appendTurns(sessionId: string, turns: SessionTurn[]) {\n const data = sessions.get(sessionId)\n if (data) {\n data.turns.push(...structuredClone(turns))\n data.updatedAt = Date.now()\n }\n },\n\n async getTurns(sessionId: string, from = 0, limit?: number) {\n const data = sessions.get(sessionId)\n if (!data)\n return []\n const sliced = data.turns.slice(from, limit !== undefined ? from + limit : undefined)\n return structuredClone(sliced) as SessionTurn[]\n },\n\n async updateRun(sessionId: string, run: SessionRun) {\n const data = sessions.get(sessionId)\n if (data) {\n // Upsert — see the matching comment in sqlite.ts's\n // `txnUpdateRun` for the failure mode. tl;dr: the agent's\n // `startRun()` only updates in-memory state; the store first\n // sees the run via `updateRun()` at finalize time, so an\n // update-only impl silently drops every new run.\n const idx = data.runs.findIndex(r => r.id === run.id)\n if (idx >= 0)\n data.runs[idx] = structuredClone(run)\n else\n data.runs.push(structuredClone(run))\n data.updatedAt = Date.now()\n }\n },\n\n async updateStatus(sessionId: string, status: SessionData['status']) {\n const data = sessions.get(sessionId)\n if (data) {\n data.status = status\n data.updatedAt = Date.now()\n }\n },\n }\n}\n","/**\n * Remote session store via HTTP API.\n *\n * Expects a REST API with:\n * GET {url}/sessions/{id} -> SessionData | 404\n * PUT {url}/sessions/{id} -> save SessionData\n * DELETE {url}/sessions/{id} -> delete\n * GET {url}/sessions?agentId=&limit=&projectRoot= -> { ids: string[] }\n * `projectRoot=__null__` is the wire encoding for \"untagged only\".\n * POST {url}/sessions/{id}/turns -> append turns\n * GET {url}/sessions/{id}/turns?from=&limit= -> SessionTurn[]\n * PUT {url}/sessions/{id}/runs/{runId} -> update run\n * PATCH {url}/sessions/{id} -> { status }\n */\n\nimport type { SessionData, SessionRun, SessionStore } from '.'\nimport type { SessionTurn } from '../types'\n\nexport interface RemoteStoreOptions {\n /** Base URL of the session API */\n url: string\n /** Optional headers (e.g. for authentication) */\n headers?: Record<string, string>\n}\n\nconst TRAILING_SLASH = /\\/$/\n\nexport function createRemoteStore(options: RemoteStoreOptions): SessionStore {\n const baseUrl = options.url.replace(TRAILING_SLASH, '')\n const defaultHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...options.headers,\n }\n\n async function request(path: string, init?: RequestInit): Promise<Response> {\n const url = `${baseUrl}${path}`\n const res = await fetch(url, {\n ...init,\n headers: { ...defaultHeaders, ...init?.headers },\n })\n return res\n }\n\n return {\n async load(sessionId: string) {\n const res = await request(`/sessions/${encodeURIComponent(sessionId)}`)\n if (!res.ok) {\n if (res.status === 404)\n return null\n throw new Error(`Remote session load failed: ${res.status} ${res.statusText}`)\n }\n return await res.json() as SessionData\n },\n\n async save(session: SessionData) {\n const res = await request(`/sessions/${encodeURIComponent(session.id)}`, {\n method: 'PUT',\n body: JSON.stringify(session),\n })\n if (!res.ok) {\n throw new Error(`Remote session save failed: ${res.status} ${res.statusText}`)\n }\n },\n\n async delete(sessionId: string) {\n const res = await request(`/sessions/${encodeURIComponent(sessionId)}`, {\n method: 'DELETE',\n })\n if (!res.ok && res.status !== 404) {\n throw new Error(`Remote session delete failed: ${res.status} ${res.statusText}`)\n }\n },\n\n async list(filter) {\n const params = new URLSearchParams()\n if (filter?.agentId)\n params.set('agentId', filter.agentId)\n if (filter?.limit)\n params.set('limit', String(filter.limit))\n if (filter && 'projectRoot' in filter) {\n const v = filter.projectRoot\n if (v === null)\n params.set('projectRoot', '__null__')\n else if (typeof v === 'string')\n params.set('projectRoot', v)\n }\n\n const query = params.toString()\n const path = query ? `/sessions?${query}` : '/sessions'\n const res = await request(path)\n\n if (!res.ok) {\n throw new Error(`Remote session list failed: ${res.status} ${res.statusText}`)\n }\n\n const body = await res.json() as { ids: string[] }\n return body.ids\n },\n\n async appendTurns(sessionId: string, turns: SessionTurn[]) {\n const res = await request(`/sessions/${encodeURIComponent(sessionId)}/turns`, {\n method: 'POST',\n body: JSON.stringify(turns),\n })\n if (!res.ok) {\n throw new Error(`Remote appendTurns failed: ${res.status} ${res.statusText}`)\n }\n },\n\n async getTurns(sessionId: string, from = 0, limit?: number) {\n const params = new URLSearchParams()\n if (from)\n params.set('from', String(from))\n if (limit !== undefined)\n params.set('limit', String(limit))\n\n const query = params.toString()\n const path = `/sessions/${encodeURIComponent(sessionId)}/turns${query ? `?${query}` : ''}`\n const res = await request(path)\n\n if (!res.ok) {\n throw new Error(`Remote getTurns failed: ${res.status} ${res.statusText}`)\n }\n\n return await res.json() as SessionTurn[]\n },\n\n async updateRun(sessionId: string, run: SessionRun) {\n const res = await request(\n `/sessions/${encodeURIComponent(sessionId)}/runs/${encodeURIComponent(run.id)}`,\n {\n method: 'PUT',\n body: JSON.stringify(run),\n },\n )\n if (!res.ok) {\n throw new Error(`Remote updateRun failed: ${res.status} ${res.statusText}`)\n }\n },\n\n async updateStatus(sessionId: string, status: SessionData['status']) {\n const res = await request(`/sessions/${encodeURIComponent(sessionId)}`, {\n method: 'PATCH',\n body: JSON.stringify({ status }),\n })\n if (!res.ok) {\n throw new Error(`Remote updateStatus failed: ${res.status} ${res.statusText}`)\n }\n },\n }\n}\n","/**\n * Session management for agents.\n *\n * A session tracks identity, turn history, and run metadata.\n * Plug in any storage backend by implementing the SessionStore interface,\n * or use one of the built-in stores: memory, sqlite, remote.\n */\n\nimport type { SessionMessage, SessionTurn, TurnUsage } from '../types'\nimport type { ToWireMessagesOptions } from './messages'\nimport { toWireMessages } from './messages'\n\nexport type { SessionContentBlock, SessionMessage, SessionTurn } from '../types'\nexport { createFileMapStore } from './file-map'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SessionRun {\n id: string\n startedAt: number\n endedAt?: number\n prompt: string\n status: 'running' | 'completed' | 'aborted' | 'error'\n turns?: number\n tokensIn?: number\n tokensOut?: number\n error?: string\n /** Per-turn usage breakdown */\n turnUsage?: TurnUsage[]\n /** Total usage across all turns */\n totalUsage?: TurnUsage\n /** Estimated cost in USD */\n cost?: number\n /**\n * The run that spawned this one, when the agent is a subagent sharing its\n * parent's session. Undefined on top-level `agent.run()`. Consumers can walk\n * `runs` by `parentRunId` to reconstruct the subagent tree.\n */\n parentRunId?: string\n /**\n * Zero-based subagent depth. 0 = top-level run, 1 = direct child, …\n * Recorded here so hosts can query/filter by level without walking the tree.\n */\n depth?: number\n}\n\nexport interface SessionData {\n id: string\n agentId?: string\n /**\n * Absolute path of the project this session belongs to — typically\n * the git root resolved from `cwd` at creation time, falling back to\n * `cwd` itself when not in a git repo. Set ONCE on creation and never\n * mutated thereafter (the session \"belongs\" to that project forever).\n *\n * Used by the TUI's sessions list to filter rows by current project,\n * so the user only sees conversations relevant to where they are\n * working — without needing one `.{prefix}/` directory per project.\n *\n * `undefined` on pre-tagging legacy sessions; the chat layer treats\n * those as \"untagged\" and hides them unless `Settings.showAllProjects`\n * is on.\n */\n projectRoot?: string\n turns: SessionTurn[]\n runs: SessionRun[]\n status: 'idle' | 'running' | 'completed' | 'error'\n metadata: Record<string, unknown>\n createdAt: number\n updatedAt: number\n}\n\n// ---------------------------------------------------------------------------\n// SessionStore interface (pluggable backend)\n// ---------------------------------------------------------------------------\n\nexport interface SessionStore {\n /** Optional: generate a session ID server-side (e.g. Supabase UUID). */\n generateSessionId?: () => string | Promise<string>\n\n /** Optional: generate a turn ID server-side. */\n generateTurnId?: () => string | Promise<string>\n\n /** Load a session by ID. Returns null if not found. */\n load: (sessionId: string) => Promise<SessionData | null>\n\n /** Save a session (create or update, full document). */\n save: (session: SessionData) => Promise<void>\n\n /** Delete a session. */\n delete: (sessionId: string) => Promise<void>\n\n /**\n * List session IDs, optionally filtered. `projectRoot` restricts to\n * sessions whose `SessionData.projectRoot` matches exactly — untagged\n * (legacy) sessions are NOT returned under that filter; pass `null`\n * explicitly to ask for untagged ones, or omit the field to ignore\n * the axis entirely. `agentId` filters by recorded agent; the two\n * conditions AND together when both are set.\n */\n list: (filter?: { agentId?: string, limit?: number, projectRoot?: string | null }) => Promise<string[]>\n\n /** Append new turns to a session (incremental, avoids full re-save). */\n appendTurns: (sessionId: string, turns: SessionTurn[]) => Promise<void>\n\n /** Return a slice of turns for a session. */\n getTurns: (sessionId: string, from?: number, limit?: number) => Promise<SessionTurn[]>\n\n /**\n * Persist a run record (called after completeRun / abortRun / errorRun).\n *\n * **Upsert semantics required.** The agent's run lifecycle calls\n * `session.startRun()` to register a new run in IN-MEMORY state and\n * then `updateRun()` at finalize. The store doesn't see the run any\n * earlier — `appendTurns` is the only intermediate call, and it\n * persists turns but not runs. An update-only implementation would\n * silently drop every newly-created run record at finalize, leaving\n * the on-disk `runs[]` missing entries that the persisted turns\n * reference (which `eventsFromTurns` then mis-classifies via\n * `ancestryOf` → broken depth, broken `child-N` labels, broken\n * `userPrompts` history).\n *\n * Implementations MUST insert when the id doesn't exist and update\n * when it does. The bundled `sqlite` / `memory` / `file-map` stores\n * all follow this contract; remote stores using HTTP PUT get upsert\n * for free via REST semantics.\n */\n updateRun: (sessionId: string, run: SessionRun) => Promise<void>\n\n /** Update the top-level status of a session. */\n updateStatus: (sessionId: string, status: SessionData['status']) => Promise<void>\n}\n\n// ---------------------------------------------------------------------------\n// Session (live instance wrapping a SessionData)\n// ---------------------------------------------------------------------------\n\nexport interface Session {\n /** Session ID */\n readonly id: string\n\n /** Agent ID (optional label) */\n readonly agentId?: string\n\n /**\n * Project this session was created under — see {@link SessionData.projectRoot}.\n * Set once on creation; surfaces here for read-only inspection.\n */\n readonly projectRoot?: string\n\n /**\n * Raw persisted turn history — kept pristine for fidelity (the model's\n * reasoning chain stays intact across reloads).\n *\n * **DO NOT pass this directly to a provider request.** Mid-pair\n * persistence (interrupted runs, partial-result writes, compaction\n * boundaries) leaves orphan `tool_use` / `tool_result` blocks here that\n * every provider 400s on (`tool_result must be preceded by a tool_call\n * with the same toolCallId`). The agent's own loop repairs these\n * just-in-time at wire-send time and never writes the repair back to\n * `turns`.\n *\n * For provider requests built outside `agent.run()`, use\n * {@link Session.toMessages} — it applies the same defensive pairing\n * repair and returns a guaranteed-safe `SessionMessage[]`.\n */\n readonly turns: SessionTurn[]\n\n /**\n * True when this session has no turns yet.\n *\n * Use this as a first-prompt signal when setting up a run — e.g. writing initial\n * configuration only on fresh sessions. Equivalent to `turns.length === 0`.\n */\n readonly isEmpty: boolean\n\n /** Top-level session status */\n readonly status: SessionData['status']\n\n /** All runs in this session */\n readonly runs: SessionRun[]\n\n /** Arbitrary metadata */\n readonly metadata: Record<string, unknown>\n\n /**\n * Start tracking a new run. `extras.parentRunId` + `extras.depth` are\n * populated by the spawn tool when a child agent shares its parent's\n * session; regular top-level `agent.run()` calls omit them.\n */\n startRun: (runId: string, prompt?: string, extras?: { parentRunId?: string, depth?: number }) => void\n\n /** Mark a run as completed */\n completeRun: (runId: string, stats: { turns: number, tokensIn: number, tokensOut: number, turnUsage?: TurnUsage[], cost?: number }) => void\n\n /** Mark a run as aborted */\n /**\n * Optional `stats` lets the agent backfill the run's token totals when\n * the abort happened *after* the loop accumulated meaningful usage —\n * common when the user presses esc mid-streaming. Without it, the run\n * record reads `0 in / 0 out` on reload regardless of how much was\n * spent before the abort. Same shape as `completeRun`'s stats so the\n * persisted `totalUsage` aggregate stays consistent across paths.\n */\n abortRun: (runId: string, stats?: { turns: number, tokensIn: number, tokensOut: number, turnUsage?: TurnUsage[], cost?: number }) => void\n\n /** Mark a run as errored */\n /** Optional `stats` — same rationale as `abortRun.stats`. */\n errorRun: (runId: string, error: string, stats?: { turns: number, tokensIn: number, tokensOut: number, turnUsage?: TurnUsage[], cost?: number }) => void\n\n /** Append turns to in-memory history AND persist via store.appendTurns (if store present) */\n appendTurns: (turns: SessionTurn[]) => Promise<void>\n\n /** Replace all turns in-memory (does not persist — use save() for that) */\n setTurns: (turns: SessionTurn[]) => void\n\n /**\n * Replace all runs in-memory (does not persist — use save() for that).\n * Mirrors {@link setTurns} for the fork / restore case: callers that\n * bootstrap a session from an externally-derived snapshot (e.g.\n * `onForkTurn` copying parent runs into a child session) need this so\n * the cloned runs land in `data.runs` before the first `save()`.\n * Production agent runs continue to mutate runs via `startRun` /\n * `completeRun` / `updateRun`; this is the bulk-replace escape hatch.\n */\n setRuns: (runs: SessionRun[]) => void\n\n /** Update the session status in memory AND via store.updateStatus (if store present) */\n updateStatus: (status: SessionData['status']) => Promise<void>\n\n /** Persist an updated run record via store.updateRun (if store present) */\n updateRun: (run: SessionRun) => Promise<void>\n\n /** Generate a turn ID using store.generateTurnId if available, else crypto.randomUUID() */\n generateTurnId: () => string | Promise<string>\n\n /** Set metadata key */\n setMeta: (key: string, value: unknown) => void\n\n /** Persist the full session document to the store */\n save: () => Promise<void>\n\n /**\n * Project the session's persisted turns into a wire-ready\n * `SessionMessage[]` safe to hand to any provider.\n *\n * The canonical answer to \"I need to send these turns to Anthropic /\n * OpenAI from outside `agent.run()`\" (sub-agent classifier, side-channel\n * summarizer, replay worker, …). Runs the same defensive pairing repair\n * the agent loop applies just-in-time at wire-send time\n * ({@link ensureToolResultPairing}), so orphan `tool_use` blocks and\n * dangling `tool_result`s never reach the provider as the 400\n * \"tool_result must be preceded by a tool_call with the same toolCallId\".\n *\n * Pass `provider` to also enforce the user-tail invariant\n * ({@link ensureEndsWithUserMessage}) — required for opus 4.7 / o-series\n * style models that reject assistant-prefill.\n *\n * Pure + idempotent. Does NOT mutate `session.turns`. **Do not write the\n * result back via `setTurns` / `appendTurns`** — synthetic placeholders\n * inserted by the repair will contaminate persisted history.\n */\n toMessages: (options?: ToWireMessagesOptions) => SessionMessage[]\n\n /** Serialize to SessionData */\n toJSON: () => SessionData\n}\n\n// ---------------------------------------------------------------------------\n// createSession\n// ---------------------------------------------------------------------------\n\nexport interface CreateSessionOptions {\n /** Session ID. If omitted and store provides generateSessionId, that is used. */\n id?: string\n /** Agent ID label */\n agentId?: string\n /**\n * Project tag — see {@link SessionData.projectRoot}. Stamped once on\n * creation; ignored when `_data` is set (restoring an existing\n * session preserves whatever was already persisted there). The TUI\n * resolves this from `findGitRoot(cwd) ?? cwd` so sessions started\n * from the same repo (no matter which subdir) share one tag.\n */\n projectRoot?: string\n /** Initial metadata */\n metadata?: Record<string, unknown>\n /** Storage backend (optional, enables save/load) */\n store?: SessionStore\n // @internal: restore from existing data (bypasses id/agentId/metadata options)\n _data?: SessionData\n}\n\n/**\n * Create a new session.\n * Async so stores that generate IDs server-side (e.g. Supabase) can be supported.\n */\nexport async function createSession(options: CreateSessionOptions = {}): Promise<Session> {\n const store = options.store\n const now = Date.now()\n\n let sessionId = options.id\n if (!sessionId && store?.generateSessionId) {\n sessionId = await store.generateSessionId()\n }\n if (!sessionId) {\n sessionId = generateId()\n }\n\n const data: SessionData = options._data ?? {\n id: sessionId,\n agentId: options.agentId,\n // Stamp the project tag at creation only — restored sessions (the\n // `_data` branch above) keep whatever was already persisted, even\n // if `options.projectRoot` differs. A session's project identity\n // is sticky for its lifetime; loading the same session from a\n // different cwd doesn't re-home it.\n ...(options.projectRoot ? { projectRoot: options.projectRoot } : {}),\n turns: [],\n runs: [],\n status: 'idle',\n metadata: options.metadata ?? {},\n createdAt: now,\n updatedAt: now,\n }\n\n function touch() {\n data.updatedAt = Date.now()\n }\n\n function findRun(runId: string): SessionRun | undefined {\n return data.runs.find(r => r.id === runId)\n }\n\n /**\n * Apply per-run usage stats onto a SessionRun. Shared by `completeRun`,\n * `abortRun`, and `errorRun` so the on-disk shape stays consistent across\n * exit paths — historically only `completeRun` filled these in, and\n * aborted/errored runs rendered `0 in / 0 out` on reload regardless of\n * how much was actually consumed.\n */\n function applyRunStats(run: SessionRun, stats: { turns: number, tokensIn: number, tokensOut: number, turnUsage?: TurnUsage[], cost?: number }) {\n run.turns = stats.turns\n run.tokensIn = stats.tokensIn\n run.tokensOut = stats.tokensOut\n if (stats.turnUsage) {\n run.turnUsage = stats.turnUsage\n const total = stats.turnUsage.reduce((acc, t) => ({\n input: acc.input + t.input,\n output: acc.output + t.output,\n cacheCreation: (acc.cacheCreation ?? 0) + (t.cacheCreation ?? 0),\n cacheRead: (acc.cacheRead ?? 0) + (t.cacheRead ?? 0),\n thinking: (acc.thinking ?? 0) + (t.thinking ?? 0),\n }), { input: 0, output: 0, cacheCreation: 0, cacheRead: 0, thinking: 0 })\n run.totalUsage = {\n input: total.input,\n output: total.output,\n ...(total.cacheCreation ? { cacheCreation: total.cacheCreation } : {}),\n ...(total.cacheRead ? { cacheRead: total.cacheRead } : {}),\n ...(total.thinking ? { thinking: total.thinking } : {}),\n }\n }\n if (stats.cost !== undefined)\n run.cost = stats.cost\n }\n\n const session: Session = {\n get id() { return data.id },\n get agentId() { return data.agentId },\n get projectRoot() { return data.projectRoot },\n get turns() { return data.turns },\n get isEmpty() { return data.turns.length === 0 },\n get status() { return data.status },\n get runs() { return data.runs },\n get metadata() { return data.metadata },\n\n startRun(runId: string, prompt?: string, extras?: { parentRunId?: string, depth?: number }) {\n data.runs.push({\n id: runId,\n startedAt: Date.now(),\n prompt: prompt ?? '',\n status: 'running',\n ...(extras?.parentRunId ? { parentRunId: extras.parentRunId } : {}),\n ...(typeof extras?.depth === 'number' ? { depth: extras.depth } : {}),\n })\n touch()\n },\n\n completeRun(runId: string, stats: { turns: number, tokensIn: number, tokensOut: number, turnUsage?: TurnUsage[], cost?: number }) {\n const run = findRun(runId)\n if (run) {\n run.status = 'completed'\n run.endedAt = Date.now()\n applyRunStats(run, stats)\n }\n touch()\n },\n\n abortRun(runId: string, stats?: { turns: number, tokensIn: number, tokensOut: number, turnUsage?: TurnUsage[], cost?: number }) {\n const run = findRun(runId)\n if (run) {\n run.status = 'aborted'\n run.endedAt = Date.now()\n // Backfill tokens when available so an aborted run's session\n // ledger reflects actual consumption rather than `0 in / 0 out`.\n if (stats)\n applyRunStats(run, stats)\n }\n touch()\n },\n\n errorRun(runId: string, error: string, stats?: { turns: number, tokensIn: number, tokensOut: number, turnUsage?: TurnUsage[], cost?: number }) {\n const run = findRun(runId)\n if (run) {\n run.status = 'error'\n run.endedAt = Date.now()\n run.error = error\n if (stats)\n applyRunStats(run, stats)\n }\n touch()\n },\n\n async appendTurns(turns: SessionTurn[]) {\n data.turns.push(...turns)\n touch()\n if (store) {\n await store.appendTurns(data.id, turns)\n }\n },\n\n setTurns(turns: SessionTurn[]) {\n data.turns = turns\n touch()\n },\n\n setRuns(runs: SessionRun[]) {\n data.runs = runs\n touch()\n },\n\n async updateStatus(status: SessionData['status']) {\n data.status = status\n touch()\n if (store) {\n await store.updateStatus(data.id, status)\n }\n },\n\n async updateRun(run: SessionRun) {\n if (store) {\n await store.updateRun(data.id, run)\n }\n },\n\n generateTurnId() {\n if (store?.generateTurnId) {\n return store.generateTurnId()\n }\n return crypto.randomUUID()\n },\n\n setMeta(key: string, value: unknown) {\n data.metadata[key] = value\n touch()\n },\n\n async save() {\n if (!store) {\n throw new Error('No SessionStore configured. Pass a store to createSession() to enable persistence.')\n }\n await store.save(data)\n },\n\n toMessages(options?: ToWireMessagesOptions) {\n return toWireMessages(data.turns, options)\n },\n\n toJSON() {\n return structuredClone(data)\n },\n }\n\n return session\n}\n\n/**\n * Load an existing session from a store.\n */\nexport async function loadSession(store: SessionStore, sessionId: string): Promise<Session | null> {\n const loaded = await store.load(sessionId)\n if (!loaded)\n return null\n\n return createSession({ store, _data: loaded })\n}\n\n// ---------------------------------------------------------------------------\n// Re-export stores\n// ---------------------------------------------------------------------------\n\nexport type { FileMapAdapter, FileMapStoreOptions } from './file-map'\nexport { createMemoryStore } from './memory'\nexport { autoDetectAndConvert, fromAnthropic, fromOpenAI, toAnthropic, toOpenAI, toWireMessages } from './messages'\nexport type { ToWireMessagesOptions } from './messages'\nexport { createRemoteStore } from './remote'\nexport type { RemoteStoreOptions } from './remote'\n\n// NOTE: `createSqliteStore` is intentionally NOT re-exported here. It lives behind\n// the dedicated `zidane/session/sqlite` subpath so that non-Bun consumers don't\n// transitively evaluate `bun:sqlite` when they import from `zidane/session`.\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction generateId(): string {\n return `ses_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`\n}\n"],"mappings":"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"}
|
package/dist/session.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { $ as autoDetectAndConvert, B as createRemoteStore, F as SessionRun, H as FileMapAdapter, I as SessionStore, L as createSession, M as CreateSessionOptions, N as Session, P as SessionData, R as loadSession, U as FileMapStoreOptions, V as createMemoryStore, W as createFileMapStore, Z as ToWireMessagesOptions,
|
|
2
|
-
export { CreateSessionOptions, FileMapAdapter, FileMapStoreOptions, RemoteStoreOptions, Session, SessionContentBlock, SessionData, SessionMessage, SessionRun, SessionStore, SessionTurn, ToWireMessagesOptions, autoDetectAndConvert, createFileMapStore, createMemoryStore, createRemoteStore, createSession, fromAnthropic, fromOpenAI, loadSession, toAnthropic, toOpenAI, toWireMessages };
|
|
1
|
+
import { $ as autoDetectAndConvert, B as createRemoteStore, F as SessionRun, H as FileMapAdapter, I as SessionStore, L as createSession, M as CreateSessionOptions, N as Session, P as SessionData, R as loadSession, U as FileMapStoreOptions, V as createMemoryStore, W as createFileMapStore, Z as ToWireMessagesOptions, at as fromOpenAI, ct as toWireMessages, dn as SessionTurn, it as fromAnthropic, ot as toAnthropic, sn as SessionContentBlock, st as toOpenAI, un as SessionMessage, z as RemoteStoreOptions } from "./agent-BNS2nx_T.js";
|
|
2
|
+
export { CreateSessionOptions, type FileMapAdapter, type FileMapStoreOptions, type RemoteStoreOptions, Session, type SessionContentBlock, SessionData, type SessionMessage, SessionRun, SessionStore, type SessionTurn, type ToWireMessagesOptions, autoDetectAndConvert, createFileMapStore, createMemoryStore, createRemoteStore, createSession, fromAnthropic, fromOpenAI, loadSession, toAnthropic, toOpenAI, toWireMessages };
|
package/dist/session.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { d as toAnthropic, f as toOpenAI, i as autoDetectAndConvert, l as fromAnthropic, p as toWireMessages, u as fromOpenAI } from "./messages-
|
|
2
|
-
import { a as createFileMapStore, i as createMemoryStore, n as loadSession, r as createRemoteStore, t as createSession } from "./session-
|
|
1
|
+
import { d as toAnthropic, f as toOpenAI, i as autoDetectAndConvert, l as fromAnthropic, p as toWireMessages, u as fromOpenAI } from "./messages-CvRQTdbR.js";
|
|
2
|
+
import { a as createFileMapStore, i as createMemoryStore, n as loadSession, r as createRemoteStore, t as createSession } from "./session-BzLou2_-.js";
|
|
3
3
|
export { autoDetectAndConvert, createFileMapStore, createMemoryStore, createRemoteStore, createSession, fromAnthropic, fromOpenAI, loadSession, toAnthropic, toOpenAI, toWireMessages };
|
package/dist/skills.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { A as SkillSource, D as SkillConfig, O as SkillDiagnostic, c as DeactivationReason, d as createSkillActivationState, j as SkillsConfig, k as SkillResource, l as SkillActivationState, o as ActivationVia, s as ActiveSkill, u as SkillActivationStateOptions } from "./agent-
|
|
2
|
-
import { S as installAllowedToolsGate, _ as inferSource, a as SkillValidationResult, b as buildCatalog, c as parseAllowedToolPattern, d as validateSkillName, f as resolveSkills, g as getDefaultScanPaths, h as discoverSkills, i as SkillValidationIssue, l as validateResourcePath, m as SourcedScanPath, n as writeSkillToDisk, o as isToolAllowedByUnion, p as interpolateShellCommands, r as writeSkillsToDisk, s as matchesAllowedTool, t as defineSkill, u as validateSkillForWrite, v as parseFrontmatter, x as IMPLICITLY_ALLOWED_SKILL_TOOLS, y as parseSkillFile } from "./index-
|
|
3
|
-
export { ActivationVia, ActiveSkill, DeactivationReason, IMPLICITLY_ALLOWED_SKILL_TOOLS, SkillActivationState, SkillActivationStateOptions, SkillConfig, SkillDiagnostic, SkillResource, SkillSource, SkillValidationIssue, SkillValidationResult, SkillsConfig, SourcedScanPath, buildCatalog, createSkillActivationState, defineSkill, discoverSkills, getDefaultScanPaths, inferSource, installAllowedToolsGate, interpolateShellCommands, isToolAllowedByUnion, matchesAllowedTool, parseAllowedToolPattern, parseFrontmatter, parseSkillFile, resolveSkills, validateResourcePath, validateSkillForWrite, validateSkillName, writeSkillToDisk, writeSkillsToDisk };
|
|
1
|
+
import { A as SkillSource, D as SkillConfig, O as SkillDiagnostic, c as DeactivationReason, d as createSkillActivationState, j as SkillsConfig, k as SkillResource, l as SkillActivationState, o as ActivationVia, s as ActiveSkill, u as SkillActivationStateOptions } from "./agent-BNS2nx_T.js";
|
|
2
|
+
import { S as installAllowedToolsGate, _ as inferSource, a as SkillValidationResult, b as buildCatalog, c as parseAllowedToolPattern, d as validateSkillName, f as resolveSkills, g as getDefaultScanPaths, h as discoverSkills, i as SkillValidationIssue, l as validateResourcePath, m as SourcedScanPath, n as writeSkillToDisk, o as isToolAllowedByUnion, p as interpolateShellCommands, r as writeSkillsToDisk, s as matchesAllowedTool, t as defineSkill, u as validateSkillForWrite, v as parseFrontmatter, x as IMPLICITLY_ALLOWED_SKILL_TOOLS, y as parseSkillFile } from "./index-KiS7w0dC.js";
|
|
3
|
+
export { type ActivationVia, type ActiveSkill, type DeactivationReason, IMPLICITLY_ALLOWED_SKILL_TOOLS, type SkillActivationState, type SkillActivationStateOptions, type SkillConfig, type SkillDiagnostic, type SkillResource, type SkillSource, type SkillValidationIssue, type SkillValidationResult, type SkillsConfig, type SourcedScanPath, buildCatalog, createSkillActivationState, defineSkill, discoverSkills, getDefaultScanPaths, inferSource, installAllowedToolsGate, interpolateShellCommands, isToolAllowedByUnion, matchesAllowedTool, parseAllowedToolPattern, parseFrontmatter, parseSkillFile, resolveSkills, validateResourcePath, validateSkillForWrite, validateSkillName, writeSkillToDisk, writeSkillsToDisk };
|
package/dist/skills.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as
|
|
1
|
+
import { _ as validateSkillForWrite, a as discoverSkills, c as parseFrontmatter, d as IMPLICITLY_ALLOWED_SKILL_TOOLS, f as installAllowedToolsGate, g as validateResourcePath, h as parseAllowedToolPattern, i as writeSkillsToDisk, l as parseSkillFile, m as matchesAllowedTool, n as resolveSkills, o as getDefaultScanPaths, p as isToolAllowedByUnion, r as writeSkillToDisk, s as inferSource, t as interpolateShellCommands, u as buildCatalog, v as validateSkillName, y as createSkillActivationState } from "./interpolate-TySiqKzc.js";
|
|
2
2
|
//#region src/skills/index.ts
|
|
3
3
|
/**
|
|
4
4
|
* Define an inline skill configuration.
|
package/dist/skills.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skills.js","names":[],"sources":["../src/skills/index.ts"],"sourcesContent":["import type { SkillConfig } from './types'\nimport { validateSkillForWrite } from './validate'\n\nexport {\n type ActivationVia,\n type ActiveSkill,\n createSkillActivationState,\n type DeactivationReason,\n type SkillActivationState,\n type SkillActivationStateOptions,\n} from './activation'\nexport {\n IMPLICITLY_ALLOWED_SKILL_TOOLS,\n installAllowedToolsGate,\n} from './allowed-tools'\nexport { buildCatalog } from './catalog'\nexport {\n discoverSkills,\n getDefaultScanPaths,\n inferSource,\n parseFrontmatter,\n parseSkillFile,\n} from './discovery'\nexport type { SourcedScanPath } from './discovery'\nexport { interpolateShellCommands } from './interpolate'\nexport { resolveSkills } from './resolve'\nexport type {\n SkillConfig,\n SkillDiagnostic,\n SkillResource,\n SkillsConfig,\n SkillSource,\n} from './types'\nexport {\n isToolAllowedByUnion,\n matchesAllowedTool,\n parseAllowedToolPattern,\n validateResourcePath,\n validateSkillForWrite,\n validateSkillName,\n} from './validate'\nexport type { SkillValidationIssue, SkillValidationResult } from './validate'\nexport { writeSkillsToDisk, writeSkillToDisk } from './writer'\n\n// ---------------------------------------------------------------------------\n// defineSkill helper\n// ---------------------------------------------------------------------------\n\n/**\n * Define an inline skill configuration.\n *\n * Strict-validates at author-time — throws if the config would be rejected by\n * `writeSkillToDisk` or the spec. Source defaults to `'inline'`.\n */\nexport function defineSkill(config: Omit<SkillConfig, 'source'> & { source?: SkillConfig['source'] }): SkillConfig {\n const normalized: SkillConfig = {\n ...config,\n source: config.source ?? 'inline',\n }\n const result = validateSkillForWrite(normalized)\n if (!result.valid) {\n const summary = result.errors.map(e => ` - [${e.code}] ${e.message}`).join('\\n')\n throw new Error(`Invalid skill \"${normalized.name}\":\\n${summary}`)\n }\n return normalized\n}\n"],"mappings":";;;;;;;;AAsDA,SAAgB,YAAY,QAAuF;CACjH,MAAM,aAA0B;EAC9B,GAAG;EACH,QAAQ,OAAO,UAAU;
|
|
1
|
+
{"version":3,"file":"skills.js","names":[],"sources":["../src/skills/index.ts"],"sourcesContent":["import type { SkillConfig } from './types'\nimport { validateSkillForWrite } from './validate'\n\nexport {\n type ActivationVia,\n type ActiveSkill,\n createSkillActivationState,\n type DeactivationReason,\n type SkillActivationState,\n type SkillActivationStateOptions,\n} from './activation'\nexport {\n IMPLICITLY_ALLOWED_SKILL_TOOLS,\n installAllowedToolsGate,\n} from './allowed-tools'\nexport { buildCatalog } from './catalog'\nexport {\n discoverSkills,\n getDefaultScanPaths,\n inferSource,\n parseFrontmatter,\n parseSkillFile,\n} from './discovery'\nexport type { SourcedScanPath } from './discovery'\nexport { interpolateShellCommands } from './interpolate'\nexport { resolveSkills } from './resolve'\nexport type {\n SkillConfig,\n SkillDiagnostic,\n SkillResource,\n SkillsConfig,\n SkillSource,\n} from './types'\nexport {\n isToolAllowedByUnion,\n matchesAllowedTool,\n parseAllowedToolPattern,\n validateResourcePath,\n validateSkillForWrite,\n validateSkillName,\n} from './validate'\nexport type { SkillValidationIssue, SkillValidationResult } from './validate'\nexport { writeSkillsToDisk, writeSkillToDisk } from './writer'\n\n// ---------------------------------------------------------------------------\n// defineSkill helper\n// ---------------------------------------------------------------------------\n\n/**\n * Define an inline skill configuration.\n *\n * Strict-validates at author-time — throws if the config would be rejected by\n * `writeSkillToDisk` or the spec. Source defaults to `'inline'`.\n */\nexport function defineSkill(config: Omit<SkillConfig, 'source'> & { source?: SkillConfig['source'] }): SkillConfig {\n const normalized: SkillConfig = {\n ...config,\n source: config.source ?? 'inline',\n }\n const result = validateSkillForWrite(normalized)\n if (!result.valid) {\n const summary = result.errors.map(e => ` - [${e.code}] ${e.message}`).join('\\n')\n throw new Error(`Invalid skill \"${normalized.name}\":\\n${summary}`)\n }\n return normalized\n}\n"],"mappings":";;;;;;;;AAsDA,SAAgB,YAAY,QAAuF;CACjH,MAAM,aAA0B;EAC9B,GAAG;EACH,QAAQ,OAAO,UAAU;CAC3B;CACA,MAAM,SAAS,sBAAsB,UAAU;CAC/C,IAAI,CAAC,OAAO,OAAO;EACjB,MAAM,UAAU,OAAO,OAAO,KAAI,MAAK,QAAQ,EAAE,KAAK,IAAI,EAAE,SAAS,EAAE,KAAK,IAAI;EAChF,MAAM,IAAI,MAAM,kBAAkB,WAAW,KAAK,MAAM,SAAS;CACnE;CACA,OAAO;AACT"}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
//#region src/stats.ts
|
|
2
2
|
/**
|
|
3
|
+
* Effective input tokens for a single `TurnUsage`-shaped record — the sum of
|
|
4
|
+
* `input + cacheRead + cacheCreation`, i.e. every token the model actually saw
|
|
5
|
+
* for that turn regardless of how it was billed. See {@link effectiveInputTokens}
|
|
6
|
+
* for the cumulative `AgentStats` analogue and the prompt-caching rationale.
|
|
7
|
+
*/
|
|
8
|
+
function effectiveInputFromTurn(usage) {
|
|
9
|
+
if (!usage) return 0;
|
|
10
|
+
return (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheCreation ?? 0);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
3
13
|
* Clamp to a non-negative finite integer. Used to defend display logic
|
|
4
14
|
* against providers that report `NaN`, `Infinity`, or negatives in their
|
|
5
15
|
* usage payloads — the rendered string stays readable instead of leaking
|
|
@@ -95,6 +105,6 @@ function statsByModel(stats) {
|
|
|
95
105
|
return out;
|
|
96
106
|
}
|
|
97
107
|
//#endregion
|
|
98
|
-
export {
|
|
108
|
+
export { statsByModel as i, flattenTurns as n, formatTokenUsage as r, effectiveInputFromTurn as t };
|
|
99
109
|
|
|
100
|
-
//# sourceMappingURL=stats-
|
|
110
|
+
//# sourceMappingURL=stats-DAKBEKjc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stats-DAKBEKjc.js","names":[],"sources":["../src/stats.ts"],"sourcesContent":["/**\n * Pure derivations over `AgentStats`.\n *\n * Both helpers are tree-shakeable — import only what you need. They never\n * touch agent state, never do I/O, and always operate on the recursive\n * `AgentStats` tree returned by `agent.run()`.\n */\n\nimport type { AgentStats, TurnUsage } from './types'\n\n/**\n * Effective input tokens for a single `TurnUsage`-shaped record — the sum of\n * `input + cacheRead + cacheCreation`, i.e. every token the model actually saw\n * for that turn regardless of how it was billed. See {@link effectiveInputTokens}\n * for the cumulative `AgentStats` analogue and the prompt-caching rationale.\n */\nexport function effectiveInputFromTurn(\n usage: Pick<TurnUsage, 'input' | 'cacheRead' | 'cacheCreation'> | undefined | null,\n): number {\n if (!usage)\n return 0\n return (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheCreation ?? 0)\n}\n\n/**\n * Per-model usage rollup produced by {@link statsByModel}.\n *\n * `turns` counts the number of `TurnUsage` entries attributed to the model\n * across the whole tree (parent loop + every recursively-spawned child).\n * Cache and cost numbers are summed from the same set of turns.\n */\nexport interface ModelUsage {\n input: number\n output: number\n cost: number\n cacheRead: number\n cacheCreation: number\n turns: number\n}\n\n/**\n * Common shape for token-usage helpers. Mirrors `AgentStats`'s cumulative\n * fields. Callers working with `TurnUsage` / `SessionRun.totalUsage`\n * (`input` / `output` / `cacheRead` / `cacheCreation`) map their fields in\n * at the call site — we don't accept both shapes here because\n * `AgentStats.output` is a structured-output payload (`Record<string,\n * unknown>`), not a token count, so the field name `output` is ambiguous.\n */\nexport interface TokenUsageLike {\n totalIn?: number\n totalOut?: number\n totalCacheRead?: number\n totalCacheCreation?: number\n}\n\n/**\n * Clamp to a non-negative finite integer. Used to defend display logic\n * against providers that report `NaN`, `Infinity`, or negatives in their\n * usage payloads — the rendered string stays readable instead of leaking\n * `NaN`s into the footer / spawn-end markers.\n */\nfunction safeNumber(n: number | undefined): number {\n if (typeof n !== 'number' || !Number.isFinite(n) || n < 0)\n return 0\n return n\n}\n\nfunction normalize(stats: TokenUsageLike): {\n input: number\n output: number\n cacheRead: number\n cacheCreation: number\n} {\n return {\n input: safeNumber(stats.totalIn),\n output: safeNumber(stats.totalOut),\n cacheRead: safeNumber(stats.totalCacheRead),\n cacheCreation: safeNumber(stats.totalCacheCreation),\n }\n}\n\n/**\n * Sum of `input + cacheRead + cacheCreation` — i.e. the number of tokens the\n * model actually saw, regardless of how they were billed.\n *\n * With provider prompt caching (Anthropic, OpenRouter→Anthropic, Gemini),\n * `AgentStats.totalIn` only counts **new uncached** tokens per turn; the\n * bulk of an N-turn run's context lives in `totalCacheRead`, plus a smaller\n * `totalCacheCreation` chunk for whichever turns added new breakpoints.\n * Showing only `totalIn` in a per-run summary makes long subagent runs look\n * suspiciously cheap (e.g. \"13 in\" for 8 turns over a large repo).\n *\n * `TurnUsage` / `SessionRun.totalUsage` callers map their fields to the\n * `total*` names at the call site (see {@link TokenUsageLike}).\n */\nexport function effectiveInputTokens(stats: TokenUsageLike): number {\n const { input, cacheRead, cacheCreation } = normalize(stats)\n return input + cacheRead + cacheCreation\n}\n\n/**\n * Render a one-line token summary suitable for spawn-end markers, child\n * tool-result text, and any other per-run \"what did this cost?\" surface.\n *\n * `in 52413 (cache 50000) / 4075 out`\n *\n * The `in` total includes cached reads and cache-creation tokens (see\n * {@link effectiveInputTokens}); the `(cache N)` parenthetical breaks out\n * the cached portion so the user can tell at a glance how much was reused\n * vs new. The parenthetical is omitted when its value is zero, so\n * non-caching providers and brand-new runs read simply as `in N / M out`.\n *\n * Output-side caching is not a concept any current provider exposes —\n * Anthropic's `cacheRead` / `cacheCreation` are both input-billed. If a\n * future provider ships an output cache, extend `TokenUsageLike` with\n * output-cache fields and add a parallel `(cache N)` slot on the `out`\n * side here; today the right side is always plain `${N} out`.\n */\nexport function formatTokenUsage(stats: TokenUsageLike): string {\n const { input, output, cacheRead, cacheCreation } = normalize(stats)\n const inputTotal = input + cacheRead + cacheCreation\n const inputCache = cacheRead + cacheCreation\n const inPart = inputCache > 0\n ? `in ${inputTotal} (cache ${inputCache})`\n : `in ${inputTotal}`\n return `${inPart} / ${output} out`\n}\n\n/**\n * Depth-first walk over the stats tree, returning every `TurnUsage` entry\n * — parent loop first, then each child subtree in completion order.\n *\n * Closes the cache-token aggregation gap: `TurnUsage.cacheRead` /\n * `cacheCreation` live only on per-turn entries, and the top-level\n * `AgentStats` deliberately doesn't carry cumulative forms (one source of\n * truth, no risk of drift). Anything that needs a tree-wide sum walks\n * through this.\n */\nexport function flattenTurns(stats: AgentStats): TurnUsage[] {\n const out: TurnUsage[] = []\n collectTurns(stats, out)\n return out\n}\n\nfunction collectTurns(stats: AgentStats, out: TurnUsage[]): void {\n if (stats.turnUsage)\n out.push(...stats.turnUsage)\n if (stats.children) {\n for (const child of stats.children)\n collectTurns(child.stats, out)\n }\n}\n\n/**\n * Group cumulative usage by `TurnUsage.modelId`. Each entry sums the input,\n * output, cache, cost, and turn-count across every turn the tree attributed\n * to that model — naturally handling cross-model runs (vision-fallback,\n * model-shifted subagents, mixed-provider workflows).\n *\n * Turns missing `modelId` (mock providers, providers that don't echo a model\n * id) are bucketed under the literal string `'(unknown)'`.\n */\nexport function statsByModel(stats: AgentStats): Map<string, ModelUsage> {\n const out = new Map<string, ModelUsage>()\n for (const turn of flattenTurns(stats)) {\n const key = turn.modelId ?? '(unknown)'\n let entry = out.get(key)\n if (!entry) {\n entry = { input: 0, output: 0, cost: 0, cacheRead: 0, cacheCreation: 0, turns: 0 }\n out.set(key, entry)\n }\n entry.input += turn.input\n entry.output += turn.output\n entry.cost += turn.cost ?? 0\n entry.cacheRead += turn.cacheRead ?? 0\n entry.cacheCreation += turn.cacheCreation ?? 0\n entry.turns += 1\n }\n return out\n}\n"],"mappings":";;;;;;;AAgBA,SAAgB,uBACd,OACQ;CACR,IAAI,CAAC,OACH,OAAO;CACT,QAAQ,MAAM,SAAS,MAAM,MAAM,aAAa,MAAM,MAAM,iBAAiB;AAC/E;;;;;;;AAuCA,SAAS,WAAW,GAA+B;CACjD,IAAI,OAAO,MAAM,YAAY,CAAC,OAAO,SAAS,CAAC,KAAK,IAAI,GACtD,OAAO;CACT,OAAO;AACT;AAEA,SAAS,UAAU,OAKjB;CACA,OAAO;EACL,OAAO,WAAW,MAAM,OAAO;EAC/B,QAAQ,WAAW,MAAM,QAAQ;EACjC,WAAW,WAAW,MAAM,cAAc;EAC1C,eAAe,WAAW,MAAM,kBAAkB;CACpD;AACF;;;;;;;;;;;;;;;;;;;AAuCA,SAAgB,iBAAiB,OAA+B;CAC9D,MAAM,EAAE,OAAO,QAAQ,WAAW,kBAAkB,UAAU,KAAK;CACnE,MAAM,aAAa,QAAQ,YAAY;CACvC,MAAM,aAAa,YAAY;CAI/B,OAAO,GAHQ,aAAa,IACxB,MAAM,WAAW,UAAU,WAAW,KACtC,MAAM,aACO,KAAK,OAAO;AAC/B;;;;;;;;;;;AAYA,SAAgB,aAAa,OAAgC;CAC3D,MAAM,MAAmB,CAAC;CAC1B,aAAa,OAAO,GAAG;CACvB,OAAO;AACT;AAEA,SAAS,aAAa,OAAmB,KAAwB;CAC/D,IAAI,MAAM,WACR,IAAI,KAAK,GAAG,MAAM,SAAS;CAC7B,IAAI,MAAM,UACR,KAAK,MAAM,SAAS,MAAM,UACxB,aAAa,MAAM,OAAO,GAAG;AAEnC;;;;;;;;;;AAWA,SAAgB,aAAa,OAA4C;CACvE,MAAM,sBAAM,IAAI,IAAwB;CACxC,KAAK,MAAM,QAAQ,aAAa,KAAK,GAAG;EACtC,MAAM,MAAM,KAAK,WAAW;EAC5B,IAAI,QAAQ,IAAI,IAAI,GAAG;EACvB,IAAI,CAAC,OAAO;GACV,QAAQ;IAAE,OAAO;IAAG,QAAQ;IAAG,MAAM;IAAG,WAAW;IAAG,eAAe;IAAG,OAAO;GAAE;GACjF,IAAI,IAAI,KAAK,KAAK;EACpB;EACA,MAAM,SAAS,KAAK;EACpB,MAAM,UAAU,KAAK;EACrB,MAAM,QAAQ,KAAK,QAAQ;EAC3B,MAAM,aAAa,KAAK,aAAa;EACrC,MAAM,iBAAiB,KAAK,iBAAiB;EAC7C,MAAM,SAAS;CACjB;CACA,OAAO;AACT"}
|
|
@@ -55,7 +55,7 @@ function spawnSync$1(command, args, options) {
|
|
|
55
55
|
spawn$1.spawn = spawn$1;
|
|
56
56
|
spawn$1.sync = spawnSync$1;
|
|
57
57
|
//#endregion
|
|
58
|
-
//#region node_modules/@modelcontextprotocol/sdk/dist/esm/types.js
|
|
58
|
+
//#region node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/types.js
|
|
59
59
|
const RELATED_TASK_META_KEY = "io.modelcontextprotocol/related-task";
|
|
60
60
|
/**
|
|
61
61
|
* Assert 'object' type schema.
|
|
@@ -1931,7 +1931,7 @@ z.union([
|
|
|
1931
1931
|
CreateTaskResultSchema
|
|
1932
1932
|
]);
|
|
1933
1933
|
//#endregion
|
|
1934
|
-
//#region node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
1934
|
+
//#region node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
1935
1935
|
/**
|
|
1936
1936
|
* Buffers a continuous stdio stream into discrete JSON-RPC messages.
|
|
1937
1937
|
*/
|
|
@@ -1958,7 +1958,7 @@ function serializeMessage(message) {
|
|
|
1958
1958
|
return JSON.stringify(message) + "\n";
|
|
1959
1959
|
}
|
|
1960
1960
|
//#endregion
|
|
1961
|
-
//#region node_modules/@modelcontextprotocol/sdk/dist/esm/client/stdio.js
|
|
1961
|
+
//#region node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/client/stdio.js
|
|
1962
1962
|
/**
|
|
1963
1963
|
* Environment variables to inherit by default, if an environment is not explicitly given.
|
|
1964
1964
|
*/
|
|
@@ -2117,4 +2117,4 @@ var StdioClientTransport = class {
|
|
|
2117
2117
|
//#endregion
|
|
2118
2118
|
export { StdioClientTransport, getDefaultEnvironment };
|
|
2119
2119
|
|
|
2120
|
-
//# sourceMappingURL=stdio-loader-
|
|
2120
|
+
//# sourceMappingURL=stdio-loader-Ce68wUmM.js.map
|