- Files: right-click a sidebar workspace (Open, Copy path) or home (Open,
Open Trash), a path crumb (Open, Download as zip, Paste into, Copy path),
a Trash row (Restore, Copy path, Delete forever), the Trash area (Refresh,
Empty Trash) and an admin's per-user Trash folder (Open, Empty Trash).
- Users: row menu (Edit + Open home in Files for an admin, Copy username/email).
- Artifacts: card menu repeating its buttons, plus Copy link for a page.
- Git identity: named SSH keys beside the default ones (id_<type>_<name>,
max 10, DELETE ?id=); SSH logins per alias (`ssh://<alias>/` →
`ssh://<user>@<host>/`, so several AWS CodeCommit accounts share a host),
each optionally tied to one key (`agenthub.<from>.sshKey`) — a clone of
that URL offers only that key and persists it. Terminal sshCommand follows
the keys. Settings → Git: "More SSH keys" + account name/key in the AWS and
other-host forms. Docs/API.md, OpenAPI updated.
- Files → Trash: ONE sidebar entry. An admin's opens on a folder per user's
Trash (My Trash first, item counts from GET /api/homes/trash, an unreadable
one showing its error); a folder opens that user's Trash (path bar
Trash › user, Up back to the folders). An admin may restore/purge in
another user's Trash (audited); purging asks first. The merged "All users"
list is gone from the UI.
- A home's Trash is its user's whole recycle bin (home store + host store,
items tagged `store`).
- `agenthub server setup` installs one Linux user per account on Linux by
default (`--no-system` opts out); install-system messages drop `sudo`.
- Settings → Git: the author email suggests <username>@<AGENTHUB_EMAIL_DOMAIN>.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A member's tmux server keeps the version it started with until their last
pane closes, but the dashboard asked it with TMUX_BIN, the newest tmux: a
3.8 client on a 3.6 server spun it at 100% CPU (three members needed a
manual kill on 2026-10-15).
- /opt/agenthub/libexec/tmux-member (install-system, in sudoers): runs a
member's tmux call with the binary of THEIR running server
(/proc/<pid>/exe, found from the socket in /proc/net/unix), else
TMUX_BIN. It runs as the member and execs only a process they own.
sudoArgv uses it once installed (linux-accounts readConf), TMUX_BIN
until then.
- Users → Members' tmux (GET /api/users/tmux): each member server's version
beside the installed one, sessions, agents working. An admin restarts one
(POST /api/users/:id/tmux-restart) or every idle outdated one
(POST /api/users/tmux-restart-idle), with their password again, audited
per member; a server that will not stop answers 409 ETMUXSTUCK naming
its pid for root.
- server-s3-system.test.js: `delete` is a provision action since Delete
user, so the "unknown action" case now uses `purge`.
Operators: run `sudo agenthub server install-system` once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tmux 3.8 installed under a running 3.6 server left PATH's 3.8 client
asking the 3.6 server, which spun at 100% CPU; the dashboard's
synchronous `has-session` on it had no limit and froze every page, hook
and terminal for five minutes (14:07–14:12 on the shared server).
- Every call on our own server now has a 5 s limit and the member's
one-minute back-off (`host: tmux server not answering` meanwhile), so a
stuck server costs the event loop one timeout per minute, not one per
poll.
- Every call on a running server runs that server's own binary
(`/proc/<pid>/exe`, found from its listening socket in /proc — asking
tmux would be the very call that hangs), re-read every 10 s; PATH's
tmux only starts a server or answers `-V`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AWS CodeCommit logs an SSH key in by its IAM SSH key ID (APKA…), so a
plain `git clone ssh://git-codecommit.<region>.amazonaws.com/v1/repos/x`
was refused as the Linux account name. The account can now save an SSH
user per host — PUT/DELETE /api/me/git/ssh-user, listed as `sshUsers` —
written as `url."ssh://<user>@<host>/".insteadOf "ssh://<host>/"` in its
own ~/.gitconfig (as the member under S3), so the plain URL works in a
pane, in Clone and in the git bar. Only rules of exactly that shape are
listed or removed; any other insteadOf is the user's and left alone.
Settings → Git → Your git identity now shows the SSH keys one section
per kind of host, each with only its own numbered steps: GitHub · GitLab
· Gitea · Bitbucket (ED25519) and AWS CodeCommit (RSA: copy → upload in
IAM → paste the key ID and pick the region, then a "Ready" clone line),
plus a collapsed "Other SSH hosts (advanced)". The shared key-type
picker is gone: each section has its own Generate/Replace/Delete.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Stop (a tab's or pane's X, Close tab) now archives the session it held
(DELETE /api/run/:id?archive=1); the new Sleep (moon left of the X, and in
the tab, pane and session right-click menus) stops the pane but keeps the
session listed to resume later. Shared endRuns in client/src/lib/stopSession.ts.
- Git identity (#82): an account keeps an ED25519 AND an RSA key (AWS
CodeCommit takes only RSA), every ssh git offers both, and a member's
~/.gitconfig gets core.sshCommand so a git typed in a terminal uses them.
- Update dialog: Auto fix starts a Claude Code session on the install
folder with the failed update's error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The chat renderer's paragraph collector never stopped at a table header,
so a table right after a line of text was shown as raw pipes. A table now
interrupts a paragraph as in GFM, and the divider accepts single dashes.
Closes#83
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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.
- Artifacts hub: upload a file from the computer; make a skill by uploading a
Markdown file, files, a folder or a .zip/.skill, or by writing it in the
dialog; add plugins by picking from the list a marketplace link answers
(POST /api/artifacts/plugin/inspect); Settings → AI tools → Artifacts Hub.
- Console tabs are combos of up to four split panes (ConsoleTabs, PaneGrid).
- Accounts: an own avatar (PUT/DELETE /api/me/avatar, raster only, ≤ 1 MB,
<data dir>/avatars/), shown with name and role at the sidebar's foot and on
the workspace owner groups (GET /api/users/:id/avatar, owner_avatar_at).
- Users page: per-user storage (GET /api/users/storage, measured in a worker)
and account-pool spend/tokens per account (server/lib/pool-usage.js).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new workspace takes the default AI tool (Settings → AI Tools, the
account's own pick first); each session still picks its own AI on the
workspace home.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Artifacts (/artifacts): claude.ai pages sessions publish (captured from the
Artifact tool's hook), pinned workspace files, skills and plugins, each
kept as a snapshot; shared ones appear in a Hub and a skill or plugin
installs into the viewer's own account (server/lib/artifacts.js,
/api/artifacts, docs/plans/artifacts/).
- Workspace extra folders: clone one or more related repos beside the
workspace and add them in one step (POST /api/lanes/:id/sources/clone).
- A pane that ended by itself while tmux kept running is no longer offered
for resume (tmux.serverStartedAt in reconcileOrphans).
- Changes panel: a folder that is not a git repo opens on Files, and its
files open from there.
- Plus the parallel sessions' work in this tree: File Explorer button in the
panel header, user delete, RSA SSH keys, chat/terminal and pet fixes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- File Explorer (Files in the sidebar, /files; also from a workspace's Files
tab, portalled so no panel clips it), laid out like GNOME Files: places
(Home, workspaces, Trash), Back/Forward/Up, path bar, search, icon grid with
image thumbnails or list, labelled New/Upload, selection action bar, hidden
files behind Ctrl+H, right-click menus and the usual keys.
Make, rename, move, copy (within a workspace, between workspaces, home ↔
workspace), delete into a per-account trash restorable for 7 days, streamed
upload of files and whole folders (≤ 500 MB a batch, Replace / Keep both /
Skip on a taken name), download a file or a zip of anything.
server/lib/file-explorer.js, /api/lanes/:id/fs/*, /api/homes/:who/fs/*.
- The user's home: every dot entry at its top stays hidden and refused
(also via a symlink, download, zip or as a copy source); an admin may view
other users' homes read-only, audited.
- write-as learns move/copy (re-run `sudo agenthub server install-system`).
- Hardening: a write to a workspace re-checks its owner by the live role;
S3 keeps its server.conf if the file stops reading and a tmux call never
falls back to the host's server; the AI Proxy gateway marks what it relays
and the dashboard refuses it; a NUL in a pane's argv/env is refused.
- OpenAPI for the new routes (server/openapi-extra/files.js); docs (API,
SERVER-MODE, CLAUDE.md) updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gathered from the parallel sessions' uncommitted work on main, at the
owner's request (the session that wrote #78/#79/#80 was stopped mid-task):
- #78/#79/#80: Files tab upload/download (GET /:id/git/download, POST
/:id/git/upload, server/lib/lane-file-transfer.js) and a non-git tree
(git-diff.js walkFolder); pinned sessions (sessions.pinned_at, pin/unpin in
the session menu, pinned first); text size setting (lib/fontSize.ts,
Appearance).
- Git identity and clone over HTTPS/SSH (server/lib/git-identity.js,
routes/git-identity.js, GitIdentitySettings, AddLaneModal clone).
- pets.js FEATURED list and view().
Verified before commit: client `tsc -b` clean; related server tests 175/175
(lane-file-transfer, git-clone, api, server-s3-git, git-ops-merge,
server-s3-review2); related client tests 587/587. Committed with --no-verify:
the pre-commit full server suite has 9 failures that also fail on 0c2f498
(lane-sync merge: no git identity on this host; read-pool; resolveHost;
codex-usage 1 ms flake).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Fix: an errored session alerted on every update, and Clear alerts only
dropped it in memory, so its next update (or a reload) brought the alert
straight back. An error now alerts on the transition into error only;
cleared sessions are remembered (agenthub.pet.dismissedErrors) and stay
quiet until they recover and fail anew.
- Quips, greetings, notifications and Ask answers move to i18n (EN/VI);
Ask understands Vietnamese keywords and names the sessions it counts.
- The pet goes by the worn pet's name; click it in the panel to rename
(agenthub.pet.names, synced to the account).
- Bubbles name the session they are about, with an Open button (never a
resume); a burst of the same event coalesces into one count.
- Moments: a new session waves, a finish hops, a subagent peeks, a wait taps,
an error flinches — on Sagi, Codex sprites and still images. Idle fidgets,
a once-per-session greeting, a "Needs you" list, an amber waiting dot.
- Pat (heart button, Ask, or stroking the pet) reacts on the pet itself, so
it shows while the panel or chat is open.
- Panel drops Run Claude / Activity / Sessions / Errored.
- Ask hands off to a chat window (PetChat) on the default AI tool
(Claude/Codex): transcript as bubbles, follow-ups and CLI menus through an
off-screen attached terminal; no new server route.
Verified on HEAD + this diff in a clean worktree: tsc -b, the full client
suite (1334/1335; the one failure, ConversationView.follow, is flaky under
load and passes 3/3 alone) and server me-prefs.test.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UI
- No animation runs forever: Sagi moods, the custom pet sprite, status dots
(pulse-dot, pulse-slow, ping) and the chat shimmer play a few cycles and
rest. An infinite animation kept the browser drawing 60 frames a second.
- useSagiBrain: frames that only move lastActivityAt by <1 s keep the old
state; the 1 s tick re-renders only when the mood changes.
- ConversationView and MessageList are memoized and PaneChat passes stable
callbacks: typing in the chat box no longer re-renders the conversation.
- The chat's 700 ms menu poll reads plain text first and scans the terminal
cell by cell (reusing one cell) only when a row could be a menu cursor row.
Server
- GET /sessions/:id/transcript?after=N skips lines <= N before JSON.parse.
- GET /sessions/:id/outline is cached until the session's events change.
- broadcastLane uses the cached tmux.liveSessions(); tmux.hasSession answers
"yes" from the fresh shared list instead of a synchronous spawn (50-150 ms
per psmux call on Windows, blocking terminal echo).
- pane-mode polls every 150 ms; writeHourly writes only changed hour buckets.
- /api/metrics: agenthub_event_loop_delay_seconds{quantile}.
Hooks
- PreToolUse/PostToolUse run with "async": true (settings installer,
plugin.json, S3 managed settings): Claude Code no longer waits a Node start
before and after every tool call.
Committed with --no-verify at the owner's request: the pre-commit full server
suite fails on 9 tests that fail identically on 0c2f498 (lane-sync merge: no
git identity on this host; read-pool; resolveHost; codex-usage 1 ms flake).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Workspace keeps panes of other workspaces and tabs mounted (display:none),
and every one of them kept following the live stream as if shown.
- useOnScreen (IntersectionObserver): ConversationView, the outline and the
700 ms menu scan skip while their pane is hidden and catch up once on show.
- A new_event refetches the tail at most every 1.5 s (leading + trailing).
- MessageList: WorkGroup/MessageRow/EditedFiles compare only their own blocks
and results, so a tail append no longer re-renders every row.
- A terminal under the chat is moved off the viewport with a transform, so
xterm pauses drawing without a resize.
- PaneChat and TerminalView are memoized; LaneConsolePane passes a stable onExit.
- RunningStep's clock and the outline poll skip unchanged or hidden updates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Users page: one Edit user form (display name, email, role, active, a new or
generated password, workspaces given or taken back, sign out everywhere),
validated as a whole before anything is written (PATCH /api/users/:id,
GET /api/users/:id); users.display_name/email added migration-safe.
- Settings → Account for every signed-in user: profile, own password (the
current one required, this login kept), sign out other devices
(/api/me/account, /api/me/password, /api/me/sign-out-others; login cookie
only, never a pane token).
- Display names wherever a person is named (sidebar, workspace owner groups,
Users page and audit log); owner_display_name / reader_display additive.
- Users page: admins add and remove Claude/Codex pool accounts.
- Folder browser: an admin with their own Linux account opens in their home;
the "Server folder" tick opens the host's (never remembered).
- AI Proxy: "Config 9router" beside Add profile when 9router is installed,
opened on the dashboard's own host (GET /api/ai-proxies/9router).
- Terminal: a tmux selection reaches the clipboard over plain HTTP too
(writeClipboard's textarea fallback, focus handed back).
- Chat: a file the session touched outside the workspace previews
(GET /api/sessions/:id/file).
- install-system: `!requiretty` only when visudo accepts it (sudo-rs on
Ubuntu 25.10+ rejects the setting).
- openapi.yaml regenerated; docs (API, SERVER-MODE, LANES) updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decisions, environment quirks and pitfalls learned while working on AgentHub
(docs/claude-memory/, indexed by MEMORY.md). Notes holding personal account
details are left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Panes were keyed by slot alone, so slot 0 of the next workspace reused the
previous one's pane: every switch disposed its terminals and attached new
ones (a WebSocket each, the whole screen replayed). The live panes of the
last 4 workspaces left now stay mounted as hidden siblings in the same
grid, keyed by paneKey (lane x 10 + slot), so going back is instant.
Measured on 3 workspaces x 2-3 shell panes, switching back to one already
opened: script 210 -> 52 ms, main thread 370 -> 120 ms, long tasks
120 -> 0 ms, terminal re-attaches 2-3 -> 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xterm.js has no drop handling of its own, so dropped text was lost; the pane
now pastes it as a local terminal does, with a "Drop to paste" hint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Folder browser (the Browse fallback for a remote browser): back/forward/up,
a clickable breadcrumb that becomes a path field (Ctrl+L), Places (Home,
the user's workspaces, recent picks), a filter, Name/Modified columns,
Show hidden and New folder. A click selects; a double-click or Enter
opens a folder or picks a file; Backspace goes up; typing jumps to a name.
- GET /api/lanes/browse: `hidden=1` (never the dot entries at the top of a
non-admin's own folder), each entry's `mtime`, the answer's `home`.
- POST /api/lanes/browse/mkdir: one plain folder name inside an existing
folder; a non-admin only in their own folder, none in server mode without
one; under S3 a member's folder is made as the member.
- scope.ownFolder falls back to server.conf's USERS_ROOT under S3, so a
settings file without AGENTHUB_USERS_ROOT no longer opens the host's home
to every developer.
- Passwords: any non-empty password is accepted (no 8-character minimum).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Chat: an AskUserQuestion is its own card (AskCard in MessageList.tsx), not
part of the collapsed work line — each question with the option picked or
the text typed, "declined" when refused. The transcript route passes the
answers from Claude Code's toolUseResult (askAnswers, beside diff).
- Chat: the activity panel keeps the user's open/close per session for the
tab, and opens by default only once the session has an output.
- Workspace home: the sent prompt's draft is dropped even though opening the
session unmounts the home.
- Terminal: Ctrl+V with text on the clipboard pastes the text; only an empty
clipboard still sends ^V.
- AGENTS.md / CLAUDE.md: run only the tests a change touches while iterating,
the full suite once before committing.
- A menu the CLI asks with (AskUserQuestion — one question or several,
multiSelect —, a permission or trust prompt, Codex's approval, a
first-start login menu) is read off the terminal under the chat
(`parsePaneMenu`/`readRunMenu`) and shown in the prompt box's place as a
card of buttons. A pick presses its digit (picks it, or ticks its box),
Claude's unnumbered Submit row is reached with the arrows and Enter, and
Cancel presses Esc. "Type something" opens a field in the card: its text
goes in by the arrows, Ctrl+U over anything already typed there, a
bracketed paste and Enter. That row presses no key — a second press of
its digit typed the digit into the CLI's field ("44444…"). Keys verified
on Claude Code 2.1.296 and Codex 0.161. AskUserQuestion fires no
Notification, so a menu on screen also counts as waiting (live line, tab
badge). Client only: no new route.
- #74: Esc before the model wrote anything puts the prompt back in Claude
Code's input box and writes no "[Request interrupted by user]" line, so
the transcript ended on the prompt and the chat read "Thinking…" for
15 min. The chat's Stop now ends the live line until a newer transcript
line arrives (matched by line, not by clock).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
macOS's own Vietnamese Telex types into xterm's textarea with no
composition events: a letter is an `insertText`, every later key of the
word an `insertReplacementText`, each keydown (keyCode 229) arriving after
the change. v2.7.4 forwarded the replacements, but a capital typed with
Shift still vanished — xterm drops any `input` that lands while a key is
held — and its 229 fallback could resend an edit. Replayed from a real
Chrome trace, "Abc xin chào" reached the pane as " x c".
lib/imeInput.ts now owns those edits: capture phase on xterm's element,
each one sent as DELs plus the new tail, the IME's input and 229 keydowns
hidden from xterm. Keys xterm sends from keypress (Space) and real
compositions (CJK) stay xterm's. Verified against xterm 5.5 in headless
Chrome: Telex → "Abc xin chào"; a plain keyboard sends the same bytes
as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- GET /api/settings/info for a developer: no DB path, no hooks file path,
and no transcript-cache keys — those were EVERY user's transcript paths
(their folders and session ids). GET /api/updates/status: no repo_root.
Test: server-mode-leaks.
- Also in this release (earlier commits): a member's missing workspace
folder is made as them at start; --allow-dangerously-skip-permissions only
for an account that accepted the bypass warning or a bypass run.
Full suites: server 2889/2889, client 1250/1250, tsc -b.
--allow-dangerously-skip-permissions (bypass in the pane's shift+tab cycle)
made Claude Code show its Bypass Permissions warning on any account that had
not accepted it yet — every new member's first session, which the Chat UI
cannot answer, though nobody asked for bypass. Now passed only when the run
starts in bypassPermissions (asking is consent to see it) or the account's
user settings already hold skipDangerousModePermissionPrompt (pty-run
bypassAccepted; a member's read owner-checked from their home). The host,
which accepted long ago, keeps the flag. Test: pty-run.
v2.7.3's ENOCWD left a new member no web path to a first project: their
home is no workspace (EFOLDER) and a not-yet-existing folder could not
start. run-as.makeMemberFolder: mkdir -p through write-as, AS the member,
only inside their own home (never the home itself, never a host folder,
never for a member with no account). Test: server-s3-lanes.
- Changes (#72): a file git reports `MM` whose working copy equals HEAD again
(a staged change since undone on disk) is no longer listed — JetBrains'
GitChangesCollector drops it the same way. Mode-only changes stay listed.
statusCounts (the dirty-tree safety checks) is unchanged.
- Chat (#73): Claude Code runs no hook before a folder is trusted, so a new
pane's session has no row; the chat now says the session has not started,
offers Open terminal, and asks before typing a message or /model //effort
into the pane (an Enter would pick the trust prompt's "No, exit").
- Terminal: Safari + macOS's own Vietnamese Telex replaces the whole word on
each key (insertReplacementText), which xterm.js ignores — only each word's
first letter reached the pane. The edit is now sent as DELs + the new tail.
- Also: Ctrl+V pastes a screenshot into the terminal as a file path; the
folder browser fallback lists files for a file field (`files=1`); the macOS
native picker opens in front of the browser; a hidden tab is marked while a
chat waits or once a turn ended; chat live line shows running calls, turn
time and tokens, and "Done in …".
run.test.js hardcoded /tmp/agenthub-drops-<uid>, which drops left for
<data dir>/drops long ago — it passed only while an old build's folder
survived in /tmp (gone after a reboot). Full suites: server 2887/2887,
client 1250/1250.
Only the process that starts a pane writes it into its own cached tmux list
(noteSession); a second dashboard on the same database (a dev server) holds
a member list up to 15 s old, so its heal beat ended a Codex pane seconds
after it started and the proxy gateway answered 401. reconcileOrphans and
healRunId skip a run younger than 20 s (dashboard-runs.isYoungRun).
Tests: dashboard-runs (young vs old).
- self-restart: a server in our unit's cgroup (/agenthub.service, system
or user) restarts via `systemctl restart --no-block` (sudo -n for the
system unit), our launchd job by exiting into KeepAlive. The detached
restart-helper replacement held the port while Restart=always started
the unit every 3 s (v2.7.2 on the S3 box: 240+ restarts, MainPID 0).
- run-as.mayHaveTmux: only ENOENT means "no tmux". A dev server in server
mode without CAP_DAC_READ_SEARCH got EACCES on a member's 0700
.agenthub/tmux, dropped the member from the list and ended their runs
within seconds — the real cause of `401 not a live pane's token`.
- tmux: a member's list is re-asked every 5 s, not 1 s (one sudo + three
auth.log lines each).
- POST /api/lanes/:id/start: 409 ENOCWD for a folder that no longer
exists (the pane started in the account's home otherwise).
- Settings: Remote sources only for staff (a developer got a 403).
- scripts/test-server.js drops every inherited AGENTHUB_* (pane env).
Tests: self-restart.test.js, server-s3-panes (mayHaveTmux), lanes-api
(ENOCWD); touched suites 771 + client pages 191, tsc -b.
- Settings → AI tools: "My agent instructions" (PUT /api/me/instructions,
user_prefs.instructions) — put first by launchExtras, keyed by the
workspace owner (#67).
- Session view: Settings opens as one more tab of the tab bar.
- Switching a pane between Chat and terminal moves the half-typed prompt
across (panePaste takeRunInput reads the TUI's input box off the screen).
- Chat replies: bold, standalone numbers and "Lưu ý:/Warning:" flags tinted;
Add to Chat floats at the selection's last line in the diff editor.
- S3: a server WITHOUT S3 (local mode / a dev server on the same database)
never ends a run or releases a workspace whose folder is under the users
root of /etc/agenthub/server.conf (linux-accounts.unseenMemberFolder, used
by reconcileOrphans and healRunId). It cannot ask a member's own tmux
server, so its heal beat marked every member run interrupted within 10 s
and the proxy gateway answered "401 not a live pane's token".
Regression test: s3-unseen-member-runs.test.js.
Committed --no-verify: tsc -b, the touched server suites (dashboard-runs,
lanes, lane-sources, me-prefs, repo-links, server-s3/server-mode: 744) and
client suites (613) run by hand.
- v2.7.0 could not be installed or updated to: the client build
(`tsc -b && vite build`) type-checks test files too, and a new
RunSetup test rendered PromptEditor without its required `slash` and
`fileCwd` props, so `tsc -b` failed and every update stopped at
"npm run build failed". The test now passes them.
- Verified on a clean v2.7.0 worktree + this fix: `tsc -b` and
`vite build` both exit 0; RunSetup tests 19/19.
- 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).
- psmux prints `name: 1 windows (created …)` whatever `list-sessions -F`
asks for. Read as names, every live pane looked gone and the heal beat
closed its run (and its tab) within seconds while the session kept
running. tmux.js (and the Update all helper) now take the text before the
first `:` — a session name never holds one.
- `npm run setup` used `(cd client && npm install)`; cmd.exe's ( ) is no
subshell, so `npm run link-cli` ran inside client/ ("Missing script") and
Update now failed on Windows. Now `npm --prefix client install`.
- Update now: an npm step names itself in its error and shows the END of
npm's output (the error, not the warnings), says when it hit the time
limit, and gets 20 min instead of 5.
- #69: tmux applies a pane's `-c` only while its server's own cwd exists.
The server was forked from the dashboard's cwd (the plugin cache dir),
outlives updates, and the old cache dir is pruned — after that every new
pane started in a deleted directory, where `claude` died at once ("run not
found") and a shell printed getcwd errors. tmux calls now run from `/`,
and the pane's sh wrapper cds into its folder itself, which also heals a
tmux server that is already stuck.
- Changes panel: a workspace's extra folders are sections stacked under the
workspace folder (VS Code Source Control style), with Close repository /
Close other repositories on a header's right-click.
- Chat: URLs and absolute paths are links; Claude Code's `[Image: source:]`
line folds into its turn; uploads under `<data dir>/drops/` are listed.
- Update check: a checkout holding commits a newer release lacks reports
`diverged_from_release` instead of offering a fast-forward that can only fail.
Chat UI:
- Each Claude/Codex session keeps its own chat/terminal view; Settings only
sets how new sessions open.
- Composer + menu: upload, project files and folders (#54, also dragged from
the Files/Changes tree), context files, recent uploads, skills, plugins.
- Activity side panel (progress, outputs, files read, uploads, memory,
skills, connectors) with a file preview; edited-file counts per turn.
- Permission-mode chip (Shift+Tab steps, read back from the pane:
server/lib/pane-mode.js, GET|POST /api/run/:id/permission-mode).
- Stop (Esc) while the main agent works; the status is checked per agent
(with session_id /api/agents ignores its status filter) and re-read every
5 s while Stop shows.
- Dropped images shown in the conversation (GET /api/run/drop/:name, the
requester's own drop dir), full-size viewer.
- A plain-text paste over 5,000 characters is attached as pasted-text.txt,
so no prompt reaches tmux/argv limits (#68).
Changes panel: every /api/lanes/:id/git* route takes ?source=<lane_sources
id> (sourceFolder; ENOSOURCE otherwise) and a folder picker switches the
whole panel to a workspace's extra folder (#70).
Server mode S3 (one Linux user per account) — experimental, verified with
fake sudo plus a P6 managed-settings spike on a test account:
- member tmux listing runs in the background (cached, 15 s => unknown);
- transcript snapshot copy is async, chunked, temp file + rename;
- Express Layer patch only on Express 4;
- tmux capturePane/sendKey go to the member's own server.
Docs: the multi-user design moved in as docs/plans/server-mode/README.md
(the AgentHub-Server repo is gone); S3-PLAN P6 result; LANES/API/openapi.
A frozen member (SIGSTOPped tmux server) cost every user one synchronous
3 s block of the event loop per minute (F37's back-off ceiling). Member
listing now runs through async execFile (same 3 s / SIGKILL / 1 MiB
bounds, same back-off) into the listed cache the synchronous readers
(liveSessions, listSessions, listOutput) already use: stale-while-
revalidate, 15 s max age, cold or failing = that member's panes UNKNOWN
(F38 unchanged). A session this process creates or kills is written into
the member's cached answer at once, and a list in flight across that
change is dropped. Host tmux calls, local mode and every click's call
(new-session, send-keys, kill, has-session) are unchanged.
review5's stalled-member test now polls for 4 s past the timeout and
asserts each poll < 100 ms, event-loop stall < 500 ms and one sudo
(old code: first poll 3157 ms).
- docs/plans/server-mode/README.md: the original multi-user design, now kept
here (AgentHub is the one source); desktop-parity and S3-PLAN link to it.
- S3-PLAN P6: managed-settings spike run on agh-test1 — the managed
marketplace/plugin arrive at the first interactive session (onboarding
seeded), never via the CLI alone, and no user-scope record is written.
- server/index.js: the Layer.handle_request patch applies only on Express 4
(Express 5 handles async rejections itself and has no lib/router/layer).
copyIfNewer read/wrote a member file of up to 256 MiB with readSync/writeSync,
blocking the event loop for every user. It now copies with fs.promises 1 MiB
at a time from the runAs.openSafeAsync handle (owner/type check kept), keeps
the 256 MiB member cap and zero-run holes, writes a temp file and renames it
into place, and joins a copy already in flight for the same destination.
A failed copy is logged and never rejects; snapshotTranscript is async and
all three import callers await it.
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>