Skip to content

fix(relay): c2c restart relay-connect no longer raises uncaught Not_found (B212)#8

Open
XertroV wants to merge 115 commits into
masterfrom
fix/b212-restart-relay-connect
Open

fix(relay): c2c restart relay-connect no longer raises uncaught Not_found (B212)#8
XertroV wants to merge 115 commits into
masterfrom
fix/b212-restart-relay-connect

Conversation

@XertroV

@XertroV XertroV commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

c2c restart relay-connect raised an uncaught Not_found (exit 125) and left the connector unchanged, instead of restarting it or printing a clear error. Because whoami/doctor advertise c2c restart relay-connect as the recovery command for a wedged/registered_unreachable connector, the advertised recovery path was broken.

Fixes #6. Addresses backlog B212.

Root cause

c2c restart <name> resolves the instance via C2c_start.load_config_opt, whose gs/gf/gl helpers raise Not_found on any missing key. The machine-wide relay connector persists only a minimal supervisor config (client/scope/supervised/relay_url/interval) — it has none of the harness-client fields (name, session_id, resume_session_id, alias, auto_join_rooms). So load_config_opt raised Not_found, uncaught, from cmd_restart (c2c_start.ml:5836) via restart_cmd (c2c_managed_cmd.ml).

Reproduced on current master (binary built from HEAD):

c2c: internal error, uncaught exception: Not_found
  Raised at C2c_start.load_config_opt.gs ... line 2655
  Called from C2c_start.cmd_restart ... line 5836
  Called from Dune__exe__C2c_managed_cmd.restart_cmd ... line 816
EXIT=125

Fix

  • c2c_managed_cmd.restart_cmd detects the relay-connect connector by its config client field and restarts it through its own machine-supervisor lifecycle instead of the harness path: stop the current supervisor (SIGTERM outer.pid, wait up to timeout, SIGKILL) so the machine singleton lock is released, then start a fresh supervisor reusing the persisted relay_url + interval. If no relay URL is known, it prints a clear, actionable error (exit 1) rather than crashing.
  • C2c_relay_managed.restart / restart_params_of_config implement the restart and parameter resolution (config relay_url, else $C2C_RELAY_URL; both trimmed, empty treated as unset).
  • load_config_opt is now total: a config.json lacking required harness fields returns None (matching its .mli contract) rather than escaping Not_found. This closes the whole uncaught-exception class on the restart path.

With the primary command working, the whoami/doctor remediation (c2c restart relay-connect ...) now names a real, working managed-lifecycle command.

Tests

  • test_c2c_relay_managed: restart_params_of_config resolves the URL from config, falls back to $C2C_RELAY_URL, and errors clearly when neither is set.
  • test_c2c_start: load_config_opt returns None (no Not_found) on a minimal relay-connect connector config.

Verification

  • just check — PASS (full-tree dune build incl. server + tools, catalog + doc-command gates).
  • test_c2c_relay_managed — 9/9 OK. test_c2c_start — 198/198 OK.
  • Fixed binary, sandboxed HOME:
    • connector config with no usable URL → clean actionable error, exit 1, no Not_found, nothing spawned.
    • connector config with a URL, no prior supervisor → starts a fresh supervisor, exit 0.

Builds/tests limited to -j 2 per repo system constraint.

Notes / follow-ups

  • The whoami/doctor remediation fallback (pkill -f 'c2c relay connect'; c2c relay connect &) spawns an unmanaged connector, which can contribute to B210-style multi-connect churn. Now that the primary managed c2c restart relay-connect works, that fallback should rarely trigger; tightening it is a possible follow-up but is out of scope here.
  • Touches connector lifecycle code adjacent to B210/B211/B213 (c2c_relay_managed.ml, connector restart); no changes to the connector sync/watchdog logic itself.

XertroV added 30 commits July 15, 2026 13:02
… it? grok likes to call it. not sure if it's a real skill or how it's injected into the client.
… monitor a given session (the jsonl file) and filter out all the noise (tool calls, thinking, etc) and forward user input and agent plaintext chat output to anoter c2c address. The idea is to allow monitoring or observation, esp to remote sessions like on a colleagues PC.
…& c2c blah' at the same time and get different aliases. This should not be possible. We should consider the starting location's git root + session id; anything else we need to be careful with.
…ll monitor a given session (the jsonl file) and filter out all the noise (tool calls, thinking, etc) and forward user input and agent plaintext chat output to anoter c2c address. The idea is to allow monitoring or observation, esp to remote sessions like on a colleagues PC.
…ed it? grok likes to call it. not sure if it's a real skill or how it's injected into the client.
…cross repos

B188 made auto-register surfaces reuse a session_id's sticky alias from
other per-repo brokers, but the scan->register sequence was not atomic
across brokers: two concurrent registrations of one session under two
different git roots both scanned empty and minted distinct aliases.

Add a machine-global advisory lock keyed by session id
(~/.c2c/locks/session-reg-<sha256(sid)[0:16]>.lock, C2C_STATE_HOME chain,
lockf + O_CLOEXEC, best-effort) held across [cross-broker scan -> alias
choice -> Broker.register] on every registration surface:

- c2c_mcp_helpers_post_broker: acquire/release/with_session_registration_lock
  + locked_sticky_auto_register (resolve -> key-migrate -> register under
  the lock); auto_register_impl body now runs under the lock.
- hooks (codex/claude/grok/agy) + c2c send auto-register: switch to
  locked_sticky_auto_register (also DRYs the duplicated key migration).
- c2c init / c2c register: imperative acquire before the sticky scan,
  release after register (error-path exits release via the kernel).

Semantics (documented): reuse, not refuse — a session working in several
repos registers in each repo's broker under the same alias; live-occupied
alias in the target broker still falls back to minting (hijack guard);
explicit --alias, B135 sticky refusal, B140 rename, worktrees and
--cross-repo behavior unchanged.

Tests: ocaml/test/test_b191_session_reg_lock.ml — lock path determinism,
fork-based mutual exclusion, and the concurrent cross-repo race regression
via both locked_sticky_auto_register and auto_register_impl.

Docs: docs/architecture.md, docs/commands.md, CLAUDE.md; design note at
.collab/findings/2026-07-15T03-24-47Z-B191-alias-git-root-session-id.md
…c2c peer

New Tier-1 subcommand: c2c forward-agent-log --file <session.jsonl> TO
follows a Claude Code session transcript (poll-based tail, like deliver
watch) and forwards ONLY the human-visible conversation to a c2c address
via the normal broker send path (C2c_watch_data.send_dm; alias@host works):

- user input forwarded as '[user] ...', assistant plaintext as '[agent] ...'
- noise dropped: tool_use/tool_result, thinking, system/meta/summary/mode/
  attachment lines, isMeta + isSidechain lines, system-reminder and
  local-command-output injections, c2c-envelope-delivered mail, interrupt
  markers; noise text blocks are filtered per-block so mixed user turns
  keep the typed text
- only newline-complete lines are consumed (a live mid-write transcript
  never yields garbage); malformed lines are skipped; truncation/rotation
  resets the tail
- flood control: starts at EOF by default (--from-start replays history);
  --max-bytes (default 2000) truncates UTF-8-safely with a note; --once
  drains the current file and exits; --dry-run prints instead of sending
- format registry (--format claude) so other clients' transcript formats
  can be added later

Core logic lives in C2c_forward_agent_log (no CLI-helper deps) with the
Cmdliner wiring in C2c_forward_agent_log_cmd. Tier1 (same safety class as
monitor + send: read-only local file tail + ordinary bus sends; B098-inert
— forwarded content is DATA, no approval/RPC semantics).

Tests (22, alcotest): pure classify/tail/format units, committed fixture
transcript replay (ocaml/cli/test_fixtures/forward_agent_log_claude.jsonl,
incl. trailing partial line), real temp-broker delivery via read_inbox,
and binary --once runs gated by C2C_SEND_MESSAGE_FIXTURE=1. Also
live-fire tested against a growing temp transcript + temp broker.

Docs: docs/commands.md Tier-1 table row + changelog entry (embedded blob
regenerated via just codegen-changelog). just check passes.
…, e2e evidence

Covers: scan->register race left open by B188; global per-session
registration lock design; reuse-not-refuse multi-repo semantics; review
notes (EINTR degrade, exit_floored lock-hold, MCP register-tool
non-minting gap); in-the-wild sequential + concurrent evidence with the
built binary.
… c2c blah' at the same time and get different aliases. This should not be possible. We should consider the starting location's git root + session id; anything else we need to be careful with.
…d it? grok likes to call it. not sure if it's a real skill or how it's injected into the client.
…l monitor a given session (the jsonl file) and filter out all the noise (tool calls, thinking, etc) and forward user input and agent plaintext chat output to anoter c2c address. The idea is to allow monitoring or observation, esp to remote sessions like on a colleagues PC.
…ncode, kimi, grok, agy), not just claude — per-format classifiers + auto-detect + opencode dir source
…pencode, kimi, grok, agy), not just claude — per-format classifiers + auto-detect + opencode dir source
… opencode)

- classifiers for codex event_msg rollouts, kimi context.jsonl roles,
  grok chat_history.jsonl (<user_query> extraction, synthetic turns),
  agy/gemini chats jsonl (message + $set journal lines)
- opencode has no single transcript: new polled directory source over
  storage/message/<sessionID> joined with storage/part/<msgID> text parts;
  user messages forward on appearance, assistant once time.completed
- --format defaults to auto: path heuristics over the standard session
  stores, falling back to a first-lines content sniff
- shared user-noise prefix filter extended (<user_instructions>,
  <environment_context>, <permissions instructions>, <session_context>)
- docs: commands.md row, man page, changelog (B193 entry extended)
- tests: per-client classify, detect-format, opencode source semantics,
  binary e2e for auto-sniff + opencode dir + dir/format mismatch
- validated against live sessions of all six clients (dry-run)
…encode, kimi, grok, agy), not just claude — per-format classifiers + auto-detect + opencode dir source
… size of incoming msg (default), per-sender max-size, default per-sender rate limit, individual per-sender rate limit, per-machine incoming rate limit, etc. All of this I think can be managed locally so that we don't need to send this stuff to the relayer.
…/machine/agent have? Particularly for untrusted incoming sources, but maybe also safeguarsd for local sources
…paction-aware replay

- follow mode warns (stderr, non-fatal) when it detects an agent session
  (is_agent_session) running it in the foreground: streaming blocks the
  turn; re-run backgrounded or use --once
- --since/--until TIME bound the replay to a range (ISO-8601 UTC or epoch
  seconds; imply --from-start). Supported where events carry timestamps:
  claude/codex/agy line 'timestamp', opencode time.created; kimi and grok
  transcripts carry none and get a clear error
- replays (--from-start/--once) now start at the transcript's most recent
  compaction boundary by default (claude compact_boundary, codex
  compacted/context_compacted); --full-history opts out. Live claude test:
  4 messages forwarded vs 3903 with --full-history
- fix: claude isCompactSummary user lines (the post-compaction context
  blob) no longer leak to the observer as [user] messages
- new: parse_time_spec (UTC civil-date math, no new deps),
  replay_start_offset, line_time/compaction format registries
- tests: 44 total (time parsing, range filters, markers, offset replay,
  opencode range, binary e2e for warning/slice/trim); docs + changelog
…hould be autorestarted when c2c updates, and connecting to the relay should only be required once (each agent still needs to register their alias if they want to recieve messages via <alias>@<relay-hostname>). Undecided whether registration should be required for delivery via 'alias@machineid'
XertroV added 30 commits July 15, 2026 16:24
…should be autorestarted when c2c updates, and connecting to the relay should only be required once (each agent still needs to register their alias if they want to recieve messages via <alias>@<relay-hostname>). Undecided whether registration should be required for delivery via 'alias@machineid'
…egistrations, triggering HTTP 429 rate limit
…y binding' on Grok (and possibly CLI-first clients)
Search previously matched only the 120-char eventLabel preview, so
tokens deep in a message body never hit. Add eventSearch helpers,
clear/Esc, and match counts; unit tests cover deep matches.

User-facing "swarm" chrome (window title, wizard, header) → agent/
c2c wording. Default room id swarm-lounge unchanged for compatibility.
Default alias was "human", so skip never showed observer mode.
Use empty default, banner + "join as peer" to re-open WelcomeWizard,
and stop double-joining the default room on complete.
Optimistic feed events used live form state after send, so retries
could attach the wrong target/body. Use attempt fields; classify
CLI stdout as delivered|queued|unknown; toast and longer confirm
labels (sent ✓ / queued ⏳). Prefer `rooms send` for room messages.
Grok CLI-first monitor looped on signature_invalid because it peeked the
cli-<alias>/cli-<alias> key while the machine relay connector owned the
alias's single live lease under (host_hash, real_session_id). The relay
keys leases by (node_id, session_id) but enforces one row per alias
(ON CONFLICT(alias) DO UPDATE), so the connector's register/heartbeat
moves the lease off the CLI convention; peeking cli-<alias> then fails
the relay owner check.

