Skip to content

feat(modules/airc): AttachRequest carries channel — owner-core model headless fix#1505

Merged
joelteply merged 1 commit into
canaryfrom
feat/headless-airc-attach-channel
May 31, 2026
Merged

feat(modules/airc): AttachRequest carries channel — owner-core model headless fix#1505
joelteply merged 1 commit into
canaryfrom
feat/headless-airc-attach-channel

Conversation

@joelteply
Copy link
Copy Markdown
Contributor

Summary

  • AIRC inbound attach now passes a channel in AttachRequest, satisfying airc's owner-core router model (airc-daemon/src/server.rs:274). Previously continuum sent AttachRequest::default(), which the daemon rejects with attach requires a channel in the owner-core model.
  • discover_default_channel() parses airc room for the scope's current room channel UUID and plumbs it through to AircModule::initialize alongside the socket discovered in feat(modules/airc): headless socket discovery via airc ipc-endpoint + auto-install #1504.
  • Honors $AIRC_DEFAULT_CHANNEL env override (UUID) for tests or operators pinning a channel explicitly.
  • Degraded mode: if the scope has no current room (fresh install before airc room <name>), the rest of continuum-core boots — only the inbound attach is skipped, with a loud warning that quotes the remedy.

Why

Next concrete break revealed by the headless moment-of-truth test (#1504 was the first break: socket discovery). With socket discovery fixed, the next-layer failure surfaced:

AIRC daemon attach stream stopped: failed to attach to airc daemon:
  attach requires a channel in the owner-core model

Per airc's owner-core router model: ONE machine account → ONE daemon → events routed per-channel (no global fan-out table). AttachRequest.channel is mandatory — clients attach once per room they care about (or N attaches for N rooms). The old AttachRequest::default() worked under an earlier model; the substrate moved + continuum didn't catch up.

Joel direction: "let's do it" (after #1504 landed).

What this PR touches

File Change
src/workers/continuum-core/src/airc/discovery.rs Add discover_default_channel() + parser for airc room stdout + 4 new unit tests (typical, alt-cap, no-channel, non-uuid) + 2 new DiscoveryError variants
src/workers/continuum-core/src/airc/mod.rs Re-export discover_default_channel
src/workers/continuum-core/src/airc/inbound_attach.rs spawn_daemon_attach + run_daemon_attach now take a channel: RoomId parameter; populate AttachRequest.channel = Some(channel)
src/workers/continuum-core/src/modules/airc.rs New attach_channel: Option<RoomId> field on AircModule; discover_and_construct runs both discoveries; initialize requires both before spawning the attach

Resolution order for the channel

  1. $AIRC_DEFAULT_CHANNEL — explicit UUID override (tests, multi-room scopes pinning the first attach).
  2. airc room stdout parse — find the channel: <uuid> line and parse the UUID. Robust to whitespace + alt-capitalization (Channel:, CHANNEL:).
  3. Otherwise Err(DiscoveryError::RoomCommandFailed | UnparseableChannel) — caller decides whether to skip attach or fail (AircModule::discover_and_construct skips attach with a loud warning quoting the operator remedy).

Test plan

  • Build succeeds: cargo build --release --bin continuum-core-server --features metal,accelerate
  • Unit tests pass: cargo test --release --lib --features metal,accelerate airc::discovery — 7 total (3 from feat(modules/airc): headless socket discovery via airc ipc-endpoint + auto-install #1504 + 4 new)
  • Standalone boot: AIRC attach succeeds + log shows Discovered airc default channel via 'airc room' + no attach requires a channel warning
  • Env override: AIRC_DEFAULT_CHANNEL=<uuid> skips the room parse
  • No-room degraded mode: scope without airc room <name> boots cleanly with loud warning quoting the remedy
  • Adversarial reviewer agent verdict (per AGENTS.md §0 doctrine landed in airc#1094)

Follow-ups

  • Multi-room attach. Today continuum attaches to the scope's single default channel. When continuum rooms become first-class (per ALPHA-GAP §0A), spawn one spawn_daemon_attach task per known channel.
  • Robustness of the airc room parser. When airc adds airc room --print-channel (mirroring the airc ipc-endpoint decoupling pattern), switch to that flag for a stable contract. Filed as airc-side follow-up.

References

  • airc#1095 (sibling PR for feat(modules/airc): headless socket discovery via airc ipc-endpoint + auto-install #1504, awaiting Windows CI) — airc ipc-endpoint command. This PR uses the same shell-out-and-parse pattern.
  • airc owner-core model: airc-daemon/src/server.rs:274, airc-ipc/src/request.rs:144 (AttachRequest docstring), airc-lib/tests/common/mod.rs (model description).
  • continuum#1504 — sibling PR that fixed the first concrete break (socket discovery). This PR fixes the second concrete break (attach channel).
  • Memories: headless-rust-must-work-soon, continuum-thesis-airc-is-the-medium, every-error-is-an-opportunity-to-battle-harden, agent-review-as-acceptable-approval (adversarial reviewer pattern this PR uses for sign-off)
  • ALPHA-GAP §0A line 706 — headless target.

Generated with Claude Code

…headless fix

Iterating on the moment-of-truth test. With #1504 (socket discovery)
landed, the next concrete break surfaced:

  AIRC daemon attach stream stopped: failed to attach to airc daemon:
  attach requires a channel in the owner-core model

Per `airc-daemon/src/server.rs:274` + `airc-ipc/src/request.rs:144`
docstring: the owner-core router subscribes PER CHANNEL — no global
fan-out table. AttachRequest.channel is mandatory; clients attach
once per room they care about. Continuum was sending
`AttachRequest::default()` (no channel), which worked under an
earlier model the substrate has since left behind.

### What ships

- `discover_default_channel()` — parses `airc room` stdout for the
  scope's current room `channel: <uuid>` line + returns the UUID.
  Honors `$AIRC_DEFAULT_CHANNEL` env override (UUID) for tests +
  multi-room operators pinning the first attach. Robust to
  whitespace + alt-capitalization (`Channel:`, `CHANNEL:`); fails
  loud (UnparseableChannel error) if airc renames the field.

- `AircModule::attach_channel: Option<RoomId>` new field, populated
  by `discover_and_construct` alongside the socket path. `initialize`
  spawns the daemon attach only when BOTH a socket AND a channel
  are available — partial degradation rather than boot failure.

- `inbound_attach::spawn_daemon_attach` + `run_daemon_attach` take a
  `channel: RoomId` and put it in `AttachRequest.channel = Some(_)`.
  Single caller updated; no other code paths.

- 4 new unit tests for the parser (typical airc room output, alt
  capitalization + whitespace, missing channel line, non-UUID after
  label) — 7 discovery tests total.

### Verification (manual end-to-end on this branch)

  $ rm -f /tmp/hctest.sock && \
    target/release/continuum-core-server /tmp/hctest.sock > boot.log 2>&1 &
  $ grep -E "Discovered airc" boot.log
  Discovered airc daemon socket via `airc ipc-endpoint`
    socket_path="/Users/joel/.airc/runtime/airc-machine-…-v5.sock"
  Discovered airc default channel via `airc room`
    channel=11c1a7ac-cb85-5ca0-a5b4-2847280ea3fa

  # No more "attach requires a channel in the owner-core model" warning.

  $ cargo test --release --lib --features metal,accelerate airc::discovery
  test result: ok. 7 passed; 0 failed.

### Next concrete break revealed (follow-up #82, not in this PR)

The attach now connects + passes the channel gate. Next-layer error:
  `AIRC daemon attach stream stopped: failed to read airc daemon
   event: Semantic(None, "missing field 'event'")`
CBOR Response variant shape changed between continuum's pinned
airc-ipc SHA (428f9281…) and the live daemon. Likely fix: SHA bump
in src/workers/Cargo.toml after the AttachRequest channel change
lands on airc canary. Tracked separately so this PR can ship the
single, complete fix for break #2.

### Pattern

Iterate-on-moment-of-truth: each fix uncovers the next layer; each
PR is one well-scoped substrate change with end-to-end verification
+ a tracked follow-up for the next surfaced break. Three breaks
revealed so far (1504, this PR, #82); breaks 1 + 2 fixed.

### Follow-ups (filed)

- airc-side: `airc room --print-channel` flag (mirror the `airc
  ipc-endpoint` pattern) so continuum's stdout parser can be
  replaced with a stable contract. Note in the parser docstring.
- continuum #82: CBOR Response shape mismatch / SHA bump.
- continuum: multi-room attach (one daemon_attach task per channel
  when continuum rooms become first-class — currently single-room).

### References

- airc owner-core model: `airc-daemon/src/server.rs:274`,
  `airc-ipc/src/request.rs:144` (AttachRequest docstring),
  `airc-lib/tests/common/mod.rs` (model description).
- continuum#1504 — sibling PR (socket discovery) — this PR's
  prerequisite, already landed on canary.
- airc#1095 — sibling PR (`airc ipc-endpoint`), pending Windows CI.
- Memories: `headless-rust-must-work-soon`, `continuum-thesis-airc-
  is-the-medium`, `every-error-is-an-opportunity-to-battle-harden`,
  `agent-review-as-acceptable-approval`.
- ALPHA-GAP §0A line 706 — headless target.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@joelteply
Copy link
Copy Markdown
Contributor Author

Adversarial reviewer verdict: APPROVE (per AGENTS.md §0 / airc#1094 agent-sign-off doctrine, applied across repos via continuum's agent-review-as-acceptable-approval memory)

Spawned as a fresh subagent with no prior context, prompted to default-to-BLOCK. Reviewer ID: a70cb73a8a569f63f.

Reasons

  1. Parser tested against live airc room — output exactly matches assumed format (channel: <uuid> on its own line). All 7 unit tests pass (3 from feat(modules/airc): headless socket discovery via airc ipc-endpoint + auto-install #1504 + 4 new).

  2. False-positive check. strip_prefix("channel:") requires the trimmed line to start with the label. A line like # old channel: xyz would not match (starts with #). A wire-channel: line wouldn't match either. Even if a value somehow matched the label, the parse::<uuid::Uuid>() gate rejects non-UUIDs. Robust.

  3. Caller audit. grep confirms only one production caller of spawn_daemon_attach (the module's initialize); run_daemon_attach only called internally. The breaking signature change is contained.

  4. Daemon behavior for unknown channel. Read airc-daemon/src/server.rs:274: daemon just builds Filter::channel(channel) and subscribes. No error if the scope hasn't joined that channel; it just sees no events. Graceful — no silent corruption, no panic.

  5. Battle-hardening doctrine (per every-error-is-an-opportunity-to-battle-harden memory). Parser fails loud (UnparseableChannel variant), error message names the field, warning at construction quotes the operator remedy (run airc room <name> or set AIRC_DEFAULT_CHANNEL).

  6. CI. mergeable: MERGEABLE. No failures.

Strongest counterargument considered

The (Some(_), None) | (None, Some(_)) | (None, None) arm in initialize is partially dead ((None, Some(_)) is unreachable since discover_and_construct short-circuits to with_queue_client on socket failure, which sets both to None). Defensive-but-redundant code, not a bug — the exhaustive match guards against future refactors of discover_and_construct that might separate the two failure modes. Acceptable; tightening would trade safety for line-count savings.

Minor nit (non-blocking)

The 5 legacy/test constructors (with_daemon_home, with_queue_client, etc.) now silently disable attach. No existing test exercises attach through them (grep confirms), so no regression — but worth a follow-up if attach-via-test-constructor is ever wanted.

BLOCK threshold not met. Merge on green CI.

@joelteply joelteply merged commit de1c10d into canary May 31, 2026
3 checks passed
@joelteply joelteply deleted the feat/headless-airc-attach-channel branch May 31, 2026 04:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant