From 65fe680d38def84242495279e1f1f8043312de90 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Mon, 5 Oct 2026 08:03:05 +0700 Subject: [PATCH] fix(pricing): book daily cost to the day tokens were spent token_usage holds a session's cumulative totals with no time, so /api/pricing/cost booked a multi-day session's whole cost to its start date and the status bar's Today read ~$0. TranscriptCache now also splits usage by each message's UTC hour (tokensByHour); the hook writer stores it in the new token_usage_hourly table (MAX per hour) and the server back-fills existing sessions at start. The daily split books each total over its hourly rows' local days and the rest to the start date, so the days still add up to the total. --- CLAUDE.md | 2 +- docs/API.md | 12 ++ docs/DATABASE.md | 6 + server/__tests__/daily-cost.test.js | 203 ++++++++++++++++++++++++++++ server/db.js | 44 ++++++ server/index.js | 13 ++ server/lib/hourly-usage.js | 72 ++++++++++ server/lib/transcript-cache.js | 36 ++++- server/routes/hooks.js | 2 + server/routes/pricing.js | 109 ++++++++++++--- server/routes/settings.js | 4 + 11 files changed, 478 insertions(+), 25 deletions(-) create mode 100644 server/__tests__/daily-cost.test.js create mode 100644 server/lib/hourly-usage.js diff --git a/CLAUDE.md b/CLAUDE.md index f8dc07ac..ed40ceda 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # Claude Code Working Guide -_Last synced: v1.8.13 (2026-10-01) + CUSTOM PETS for the mascot (`server/lib/pets.js` + `server/routes/pets.js` behind `/api/pets`, `/pets//`; the `.codex-pet.zip` format — `pet.json` + a 1536×1872 `spritesheet.webp`, 8×9 frames of 192×208, one row per state — played by mood in `client/src/components/Sagi/PetSprite.tsx`; the right-click `PetMenu.tsx`: hide, size, choose, upload an image, import a bundle; four featured pets DOWNLOADED from codex-pets.net on a pick, never bundled; worn pet and size in `agenthub.pet`/`agenthub.pet.size`) + a sidebar workspace row opening the workspace's live TABS on the pane it last focused (its home when it has none; **Home** at the tab bar's left end), a bare tab listed at once as **New session** from its run (`WorkspaceSessionList`), and the sidebar's live panes following the Workspace page's poll (`LIVE_RUNS_EVENT` in `WorkspaceTree.tsx`) + the session view's console SPLITTING like VS Code (`client/src/components/run/PaneGrid.tsx` over `client/src/lib/paneLayout.ts`: a binary split tree of slots, ≤ 4 panes, each an absolutely placed sibling keyed by slot; Ctrl/Cmd+click on a tab, a `+` item or a sidebar session row splits — router state `split`, never the URL; pane headers drag to move/swap, pick another live session, split right/down, close the pane only; dividers drag or take arrow keys; `?run=` = the focused pane, pushed with `focusOnly`; layout per workspace in `agenthub.workspace.paneLayout`) and the session view going EDGE TO EDGE (no page padding, no cards, the tab bar and the right panel's activity bar one `h-10` header strip, the terminal on the page colour) + the session view's console as TABS instead of a 1/2/4 split (`client/src/components/run/ConsoleTabs.tsx`: one tab per live tmux slot 0–3, the active one = `?run=`, inactive ones mounted and hidden with CSS; `+` opens **New terminal** — `shell: true` on `POST /api/lanes/:id/start`, `spawnShell` in `server/lib/pty-run.js`, tool `shell`, no `run_id`/`status` change — or a bare AI tool, and links Settings → Agents; `splitViewStorage.ts` and `&pane=` gone — the sidebar's Ctrl/Cmd+click came back as a split, see above; `LaneConsolePane`'s `homeOnExit` → `onRunExit`), the tab bar then becoming the session view's ONLY header (the status/name/back-arrow row gone — Tools at the tab bar's right end, the pipeline/feature pickers only with a template —, `LaneConsolePane` reduced to the terminal: no title, no **Active runs** — `RunHistory.tsx` deleted with the client's `api.run.history`/`DashboardRunHistoryItem` and the page's active-sessions poll —, no **New run**) + an ENDED session opening in its workspace (`&session=`, `SessionReplay.tsx`: conversation + prompt box, sending resumes) instead of `/sessions/`, which the right panel's **Analyst** button opens + a session's AI TOOL chosen per session (`tool` on `POST /api/lanes/:id/start`, the home's **AI** select; the sidebar's tool icon moved from the workspace row to each session row) and the pane's run form GONE (`RunSetup` keeps only `PromptEditor`/`CwdAutocomplete`/`ModelPicker`; New run/exit/an empty pane go to the workspace home) + the DESKTOP-PARITY plan (`docs/plans/desktop-parity/`, phases 1–7; the multi-user server plan lives in the AgentHub-Server repo): an AI Router chosen PER SESSION (the **AI Router** select beside the model picker in the workspace home and a pane's run form, `client/src/components/run/RouterPicker.tsx`, `router` on both start routes; Claude `--settings=/router-settings/.json`, Codex `-p `; `dashboard_runs.router` for a resume; `POST /api/lanes/:id/router/apply`, `LaneRouterRow`, the Add-workspace router radio and the claude-/codex-home `routers` answers gone; `clearLaneRouters` takes the old keys out of `settings.local.json` at start) + Settings and the collapse toggle pinned to the sidebar's foot + shipped SVG marks for claude/codex/gemini/openclaw/hermes (Gemini's from zonalogo.com) (`client/public/ai-tools/.svg`, `BUNDLED` in `AiToolIcon.tsx`; the server's favicon route stays raster-only for the rest) + a workspace OWNS its sessions and lists only those (`sessions.lane_id`, stamped by `bindRunSession`, backfilled only from `dashboard_runs`; `GET /api/sessions?lane=&archived=0|1`; `PATCH /api/sessions/:id {archived|name}`), `dashboard_runs` keeping a pane's previous run as `@` instead of overwriting it and flagging a run `abandoned` only once its tmux session is gone (`reconcileOrphans(hasSession)` at boot and on the run-heal beat), a session row's state dot (`sessionDot`), double-click rename and archive (`sessions.archived_at`, hides only; the home folds the archived with Unarchive), the activity bar's Sessions tab and `WorkspaceSessionsTab.tsx` gone with `LaneConsolePane`'s `resumeRequest`, **Fork** beside Resume (`forkSession` → `--fork-session`, Claude only, `EUNSUPPORTED` → 400 on start), Ctrl/Cmd+click a live session → `&pane=1`, and a diff line's number opening a comment pasted into the workspace's last-focused pane (`client/src/lib/panePaste.ts`) + the sidebar losing its footer, language and theme controls (the collapse toggle sits beside Settings at the sidebar's foot; the version and **Check for updates** moved into the connection details modal, the status bar showing a green dot while a release is out; the language is Settings → General, `SettingsSegmented`) + a session in its OWN WORKTREE, Claude's way (desktop-parity phase 6: the home's **Local / New worktree** select → `claude --worktree=`, `dashboard_runs.worktree`, `?session=` on every lane git route via `server/lib/session-worktree.js`, Resume/Fork into the worktree, `ENOWORKTREE` when it is gone, Archive = `POST /api/lanes/:id/session-worktree/remove` never forced; `listWorktrees` now reads `locked `) + ONE `client/src/components/Switch.tsx` for every on/off switch and the themed `Checkbox.tsx` (now with `tone`/`disabled`) for every checkbox — no native ``, no hand-rolled switch + the AI Tools catalog TRIMMED to 10 (`server/data/ai-tools/`: claude, codex, gemini, antigravity, copilot, opencode, cursor — labelled **Cursor CLI**, `--continue` resume, `cursor-agent update` — omp, hermes, openclaw; the other 28 deleted; the argv-separator / fixed-args / after-start+bypassEnv prompt modes no built-in uses any more stay supported for custom files and are tested through `server/__tests__/fixtures/ai-tools/`) + the workspace-sessions plan's leftovers (a sidebar TOOL FILTER, `agenthub.sidebar.projects` → `tool`; search also finding a workspace by a session's name; the home naming its repository from `GET /api/projects` and Recents showing cost; the transcript page's explicit **Resume** in `lanes/SessionResumeButton.tsx`) + a WORKSPACE HOME like a Claude Desktop project page (`client/src/components/lanes/WorkspaceHome.tsx`: `/workspace?lane=` opens it and hides — CSS — the session view, which `&run=` opens; its prompt box starts a new session in the first free pane slot; Recents; Instructions / Memory / Context / Folder over `server/lib/lane-sources.js` + `server/routes/lane-sources.js`: `lanes.instructions`, the new `lane_sources` table, uploaded files under `/workspace-context//`, read-only memory, and `spawnRun` adding them to Claude/Codex argv itself) + Settings losing its Projects group (`pages/ProjectSettings.tsx` and the per-project override gone; Worktrees now picks the home's folder suggestions) + notifications opening the lane's pane (`&run=`) + the sidebar becoming a Claude-Desktop-style WORKSPACE LIST (`client/src/components/WorkspaceTree.tsx` replaces `ProjectTree.tsx`: one flat row per workspace in the stored order, no repo grouping and no untracked-worktree rows; a row's chevron expands its sessions, `lanes/WorkspaceSessionList.tsx`; a session with a live pane opens `/workspace?lane=&run=`, which only ATTACHES pane 0 — `attachRequest` on `LaneConsolePane` — and any other one its read-only transcript; the sidebar never starts a resume; expanded rows in `agenthub.sidebar.projects` → `expanded`) + the Workflows page folding into Dashboard → Analytics → **Workflow** (`Dashboard.tsx` renders `pages/Workflows.tsx` under that sub-tab, `?tab=`/`?sub=` deep-link the Dashboard, `/workflows` redirects to `/?tab=analytics&sub=workflow`, no Workflows item in the sidebar) + the named themes gaining `orca-dark`/`orca-light` (Orca's neutral palette in `client/src/index.css`; Orca Dark's accent is `#3b82f6`, not Orca's near-white primary, because `.btn-primary` puts white text on `bg-accent`) + a sound per notification type (`sound` + `soundByType` in `client/src/lib/notifPrefs.ts`, default `"none"`; `playSound` in `lib/notify.ts`, played by `useNotifications` only when a notification was actually shown; the WAVs in `client/public/sounds/` are generated by `scripts/gen-notification-sounds.js` — edit the script, never the files) + Settings becoming `/settings/:section` with a grouped left nav (`client/src/pages/SettingsHub.tsx`, route `settings/*`: General / Appearance / Notifications — `Settings.tsx` now takes `section`, its TOC gone —, Agents with `agents/` per tool — a tool's own settings live there, the AI Tools modal is now the `agents` page and `AiToolsModal.tsx` is gone —, Routing (AI Router, an all-vendor Pricing page), Git (credentials, **Worktrees** = `WorktreeVisibilitySettings.tsx`), Projects = `pages/ProjectSettings.tsx` per repo with a per-project `projectHideSources` override and Forget all; `?tool=`/`#ai-tools` redirect to the path form; the whole-page search index is `SETTINGS_INDEX` in `client/src/lib/settingsSearch.ts`, ranked by `rankSettings`, Ctrl/Cmd+F focuses it; row primitives in `components/settings/SettingsRow.tsx`) + the Workspace page losing its lane STRIP to a sidebar PROJECT TREE (`client/src/components/ProjectTree.tsx` + `lanes/WorkspaceRow.tsx`/`WorkspaceRowMenu.tsx`: workspaces grouped by git common dir from the new `GET /api/projects` (`server/lib/projects.js`, `server/routes/projects.js`, `POST /api/projects/refresh` same-origin), untracked worktrees with **Adopt** (`ensure` `exact: true`), double-click rename, drag/Alt+↑/↓ order still in `agenthub.workspace.laneOrder`, a right-click menu with Start/Stop/Rename/router/readme/New here/Copy path/Reset/Remove; prefs in `agenthub.sidebar.projects`, `client/src/lib/projectTreePrefs.ts`), the selected lane being the URL's `?lane=` (mirrored to `agenthub.workspace.selected`), ONE shared lane list (`client/src/hooks/useLanes.ts`), a fixed bottom `StatusBar.tsx` (connection, version, running/needs-you counts, today's cost) taking the sidebar's connection footer, the Changes panel gaining an activity bar (Files / Changes / **Sessions** = `lanes/WorkspaceSessionsTab.tsx`, a session resumed through `LaneConsolePane`'s `resumeRequest`; `agenthub.workspace.panelTab`), and a per-pane header in split view + the user-facing word "lane" becoming "workspace" in UI copy (en + vi), CLI prose, server error messages and docs, every identifier unchanged (`/api/lanes`, `agenthub lanes`, `lanes.json`, `Lane*` components; `docs/LANES.md` keeps its name) + `server/db.js` running every migration under a cross-process lock (`BEGIN IMMEDIATE` on `-migrate.lock`, held from before the legacy-DB copy until just before `stmts`) — two dashboards started at once on a fresh DB raced the probe-then-ALTER and the second died on `duplicate column name`; never wrap those migrations in a transaction on the main DB instead, since several rebuilds open their own `BEGIN` and turn `foreign_keys` off + a second dashboard on the SAME DB re-broadcasting the lanes the other one changed (`broadcastForeignLaneChanges` in `server/routes/lanes.js`, `PRAGMA data_version` every `DASHBOARD_LANE_SYNC_MS`=2 s from `server/index.js` — hooks reach only the lowest port, so a browser on the other port saw a lane's status only after F5) + the pricing sync never adding a RETIRED Claude model in advance (`RETIRED_PRICING_PATTERNS` moved to `server/lib/pricing-sync.js`, imported by `db.js`; one is priced only once used) + a just-started run no longer reading `dead` (`lastEventAge` also counts the run's `dashboard_runs.started_at`) + a Settings TAB PER CATALOG TOOL (`client/src/pages/AiToolConfig.tsx` for every installed, enabled tool with no `launcher`, built in `SettingsHub.tsx` from `useAiTools`, `?tool=`; `AntigravityConfig.tsx` deleted, its pricing + hooks cards living on there): path, homepage, an **Update…** link into the AI Tools modal, `GET /api/ai-tools/:id/usage` sessions/cost, the tool's lanes, a **Default model** saved through `PUT {defaultModel}` (`ModelPicker` now exported from `RunSetup.tsx`), `PricingSettings` for the catalog's `model.vendor` (antigravity now `google`), and the catalog's trusted `config` files edited through `server/lib/ai-tool-config.js` behind `GET/PUT /api/ai-tools/:id/config[/:index]`; the view gains `modelVendor`/`configFiles` + MODEL LISTS from each tool itself (`server/lib/ai-tool-models.js` behind `GET /api/ai-tools/:id/models`: Codex's `models_cache.json` listed slugs — its `identity` never read, Claude's aliases labelled with the newest priced version plus pinned priced versions, a catalog tool's vendor-priced ids plus the models its sessions used), a catalog `model: { flag, vendor? }` field (antigravity, gemini, opencode, cursor) putting ` ` before the prompt, `validModel` refusing a flag- or shell-shaped id (`EBADMODEL`) at the route AND in `spawnRun`, a per-tool `defaultModel` in `ai-tools.json` (`PUT /api/ai-tools/:id {defaultModel}`) that `LaneConsolePane` starts each run on, the run form's picker fetching that list (`RUN_MODEL_CHOICES` only as Claude's offline fallback, `RUN_MODEL_CHOICES_CODEX` deleted) + AUTOMATIC pricing that ADDS and FOLLOWS: `pricing-sync.js` fetches LiteLLM at most daily, inserts every current-generation Claude/OpenAI (gpt ≥ 5)/Gemini (≥ 3) chat model before it is used (`isCurrent`, `MIN_GEN`), keeps its own rows (`model_pricing.source = 'sync'`, the **Auto** badge in `PricingSettings`) at the published rate, adopts a pre-column row only while it equals the list, and never touches a `source = 'user'` row (any `PUT /api/pricing`) or a deleted pattern (`model_pricing_dismissed`, emptied by a reset for its scope) + a plugin build installed BESIDE the running one (a terminal `claude plugin update` starts nothing) being reported: `installed_not_running` + `update_available` in `pluginStatus` (`server/lib/update-check.js`, via `supersededBy`, no network), the sidebar's ** installed, not running**, the update modal keyed on it, `plugin-update-helper.js` handing over to the recorded install first; and a plugin install whose release lookup failed reading **Could not check for updates** instead of **Up to date** (`Sidebar.tsx`) + a markdown PREVIEW (`client/src/components/MarkdownPreview.tsx`: react-markdown + remark-gfm, raw HTML never run, frontmatter as a code block) behind a **Preview**/**Raw** toggle in the config `FileViewer` and the Changes panel's whole-file view + AI Tools moving from a Settings → Dashboard section to a MODAL behind a button at the Settings tab bar's right edge (`client/src/components/settings/AiToolsModal.tsx`, `actions` on `PageTabs`, opened by `/settings#ai-tools` too); its rows an enable switch + Default badge/Set default + **Update** for EVERY tool whose catalog names an updater (no version lookup on open — `api.aiTools.versions` left the client, `GET /api/ai-tools/versions` stays), the not-installed tools folded behind a disclosure, a finished job's log folded to one line; `codex` updating through its own `codex update` (standalone or npm) and `openclaw`/`hermes` gaining `update --yes`; update-all running every installed updater, skipping an npm one only when npm does not manage the binary, with no `npm view`; the shared `client/src/components/AiToolIcon.tsx`; the Add-lane tool picker being ONE button showing the chosen tool that opens the enabled tools as buttons (no Default/Other… select); and the Claude Code / Codex **AgentHub** sub-tab folded into their **Overview** (home editor, hooks, pricing under the overview card; the overview's path list drops CLAUDE_HOME/CODEX_HOME; `HomeDirSetting` shows the saved value only while the field differs) + ANTIGRAVITY PARITY (#40 part D: `server/lib/antigravity-usage.js` reads agy's own per-conversation SQLite files `~/.gemini/antigravity-cli/conversations/.db` read-only every `DASHBOARD_ANTIGRAVITY_POLL_MS` — tokens from `gen_metadata`'s protobuf `ModelUsageStats`, cwd from `trajectory_metadata_blob`, name/start from `brain//…/transcript_full.jsonl` — into `tool = 'antigravity'` sessions, mtime per file in the new `antigravity_conversations` table, and sets idle the lane of a session gone quiet that the hooks left running; `server/lib/antigravity-hooks.js` installs on a click the named hook `agenthub` (PreInvocation, PostToolUse, Stop — NEVER PreToolUse) into the global `~/.gemini/config/hooks.json` through `~/.gemini/config/agenthub-hook.sh`, `GET/POST/DELETE /api/settings/antigravity-hooks`; `routes/hooks.js` `antigravityHookInput` maps a CLI hook — `conversationId` → session, `workspacePaths[0]` → cwd, first PreInvocation → UserPromptSubmit — and ignores the IDE's; `server/lib/antigravity-home.js` (`AGENTHUB_GEMINI_DIR`); Gemini families in `pricing-sync.js`; the new Settings → **Antigravity** tab `client/src/pages/AntigravityConfig.tsx` with `PricingSettings vendor="google"` and `AntigravityHooksSettings.tsx`. Its review fixed: an empty `-wal` (our own read-only open leaves one) is not activity; activity counts the session's newest hook event, with a 10-minute window mid-turn; an agy Stop carrying an error lands as `error`, one with `fullyIdle: false` is dropped, and the first PreInvocation after a Stop is a prompt; `agyCliDirs()` for a symlinked `~/.gemini`; both tools' hook installers refuse to overwrite or remove a forwarder that is not theirs (`EFOREIGN`) and keep hooks.json's mode. The C2 review also fixed: Codex hooks matched by exact command, one hook at a time, so a user's hook in our group or with a similar name survives; `sameOriginGuard` on the codex-hooks writes; `codexHomes()` in `codex-home.js` for a symlinked CODEX_HOME; a missing main agent created for a poll-imported Codex session; `refreshScript` leaving a foreign file alone; Refresh on the card. The whole-change review also fixed: settings load real env > repo `.env` > the settings file (located after `.env` loads, so `AGENTHUB_SETTINGS_FILE` may live there), a `.env` key `env_owned` too, `writeSetting` replacing every earlier line for its key and writing through a symlink, `validateHomeDir` comparing device+inode with the home and each ancestor, `sameOriginGuard` on both home PUTs, a claude-home PUT answering `routers.missing` (`lanesWithMissingRouter` in `router-apply.js`), CLAUDE_HOME-derived paths read per call in the hook status, `install-hooks.js` and `import-history.js`; `reset-pricing` `{scope: "claude"}` from the Claude tab; `GET /api/sessions?tool=` before the limit; `clear-data` emptying `codex_rollouts` and `antigravity_conversations`, plus `idx_codex_rollouts_thread`; Compaction tagging for claude only, a Codex sub-agent's folded tool event (`_codexChild`) moving no session, agent or lane status, and hooks owning a Codex session's status only while current (`hooksCurrent`: newest event ≥ newest journal write − 2 min, also in `settleActive`); `scriptIsOurs` treating other content, an unreadable file or a directory as foreign, a symlinked hooks.json emptied through the link; agy's quiet window 10 min for any session with hook events (2 min without), a settle completing its agents and clearing waiting flags, a PreInvocation a prompt also with no prior event, a non-active session or a last event >2 min old, a `terminationReason` matching /error|fail/ an error Stop, a file that is not agy's recorded with `session_id ''` and retried on change or at start (anything else next pass), and the session id taken from the file name) + CODEX HOOKS (#40 part C2: `server/lib/codex-hooks.js` installs — only on a click in Settings → Codex → AgentHub → **Codex hooks**, `GET/POST/DELETE /api/settings/codex-hooks` — one group per event (SessionStart, UserPromptSubmit, PreToolUse, PermissionRequest, PostToolUse, Stop, Interrupt, SessionEnd) into `$CODEX_HOME/hooks.json`, all through the stable forwarder `$CODEX_HOME/agenthub-hook.sh` (content rewritten each start by `refreshScript`, command never changes, because Codex trust is keyed on the definition); `scripts/hook-handler.js codex` posts `tool: "codex"`; `routes/hooks.js` creates `tool = 'codex'` sessions with no `transcript_path`, skips every Claude-transcript read for them, maps `PermissionRequest` → `Notification` and `Interrupt` → `Stop`, folds a sub-agent thread's hooks into the root session (`threadOf` in `codex-usage.js`), and `touchLaneFromHook` resolves the lane by (cwd, tool) with detection for claude only; a Codex session with events keeps its hook-given status in `codex-usage.js`, and `codex-ingest.js` skips a lane whose hooks are current. Also from the C1 review: one `codex_rollouts` row per thread, the resume picker lists Claude sessions only, a failed price fetch waits 15 min, a fork skipped until its first own total, 16 MB per read, usage rewritten whole, root-thread cwd/start/model, the startup cleanup and a lane purge scoped by tool, and the codex lane mirror deciding once per lane with a second of future-mtime slack) + CODEX SESSIONS + TOKEN COST (#40 part C1: `server/lib/codex-usage.js` reads every Codex rollout journal into `sessions` (new column `tool`, `'claude'` default, `'codex'` here) and `token_usage` — offset + parse state per file in the new `codex_rollouts` table, a fork's copied parent history skipped by total vector, sub-agent threads summed into the root session, input = input − cached, exact totals via `setTokenUsage`, no `transcript_path`; `startCodexUsagePoll` on the `DASHBOARD_CODEX_POLL_MS` beat; `allRolloutFiles()` exported from `codex-ingest.js`; `pricing-sync.js` also prices `gpt-…`/`o…`/`codex-…` from LiteLLM's `openai` entries and `reset-pricing` syncs right after reseeding; `AGENTHUB_PRICING_SYNC=0` in `scripts/test-server.js`; `client/src/components/settings/PricingSettings.tsx` = the pricing table with a `vendor` filter + **Show all models**, in both tools' AgentHub sub-tabs; `SessionToolBadge.tsx`; the Workspace attach/resume list offering `tool = 'claude'` rows only) + per-tool SETTINGS (#40 part B: Model Pricing, Hook Configuration and CLAUDE_HOME moved from Settings → Dashboard to the Claude Code tab's **AgentHub** sub-tab, `client/src/components/settings/ClaudeAgentHubSettings.tsx` — a tool's own settings go in that tool's tab; `setClaudeHome`/`setCodexHome` refuse any dir that is or contains the OS home (`EBADHOME`, `validateHomeDir` in `server/lib/settings-env.js`) and persist to AgentHub's settings file `~/.config/agenthub/settings.env` (`AGENTHUB_SETTINGS_FILE`; real env > repo `.env` > that file, loaded only when `server/index.js` is main — never the plugin cache's `.env`, which an update wipes), a first CLAUDE_HOME change pinning `DASHBOARD_DATA_DIR` so the DB stays put; CODEX_HOME editable in Codex → AgentHub (`/api/settings/codex-home`, `HomeDirSetting.tsx`, `reapplyCodexLayers` re-writing codex lanes' router layers into the new home); a codex pane is started with the dashboard's `CODEX_HOME` through tmux `-e`, in `server/lib/pty-run.js`) + AI Tools INSTALL/UPDATE (#40 part A2: catalog fields `install`/`update` honoured only in built-in and release files; `server/lib/ai-tool-jobs.js` runs ONE job at a time as an argv — the login shell's npm, a downloaded vendor script via `bash `, or a tool's own updater; `server/lib/ai-tool-versions.js` offers an npm update only when the binary resolves under `npm prefix -g`; `server/lib/ai-tools-sync.js` copies the newest release's catalog daily when it is newer than the build, off with `AGENTHUB_AI_TOOLS_SYNC=0`; `server/lib/ai-tool-icons.js` caches raster favicons server-side; `shellPath()` in `shell-path.js`; `repoUrlFor` in `update-check.js`; six `/api/ai-tools` routes; Install/Update/Update tools in `AiToolsSettings.tsx`) + the pipeline layer going DORMANT (no template ships: `server/data/pipelines/` deleted, `EMPTY_PIPELINE`, `ENOPIPELINE`, test templates under `server/__tests__/fixtures/pipelines/`) and the workflow skills leaving the plugin (`skills/ship-feature`, `skills/ship-feature-lane`, the 18 stage/ship commands, all four `agents/`, `agenthub skills install`, `agenthub lanes agents install`, `agenthub ship-full|ship-quick`, `POST /api/skills/install`, `POST /api/lanes/:id/agents/install`, and the Superpowers auto-install with `server/lib/skill-plugins.js` and its doctor row) + the AI TOOLS catalog (#40 part A1: `server/lib/ai-tools.js` loads one JSON per AI coding CLI from `server/data/ai-tools/` — 38 tools at the time, data from Orca, MIT, trimmed since to 10 — plus overrides in `DASHBOARD_AI_TOOLS_DIR`, detects each by walking PATH, and keeps enable/default in `/ai-tools.json`; `server/lib/shell-path.js` merges the login shell's PATH (`$SHELL -ilc`, 5 s, off with `AGENTHUB_SHELL_PATH=0`) at startup and on Refresh; `/api/ai-tools` GET/refresh/PUT; **AI Tools** (`client/src/components/settings/AiToolsSettings.tsx`, now a modal opened from the Settings tab bar's right edge, `AiToolsModal.tsx`) and the shared `client/src/hooks/useAiTools.ts`, which replaced `LANE_TOOL_LABEL`; the Add-lane modal's **Default ()** + **Other…** picker; the built-in router profile now labelled plain `Native`, rendered **Native ()**; the Claude Code/Codex settings tabs hidden while that tool is disabled; a per-run **Bypass permissions** switch; `POST /api/run` now forwarding `sandbox`/`approval`/`resumeLast`, which it had dropped) + `antigravity` lanes (a third `lanes.tool`, running Antigravity's `agy` CLI; no pipeline, no AI Router, no model/effort picker; the lane card no longer renders a bare tool chip, and the prompt box names the lane's CLI via `LANE_TOOL_LABEL` in `client/src/lib/types.ts`) + a lane whose pane EXITED being released on a server beat (`healAllRunIds` in `server/routes/lanes.js`, every `DASHBOARD_RUN_HEAL_MS`=10 s from `server/index.js`) — a codex/antigravity pane fires no hook, so nothing else re-read it and the lane stayed `running` + a codex Start no longer sending `resumeLast` (only **Resume last session** does) + per-CHANGE-BLOCK review bars (issue #50: `stage-hunk`/`discard-hunk` take an optional `block` — the index of a run of consecutive +/- lines inside the `@@` hunk — and `onlyBlock` in `server/lib/git-diff.js` cuts git's fresh hunk down to it, folding every other block into the side being checked; `changeBlocks`/`LaneDiffChange` in `LaneDiffView.tsx` hang one bar per block, and the arrows land on `data-change-index`) + `codex` lanes losing their pipeline (`codex-ingest.js` mirrors only idle↔running — no stage detection or auto-advance, no `keepDetection`; `codex-nudge.js` deleted; every server start runs `clearCodexPipelines` (`server/lib/lanes.js`: a codex lane's stage columns, `lane_stage_events` and `lane_features` wiped) and `removeStageDoc` (`server/lib/lane-agents.js`: the old `` block taken out of each codex lane's `AGENTS.md`, the file deleted when nothing else is left); `agenthub lanes agents install` installs nothing on a codex lane; the Workspace hides stage, time-on-stage, pipeline/feature pickers and PipelineMap for one) + `info/exclude` entries AgentHub writes into a lane going to the COMMON git dir with the lane's path prefix (`server/lib/git-exclude.js`, used by `router-apply.js` and `lane-mcp.js` — a linked worktree's own `info/exclude` is never read by git, and a subdir lane has no `.git`) + macOS start-at-login (`scripts/launchd-service.js`: the LaunchAgent `~/Library/LaunchAgents/io.smartgift.agenthub.plist`, same API as `systemd-service.js`, chosen by `scripts/service-manager.js` — which the bootstrap, `agenthub service` and `/agenthub-doctor` now require instead of `systemd-service` directly) + v1.8.6 (2026-09-24) + four fixes found by the #37 evaluation, shipped without #37 itself: tmux targets are EXACT (`=name` for has-/kill-/attach-session, `=name:` for pane and option targets, in `server/lib/tmux.js`, `pty-attach.js` and `scripts/cc-update-all-helper.js`) because a bare `-t agenthub-lane-1` resolves by prefix to `agenthub-lane-12` once lane 1 is gone; `getRun`/`killRun` in `server/lib/pty-run.js` refuse any id that is not `agenthub-lane-`, so `DELETE /api/run/main` can no longer kill a user's own tmux session; `POST /api/lanes/:id/router/apply` reports `restart_required` only when the pane's status is `running` (it was always true, since `getRun` returns a `gone` handle); and a lane's `needs_action` is cleared only by the session that raised it (new nullable `lanes.needs_action_session`, `server/routes/hooks.js`) + macOS support, code-audited and untested on a Mac (#22): `server/index.js` deletes a DENYLIST of parent-session variables at startup (`PARENT_SESSION_VARS`: CLAUDECODE, CLAUDE_CODE_SESSION_ID, CLAUDE_CODE_MESSAGING_*, CLAUDE_PLUGIN_ROOT, AI_AGENT, …, plus GIT_EDITOR only when it is Claude Code's `true`; user config such as CLAUDE_CONFIG_DIR/CLAUDE_CODE_USE_BEDROCK/ANTHROPIC_* passes through), because a server started from a Claude Code session handed them through tmux to every lane pane; the `~/.local/bin/agenthub` launcher falling back to `node` on PATH when its pinned node is gone (`linkCli` in `scripts/plugin-bootstrap.js`); `liveServers` ignoring a discovery entry whose `startedAt` predates this boot, so a recycled pid is never a live dashboard; lane cwds stored by real path (`canonicalCwd` in `server/lib/lanes.js`, used by `createLane`, `POST /api/lanes/ensure` and the liveness reaper — `/tmp` becomes `/private/tmp`, a not-yet-existing path through its deepest existing ancestor, existing rows untouched); an `__setExecImpl` seam in `server/lib/session-liveness.js`; and the `INSTALL.md` § macOS section with the Mac checklist + issue #34's empty-repo half — `publish` as a git write (`server/lib/git-ops.js`: `remote add origin -- ` + `push -u origin HEAD`, only in a repo with a commit and NO remote, URL allowlisted by `validateRemoteUrl` to https-without-userinfo/ssh:///scp-like, `origin` removed again on a failed push; `ENOCOMMITS`/`EREMOTEEXISTS`/`EBADURL`), an unborn repo being readable (`gitFacts`/`statusCounts` in `server/lib/worktree.js`: `head: null`, new `unborn`, branch via `symbolic-ref`), `POST /api/lanes/worktree` answering `400 EEMPTYREPO` instead of leaving a `failed` row, and `LaneGitBar.tsx` gaining **Publish…** (no remote), **Publish branch** (remote but no upstream) and a commit-first hint (unborn) while `AddLaneModal.tsx` steers an empty repo to Repo mode + a dashboard fetch/pull/push that needs a git LOGIN asking for one in the git bar (issue #45: `409 EGITAUTH` with `auth`, resent with `credentials`; AgentHub's own shared store `server/lib/git-credentials.js` + helper `server/lib/git-credential-helper.js`, `/api/git-credentials`, Settings → **Git credentials** = `client/src/pages/GitCredentialsConfig.tsx`) + MERGE and conflict resolution from the dashboard (issue #45 part B: `merge`/`resolve`/`merge-commit`/`merge-abort` in `server/lib/git-ops.js`, `gitFacts.merging`/`conflicts`, the git bar's **Merge into current** and diverged-upstream **Merge**, the Changes panel's merge banner and **Merge conflicts** group, and the per-block conflict view `client/src/components/lanes/LaneConflictView.tsx` over the pure parser `client/src/lib/conflicts.ts` — keep mine/theirs/both per block, saved through the whole-file editor's `PUT /git/file` + `resolve done`, no new API) + drag-and-drop of an image onto the lane terminal (`TerminalView.tsx` uploads to `POST /api/run/drop-image` in `server/routes/run.js` — raw body, png/jpeg/gif/webp only, random name under `/agenthub-drops-/`, never a name from the request — then `term.paste()`s the returned path, which is how `claude` attaches it) + Claude Opus 5.5 getting its own pricing row (`claude-opus-5-5%` at 4/20/0.2/5/8, fast 8/40 — `claude-opus-5%` had been pricing it as Opus 5) + AUTOMATIC pricing (`server/lib/pricing-sync.js`, started in `startBackgroundServices`: a model family used in `token_usage` with no `%` row of its own is INSERTED from LiteLLM's public price list — since followed daily for the rows it owns, see the entry above — off with `AGENTHUB_PRICING_SYNC=0`) + the Changes panel at **Full screen** moving the whole Files view (finder + expand-all + tree, one `browsePane`) into a left SIDEBAR and dropping the Files tab and in-place editing (`full` prop on `LaneChangesPanel` from `Workspace.tsx`; a whole file opens for READING and enters the editor only on a click on its text — `onEditAt` on `LaneFileView`, caret at the clicked line, a drag-selection is not a click; Cancel returns to reading, and only a diff keeps an Edit button) and height-locking the page (`viewportLocked` also true at full, else the panel shrank to content with no live run) + the overview splitting into **Staged Changes** / **Changes** groups in the `work` range + a changed tree row tinted whole (`treeMark` tone carries a `bg-status-*/10`) + the Files tree listing untracked-not-ignored files and carrying each entry's git status (`treeStatus`/`treeEntries`/`TREE_LS` in `server/lib/git-diff.js`: file `status` = porcelain code, folder `changed: true`, from ONE `laneStatus` per tree read; `treeMark` in `LaneChangesPanel.tsx` paints M/U/A/D/R) + split panes on ONE lane (every Workspace pane shows the strip's selected lane, each in its own tmux session: `slot` 0–3 on `POST /api/lanes/:id/start`, `agenthub-lane-` for slot 0 = `run_id`, `agenthub-lane--` for 1–3 — `runIdForLane(id, slot)`/`MAX_PANE_SLOT` in `server/lib/pty-run.js`, the `-[1-3]` suffix accepted by `pty-attach.js` and `websocket.js`; `stop`/`reset`/`remove` kill every slot via `laneRunIds`; a pane's Active runs hides what the other panes show; no per-pane lane picker) + the server suite running on a PRIVATE tmux server (`npm run test:server` = `scripts/test-server.js`: drops `TMUX`, sets `TMUX_TMPDIR` to a temp dir, kills that server after — suites spawn and kill real `agenthub-lane-` sessions with ids from their own DB, and on the default server that killed the user's real lane panes, including the one running the tests) + the whole-file EDITOR (`PUT /api/lanes/:id/git/file`, `server/lib/lane-file-write.js`: the read path's guards plus existing-text-file-only and a `baseSha` stale check → `409 ESTALE`; `readFileAt` returns that `sha`) + the per-hunk controls moving into an editor-style review bar under each hunk's last changed line (`HunkBar` in `LaneDiffView.tsx`: ^/v, `n of N`, Undo, Keep) + **Unstage all** on the ONE commit/Stage-all row + Changes sizes Narrow (30%) / Medium (50/50) / Full screen, no 70/30 + the lane card's needs-you banner gone (the strip tile keeps it) + per-HUNK Stage/Undo in the Changes panel's Review and single-file views (`stage-hunk`/`discard-hunk` in `server/lib/git-ops.js`: the hunk is named by its `@@` line and re-matched against a FRESH `git diff -U HEAD` by `hunkPatch` in `git-diff.js`, then `git apply --cached` / `git apply -R` from a temp file — `409 EHUNKGONE` when it moved, `400 EUNTRACKED` on an untracked file; `discard-hunk` is the SECOND irreversible git action and carries the same `confirm: true` gate and two-click UI; the controls reach every `HunkHeader` through a context in `LaneDiffView.tsx`), each Review file header sticky with its own stage/unstage/discard (`FileActions`, shared with the overview row) + the Changes panel gaining a FULL-width step (`CHANGES_SIZES` in `Workspace.tsx`: narrow → 50/50 → 70/30 → full → closed; at full the `lane-detail` section gets `hidden` — CSS, so the one console render site stays mounted), **Stage all** plus the staged-only commit box ABOVE the overview list and sticky (as is the Files finder), and **Expand all folders** being ONE `GET /api/lanes/:id/git/tree-all` (`listTreeAll` in `server/lib/git-diff.js`: one `ls-files`, every level shaped like `listTree`'s) instead of a request — and a `git ls-files` — per folder + the lane's git section MOVING from the lane card to the top of the Changes panel (`client/src/components/lanes/LaneGitHeader.tsx`: `useLaneGitFacts`, the working-copy line, `LaneGitFilesPanel` and `LaneGitBar`, sharing ONE facts read with `LaneChangesPanel`, which refreshes it after every stage/unstage/discard/commit while a git bar action re-reads the diff; `onNewLane` comes from `Workspace.tsx`, and `LaneCard` has no git row and no `onNewLaneFromRepo` any more) and the git bar losing its commit UI — the overview's staged-only box is the ONE commit path, the server's `commit` without `only` is unchanged; the git line and its icon-only actions are ONE row in one card, the panel toolbar ONE wrapping row, and `LaneCard` hides itself (`empty:hidden`) when it has nothing to show + the run form's `/` and `@` autocomplete being REAL again (`PromptEditor` in `client/src/components/run/RunSetup.tsx` — the `2f39f4e` rewrite had left a bare textarea: `/` offers the run cwd's project + user commands and skills, via `api.ccConfig.commands|skills(scope, cwd)`, plus the CLI built-ins, claude lanes only; `@` offers `GET /api/run/files`; Enter picks while the list is open and sends otherwise) + the console's **Active runs** listing only the CURRENT lane's runs and external sessions (`LaneConsolePane.tsx`: runs by `laneId`, sessions by the longest path-boundary lane cwd, `owningLaneId`) + v1.8.3 (2026-09-23) + `GET /api/settings/info` reporting `server.memory.heapLimit` (V8's `heap_size_limit`), which the Dashboard's V8 Heap bar and Health Score now divide by instead of `heapTotal` + every `claude plugin install|update -y` (`cc-plugin-cli.js`, `skill-plugins.js`, both detached update helpers, `/agenthub-update`) retrying WITHOUT `-y` when an older Claude Code answers `unknown option '-y'` + `LaneDiffView.tsx` WINDOWING a diff above `WINDOW_THRESHOLD` (500) items at a fixed `ROW_PX` row height (`visibleRange`, spacers, a `ch` min-width off the longest line; at or below the threshold the DOM is unchanged) and clamping its mirrored split scroll (`clampScroll`/`follow`) + a git action that git itself refuses (a numeric exec exit status) answering 409 `EGIT` instead of 500 with a number for a code (`gitErrorCode` in `server/routes/lanes.js`), and one `parseNumstat` (`server/lib/git-diff.js`) shared by the Changes panel and `gitFacts.files` + the Dashboard heatmap ramp mixing `--accent` into `--surface-0` so it reads on every theme + the lane git surface being HARDENED (`server/lib/git-diff.js`, `server/lib/git-ops.js`, `server/lib/worktree.js`: every lane git call runs with `GIT_LITERAL_PATHSPECS=1` and `-z` porcelain, is scoped to the LANE's cwd — a subdir lane sees and acts on only its own paths, `gitFacts.files` included — and reads through `lanePath` at every rev; any file whose basename is `settings.local.json`, the lane router key file, is refused as `ESECRET`/403 on disk and at every rev; `commit` refuses with `ESTAGEDOUTSIDE` while anything outside the lane is staged, checked with `--no-renames`; `push` without a resolvable remote is `ENOREMOTE`; git writes serialize on the repo's absolute git dir, not the lane id) + the xterm terminal following the selected DARK theme (`client/src/lib/terminalTheme.ts`, live via `term.options.theme`) and chart colours using structural tokens + the Add-lane modal asking `POST /api/lanes/ensure` with `exact: true` (#42) — without it a directory NESTED inside an existing lane resolves to that parent lane with `created: false`, so the modal closed on the parent and no new card ever appeared; `exact` counts only a lane on that very directory, and a `created: false` answer now names the lane that owns the folder instead of closing. The CLI, the run console and Sagi keep the parent lookup + Settings → **Appearance** (`client/src/components/settings/AppearanceSettings.tsx`: mode Dark/Light/**System** — `prefers-color-scheme`, followed live — plus one NAMED theme remembered per mode: dark Default/Darcula/Apple Dark, light Default/Apple Light/IntelliJ Light/Canvas). A theme is a CSS-variable override block in `client/src/index.css` keyed by `data-theme` on `` (`.dark[data-theme=…]` / `[data-mode="light"][data-theme=…]`, `data-mode` is set on `` too), and `--tint-base`/light `--syn-plain` are now `var(--surface-3)`/`var(--fg-primary)` so no theme restates them; `hooks/useTheme.ts` is a `useSyncExternalStore` reader over `localStorage` (`theme` = `dark|light|system`, `agenthub.theme.dark|light`) that caches NOTHING in the module, so the Sidebar and Settings can never disagree — do not reintroduce per-instance `useState` there; an inline `