Skip to content

Remove Agent Teams + session-health; port upstream fixes; deep-audit hardening - #1

Merged
kreaddis-julien merged 14 commits into
mainfrom
chore/remove-agent-teams
Jul 1, 2026
Merged

kreaddis-julien merged 14 commits into
mainfrom
chore/remove-agent-teams

Conversation

@kreaddis-julien

Copy link
Copy Markdown
Owner

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 --test green (95 tests), verified by real Electron boot.

1. Removals

  • Agent Teams orchestration prototype removed end-to-end (orch-.js, worktree-manager, path-guard, profiles/session-profiles, the Teams tab + run board + orch view/CSS/i18n, sb- commands, docs/spec). General worktree support is kept.
  • session-health badge removed (module, sidebar badge, health.* i18n, CSS). Session-shape metrics stay (used by stats).

2. Ported upstream/fork fixes (Tier 1 + follow-ups)

3. Deep-audit fixes

Full main+renderer+workers audit (6 parallel passes, hand-verified; report in docs/audit-2026-07.md). Highlights:

  • A2 unify project-path encoder with the CLI's rule (fixes "No conversation found" for _/space paths — verified against the real claude binary).
  • A3 re-key terminal-manager buffers/LRU on fork/detect (dropped output + per-session LRU leak).
  • A6 bounded 1h scheduler catch-up for slots missed during sleep/quit.
  • A1 cap transcript reads (25 MB) to prevent OOM/freeze on huge .jsonl.
  • A5 fix cleanStaleLockFiles liveness; 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.

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.
@kreaddis-julien
kreaddis-julien merged commit 425bcf2 into main Jul 1, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant