Commit Graph
27 Commits
Author SHA1 Message Date
tommy0103 bbf16d8f9f fix(app): keep live session updates responsive
Cache timeline DOM indexes and memoize the timeline so progress and flap updates avoid rescanning or rerendering old messages. Preserve scroll anchors and disclosure state across targeted live message replacements.
2026-07-14 00:53:38 +08:00
tommy0103 7e6e9d9fc3 fix(app): keep live session updates incremental
Preserve unchanged message identities and isolate Vue row rendering during live refreshes.

Use a tail-only scroll path so updates do not interrupt active readers or expanded tools. Add regression coverage for 908-message appends, changed snapshots, and scroll policy.
2026-07-13 22:31:12 +08:00
tommy0103 f963f14b10 fix: stabilize live session UI and skill publishing
Keep message navigation and progress state synchronized, and preserve
sequential split-flap count updates with a bounded queue.

Publish the Obelisk skill under skills/obelisk for npx skills, sharing
the same staging layout between CI and local releases with regression coverage.
2026-07-13 22:02:44 +08:00
tommy0103andCodex a9687ba8b7 feat(app): live session update + tool renderer + input_tokens migration
SessionDetail live update:
- Extract session-view-state.mjs: capture scroll position, disclosure
  (open/skill-md-open) state, and visible-UUID anchor before refresh;
  reconcile messages by UUID (in-place update, append tail only); restore
  scroll and disclosure state after DOM patch. findLastMessageAtOrAbove uses
  binary search (O(log n)) instead of linear scan.
- scrollRevision tracks user scrolls during refresh to avoid stale anchors
  overriding manual navigation.
- Throttle onScroll to one rAF per frame.

Tool renderer:
- Extract tool-renderer.js: standalone module for rendering tool call cards
  (Read/Write/Edit diffs, Bash terminal output, search results, JS/TS
  syntax highlighting). Replaces inline rendering in SessionDetail.
- tests/app-tool-renderer.test.mjs covers escaping, highlighting, and
  terminal formatting.

Input tokens semantics migration:
- Claude provider now sums input_tokens + cache_creation_input_tokens +
  cache_read_input_tokens into a single input_tokens value (was previously
  only the raw field, undercounting when cache tokens are present).
- One-time index-wide re-parse triggered when the marker
  __claude_input_tokens_include_cache_v1__ is absent and the DB already
  has token data (self-healing on first build after upgrade).
- App indexer.ts carries the same marker check for the app's build path.

Also:
- PRODUCT.md: product register (users, purpose, brand, design principles,
  accessibility targets).
- README.md: minor wording updates.

Co-Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-07-13 21:02:38 +08:00
tommy0103 78aacfac6c feat(session): animate total count with split flap 2026-07-12 20:00:07 +08:00
tommy0103 48f1a4c225 feat(activity): add contribution ledger 2026-07-12 15:15:48 +08:00
tommy0103 644942202e Complete Vue renderer migration 2026-07-12 02:26:01 +08:00
tommy0103 c4c458fa8f refactor(core): share sqlite boundary types 2026-07-12 00:44:06 +08:00
tommy0103 3a6e3a00a7 fix(core): ship schema with package build 2026-07-12 00:39:57 +08:00
tommy0103 535d8edb4d refactor(core): finish TypeScript workspace migration 2026-07-12 00:37:32 +08:00
tommy0103 83d5703f7b refactor(core): move shared runtime into workspace package 2026-07-12 00:28:17 +08:00
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 e44ab7a1da build(skill): add build:skill for a readable, non-bundled skill artifact (Phase 6)
`npm run build:skill` emits dist/obelisk-skill/: the whole scripts/ Core
compiled to readable JS (module structure + comments preserved, ~1:1 with
source, no bundling/minifying per ADR-0004), plus schema.sql, SKILL.md,
references/, and a package.json. It excludes app/, tests/, and release/, which
is what keeps the artifact small.

