feat(keepalive): add amq-keepalive, a terminal-session wake supervisor for macOS - #245
feat(keepalive): add amq-keepalive, a terminal-session wake supervisor for macOS#245ohade wants to merge 2 commits into
Conversation
…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
left a comment
There was a problem hiding this comment.
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/keepaliveimports 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.yamlbuilds only./cmd/amqand the Homebrew formula installs onlyamq. 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.jsonand Codex hook config and manages LaunchAgents — a generic transport shouldn't own editing other products' configs.
Two contract failures (blocking regardless of placement)
retire-sessioncalls a command that doesn't exist.internal/keepalive/amq/runner.go:129invokesamq wake retire -json .... There is noretiresubcommand — it was deliberately deferred out of #232 pending #235's identity-safety work. Runtime-verified against a freshamqbuild: the invocation parses as a plain foreground wake and exits 1 ("amq wake requires a real terminal…"). The suite can't see this becauserunner_test.go:91/app_test.go:300stubamqwith a fake that prints{"status":"retired"}.- Exact-target reattach doesn't exist in AMQ.
supervisor.go:54-58assumes--accept-existing-wakeaccepts only the exact live target, but the reuse path ininternal/cli/wake_unix.gonever compares the requested target/TargetDigestagainst 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.TestWrongExistingTargetFailsClosedInsteadOfMarkingActivefabricates 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.
|
Re-reviewed against current
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 |
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>
This adds
amq-keepaliveas 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-awareamq 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 adoctorcommand.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:
GOOStargets 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 unixfile with a compile-only no-op fallback for Windows; the in-process mutex still serializes access within a process there.scripts/amq-keepalive-session-start.shwith a drift test keeping it in sync with the embedded copy.Verification:
make cigreen locally (check-skills, fmt-check, vet, golangci-lint 0 issues, fullgo test, smoke). The ported packages bring 70 tests of their own; adapters are tested through a deterministicfileadapter.