The connector now persists alias -> session_id in connector-state.json
(sessions map). New pure connector_peek_key returns the connector's
recorded (node_id, session_id) for a managed alias, falling back to the
locally-resolved session id only for older state files. The monitor uses
it, so it peeks the exact binding register/connect established regardless
of local session-id resolution, and connector heartbeats keep it fresh.

Regression tests: ocaml/test/test_c2c_relay_connector.ml (B209 group).
Client-only; no relay-server change, no deploy needed.
Skip when focus is already in an input/textarea; Esc clears and blurs.
…ound (B212)

`c2c restart relay-connect` fell through to the harness-client restart path
(cmd_restart -> load_config_opt), whose `gs` helpers raise Not_found on the
machine connector's minimal supervisor config.json (client/scope/supervised/
relay_url/interval — no name/session_id/alias/auto_join_rooms). The exception
was uncaught, crashing the command (exit 125) and leaving the connector
unchanged, so whoami's remediation pointed at a broken command.

Fixes:
- c2c_managed_cmd.restart_cmd detects the relay-connect connector by its
  config `client` field and restarts it via the machine-supervisor lifecycle:
  stop the current supervisor (SIGTERM outer.pid, wait, SIGKILL on timeout) so
  the machine singleton lock is released, then start a fresh supervisor reusing
  the persisted relay_url + interval. When no relay URL is known it prints a
  clear, actionable error (exit 1) instead of crashing.
- C2c_relay_managed.restart / restart_params_of_config implement the connector
  restart + parameter resolution (config relay_url, else $C2C_RELAY_URL, both
  trimmed; empty treated as unset).
- load_config_opt is now total: a config.json lacking required harness-client
  fields returns None (matching its .mli contract) rather than escaping
  Not_found — closing the whole uncaught-exception class on the restart path.

whoami/doctor remediation (`c2c restart relay-connect ...`) now names a working
managed-lifecycle command.

Tests:
- test_c2c_relay_managed: restart_params_of_config resolves url from config,
  falls back to $C2C_RELAY_URL, and errors clearly when neither is set.
- test_c2c_start: load_config_opt returns None (no Not_found) on a minimal
  relay-connect connector config.

Fixes #6
Addresses backlog B212.
… (B212)

Follow-up hardening on the B212 restart fix. `C2c_relay_managed`'s
supervisor-lifecycle paths resolved the instances dir from $HOME and ignored
$C2C_INSTANCES_DIR, while `restart_cmd` resolved config.json via
`C2c_start.instance_dir` (which honors $C2C_INSTANCES_DIR). So `c2c restart
relay-connect` with $C2C_INSTANCES_DIR set read config from the override dir
but SIGKILLed the outer.pid under $HOME — i.e. it would kill the real machine
supervisor. This PR shipped that hazard; close it.

- C2c_relay_managed.instances_dir now honors $C2C_INSTANCES_DIR with the same
  semantics as C2c_start.instances_dir (non-blank env wins, else the historical
  $HOME/.local/share/c2c/instances default). machine_state_dir (home of the
  singleton lock) is derived as its parent, so the lock tracks the same base:
  an override dir gets an isolated lock; unset stays $HOME/.local/share/c2c.
- Extracted connector_pid_path + stop_supervisor so restart's outer.pid read,
  the machine lock, and the fresh supervisor all resolve through one
  env-honoring instances_dir. Pure consistency: env-UNSET behavior is
  byte-for-byte unchanged (existing test_machine_lock_is_name_independent still
  passes). Singleton-lock model itself is unchanged (B210 is separate).

Tests (test_c2c_relay_managed):
- instances_dir + machine_lock_resource honor C2C_INSTANCES_DIR (override → temp
  base; connector pid path is NOT under the real $HOME default).
- unset/blank falls back to the $HOME default (production unchanged).
- restart's stop step signals ONLY the supervisor under $C2C_INSTANCES_DIR and
  leaves an outer.pid under the $HOME path untouched.

Verified end-to-end: `c2c restart relay-connect` with C2C_INSTANCES_DIR + HOME
sandboxed signals the override supervisor and leaves the HOME-path supervisor
alive. Builds/tests -j 2; just check PASS.

Addresses backlog B212 (review follow-up).
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.

c2c restart relay-connect fails with uncaught Not_found

1 participant