Skip to content

notify 0.1.0: a push when an agent's turn is done - #541

Draft
YanYunCY wants to merge 2 commits into
magpie-community:mainfrom
YanYunCY:gateway-notify
Draft

YanYunCY wants to merge 2 commits into
magpie-community:mainfrom
YanYunCY:gateway-notify

Conversation

@YanYunCY

Copy link
Copy Markdown

A new gateway hooks package, @magpie-community/gateway-notify: a push to your phone or team chat when an agent's turn is over — so a long Claude Code / Codex task can run while you're away from the screen.

When it sends

  • A turn opens when the user's own words reach the model in a request that offers tools (onRequest), and closes when the reply asks for no more tools (onResponse), read in each API's shape: Anthropic Messages, Responses, Chat completions, Gemini.
  • Only turns that ran minMinutes (default 3); a failed request (ctx.success === false) closes the turn with a ❌ push (errors, on by default).
  • Side calls without tools (titles, probes) neither open nor close a turn; a new user message restarts it; one push per conversation per cooldownSeconds.
  • Own origins only (local, magpie-key) unless allOrigins; optional agents allow-list.

Channels: ntfy, Bark, Server酱 (SCT and sctp keys), Feishu and DingTalk robots (with signing secrets), WeCom robot, Telegram bot, generic webhook. One HTTPS POST each, 10 s limit, sent in the background; nothing is awaited on the request's path and nothing throws.

Privacy: by default the push carries agent, project folder name, model and duration only; the reply's text is opt-in (includeText, capped by maxTextLength).

Tests: bun test packages/notify — 19 tests, every channel faked with fetch (no network, no account). bun scripts/check.mjs passes (it skips hooks packages). Also checked a live publish to ntfy.sh.

Also adds the registry entry (en/zh summary) and a row in the README's gateway hooks table.

Not yet run inside a live magpie install — feedback on the turn detection against real agent traffic is very welcome.

…酱, Feishu, DingTalk, WeCom, Telegram, webhook)
@yetone

yetone commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Thanks. This is in scope: a notifications hooks package, not a relay. The channel code is clean, and the turn detection holds on every wire shape. A few things need fixing before it can merge.

What was run, at 3182649

  • On mote, bun test packages/notify passed 19/19 and bun scripts/check.mjs exited 0.
  • Broken by hand: dropping the \n from DingTalk's string-to-sign turned "Feishu and DingTalk sign…" red, and removing the cooldown check turned "cooldown…" red.
  • End to end: this package ran in magpie's real plugin host behind the real gateway (origin/main 86e19b9b7). The upstream was a fake Chat endpoint and the channel was a webhook pointed at a local server, so no push service was called. It covered chat, responses, anthropic and gemini clients, each streamed and not streamed. In all 8 cases, a tool-call reply sent no push, the tool-result → text reply sent exactly one ✅ (project name read from <cwd>, no text unless includeText), and an upstream 500 sent ❌. So onResponse gets the consolidated reply with stop_reason / finish_reason / finishReason / status as the code expects, and nothing waits on the request path.
  • Feishu signing (HMAC-SHA256 keyed by timestamp\nsecret over an empty message, timestamp in seconds) and DingTalk 加签 (key = secret, message timestamp\nsecret with the timestamp in ms, base64, URL-encoded) match the vendor docs. Server酱³'s <uid>.push.ft07.com/send/<key>.send, Bark's /push and ntfy's JSON publish are right too. Send errors are never logged, so Telegram tokens and Server酱 keys in URLs don't leak.

Blockers

  1. Claude Code subagents send a false "done" push and reset the real turn. Claude Code's subagent requests carry the conversation's own session header (magpie's subagentOf in internal/gateway/gateway.go notes this). A subagent's first request is user words plus tools, so onRequest restarts the main turn under s:<session>. The subagent's final end_turn then closes it with ✅ while the main agent is still working. Seen end to end in all 8 cases: main user → tool_call, then a subagent request in the same session → text gives a ✅ "done" push. The main turn's real end is then timed from after the subagent, so it can fall under minMinutes and be dropped. One way to fix it: key a conversation by session plus a hash of its first user message, and treat a conversation that first shows up while another one in the same session is waiting for a tool result as nested, so it never pushes. (Or ask magpie to put the subagent id in the hook context.) Please add a test for it.
  2. The suggested options send to a shared public ntfy topic. package.json magpie.options and the README use "topic": "pick-a-long-random-topic". magpie's options editor starts from magpie.options (optionsExample), and that value passes usable(). A user who saves the editor without changing it publishes agent, project folder name and model to a public ntfy.sh topic that every such user shares. Please make the example unusable until the user fills it in (for example "topic": ""), and have the README say to pick a random topic.
  3. ctx.origin === "magpie-key" is not a value magpie sends. The origins are local, gateway-key (any named key, including magpie's sharing key, used on loopback or from another device) and remote (gatewayOrigin in internal/gateway/middleware.go; docs/subsystems/gateway-request-hooks.md). So the README's "magpie's local and sharing key" isn't what the code does. In practice only local passes. Excluding gateway-key by default is reasonable, since a named key may be a friend's. Please drop magpie-key, say in the README what actually counts, and use the real gateway-key / remote values in the test instead of "team-key".

Should fix

  1. A transient failure sends ❌ mid-task. Agents retry 429/5xx/529 on their own, and pressing Esc also arrives as success: false. Right now the first failure closes the turn with ❌ "failed", the retry opens a new turn, and a ✅ follows. Please hold the failure push for a while (say 60 s) and cancel it if another request comes in on the same conversation.
  2. ctx.report(...) inside onResponse has no effect: the host's report checks the request phase's signal, which has already been aborted by then (host.js gatewayBegin). The tests' ["ready"] / ["unavailable"] assertions only check the fake ctx. That's harmless, but don't rely on it for diagnostics.

Nits

  • An agent id with no name shows as is (for example "other done").
  • words.in and words.turn are unused.
  • Long turns read "125 min".

…eld failure push

- A conversation is the session plus a hash of its first user message's text.
  One that first shows up while another conversation in the same session is
  waiting on a tool is nested (a Claude Code subagent): it never pushes and
  never resets the main turn. Requests with no user message (Responses
  continuing by previous_response_id) stay in the newest top-level one.
- package.json options ship with an empty ntfy topic, so nothing is sent
  until the user fills one in; README generates a random topic.
- Only origin "local" counts by default; drop "magpie-key". README says
  what gateway-key and remote mean; tests use the real values.
- A failure push waits failureHoldSeconds (60) and is dropped when the
  conversation goes on (agent retry of 429/5xx, Esc then a new message).
- Drop ctx.report from onResponse (it has no effect there).
- Nits: capitalise unknown agent ids, remove unused words, 2 h 5 min.
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