Skip to content

Inbox: replies go into running Qwen Code sessions - #2274

Merged
backnotprop merged 23 commits into
mainfrom
feat/inbox-qwen-wake
Oct 28, 2026
Merged

backnotprop merged 23 commits into
mainfrom
feat/inbox-qwen-wake

Conversation

@backnotprop

@backnotprop backnotprop commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

What

When you reply to a running Qwen Code session, your reply now arrives in that session as its next turn, the same way Codex works today. It is stacked on #2271 (connect Qwen Code and tag its sessions), so please review that one first.

  • Trusted sender. Qwen Code takes a message from another program without review only when that program presents a "controller" token. When Plannotator connects Qwen Code (0.24 or later), it makes one for itself with qwen sessions controllers add. The label is "Plannotator Inbox" plus a short code for the Plannotator data folder, so two data folders on one Qwen home never revoke each other's grant. The token is kept in inbox/qwen-trust.json with mode 0600. The Connected band and the terminal say what it allows: Plannotator can put your replies straight into any running Qwen session, without Qwen asking you first, and Settings → Agents turns it off. No token is made on Windows, where nothing is woken.
  • Turning it off. Settings → Agents first removes the Inbox entry, then records "off", then revokes the grant. If Qwen refuses the revoke, Plannotator drops the token at once (the wake stops), keeps only the grant id, and tries the revoke again at each start until it works. The message names the command to remove it by hand. plannotator uninstall revokes it too. Before Qwen is asked to revoke, the token is taken out of the data folder, so stopping mid-call never leaves it in use. A data folder that cannot be written never stops plannotator uninstall; it says so. A run that starts with a different QWEN_HOME moves no grant: the grant stays with the entry. Turning Qwen Code on in Settings → Agents there moves both, the entry first.
  • Revoked in Qwen. A grant the person revoked with qwen sessions controllers remove is said once and never made again on its own. A grant list in a shape Plannotator does not know counts as unknown, never as revoked. This is checked at each connect run, and also while the Inbox runs: when Qwen stops confirming, the Inbox asks Qwen (at most every five minutes) whether the grant is still there, and drops the token if not.
  • The wake. The new packages/server/inbox-qwen-wake.ts follows the Codex wake.
    • It finds the session in Qwen's session registry ($QWEN_HOME/sessions/) and checks that the process is alive. On Linux it also checks the boot and the pid namespace, read as Qwen records them.
    • It sends one connection with the auth line and a user frame that names the session.
    • Each reply is claimed on disk first, so it is never sent twice, also across restarts.
    • Receipts come back to a socket the Inbox owns. A receipt counts only if it presents the reply token from that frame (compared as bytes). A line that cannot be read closes only that connection. Socket folders left by a crashed Inbox are removed.
    • delivered: "Queued for Qwen Code". held, refused, denied or no receipt: "Saved for its next read", never sent again. misaddressed or a dead process: the session has ended. dropped: tried again later with a new id. A resumed session gets the reply then.
  • Sessions that are not woken fall back to "call wait_for_reply": qwen -p runs, sessions an editor or qwen serve drives, daemon calls, Windows, and sessions whose Qwen settings hold or refuse other programs' messages. Those settings are merged as Qwen merges them: system defaults, the person's, the project's (which can only tighten), then system settings. A value Qwen does not know counts as holding.

Why

The owner asked for full Qwen Code support in the Inbox, including replies going straight to Qwen (2026-10-27). On who creates the token, the owner decided that Plannotator registers itself as Qwen's trusted sender automatically when it connects Qwen. The Connected band and the terminal say so, and the token is revoked on disconnect or uninstall. INBOX_QWEN_TRUST_ON_CONNECT holds that decision, so moving it behind a Settings switch later would be a small change.

Changes for existing users

  • People with Qwen Code 0.24 or later get a "Plannotator Inbox (…)" grant in their Qwen home (peer-controllers.json) the next time plannotator inbox runs. The band and the terminal say so.
  • The token lets Plannotator put messages into any of that person's Qwen sessions. Qwen marks these messages as coming from Plannotator, not from the person.

