Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.

Commit 4e7eb9e

Browse files
authored
Merge pull request #264 from closedloop-ai/feat/in-process-agent-database
FEA-1504: Gate embedded agent dashboard
2 parents ba54bc9 + 0372f26 commit 4e7eb9e

115 files changed

Lines changed: 17966 additions & 1190 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,14 @@ jobs:
4545
- name: Typecheck
4646
run: pnpm typecheck
4747

48+
- name: Assert design-system boundary
49+
run: pnpm -C apps/desktop assert:design-system-boundary
50+
51+
- name: Assert design-system flag-off boot
52+
run: pnpm -C apps/desktop test:boot:design-system-off
53+
54+
- name: Measure Agent Dashboard storage
55+
run: pnpm -C apps/desktop measure:agent-dashboard-storage
56+
4857
- name: Run tests
4958
run: pnpm test

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ apps/desktop/.generated/
2626
apps/desktop/test-results/
2727
apps/desktop/playwright-report/
2828

29+
# Desktop-local workflow/debug artifacts
30+
apps/desktop/.closedloop-ai/
31+
2932
# UI Numbers Audit generated reports (FEA-1415 / PLN-738)
3033
# These embed a timestamp; regenerate locally via `pnpm --filter desktop audit:report`
3134
# or `node apps/desktop/test-e2e/agent-monitor/inventory/scan-tiles.mjs`.

THIRD_PARTY_NOTICES.md

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,9 @@ development/build, and the desktop build generates a runtime tree under
1313
- **Pinned commit:** `840c518d7fa69231de049e41b893938228b67e40`
1414
- **Imported via:** pnpm dependencies `agent-dashboard` and
1515
`agent-dashboard-client`
16-
- **Usage:** Bundled and run as a local `127.0.0.1` sidecar process by the
17-
desktop app (the embedded "Claude Dashboard" tab). The desktop build applies
18-
three local host patches while generating
19-
`apps/desktop/.generated/agent-monitor/`: loopback-only bind, explicit
20-
`CCAM_AUTO_INSTALL_HOOKS` gating, and a hook uninstall script.
16+
- **Usage:** Bundled and run as the default local `127.0.0.1` legacy sidecar
17+
dashboard. The desktop build applies local host patches while generating
18+
`apps/desktop/.generated/agent-monitor/`.
2119
- **License:** MIT — © 2026 Son Nguyen.
2220

2321
Bundled runtime dependencies remain pure JS. The generated sidecar runtime uses
@@ -57,9 +55,8 @@ SOFTWARE.
5755
- **Used as:** documentation/algorithm reference for the OpenAI **Codex** CLI
5856
rollout JSONL format — the cumulative→session token semantics and the
5957
`turn_context.model` attribution rule (`docs/codex.md`). No CodexBar source
60-
is bundled. Our own Codex ingestion modules
61-
(`apps/desktop/scripts/agent-monitor-codex/codex-*.js`, copied into the
62-
generated runtime tree at build time) were merely informed by it.
58+
is bundled. Our own first-party Codex ingestion modules
59+
(`apps/desktop/src/main/collectors/codex/`) were merely informed by it.
6360
- **License:** MIT — © 2026 Peter Steinberger.
6461

