Skip to content

feat(modules/airc): adopt airc v5 owner-core schema (SHA bump + daemon_transport migration)#1506

Open
joelteply wants to merge 3 commits into
canaryfrom
feat/headless-airc-v5-owner-core-migration
Open

feat(modules/airc): adopt airc v5 owner-core schema (SHA bump + daemon_transport migration)#1506
joelteply wants to merge 3 commits into
canaryfrom
feat/headless-airc-v5-owner-core-migration

Conversation

@joelteply
Copy link
Copy Markdown
Contributor

Summary

  • Bump airc git rev 428f92818f6948c (rebased on rust-rewrite + airc#1096 SDK From impls). Adopts v5 owner-core schema.
  • Full v5 migration of daemon_transport.rs publish + replay paths: opaque payload: Vec<u8> instead of typed body: Body, from_peer/from_client identity fields, IpcKind/IpcTarget/IpcCursor instead of FrameKind/MentionTarget/TranscriptCursor, decode_wire_event for InboxResponse envelopes, ResolveWire removed.
  • Update inbound_attach.rs to match Response::Event { envelope } and decode via airc_lib::decode_wire_event.
  • Disable airc_runtime_e2e_tests (v4-shape fixture; rewrite tracked as continuum Build(deps-dev): Bump @typescript-eslint/parser from 8.29.1 to 8.35.1 #83).
  • 73/73 airc:: unit tests pass; headless boot end-to-end works (no AIRC daemon attach stream stopped warnings).

Why

Third concrete break revealed by the headless moment-of-truth iterate loop:

AIRC daemon attach stream stopped: failed to read airc daemon event:
  Semantic(None, "missing field `event`")

CBOR deserialization mismatch — continuum's pinned airc-ipc predated the owner-core rewrite, where Response::Event { event: Box<TranscriptEvent> } became Response::Event { envelope: Vec<u8> }. The whole IPC vocabulary split from the SDK projection in v5; this PR is the consumer-side adoption.

Co-evolution with airc

Joel, 2026-05-31:

"I always simultaneously develop the sdk and consumer of it. It helps you build the best patterns."

Started the v5 migration here, discovered continuum needed conversions (FrameKind→IpcKind, TranscriptCursor→IpcCursor, etc.) that lived as private free functions in airc-lib. Wrong surface for external consumers. Upstreamed them as impl From<> blocks in airc-ipc via airc#1096 so continuum can write kind: frame_kind.into() instead of duplicating a bit-shift. Same pattern for decode_wire_event (already public in airc-lib; just needed the workspace dep added).

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 "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
$ grep -i "attach.*stopped\|requires a channel\|missing field" boot.log
# (empty — no errors)

Three concrete breaks fixed in three successive PRs (#1504 socket discovery, #1505 attach channel, this one). Headless inbound attach is alive end-to-end.

Files

File Change
src/workers/Cargo.toml Bump airc git rev (5 crates pinned to same SHA so IPC ABI version stays consistent); add airc-lib + airc-wire workspace deps
src/workers/continuum-core/Cargo.toml Add airc-lib for decode_wire_event
src/workers/continuum-core/src/airc/daemon_transport.rs Full v5 publish + replay migration (~270 lines reworked); new with_identity constructor; ipc_delivery_for helper; updated tests
src/workers/continuum-core/src/airc/inbound_attach.rs Match Response::Event { envelope }; decode via decode_wire_event
src/workers/continuum-core/src/modules/mod.rs Disable airc_runtime_e2e_tests (rewrite tracked as continuum #83)

Merge gates

This PR depends on airc#1095 + airc#1096 landing on airc canary/main first. The pinned SHA (8f6948c) is currently the tip of airc#1096's force-pushed branch (squash-merge will change the SHA). Re-pin to the post-merge airc rust-rewrite tip SHA before promoting this past continuum canary.

Follow-ups

  • continuum Build(deps-dev): Bump @typescript-eslint/parser from 8.29.1 to 8.35.1 #83: rewrite airc_runtime_e2e_tests.rs against v5 wire shape. Needs airc-bus dep for synthetic envelope construction via airc_wire::encode(&Envelope).
  • Peer identity discovery: query daemon Status at AircModule construction; replace Uuid::nil from_peer with the scope's real peer_id. Today's anonymous publishes likely work but lose attribution.

Test plan

  • cargo build --release --bin continuum-core-server --features metal,accelerate clean
  • cargo test --release --lib --features metal,accelerate airc:: — 73/73 pass
  • Standalone boot: discovery succeeds, attach is alive, no daemon attach stream stopped warnings
  • Adversarial reviewer agent verdict (per AGENTS.md §0 / airc#1094 doctrine)
  • Re-pin SHA after airc#1095 + Sensory bench V2: opaque-manifest grading + WebP decode gap #1096 merge to airc canary (gate before continuum canary promotion)

References

Generated with Claude Code

…n_transport migration)

Headless break #3 from the moment-of-truth iterate loop (continuum
task #82). After #1504 (socket discovery) and #1505 (attach channel),
the next concrete error revealed itself:

  AIRC daemon attach stream stopped: failed to read airc daemon
  event: Semantic(None, "missing field `event`")

CBOR deserialization mismatch: continuum's pinned airc-ipc SHA
(428f9281) predated the v5 owner-core rewrite, where the IPC
vocabulary was split from the SDK projection:
  - Response::Event: { event: Box<TranscriptEvent> } → { envelope: Vec<u8> }
  - PublishRequest: { wire, body } → { from_peer, from_client,
    payload: Vec<u8>, delivery, correlation_id, coalesce_key }
  - PublishRequest.kind: FrameKind → IpcKind
  - PublishRequest.target: MentionTarget → IpcTarget
  - InboxRequest.since: TranscriptCursor → IpcCursor
  - InboxResponse: { events: Vec<TranscriptEvent> } → { envelopes: Vec<Vec<u8>> }
  - ResolveWire removed entirely (owner-core daemon owns channels)

Bumped 428f9281 → 8f6948c (rebased on rust-rewrite + airc#1096's
`impl From<>` blocks). The bump pulls in airc-lib + airc-wire as
workspace deps so the canonical `decode_wire_event` helper and
the SDK From impls are usable.

### What this PR touches

- `src/workers/Cargo.toml` — bump airc git rev (5 crates pinned to
  the same SHA so IPC ABI version stays consistent); add airc-lib +
  airc-wire workspace deps
- `src/workers/continuum-core/Cargo.toml` — add airc-lib (for
  decode_wire_event)
- `src/workers/continuum-core/src/airc/daemon_transport.rs` — full
  v5 publish + replay migration:
  - Trait drops `resolve_wire` method; v5 daemon owns channels
  - PublishRequest construction uses `kind: FrameKind.into()`,
    `target: MentionTarget::All.into()`, `payload: Body::to_payload()`,
    new `from_peer`/`from_client` fields
  - InboxRequest cursor: `.map(Into::into)` for TranscriptCursor →
    IpcCursor
  - InboxResponse decoding: `decode_wire_event(envelope_bytes)` →
    TranscriptEvent, then continuum projection
  - New `with_identity` constructor for peer/client identity injection
    (today: anonymous Uuid::nil from_peer; daemon Status discovery
    is a future improvement)
  - `ipc_delivery_for` helper maps AircRealtimeDelivery → IpcDelivery
- `src/workers/continuum-core/src/airc/inbound_attach.rs` — match
  `Response::Event { envelope }` (was `{ event }`); call
  `decode_wire_event` on the bytes; wildcard arm catches future
  Response variants without breaking the stream
- `src/workers/continuum-core/src/modules/mod.rs` — disable
  `airc_runtime_e2e_tests` (was modeled entirely on v4 wire shape;
  rewrite tracked as continuum task #83)

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

  $ rm -f /tmp/hctest.sock && \
    target/release/continuum-core-server /tmp/hctest.sock > boot.log 2>&1 &
  $ grep "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
  $ grep -i "attach.*stopped\|requires a channel\|missing field" boot.log
  # (empty — no errors)

Three concrete breaks fixed in three successive PRs (#1504, #1505,
this one). Headless inbound attach is now alive end-to-end.

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

### Co-evolution pattern

Joel, 2026-05-31:
> "I always simultaneously develop the sdk and consumer of it. It
>  helps you build the best patterns."

Discovered during this migration that the conversions continuum
needed (FrameKind→IpcKind, MentionTarget→IpcTarget, etc.) lived
as private free functions in airc-lib. Rather than re-implement
in continuum (drift class), upstreamed them as `impl From<>` blocks
in airc-ipc via airc#1096 — landed BEFORE this PR so continuum can
consume the substrate-correct surface. The continuum side is then
a clean `kind: frame_kind.into()` instead of reaching for a
duplicated helper. Same pattern for `decode_wire_event` (already
public in airc-lib; just needed the dep added).

### Follow-ups (filed)

- continuum #83: rewrite `airc_runtime_e2e_tests.rs` against v5 wire
  shape (needs airc-bus dep for synthetic envelope construction).
- airc PR #1095 (open, pending Windows CI): `airc ipc-endpoint` CLI.
  Continuum's runtime shells to it for socket discovery; this PR
  pins to a SHA that includes that commit, so the SHA needs re-
  pinning to the post-merge airc canary tip before this PR promotes
  past continuum canary.
- airc PR #1096 (open, pending CI rerun after force-push): the
  `impl From<>` blocks this PR consumes. Same re-pinning gate.
- Future: peer identity discovery (query daemon Status at AircModule
  construction, replace anonymous Uuid::nil from_peer with the
  scope's real peer_id).

### References

- continuum #1504 + #1505 — sibling fixes for breaks #1 + #2; this PR
  fixes break #3.
- airc PR #1095 — `airc ipc-endpoint` CLI (continuum's runtime
  shell-out).
- airc PR #1096 — SDK-side `impl From<>` blocks (continuum's
  compile-time imports).
- 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 and others added 2 commits May 31, 2026 05:26
…nded waits at boot

Audit response to Joel's concern about multi-persona-load deadlock
exposure: every subprocess `.output().await` in continuum's airc
discovery path was unbounded. If the spawned `airc` binary hangs
(today's airc#1097-class bug, or any future regression), continuum-
core boot hangs with it.

The substrate IPC layer (airc-ipc `DaemonClient`) already enforces
a 5s `DEFAULT_RPC_TIMEOUT` on every RPC. Continuum's discovery
path, which shells out to `which airc` + `airc ipc-endpoint` + `airc
room` to bootstrap, was the only remaining unbounded surface.

### What this PR adds

- `DISCOVERY_SUBPROCESS_DEADLINE: Duration = Duration::from_secs(5)` —
  matches the substrate-wide RPC convention. Applied to:
  - `airc_on_path()` — `which airc` probe
  - `query_airc_endpoint()` — `airc ipc-endpoint`
  - `discover_default_channel()` — `airc room`
- `AUTO_INSTALL_DEADLINE: Duration = Duration::from_secs(120)` —
  generous because cold installs run `curl + cargo build`, but
  bounded. Applied to:
  - `auto_install_airc()` — `bash -c "curl -fsSL .../install.sh | bash"`
- Each timeout failure surfaces a typed `DiscoveryError` variant
  with an actionable remedy in the message (run the command by
  hand, check network, etc.).

### Doctrinal alignment

Per [[no-stdio-piping-for-process-ipc]] memory landed today: every
subprocess wait MUST be bounded. An unbounded `.output().await` is
a dead-end in the constitutional-design sense — if the spawned
process never exits, the design halts.

Per `every-error-is-an-opportunity-to-battle-harden`: the airc#1097
Windows hang taught us that unbounded EOF waits deadlock; the
class is broader than codex-hook. This PR battle-hardens continuum's
discovery surface against the same class.

### Scaling story this confirms

Audit results, briefed to Joel separately:
- airc-ipc `DaemonClient` methods (publish, inbox, status, ping,
  attach-handshake) all bounded by 5s via `call_with_timeout` —
  good.
- Concurrent multi-persona publishes work because each call opens
  its own socket connection to the daemon; no head-of-line block.
- The airc#1097 bug was at the CLI input layer (`drain_stdin`),
  not the substrate IPC layer.
- Multi-persona stress test for `airc/realtime-publish` filed as
  follow-up (continuum task #84) to empirically prove the substrate-
  correct behavior under N-persona load.

### Test plan

- [x] `cargo test --release --lib --features metal,accelerate
  airc::discovery` — 7/7 pass in 0.00s (timeouts not triggered;
  pure parsing + env-override paths).
- [ ] Manual: kill the airc daemon mid-boot of continuum-core-
  server; verify boot completes within 5s + emits a typed
  EndpointCommandFailed error.

### Follow-ups (filed)

- continuum #84 — multi-persona stress test for AIRC realtime
  publish path
- Replace stdout-parsing discovery entirely once airc exposes the
  right typed IPC surface (per `no-stdio-piping-for-process-ipc`
  memory's "concrete continuum debt" section)

### References

- [[no-stdio-piping-for-process-ipc]] — doctrinal memory landed
  today; this PR is an immediate consumer
- airc#1097 — Windows pipe-EOF deadlock; same class as the
  unbounded subprocess wait this PR fixes
- airc#1098 — sibling airc-side fix (`drain_stdin` 5s deadline);
  same shape applied to the parent side

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… carry real attribution

Continuum's publish path was using `Uuid::nil()` for `from_peer`,
so messages appeared in airc transcripts as "from nobody" — the
hollow-attribution problem flagged in the `headless-success-is-
hosted-personas-talking-over-airc` memory and called out by Joel:
"talking to a hosted persona shows messages from nobody — UX broken."

### What this ships

- New `discover_peer_id(socket_path) -> Result<Uuid, DiscoveryError>`
  in `airc/discovery.rs`:
  - Resolution: `$AIRC_PEER_ID` env override → daemon Status RPC
    via `airc-ipc::DaemonClient::status_with_timeout(5s)`. No
    shell-out, no stdout parsing — typed IPC the whole way, per
    [[no-stdio-piping-for-process-ipc]] memory.
  - Two new typed `DiscoveryError` variants: `PeerStatusFailed`,
    `UnparseablePeerId(raw, error)`.

- `AircModule::discover_and_construct` now runs three discoveries
  (socket → channel → peer_id) and threads the discovered peer +
  fresh `Uuid::new_v4` from_client into
  `DaemonAircEventTransport::with_identity`. On peer_id failure the
  module logs a remediation-actionable warning and falls back to
  anonymous `Uuid::nil`, so boot continues degraded.

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

```
$ rm -f /tmp/hctest.sock && \
  target/release/continuum-core-server /tmp/hctest.sock > boot.log 2>&1 &
$ grep "Discovered" 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
Discovered airc scope peer_id via daemon Status
  peer_id=9bb24964-1a1a-43e2-a5aa-8140362bab63
```

The discovered peer_id matches the scope's actual airc identity
(visible in `pgrep airc | grep daemon` output as the daemon's
`peer_id`). Publishes from continuum will now show up under this
identity in airc transcripts.

### Doctrinal alignment

- Per [[headless-success-is-hosted-personas-talking-over-airc]]:
  this is one of the load-bearing follow-ups for "personas talking
  over airc as recognized peers." Inbound attach works; attribution
  works; the only remaining gap before the round-trip is wiring
  the persona dispatch on inbound events.
- Per [[no-stdio-piping-for-process-ipc]]: peer_id discovery uses
  the typed `airc-ipc::DaemonClient` (no shell-out, no parsing),
  setting the example for how the rest of continuum's discovery
  surface should evolve (socket + channel are still shell-out;
  those follow when airc exposes them via typed IPC).

### Follow-ups (filed)

- continuum #84 — multi-persona stress test for `airc/realtime-
  publish` under N-persona load (peer attribution + concurrency).
- continuum #85 — diagnose airc#1097 Windows hang on the 5090.
- Socket + channel discovery still shell out (`airc ipc-endpoint`,
  `airc room`). When airc exposes these as typed RPCs, migrate to
  match this PR's pattern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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