Remove Agent Teams + session-health; port upstream fixes; deep-audit hardening - #1
Merged
Merged
Conversation
Remove the experimental multi-agent orchestration feature end to end: master/worker/reviewer runs, the run board + Teams tab, worktree-per-task, the file-based run protocol, cost/tier routing, and the sb-* boot commands. Also drops the session-profiles backend (per-session env/model profiles with the data-egress guard) that only the orchestrator consumed. Removed: orch-*.js, worktree-manager.js, path-guard.js, profiles.js, session-profiles.js, public/orchestration-view.js, docs/agent-teams-spec.md and the associated tests; the orch wiring in main.js/preload.js and the Teams tab / orch-viewer / orch CSS / orch i18n keys in the renderer. General worktree support (new-session-in-worktree, delete-worktree, sidebar nesting) is kept. Session-shape metrics stay (used by stats).
Remove the session-health indicator (Marathon Risk / Handoff Recommended dot on sidebar rows) and its unwired handoff-packet builders: the whole public/session-health.js module, its test, the sidebar badge, the health.* i18n keys and the .session-health CSS. The underlying session-shape metrics (user-turn count, active span, largest prompt, cache-read tokens) are kept — they are computed during the normal scan and consumed by stats. Comments that referenced the removed module are reworded.
…busy-retry Three self-contained fixes cherry-ported from upstream/fork work: - Terminal copy via main-process clipboard + OSC 52 handler. The renderer's navigator.clipboard.writeText is gated on focus/user-activation and is dead on Linux/Wayland; route copy through a clipboard-write-text IPC and wire OSC 52 (how Claude Code copies). (upstream PR doctly#55 / HaydnG dbfb31a) - Cap the FTS5 query at 48 chars in searchByType. A long pasted phrase query generates dozens of overlapping trigrams whose doclist intersection blocks the synchronous main thread for up to ~60 s on a large index. (jbr 7c74a8e) - sqlite-busy-retry: retry better-sqlite3 writers a few times with a short backoff on SQLITE_BUSY/LOCKED, as defense-in-depth over WAL + busy_timeout when scheduler/UI/watcher writes overlap. (HaydnG 8e49229)
Hidden terminals (display:none) still paid xterm's full VT-parse cost on every flush — the dominant renderer CPU when many Claude sessions stream at once. Now: - Stage A: flushTerminalBuffer skips terminal.write() for non-visible sessions and accumulates raw PTY data in a per-session replay buffer (capped at ~2 MB, oldest chunks dropped). showSession / grid wrapInGridCard drain the buffer in one coalesced write() on (re)visibility. - Stage B: scheduleFlush uses a 2000 ms interval for non-visible sessions instead of 33 ms, cutting background coalescing overhead. Visible sessions keep the ~30 fps budget unchanged. Safe: OSC 0/9 busy/attention badges are parsed in main on raw PTY data before the renderer, so skipping renderer writes never drops a badge. Ported from jbr 4278b2c + 62ee73a (adapted to our flush/visibility code). Renderer-only globals; not covered by the node test suite.
…lowlist Three small, self-contained improvements ported from ivandobsky: - Persist getNormalBounds() instead of getBounds() so a window maximized via snap/double-click no longer reopens pinned full-screen. (feb8c25) - flashFrame the taskbar / bounce the dock when a session needs attention while the window is unfocused; cleared on focus. Hooked into the existing notifySessionAttention decision (reuses its dedup). (2ac5448) - Allowlist setting keys (global | searchTitlesOnly | project:*) on the get/set/delete-setting IPC so the renderer can't write internal keys. (7b26c69)
Switchboard can now remember which Claude sessions were open and re-open them on the next launch, each resumed in turn via `claude --resume`. Plain shell sessions are excluded (no resumable state). - The open set is persisted to global.openWorkingSet, serialized read-modify-write, updated incrementally on open/close/exit/fork so it survives a crash (not only a clean quit). - Gated by a new `restoreOnStartup` setting: off (default) / ask / auto. In ask mode a non-modal toast offers to restore; auto restores silently. Restore is sequential (staggered) to avoid spawning N claude at once. - Setting UI + toast are localized (EN/FR); toast uses shadcn tokens. Default off, so behavior is unchanged unless explicitly enabled. Ported/adapted from jbr 65bd884 (doctly#80). Renderer-only; jbr's jsdom test suite was not ported (harness absent in this fork).
deriveProjectPath matched folders by encoding candidate cwds with a "/" + "."
only rule, while folders are created with encodeProjectPath's [^a-zA-Z0-9]->"-"
rule (verified against real ~/.claude/projects folders and the CLI 2.1.197
binary: "_", spaces and parentheses all become "-"). For a project path
containing any of those, the derive encoder never matched the folder and
silently fell back to the dominant-cwd heuristic — picking a drifting subdir
instead of the resumable startup cwd ("No conversation found" on --resume).
Reuse the shared encodeProjectPath so create and derive can never diverge
again. No change for the common all-lowercase/hyphenated paths (both rules
agree there). Adds a regression test for an "_"/space/parens path.
…lback Audit follow-ups (defense-in-depth; none exploitable in the shipped config given sandbox+CSP+DOMPurify, but the app's own guard rails had gaps): - SENSITIVE_PATH_PATTERNS missed the app's own OAuth token (~/.claude/.credentials.json) plus private keys outside ~/.ssh, .npmrc, .pypirc and shell history — add them so read/save-file-for-panel can't surface them. - cleanStaleLockFiles compared the lock's pid to process.pid, which never matches a prior run, so crashed-run locks were never removed. Probe pid liveness with process.kill(pid, 0) instead. - renderJsonlText returned raw marked() HTML when DOMPurify was absent; fall back to escaped plain text so XSS safety never depends on script load order. - Remove dead validateShellArg/SHELL_META_CHARS (never called; the real quoting is quoteArgvForShell) — it implied a guard that didn't run.
Correctness + robustness fixes from the deep audit: - Re-key the terminal-manager per-session maps on fork/detect. app.js re-keyed openSessions but not terminalWriteBuffers/lastFlushAt/rawReplayBuffers/lruOrder (module-local), dropping buffered output on the id change and leaking a dead lruOrder id per new session (degrading the WebGL LRU cap over time). New rekeyTerminalState() moves the buffer with timers cancelled + re-armed (a stale rafId would freeze scheduleFlush), the tail maps, and the lruOrder slot. - Reap phantom pending sessions: a Claude launch that died before writing any .jsonl was re-injected into the sidebar forever. The reconcile loop now drops a pending record once no live/mounted terminal remains for it (LRU-bounded); the relaunch row is kept while the closed-with-banner terminal is still open. - Guard showTerminalHeader's shell-profile chip against a session switch landing mid-await (two awaits) — it could stamp session A's shell onto session B. - Bind the grid focusin listener once on the reused terminal container instead of per grid enter/exit cycle (was leaking a handler each toggle); read the id from entry.session so it survives a rekey. - Cap transcript reads at 25 MB (parseJsonlCapped): a hundreds-of-MB .jsonl could OOM main and freeze the renderer's synchronous render. Beyond the cap keep the recent tail + drop the partial first line; return truncated:true.
- mcp-bridge WebSocketServer had no maxPayload (ws default 100 MiB, each frame
JSON.parsed on the main process). Cap at 8 MiB so a same-user client can't
spike RSS / stall the event loop with giant frames.
- run-schedule-now took a renderer-supplied filePath with no validation and
executed claude from it with the file's frontmatter (model/allowed-tools/
permission-mode) + prompt. Require a .../commands/schedule-*.md path; use the
resolved path throughout.
- read-session-file extracted only content[0].text; join all text blocks so a
message shaped [{tool_result|image},{text}] doesn't lose the real prompt from
summary/search (rare in practice, but the code now matches bookmarks' logic).
Off-screen grid cards kept the '.visible' class (the IntersectionObserver only toggled WebGL), so isSessionVisible() returned true and their flush path still paid full VT parse at 30fps — disabling the background-write optimisation for the entire grid, exactly the many-sessions scenario it targets. The observer now marks scrolled-out cards '.offscreen' (isSessionVisible treats them as background) and drains the accumulated replay buffer when they scroll back in. Single-view terminals never carry '.offscreen', so they're unaffected.
The scheduler was a pure "does now match cron?" poll with no memory, so a slot that came due while the machine was asleep or the app was quit was silently dropped (the main-thread timer itself is precise — measured, not throttled — so sleep/quit are the only real gaps). Now: - Persist last-fired per task to <dataDir>/schedule-state.json (atomic write). - On each tick, recover the newest cron slot after the last fire, bounded to a 1h window, and fire it ONCE (never once-per-missed-minute). On-time firing is unchanged (the current minute is the newest slot). - First sight of a task baselines to now, so enabling this (or adding a task) never retro-fires old slots — no first-launch flood. - Overlap guard unchanged; a busy task retries next tick within the window. Not addressed (documented as limitations): cron is evaluated in LOCAL time with no DST handling (a spring-forward slot can't occur; the fire-once + advance de-dups a fall-back repeat), and nothing runs while the machine is asleep or the app is fully quit — for guaranteed schedules use launchd/cron. tz support was deliberately skipped as over-engineering for a personal tool.
Consolidated findings (severity, file:line, status, how verified), the deliberately-skipped items with reasons, and the positive-assurance list (what was checked and found sound). Records the calibration: several subagent HIGHs were downgraded after tracing real exploitability.
No paid Apple Developer ID, so signed/notarized auto-update (Squirrel.Mac) is impossible on macOS. Instead: a notify-only check. - main.js: read this fork's latest GitHub release via the API (no signing needed) on launch + once a day (packaged only); if newer than app.getVersion(), emit `update-available`. `updater-check` IPC triggers it manually; no auto-install. - Renderer: reuse the existing toast — "A new version is available vX" with a Download button (opens the release page) + Later (remembers the dismissed version). EN/FR strings added. - CI: build macOS only (Windows native rebuild was broken; no other target wanted), and publish releases NON-draft so the notifier's API check can see them. package.json publish owner -> the fork. - README: document notify-only updates + the unsigned/Gatekeeper install step. Verified: version compare + GitHub fetch/parse against the live API (doctly returns v0.0.30; the fork 404s until its first release, handled silently); renderer i18n + toast wiring confirmed via a live Electron boot; 95 tests pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Personal-fork maintenance branch: remove a large recent prototype, port a curated set of upstream/fork fixes, and land the fixes from a full deep audit. 13 commits,
node --testgreen (95 tests), verified by real Electron boot.1. Removals
2. Ported upstream/fork fixes (Tier 1 + follow-ups)
navigator.clipboardis dead on Wayland). (upstream Make copy actually copy on Wayland (main-process clipboard + OSC 52) doctly/switchboard#55 / HaydnG)Korn Shell Errorpopup on Windows when opening Stats — macOSsecuritycommand dispatched via ksh from MKS Toolkit doctly/switchboard#80)getNormalBounds(no reopen-maximized), taskbar flash on attention,set-settingkey allowlist.3. Deep-audit fixes
Full main+renderer+workers audit (6 parallel passes, hand-verified; report in
docs/audit-2026-07.md). Highlights:_/space paths — verified against the realclaudebinary)..jsonl.cleanStaleLockFilesliveness; C1–C4, B1–B5 correctness/hardening.No exploitable XSS / injection found; posture (sandbox+CSP+DOMPurify+argv quoting) verified sound. Deliberately skipped (documented): scheduler tz/DST, full render virtualization.