6562
```

apps/desktop/CLAUDE.md

Lines changed: 90 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -136,108 +136,94 @@ Typical log locations:
136136

137137
The Diagnostics tab shows the current in-memory gateway log plus a bounded previous-session tail read from `main.log` at startup. First-run or unreadable log files must not block boot; return an empty previous-session tail and continue.
138138

139-
## Agent Monitor Sidecar
140-
141-
The desktop app bundles the MIT-licensed `Claude-Code-Agent-Monitor`
142-
(`agent-dashboard` + `agent-dashboard-client`, pinned in
143-
`apps/desktop/package.json`) and runs a generated runtime tree as a managed
144-
localhost **sidecar** for local Claude Code session/agent observability. It is
145-
the single embedded observability tool. It powers the **Dashboard** and the
146-
agent nav items (Sessions, Kanban, Activity Feed, etc.) in the desktop left
147-
sidebar. The feature is gated by the persisted `agentMonitorEnabled` desktop
148-
setting, which **defaults ON**; when disabled, the agent nav items are hidden
149-
and only the Gateway section remains.
150-
151-
- **Process model:** `src/main/agent-monitor-sidecar.ts` spawns the generated
152-
`server/index.js` from `apps/desktop/.generated/agent-monitor/` (packaged:
153-
unpacked `extraResources/agent-monitor`) using the Electron binary as Node
154-
(`ELECTRON_RUN_AS_NODE=1`, `process.execPath`) — a packaged app ships no
155-
standalone `node`. Started fire-and-forget from `boot()` **only when
156-
`agentMonitorEnabled` is true**, and still before the gateway-start try-block
157-
so a gateway-start failure never prevents it from running and a sidecar
158-
failure never blocks or fails app boot.
139+
## Agent Monitor
140+
141+
> **Status (FEA-1504):** Agent Monitor has three boot modes. The default user
142+
> experience is the legacy sidecar-backed dashboard (`agentMonitorEnabled=true`,
143+
> `agentDashboardDesignSystemEnabled=false`): pnpm-managed upstream packages are
144+
> materialized into `.generated/agent-monitor`, shipped unpacked, and rendered in
145+
> the legacy iframe shell. The in-process design-system dashboard is a Labs
146+
> opt-in only. When `agentDashboardDesignSystemEnabled` is not the literal
147+
> boolean `true`, the main process must not load `src/main/database/`,
148+
> `src/main/collectors/`, `AgentHookListener`, `desktop:db:*`, or the `app://`
149+
> design renderer path.
150+
151+
The desktop app provides local Claude Code (and opt-in Codex) session/agent
152+
observability. It powers the **Dashboard** and the agent nav items (Sessions,
153+
Activity, Analytics, Workflows, Kanban) in the desktop left sidebar. The feature
154+
is gated by the persisted `agentMonitorEnabled` desktop setting, which
155+
**defaults ON**; when disabled, the agent nav items are hidden and only the
156+
Gateway section remains.
157+
158+
- **Legacy sidecar (default):** `src/main/agent-monitor-sidecar.ts` launches the
159+
generated Claude-Code-Agent-Monitor runtime tree. `build:agent-monitor`
160+
materializes the tree from pnpm-managed upstream packages; package/stage logic
161+
must keep `.generated/agent-monitor` available for default users.
162+
- **Design-system runtime (Labs opt-in):** `src/main/agent-dashboard-design-system-runtime.ts`
163+
is the only module allowed to import `src/main/database/`,
164+
`src/main/collectors/`, `AgentHookListener`, or register `desktop:db:*`. It is
165+
reached only through `await import()` after boot mode resolves to
166+
`design-system`.
167+
- **Disabled mode:** `agentMonitorEnabled=false` starts no sidecar, no
168+
design-system runtime, no dashboard-derived sync source, and no
169+
dashboard-derived cost source.
170+
- **Hook listener:** in design-system mode, `src/main/agent-monitor-listener.ts`
171+
binds `127.0.0.1:4820` in the main process and accepts the hook payload
172+
(`POST /api/hooks/event`, `GET /api/health`). Each event is gated by the
173+
FEA-1407 sandbox check, harness-stamped from `__provider`, and applied by the
174+
lifecycle state machine.
175+
- **Collection layer (`src/main/collectors/`):** design-system mode uses
176+
`CollectorManager` for best-effort boot bulk import and live file watchers for
177+
all five agent CLIs, writing through the first-party `importSession` into the
178+
same in-process DB.
159179
- **Fixed port (differs from the gateway):** `127.0.0.1:4820`
160-
(`AGENT_MONITOR_PORT` in `src/shared/contracts.ts`), passed via
161-
`DASHBOARD_PORT`. It MUST be fixed — Claude Code hooks bake a port at install
162-
time and the hook handler POSTs to `127.0.0.1:${CLAUDE_DASHBOARD_PORT||4820}`,
163-
so 4820 (upstream's default) means hooks need zero per-hook env. 4820 is
164-
outside `PORT_PROBE_ORDER`, so it never collides with the gateway.
165-
- **Durable DB:** `DASHBOARD_DB_PATH` is set to
166-
`app.getPath("userData")/agent-monitor/dashboard.db` (the packaged app dir is
167-
read-only). Uses Node's built-in `node:sqlite`; the generated `server/db.js`
168-
is patched to prefer `./compat-sqlite`, and staged packaging removes the
169-
hoisted `better-sqlite3` module as a belt-and-suspenders guard.
170-
- **UI:** embedded in the main window (`src/renderer/index.html`) as a plain
171-
`<iframe>` pointed at the sidecar URL fetched via
172-
`desktop:get-agent-monitor-url` (renderer polls until `ready`, then sets
173-
`src` once). No separate window. The host left sidebar drives it: the
174-
**Dashboard** + agent nav items each map to a sidecar route. The first load
175-
bakes the route + `embed=1` into the iframe `src`; later agent-nav clicks
176-
are a `postMessage` (`{ type: "closedloop:navigate", path }`) so there is no
177-
reload. In embed mode the sidecar's own `Layout` drops its internal sidebar
178-
(see `scripts/agent-monitor-embed/Layout.tsx`) so the host shell is the only
179-
chrome. The agent nav items are hidden when the feature is disabled. The
180-
embed depends on the renderer having **no CSP** — if a CSP is ever added it
181-
must include `frame-src http://127.0.0.1:*`. Iframes in a `display:none`
182-
panel collapse to 0px, so an explicit px height is set via JS *after* the
183-
panel is `.active`, re-applied on `resize`.
184-
- **Hooks are explicit opt-in (consent-bearing).** Upstream silently writes 8
185-
hooks into `~/.claude/settings.json` on every startup — the generated
186-
`server/index.js` gates that behind `CCAM_AUTO_INSTALL_HOOKS` (which the
187-
sidecar sets to `"0"`).
188-
The user enables/disables tracking via the toggle on the Agent Dashboard
189-
view → `src/main/agent-monitor-hooks.ts` writes/removes the 8 hook entries. The
190-
hook command runs the Electron binary as Node against a **userData copy** of
191-
`hook-handler.js` (location-independent across app moves/updates), at the
192-
fixed port 4820. Default is OFF; disabling fully removes the entries;
193-
re-enabling is idempotent and self-heals a stale path (also repaired at boot
194-
via `syncAgentMonitorHooksOnBoot()`). Disk state: a dedicated electron-store
195-
(`agent-monitor-hooks`, key `enabled`).
196-
- **Lifecycle:** health-checked readiness on `GET /api/health` (60s ready
197-
timeout — first run synchronously imports legacy `~/.claude` sessions; ready
198-
≠ import-complete, the iframe populates progressively), crash-restart with
199-
exponential backoff (hard cap; a fixed-port `EADDRINUSE` degrades to "no
200-
monitor", never blocks boot or Claude Code), process-group SIGTERM→SIGKILL
201-
stop wired into `runShutdownSequence` (`agentMonitor.stop`, before
202-
`server.stop`).
203-
- **Security model (by design):** the sidecar reads `~/.claude` **directly**,
204-
*outside* the gateway `isPathAllowed` sandbox. Acceptable and intentional:
205-
bound to `127.0.0.1` only (patched at build time; verified the LAN interface
206-
is refused), the user's own local data, no cloud egress, no auth (consistent
207-
with the unauthenticated `/health` precedent). Hooks only mutate global
208-
Claude config on explicit user opt-in and are fully reversible.
209-
- **Build/packaging:** `scripts/build-agent-monitor.mjs` (run via
210-
`pnpm build:agent-monitor`, chained into `build`) resolves the pnpm-managed
211-
upstream packages, builds the client with Vite, generates
212-
`apps/desktop/.generated/agent-monitor/`, applies the ClosedLoop host
213-
patches (loopback bind, `CCAM_AUTO_INSTALL_HOOKS` gate, uninstall script,
214-
`compat-sqlite` bootstrap), and hard-gates the build on the generated
215-
`compat-sqlite.js` working under Electron-as-Node. Shipped via
216-
`electron-builder.yml` `extraResources` (unpacked, outside the asar)
217-
preserving the `server/``client/dist/` relative layout.
218-
- **Multi-harness support (5 agent tools):** the same dashboard ingests
219-
sessions from **Claude Code** (via hooks), **OpenAI Codex** (rollout JSONL
220-
under `~/.codex/sessions/`), **Cursor** (agent transcripts under
221-
`~/.cursor/projects/`), **GitHub Copilot** (chat JSON under VS Code
222-
`workspaceStorage/` + CLI JSONL under `~/.copilot/session-state/`), and
223-
**OpenCode** (per-message JSON under `~/.local/share/opencode/storage/`).
224-
All non-Claude tools have **no hook system** — their data comes from
225-
file-based importing/watching. Proven, architecture-independent modules
226-
live in-repo at `apps/desktop/scripts/agent-monitor-{codex,cursor,copilot,
227-
opencode}/{tool}-{home,parser,import,watcher}.js` and are copied into the
228-
generated `server/lib/` at materialize time. Each parser emits the same
229-
normalized shape as the upstream Claude importer so the shared
230-
`importSession()` renders all harnesses through the unchanged UI; all
231-
watchers self-heal if data directories don't exist at boot (no app restart
232-
needed for a first-ever session with any tool). All non-Claude paths are
233-
best-effort and never block boot or the Claude path. The build hard-gates
234-
all watcher/import wiring so a future upstream bump can't silently drop
235-
any harness. The user-facing nav/tray label is **"Agent Dashboard"**
236-
(internal ids/IPC channels unchanged). Environment variable overrides:
237-
`$CODEX_HOME`, `$CURSOR_HOME`, `$COPILOT_HOME`, `$OPENCODE_DATA_DIR`.
238-
- **Update procedure:** bump the git dependency commit(s) in
239-
`apps/desktop/package.json`, regenerate the lockfile, and rerun
240-
`pnpm -C apps/desktop build:agent-monitor`. Any change here requires the
241-
`apps/desktop/package.json` version bump (CI-enforced) and a clean-machine
242-
packaged-DMG smoke test (the highest-risk path: `node:sqlite` from the
243-
asar-external, universal-merged binary).
180+
(`AGENT_MONITOR_PORT` in `src/shared/contracts.ts`). It MUST be fixed — the
181+
hook handler POSTs to `127.0.0.1:${CLAUDE_DASHBOARD_PORT||4820}`, baked into
182+
`~/.claude/settings.json` at install time, so 4820 means hooks need zero
183+
per-hook env. 4820 is outside `PORT_PROBE_ORDER`, so it never collides with
184+
the gateway. (FEA-1500 tracks migrating this transport later.)
185+
- **Durable DB:** `app.getPath("userData")/agent-dashboard.sqlite` (schema in
186+
`src/main/database/schema.ts`), Node's built-in `node:sqlite`. Persisted
187+
collector caches live under `<userData>/agent-monitor/`.
188+
- **UI:** a first-party React app in the main window (`src/renderer/`) — NO
189+
iframe. The left sidebar drives the **Dashboard** + agent nav items; live
190+
updates arrive via the `desktop:db:changed` IPC push after each write.
191+
- **Hooks are explicit opt-in (consent-bearing).** The user enables/disables
192+
tracking via the toggle → `src/main/agent-monitor-hooks.ts` writes/removes the
193+
hook entries in `~/.claude/settings.json` (and, opt-in, `~/.codex/hooks.json`).
194+
The hook command runs the Electron binary as Node against a **userData copy**
195+
of the first-party `hook-handler.js` (location-independent across app
196+
moves/updates), at the fixed port 4820. Default is OFF; disabling fully removes
197+
the entries; re-enabling is idempotent and self-heals a stale path (also
198+
repaired at boot via `syncAgentMonitorHooksOnBoot()`). When hooks are ON they
199+
own live Claude capture, so the Claude **file watcher** is gated off (boot
200+
historical import still runs); the four non-Claude tools always file-watch.
201+
Disk state: a dedicated electron-store (`agent-monitor-hooks`, key `enabled`).
202+
- **First-party hook handlers:** `resources/hooks/{hook-handler,codex-hook-handler}.js`
203+
— zero-dependency CommonJS scripts that POST `{ hook_type, data }` to
204+
`:4820`. Shipped via `electron-builder.yml` `extraResources` (`to: hooks`,
205+
unpacked) and resolved by `agent-monitor-path.ts`. No build step, no generated
206+
tree.
207+
- **Security model (by design):** the collectors + listener read the agent-CLI
208+
home dirs (`~/.claude`, `~/.codex`, …) **directly**, outside the gateway
209+
`isPathAllowed` sandbox, but every captured session is dropped unless its
210+
`cwd` is inside the FEA-1407 sandbox base directory (fail-closed). The listener
211+
is bound to `127.0.0.1` only; no cloud egress from collectors. Hooks only
212+
mutate global Claude/Codex config on explicit user opt-in and are reversible.
213+
- **Multi-harness support (5 agent tools):** ingests sessions from **Claude
214+
Code** (hooks live + file historical), **OpenAI Codex** (rollout JSONL under
215+
`~/.codex/sessions/`), **Cursor** (agent transcripts under `~/.cursor/projects/`),
216+
**GitHub Copilot** (chat JSON under VS Code `workspaceStorage/` + CLI JSONL
217+
under `~/.copilot/session-state/`), and **OpenCode** (the `opencode.db` SQLite
218+
store under `~/.local/share/opencode/`). The four non-Claude tools have **no
219+
hook system** — file import/watching is the only capture path. Each
220+
harness's parser (`src/main/collectors/<tool>/`) emits the same normalized
221+
session shape so `importSession` renders all harnesses through the unchanged
222+
UI. Environment variable overrides: `$CODEX_HOME`, `$CURSOR_HOME`,
223+
`$COPILOT_HOME`, `$OPENCODE_DATA_DIR` (Claude uses `$CLAUDE_HOME`).
224+
- **Build/packaging:** the main process is plain `tsc``dist/`; there is no
225+
agent-monitor generative build step. Packaging ships `dist/` (via
226+
`stage-packaging-app.mjs`) plus the unpacked `resources/hooks` handlers. Any
227+
change to `apps/desktop/` requires the `package.json` version bump
228+
(CI-enforced) and a clean-machine packaged-DMG smoke test (the highest-risk
229+
path: `node:sqlite` from the asar-external, universal-merged binary).

