Skip to content

feat(keepalive): add amq-keepalive, a terminal-session wake supervisor for macOS - #245

Closed
ohade wants to merge 2 commits into
avivsinai:mainfrom
ohade:feat/amq-keepalive-supervisor
Closed

feat(keepalive): add amq-keepalive, a terminal-session wake supervisor for macOS#245
ohade wants to merge 2 commits into
avivsinai:mainfrom
ohade:feat/amq-keepalive-supervisor

Conversation

@ohade

@ohade ohade commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

This adds amq-keepalive as a second binary (cmd/amq-keepalive + internal/keepalive/*): a supervisor that keeps wake delivery attached to explicitly registered terminal sessions on macOS. It implements the Supervisor recipes contract from COOP.md, meaning the OS supervises it (user LaunchAgent), it never parses AMQ mailbox, lock, presence, or target files, and it talks to AMQ only through the public CLI (target-aware amq wake, amq env --json).

What it adds over the raw launchd recipe: a registry of attached terminal targets with Ghostty and cmux adapters (exact terminal-id targeting, fail-closed injection), reattach after reboot or sleep, SessionStart reattach hooks for Claude Code and Codex (install-hook), and a doctor command.

I know the repo is deliberately daemon-free, so to be upfront about it: I read this as staying inside that boundary (launchd owns the lifecycle, this is just the CLI it supervises plus a registry). If you'd rather not take a supervisor into the tree at all, no hard feelings, close it, but I wanted to offer the real thing rather than just a docs pointer.

Scope notes:

  1. Runtime behavior is macOS-oriented (launchd, Ghostty AppleScript, cmux JSON RPC). It compiles on all four GOOS targets your CI cross-builds (linux, darwin, windows, freebsd) and vets clean. The one platform-specific call is the registry advisory lock (syscall.Flock), which is behind a //go:build unix file with a compile-only no-op fallback for Windows; the in-process mutex still serializes access within a process there.
  2. Not touched: goreleaser/Homebrew packaging and the release manifest. If you take this, shipping the second binary in releases can be a follow-up.
  3. The sample SessionStart hook script lives at scripts/amq-keepalive-session-start.sh with a drift test keeping it in sync with the embedded copy.

Verification: make ci green locally (check-skills, fmt-check, vet, golangci-lint 0 issues, full go test, smoke). The ported packages bring 70 tests of their own; adapters are tested through a deterministic file adapter.

ohad.e added 2 commits July 19, 2026 23:50
…uild

The registry lock used syscall.Flock unconditionally, which fails
GOOS=windows go build (part of the repo CI cross-platform contract).
Move the advisory lock behind //go:build unix (flock_unix.go) with a
compile-only no-op fallback for other platforms (flock_other.go). The
in-process mutex in withLock still serializes access within a process on
platforms lacking flock; cross-process locking is preserved on unix/macOS.

@avivsinai avivsinai left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for building this — and for the honesty in the PR body about the philosophy call. We did a deep pass (independent Claude + Codex reviews, cross-checked, findings runtime-verified). Verdict up front:

Not in-tree. Keep amq-keepalive in your repo, and reopen #244 — I'll take the docs link. Closing #244 as "keep it simple" was the wrong simplification; it's the right shape.

Why external is the answer

  • It's the layer AMQ explicitly disclaims. CLAUDE.md: "Long-running wake/monitor supervision belongs to launchd, systemd, or another layer above daemon-free AMQ." This is that layer. Your launchd-owns-the-lifecycle argument is technically true, but the boundary in question isn't daemon-free — it's which repo owns the supervision layer, and that's already carved out.
  • Your own architecture proves the seam. internal/keepalive imports zero AMQ internals and talks to AMQ only by exec-ing the public CLI (amq env --json, amq wake ...). That's exactly right — and it means in-tree buys you nothing except blessing. It would build verbatim as a standalone module.
  • In-tree as-is doesn't even ship. .goreleaser.yaml builds only ./cmd/amq and the Homebrew formula installs only amq. Merged as-is, users still can't install amq-keepalive from a release — +5,580 LOC, a second cross-build target, and two third-party adapter surfaces (Ghostty AppleScript, cmux RPC) for zero delivered distribution.
  • Precedent. amq-squad stayed external for a less invasive scope. keepalive additionally rewrites ~/.claude/settings.json and Codex hook config and manages LaunchAgents — a generic transport shouldn't own editing other products' configs.

Two contract failures (blocking regardless of placement)

  1. retire-session calls a command that doesn't exist. internal/keepalive/amq/runner.go:129 invokes amq wake retire -json .... There is no retire subcommand — it was deliberately deferred out of #232 pending #235's identity-safety work. Runtime-verified against a fresh amq build: the invocation parses as a plain foreground wake and exits 1 ("amq wake requires a real terminal…"). The suite can't see this because runner_test.go:91 / app_test.go:300 stub amq with a fake that prints {"status":"retired"}.
  2. Exact-target reattach doesn't exist in AMQ. supervisor.go:54-58 assumes --accept-existing-wake accepts only the exact live target, but the reuse path in internal/cli/wake_unix.go never compares the requested target/TargetDigest against the persisted lock. Reattaching terminal A→B can mark B active in your registry while the live wake keeps injecting into A — wrong-terminal delivery of message-derived text. TestWrongExistingTargetFailsClosedInsteadOfMarkingActive fabricates the fail-closed sentinel in a fake runner; the real path can't produce it.

Credit where due — the security review came back clean

osascript gets the payload as argv (never spliced into script source), cmux params are json.Marshal-built with no shell anywhere, hook edits are parse→modify→atomic 0600 write with backups, registry is 0700/0600 with correct flock+tempfile+rename atomicity, and the "never parses AMQ internals" claim verified true. Genuinely well built.

Worth fixing wherever it lives: uninstall removes the LaunchAgent but never removes the installed Claude/Codex hooks (one-way persistence); CLI.RepairWake is dead code; context.Background() in main means the supervise loop's ctx.Done() can never fire.

The part that does belong upstream

If the --inject-via contract has gaps for external supervisors, file a focused issue — stabilizing/documenting that as a versioned surface helps every terminal adapter, not just this one. wake retire is wanted but blocked on #235; keepalive's retire path should track that issue rather than assume the command.

So: reopen #244 and I'll merge the link. And thank you — this is the most rigorous external contribution this repo has had.

@avivsinai

Copy link
Copy Markdown
Owner

Re-reviewed against current main (post v0.45.0), and I owe you a good-news update: both blocking findings from my earlier review are now resolved by AMQ's own hardening work, so the contract amq-keepalive builds on is now stable and released.

So the two correctness blockers weren't really about your code — they were AMQ contract gaps, and we've now filled them.

The placement call is unchanged, and it's the one that governs: this is the supervision layer AMQ explicitly disclaims ("belongs to launchd, systemd, or another layer above daemon-free AMQ"), and your own clean seam (zero AMQ internals, public-CLI-only) proves it needs no in-tree blessing. So I'm keeping it external, exactly as you generously offered.

Closing this, and I've taken the docs link myself — #263 adds the COOP.md pointer to ohade/amq-keepalive, using your wording from #244, credited to you. Thank you again; this remains the most rigorous external contribution the repo has had.

@avivsinai avivsinai closed this Jul 21, 2026
avivsinai added a commit that referenced this pull request Jul 21, 2026
Recovers the docs link from Ohad's #244 (which he closed) and the
outcome of the #245 review: keep `amq-keepalive` external, and link it
from AMQ's supervisor-recipes section.

Adds a short pointer in COOP.md to
[ohade/amq-keepalive](https://github.com/ohade/amq-keepalive) — a
standalone macOS companion that supervises target-aware `amq wake` for
explicitly registered terminal sessions (Ghostty, cmux) via a user
LaunchAgent, with SessionStart reattach hooks. It stays external, keeps
AMQ daemon-free/transport-only, and talks to AMQ solely through the
public CLI.

Text is Ohad's own wording from #244; verified the target repo is public
and non-empty.

Co-authored with Ohad (credited in the commit).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Ohad Edelstein <ohade@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants