From d88af088fc1fb46f02e2de82e419721411579a53 Mon Sep 17 00:00:00 2001 From: Thadeus Burgess Date: Fri, 5 Jun 2026 10:42:25 -0500 Subject: [PATCH 1/2] FEA-1500: Obsolete; replace dead in-code anchors - Mark FEA-1500 OBSOLETE on ClosedLoop. Its premise was wrong: the :4820 hook transport is not externally pinned. It lives inside the app-owned handler scripts, which refreshHandlerCopy() overwrites in place on every boot; settings.json only pins the handler path. So handler + listener update together atomically on launch, with no version-skew window. The only real flaw of TCP 4820 is rare port collision, which already degrades safely. Not worth a migration. - agent-monitor-listener.ts: replace the CLOSEDLOOP-TICKET FEA-1500 anchor with the actual design rationale (fixed-port INTERNAL contract, zero per-hook env; unix-socket alternative considered and declined). - hook-handler.js / codex-hook-handler.js: drop the "backward-compatible contract (FEA-1500)" framing; state that the handler and listener must stay in sync since they ship together. - apps/desktop/CLAUDE.md: the Fixed-port bullet now states the loopback-HTTP transport is the accepted permanent design. - Bump desktop version 0.15.111 -> 0.15.112 (CI-enforced for any apps/desktop change). Testing: Comment/doc/version-only change; no code behavior modified. Did not run full typecheck (fresh worktree has no node_modules); the edits are limited to comments, a markdown doc, and the package.json version string, none of which affect compilation or runtime. Risks: None identified. No runtime, gateway, relay, or persisted-store contract changed; the :4820 transport behavior is unchanged. --- apps/desktop/CLAUDE.md | 3 ++- apps/desktop/package.json | 2 +- .../resources/hooks/codex-hook-handler.js | 3 ++- apps/desktop/resources/hooks/hook-handler.js | 5 +++-- apps/desktop/src/main/agent-monitor-listener.ts | 17 ++++++++++------- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/apps/desktop/CLAUDE.md b/apps/desktop/CLAUDE.md index b08711c1..2f2dd659 100644 --- a/apps/desktop/CLAUDE.md +++ b/apps/desktop/CLAUDE.md @@ -181,7 +181,8 @@ Gateway section remains. hook handler POSTs to `127.0.0.1:${CLAUDE_DASHBOARD_PORT||4820}`, baked into `~/.claude/settings.json` at install time, so 4820 means hooks need zero per-hook env. 4820 is outside `PORT_PROBE_ORDER`, so it never collides with - the gateway. (FEA-1500 tracks migrating this transport later.) + the gateway. This loopback-HTTP transport is the accepted permanent design; a + unix-socket alternative was considered and declined (FEA-1500, obsoleted). - **Durable DB:** `app.getPath("userData")/agent-dashboard.sqlite` (schema in `src/main/database/schema.ts`), Node's built-in `node:sqlite`. Persisted collector caches live under `/agent-monitor/`. diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ebe46e1c..415e7e0e 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "desktop", - "version": "0.15.111", + "version": "0.15.112", "description": "ClosedLoop Desktop", "author": "ClosedLoop AI ", "private": true, diff --git a/apps/desktop/resources/hooks/codex-hook-handler.js b/apps/desktop/resources/hooks/codex-hook-handler.js index 56077c40..97d8febb 100644 --- a/apps/desktop/resources/hooks/codex-hook-handler.js +++ b/apps/desktop/resources/hooks/codex-hook-handler.js @@ -10,7 +10,8 @@ * Zero-dependency, plain CommonJS, fail-silent — a hook must never block a Codex * turn. Codex calls this once per lifecycle event (SessionStart, UserPromptSubmit, * PreToolUse, PostToolUse, Stop) with the event name as the single argv arg. The - * port + path + payload envelope are a backward-compatible contract (FEA-1500). + * port + path + payload envelope must stay in sync with the in-process listener + * (src/main/agent-monitor-listener.ts); both ship in the same build. * * Part of FEA-1444 (opt-in Codex hook ingestion). */ diff --git a/apps/desktop/resources/hooks/hook-handler.js b/apps/desktop/resources/hooks/hook-handler.js index 3f564bfa..68c60ef6 100644 --- a/apps/desktop/resources/hooks/hook-handler.js +++ b/apps/desktop/resources/hooks/hook-handler.js @@ -12,8 +12,9 @@ * * Zero-dependency, plain CommonJS, fail-silent: it runs via the Electron binary * as Node (ELECTRON_RUN_AS_NODE) from a userData copy, and must NEVER block or - * fail a Claude turn. The port + path + payload envelope are a backward-compatible - * contract baked into ~/.claude/settings.json (see FEA-1500); do not change them. + * fail a Claude turn. The port + path + payload envelope must stay in sync with + * the in-process listener (src/main/agent-monitor-listener.ts); both ship in the + * same build and refresh together, so do not change one without the other. */ const http = require("http"); diff --git a/apps/desktop/src/main/agent-monitor-listener.ts b/apps/desktop/src/main/agent-monitor-listener.ts index f40f1b52..cb4f2091 100644 --- a/apps/desktop/src/main/agent-monitor-listener.ts +++ b/apps/desktop/src/main/agent-monitor-listener.ts @@ -6,13 +6,16 @@ import { AGENT_MONITOR_PORT } from "../shared/contracts.js"; import { isSessionInSandbox } from "./agent-session-sync-service.js"; import type { createLifecycle, HookData } from "./database/lifecycle.js"; -// CLOSEDLOOP-TICKET FEA-1500: remove legacy HTTP hook listener on 4820 after -// transport migration (FEA-1497 breaking-change discipline contract #1). The hook -// commands baked into ~/.claude/settings.json and ~/.codex/hooks.json POST to -// 127.0.0.1:4820/api/hooks/event; this in-process listener replaces the vendor -// sidecar that previously owned that port. The contract (port, path, payload -// envelope) MUST stay backward-compatible until all installs self-heal to a -// lighter transport. +// Fixed-port hook transport (127.0.0.1:4820). The first-party hook handlers +// (resources/hooks/hook-handler.js, codex-hook-handler.js) POST the +// `{ hook_type, data }` envelope to /api/hooks/event (Codex: +// /api/hooks/codex/event); this in-process listener serves it. Port + path + +// envelope must stay in sync with those handlers, but both ship in the same app +// build and refresh together (refreshHandlerCopy rewrites the userData handler +// copy on boot), so this is an INTERNAL contract — not externally pinned. 4820 is +// fixed and outside PORT_PROBE_ORDER so hooks need zero per-hook env. This +// loopback-HTTP transport is the accepted permanent design; a unix-socket +// alternative was considered and declined (FEA-1500, obsoleted). const HOST = "127.0.0.1"; const MAX_BODY_BYTES = 8 * 1024 * 1024; // hook payloads (incl. large tool_input) cap From 6d6b1ffc4bf378ceba6f78dad8416e35e29054f6 Mon Sep 17 00:00:00 2001 From: Thadeus Burgess Date: Fri, 5 Jun 2026 11:39:18 -0500 Subject: [PATCH 2/2] FEA-1500: Correct anchor wording per review Address shafty023 review on PR #269. The earlier rewording over- corrected from "frozen external contract" to "freely-changeable internal contract"; the real (and now permanent) requirement is in between. - agent-monitor-listener.ts: /api/hooks/event has TWO receivers across modes (legacy AgentMonitorSidecar in default mode, in-process AgentHookListener in design-system mode). Drop the "INTERNAL contract" framing and state the compatibility requirement explicitly: the route + envelope must stay backward-compatible because the userData handler copy refreshes only best-effort on boot (failures caught) and can outlive an app upgrade. - hook-handler.js: envelope must stay compatible with both receivers, not only the in-process listener. - codex-hook-handler.js: same backward-compat framing for /api/hooks/codex/event. - apps/desktop/CLAUDE.md: settings.json pins only the Electron-as-Node command + handler path; the copied handler owns the CLAUDE_DASHBOARD_PORT || 4820 fallback, route, and envelope. Removes the inaccurate "baked into settings.json" premise. Testing: Comment/doc-only change; no code behavior modified. Risks: None identified. No runtime, route, or envelope behavior changed; only the documenting comments. --- apps/desktop/CLAUDE.md | 17 ++++++++++------ .../resources/hooks/codex-hook-handler.js | 6 ++++-- apps/desktop/resources/hooks/hook-handler.js | 10 +++++++--- .../src/main/agent-monitor-listener.ts | 20 +++++++++++++------ 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/apps/desktop/CLAUDE.md b/apps/desktop/CLAUDE.md index 2f2dd659..8f9d11a6 100644 --- a/apps/desktop/CLAUDE.md +++ b/apps/desktop/CLAUDE.md @@ -177,12 +177,17 @@ Gateway section remains. all five agent CLIs, writing through the first-party `importSession` into the same in-process DB. - **Fixed port (differs from the gateway):** `127.0.0.1:4820` - (`AGENT_MONITOR_PORT` in `src/shared/contracts.ts`). It MUST be fixed — the - hook handler POSTs to `127.0.0.1:${CLAUDE_DASHBOARD_PORT||4820}`, baked into - `~/.claude/settings.json` at install time, so 4820 means hooks need zero - per-hook env. 4820 is outside `PORT_PROBE_ORDER`, so it never collides with - the gateway. This loopback-HTTP transport is the accepted permanent design; a - unix-socket alternative was considered and declined (FEA-1500, obsoleted). + (`AGENT_MONITOR_PORT` in `src/shared/contracts.ts`). It MUST be fixed — + `~/.claude/settings.json` pins only the Electron-as-Node command pointing at the + userData `hook-handler.js` copy; that copied handler owns the + `CLAUDE_DASHBOARD_PORT || 4820` fallback, the route, and the payload envelope, so + hooks need zero per-hook env. 4820 is outside `PORT_PROBE_ORDER`, so it never + collides with the gateway. The route + envelope must stay backward-compatible + across handler/receiver changes — a persisted handler copy refreshes only + best-effort on boot, and `/api/hooks/event` is served by **both** the legacy + sidecar (default mode) and the in-process listener (design-system mode). This + loopback-HTTP transport is the accepted permanent design; a unix-socket + alternative was considered and declined (FEA-1500, obsoleted). - **Durable DB:** `app.getPath("userData")/agent-dashboard.sqlite` (schema in `src/main/database/schema.ts`), Node's built-in `node:sqlite`. Persisted collector caches live under `/agent-monitor/`. diff --git a/apps/desktop/resources/hooks/codex-hook-handler.js b/apps/desktop/resources/hooks/codex-hook-handler.js index 97d8febb..36d804be 100644 --- a/apps/desktop/resources/hooks/codex-hook-handler.js +++ b/apps/desktop/resources/hooks/codex-hook-handler.js @@ -10,8 +10,10 @@ * Zero-dependency, plain CommonJS, fail-silent — a hook must never block a Codex * turn. Codex calls this once per lifecycle event (SessionStart, UserPromptSubmit, * PreToolUse, PostToolUse, Stop) with the event name as the single argv arg. The - * port + path + payload envelope must stay in sync with the in-process listener - * (src/main/agent-monitor-listener.ts); both ship in the same build. + * port + route + payload envelope must stay backward-compatible: this handler is + * a persisted userData copy that settings keep invoking and that refreshes only + * best-effort on boot, so do not make breaking changes to /api/hooks/codex/event + * without keeping the 4820 receiver (AgentHookListener) backward-compatible. * * Part of FEA-1444 (opt-in Codex hook ingestion). */ diff --git a/apps/desktop/resources/hooks/hook-handler.js b/apps/desktop/resources/hooks/hook-handler.js index 68c60ef6..05ee9b28 100644 --- a/apps/desktop/resources/hooks/hook-handler.js +++ b/apps/desktop/resources/hooks/hook-handler.js @@ -12,9 +12,13 @@ * * Zero-dependency, plain CommonJS, fail-silent: it runs via the Electron binary * as Node (ELECTRON_RUN_AS_NODE) from a userData copy, and must NEVER block or - * fail a Claude turn. The port + path + payload envelope must stay in sync with - * the in-process listener (src/main/agent-monitor-listener.ts); both ship in the - * same build and refresh together, so do not change one without the other. + * fail a Claude turn. The port + route + payload envelope must stay + * backward-compatible with BOTH receivers of /api/hooks/event: the legacy + * AgentMonitorSidecar (default Agent Monitor mode) and the in-process + * AgentHookListener (design-system mode). This handler is a persisted userData + * copy that settings.json keeps invoking and that refreshes only best-effort on + * boot, so do not make breaking route/envelope changes without keeping the + * receivers backward-compatible. */ const http = require("http"); diff --git a/apps/desktop/src/main/agent-monitor-listener.ts b/apps/desktop/src/main/agent-monitor-listener.ts index cb4f2091..22f6b3ef 100644 --- a/apps/desktop/src/main/agent-monitor-listener.ts +++ b/apps/desktop/src/main/agent-monitor-listener.ts @@ -9,12 +9,20 @@ import type { createLifecycle, HookData } from "./database/lifecycle.js"; // Fixed-port hook transport (127.0.0.1:4820). The first-party hook handlers // (resources/hooks/hook-handler.js, codex-hook-handler.js) POST the // `{ hook_type, data }` envelope to /api/hooks/event (Codex: -// /api/hooks/codex/event); this in-process listener serves it. Port + path + -// envelope must stay in sync with those handlers, but both ship in the same app -// build and refresh together (refreshHandlerCopy rewrites the userData handler -// copy on boot), so this is an INTERNAL contract — not externally pinned. 4820 is -// fixed and outside PORT_PROBE_ORDER so hooks need zero per-hook env. This -// loopback-HTTP transport is the accepted permanent design; a unix-socket +// /api/hooks/codex/event). This in-process listener serves that route in +// design-system mode; in default mode the legacy AgentMonitorSidecar serves the +// same route, so /api/hooks/event has two receivers across modes. +// +// COMPATIBILITY REQUIREMENT — the route + payload envelope must stay +// backward-compatible; do NOT treat it as freely changeable. The handler is a +// persisted userData copy that Claude/Codex settings keep invoking; +// refreshHandlerCopy() only refreshes it best-effort on boot (failures are +// caught and logged), so a stale copy can outlive an app upgrade. Any route or +// envelope change therefore needs backward-compatible receiver support until all +// persisted handler copies are known refreshed. +// +// 4820 is fixed and outside PORT_PROBE_ORDER so hooks need zero per-hook env. +// This loopback-HTTP transport is the accepted permanent design; a unix-socket // alternative was considered and declined (FEA-1500, obsoleted). const HOST = "127.0.0.1";