fix: event-loop freezes, plugin-install boot crash, tmux attach/scrollbar, Codex transcripts
- GET /api/workflows: one LEAD() window pass in a read worker (read-pool.js, workflow-stats.js); idx_events_session restored, idx_events_tool added - compat-sqlite gains function/pluck/readonly so a plugin install without a better-sqlite3 build boots again; antigravity-usage uses db.js's Database - install-hooks adds no checkout hooks while the agenthub plugin is enabled (events were doubled) - terminal attach runs the live tmux server's own binary; the pane scrollbar gets a visible 2-column style, and claude panes start with CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 so tmux keeps history to scroll - Codex sessions' conversation in GET /api/sessions/:id/transcript - Codex/Antigravity sessions claimed by their workspace without hooks - UI-audit follow-ups, docs and OpenAPI
This commit is contained in:
+27
-1
@@ -1071,6 +1071,31 @@ records anything — a pipeline going unused has to be visible as such.
|
||||
|
||||
### Workflows
|
||||
|
||||
#### Workflow aggregate bundle
|
||||
|
||||
```http
|
||||
GET /api/workflows?status=active|completed§ions=stats,toolFlow
|
||||
```
|
||||
|
||||
The Workflow analytics page's eleven sections in one payload: `stats`,
|
||||
`orchestration`, `toolFlow`, `effectiveness`, `patterns`, `modelDelegation`,
|
||||
`errorPropagation`, `concurrency`, `complexity`, `compaction`, `cooccurrence`.
|
||||
`status` scopes every section to sessions of one lifecycle status; omit it (or
|
||||
pass `all`) for no filter. `sections` computes and returns only the named ones —
|
||||
Dashboard → Health asks for the seven its cards read. `stats.topFlow` is the
|
||||
head of `toolFlow.transitions` (one window pass over the events for both).
|
||||
|
||||
The bundle scans every event, so it is computed in a worker thread on its own
|
||||
read-only connection (`server/lib/read-pool.js`) and never stalls another
|
||||
request, hook or websocket meanwhile. Identical requests in flight share one
|
||||
run. `AGENTHUB_READ_WORKER=0` computes it inline instead.
|
||||
|
||||
**Status codes:**
|
||||
- **200** — the bundle, or the requested sections of it
|
||||
- **400** `{ "error": { "code": "INVALID_SECTIONS", "message": "sections must name some of: …" } }` — `sections` names no known section (unknown names beside known ones are ignored)
|
||||
- **500** `{ "error": { "message": "…" } }`
|
||||
- **503** `{ "error": { "code": "EREADTIMEOUT", "message": "read query took over 60000 ms" } }` — the run passed `AGENTHUB_READ_TIMEOUT_MS` (default 60000); its worker is replaced for the next request
|
||||
|
||||
#### Skill and plugin usage
|
||||
|
||||
```http
|
||||
@@ -1319,7 +1344,8 @@ Body: any of `name`, `status`, `ended_at`, `metadata` and `archived`.
|
||||
the session leaves its workspace's lists and nothing is deleted. Ownership
|
||||
(`lane_id`) is not patchable: `bindRunSession` (`server/routes/hooks.js`)
|
||||
stamps the session a dashboard pane holds with that pane's lane, first writer
|
||||
wins.
|
||||
wins (`claimToolSessions` in `server/lib/dashboard-runs.js` does it for a
|
||||
Codex/Antigravity session from the run that was live in its folder).
|
||||
Returns `{ session }`.
|
||||
|
||||
---
|
||||
|
||||
+1
-1
@@ -211,7 +211,7 @@ When the server is down, **read-only commands automatically fall back to reading
|
||||
| ------- | ----------- |
|
||||
| `agenthub import rescan` | Re-scan the default `~/.claude/projects` tree (idempotent; prints imported / backfilled / skipped / errors) |
|
||||
| `agenthub import path <dir>` | Recursively import every `.jsonl` under an absolute directory (`~` is expanded server-side) |
|
||||
| `agenthub import-data <file.json>` | Restore a full dashboard export produced by `agenthub export` (or **Settings → Export data**). Idempotent and non-destructive — sessions already present are skipped whole, so it safely **consolidates several machines** into one dashboard. The file path is resolved to absolute and read server-side |
|
||||
| `agenthub import-data <file.json>` | Restore a full dashboard export produced by `agenthub export` (or **Settings → Export data**). Idempotent and non-destructive — sessions already present are skipped whole, so it safely **consolidates several machines** into one dashboard. The file is uploaded to the server, like the dashboard's Restore button |
|
||||
|
||||
### Remote Sources
|
||||
|
||||
|
||||
+12
-1
@@ -212,7 +212,7 @@ CREATE TABLE sessions (
|
||||
| `transcript_path` | TEXT | YES | Absolute path to the session's JSONL transcript. Written by `routes/hooks.js` on the first event that carries it (subsequent events no-op via a SQL guard) and read by the periodic compaction sweep — so the sweep touches only active session rows instead of scanning the entire `events` table for `json_extract(data,'$.transcript_path')`. Backfilled once from `events` by the `db.js` migration |
|
||||
| `source` | TEXT | NO | Data source this session was captured from. `'local'` for this machine's own Claude Code history (the default); otherwise the `remote_sources.id` of the remote SSH machine it was pulled from. Powers the `sources` query filter on `/api/sessions`, `/api/events`, `/api/agents`, `/api/stats`, and `/api/analytics`, and the `sources` facet on `/api/sessions/facets`. Indexed by `idx_sessions_source` |
|
||||
| `tool` | TEXT | NO | The AI coding CLI that produced the session. `'claude'` for every hook-created and imported row (the default); `'codex'` for a session ingested from Codex's rollout journals by `server/lib/codex-usage.js` (see `codex_rollouts`) or created by a Codex hook (`tool: "codex"`, `server/lib/codex-hooks.js`) — a Codex session keeps the status its hooks give it while they are current (its newest event at or after its newest journal write minus two minutes); once they stop (removed, untrusted) the journal decides again. A codex row has no `transcript_path`, and, going by the journal, its `status` is `active` only while one of its journals was written in the last two minutes. `'antigravity'` for an agy CLI conversation, ingested by `server/lib/antigravity-usage.js` (see `antigravity_conversations`) or created by an Antigravity hook (`server/lib/antigravity-hooks.js`); no `transcript_path`, and `active` until it has been quiet — no write to its conversation file (or a non-empty `-wal`) or transcript, no hook event — for ten minutes when it has hook events, two minutes when it has none; it then settles to `completed`, its agents too. The startup cleanup of stale `active` sessions and a workspace purge touch only the matching tool's rows (a purge takes the sessions of the workspace's own `tool`). The Workspace's attach/resume list offers `'claude'` rows only |
|
||||
| `lane_id` | INTEGER | YES | The workspace that OWNS the session — the only sessions a workspace lists: the lane whose dashboard pane held it, stamped by `bindRunSession` (`server/routes/hooks.js`) from the hook's `run_id` — first writer wins, a nested `claude -p` excluded. NULL for a session no workspace started (a terminal, an import, a tool with no hooks). No FK: removing a workspace leaves its sessions. Backfilled once from `dashboard_runs`. Indexed by `idx_sessions_lane` |
|
||||
| `lane_id` | INTEGER | YES | The workspace that OWNS the session — the only sessions a workspace lists: the lane whose dashboard pane held it, stamped by `bindRunSession` (`server/routes/hooks.js`) from the hook's `run_id` — first writer wins, a nested `claude -p` excluded — or, for a Codex/Antigravity session, by `claimToolSessions` (`server/lib/dashboard-runs.js`) from the same-tool run that was live in its folder when it started. NULL for a session no workspace started (a terminal, an import, a tool with no hooks). No FK: removing a workspace leaves its sessions. Backfilled once from `dashboard_runs`. Indexed by `idx_sessions_lane` |
|
||||
| `archived_at` | TEXT | YES | When the user archived the session out of its workspace's lists (`PATCH /api/sessions/:id {archived}`). Hides only — nothing is deleted |
|
||||
|
||||
**Constraints:**
|
||||
@@ -607,6 +607,17 @@ CREATE INDEX idx_agents_status ON agents(status);
|
||||
-- before inserting; on a subagent-heavy re-import this drops a large sweep from
|
||||
-- tens of seconds to sub-second.
|
||||
CREATE INDEX idx_events_agent_type ON events(agent_id, event_type);
|
||||
|
||||
-- A session's events in insert order. NOT a prefix of idx_events_session_type
|
||||
-- (not id-ordered): every hook's duplicate guard reads a session's latest event
|
||||
-- through it. Dropping it (ad6e801) made that read sort the whole session per
|
||||
-- hook and made a correlated "next tool event" query quadratic — minutes on a
|
||||
-- 29k-event session, the event loop frozen for every page.
|
||||
CREATE INDEX idx_events_session ON events(session_id, id);
|
||||
|
||||
-- Per-tool counts (analytics, workflows) and the events filter's tool list
|
||||
-- read this alone instead of every event row (~300 ms -> ~20 ms at 233k rows).
|
||||
CREATE INDEX idx_events_tool ON events(tool_name);
|
||||
```
|
||||
|
||||
### tool_executions Indexes
|
||||
|
||||
@@ -260,6 +260,8 @@ AGENTHUB_GEMINI_DIR= # where agy keeps its data (default ~/.g
|
||||
|
||||
# Pricing and saved settings
|
||||
AGENTHUB_PRICING_SYNC= # 0|false|off disables automatic pricing from LiteLLM (default on)
|
||||
AGENTHUB_READ_WORKER= # 0 runs heavy aggregates (GET /api/workflows) inline instead of in the read worker (default worker)
|
||||
AGENTHUB_READ_TIMEOUT_MS=60000 # a read-worker job past this answers 503 EREADTIMEOUT and gets a fresh worker
|
||||
AGENTHUB_SETTINGS_FILE= # where dashboard-saved settings persist (default ~/.config/agenthub/settings.env);
|
||||
# load order: real env > the checkout's .env > this file
|
||||
|
||||
|
||||
+6
-2
@@ -168,8 +168,12 @@ npm run install-hooks
|
||||
> `~/.claude/settings.json.agenthub-bak`) — and since v1.6.17 a settings file that
|
||||
> still carries them also blocks the bootstrap's fast path (`hasLegacyHooks`),
|
||||
> because the strip runs only in the full pass and a healthy install skipped it
|
||||
> every session, keeping the duplicates alive indefinitely. `install-hooks.js` warns when it detects
|
||||
> a plugin install, and `/agenthub-doctor` reports any duplicates that remain. See
|
||||
> every session, keeping the duplicates alive indefinitely. And `install-hooks.js` itself —
|
||||
> run by hand or by every checkout server start (`npm run dev`, `npm start`) — adds nothing
|
||||
> while settings.json enables `agenthub@…` and removes every `hook-handler.js` entry it finds
|
||||
> instead (same backup); it used to put them straight back after each strip, so a dev server
|
||||
> beside the plugin kept ~40% of events doubled. Settings reports such hooks as installed
|
||||
> (`source: "plugin"`). `/agenthub-doctor` reports any duplicates that remain. See
|
||||
> [PLUGINS.md](PLUGINS.md).
|
||||
|
||||
> [!IMPORTANT]
|
||||
|
||||
@@ -406,6 +406,7 @@ The dashboard web UI merges workspaces and runs into a single **Workspace** page
|
||||
- **Terminal** — a real interactive terminal (xterm.js) displaying the tmux session's PTY output, with full support for interactive commands, editors, and pagers. A live run keeps its rendered history and scroll position when scrolling. It grabs the keyboard the moment it appears, so typing goes into the pane without clicking it first. A pane has **no run form of its own**: a session with a prompt starts from the workspace home's prompt box. There is no **New run** button: when a pane's process ends (a double Ctrl+C out of `claude`, `exit`, a crash) its pane closes; the last one shows the next open tab, or the home when none is left. A pane with nothing to show offers **New session**, which opens the home too.
|
||||
Keyboard: the terminal keeps Tab and Shift+Tab (`claude` cycles its permission mode on Shift+Tab), so **Ctrl+M** toggles *Tab moves focus* — VS Code's way out of its terminal — and Tab then leaves the pane; **Cmd+F** (macOS) / **Ctrl+Shift+F** opens a find bar. URLs open in a new tab; a selection tmux copies (OSC 52) lands in the browser clipboard, and that path is write-only — nothing in the pane can read your clipboard. `agenthub.terminal.screenReader` = `"1"` in `localStorage` turns on xterm's screen-reader mode. While the socket is down a **Reconnecting…** overlay says keystrokes are not reaching the pane; it re-attaches by itself. In the session view, outside a terminal or a text field, `Alt+1`–`Alt+4` focuses pane 1–4, `Alt+\` splits the focused pane right with a new terminal, and `Alt+W` closes the focused pane (its session stays a tab).
|
||||
- **Clipboard in a pane** — the workspace's tmux session is started with the graphical session's own `DISPLAY`/`WAYLAND_DISPLAY`/`XAUTHORITY`/`XDG_RUNTIME_DIR` (read from `systemctl --user show-environment`, this process's own values winning; `server/lib/desktop-env.js`). The dashboard normally runs as a systemd *user* service, which has none of them, so a pane it started had no clipboard at all and `claude` answered "no image found in clipboard" on every image paste while the same CLI pasted fine in a terminal. Only those four names are forwarded, and only into panes this dashboard spawns — an already-running pane keeps the environment it was started with, so it needs a restart to gain one.
|
||||
- **Scrollbar in a pane** — every pane gets tmux's `mouse on` and `pane-scrollbars on` with a visible style (`pane-scrollbars-style fg=brightwhite,bg=black,width=2`, `enableMouse` in `server/lib/tmux.js`; tmux 3.6+). tmux keeps no history and draws no scrollbar on the alternate screen, so a `claude` pane is started with `CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1` (`spawnRun` in `server/lib/pty-run.js`) and renders inline even when `~/.claude/settings.json` says `"tui": "fullscreen"`. That setting still applies to `claude` in your own terminals; a pane started before this change keeps fullscreen until it is restarted.
|
||||
- **Tabs** — the session view's console is a tab bar (`client/src/components/run/ConsoleTabs.tsx`), one tab per live tmux session of the workspace — at most four, its slots 0–3: slot 0 is the workspace's `agenthub-lane-<id>` (its `run_id`), slots 1–3 are `agenthub-lane-<id>-<slot>`. A tab shows its AI tool's icon and name (or **Terminal**); the active tab is the focused pane's, which is the URL's `?run=`, so the Changes panel and Analyst follow it, and a tab another pane shows carries a grey underline. A click shows a tab in the focused pane (or focuses the pane already showing it), **Ctrl/Cmd+click** — on a tab or on a `+` item — opens it in a split, and a tab drags onto the panes. Every tab with a live run stays mounted and those no pane shows are hidden with CSS, so switching tabs never drops a terminal. A tab's **×** stops its session (`DELETE /api/run/<runId>`) and closes its pane; with no pane left it moves to the next tab, or the home. The **+** menu opens a new tab in the first free slot, which the server picks (`slot: "auto"`, under the workspace lock): **New terminal** (your own default shell in the workspace folder — `POST /api/lanes/:id/start {slot, shell: true}`, recorded as tool `shell`; it is not an agent, so the workspace's `run_id`/`status` are left alone), each installed, enabled AI tool (started bare, on its default model, no prompt), and **Agent settings…** (Settings → Agents). With four tabs open **+** is disabled. EVERY tab is bound to the sidebar's selection. A tab has no header of its own (no title, no **Active runs** list, no **New run**): other sessions are opened from the sidebar or the home's Recents. (The old fixed 1/2/4 split layout and its `agenthub.workspace.splitView` key are gone; **Split panes** replaces them.)
|
||||
|
||||
- **Split panes** — the console splits like VS Code (`client/src/components/run/PaneGrid.tsx`, model in `client/src/lib/paneLayout.ts`): a binary tree of splits, each side by side or stacked at a ratio, whose leaves are the workspace's slots — at most four panes, one per live session, a session never shown twice. **Ctrl/Cmd+click** a tab, a `+` item or a sidebar session row opens it to the right of the focused pane (the intent rides on the navigation's state, never in the URL); with more than one pane each gets a slim header: **drag** it (or a tab) onto another pane — its edge splits there, its middle swaps the two —, its **▾** shows another live session in it (swapping with the pane that showed it), **Split right / Split down** puts a not-shown session or a **New terminal** beside it, and **×** closes the pane only (its session stays a tab). Dividers drag (or move by arrow keys) between 10% and 90%. Clicking in (or tabbing into) a pane focuses it, `?run=` follows — so the right panel, Analyst and the sidebar read the focused session — and the keyboard goes with it: only the focused pane's terminal takes it when it appears. A pane closes when its terminal reports that its session ended (exited, closed from another browser); the run list itself only prunes a remembered layout on a workspace's first read, and a pane opened here whose session never got listed — one empty read, which the server returns on any tmux error, never wipes a split. A session ending while the home or a replay is shown only closes its pane; the URL stays. The layout is a viewer preference kept per workspace in `localStorage` (`agenthub.workspace.paneLayout`), never on the server. Every pane is an absolutely placed sibling keyed by its slot, so splitting, swapping or resizing never re-parents — and so never remounts — a live terminal; the session view itself fills the page edge to edge, its tab bar and the right panel's activity bar one header strip.
|
||||
|
||||
Reference in New Issue
Block a user