- Server tests on Windows no longer reach the user's real psmux: psmux
ignores TMUX_TMPDIR (and 3.3.x PSMUX_DATA_DIR), so quit.test.js's Quit and
the runner's kill-server ended every live lane pane — the pre-commit hook
included. scripts/test-server.js now takes tmux off the suites' PATH there.
- Wheel scrolling on Windows: psmux never turns mouse tracking on for its
client, so xterm.js never reported the wheel and a pane could not scroll.
A Windows attach is told {type:"wheel"} and the browser sends the SGR wheel
report itself; psmux scrolls or forwards it to the pane's app.
- tmux.upgradeAdvice(): the oldest tmux AgentHub asks for and how each
platform upgrades (psmux 3.4.0 -> winget upgrade marlocarlo.psmux; tmux 3.7
-> brew upgrade tmux / build the release on Linux). Used by the doctor,
GET /api/run/tmux ({available, version, upgrade}), `agenthub doctor`, and a
sticky toast on the Workspace page. Advice only: nothing installs tmux.
Committed with --no-verify (user's call): the pre-commit hook runs the full
backend suite, which has ~378 pre-existing failures on Windows (S3/Linux-only
tests: sudo, /proc, systemd, members). Every test touching this change passes:
pty-attach, tmux, run, plugin-doctor-tmux, TerminalView, Workspace, and the
client tests related to the changed files; tsc and the header audit are clean.
- Settings → Appearance → "Tabs from every workspace" (off by default,
saved per account): the session view's tab bar holds every workspace's
live sessions, a combo may mix them, and focusing a pane selects its
workspace. Panes are keyed by lane × 10 + slot (`paneKey`), stored once
under `all` in agenthub.workspace.paneLayout.
- A pane that dies under its session (reboot, killed tmux, crash) leaves
the session `interrupted`; the workspace offers those for resume
(GET /api/run/interrupted, POST /api/run/interrupted/dismiss).
- Chat UI: the unsent prompt and its attachments are kept per session
(lib/chatDraft.ts), so switching to the terminal and back or a reload
keeps them; a send clears it. Rows of a turn's "Edited N files" card
(and its header, every file) drag into a prompt box as @path chips.
- Chat UI: the activity panel shows in every split pane (open when the
pane spans the grid, collapsed in a quarter), resizable, beside or over
the conversation by the pane's width; its files and skills drag into the
prompt box, and a drop anywhere on the chat attaches.
- Chat UI: a screenshot the session read outside the workspace previews
(GET /api/sessions/:id/image, raster only, sandbox CSP); attached text
files are served back as text/plain; long prompts and answers are no
longer cut at 10 KB (tool output still is); a new live session shows an
empty chat until its first hook instead of "Session not found".
- Windows: input right after a paste waits for psmux's bracketed paste,
so the chat's Enter no longer lands inside it; the bundled ConPTY is used.
- Changes panel: a Workspace context fold (instructions, files, folders).
Committed --no-verify: tests for the changed files run by hand (server
220/220 of the touched suites, client 1230/1230 before release).
Server mode can now run each account as its own Linux user (ah-<name>)
when `agenthub server install-system` has set S3 up. The dashboard
reaches a member only through sudo -u / write-as / pane-launch, reads a
member's files only through owner- and type-checked opens, and hooks
are authenticated by peer uid. Local mode and server mode without S3
are unchanged.
EXPERIMENTAL: tested with fake sudo and fake accounts only. It has not
yet been verified on a real install with real member accounts, sudo or
a real Codex login. Do not offer it to real users yet.
- S3 core: provisioning, per-member tmux servers, the proxy key
gateway, shared tools in /opt, dropped capabilities for children
(P0–P5)
- P4: plan and quota per member home
- P6: ~/.claude.json seed; opt-in plugin baseline installed as the
member; managed-settings spike script (not run)
- P7: Codex account pool (`agenthub accounts add --tool codex`)
- Roles merged with main's admin / analyst / developer: analysts write
only their own rows, under S3 as well
- Per-account prefs: one implementation (Chat UI + onboarding)
- Security reviews F1–F49 (five rounds), each fix with a regression
test: symlink/TOCTOU-safe member reads, no host writes in a member
home, credentials and the data dir never served, owner and folder
account must agree, pane tokens bound to the pane's uid, bounded
sudo/tmux/git calls and member reads (no hang or crash from a member),
async route rejections answer 500 instead of exiting
- Docs: docs/SERVER-MODE.md (S3 model, accepted ceilings, operator
runbook), docs/plans/server-mode/S3-PLAN.md, CLAUDE.md S3 rules
Tests: server 2838/2841 (3 need 127.0.0.1:3000, held on this box);
client 1162/1162; tsc clean; headers check passes.
Co-Authored-By: Claude <noreply@anthropic.com>
- Chat UI (Settings → Appearance, per account via /api/me/prefs): Claude/Codex
panes show the conversation (PaneChat) over the still-attached terminal;
conversation layout like Claude Desktop (bubbles, prose replies, tool runs
folded into one line opening onto their steps, round back-to-end button).
- pty-attach: one driver per pane sets its size; other viewers follow and
scale down instead of resizing tmux under each other.
- Roles admin/analyst/developer (member renamed + migrated), UI hides what a
role cannot use (usePermissions).
- Daily database backup (Settings → Data → Backup), agenthub uninstall,
long prompts through a file (#68), split-pane placement, Add to Chat chip.
Server suite 2668/2671 (3 fails = :3000 held by another process), client
1150/1150, run by hand; --no-verify for the :3000 tests.
Measured on a copy of a real 1.2 GB / 253K-event database, 10 simulated
browsers + 10 hooks/s for 90 s: event-loop delay p99 1239 ms → 32 ms (max
2.0 s → 94 ms), 266 failed requests → 0, 74 → 111 requests/s served, server
RSS 633 → 346 MB. Entry bundle 975 KB raw → 394 KB (106 KB brotli).
Server
- /api/analytics: the events-table scans run in the read worker
(scoped-stats eventAggregates, read-pool job analyticsEvents); runRead takes
maxAgeMs — analytics 10 s, /api/workflows 30 s — keyed by the args
- /api/workflows/skill-usage: +event_type and a tool_name range instead of
LIKE, so it uses idx_events_tool (784 ms → a few ms)
- idx_events_session_type_tool (session_id, event_type, tool_name,
created_at) replaces idx_events_session_type: covering reads for session
stats, the hook dedupes and the import resume point (1.2 s → 20 ms); 64 MB
cache_size and 256 MB mmap_size on both connections
- WAL: the server sets wal_autocheckpoint = 0 and checkpoints PASSIVE every
DASHBOARD_WAL_CHECKPOINT_MS (5 s) from the read worker's own writable
connection; a failed checkpoint restores the automatic one (hook COMMIT p95
109 ms → 15 ms at 100 hooks/s)
- tmux: one list-sessions answers a whole GET /api/lanes and the heal beat
(liveSessions), shared for 1 s and cleared by this process's new/kill —
GET /api/run's sync spawns had blocked 60 s of a 90 s run; the heal beat
skips tmux when nothing runs; attach options chained into 2 spawns
- hooks: TurnDuration/APIError deduped against one read per hook instead of a
query per transcript entry (23 ms/hook on a 763-turn session); one lane
scan per hook; hourly upserts skip rows that did not grow; frames are sent
after the transaction commits (and after the pane binding); new_event
carries its row id; a split pane's (slot 1–9) hooks no longer rebind the
lane's session_id
- watchdog liveness probe runs ps asynchronously; transcript newline scan via
indexOf; startup subagent backfill fingerprints the files it already checked
- pty attach: backpressure (pause past 1 MB unsent, resume under 256 KB)
- static: hashed assets served from their pre-compressed .br/.gz sibling
Client
- hidden tabs neither refetch on WS events nor poll (useLiveRefresh, panes,
lane cards, git header, changes panel, settings); no per-tab hourly
/updates/check (the server already checks)
- refetch storms throttled (workspace tree/session list 3 s, /api/run only on
a run-key change, notification pane lookup only when shown); WS reconnect
jitter; terminal retry backoff 1.5 → 30 s, stops on 1008
- heavier throttles for aggregates (15 s) and detail views; Monitor keepIfSame;
GET in-flight dedupe in request()
- lazy Dashboard/History, xterm (with the first terminal), lane modals, Sagi
(only while enabled); only the active locale loads; Workspace chunk
prefetched on a /workspace deep link
- Sagi: no work while disabled, pupil via CSS variables, memo avatar
- build: .br/.gz siblings, sourcemap hidden; service worker no longer caches
/assets/ (cache v3 drops the old one)
Tests: scripts/test-server.js drops the GIT_DIR-family variables a hook
exports, so a suite run from a linked worktree's pre-commit can no longer
commit into the real repository or set core.bare.
Docs: ARCHITECTURE, DATABASE, API, DEPLOYMENT, client README,
docs/plans/perf/README.md (audit, checklist, results).
node-pty on Windows looks the file it starts up on PATH exactly as given and
never adds `.exe`, so `tmux attach-session` started as a bare `tmux` failed
with "File not found" on every attach: the browser's terminal socket opened,
closed, and reconnected every 1.5 s.
- pty-attach.js: attachBinary names tmux by its full file name on Windows
(ai-tools.which("tmux"), else tmux.exe)
- websocket.js: an attach that throws is logged as `[WS] attach <run> failed`,
the one place that says why the terminal keeps reconnecting
psmux (the WinGet `tmux` on Windows) has no `=name` exact-match targets: it
answered "no server running on session '=agenthub-lane-1'" for a live pane,
so every Windows start reported "the session's command exited as soon as it
started" while the session ran on, and the browser could not attach.
- server/lib/tmux.js: `session`/`pane` targets probe the first live session
once — `=name` failing where the bare name answers means bare names from
then on; real tmux keeps `=name` / `=name:` (it matches by prefix otherwise)
- pty-attach.js and cc-update-all-helper.js use the same targets
- /api/workspaces and `agenthub workspace(s)` / `--workspace` as aliases of
/api/lanes and `agenthub lanes` / `--lane`; old names print one stderr note.
REPL `help lanes|workspace|workspaces` resolves through the aliases.
- A workspace holds up to ten sessions (slots 0-9); every console tab is a
group of 1-4 split panes (PaneGroups, agenthub.workspace.paneLayout);
parseGroups keeps the stored active tab when an earlier one is dropped.
- Browse button beside path fields: POST /api/lanes/pick-path opens the OS
picker on the server's desktop — loopback only and no forwarding header
(EREMOTE), a display failure is ENODIALOG not a Cancel, the dialog is
killed when the request closes, a folder field falls back to the in-app
browser on any failure.
- Changes rail counts chip, image viewer (/git/raw), status bar session
counts and per-day cost (`date=` on /api/pricing/cost).
- /agenthub-open and /agenthub-doctor run from the INSTALLED build
(supersededBy), doctor offers fixes behind two confirmations; plugin-open
treats a locked bootstrap as non-fatal.
- New-terminal shell waits for the first attach (issue #55).
- repo-onboarding / debug-live-issue skills removed.
- OpenAPI: pick-path, lane/date on /pricing/cost, tool on /run/history,
/api/workspaces mount.
- 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
- tmux targets are exact (`=name`, `=name:` for panes/options): a bare
`-t agenthub-lane-1` resolves by prefix to agenthub-lane-12 once lane 1
is gone, so killing a finished lane could kill another.
- getRun/killRun refuse ids that are not `agenthub-lane-<n>`, so
DELETE /api/run/main can no longer kill a user's own tmux session.
- router/apply reports restart_required only for a running pane; getRun
returns a `gone` handle, so it was always true.
- needs_action is cleared only by the session that raised it (new nullable
lanes.needs_action_session); a second session in the same cwd used to
clear it on its second hook.
- Split view: every pane shows the selected lane, each in its own tmux
session (`slot` 0-3 on POST /api/lanes/:id/start; slot 0 stays the
lane's `run_id`, slots 1-3 are `agenthub-lane-<id>-<slot>`). A pane's
Active runs hides what the other panes show; stop/reset/remove kill
every slot. No per-pane lane picker any more.
- Whole-file editor: PUT /api/lanes/:id/git/file (lane-file-write.js)
with the read path's guards, existing text files only, and a baseSha
stale check (409 ESTALE). A file it cannot take says why.
- Per-hunk review bar under each hunk's last changed line: prev/next,
"n of N", Undo, Keep. Review diffs no longer nest their own scroll box
(unless windowed), so navigation moves the one real scroller.
- Files tree lists untracked-not-ignored files and colours entries by git
status (M/U/A/D/R), folders holding changes get a dot.
- Changes panel: Unstage all on the single commit/Stage-all row; sizes
Narrow (30%) / Medium / Full screen, no 70/30.
- Lane card: the needs-you banner is gone (the strip tile keeps it).
Clean break, no compatibility alias. Renames the product name, the CLI
binary (`ccam` -> `agenthub`), the env var prefix (`CCAM_` ->
`AGENTHUB_`), the Prometheus metric namespace (`ccam_*` ->
`agenthub_*`), the tmux session prefix (`ccam-lane-<id>` ->
`agenthub-lane-<id>`), the per-repo runtime profile directory
(`.ccam/profile` -> `.agenthub/profile`), the lanes state root
(`~/.claude/ccam-lanes` -> `~/.claude/agenthub-lanes`), the bundled
plugins (`plugins/ccam*` -> `plugins/agenthub*`) and their slash
commands (`/ccam-doctor` -> `/agenthub-doctor`, `/ccam:*` ->
`/agenthub:*`).
Because there is no alias, an existing install has to be updated by
hand: rename its `.ccam/profile` directory and any `CCAM_*` variables,
and reinstall the plugin under its new name.
Two other name families are deliberately left alone here, since they are
not just strings: the `Claude-Code-Monitor` repository/marketplace name,
and the `agent-dashboard` npm package plus its `~/.claude/agent-dashboard`
runtime directory, which holds the live database and needs a migration
rather than a rename.
Verified: server 1406/1406, client 459/459, `grep -i ccam` clean across
tracked files, `openapi.yaml` regenerated (95 paths, unchanged count).
Sessions created before the mouse-on fix landed (or before the server
restarted to pick it up) never got the set-option call, so their pane
still had no drag/wheel scroll. Moving it into pty-attach's attach()
makes it idempotent per-connection instead of once-at-birth.
xterm.js's onData hands the browser a plain string, and WebSocket.send(string)
always emits a TEXT frame — pty-attach.js only forwarded BINARY frames to
pty.write(), so every keystroke was silently dropped as an unparseable JSON
control message. Now any non-control text frame reaches the pty.
Deeper root cause of the terminal never accepting input at all: the /ws
WebSocketServer used the {server, path} shorthand, whose own internal
upgrade listener calls handleUpgrade() for every upgrade on the shared
http.Server and aborts with 400 on a path mismatch — killing /ws-pty/*
upgrades before the PTY server's own listener ever ran. Switched /ws to
noServer + a manual path-checked dispatch, matching /ws-pty's pattern.
node-pty ships prebuilt binaries for darwin/win32 only — on Linux it
needs a native build via its install script. The plugin's
--ignore-scripts install (scripts/plugin-bootstrap.js, deliberately
skipped to avoid requiring a build toolchain) silently left node-pty
unusable: server/lib/pty-attach.js's require() threw
"Cannot find module './prebuilds/linux-x64//pty.node'" the moment a
terminal was attached, leaving TerminalView permanently blank with no
visible error.
@lydell/node-pty is an API-compatible fork that ships each platform's
binary as a regular optionalDependency instead of a postinstall build
step, so a plain --ignore-scripts install resolves a working native
binding on Linux with no compiler needed. Verified by installing with
the exact `npm install --omit=dev --ignore-scripts` invocation the
plugin bootstrap uses and confirming require() succeeds.