Skip to content

feat(messenger): ao send CLI + live zellij pane ping (aa-43)#74

Merged
harshitsinghbhandari merged 3 commits into
stagingfrom
feat/aa-43-ao-send-pane-ping
Jun 2, 2026
Merged

feat(messenger): ao send CLI + live zellij pane ping (aa-43)#74
harshitsinghbhandari merged 3 commits into
stagingfrom
feat/aa-43-ao-send-pane-ping

Conversation

@harshitsinghbhandari
Copy link
Copy Markdown
Collaborator

Summary

Closes the agent-nudge loop: the inbox messenger writes a durable file under <workspace>/.ao/inbox/, but the agent in its zellij pane has had no way to know a new file appeared, and there has been no ao send CLI (only an HTTP route). This PR closes both — the e2e demo can now drive an agent end-to-end through ao send.

  • inbox.FilenameFor — exported helper so the inbox messenger (writes) and the new panep messenger (points) agree on a single filename per Send.
  • inbox.WithTime / TimeFromContext — the composite messenger pins one timestamp per Send and stashes it on ctx, so multiple inner messengers that derive a filename from (t, message) see the same t. Without this, inbox's clock and panep's clock fire at different nanoseconds and the ping points at a file that does not exist.
  • panep.Messenger (new package) — implements ports.AgentMessenger by typing a pointer into the agent pane via zellij action write-chars: 📥 ao: new message at .ao/inbox/<file> — please read it then a newline. Pointer over full-body typing because multi-line messages (CI tails, code blocks) fight with the agent's input handler.
  • composite.Messenger (new package) — primary (must succeed) + best-effort secondaries (logged at WARN, swallowed). Daemon wires [inbox, panep]: if the durable file write fails we MUST NOT tell the agent about a file that doesn't exist; if the file is written but the ping fails the agent still sees it on its next inbox check.
  • ao send CLI — mirrors ao spawn: --session and --message required, POSTs to the existing /api/v1/sessions/{id}/send route (HTTP route itself is unchanged on staging). Exits 0/1/2 per CLI convention.
  • zellij.Runtime.WriteChars — the narrow seam panep depends on (via a local RuntimePaneWriter interface in the panep package). NOT added to ports.Runtime; the runtime port stays narrow.

Test plan

  • TDD throughout — failing tests landed before implementation in every package
  • go test -race ./... — 434 pass, 1 acceptable pre-existing failure (TestSessionStreamsRealZellijPane, macOS socket-length issue on staging)
  • go vet ./... clean
  • gofmt -l backend/ clean
  • go build ./... clean
  • Self-review via subagent — Critical finding (panep/inbox filename clock mismatch) fixed in follow-up commit, plus 4 Important + 1 Minor
  • End-to-end integration test in composite_test.go::TestSend_PingFilenameMatchesOnDiskFilename wires real inbox + real panep over a tempdir and asserts the on-disk filename equals the filename in the ping body — the contract the brief implicitly required

🤖 Generated with Claude Code

harshitsinghbhandari and others added 2 commits June 2, 2026 00:38
Closes the agent-nudge loop: the inbox messenger writes a durable file
under <workspace>/.ao/inbox/ but the agent in its zellij pane has no
way to know a file appeared. This PR adds three pieces that finish the
chain end-to-end, plus an `ao send` CLI that drives prateek's existing
POST /api/v1/sessions/{id}/send route.

  1. inbox.FilenameFor — exported helper so both the inbox messenger
     (writes the file) and the panep messenger (points at the file)
     agree on the same name for the same (timestamp, message) input.

  2. panep.Messenger — new ports.AgentMessenger implementation that
     types a short pointer into the agent's pane via the zellij
     write-chars action: "📥 ao: new message at .ao/inbox/<file>
     — please read it" + newline. Pointer over full-body typing because
     multi-line messages (CI tails, code blocks) fight with the agent's
     input handler.

  3. composite.Messenger — fans Send out to a primary (must succeed) +
     N best-effort secondaries (errors logged at WARN, swallowed).
     Daemon wires inbox as primary, panep as secondary: if the durable
     file write fails we MUST NOT tell the agent about a file that
     doesn't exist; if the file is written but the ping fails the
     agent will see it on its next inbox check.

  4. ao send CLI — mirrors `ao spawn`'s shape: --session and --message
     required, POSTs to the existing send route, exit 0/1/2 with the
     same semantics as spawn. The HTTP route itself is unchanged.

zellij runtime gains WriteChars(ctx, handle, s) as the narrow seam panep
calls; it is NOT added to ports.Runtime (panep depends on a local
RuntimePaneWriter interface), keeping the runtime port narrow.

TDD throughout; full suite passes with -race (one acceptable pre-existing
macOS socket-length failure in TestSessionStreamsRealZellijPane).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses self-review findings on the prior commit:

1. (Critical) panep filename now matches the inbox filename on disk. Each
   messenger previously had its own time.Now clock — they fired at different
   nanoseconds, so the ping pointed at a file the inbox messenger never
   wrote. Fix: composite.Messenger captures one timestamp per Send and
   stashes it on ctx via inbox.WithTime; both inbox and panep read it back
   via inbox.TimeFromContext, falling back to their own clock when called
   stand-alone. New integration test in composite proves the on-disk
   filename equals the filename inside the ping body.

2. (Important) `ao send` was sending the untrimmed --message body over the
   wire even though it validated the trimmed version. Now sends the
   trimmed message; new test pins the contract.

3. (Important) composite test now uses a slog.Handler that records full
   Records, so we assert the secondary failure is logged at WARN
   specifically (not just that the text appears in a level-filtered
   buffer).

4. (Important) `ao send` 5xx test now asserts the server's error kind
   propagates to the operator instead of being swallowed for an
   "HTTP 500"-shaped message.

5. (Minor) Renamed `cap` local to `capture` in send_test.go to avoid
   shadowing the cap builtin.

panep.SetClock dropped — context injection makes it unnecessary.

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

greptile-apps Bot commented Jun 1, 2026

Greptile Summary

This PR closes two gaps in the agent-nudge loop: it adds an ao send CLI command that POSTs to the existing /api/v1/sessions/{id}/send route, and a new panep messenger that pings the agent's zellij pane with a pointer to the newly-written inbox file. A shared-timestamp mechanism (inbox.WithTime/TimeFromContext) ensures both the inbox write and the pane ping reference the same filename.

  • New packages: composite.Messenger (primary+best-effort secondary fan-out), panep.Messenger (zellij pane-ping via write-chars), and a new ao send CLI subcommand mirroring ao spawn.
  • Inbox changes: FilenameFor exported and WithTime/TimeFromContext helpers added so the composite can pin one timestamp across all inner messengers, preventing clock-skew divergence between the file write and the ping body.
  • Daemon wiring: inbox (primary) is now composed with panep (secondary) in newSessionMessenger; wiring test asserts ordering and types.

Confidence Score: 4/5

Safe to merge for functionality; the inbox write (durable delivery) is correct in all paths. The outstanding concern is that ao send reuses the 2 s shared HTTP client for a handler that synchronously runs file I/O and two zellij subprocesses — on a loaded machine the CLI can report failure even when the message was delivered.

The composite/inbox/panep logic is sound and well-tested, including the end-to-end filename-consistency proof. The ao send CLI, however, does not apply the same per-request timeout pattern that ao spawn uses: spawn creates a dedicated &http.Client{} with a 90 s context deadline because the handler does slow work; send reuses deps.HTTPClient with its 2 s client-level timeout against a handler that now synchronously runs inbox I/O and two zellij action write-chars subprocesses. On a slow machine or when zellij is congested, the CLI exits 1 while the agent already has the message.

backend/internal/cli/send.go — HTTP client timeout mismatch with the handler's synchronous workload.

Important Files Changed

Filename Overview
backend/internal/cli/send.go New ao send CLI command. Uses the shared deps.HTTPClient (2 s client timeout) for a handler that runs inbox I/O + two zellij subprocesses synchronously — can timeout before the daemon responds even when the message was durably written.
backend/internal/adapters/messenger/panep/panep.go New pane-ping messenger. Two sequential WriteChars calls (body then newline); if the body write succeeds but the newline write fails, the agent pane is left with an unterminated input line in its buffer. Error is swallowed by composite, so inbox delivery is safe.
backend/internal/adapters/messenger/composite/composite.go New composite messenger with correct primary-must-succeed / secondary-best-effort semantics; pinned timestamp via inbox.WithTime prevents clock-skew filename divergence.
backend/internal/adapters/messenger/inbox/inbox.go FilenameFor exported and WithTime/TimeFromContext helpers added; change is backwards-compatible and well-tested.
backend/internal/adapters/runtime/zellij/zellij.go WriteChars added using existing execRunner pattern; correctly re-uses handleID validation; not added to ports.Runtime interface.
backend/internal/daemon/session_wiring.go storeSessionHandleLookup adapter and newSessionMessenger wiring factory added; ordering assertion in wiring_test confirms inbox-first contract.

