Skip to content

feat(adapter-prime-agent): collocate a Prime Intellect Prime Agent with a DKG node - #2113

Merged
branarakic merged 8 commits into
testnet-canaryfrom
feat/adapter-prime-agent-design
Aug 6, 2026
Merged

feat(adapter-prime-agent): collocate a Prime Intellect Prime Agent with a DKG node#2113
branarakic merged 8 commits into
testnet-canaryfrom
feat/adapter-prime-agent-design

Conversation

@Zigoljube

Copy link
Copy Markdown
Contributor

Summary

Adds packages/adapter-prime-agent, which collocates a Prime Intellect Prime Agent with a DKG V10 node the way adapter-hermes does for Hermes: connect it from the Node UI, chat to it from the Connected Agents panel, and have the node route to the session the operator is actually using.

The interesting constraint is that Prime Agent has no HTTP server, and its daemon socket protocol disclaims itself as "not the final remote gateway protocol". What it does have is extensions — TypeScript modules jiti-imported into the process that owns a running session. So the adapter ships an extension that hosts a small loopback HTTP bridge speaking the contract the daemon already speaks for Hermes (/health, /send, /stream). Because the extension lives inside the session, a message from the Node UI lands in the session the user is looking at — which a spawned --mode rpc subprocess cannot do.

Two consequences of that design are not incidental, and the code comments say so:

  • Bridges are per session, not per installation. Extensions load per session and one worker hosts many, so each session publishes a descriptor into a discovery directory and the daemon reads it. "Installed but no session running" is therefore a first-class non-error state, not a fault — the UI says so rather than showing a red badge the operator cannot act on.
  • Ephemeral ports (listen(0)) are mandatory. On /reload, /new, /fork and /resume the process survives and the extension is re-imported with moduleCache: false, so the successor holds no reference to the old server, and nothing in the host closes extension-owned sockets. A fixed port would be held by an orphan until process exit.

Design rationale, alternatives, and the parity analysis against adapter-hermes are in agent-docs/adapters/prime-agent/DESIGN.md; the transport decision is .ai/adr/0007.

Changes

New package — packages/adapter-prime-agent

  • Extension-hosted loopback bridge: /health (echoes its own sessionId, so the daemon can detect a port the OS recycled to another process), /send, /stream (SSE). All three require x-dkg-bridge-token, verified with a timing-safe comparison, and return 503 rather than 401 when no token has been provisioned at all — an unauthenticated bridge would let anything on loopback drive the agent.
  • Per-session discovery under ~/.prime/agent/.dkg-adapter-prime-agent/sessions/<sessionId>.json, written on session_start, removed on session_shutdown, pruned by the daemon when the owning pid is gone.
  • Reversible setup, in the order that has to survive a SIGINT: state file with priorSettings → verbatim settings.json.bak.<unix-ms> → rewrite. priorSettings is first-wins so a re-run never overwrites the original truth, and restore is surgical — it removes exactly our entry, because unlike Hermes' scalar memory.provider this is an array the user may legitimately have edited.
  • Guards default closed: allow_direct_publish and allow_context_graph_admin_tools are false, import_roots empty. Prime Agent executes model-authored Python in a persistent kernel with the user's permissions and is explicitly not a sandbox, so a publish path should be opened by an operator, never inferred.

Daemon (packages/cli)

  • daemon/prime-agent.ts — target resolution from the discovery directory, health probing, and a loopback-only guard. A descriptor is written by another process, so it is untrusted input: the daemon must never be talked into POSTing a user's chat to an off-box address.
  • daemon/routes/prime-agent.ts/api/prime-agent-channel/{send,stream,health}. Health always answers 200; ok: false with sessionCount: 0 is idle. An addressed session that has ended returns 409 and never falls back to another live session — a message meant for one conversation must not land in another. The bridge's one-turn 429 is surfaced as "busy", not as an error.
  • The integrations listing reports live Prime Agent session counts, so the panel can tell "installed but idle" from "not installed" without a second round trip.
  • Connect-from-UI runs the adapter setup first (idempotent), then probes.

Node UI (packages/node-ui)

  • Prime Agent panel block with session state and a Connect button; LocalAgentIntegration carries sessionCount / activeSessionId.
  • A prime-agent surface in ui/api.ts. The Hermes SSE reader was generalised into streamDeltaFrameLocalChat and both channels now share it rather than keeping two copies of the partial-frame handling.
  • Vendor marks for OpenClaw, Hermes and Prime Agent, inlined as alpha masks painted with currentColor so one asset serves both themes.

Shared SSE fix — affects OpenClaw and Hermes too

final is the terminal semantic event; upstream EOF is a transport event, and a bridge is entitled to hold its connection open after answering — the Prime Agent extension bridge does, since the session outlives the turn. Both the shared daemon proxy (daemon/openclaw.ts) and the Node UI readers were waiting for EOF, which left the composer spinning over an answer already on screen and the agent's turn lock looking held. Both now end on the frame. Detection is frame-accurate: it buffers across chunk boundaries, tolerates CRLF, never treats a non-JSON data: frame as terminal, and stops scanning past a 1 MB unterminated event rather than buffering without bound.

Reviewers may want to look hardest here, since it is the one change on a path OpenClaw and Hermes already use. Both routes guard their trailing writes with if (!res.writableEnded), so nothing downstream changes for them — but if either bridge emits something meaningful after final, this would truncate it and the termination should be scoped to the prime-agent channel instead.

Test Plan

  • Tests pass locally (see below for exactly what was run)
  • Build succeeds (pnpm --filter @origintrail-official/dkg build)
  • Manually tested against a running daemon

Added

Suite Count Covers
packages/adapter-prime-agent/test/* 27 Bridge contract driven by a stub pi with no Prime Agent running, so our half of the wire is asserted independently of upstream: the 401/503 auth split, health shape, SSE frame format, the 429 concurrency guard, and the discovery registry
packages/cli/test/daemon-prime-agent.test.ts 19 Routing, the loopback guard, dead-pid pruning, recycled-port detection, busy mapping, the no-silent-reroute property, SSE headers, and the three connect outcomes
packages/cli/test/daemon-sse-final-frame.test.ts 5 Terminal-frame handling in the shared proxy, over a reader that never reports EOF
packages/node-ui/test/prime-agent-panel.test.ts 4 Panel rendering, including zero-sessions-is-not-an-error
packages/node-ui/test/ui-sse-final-frame.test.ts 2 UI readers resolving on final, against a server that deliberately never closes the response
packages/node-ui/e2e/specs/prime-agent-connect.spec.ts 3 Real-node connect surface, mirroring hermes-connect.spec.ts

Each SSE regression test was checked to fail (hang) without its fix, not merely to pass with it.

Full packages/cli suite: 55 failed | 2933 passed | 217 skipped. That failure count is identical to origin/main — I ran the same suite on 843f521 for a baseline (55 failed | 2909 passed), and the set of failing files is byte-identical (24 files, environment-dependent: sqlite embeddings, StorageACK timing, publisher handoff). This branch adds 24 passing tests and introduces no new failures.

packages/node-ui's unit suite has pre-existing failures in a clean clone (happy-dom's localStorage lacks the Storage methods and stores/journey.ts reads it at module scope); the suites touching the shared reader and the new panel all pass — 98 tests across ui-api-stream, openclaw-bridge, prime-agent-panel and ui-sse-final-frame.

Manual validation was performed by a Prime Agent instance running the branch against a live daemon (10.0.12 / bb83f98): health ok: true against a real per-session bridge, a stream probe returning a final frame and closing in ~2.9s, a follow-up /send completing in ~2.0s proving the session lock is released, and a Playwright check of the Prime tab rendering a reply with no spinner hang and no pattern error. The SSE fixes in this PR came out of that testing.

Known limits, stated so reviewers do not have to find them

  • No session picker. The daemon honours an explicit sessionId and otherwise routes to the newest live session, but the panel offers no selector — the descriptor carries no stable, human-meaningful per-session label yet.
  • No default session id, so chat history for a session opened outside the UI is not back-filled.
  • chatAttachments is deliberately not advertised: the route omits the attachment-provenance pipeline, so claiming the capability would promise more than the channel delivers.
  • Memory election hooks (before_agent_start, turn_end) are not wired yet; the capability is advertised but the hook set is staged. Likewise the Python kernel skill, so there is no model-facing dkg API yet.

Staging for the remainder is in IMPLEMENTATION-PLAN.md; open risks are in RISKS.md, including that prime-agent is a fast-moving upstream whose own docs already lag its code (the daemon protocol is v7/schema-13 while the docs say v4).

Related Issues

None — this is the first Prime Intellect adapter in the repo.

🤖 Generated with Claude Code

Žiga Drev and others added 5 commits August 6, 2026 10:59
…gent

Stage-1 investigation and design only — no adapter code. Written against
843f5213 and PrimeIntellect-ai/prime-agent@0e0d2339.

Recommendation: ship the adapter as a Prime Agent *extension* hosting a
loopback HTTP bridge (/health, /send, /stream) — the contract the daemon
already speaks for Hermes bridge targets — plus a Python-backed `dkg`
kernel skill and a thin TS setup package mirroring adapter-hermes.

Why the extension: extensions are jiti-imported into the process that owns
the running session, so this is real collocation rather than spawning a
second agent, and it avoids prime-agent's daemon socket, whose own header
calls it "not the final remote gateway protocol" and which has no
in-protocol auth.

Three architectural mismatches are named as constraints rather than smoothed
over: there is no memory-provider slot (election is re-derived as a hook
set writing through to the Continual Harness), there is no HTTP server of
any kind, and tools are not the idiom (prime-agent ships exactly one
built-in tool, `ipython`).

Documents:
- agent-docs/adapters/prime-agent/DESIGN.md — Q1-Q10 with citations
- .ai/adr/0007-prime-agent-adapter-transport.md — the transport decision
- agent-docs/adapters/prime-agent/PARITY-MATRIX.md — every Hermes capability
  as v1 / v2 / dropped-with-reason
- agent-docs/adapters/prime-agent/IMPLEMENTATION-PLAN.md — staged, Stage 1
  proves the transport before any memory or tool work
- agent-docs/adapters/prime-agent/RISKS.md — upstream churn, pinning,
  silent-vs-loud failure modes

Two Stage-0 blockers remain open and are marked UNVERIFIED in DESIGN.md:
how an extension learns its own active session id, and whether a listener
can be bound at load time or only from session_start.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the transport decided in .ai/adr/0007: a Prime Agent extension that
hosts a loopback HTTP bridge speaking the /health, /send, /stream contract the
DKG daemon already speaks for Hermes bridge targets. Because extensions are
jiti-imported into the process that owns a running session, a message from the
Node UI lands in the session the user is actually using — which a spawned
--mode rpc subprocess cannot do.

Both Stage-0 blockers are now resolved against prime-agent@0e0d2339 and the
answers shaped the implementation:

- Session identity is ctx.sessionManager.getSessionId() (uuidv7, in the
  ReadonlySessionManager Pick, session-manager.ts:314/1398-1400). It is NOT on
  `pi`, so the earliest bind point is the session_start handler, not the
  factory body.
- Ephemeral ports are mandatory, not a preference: on /reload, /new, /fork and
  /resume the process survives and the extension is re-imported with
  moduleCache:false, so a successor has no reference to the old server and
  nothing in the host closes extension-owned sockets. A fixed port would be
  held by an orphan until process exit.
- session_shutdown fires for all five teardown reasons and is awaited, so
  cleanup is unconditional — gating on reason === 'quit' would leak a listener.

Contents:
- packages/adapter-prime-agent: types, setup lifecycle (first-wins priorSettings
  snapshot -> .bak.<unix-ms> -> settings.json rewrite, surgical restore),
  per-session discovery registry, daemon plugin, lazy setup-entry, and the
  extension bridge itself.
- packages/cli/src/daemon/prime-agent.ts: target resolution from the discovery
  directory (Hermes reads static config; we cannot), loopback enforcement,
  health probe with sessionId echo to detect recycled ports, pre-send gate,
  payload normalisation, transport patch.
- local-agents.ts: prime-agent registry entry (transport.kind is an
  unconstrained string, so config.ts needs no change).

27 tests pass. bridge-contract.test.ts drives the bridge with a stub `pi` and
no Prime Agent running, asserting our half of the wire independently of
upstream: the 401-vs-503 auth split, ok===true health shape, SSE data: <json>
framing, 429 concurrency guard, and descriptor lifecycle including stale-pid
pruning and non-loopback rejection. It caught a real bug — Node buffers SSE
headers until the first write, so /stream needed an explicit flush or a slow
first token would look like a hang.

Not yet implemented (staged): memory-election hooks, the Python kernel skill,
Node UI session selector. See agent-docs/adapters/prime-agent/IMPLEMENTATION-PLAN.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nt channel

Before this, the integration registered but the Node UI had no branch for it,
so it surfaced as "registered, panel pending" — visible in the list, with no
way to connect, chat, or see whether anything was live.

Daemon:
- routes/prime-agent.ts serves /api/prime-agent-channel/{send,stream,health},
  dispatched from handle-request.ts. Health always answers 200: `ok: false`
  with `sessionCount: 0` is the idle state, not a fault.
- An addressed session that is gone returns 409 rather than falling back to
  another live session — a message meant for one conversation must never land
  in a different one.
- The bridge's one-turn 429 is mapped through as "busy", not as an error.
- GET /api/local-agent-integrations reads the discovery directory for the
  prime-agent record, so the panel can tell "installed but idle" from "not
  installed" without a second round trip.
- Connect from the UI now runs the adapter setup first (idempotent), then
  probes. Previously it only probed, so a first-time operator would wait for a
  bridge that no installed extension could ever publish.

Node UI:
- Prime Agent panel block with per-session copy and a Connect button;
  LocalAgentIntegration carries sessionCount / activeSessionId.
- A `prime-agent` surface in ui/api.ts (health + stream + connect). The Hermes
  SSE reader is generalised into streamDeltaFrameLocalChat and shared, rather
  than forking a second copy of the partial-frame handling.
- Agent marks for OpenClaw, Hermes and Prime Agent, inlined as alpha masks and
  painted with currentColor so one asset serves both themes.

Tests: 18 daemon tests (routing, loopback guard, descriptor pruning,
recycled-port detection, busy mapping, no-silent-reroute, connect paths),
4 panel render tests, and an e2e spec mirroring hermes-connect.

Not included, and now stated as such in the plan and parity matrix: no session
picker (the descriptor carries no stable per-session label yet), and chat
attachments stay unadvertised because the route omits the provenance pipeline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
At 14px the Hermes portrait collapses into an indistinct blob — the OpenClaw
bug and the Prime Intellect swoosh survive that size, but a mixed row where one
mark is unreadable is worse than no marks. 18px in the detail rows, 14px in the
compact tab strip where the agent name is right beside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…stream

An SSE contract bug across three layers, reported from a live Prime Agent
session on bb83f98. Prime was answering correctly; the DKG and UI layers were
mishandling the terminal event and the response headers.

`final` is the terminal SEMANTIC event. Upstream EOF is a TRANSPORT event, and
a bridge is entitled to hold its connection open after answering — the Prime
Agent extension bridge does, since the session outlives the turn. Every layer
here was waiting for EOF instead:

1. packages/node-ui/src/ui/api.ts — both SSE readers looped until the socket
   closed, so the promise never settled and the composer kept spinning over an
   answer already rendered on screen. They now resolve on the final frame and
   cancel the reader.

2. packages/cli/src/daemon/openclaw.ts — the shared proxy (OpenClaw, Hermes and
   Prime Agent all use it) forwarded the final frame downstream but kept the
   browser response open pending upstream EOF, which also left the Prime
   session looking locked. It now detects a complete SSE frame whose JSON
   payload is `type: "final"`, ends downstream and cancels upstream. Detection
   is frame-accurate: it buffers across chunk boundaries, tolerates CRLF, never
   treats a non-JSON frame as terminal, and stops scanning past a 1 MB
   unterminated event rather than buffering without bound.

3. packages/cli/src/daemon/routes/prime-agent.ts — the route piped SSE bytes
   without ever writing a Content-Type, so the browser misclassified the body
   and threw "The string did not match the expected pattern". It now writes
   text/event-stream; charset=utf-8, no-cache/no-transform and keep-alive
   before the first byte, matching the Hermes and OpenClaw routes.

Regression coverage for all three, each verified to fail (hang) without its
fix: 5 proxy tests over a reader that never reports EOF, 2 UI reader tests
against a server that deliberately never closes the response, and a route test
asserting the SSE headers and that the response closes on the final frame.

Reported by the Prime Agent instance testing the branch, with local validation
already performed against a running daemon (health ok, ~2.9s stream close, a
follow-up send proving the session lock is released).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@otReviewAgent otReviewAgent left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Operational Notice: Review Agent could not complete this review.

Business logic reviewer failed: WARNING: failed to clean up stale arg0 temp dirs: Permission denied (os error 13)

@branarakic branarakic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff (40 files), with the daemon/UI changes checked against the existing OpenClaw and Hermes code paths they touch or mirror. The architecture is sound and the docs are unusually honest, but as shipped the connect flow cannot work end-to-end: two functional blockers, one security-consistency fix, and a set of should-fix correctness items below. The shared SSE change — the part the PR asks reviewers to look hardest at — I verified against every current emitter and believe is safe (details at the end).

Blockers

1. The bridge auth token is never provisioned — every bridge answers 503 out of the box.
The extension accepts a token only from DKG_BRIDGE_TOKEN env or <stateDir>/dkg.json.bridge_token (extension/src/extension.ts:80-91). Nothing in the PR writes either: ADAPTER_CONFIG_FILENAME = 'dkg.json' is declared in setup.ts:47 and exported but never used in a write, and PrimeAgentAdapterConfig.bridgeToken (types.ts) is never consumed. Compare the parity model: Hermes setup writes a managed dkg.json (adapter-hermes/src/setup.ts:228-236) and sources the node token via loadDkgAuthToken() (env → $DKG_HOME/auth.token, setup.ts:1198), converging with the daemon's loadBridgeAuthToken() (daemon/openclaw.ts:151). As shipped, Connect-from-UI runs setup, probes health with the daemon's token, and the bridge replies 503 "Bridge auth token unavailable" — the panel is permanently stuck at "sessions but none answered a health probe". The tests mask this by setting DKG_BRIDGE_TOKEN directly, and PARITY-MATRIX.md claims adapter config is "Written to adapter dkg.json" in v1 — the docs promise it, the code doesn't do it.

Fix: have setup write dkg.json (mode 0600) with bridge_token sourced like Hermes' loadDkgAuthToken, or give the extension a fallback read of $DKG_HOME/auth.token; add one test that goes token-source → bridge accept without env injection.

2. The extension bundle is never built.
defaultExtensionPath() resolves to extension/dist/extension.js (setup.ts:83-87), but the package build script is plain tsc on the root tsconfig (include: ["src"]); nothing invokes extension/tsconfig.json, extension/dist is gitignored and not committed, and no prebuild step references it. So pnpm --filter …adapter-prime-agent build succeeds while producing no extension, setup registers a path to a nonexistent file in the user's settings.json (Connect-from-UI passes verify: false, so the "extension bundle missing" signal is only a warning), and the published tarball's files: ["extension", …] would ship sources without the bundle.

Fix: "build": "tsc && tsc -p extension/tsconfig.json", and make setup hard-fail (not warn) when the bundle is absent.

3. Security: the loopback guard regressed relative to the function it mirrors.
Both new copies — isPrimeAgentLoopbackUrl (packages/cli/src/daemon/prime-agent.ts:53-64) and isLoopbackBridgeUrl (session-registry.ts:92-106) — use hostname.startsWith('127.'). That matches DNS names like 127.evil.example.com, which resolve wherever an attacker points them. The existing isHermesLoopbackUrl (daemon/hermes.ts:147-159) gets this right: isIP(host) === 4 && host.startsWith('127.'). These guards exist precisely because descriptors are untrusted input written by another process (the code's own words), so within that stated threat model the guard is bypassable — a planted descriptor gets the daemon to POST the operator's chat plus the bridge token to an off-box host. The loopback test covers evil.example.com but not the 127.-prefixed DNS shape.

Fix: add the isIP check, and ideally collapse the two new copies into one shared helper so they can't drift again.

Should-fix

4. Torn-write race, and the code contradicts its own comment. writeSessionDescriptor says "a torn read is handled by the reader (parse failures are skipped)" (session-registry.ts:42-43) — but both readers delete unparseable files (session-registry.ts:134-136, daemon/prime-agent.ts:112-119). writeFileSync isn't atomic, so a reader catching a descriptor mid-write permanently deletes a live session's discovery entry; the extension never rewrites it until the next session_start. Write temp-then-rename, or prune only on dead pid.

5. Re-run clobbers the original settings backup. Step 2 of setupPrimeAgentProfile writes current settings bytes to state.priorSettings!.settingsBackupPath (setup.ts:276-279), which first-wins preserves from run 1. If the user removed the entry and re-runs setup, the pristine run-1 backup is overwritten with the current file — contradicting "a re-run never overwrites the original truth". Skip the write when the backup file already exists, or mint a fresh backup path per run.

6. The turn timeout is a flat cap misnamed IDLE, and it enables cross-turn transcript bleed. TURN_IDLE_TIMEOUT_MS is armed once at turn start and never reset on deltas (extension.ts:262-263), so an agentic turn longer than 15 minutes (Prime Agent runs a persistent Python kernel — long turns are its normal mode) settles with partial text presented as a clean final, with no timedOut marker. Worse: after force-settle the busy-check clears, a new /send is accepted, and the still-running previous turn's deltas then append to the new turn's chunks (onMessageUpdate only checks the current turn's settled). Reset the timer per delta so it's genuinely idle-based, and don't accept a new turn until agent_end.

7. Delta extraction ignores the event type. onMessageUpdate takes delta ?? text from any assistantMessageEvent (extension.ts:300-302) with no type filter. If upstream ever emits cumulative-text snapshots or thinking/reasoning events alongside deltas, transcripts duplicate or leak reasoning into the reply. The header comment says behavior was verified against prime-agent @0e0d2339 — pin the filter to the event types you verified, since RISKS.md itself calls upstream fast-moving with lagging docs.

8. dkg prime-agent CLI command is not registered. setup.ts ships seven verbs "named to match dkg hermes <verb>", but there is no commands/prime-agent.ts (Hermes has commands/hermes.ts:150), and no prime-agent reference exists in the CLI outside the daemon. Doctor/verify/uninstall — the recovery story the README leans on — are unreachable except via UI connect. Register the command or scope the claim.

9. sendUserMessage(text) drops deliverAs. DESIGN.md and the parity matrix promise pi.sendUserMessage(..., { deliverAs }) in three places; the code passes no options (extension.ts:284), so the host default silently decides whether a UI message steers (interrupts) or queues behind an operator's in-flight local turn. Related and worth documenting: the bridge's busy-check only tracks bridge-initiated turns — concurrent local typing interleaves attribution (the assistant's response to the operator can settle a pending bridge turn). That may be acceptable "it's the same session" semantics, but it should be a stated decision, not an accident.

Verified safe: the shared SSE final-frame change

Checked every current emitter rather than taking the PR's caveat at face value:

  • OpenClaw's generator yields final last and returns (DkgChannelPlugin.ts:1737,1978); the route is a raw passthrough with persistence out-of-band via /persist-turn — nothing follows final.
  • Hermes: the hermes-openai branch synthesizes exactly one enriched final itself after pipeHermesOpenAiStream (untouched by this PR, routes/hermes.ts:456-463); the native hermes-channel branch is a raw passthrough where upstream's final is the last meaningful frame. No path emits a second, enriched final after a forwarded one — so first-final-wins in the UI readers loses nothing.
  • The detector is properly fail-open: non-JSON frames aren't terminal, multi-line data: events simply fall back to EOF behavior, the 1 MB cap stops scanning rather than the stream, and the final frame is forwarded before res.end(). The regression tests (checked to hang without the fix) are exactly right.

One suggestion: the README now states "nothing meaningful follows final" as a contract — add a daemon-side test asserting frames after final are intentionally dropped, so a future bridge change fails a test instead of silently truncating.

Minor

  • Descriptor parsing/pruning/liveness is duplicated between daemon/prime-agent.ts and the adapter registry (~120 lines), and they already diverge (the daemon skips isSafeSessionId and startedAt type validation). packages/cli depends on the adapter package — import the registry instead.
  • version: '10.0.12' is hardcoded in setup.ts (buildState) and prime-agent-routes.ts — read from package.json; these rot on the next release.
  • runUninstall prints "(backup retained)" unconditionally, but the backup-rename fallback consumes the backup.
  • Restore's fallback renames the backup over a malformed settings.json the user may be mid-edit (setup.ts:314-326) — setup refuses to touch malformed files, restore replaces them; consider preserving the malformed file as .rejected.
  • already is an exact-path match; after the install path changes, a stale entry both survives restore and coexists with the new one. Dedupe by the dkg-adapter-prime-agent marker on add and remove.

Tests are genuinely good (recycled-port detection, no-silent-reroute, dead-pid pruning, zero-sessions-is-idle, an e2e that deliberately doesn't rewrite the operator's real profile) — the gaps are precisely the two blockers, which the tests bypass via env injection and TS-source imports. The design docs are a model for adapter PRs here, but three doc-vs-code divergences need reconciling: dkg.json "written in v1" (it isn't), deliverAs (not passed), and "parse failures are skipped" (they're pruned).

🤖 Generated with Claude Code

@otReviewAgent otReviewAgent left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Operational Notice: Review Agent could not complete this review.

Business logic reviewer failed: WARNING: failed to clean up stale arg0 temp dirs: Permission denied (os error 13)

@branarakic

Copy link
Copy Markdown
Contributor

Addressed the consolidated review in b1df73c.

Key fixes:

  • setup now provisions the DKG bridge token into a private mode-0600 adapter dkg.json, with an end-to-end setup-token-to-live-bridge test
  • the Prime Agent extension is built by the package build, setup fails when the bundle is absent, and the packed artifact was verified to contain extension/dist/extension.js
  • loopback validation now requires a literal IPv4 loopback address and is shared with the daemon; 127.evil.example.com is rejected
  • descriptor writes are atomic; malformed/torn descriptors are skipped rather than deleted
  • setup preserves the original backup, preserves rejected malformed settings, and removes stale managed extension paths
  • turn timeout is now activity-based, timed-out turns remain locked until agent_end, local-agent busy is detected, and only text_delta contributes reply text
  • delivery is explicitly followUp
  • dkg prime-agent setup/status/verify/doctor/disconnect/reconnect/uninstall is registered in the built CLI
  • hardcoded versions and unimplemented Stage-1 capability claims were removed
  • a daemon regression test now proves bytes after the first final SSE frame are dropped

Validation at the pushed head:

  • full dependency build and final CLI build: pass
  • adapter tests: 37/37
  • focused CLI Prime Agent tests: 20/20
  • shared SSE daemon tests: 6/6
  • Node UI Prime Agent/SSE tests: 6/6
  • scripts/CI routing tests: 95/95
  • adapter pnpm pack --dry-run: pass; extension bundle present
  • built dkg prime-agent --help: all seven verbs present
  • git diff --check: pass

A real Prime Agent host smoke remains the final runtime acceptance step; Prime Agent is not installed on the machine used for this implementation pass.

@branarakic branarakic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed b1df73c5b. All findings from the previous review are fixed, and I verified each independently — fresh worktree at the pushed head, full dependency build, then: adapter suite 37/37; daemon-prime-agent + daemon-sse-final-frame + commands-prime-agent 27/27; pnpm pack --dry-run includes extension/dist/extension.js; the built CLI's dkg prime-agent --help lists all seven verbs; and the setup test's token path (auth.token → dkg.json → live bridge accepting the header) is exactly the end-to-end proof that was missing. The daemon delegating descriptor validation to the adapter package resolved the duplication concern wholesale, and the timeout/lock/text_delta reworks in the extension are all pinned by tests. Good round.

One new issue introduced by this commit, though — real, reproduced, and on the shared path:

UTF-8 corruption in the post-final truncation (pipeOpenClawStream)

inspectChunk truncates the terminal chunk by slicing the decoded string and re-encoding it (encoder.encode(decodedChunk.slice(0, charsFromCurrentChunk)), packages/cli/src/daemon/openclaw.ts). When the chunk carrying final begins mid-UTF-8-codepoint, TextDecoder (with stream: true) is holding that codepoint's lead bytes from the previous chunk — which was already forwarded verbatim. The decoder then emits the completed character at the head of the terminal chunk's decoded text, and the re-encode sends all of its bytes again. The wire ends up with orphaned lead bytes followed by the full character, and the client renders a stray in the last delta before final.

Failing repro against this head (drop into packages/cli/test/):

import { EventEmitter } from 'node:events';
import { describe, expect, it } from 'vitest';
import { pipeOpenClawStream } from '../src/daemon/openclaw.js';

function makeRes() {
  const chunks: Buffer[] = [];
  return {
    chunks, writableEnded: false, headersSent: true,
    write(c: Uint8Array | string, cb?: () => void) { chunks.push(Buffer.from(c as Uint8Array)); cb?.(); return true; },
    end() { (this as { writableEnded: boolean }).writableEnded = true; },
    on() {}, once() {}, flushHeaders() {},
  } as never;
}

function makeReader(byteChunks: Uint8Array[]) {
  let i = 0;
  return {
    read: async () => (i < byteChunks.length ? { done: false, value: byteChunks[i++] } : { done: true, value: undefined }),
    cancel: async () => {}, releaseLock: () => {},
  };
}

it('forwards byte-identical output when a multi-byte char straddles the chunk before a coalesced final', async () => {
  const frame1 = 'data: {"type":"delta","text":"hi 😀"}\n\n';
  const frame2 = 'data: {"type":"final","text":"done"}\n\n';
  const frame3 = 'data: {"type":"delta","text":"after"}\n\n';
  const all = Buffer.from(frame1 + frame2 + frame3, 'utf8');
  const splitAt = Buffer.byteLength(frame1, 'utf8') - 6; // inside the 4-byte emoji
  const res = makeRes();
  await pipeOpenClawStream(new EventEmitter() as never, res, makeReader([all.subarray(0, splitAt), all.subarray(splitAt)]) as never);
  const forwarded = Buffer.concat((res as { chunks: Buffer[] }).chunks);
  expect(forwarded.toString('utf8')).toBe(frame1 + frame2); // fails: "hi �😀"
});

Observed: data: {"type":"delta","text":"hi �😀"} — expected hi 😀. Trigger conditions are narrow (non-ASCII delta content + a TCP/proxy split landing inside a codepoint + final coalesced into the following chunk), but this is user-visible output corruption on the path OpenClaw and Hermes already use, and it did not exist before this commit (the previous version forwarded the terminal chunk verbatim).

Suggested fix: do the frame scan and the truncation in byte space. SSE separators (\n\n / \r\n\r\n) are pure ASCII, so keep a Uint8Array tail buffer, find the separator byte pattern, and slice the original chunk bytes at that offset — no decode→slice→re-encode round trip on the forwarded bytes. Decoding a complete frame just for isTerminalSseFrame's JSON parse stays safe, since a complete frame can't end mid-codepoint. That keeps the (good) post-final drop semantics and makes byte-identical passthrough structural. The repro above can land as the regression test.

Two small notes, take or leave:

  • setup.test.ts's existsSync(defaultExtensionPath()) assertion makes the adapter suite depend on a prior pnpm build — on a clean checkout vitest run fails on that one test. A pretest build hook (or skipping that assertion when the bundle is absent) would decouple them.
  • The bridge's /send idle-timeout returns 504, but the daemon route remaps every non-429 bridge failure to a generic 502 BRIDGE_ERROR; propagating 504/timedOut would keep the new timeout semantics visible end-to-end.

🤖 Generated with Claude Code

@Zigoljube

Copy link
Copy Markdown
Contributor Author

Real Prime Agent smoke test result after removing previous local adapter entry: FAIL

Tested live PR head:

Environment:

  • Prime Agent: 0.7.0
  • Prime executable: /opt/homebrew/bin/prime-agent
  • Prime config dir: /Users/clawdnode/.prime/agent
  • DKG home: /Users/clawdnode/.dkg-mainnet
  • DKG daemon under test: 10.0.12, commit b1df73c5, monorepo, edge, chain base:8453
  • Node/pnpm: v22.22.0 / 10.28.1

Build/setup validation:

  • pnpm install --frozen-lockfile: PASS
  • pnpm -r --filter @origintrail-official/dkg... run build: PASS
  • pnpm --filter @origintrail-official/dkg-node-ui run build:ui: PASS
  • pnpm --filter @origintrail-official/dkg-adapter-prime-agent test: PASS, 37/37
  • node packages/cli/dist/cli.js prime-agent --help: exposes setup, status, verify, doctor, disconnect, reconnect, uninstall
  • setup dry-run: completed, degraded; settings patched: true
  • setup: completed, degraded; settings patched: true before Prime session start
  • verify/doctor: PASS before Prime start with live sessions: 0
  • adapter dkg.json mode: 0600

Initial live smoke after cleanup:

  • Prime settings contained exactly one adapter extension entry, pointing at the fresh PR checkout.
  • dkg prime-agent status discovered exactly one live session:
    • session 019fd788-d857-7479-9924-792740b9bdf9
    • bridge http://127.0.0.1:58041
    • pid 56935
  • health: ok:true, sessionCount:1
  • first stream: PASS, returned PR2113_FIRST_OK, incremental deltas, final frame, closed in ~2.28s
  • second stream: PASS, returned PR2113_SECOND_OK, no leakage from first response, closed in ~1.59s
  • SSE headers: PASS, Content-Type: text/event-stream; charset=utf-8, Cache-Control: no-cache, no-transform, Connection: keep-alive
  • overlapping request: PASS, returned 429 PRIME_AGENT_SESSION_BUSY while the original stream completed cleanly

Failing condition:

  • Restart/session lifecycle check failed.
  • After restarting the Prime tmux session, discovery showed two live sessions instead of exactly one:
    • old session 019fd788-d857-7479-9924-792740b9bdf9
    • new session 019fd78b-4b39-72f5-9366-a08208ff087d
  • I then killed the old test-owned PIDs, but another live descriptor for the older session respawned with a new bridge, so prime-agent status still showed two sessions.
  • The DKG route still worked after restart because it selected a working session and returned PR2113_RESTART_OK, but the checklist requirement “restarting removes stale descriptor and discovers the new session” is not satisfied.

Token/log checks:

  • No DKG auth token was printed in the report.
  • Exact token search in daemon log: 0 matches.
  • Exact token search in Prime settings/session descriptors: 0 matches.
  • Sanitized daemon logs around restart mostly contained normal sync/backpressure warnings; no clean Prime route error tied to the restart failure.

Not run because the restart check failed and the smoke instruction said to stop on failure:

  • disconnect/reconnect reversibility
  • browser-level Playwright UI smoke; route-level Node UI API smoke passed, but Playwright was not installed in the fresh checkout

Verdict: FAIL

The core SSE/chat path looks good after cleanup, including final-frame close, headers, no second-turn leakage, and 429 busy behavior. The remaining blocker is session lifecycle: restart can leave multiple live descriptors for the same configured adapter/session family. The adapter/session registry likely needs stronger stale-session cleanup or an explicit single-active-session election story before this should be called a pass.

…ncation, timeout propagation

Resolves the live-smoke restart blocker and the round-2 review findings.

Session election (smoke blocker): Prime Agent sessions are daemon-managed
and survive restarts — a resumed session is a legitimate live session, so
convergence cannot mean deletion. Descriptors now carry lastActiveAt,
stamped atomically on every agent_start; unaddressed chat routes to the
most recently ACTIVE session (a resumed session's newer startedAt no
longer steals routing). The extension prunes dead-pid sibling descriptors
at session start, age-gated (30s mtime) so a respawned session's freshly
republished descriptor is never deleted by the read-then-remove race; the
same guard applies daemon-side. The pre-send gate now verifies the
/health sessionId echo, closing the recycled-port delivery hole. Status,
UI copy, and the smoke checklist present multiple live sessions honestly.

SSE terminal-frame truncation now scans in byte space: frame separators
are ASCII, so complete frames are decoded whole and forwarded bytes are
always the original chunk bytes — the decode/slice/re-encode round trip
that duplicated lead bytes of a UTF-8 codepoint straddling the chunk
before a coalesced final frame is gone. Post-final bytes are still
dropped; the 1MB fail-open scan limit applies to the byte tail.

Timeout authority: the bridge's activity-based idle timeout (which
preserves partial output) is authoritative; the daemon's fetch abort is
now a 60-minute hard backstop, so bridge 504s actually reach clients and
long active streams are no longer killed at 15 minutes. /send propagates
504 with text and timedOut; LocalAgentApiError carries both to the UI.

Also: a failed session_start closes the bound listener instead of
leaking it; adapter tests chain-build the extension so a clean checkout
can run them; the adapter suite is wired into CI (kosava-supporting
filter + root vitest projects); PRIME_AGENT_SESSION_BUSY renders friendly
copy; stale newest-session comments and descriptor-shape docs corrected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@branarakic

Copy link
Copy Markdown
Contributor

All open items are addressed in 075e87d88 — the restart blocker from the smoke test, plus the three items from the follow-up review (UTF-8 SSE corruption, 504 flattening, test/build decoupling).

The restart blocker: diagnosis and fix

What the smoke observed is Prime Agent working as designed: sessions are daemon-managed and survive terminal restarts — the restart resumed the old session alongside the new one, and killing worker pids just made the Prime daemon respawn the session and the bridge republish its descriptor. Two live descriptors were the truth, so convergence cannot mean deletion. The failure mode that actually matters is routing: a resumed session's descriptor is rewritten at resume time, so startedAt ordering would route new chat into the resurrected old conversation.

The fix is an explicit most-recently-active election:

  • Descriptors now carry lastActiveAt, re-published atomically by the bridge on every agent_start (local and bridge-injected turns alike). Unaddressed chat routes to the session most recently used, not most recently started. Explicit sessionId still wins, and a miss is still a 409, never a fallback.
  • The extension prunes dead-pid sibling descriptors at session_start, age-gated by a 30s mtime threshold so the read-then-remove race can never delete a respawned session's freshly republished descriptor (the same guard now applies to the daemon-side reader). A wrongly lost descriptor self-heals on that session's next turn.
  • The pre-send gate now verifies the /health sessionId echo, so a stale descriptor pointing at a recycled port answering ok:true gets a 503 instead of receiving someone else's chat — the probe path already had this check; the send path now matches.
  • Status, the panel copy, and the connect notice present multiple live sessions honestly ("N live sessions — the most recently active one is used") instead of implying exactly one.
  • IMPLEMENTATION-PLAN.md's restart smoke criterion is redefined to match reality: PASS = dead-pid descriptors pruned + chat routes to the most recently active session — explicitly not "exactly one live session after restart", because with a daemon that resumes sessions that expectation tests Prime Agent, not the adapter. RISKS.md gained the session-resurrection entry.

For the re-run: after restart you should expect to see the resumed session listed if Prime's daemon revived it — the assertion to make is that a fresh chat turn lands in the session you're typing in, and that dkg prime-agent status orders it first once you've used it.

Follow-up review items

  • UTF-8 SSE corruption: the terminal-frame truncation now scans in byte space (separators are ASCII, complete frames can't end mid-codepoint), and forwarded bytes are always the original chunk bytes — the decode→slice→re-encode round trip is gone. The straddled-emoji repro and a split-separator case are in daemon-sse-final-frame.test.ts (8 tests). An independent review agent fuzzed the scanner against a byte-space oracle across chunk-split offsets and could not refute it.
  • Timeout semantics: the bridge's activity-based idle timeout is now authoritative and the daemon's fetch abort is a 60-minute hard backstop (PRIME_AGENT_CHANNEL_HARD_TIMEOUT_MS) — previously the daemon's own 15-minute absolute abort fired first, making the bridge's partial-output-preserving 504 unreachable and killing actively-streaming turns longer than 15 minutes. /send propagates the bridge 504 (PRIME_AGENT_BRIDGE_RESPONSE_TIMEOUT, text, timedOut), and LocalAgentApiError carries text/timedOut so the partial answer reaches UI callers.
  • Clean-checkout tests: adapter test scripts chain-build the extension first, so pnpm --filter …adapter-prime-agent test works without a prior full build.
  • Found during review: no CI lane was running the adapter suite at allci-delta routed the package to kosava-supporting, but that job's filter list and the root vitest.config.ts projects both omitted it. Both are wired now.
  • Smaller: a failed session_start closes the bound listener instead of leaking it until process exit; PRIME_AGENT_SESSION_BUSY renders friendly copy instead of raw bridge JSON; stale "newest live session" comments and the DESIGN.md descriptor shape (portbridgeUrl, lastActiveAt added) corrected.

Validation at 075e87d88

  • full workspace build: pass
  • adapter suite: 46/46 (includes the prune-race pair, the listener-leak test, election ordering, and the token end-to-end)
  • CLI focused: 43/43 across daemon-prime-agent (21, incl. mismatched-echo 503 and 504 propagation), daemon-openclaw.part-11 (11, shared-path guard), daemon-sse-final-frame (8), election (2), commands (1)
  • node-ui: 21/21 (ui-api-stream 15, panel 4, ui-sse-final-frame 2)
  • ci-delta routing tests: 22/22; the edited ci.yml parses and the kosava-supporting step now filters the adapter package
  • pnpm pack --dry-run: extension bundle present
  • The localhost_contracts.json build churn was reverted before commit.

Changes were additionally adversarially reviewed by independent agents (byte-level scanner fuzzing, lifecycle/concurrency interleavings, contract consistency across daemon/bridge/UI/docs); the two accepted residuals are documented in code: the microsecond stat→rm window on prune (self-heals on next turn) and the comment-coupled 15-minute idle constants in extension and daemon.

The live smoke re-run at 075e87d88 — restart per the updated criterion, then the disconnect/reconnect reversibility and Playwright steps that were skipped — is the remaining acceptance gate.

🤖 Generated with Claude Code

@otReviewAgent otReviewAgent left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Operational Notice: Review Agent could not complete this review.

Business logic reviewer failed: WARNING: failed to clean up stale arg0 temp dirs: Permission denied (os error 13)

@Zigoljube

Copy link
Copy Markdown
Contributor Author

Follow-up on updated PR head 075e87d881260a1aad2d86b53fa250d5d3f67d40: restart lifecycle now passes, but reversibility still fails.

Retest summary:

  • Fresh checkout: /Users/clawdnode/code/dkg-pr2113-prime-smoke-3
  • Local HEAD equals GitHub headRefOid: 075e87d881260a1aad2d86b53fa250d5d3f67d40
  • DKG daemon under test: 10.0.12, commit 075e87d8, monorepo, edge, chain base:8453
  • Prime Agent: 0.7.0
  • Build: PASS
  • Node UI build: PASS
  • Adapter tests: PASS, 46/46
  • Initial live discovery: PASS, exactly one session
  • First stream: PASS, PR2113_UPDATED_FIRST_OK
  • Second stream: PASS, PR2113_UPDATED_SECOND_OK, no first-response leakage
  • Busy overlap: PASS, 429 PRIME_AGENT_SESSION_BUSY
  • Restart lifecycle: PASS
    • after stopping old session: sessions: []
    • after restart: exactly one new session
    • post-restart route returned PR2113_UPDATED_RESTART_OK

Failing step: disconnect / reconnect reversibility

Observed:

  1. Before disconnect, settings.json.extensions had exactly one entry:
    /Users/clawdnode/code/dkg-pr2113-prime-smoke-3/packages/adapter-prime-agent/extension/dist/extension.js
  2. Ran:
    node packages/cli/dist/cli.js prime-agent disconnect --agent-dir ~/.prime/agent
  3. CLI printed:
    [prime-agent] disconnected
  4. But the extension entry was still present in settings.json.
  5. Ran:
    node packages/cli/dist/cli.js prime-agent reconnect --agent-dir ~/.prime/agent
  6. Reconnect appended a duplicate, leaving two identical extension entries.

Likely cause:

  • packages/adapter-prime-agent/src/setup.ts uses isManagedExtensionPath() for setup/restore filtering.
  • That function currently returns true only for paths containing dkg-adapter-prime-agent.
  • The actual local package path in this repo is packages/adapter-prime-agent/..., so the active adapter entry is not classified as managed.
  • Result: restorePrimeAgentProfile() sees no managed extension, returns noop/success, and disconnect reports success without removing the entry.

Proposed fix:

  1. Make managed-extension detection match both the package name and the source-tree path, for example:
function isManagedExtensionPath(value: string): boolean {
  return /(^|[/\\])(?:dkg-)?adapter-prime-agent([/\\]|$)/.test(value)
    || value.includes('@origintrail-official/dkg-adapter-prime-agent');
}
  1. Make setup idempotent for the exact extension path even if the managed predicate misses a historical variant:
const nextExtensions = [
  ...extensions.filter((entry) => entry !== extensionPath && !isManagedExtensionPath(entry)),
  extensionPath,
];
  1. Make restorePrimeAgentProfile() remove the exact state.extensionPath in addition to managed-looking paths:
const statePath = state.extensionPath;
const nextExtensions = extensions.filter(
  (entry) => entry !== statePath && !isManagedExtensionPath(entry),
);
  1. Treat “disconnect removed nothing while state.extensionPath is still present” as an error, not a successful noop. The CLI should not print [prime-agent] disconnected if the managed extension remains registered.

  2. Add regression tests:

  • disconnect removes an extension path shaped like .../packages/adapter-prime-agent/extension/dist/extension.js
  • disconnect removes the exact state.extensionPath even if the heuristic predicate changes
  • reconnect after disconnect leaves exactly one extension entry, not duplicates
  • setup is idempotent when the exact extension path is already present

Token/log note:

  • No DKG auth token was printed.
  • Exact token search in daemon log and Prime settings/session descriptors returned 0 matches.

Current verdict after this update: FAIL, but now only on reversibility/idempotency. The chat path and restart lifecycle passed.

@branarakic

Copy link
Copy Markdown
Contributor

Follow-up fix pushed in 4caa974d2 for the confirmed setup lifecycle failure.

Root cause confirmed locally: the managed-path predicate recognized dkg-adapter-prime-agent but not the actual source-tree segment packages/adapter-prime-agent. A second setup/UI Connect duplicated the path, disconnect reported success without removing it, and reconnect appended another duplicate.

Fix:

  • recognize both installed-package and source-tree adapter paths
  • always treat the exact persisted state.extensionPath as owned
  • de-duplicate the exact path during setup
  • make disconnect/uninstall fail instead of reporting success when restore fails
  • make verify reject duplicate exact registrations
  • add source-tree, exact-state-path, disconnect, reconnect, and repeat-setup regression coverage

Real Prime Agent 0.7.0 profile replay on this machine now passes:

  • repeat setup: 1 -> 1
  • disconnect: 1 -> 0
  • reconnect: 0 -> 1
  • total registered extensions after reconnect: 1

Validation:

  • adapter: 50/50
  • focused CLI Prime Agent/shared SSE: 43/43
  • Node UI focused: 21/21
  • ci-delta routing: 22/22
  • real Codex-backed DKG streams: first/second turn, UTF-8, terminal final-frame close, and overlapping-request 429 pass
  • browser Node UI: Prime Agent tab replied PR2113_UI_OK and returned to idle

The prior red GitHub jobs were also inspected individually. Each failure was an UNKNOWN STEP action-download failure: Failed to resolve action download info. Error: Service Unavailable; downstream jobs were cancelled by fail-fast. The new commit has triggered a clean run so those checks can be evaluated on actual code.

@otReviewAgent otReviewAgent left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Operational Notice: Review Agent could not complete this review.

Business logic reviewer failed: WARNING: failed to clean up stale arg0 temp dirs: Permission denied (os error 13)

@branarakic
branarakic changed the base branch from main to testnet-canary August 6, 2026 18:16
@branarakic
branarakic merged commit aa3b8ff into testnet-canary Aug 6, 2026
6 of 26 checks passed
@branarakic

Copy link
Copy Markdown
Contributor

ca8bcb347 adds CI-runnable integration coverage for the whole adapter chain, answering "what verifies this integration in CI" with something stronger than the existing unit/contract suites: a fake Prime Agent extension host (test-fixtures/fake-prime-agent-host.mjs) — a dependency-free child process that imports the built extension bundle exactly as the real host does and is scripted over a JSON-lines control channel. Real process boundary, real pids, real loopback HTTP, real descriptor files; only the LLM is scripted.

Two suites ride existing CI lanes:

  • Adapter host-integration.test.ts (5 tests, kosava-supporting) — file-based token chain with zero env injection (auth.token → setup dkg.json → bridge auth over HTTP), SSE across the process boundary, shutdown/rebind lifecycle, SIGKILL crash convergence through the age-gated prune, and cross-process election stamping.
  • CLI daemon-prime-agent-host-integration.test.ts (6 tests, bura-cli) — the real daemon route handlers against the real bridge: send (deliverAs asserted on the wire), stream ending on final, busy 429 with an order-proven no-stray-injection check, election following activity across two host processes, no-silent-reroute after SIGKILL (409, zero deliveries to the surviving session), and health up/idle transitions. These two pin exactly the properties the live smoke exercises.

The adversarial review pass caught one thing worth calling out: the CI build-outputs tarball only collected depth-2 dist dirs, so extension/dist would never have reached the bura-cli shards and the CLI suite would have failed deterministically there — the bundle is now packed explicitly in ci.yml. Also: adapter testTimeout raised to 20s so the fixture's diagnostics fire before vitest kills a hung test, and bundle-freshness limits are documented in the fixture README.

Determinism: each suite ran 3× consecutively green (adapter 55/55 including the new reversibility tests from 4caa974d2; CLI file 6/6, sub-second runtime); zombie-process and tmp-leak checks clean.

A Playwright chat-through-the-panel spec was assessed and deliberately not added: the e2e devnet daemon's environment is fixed at exec and reused across runs, so no spec can point it at a spec-owned agent dir without shared-harness changes. A design sketch for that harness change is in the assessment if we want it later.

🤖 Generated with Claude Code

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.

3 participants