Skip to content

feat(cli): add minimal ao send#83

Merged
harshitsinghbhandari merged 6 commits into
mainfrom
feat/ao-send
Jun 2, 2026
Merged

feat(cli): add minimal ao send#83
harshitsinghbhandari merged 6 commits into
mainfrom
feat/ao-send

Conversation

@harshitsinghbhandari
Copy link
Copy Markdown
Collaborator

@harshitsinghbhandari harshitsinghbhandari commented Jun 2, 2026

Summary

Adds the minimal ao send path for sending a message to a running session through the existing daemon API.

ao send --session <id> --message "..." now:

  1. validates required CLI flags
  2. POSTs { "message": "..." } to /api/v1/sessions/{id}/send
  3. resolves the session runtime handle from sqlite
  4. calls the existing zellij SendMessage, which pastes the message into the agent pane and presses Enter

Intentionally not included:

  • durable .ao/inbox files
  • composite messenger fanout
  • pane-ping pointer messages
  • new runtime WriteChars seam

Closes #79.

Test plan

  • go test ./internal/cli ./internal/daemon ./internal/session_manager ./internal/httpd/controllers

Focused tests cover CLI success/validation/error handling, preserving message whitespace, daemon messenger runtime-handle lookup, and sentinel error wrapping for HTTP error mapping.

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>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 2, 2026

Greptile Summary

This PR wires the ao send CLI path end-to-end: a new send subcommand in cli/send.go POSTs to the existing /api/v1/sessions/{id}/send HTTP endpoint, which then flows through the session service and a new runtimeMessenger adapter that replaces the previous noopMessenger stub to actually paste the message into the live zellij pane.

  • CLI (send.go): validates --session and --message, preserves message whitespace intentionally (tested), and URL-path-escapes the session ID before calling postJSON.
  • Daemon wiring (lifecycle_wiring.go): removes noopMessenger, adds runtimeMessenger which resolves the RuntimeHandleID from the store, checks IsTerminated, and delegates to runtime.SendMessage; sentinel errors (ErrNotFound, ErrTerminated, ErrIncompleteHandle) are wrapped for writeSessionError to map to appropriate HTTP status codes.
  • HTTP controller (sessions.go): adds a ErrTerminated → 409 SESSION_TERMINATED case to writeSessionError to complete the error-mapping table for the send path.

Confidence Score: 5/5

Safe to merge; the change is a self-contained, well-tested addition with no modifications to existing logic paths.

The new messenger replaces a no-op stub with a straightforward store-lookup-then-runtime-call, all sentinel errors are properly chained for existing HTTP error mapping, CLI validation is correct and covered by tests, and the daemon wiring is additive. No existing behavior is altered.

No files require special attention.

Important Files Changed

Filename Overview
backend/internal/cli/send.go New ao send command; validates flags, preserves message whitespace, URL-escapes session ID, delegates to existing postJSON.
backend/internal/daemon/lifecycle_wiring.go Replaces noopMessenger with runtimeMessenger; correctly handles not-found, terminated, and incomplete-handle sentinel errors via errors.Is-compatible wrapping.
backend/internal/daemon/wiring_test.go Adds four targeted tests: happy path handle resolution, ErrNotFound wrapping, ErrIncompleteHandle, and ErrTerminated (verifies runtime is never called for terminated sessions).
backend/internal/httpd/controllers/sessions.go Adds ErrTerminated → 409 SESSION_TERMINATED case to writeSessionError; placement in the switch is correct and does not affect other error mappings.
backend/internal/session_manager/manager.go Adds ErrTerminated sentinel and a thin Send method that delegates to the messenger; error chain from runtimeMessenger is preserved via fmt.Errorf %w.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as ao send (cli/send.go)
    participant Daemon as HTTP Daemon
    participant Ctrl as SessionsController
    participant Svc as sessionsvc.Service
    participant Mgr as sessionmanager.Manager
    participant Msng as runtimeMessenger
    participant DB as sqlite.Store
    participant RT as zellij.Runtime

    User->>CLI: ao send --session X --message ...
    CLI->>CLI: validate flags (usageError exit 2 if blank)
    CLI->>Daemon: POST /api/v1/sessions/X/send
    Daemon->>Ctrl: send(w, r)
    Ctrl->>Ctrl: decodeJSON, validate, stripUnsafeControlChars
    Ctrl->>Svc: Send(ctx, id, message)
    Svc->>Mgr: Send(ctx, id, message)
    Mgr->>Msng: messenger.Send(ctx, id, message)
    Msng->>DB: GetSession(ctx, id)
    DB-->>Msng: rec, ok
    alt not found
        Msng-->>Ctrl: "ErrNotFound -> 404"
    else terminated
        Msng-->>Ctrl: "ErrTerminated -> 409"
    else no handle
        Msng-->>Ctrl: "ErrIncompleteHandle -> 409"
    else ok
        Msng->>RT: SendMessage(ctx, handle, message)
        RT-->>Msng: nil
        Ctrl-->>CLI: 200 ok
        CLI-->>User: exit 0
    end
Loading

Reviews (7): Last reviewed commit: "fix(send): reject terminated sessions" | Re-trigger Greptile

Comment thread backend/internal/adapters/messenger/inbox/inbox.go Outdated
…bering

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>
Comment thread backend/internal/adapters/messenger/inbox/inbox.go Outdated
harshitsinghbhandari and others added 2 commits June 2, 2026 19:12
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>
@harshitsinghbhandari
Copy link
Copy Markdown
Collaborator Author

@greptile-apps review.

@harshitsinghbhandari harshitsinghbhandari changed the title feat(messenger): ao send + live zellij pane ping (live agent nudges) feat(cli): add minimal ao send Jun 2, 2026
@harshitsinghbhandari harshitsinghbhandari merged commit 5435246 into main Jun 2, 2026
0 of 7 checks passed
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.

Replace noopMessenger stub with live AgentMessenger in daemon wiring

1 participant