How it was tested

  • packages/server/inbox-qwen-wake.test.ts (17 tests) uses a fake Qwen session: a socket owned by the test and a registry record in a scratch Qwen home. It covers a reply sent once across sweeps and a restart, delivery at the next tool call, no token, ACP/headless/daemon sessions, settings that hold (system, the person's, the project's, an unknown value) and the merge rules, misaddressed, a dead process then a resume, held then released, no receipt, refused, dropped and retried, a grant revoked mid-run, Qwen Code turned off while Qwen refuses the revoke (the wake stops at once), a malformed receipt, a forged receipt, a crashed Inbox's socket folder, and record liveness with the pid namespace read in another process.
  • packages/server/inbox-qwen-trust.test.ts (27 tests) uses a fake qwen with sessions controllers add/list/remove. It covers the token made once and kept 0600, a Qwen that is too old, Windows, a revoke outside said once and not made again, Settings off (entry first, then revoke), an entry that cannot be removed, a revoke that fails (token dropped, retried each run, revoked before a new mint), a grant list of another shape, a failing mint at turn-on, a run under another QWEN_HOME (no grant moved) and Settings moving both, a grant waiting to be revoked in another home (revoked there at the next run, and before a new mint), the token gone from disk while Qwen is asked, a data folder that cannot be written, no qwen to run, uninstall, an orphan grant, two data folders on one Qwen home, one home spelled two ways (never re-minted, and no QWEN_HOME= in the revoke command), two homes that differ only by case on a case-sensitive disk (the grant is never taken for removed), and PLANNOTATOR_INBOX_CONNECT=0.
  • I mutated the code to check the tests catch it: the receipt-token check, the byte-length compare, the per-line error catch, the revoke check, keeping the token after a failed revoke, reading an odd list as empty, the settings check, the misaddressed handling and the restart claims each make a test fail.
  • The band (unit and DOM), the connect step wording and uninstall have tests too. Uninstall still removes the binary when Qwen refuses and the data folder cannot be written.
  • One Qwen home spelled two ways (a symlink, a trailing slash) keeps its grant: it is never revoked and minted again (tested). A test checks that Settings saves agents.json before it calls qwen, for both on and off.
  • Two rare gaps in how a second Qwen entry is handled after a failed rollback come from Inbox: connect Qwen Code and tag its sessions #2271 and are described there.
  • Known gap, older than these PRs: a connect run reads agents.json at its start and writes it at its end (up to 30 s later), so a Settings switch made in that window can be overwritten. The UI does not disable the switches while a run is in progress. Left as is.
  • Known gap, older than these PRs and the same for every agent: if the Inbox is killed between writing an agent's entry and saving its records at the end of the first connect run, the next run records that entry as found, and uninstall leaves it. Settings now saves the record before it calls qwen.
  • Typecheck is clean. The related server, hook and shared suites and the Inbox DOM tests pass after merging main.
  • The real qwen was never run, and the real ~/.qwen was never read.
  • Not verified without a real Qwen: the real controllers add --json output and --version line, real socket and receipt behaviour, and that Qwen's TUI starts a turn for a queued message at once when idle.

Plannotator registers itself as Qwen Code's trusted sender when it connects
Qwen (qwen sessions controllers add, Qwen 0.24+), keeps the token 0600 in its
data dir, says so in the Connected band, and revokes it when Qwen Code is
turned off in Settings or Plannotator is uninstalled.

The Qwen wake (inbox-qwen-wake.ts, after the Codex wake) finds a live session
in Qwen's registry and sends the reply over its socket: the auth line with the
token and one user frame naming the session, once per item (claim folders),
with Qwen's receipts read on a socket of the Inbox's own. Held, refused or
unconfirmed replies are saved for the next read and never sent twice;
misaddressed or a dead process ends the session; ACP, headless, daemon,
hold/refuse settings and Windows fall back to MCP.
# Conflicts:
#	apps/hook/server/inbox-cli.test.ts
#	apps/hook/server/inbox-mcp-shim.ts
#	docs/agent-guides/inbox-foundation.md
#	packages/server/inbox-agents.ts
#	packages/server/inbox.ts
- Read the pid namespace with stat (the namespace's inode, as Qwen records it);
  the test reads it in another process.
- A grant label unique to each data dir; a grant in another QWEN_HOME is
  revoked there before one is minted here; a grant revoked outside is said once.
- No token on Windows, where nothing is woken.
- Settings off: remove the MCP entry, record off, then revoke; a revoke that
  fails keeps the token and never mints a new one.
- Receipt tokens compared as bytes; a bad receipt line ends only its socket.
- A missing receipt asks Qwen (at most every 5 min) whether the grant is still
  listed; a revoked one drops the token.
- Qwen settings merged as Qwen merges them (system defaults, user, workspace
  tighten-only, system); an unknown inbound value holds.
- Turn-on says a failing mint; the band and terminal say what the grant allows.
- --version answers cached a minute; leftover socket folders cleaned.
# Conflicts:
#	docs/agent-guides/inbox-foundation.md
- Turning Qwen Code off when Qwen refuses the revoke drops the token at once
  (the wake stops) and keeps only the grant id (revoke_pending). Each connect
  run while Qwen Code is not connected retries the revoke; turning it on
  revokes it before minting; uninstall revokes it too. The message names the
  command to remove it by hand.
- A controllers list line that is not an {id, label} object makes the list
  unknown (token kept, retried later), never "revoked".
- Tests: the wake stops after a failed-revoke turn-off; retries until it works;
  a list of another shape.
# Conflicts:
#	apps/hook/server/inbox-command.ts
# Conflicts:
#	packages/inbox/agents.test.ts
#	packages/inbox/agents.ts
#	packages/inbox/components/AgentNoteDialog.tsx
#	packages/server/inbox-agents.ts
- A run under another QWEN_HOME than the entry's moves no grant; turning
  Qwen Code on in Settings there moves the entry, then the grant.
- Turning off (and uninstall) takes the token out of the data folder before
  Qwen is asked to revoke (revoke_pending), so a stop mid-call never leaves
  it in use. A data folder that cannot be written never throws: uninstall
  goes on to the entries and the binary, and says so.
- Settings writes agents.json before it calls qwen (on and off).
- Tests: no grant moved by a run elsewhere; a pending grant in another home
  revoked at the next run and before a mint; the token gone while Qwen is
  asked; an unwritable data folder (trust and uninstall).
# Conflicts:
#	packages/server/inbox-agents.ts
… fold case; test the revoke command's home

Two Qwen homes that differ only by case are two homes on a case-sensitive
volume: a grant in one is never taken for removed from the other. The revoke
command names QWEN_HOME only for another home, never for the same one spelled
another way.
# Conflicts:
#	packages/server/uninstall.test.ts
@backnotprop
backnotprop merged commit 2cc2d2d into main Oct 28, 2026
32 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