Commit Graph
33 Commits
Author SHA1 Message Date
tommy0103andClaude Opus 4.8 e3e61cc7ab fix: coordinate sqlite index writers
Implement the full ADR-0006 plan: three-layer separation of transaction
correctness, retry policy, and cross-process writer coordination.

Layer 1 — scripts/tx.ts (transaction correctness):
- runWriteTransaction executes work exactly once; no internal retry.
- BEGIN IMMEDIATE takes the write lock up front (avoids SQLITE_BUSY_SNAPSHOT).
- Guarded rollback: checks inTransaction() via adapter before attempting
  ROLLBACK; never masks the primary exception.
- WriteTxDiagnostics attached to errors: phase, code, label,
  rollbackSucceeded, rollbackError, transactionActive.
- Binding adapters (betterSqliteTransactionAdapter, nodeSqliteTransactionAdapter)
  mapping better-sqlite3's `.inTransaction` and node:sqlite's `.isTransaction`.
- configureConnection centralizes WAL + synchronous + busy_timeout.

Layer 2 — scripts/write-coordinator.ts (retry policy):
- runRetryableWriteTransaction: bounded retry with total time budget.
- Only retries when the transaction confirmed ended (transactionActive=false)
  and the error is SQLITE_BUSY during work/commit phase.
- BEGIN-phase BUSY = abort entire build (isBeginBusyFailure); the caller
  returns `{ deferred: true, reason: 'writer_busy' }` instead of waiting.
- hasUnusableTransaction detects a still-active transaction after failure;
  aborts the build immediately, never retries.

Layer 3 — scripts/writer-lease.ts (cross-process coordination):
- acquireWriterLease: dedicated writer.lock.sqlite with busy_timeout=0 +
  BEGIN IMMEDIATE. Non-blocking attempt; bounded wait with retryDelayMs.
- writerLockPathFor derives lock path from the target DB path.
- Lease held for the entire build; released on completion or failure.
- Lock DB uses DELETE journal (not WAL); crash/close auto-releases.
- All consumers obey: skill acquires at build start (returns deferred if
  unavailable); app daemon (via worker) acquires for its build cycle.

Build semantics changes:
- affectedSessionIds updated only after successful commit.
- BuildIndexResult gains skipped/skippedFiles for observability.
- Skill finalize failure now fails the build (was silently warned).
- Checkpoint changed to PASSIVE (TRUNCATE reserved for maintenance/exit).
- Skill buildIndex returns { deferred, reason } on lease contention;
  indexer-service reschedules the build (deferredRetryMs) without publishing
  a heartbeat (so the build-deferred state is visible to cross-process
  arbitration).
- Service publishes heartbeat immediately on start() for correct arbitration.

Tests:
- tests/write-transaction.test.mjs: single-shot execution, diagnostics
  propagation, auto-rolled-back transaction detected, rollback failure
  captured as metadata, BEGIN IMMEDIATE semantics.
- tests/writer-lease.test.mjs: acquire/release, contention returns null,
  bounded wait with release during budget.
- tests/app-writer-lease.test.mjs: better-sqlite3 adapter integration.
- tests/app-rollback-guard.test.mjs: rewritten — transient BUSY recovered
  by coordinator, persistent BUSY skips file, begin-busy aborts build,
  live-transaction aborts build, phantom affectedSessionIds prevented.
- tests/daemon-arbitration.test.mjs: skill defers to fresh app heartbeat,
  builds when heartbeat is stale.
- tests/app-indexer-service.test.mjs: new cases for deferred-retry
  scheduling and immediate heartbeat on start.
- app/tests/electron-concurrency.mjs + child: dual-child IPC structure for
  real better-sqlite3 contention (holder acquires lock → build child starts
  → delayed release → result collected; persistent contention bounded).

ADR-0006 updated to reflect the implemented design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 00:42:43 +08:00
tommy0103andClaude Opus 4.8 44029676d4 fix(indexer): guard cleanup rollback so it never masks the real build error
Reported as "cannot rollback - no transaction is active" — a secondary error.
SQLite auto-rolls back certain failures (SQLITE_BUSY / SQLITE_BUSY_SNAPSHOT,
disk full), after which the per-file loop's unguarded ROLLBACK in its catch
threw over the real error and aborted the whole build instead of skipping just
the bad file.

Stopgap only (see docs/adr/0006 for the planned full fix):
- Add safeRollback(db) in both indexers: it swallows only the rollback's own
  error, so the true cause surfaces. Per-file failures are logged and the build
  continues; the finalize failure still propagates.
- Give the skill's node:sqlite connection an explicit PRAGMA busy_timeout=5000
  (it has no default). The app adds none: better-sqlite3 already defaults to
  5000ms, so busy_timeout is NOT the root-cause fix and is not treated as one.

Add tests/app-rollback-guard.test.mjs: injects a DB that faithfully reproduces
"a write auto-rolls back the txn, then ROLLBACK errors" and asserts the build
survives (bad file skipped, other file indexed). Revert-checked: without the
guard the test fails with the exact "cannot rollback - no transaction is active".

docs/adr/0006 records the real fix (shared runWriteTransaction, single-writer
coordination, BEGIN IMMEDIATE, whole-transaction retry, PASSIVE checkpointing)
as deferred, two-phase work — and why bumping busy_timeout is not it.

Verified: suite 124/124, typecheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:36:36 +08:00
tommy0103andClaude Opus 4.8 c964653362 fix(indexer): force build purges stale sessions instead of only clearing index_state
The skill's `--build` (always a force build) cleared `index_state` and
re-indexed existing files, but never removed rows for transcripts that no
longer exist on disk, so deleted sessions accumulated forever (the dogfood
showed 328 indexed sessions vs 283 current files). A force build is meant to
be a clean rebuild, matching what the app already does.

Drop every derived table (messages, tool_calls, tool_results, sessions,
summaries, subagents, workflows, workflow_agents) in the force path, then
re-index from the current files. `memories` is the durable, human-approved
layer and is never cleared; messages_fts is repopulated by the existing
'rebuild' command in finalize.

Add a test that builds two sessions, deletes one transcript, force-rebuilds,
and asserts the stale session is purged while a seeded memory survives.
Verified the test is discriminating: without the fix it reports
['gone','keep'] instead of ['keep'].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 03:18:52 +08:00
tommy0103 1bda4da170 refactor: extract node:sqlite-free parsing.mjs so the core can be app-consumable (Phase 5d-1)
Move all pure parse/discover helpers (message extraction, project-path, codex
helpers, discovery) out of db.mjs/indexer.mjs into scripts/parsing.mjs, which
imports only node:fs/path/os. Providers now import from parsing.mjs, so the
provider import graph no longer transitively loads node:sqlite — a prerequisite
for the app (Electron/Node without node:sqlite) to consume the compiled core.
Verbatim move, no behavior change; indexer.mjs 840→209 lines. 119/119.
2026-07-09 09:20:37 +08:00
tommy0103 0598c29aad feat(providers): migrate codex indexing to adapter + persist, remove legacy indexers (Phase 5c)
Complete the skill-side provider migration: codex now goes through a pure adapter
and the shared persist layer, and the two original monolithic indexers are gone.

New:
- scripts/providers/codex.ts — pure codex adapter. Full-reparse (buffers the whole
  file) because the event_msg↔response_item dedup needs whole-file, bidirectional
  knowledge; emits SessionRecord with countMode 'total'. Handles guardian threads
  (→ delete-session), agent spawns/tool calls (→ tool_call/subagent), token_count
  (patched onto the message record) and task_complete (→ message-turn-duration).

Contract:
- SessionRecord.countMode ('total' | 'delta') tells persist whether to replace or
  accumulate message_count — claude is line-incremental (delta), codex full-reparse
  (total). SubagentRecord non-key fields are optional; persist merges them
  column-wise with COALESCE. MessageTurnDurationRecord.turn_duration_ms is nullable.

Orchestration:
- buildIndex's codex branch parses via the adapter and writes via persist. An
  unchanged file is skipped but still swept for stale guardian rows (routed through
  persist as a delete-session), preserving prior behavior.

Cleanup:
- Remove the now-unused indexJsonl, indexCodexJsonl, deleteCodexThreadRows and
  upsertCodexSubagent — their semantics now live in the adapters + persist.
  indexer.mjs drops from ~840 to 428 lines. Codex pure helpers stay exported for
  codex.ts and the guardian sweep (physical move deferred to the app-side reorg).
- Migrate the upsert drift test off indexJsonl to the claude.parse + persist path,
  keeping the rowid-stability and count-replace regression guards.

Tests: tests/codex-parse.test.mjs (record-stream golden: dedup, tools, token patch,
turn-duration, guardian→delete) and tests/codex-index.test.mjs (full buildIndex
path: fresh build + incremental full-reparse, total-count replace, no duplicates).

Verified equivalent on the real ~/.obelisk index: codex messages 82476 and
subagents 522 identical before/after, zero guardian leakage; real incremental
confirmed (touch a codex file → reparsed idempotently, unchanged files skipped).
lint + typecheck clean, 119/119.
2026-07-08 20:43:54 +08:00
tommy0103 1c346039f1 refactor(indexer): route claude indexing through adapter + persist (Phase 5b-2b)
buildIndex's claude branch now parses via providers/claude.ts and writes via
persist.ts instead of the inlined indexJsonl. Behavior is equivalent — the full
buildIndex integration suite (runtime.test.mjs) stays green, 116/116 — and a
force rebuild of the real ~/.obelisk index (327 sessions, 119k messages)
reproduced identical session counts with project_path fully populated.

codex, indexSubagentMeta, workflows, history and the project_path pass are
untouched. indexJsonl is now unused by buildIndex (kept for its drift test;
removed once codex is migrated).

Adds tests/incremental-index.test.mjs: verifies resume/accumulate through the
full buildIndex path (append new lines to an indexed session → incremental
build resumes from the cursor, message_count accumulates, no duplicates). The
30s shouldSkipBuild debounce is cleared in-test so the incremental run fires.
2026-07-08 19:46:34 +08:00
tommy0103 1d652d823f feat(persist): add shared record-stream persist layer (Phase 5b-2a)
Introduce scripts/persist.ts — the single, provider- and binding-agnostic
layer that consumes an adapter's IndexRecord stream and writes rows into an
injected SQLite handle (node:sqlite for skill/CLI, better-sqlite3 for the app).
It is the only layer that touches the database.

Write semantics are the canonical ones reconciled from the earlier drift:
- messages upsert via ON CONFLICT (turn_duration_ms not in the column list, so
  it is never clobbered)
- sessions merge with the existing row: started_at MIN, ended_at MAX,
  message_count reset-or-accumulate by resume state, fill-if-null for the rest;
  project_path is preserved and left to refreshSessionProjectPaths
- message-turn-duration applies as a targeted UPDATE
- delete-session cascades across all tables
- the generator's return cursor is written back to index_state (mtime:lines →
  the two existing columns; no schema migration yet)

Purely additive — buildIndex still uses the old indexJsonl path, so existing
behavior is unchanged. Rewiring happens in 5b-2b.

Adds tests/persist.test.mjs: all record kinds written, resume does not
double-count message_count, fresh re-scan resets it, delete-session cascades.
Full suite 115/115, lint + typecheck green.
2026-07-08 19:01:54 +08:00
tommy0103 2b30d9596d feat(providers): add pure claude adapter with record-stream golden tests (5b-1)
claude.parse mirrors indexJsonl line-for-line but yields IndexRecords (no db).
Adds MessageTurnDurationRecord op. Purely additive — buildIndex still uses the
old path. 111/111 green.
2026-07-08 18:27:28 +08:00
tommy0103 338dae413d feat(indexer): define source-agnostic Provider + IndexRecord contract (Phase 5a)
Provider adapters own discovery/change-detection/cursoring and emit records;
one shared binding-agnostic persist consumes them. Records map 1:1 to schema
tables. Revises ADR-0001 to the provider-registry model.
2026-07-08 18:10:04 +08:00
tommy0103 51597f42f0 fix(indexer): align claude message writes to canonical upsert semantics
scripts/indexer.mjs indexJsonl used INSERT OR REPLACE (rowid/FTS churn) and
carried message_count forward on full re-scan; align to app's ON CONFLICT upsert
+ count reset so the two indexers no longer silently diverge. Regression test added.
2026-07-08 17:23:55 +08:00
tommy0103 3391bf6ac3 refactor: extract runtime Core into scripts/core.ts; runtime.mjs becomes a thin shell
Core exposes buildIndex/searchText/executeQuery/executeAttune as the single shared
implementation for all transports. First TypeScript module, run via Node type
stripping in dev. Adds typescript-eslint. lint/typecheck green, test 107/107.
2026-07-08 16:59:30 +08:00
tommy0103 a32461b2ab feat: make search() FTS-safe; complete Tier 2 helper-shape contract tests
search() falls back to safe per-token quoting on malformed FTS input instead of
crashing (documented in api-reference.md). Adds raw() to the doc-synced shape
contract. Full suite 107/107.
2026-07-08 16:35:10 +08:00
tommy0103 25537d3a53 test: lock runtime CLI I/O envelope; unify --build/--search error handling 2026-07-08 16:20:48 +08:00
tommy0103 12407227cc feat: index Codex sessions alongside Claude Code with source tagging
Add `source` column to sessions and messages ('claude' | 'codex').
  Discover and parse Codex JSONL files from ~/.codex/sessions/, mapping
  Codex thread/item structures to the same schema (messages, tool_calls,
  tool_results, subagents). Move DB to ~/.obelisk/ with legacy migration.
  Add rebuild-to-temp-then-swap for safe full rebuilds. On the app side:
  source filter toggle, collapsible untitled session fold, configurable
  codexDir in Settings, and a dev script. Update SKILL.md and query
  helpers to expose source fields and accept source filter opt.
