Skip to content

Bridge seam: multi-session control — target-filtered drain + per-workflow cursor + FO heartbeat #436

Description

@gcko

Summary

The Spacedock-repo half of "multi-session control" — completing the Bridge↔Spacedock seam so captain intent (conn/tell) routes to a specific workflow's FO (or broadcasts) and Bridge can show per-workflow FO liveness when several spacedock claude sessions run concurrently from one repo root.

The Bridge half is already merged (it writes a target on each inbox record, reads per-workflow heartbeat files, and shows a per-workflow roster). This issue is the Spacedock side: teach the bridge-inbox drain mod to honor target with a per-workflow cursor, and write the per-workflow FO heartbeat Bridge reads.

Mirrors Linear DRC-3732 (and pairs with PR #435 / the bridge-seam-inbox-events branch).

Contract Bridge already emits/expects (must match exactly)

  • Inbox «FO-cwd»/_bridge/inbox.jsonl (append-only, Bridge writes): {"ts","kind":"tell"|"conn","text","granted"?,"target":"<workflow-slug>"|"all"}. target absent ⇒ all.
  • Per-workflow cursor «FO-cwd»/_bridge/.inbox-cursor.<slug> — each FO advances only its own.
  • Heartbeat «FO-cwd»/_bridge/fo.<slug>.json (FO writes, Bridge reads): {"session_id","ts":"<rfc3339>","state":"idle"}. Bridge keys liveness on ts freshness (age ∈ [0, 30m]; a future-dated ts is rejected → honest not-attached) and defaults empty state to idle.
  • Routing key <slug> = the FO's --workflow-dir basename.

Changes (in docs/dev/_mods/bridge-inbox.md)

  1. Update the inbox-record schema block to include target (currently omitted).
  2. Target-filtered drain: read records since _bridge/.inbox-cursor.<myslug>; act on only those where target == myslug || target == "all" (absent ⇒ all); advance the per-workflow cursor. Malformed/unknown records are skipped but still advance the cursor — never block the loop. Concurrent FOs never race (each owns its cursor).
  3. Cursor migration (one-time): on first run, if .inbox-cursor.<myslug> is absent but the old shared .inbox-cursor exists, seed the per-slug cursor from it — do not start at 0, or the FO re-drains the whole inbox and replays old conn grants.
  4. FO heartbeat: on boot and each idle tick, write _bridge/fo.<myslug>.json with a present-time RFC3339 UTC ts, state:"idle" (the mod runs at startup/idle boundaries — it cannot honestly assert working), and session_id from $CLAUDE_CODE_SESSION_ID. Observe-only; never block the loop.
  5. Slug safety: myslug = basename {workflow_dir}; refuse a slug that is not a plain filename component (contains /, .., whitespace, or empty) before constructing any _bridge/*.<myslug> path.

The mod stays workflow-scoped (copied manually into other workflows' _mods/, same as today); promoting it to a built-in is a later follow-up.

Acceptance criteria

  1. FO for X drains target:"X" and target:"all"/absent, but NOT target:"Y".
  2. Two concurrent FOs never clobber — each advances only .inbox-cursor.<its-slug>.
  3. First run seeds .inbox-cursor.<slug> from the old shared cursor when present (no re-drain).
  4. The FO writes/refreshes fo.<slug>.json (present-time ts, state:"idle") on boot + each idle tick.
  5. A malformed record or unsafe slug is skipped/refused without blocking the loop.

Honest-degradation note

A session idle >30m stops ticking, so its heartbeat goes stale and Bridge shows not-attached though the process is alive. Intended (Bridge degrades rather than fabricate freshness), not a bug.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions