Skip to content

feat(daemon): send-to-session endpoint — drive any agent from CodeWatch#44

Merged
ThinkOffApp merged 2 commits into
mainfrom
feat/send-to-session
Jul 16, 2026
Merged

feat(daemon): send-to-session endpoint — drive any agent from CodeWatch#44
ThinkOffApp merged 2 commits into
mainfrom
feat/send-to-session

Conversation

@ThinkOffApp

Copy link
Copy Markdown
Owner

What

The backend primitive for CodeWatch's send box (contract v1 as posted in thinkoff-development on 2026-07-16, requested by Petrus):

  • POST /sessions/send {agent, text, from?} → deliver text as user input into the named agent's live session. 202 {ok, agent, deliveredVia} accepted-async; 400/404/503/502 for validation / unknown agent / no adapter / peer failure. Bearer-token auth like every :8788 route; every attempt receipted (kind: sessions.send).
  • GET /sessions/agents{agents:[{name, adapter}]} for the app's target picker.

Adapters (config: mcp.confirmations.sessions.agents.<name>)

adapter delivery notes
wake spawn script with the message as argv existing /wake pattern
gui same, chained behind idle_guard --wait never types over the human (claudeMB's requirement, reuses human-idle-guard)
tmux send-keys -l <text> then C-m literal, no key-name interpretation
forward POST same body to peer daemon provenance passed through so [from …] prefix is applied exactly once, by the owner

Cross-machine flow: app → any reachable daemon → forward → owning daemon → GUI/tmux injection.

Tests

9 new cases in test/session-send.test.mjs: validation matrix, wake delivery + provenance prefix, gui guard-before-script ordering, tmux literal+Enter call sequence, real two-daemon forward relay (ephemeral ports) incl. single-prefix assertion, unreachable-peer 502, HTTP routes (202/503/401), picker. Suite run with confirmations + mcp-server tests: 53/53.

Out of scope (tracked)

🤖 Generated with Claude Code

@cursor

cursor Bot commented Jul 16, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a6d5197c6b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/session-send.mjs
'-c', '"$1" --wait && exec "$2" "$3"', '--', guard, script, message,
], { detached: true, stdio: 'ignore' });
} else {
child = spawn(script, [message], { detached: true, stdio: 'ignore' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle spawn errors before returning 202

When a wake agent (or gui without idle_guard) has a stale or non-executable script path, spawn() emits its failure asynchronously as a child error event; this catch does not run and no error listener is attached, so Node treats it as an unhandled error and terminates the daemon after the endpoint has already returned 202. Attach an error handler or validate the script before accepting the delivery.

Useful? React with 👍 / 👎.

Comment thread src/session-send.mjs
}
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), FORWARD_TIMEOUT_MS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear the forward timeout on failures

When a forward peer is down or the request aborts, fetch() throws before the later clearTimeout(timer) runs, leaving this referenced timer alive for the full 8 seconds; short-lived callers and tests that hit an unreachable peer hang until that timeout per failed attempt, and repeated failures accumulate pending timers. Clear it in a finally block or unref the timer.

Useful? React with 👍 / 👎.

ThinkOffApp added a commit that referenced this pull request Jul 16, 2026
Review findings from @codexmb on #44:

1. Loop guard: forwards carry an x-iak-send-hops header and the chain
   is bounded to one hop (app -> reachable daemon -> owning daemon);
   a self-pointing peer now terminates in two requests with 508/502
   instead of recursing until timeout.
2. Strict peer confirmation: forward success requires exactly 202 +
   ok:true from the peer; a bare 200/204 or unparseable body reads as
   failure (502), never as delivered_via.
3. Input hardening: `from` must be a string of <=200 chars, request
   bodies over 64 KiB are cut off with 413, and failures after the
   detached spawn (accepted-async by design) are surfaced through an
   onAsyncError hook receipted as sessions.send.async_error.

Six new regression tests incl. a live self-pointing-peer loop test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ThinkOffApp

Copy link
Copy Markdown
Owner Author

All three findings fixed in the new commit:

  1. Forward loop (P1) — forwards now carry x-iak-send-hops, bounded to ONE hop by design (app → reachable daemon → owning daemon; the owner must deliver locally). A self-pointing peer terminates after two requests: inner refuses with 508 "hop limit reached", outer reports 502. Live loop test included (daemon whose peer is itself).
  2. Lax peer success (P1) — peer confirmation must be exactly 202 + ok: true; bare 200/204 or malformed JSON → 502, never delivered_via. Regression test with a raw 200-{} peer stub.
  3. Validation (P2)from capped at 200 chars and type-checked before prefixing; request bodies cut at 64 KiB with 413 (req.destroy() on overflow). Post-spawn failures (inherent to accepted-async 202) now surface via an onAsyncError hook that the route receipts as sessions.send.async_error — a dead injection script is visible in receipts instead of vanishing.

59/59 across session-send + confirmations + mcp-server suites. Re-verify when convenient.

ThinkOffApp and others added 2 commits July 16, 2026 13:31
POST /sessions/send {agent, text, from?} delivers text as user input
into a named agent's live session; GET /sessions/agents feeds the
target picker. Adapters per agent (mcp.confirmations.sessions.agents):
wake (spawn injection script), gui (same, chained behind a human-idle
guard so it never types over the user), tmux (literal send-keys +
Enter), forward (relay to the owning machine's daemon; provenance
prefix applied exactly once, by the owner). 202 accepted-async; every
attempt receipted; token auth like all :8788 routes.

Backend half of the CodeWatch send box (contract v1 posted in-room
2026-07-16); app half lands in CodeWatch against the same shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review findings from @codexmb on #44:

1. Loop guard: forwards carry an x-iak-send-hops header and the chain
   is bounded to one hop (app -> reachable daemon -> owning daemon);
   a self-pointing peer now terminates in two requests with 508/502
   instead of recursing until timeout.
2. Strict peer confirmation: forward success requires exactly 202 +
   ok:true from the peer; a bare 200/204 or unparseable body reads as
   failure (502), never as delivered_via.
3. Input hardening: `from` must be a string of <=200 chars, request
   bodies over 64 KiB are cut off with 413, and failures after the
   detached spawn (accepted-async by design) are surfaced through an
   onAsyncError hook receipted as sessions.send.async_error.

Six new regression tests incl. a live self-pointing-peer loop test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ThinkOffApp
ThinkOffApp force-pushed the feat/send-to-session branch from c3ed464 to 21e3c34 Compare July 16, 2026 10:34
@ThinkOffApp
ThinkOffApp merged commit f75176e into main Jul 16, 2026
3 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.

1 participant