- tsconfig.skill.json compiles scripts/**/*.{ts,mjs} (allowJs) with
  rewriteRelativeImportExtensions, so the .ts specifiers inside the .mjs shells
  (runtime.mjs -> ./core.js, indexer.mjs -> ./providers/*.js, ./persist.js) are
  rewritten to .js. declaration:false — the skill needs no .d.ts.
- packaging/skill-package.json provides the artifact's package.json; it sets
  "type":"module" because the emitted .ts->.js files are ESM.
- build:skill runs tsc then copies schema.sql/SKILL.md/references/package.json.

Add tests/build-skill.test.mjs: runs the real build:skill, asserts the artifact
structure, that no emitted .js/.mjs still imports a .ts module, and that the
compiled artifact builds an index and answers a search end-to-end under plain
Node (no type-stripping) against a temp HOME.

Verified: suite 123/123, typecheck clean, artifact smoke run green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:36:01 +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
tommy0103andClaude Opus 4.8 01a390fa10 refactor(app): migrate main + preload to TypeScript with typed seams (Phase 5d-3c-ii)
Convert the app's main and preload source from .js to .ts (git mv preserves
history), adding types where they carry value: the core-consumption seam
(BuildIndexOptions/BuildIndexResult, FileInfo), the indexer service/worker
factories, and the preload IPC bridge. Module-to-module specifiers use the
real .ts extension (mirroring scripts/, since Node type-stripping does not
rewrite .js->.ts); the worker's runtime path stays indexer-worker.js because
that is the built output.

Toolchain:
- Add app/tsconfig.json: strict but noImplicitAny:false (the app orchestrates
  the already-strict core; annotating every SQLite-handle helper is low-value
  churn) + allowImportingTsExtensions (safe under noEmit).
- Add @types/better-sqlite3 for the injected binding.
- electron.vite.config.ts inputs -> .ts; refresh the stale CommonJS comment.
- typecheck script runs root + app projects. Root tsconfig excludes the
  app-importing tests (app-*.test.mjs, recap-capture-query.test.mjs) so the
  lenient app files are not dragged into the strict root program; the app
  source is covered by app/tsconfig.json instead. See docs/adr/0005.

Verified: npm run typecheck (root + app) clean; suite 121/121; electron-vite
build emits all 6 main entries + preload with no .ts/node:sqlite residue in
the bundles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 16:25:59 +08:00
tommy0103andClaude Opus 4.8 60d47a852e refactor(app): consume shared indexing core, remove duplicated indexer (Phase 5d-3c-i)
The desktop app now indexes through the shared provider adapters + persist
layer (scripts/providers/{claude,codex}, scripts/persist, scripts/parsing)
instead of maintaining its own parallel indexer. buildIndex shrinks from
~1173 to ~592 lines, eliminating the skill<->app parse duplication that
Phase 5 set out to remove. electron-vite bundles the .ts core from source
with better-sqlite3 injected; the provider->parsing graph stays
node:sqlite-free so nothing drags node:sqlite into the app.

Also fix a misleading log: when a manual rebuild tears down the worker
mid-build, the cancelled background build is a deliberate stop, not a
failure. Guard the service's failure log with the stopped flag so it no
longer prints "Obelisk index build failed: Indexer worker stopped" on
every rebuild.

- CONTEXT.md: provider-adapter + single-persist + node:sqlite-free parsing.
- docs/adr/0005: app builds with electron-vite (TS+ESM), packages with
  electron-builder; preload CJS for sandbox; app consumes core from source.

Verified: full suite 121/121; a node:sqlite-adapter dogfood of the rebuild
path over real data (969 files, 285 sessions, FTS rebuilt) runs clean; app
Rebuild confirmed in real Electron/better-sqlite3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 12:46:02 +08:00
tommy0103 905c10789a refactor(app): migrate to ESM; preload emitted as CJS for sandbox (Phase 5d-3b)
App source (main/preload/worker/renderer) -> ESM; app is now type: module;
__dirname via import.meta.url; worker spawned with type module. Preload is built
as CJS (electron-vite output format) because the sandboxed renderer does not
support ESM preload; main loads ../preload/index.js. Removed dead imports
(nativeImage, readline) and the obsolete scripts/dev.js.

Tests: 4 app tests require->import; app-main-settings rewritten with node:test
mock.module + dynamic import (replacing CJS Module._load mocking); test script
adds --experimental-test-module-mocks. electron-vite build clean, 119/119, and
npm run dev verified: app launches, preload bridges IPC, data loads.
2026-07-09 11:05:00 +08:00
tommy0103 80c125a572 build(app): migrate to electron-vite (main/preload/renderer), keep electron-builder (Phase 5d-3a)
Restructure app into src/{main,preload,renderer}; electron.vite.config.ts builds
all three (each main module its own input so CJS requires + the indexer worker
resolve; better-sqlite3 externalized; Vue plugin for renderer). main/index.js
paths updated for the out/ layout + ELECTRON_RENDERER_URL. Still JS/CJS — TS+ESM
and core consumption are the next stages. Verified: npm run dev launches clean;
electron-vite build succeeds; root suite 119/119.
2026-07-09 10:20:18 +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 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 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 1a34245618 chore: sanitize tests, add CONTEXT.md + ADRs, track tests/docs 2026-07-08 16:11:12 +08:00