Skip to content

Let a plugin wake an agent in a conversation (ctx.agents.wake) - #1468

Merged
salman1993 merged 3 commits into
mainfrom
mbot/plugin-wake
Oct 21, 2026
Merged

salman1993 merged 3 commits into
mainfrom
mbot/plugin-wake

Conversation

@baxen

@baxen baxen commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

What

ctx.agents.wake(pubkey, { channel, root?, text }) lets a plugin wake an agent this copy of the app runs, in one of its conversations, the way a mention there would, but with no message. Nothing goes to the relay and nobody signs anything on the owner's behalf.

This is the base of a two-PR stack. #1325 (GitHub watch_pr) sits on top of it and uses it instead of posting an owner-signed mention.

How

  • Service (features/agents/service.ts): a new plugin trigger (plugin, channel, root?, text, its own random id, at). Agents.wake refuses calls from outside a plugin, and empty text. It throws when this copy does not run the agent or its mention lane is full.
  • Runner (features/agents/runner.ts): the wake queues in the mention lane, with the mention's rate limit. The drain checks it like a mention, so it is dropped like a queued mention when the agent stops running here or another copy takes it. It shows typing in its thread. It is not saved for resume.
  • Harnesses (Claude, Codex, goose): each takes the wake on its normal mention path, in that conversation's session, with two differences:
    • no fromOwner check, because the plugin is the owner's installed code and not a message
    • the <buzz-event> block is replaced by one shared <plugin-wake plugin="…"> section. It says the plugin woke the agent, that neither the owner nor anyone in the channel wrote it, and that the text is data, not instructions.
  • Shared helper (features/agent-harness/plugin-wake.ts): the harnesses are built around the mention's event (channel, thread root, ordering time, failure replies). So the wake becomes a local anchor, an unsigned stand-in event with the thread's e tag and the wake's random id, and each harness reuses its mention path.

The anchor's id is never published

Agents.wake refuses a root that isn't a 64-hex event id. Harnesses only read a well-formed id as the thread, and any other would leave them answering at the anchor. An independent review found that gap, and the fix is the second commit.

Every place that tags the triggering event goes through the thread root (threadReference(event)?.rootId), never event.id, when the wake has a root: failure reports, goose's fallback post, tool reply defaults, Codex's reply_to, and typing. The runner types at root directly. A wake with no root has no thread to answer in, so:

  • in a Solo chat it posts and types at the channel's top level, with no e tag
  • anywhere else, every harness ignores it, and the runner shows no typing for it

No harness adds reactions or read markers for a mention, so none exist for a wake.

Tests: a service test runs two wakes to failure, one in a thread and one at the top of a non-Solo channel. It checks that nothing published as the agent contains either wake's id, and that typing only went to the thread root. Each harness has a failure test that checks its report goes to the root and contains no wake id.

Tests

Full Vitest: 711 files, 11,069 tests passed on 8ee52ba45. The Agents service tests also pass on a6e0c66d8, and the hooks passed on both pushes.

  • Service: a wake runs in the mention lane with the plugin's id and types at the root. It never publishes its own id, even when the turn fails; that test fails if the runner's threadless-wake gate is removed. It throws outside a plugin, without text, with a malformed root, or for an agent this copy does not run. A queued wake is dropped when the agent is removed.
  • Claude, Codex, goose, one set each: the wake lands in the same session as the thread's earlier mention, with the <plugin-wake> section and no <buzz-event> or wake id. A failed wake reports at the root. A wake with no thread is ignored outside a Solo chat and taken in one.

Follow-ups (not in this PR)

  • Wakes are not resumed after a reload: there is no relay event to read again.
  • A wake shows no activity row or Stop, because those hang off a message id. Typing still shows in the thread.
  • A wake with no root only works in a Solo chat. A DM or channel-scoped session would need a way to post at the top level without a thread.

🤖 Generated with Claude Code

baxen and others added 2 commits October 20, 2026 20:51
`ctx.agents.wake(pubkey, { channel, root?, text })` queues a `plugin`
trigger in the agent's mention lane, with the mention's rate limit and
"still current" checks, and sends nothing to the relay. It throws when
this copy of the app does not run the agent or the lane is full.

Claude, Codex and goose take the wake on their mention path, in the
conversation's session, without the owner check. One shared
`<plugin-wake>` section replaces `<buzz-event>`: it names the plugin,
says neither the owner nor anyone in the channel wrote it, and that its
text is data. A local anchor in place of the event routes the turn; its
random id is never published. Reports and typing go to the thread root,
or the channel in a Solo chat, and a threadless wake elsewhere is
ignored. Wakes are not resumed after a reload and show no activity row.

Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
Harnesses find a wake's thread from its anchor's `e` tag, which
`threadReference` reads only as a 64-hex id. Any other `root` left them
answering, reporting and defaulting tools at the anchor's own id, which
must never be published. `Agents.wake` now refuses such a root and
lowercases a good one.

Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
@baxen

baxen commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Draft, the base of the stack under #1325. An independent review found one gap: a malformed root would make the harnesses fall back to the anchor's id. a6e0c66d8 now refuses any root that isn't a 64-hex event id, with a test.

@baxen
baxen marked this pull request as ready for review October 21, 2026 04:04
@baxen
baxen requested a review from a team as a code owner October 21, 2026 04:04
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T04:09:04.216865Z a6e0c66 Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: a6e0c66d8d