Sequence Diagram

sequenceDiagram
    participant CLI as ao send (CLI)
    participant HTTP as HTTP Handler
    participant Comp as composite.Messenger
    participant Inbox as inbox.Messenger
    participant Panep as panep.Messenger
    participant FS as Filesystem
    participant Zellij as zellij (write-chars)

    CLI->>HTTP: "POST /api/v1/sessions/{id}/send"
    HTTP->>Comp: Send(ctx, id, message)
    Comp->>Comp: "ctx = inbox.WithTime(ctx, clock())"
    Comp->>Inbox: Send(ctx, id, message) [primary]
    Inbox->>FS: WriteFile(.ao/inbox/ts_hash.md)
    FS-->>Inbox: ok
    Inbox-->>Comp: nil
    Comp->>Panep: Send(ctx, id, message) [secondary, best-effort]
    Panep->>Panep: "filename = FilenameFor(TimeFromContext(ctx), message)"
    Panep->>Zellij: WriteChars(handle, ping body)
    Zellij-->>Panep: ok / err (swallowed)
    Panep->>Zellij: WriteChars(handle, newline)
    Zellij-->>Panep: ok / err (swallowed)
    Panep-->>Comp: nil / err (logged+swallowed)
    Comp-->>HTTP: nil
    HTTP-->>CLI: 200 OK
Loading

Reviews (2): Last reviewed commit: "fix(cli): repair `cap` → `capture` over-..." | Re-trigger Greptile

Comment thread backend/internal/cli/send_test.go Outdated
The earlier replace-all turned every occurrence of `cap` into `capture`,
including substrings inside `captures` and `captured` — producing
`capturetures` (comment), `capturetured` (test message), and a stray
`captureture` local in TestSend_TrimsLeadingAndTrailingWhitespace.

Restore the originals so the names match `sendCapture` and the rest of
this file.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@harshitsinghbhandari harshitsinghbhandari merged commit f6ef476 into staging Jun 2, 2026
7 checks passed
harshitsinghbhandari added a commit that referenced this pull request Jun 2, 2026
* feat(messenger): ao send + live zellij pane ping (live agent nudges)

Replace the daemon's noopMessenger stub with a composite AgentMessenger
that writes a durable inbox file (primary) and types a live pointer into
the running zellij pane (best-effort secondary), plus the `ao send` CLI
that drives the existing POST /api/v1/sessions/{id}/send route.

- composite: fans Send to inbox then panep, pinning one timestamp so both
  derive the same filename; a secondary failure is logged at WARN and
  swallowed (the file is on disk), a primary failure aborts the call.
- inbox: writes <workspace>/.ao/inbox/<rfc3339nano>_<hash>.md.
- panep: types "new message at .ao/inbox/<file>" + Enter via a new narrow
  zellij WriteChars seam (RuntimePaneWriter), kept off ports.Runtime.
- wiring: newSessionMessenger composes inbox+panep over the shared store;
  startSession takes the messenger instead of the noop stub.

Carries across @Aa-43's work from PR #74 (staging), adapted to main's
post-#65/#77 daemon wiring shape.

Closes #79

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(inbox): use O_EXCL so a filename collision errors instead of clobbering

os.WriteFile opens with O_CREATE|O_WRONLY|O_TRUNC, which silently overwrites
an existing file. The doc comment already stated the intent ("we do not retry
on EEXIST"), but O_TRUNC never yields EEXIST — two identical messages sent on
the same composite-pinned nanosecond would produce the same filename and the
second Send would silently lose the first message. Switch to
O_CREATE|O_EXCL|O_WRONLY so a collision surfaces as an error; O_EXCL also
refuses to follow a symlink at the final path component. Add a regression test.

Addresses greptile review on PR #83.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(inbox): remove the freshly-created file when write or close fails

The O_EXCL switch creates the inbox file before writing its body; if
WriteString or Close then fails, the empty/partial .md was left on disk and
the agent's next inbox scan would pick up a truncated ghost message. Remove
the file on those error paths. O_EXCL guarantees the file did not exist before
this call, so the cleanup can only delete our own partial write, never a
legitimate earlier message.

Addresses greptile review on PR #83.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(messenger): reduce ao send to live pane delivery

* fix(send): preserve messages and map lookup errors

* fix(send): reject terminated sessions
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.

1 participant