2026-06-17 23:40:36 +08:00
tommy0103 b7506ee765 feat(app): add Settings view, configurable claude dir, and recap docs refactor
Introduce a Settings view for configuring the Claude data directory
  (with WSL auto-detection on Windows), sidebar project grouping module,
  and an empty-state onboarding screen for SessionList. Refactor
  recap-patterns.md into per-card reference files under references/recap/
  with separate retrieval and writing guides. Remove the legacy panel.html.
  On the data layer: incremental indexing via changedPaths, per-session
  live-update IPC (obelisk:session-updated), and session dirty-tracking
  in the renderer.
2026-06-15 01:44:32 +08:00
tommy0103 9fc7f202f0 feat(app): add weekly recap cards with swipeable story UI and export
Introduce a Spotify-Wrapped-style recap feature: five themed cards
  (Cover, Path, Vibe, Workflow, Closing) rendered per archetype palette,
  with keyboard/swipe navigation and image export via capture IPC. Add
  RecapList, RecapDetail, RecapExport views and recap component library.
  Wire recap:list/read/updated IPC channels through preload, document the
  retrieval-to-card contract in references/recap-patterns.md, and bundle
  dist-renderer for production use.
2026-06-14 03:16:49 +08:00
tommy0103 4eec6b38c9 feat(app): embed indexer in Electron with file-watching service and UI refinements
Extract schema DDL into scripts/schema.sql shared between CLI and app.
  Add an in-process chokidar-based indexer-service that watches ~/.claude/projects
  for JSONL changes, debounces, and triggers background rebuilds via a worker
  thread. Rename Usage view to Activity, flesh out MemoryDetail and SubagentDetail
  views, and refine App.vue layout/routing. The main process now starts/stops the
  indexer lifecycle and notifies renderer windows on index updates.
2026-06-13 03:42:01 +08:00
tommy0103 b524339d85 feat(app): add Electron desktop UI and evolve memory/retrieval layer
Introduce an Electron app with session browser, memory list, and usage
  views (vanilla JS + Vue scaffolding). On the data layer: add content_type
  and is_meta to messages for transcript control-plane filtering, introduce
  FTS5-backed memory recall with safe tokenization, support memory archival
  via forget() through the renamed --attune runtime, and expose anchors on
  memory records.
2026-06-12 22:20:29 +08:00
tommy0103 b52f57b538 feat(memory): enforce English-only memory indexing with CJK guardrail
Memory layer is now English-indexed: memories() query terms and
  remember() summaries must be English. Adds a runtime assertion that
  rejects CJK text in both paths, guiding the agent to translate
  non-English user requests before querying or writing memories.
  Ensures consistent retrieval regardless of conversation language.
2026-06-10 03:50:38 +08:00
tommy0103 10b51d8878 fix(indexer): derive project_path from message cwd instead of slug decoding
The old slug-to-path conversion (replace hyphens with slashes) was
  lossy and wrong for paths containing hyphens. Now infers project_path
  from the most-frequent observed cwd across session messages, falling
  back to slug decoding only when no cwd data exists. Adds
  refreshSessionProjectPaths() to backfill existing sessions on rebuild.
2026-06-10 03:15:32 +08:00
tommy0103 807e5141fa feat(query): add overview() for project-aware session/memory discovery
Resolves current project from cwd, lists all known projects with session
  and memory counts, and returns the current project's recent sessions and
  memories in one call. Enables the agent to orient itself at the start of
  a retrieval without multiple exploratory queries.
2026-06-10 03:04:11 +08:00
tommy0103 34f3a164ab feat(memory): persistent memory layer with remember/recall CodeAct API
Add a memories table (survives index rebuilds) for agent-written
  conclusions with provenance (session, message range, project). The agent
  writes markdown files via Write tool (user-approved), then registers
  them via a --remember CodeAct script with remember(). Recall via
  memories() in --query scripts, filtered by project/session/time.

  Separates query (read-only, assertReadOnlySql) from remember (write)
  execution contexts in runtime.mjs.
2026-06-10 02:05:27 +08:00
tommy0103 297ef01eaf refactor(docs): restructure SKILL.md into progressive-disclosure layers
Split the monolithic skill prompt into three tiers:
  - Core API (search/context/sql) stays in the first prompt
  - Structured helpers listed as one-liners with filter signatures
  - Detailed patterns and pitfalls extracted to references/

  Add references/query-patterns.md (copyable CodeAct recipes) and
  references/pitfalls.md (scope, FTS, ordering, compactness traps).
  Clarify project scope semantics (slug vs path vs cwd) throughout.
  Add ORDER BY timestamp DESC to failures() for newest-first default.
2026-06-07 16:42:41 +08:00
tommy0103 67513b793f feat(workflow): enrich workflow/agent metadata from workflowProgress;
lightweight workflowTree, build debounce, minor fixes

  - Index phase, label, model, state, duration, tokens per workflow agent
  - Index duration, total_tokens, status, name per workflow run
  - workflowTree returns parsed result + agent summaries instead of
    dumping all messages
  - 30s debounce on buildIndex to avoid repeated directory scans
  - Fix broken BASH_EXIT_PAT (SQLite LIKE has no character classes)
  - Fix SKILL.md step numbering, document FTS5 hyphen limitation
2026-06-04 15:49:57 +08:00
tommy0103 ab828364ad feat(index): capture cwd, skill, turn_duration, is_error from JSONL;
expose FTS5 rank and cwd filter in search()
2026-06-03 19:18:22 +08:00
tommy0103 b12ddab883 fix(failures): use is_error field instead of content pattern matching
Index the is_error boolean from JSONL tool_result blocks into a new
  column, replacing the old ERROR_PATS text-matching approach that
  produced ~90% false positives. Bash exit code pattern kept as fallback.
2026-06-02 09:19:46 +08:00
tommy0103 fb35cca8fe feat(query): unified filter opts across all list-returning APIs
Add sessions() as first-class entry point for session discovery with
  project/time/branch filters. Extend summaries, workflows, failures,
  subagents, fileHistory with the same opts vocabulary that search()
  already had — no more pulling full datasets to filter client-side.
2026-06-02 01:36:47 +08:00
tommy0103 4bfced92b7 feat(obelisk): index session summaries and guide incremental retrieval
- Add generic summaries table (source-agnostic, ready for Codex)
  - Index Claude Code away_summary events as session-level recaps
  - Add summaries() query API
  - Rewrite retrieval strategy in SKILL.md: never pull entire sessions,
    navigate horizontally (by timestamp) or vertically (by parent chain)
2026-06-01 16:52:58 +08:00
tommy0103 a6be5172c3 refactor: split runtime.mjs into db, indexer, query modules
Prepares for adding Codex JSONL support — new indexers can be added
  without touching the query layer.

  - db.mjs: schema, openDb, truncation helpers, shared utilities
  - indexer.mjs: file discovery, JSONL parsing, SQLite writes
  - query.mjs: all query API functions (search, context, raw, etc.)
  - runtime.mjs: thin CLI entry point + VM script executor
2026-05-31 03:29:41 +08:00
tommy0103 fb353f5deb fix(indexer): streaming reads, per-file transactions, and error reporting
- Replace readFileSync+split with chunked line reader (readLines) to
    avoid loading entire JSONL files into memory at once
  - Wrap each file's indexing in its own transaction so one corrupt file
    doesn't roll back the entire index
  - Log warnings to stderr instead of silently swallowing errors in
    indexSubagentMeta, indexWorkflows, and indexHistory
  - Add try/catch around readdirSync calls in discoverJsonlFiles and
    indexWorkflows to survive unreadable directories
  - Push error-pattern filtering in failures() into SQL LIKE clauses
    instead of scanning all tool_results in JS
2026-05-31 02:33:22 +08:00
tommy0103 cf8bd1154e fix(indexer): safe JSON truncation and windowed raw access
- Don't truncate workflow result_json and script (low-volume, must
    stay parseable)
  - Replace blind string truncation on tool_calls.input_json with
    structure-aware truncJson that shortens individual string values
    while keeping the JSON skeleton valid
  - Add raw(uuid, {offset, limit}) API for windowed access to the
    original JSONL line, so truncated content can be recovered on demand
2026-05-30 04:06:42 +08:00
tommy0103 2b02a7b6c8 fix(indexer): preserve session metadata during incremental reindex
Previously the session metadata accumulator started from zero on every
  reindex pass, so an incremental update would overwrite started_at and
  message_count with values derived only from the new lines.

  Now reads the existing session row first and merges new data on top.
2026-05-30 03:53:16 +08:00
tommy0103 bc9b6dde72 build(obelisk): let Claude Code search its own session history
- SQLite + FTS5 index over ~/.claude JSONL transcripts.
  - Agent writes JS queries at runtime — same sandbox pattern as workflows.
  - Covers sessions, subagents, workflow executions, tool calls, and full-text search.
2026-05-30 03:21:35 +08:00