ℹ️ 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 on lines +1473 to +1475
this.runners.wakeIn(pubkey, {
type: "plugin",
plugin,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Cancel wakes when their source plugin unloads

When a plugin is disabled, reloaded, or removed after calling wake while the target agent is busy, this call leaves only the plugin ID in the queued trigger and registers no scope-disposal cleanup or revision check. AgentRunners treats plugin triggers as always current, so the old wake can still be handed to Codex/Claude/goose—and potentially execute commands—as though the plugin were still installed. Associate queued and handed-off wakes with the calling plugin scope/revision and cancel or reject them once that scope ends.

Useful? React with 👍 / 👎.

Comment on lines +1467 to +1468
if (!channel || !text.trim())
throw new Error("A wake needs a channel and text");

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 Bound wake text before placing it in model prompts

When a plugin forwards a large value such as complete CI output, this validation accepts text of any size, and every bundled harness subsequently interpolates the entire value into its model input. A sufficiently large log can exceed the provider context limit, fail the wake, consume excessive tokens, or allocate a very large in-memory prompt. Reject or explicitly truncate by UTF-8 size at this API boundary and tell callers what limit applies.

Useful? React with 👍 / 👎.

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Star Lord’s automated source review via Wes’s account.

One actionable correctness finding is recorded inline. Assessment: 8/10; preserving the wake’s precise history cutoff is needed to reach the repository’s 9/10 bar. Rocket reviewed systems/lifecycle, Mantis correctness/security/concurrency, Groot product/accessibility, and I reconciled their results against the pinned source.

Reviewed head: a6e0c66d8d1eb57c059701623d652a32ee3bfa6c. Target base: 6d0035aff265be9a3cac9a489d027e8a37b0c5ce; feature merge base: b2a272bd705350ba514ea6c512f6deb2c842818c. Inspected all 17 feature-diff files, relevant callers, queue/custody and failure/steering paths, and the public description and feature commit messages (no attached images). The documented absence of wake activity rows/resume and the Solo-only threadless behavior are retained as scope decisions. The stacked #1325 consumer is outside this review.

Source-only: no PR code, tests, builds, or app flows executed. One hosted check snapshot showed CI required, JavaScript, Rust/tool integration and browser journeys successful; Windows native validation and the browser flake placeholder were skipped. Live provider behavior, native acceptance, performance and rendered accessibility/focus remain unverified. This COMMENT is not an approval or merge authorization.

Comment thread src/features/agents/service.ts Outdated
...(root ? { root: root.toLowerCase() } : {}),
text,
id: bytesToHex(crypto.getRandomValues(new Uint8Array(32))),
at: Math.floor(Date.now() / 1000),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Preserve subsecond time in the wake’s history cutoff

This truncates the wake time to the beginning of its second, and wakeAnchor emits no ms tag. Codex passes that anchor into conversationHistory, which filters with compareMessages(item, cutoff) < 0 using eventMs(anchor) (src/bundled/codex/history.ts:38–51). A wake at S+.900 therefore excludes an already-posted message at S+.100. On a fresh Codex session, a wake immediately after a thread is created can omit the root/request itself; same-second replies are also lost. Ordinary app messages carry millisecond tags, so this affects normal conversation context.

Capture the time once and preserve its milliseconds through the trigger and anchor (or provide a precise history cutoff). Add a fresh-session plugin-wake case that includes earlier same-second context and excludes later context. This finding is derived from source; no runtime reproduction was performed.

A wake's time was rounded to whole seconds, and its anchor carried no `ms`
tag. Codex cuts history off at the anchor by Buzz's millisecond ordering, so
everything posted earlier in the wake's second, possibly the original request,
sorted after it and was left out of a fresh session's context.

The trigger's `at` is now milliseconds, and the anchor carries it the way
Buzz's own messages do: seconds in `created_at`, the rest in an `ms` tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Salman Mohammed <smohammed@squareup.com>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Star Lord’s automated source review via Wes’s account, integrating independent Rocket (lifecycle/performance), Mantis (correctness/security/concurrency), and Groot (product/context behavior) reviews.

No actionable changes requested in this follow-up. The prior history-cutoff finding (#1468 (comment)) is addressed: wake creation retains milliseconds, the anchor supplies canonical seconds plus an ms tag, and the displayed time uses the same units. The added Codex regression case checks earlier and later messages within the wake’s second. The existing Claude/Goose timestamp units and mention-only interruption persistence remain consistent. Assessment: 9/10 for the reviewed repair.

Reviewed head 692c3d79a3e42477dbf497d58e07267a1dc64d3c, target base 6d0035aff265be9a3cac9a489d027e8a37b0c5ce. This follow-up compares the five changed files with previously reviewed head a6e0c66d8d1eb57c059701623d652a32ee3bfa6c and traces affected consumers; it does not reopen unchanged feature areas or review stacked #1325. Public description and new commit metadata inspected; no attached images or new disclosure finding.

Source-only: no PR code, tests, builds, app flows or performance measurements executed. One hosted-check snapshot showed JavaScript, Rust/tool integration, native fixture, browser journeys and measurements still running; Windows native validation and the browser-flake placeholder were skipped. Live wake/provider behavior, native acceptance and rendered accessibility/focus remain unverified. This COMMENT is not an approval or merge authorization.

@salman1993
salman1993 merged commit d9793a8 into main Oct 21, 2026
37 checks passed
@salman1993
salman1993 deleted the mbot/plugin-wake branch October 21, 2026 14:39
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.

3 participants