apps/desktop/electron-builder.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ extraResources:
2828
- scripts/**/*
2929
- package.json
3030
- LICENSE
31+
# First-party agent-monitor hook handlers (FEA-1503), shipped unpacked outside
32+
# the asar so they resolve as real files. agent-monitor-hooks.ts copies them
33+
# into userData at install time; the installed hook command runs them via the
34+
# Electron binary as Node (ELECTRON_RUN_AS_NODE) against that userData copy.
35+
- from: resources/hooks
36+
to: hooks
37+
filter:
38+
- "*.js"
3139

3240
mac:
3341
icon: resources/icon.icns

apps/desktop/eslint.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ export default tseslint.config(
77
...tseslint.configs.recommended,
88
{
99
files: ["src/**/*.ts"],
10+
// src/renderer is built by Vite and is excluded from tsconfig.json, so it
11+
// is not part of the type-aware TypeScript program. Exclude it from this
12+
// type-checked block too (otherwise its .ts/.d.ts files error with
13+
// "parserOptions.project ... file not found"); renderer files are still
14+
// linted by the recommended (syntactic) config above.
15+
ignores: ["src/renderer/**"],
1016
languageOptions: {
1117
parserOptions: {
1218
project: "./tsconfig.json",

0 commit comments

Comments
 (0)