pendingyou 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +134 -11
  2. package/README.md +114 -34
  3. package/dist/api.js +27 -50
  4. package/dist/apps/claude-code.js +14 -49
  5. package/dist/apps/codex-app.js +0 -18
  6. package/dist/apps/codex-wake.js +290 -122
  7. package/dist/apps/codex.js +102 -100
  8. package/dist/apps/ids.js +0 -11
  9. package/dist/apps/opencode-plugin.js +91 -67
  10. package/dist/apps/opencode.js +13 -80
  11. package/dist/apps/pi-extension.js +100 -75
  12. package/dist/apps/pi.js +13 -72
  13. package/dist/apps/registry.js +0 -2
  14. package/dist/apps/switch.js +15 -0
  15. package/dist/args.js +72 -41
  16. package/dist/bridge.js +0 -94
  17. package/dist/claude.js +6 -96
  18. package/dist/cli.js +0 -2
  19. package/dist/confirm.js +0 -69
  20. package/dist/credentials.js +0 -27
  21. package/dist/environment.js +0 -28
  22. package/dist/errors.js +0 -1
  23. package/dist/files.js +0 -16
  24. package/dist/format.js +59 -13
  25. package/dist/handed.js +87 -0
  26. package/dist/hold.js +0 -24
  27. package/dist/hooks.js +2 -27
  28. package/dist/index.js +0 -2
  29. package/dist/init.js +0 -32
  30. package/dist/install.js +0 -32
  31. package/dist/io.js +1 -10
  32. package/dist/json.js +0 -29
  33. package/dist/jsonc.js +0 -33
  34. package/dist/main.js +29 -31
  35. package/dist/mod.js +0 -23
  36. package/dist/oauth.js +0 -45
  37. package/dist/permission.js +118 -144
  38. package/dist/pickup.js +113 -63
  39. package/dist/presence.js +330 -0
  40. package/dist/qr.js +0 -28
  41. package/dist/redact.js +0 -77
  42. package/dist/remote.js +0 -90
  43. package/dist/setup.js +0 -30
  44. package/dist/shim.js +0 -23
  45. package/dist/signin.js +0 -42
  46. package/dist/state.js +19 -10
  47. package/dist/stopcheck.js +42 -86
  48. package/dist/toml.js +0 -34
  49. package/dist/version.js +1 -2
  50. package/dist/watch.js +3 -20
  51. package/mod/.claude-plugin/plugin.json +1 -1
  52. package/mod/README.md +2 -2
  53. package/mod/hooks/register.js +142 -61
  54. package/mod/hooks/wake.js +179 -108
  55. package/package.json +2 -1
  56. package/mod/tests/wake.test.ts +0 -247
package/CHANGELOG.md CHANGED
@@ -1,11 +1,134 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.0 (2026-10-05)
4
+
5
+ - Presence: Pending You hears which of your agents' sessions are open right now. Each app tells it with its own
6
+ connection here, `POST <origin>/mcp/cli/presence` with `{"source", "name", "sessionId", "cwd", "state"}` (`name` the
7
+ name the session goes by with Pending You, else null; `cwd` only as `~/…`, else null; `state` `live` or `closed`).
8
+ Pending You counts a session live for 15 minutes after its last `live`, so an open session says so every 5 minutes,
9
+ with no model involved:
10
+ - Claude Code: its session-start hook says live; the wake mod (Claude Code 2.1.287 or later) says live as the
11
+ session starts, after /clear or /resume, and every 5 minutes while it's open, by running the hooks' script; a new
12
+ SessionEnd hook, `presence`, through the same script, says closed. Without the wake mod, the next-message hook
13
+ says live again once the last was 4 minutes ago. Only `init`'s copy of the mod does it, never the plugin's.
14
+ - Codex: its session-start and next-message hooks start a keeper for the thread (`presence --keep`, detached, a lease
15
+ of its own), which says live every 5 minutes while the Codex process the hooks ran under is running (Codex CLI, or
16
+ the Codex app's own codex), and closed once it's gone. A new SessionEnd hook (Codex fires it when a conversation
17
+ is archived or deleted, when Codex closes, or after 30 idle minutes) says closed and stops the keeper; its timeout
18
+ is 3 seconds, the most Codex gives one. Where the Codex process can't be found, the hooks say live as they run.
19
+ - OpenCode's plugin and Pi's extension say live as a session starts (OpenCode: when it's created, or its first
20
+ message) and every 5 minutes while the app runs, and closed when OpenCode deletes the session or goes, or Pi shuts
21
+ the session down.
22
+ A hook only starts the send in the background; a send gives up after 3 seconds; a Pending You from before presence
23
+ answers 404, which is taken quietly. A sign-in that only hears (`--oauth`) sends nothing. Events are written down in
24
+ `~/.config/pendingyou/presence/<app>.json` (ids, states and times), so a late `live` never overrides a newer
25
+ `closed`, and a reload's `live` goes after its `closed`. `status` has a line for each app: “Presence: tells Pending
26
+ You when this session is open”.
27
+ - Codex's permission prompts on a card, as Claude Code's since 0.13.0. `init` adds Codex's PermissionRequest hook (it
28
+ fires when Codex is about to ask for approval: a shell command that needs more than the sandbox gives, an
29
+ `apply_patch`, an MCP tool; `tool_name` Bash, apply_patch or the tool's, and `tool_input.description` when Codex has
30
+ a reason), through the shim with `--app codex`. Codex reads that hook's output as its decision, so it prints nothing
31
+ and exits 0: it declines, and Codex asks as before. Once a prompt has waited 10 seconds, the same detached worker
32
+ posts one card with Codex's own connection: “Codex is waiting for your OK: git push origin main”, “…: edit
33
+ src/app.ts and 1 more” for a patch (the files its `*** Update File:` lines name), “…: create_issue on github” for an
34
+ MCP tool, with Codex's reason (redacted) in the summary, under the name the thread went by on Pending You's card tools,
35
+ else “Codex”, and the same redaction. It's withdrawn when the call runs (a PostToolUse hook for every tool, which
36
+ also settles a prompt for the same tool when Codex gives the call that ran its input in another form), with your
37
+ next message, at the end of the turn, or when the thread ends (SessionEnd). `--no-permission-cards` leaves Codex's
38
+ out too. The Codex app's browser-use prompts run no PermissionRequest hook, so they get no card.
39
+ - Presence keeps to Pending You's contract: the name trimmed to one line of at most 60 characters, a folder over 300
40
+ characters sent as null, a 401 tried once more after a refresh in the background, a 429 waited out once (its
41
+ `retry-after`, 60 seconds) by a send in its own process (never a hook), and 400, 403 and 404 taken quietly; Codex's
42
+ keeper stops once a send is refused, its sign-in is gone, or the server has no presence.
43
+ - A question you handed an agent, when the assistant that asked answers what the helper asked it on the card (or you
44
+ write in to them both, guide 2.30), wakes the helper with those words, not the handed question again: “Pending You:
45
+ billing-webhooks replied on the question handed to you, “Which queue?” (req_…): “It's the jobs queue.”. Call
46
+ get_request (requestId req_…, name “infra”) to read what you've said to each other. If you know now,
47
+ answer_delegated …; if you need more, reply_in_thread to “asker”; if it's your person's to decide, reply_in_thread
48
+ with escalate true; if not, hand_back …” (“your person wrote to you both …” for yours). The same from the hooks, the
49
+ wake mod, OpenCode's plugin and Pi's extension; Codex's `codex queue` leaves the words out of its arguments, where any
50
+ program could read them, and get_request has them. The wake mod now wakes a session for such a reply after it read
51
+ the question itself (before, a question it had read never woke it again), and the Stop check and the hooks know a
52
+ turn such a wake started.
53
+ - Switching an agent's own sign-in to this computer's says what becomes of the old one: once the agent shows up on this
54
+ computer's sign-in, Pending You moves the old sign-in's agents over, with their cards and the questions you delegated
55
+ to them, and signs it out. Where there's no browser (an SSH session) init says what it switched instead of switching
56
+ without a word.
57
+ - Codex runs a hook only once you trust it, so the three new hooks (PermissionRequest, PostToolUse for every tool,
58
+ SessionEnd) need trusting once. They go at the end of their lists, so the ones you trusted stay trusted. `init` says
59
+ so when it adds them to a Codex set up before (“Codex has new Pending You hooks (session end and permission prompts),
60
+ which it skips until you trust them once: …”), and `status` has a line for each, with each hook's trust:
61
+ “Permission prompts: a card when Codex waits for your OK” and “Presence: …”. Neither keeps `status` from saying
62
+ Ready. Codex's record of its hooks (`codex.json`) now names each by its event and command.
63
+
64
+ ## 0.14.0 (2026-10-05)
65
+
66
+ - Questions your person hands one of your agents from another assistant (Delegate: Pending You's card link, or D on the
67
+ desk). Once the delegation's 5-second hold is over, Pending You's `/mcp/cli/answers` brings such a question to the
68
+ app's sign-in, marked `delegated` (who asked, `freely` or `loop`, the person's note), under the name the helper
69
+ answers with (`sessionLabel`, the task the person picked) and in that task's folder (`cwd`). The hooks used to say
70
+ “Pending You: they wrote back on req_…”; now a session start or message says “Pending You: your person handed you
71
+ req_…, a question from billing-webhooks (answer freely: your answer goes straight to billing-webhooks).”, the
72
+ question, “Their note: “…””, and “Call get_request (requestId req_…, name “infra”) to read it. If you know,
73
+ answer_delegated with your answer and how you know; if not, hand_back with what you checked.” (“they’ll see your
74
+ answer first” when the person kept themselves in the loop). The header says “your person handed you a question from
75
+ another assistant” (or “answered, and handed you …”), never “your person answered”, and the line to tell the person
76
+ first is “You handed me a question from …”.
77
+ - Which session hears one (`src/handed.ts`), the same for every app: a session that goes by its name in its folder
78
+ (the task the person picked; the most recent of several), else the session in that folder that used Pending You
79
+ most recently, never the one that asked it (a session that goes by the asking assistant's name), and never two: the
80
+ session that takes it claims it first in `state.json` (`claimHanded`), and gives it back if it couldn't be told. A
81
+ question with no folder (a task that never said one) goes to a session by its name anywhere, else the app's most
82
+ recent. A sign-in that only hears (`--oauth`) hears every computer's questions, so it leaves one whose `machine`
83
+ (the computer Pending You says it went to, as the Assistants page names it) is another computer than this one, by
84
+ its name or its hostname (the hostname, the names its code sign-ins were given, a Mac's own name, and, outside a
85
+ hook, Tailscale's); one with no `machine` goes by the folder. The hooks hand a session one at once when it goes by that name (or it's the session the listeners would
86
+ wake); another session in the folder only after 5 minutes, so the one it went to hears it first.
87
+ - Claude Code's wake mod wakes the session it's for: while a session used Pending You in the last 12 hours (as long as
88
+ Pending You offers its task as running), it asks `list_pending` with the session's name every minute for the first
89
+ half hour, then every 3 minutes; reads each new one with `get_request` (who asked, how its answer travels, whether
90
+ there's a note); and starts one turn, “Pending You: your person handed you “…” (req_…), a question from … Call
91
+ get_request (requestId req_…, name “infra”) to read it and their note. If you know, answer_delegated …; if not,
92
+ hand_back …”, never with the note. It claims each in its store (`handed:<requestId>`), so another session by the same
93
+ name doesn't hear it too, and never wakes a session for one it read or answered itself. Among sessions by one name,
94
+ the one working in the folder of the task it went to (`list_pending`'s `cwd`: the folder the session started in, or
95
+ one it gave its own cards, `~/…` or full) takes it, one elsewhere never does, and one that can't tell its folder
96
+ waits 5 minutes. The hand-off hook that
97
+ runs for the mod's prompt claims it in `state.json` without saying it twice. `answer_delegated` and `hand_back` are
98
+ among the calls it reads. Regenerated into the plugin's builds too, which reach installed plugins with the next guide
99
+ release.
100
+ - Codex: one listener for the app, `pendingyou listen --app codex --handed` (detached, a lease of its own), started by
101
+ the hooks while any thread may be handed a question, `codex queue`s each into the thread it's for, in the wake mod's
102
+ words, and gives it back (forgetting the thread) when Codex doesn't know the thread. Codex's hooks now remember each
103
+ thread's names and folder (`codex-threads.json`), and keep a thread for 12 hours after it last used the card tools,
104
+ with no card of its own waiting; a session start or message marks it active. A thread's own listener is never woken
105
+ for its own card while it's with another assistant.
106
+ - OpenCode and Pi: a session's listener also wakes it for a question handed to it, and keeps listening for one for 12
107
+ hours after the session last used the card tools (`opencode-threads.json`, `pi-threads.json`), while the app is
108
+ open. OpenCode's plugin starts it with each message while the record says so, and passes the session's folder to
109
+ `posted`; Pi's extension starts it as before, now for that too. Both start a listener again when a card call asked for
110
+ one while the last was on its way out: a card posted just as a session's first listener stopped (finding nothing to
111
+ listen for) went unheard until the next message.
112
+ - `watch` names a handed question's event `delegated_ready` (`PENDINGYOU_EVENT`), with `delegated` in the JSON on
113
+ stdin.
114
+ - The Stop check never asks a helper to post the question as a card of its own. `answer_delegated` and `hand_back`
115
+ count as the turn's card calls (Claude Code's transcript, OpenCode's turn, Pi's extension); a turn a handed
116
+ question's wake started (its first message the wake's words: Claude Code's transcript, OpenCode's turn, Pi's extension
117
+ says `handed`, Codex's next-message hook records it when Codex gives the message) that left the person something in
118
+ chat without either is asked once: “Your person handed you that question to answer, not to ask them back:
119
+ answer_delegated if you know, or hand_back with what you checked, unless you already did. Never post it as a card of
120
+ your own.” Codex's PostToolUse hook is trusted by its exact matcher, which names only the four card tools, so it never
121
+ sees those two calls (a new matcher would have to be trusted again).
122
+ - The package names no one, and carries only what runs. Its JavaScript is built without the source's comments, and so is
123
+ the wake mod's (`mod/hooks`); its examples and this changelog use made-up names (`--name build-01`); and the mod's
124
+ test, which only `claude plugin test` reads, stays out of the package. Nothing the command line does changes.
125
+
3
126
  ## 0.13.0 (2026-10-04)
4
127
 
5
- - Claude Code's permission prompts on a card (Darin, 2026-10-04: "Tell me: a card and a push, cleared once I answer in
6
- the terminal"). A Claude Code agent kept asking to remove files and sat waiting until Darin happened to look. `init`
7
- now adds Claude Code's PermissionRequest hook (https://code.claude.com/docs/en/hooks: it runs as Claude Code is about
8
- to ask for permission to use a tool, with `tool_name` and `tool_input`), through the shim with `--app claude-code`.
128
+ - Claude Code's permission prompts on a card: a card and a push, cleared once you answer in the terminal. A Claude Code
129
+ agent kept asking to remove files and sat waiting until its person happened to look. `init` now adds Claude Code's
130
+ PermissionRequest hook (https://code.claude.com/docs/en/hooks: it runs as Claude Code is about to ask for permission
131
+ to use a tool, with `tool_name` and `tool_input`), through the shim with `--app claude-code`.
9
132
  It never decides: it prints nothing, so the prompt shows exactly as before, and it's done in about a tenth of a
10
133
  second. It writes the prompt down (its card's redacted words, never the tool's input) in the session's own file,
11
134
  `~/.config/pendingyou/permission-cards/<session>.json`, and starts that session's one worker, detached (`pendingyou
@@ -67,8 +190,8 @@
67
190
  approve: 1. whoami with your name. If it says app.setUp is true, another session did this: stop. 2. report_setup with
68
191
  source codex, your name, skillSaved true, hears instant, and skillVersion set to whoami’s skill.latest. Its answer
69
192
  has a testAreaId. 3. post_request a short test question in that testAreaId, tell your person it’s on its way, and end
70
- your turn: you’re woken when they answer, so don’t ask them to type anything.” Pi's first setup on Darin's devbox
71
- spent about 20,000 tokens on the words before: “whoami with your name and guide true” answered with the whole guide
193
+ your turn: you’re woken when they answer, so don’t ask them to type anything.” Pi's first live setup spent about
194
+ 20,000 tokens on the words before: “whoami with your name and guide true” answered with the whole guide
72
195
  (87,000 characters), which Pi cut into a file the model then parsed, and “your test question in its testAreaId” sent
73
196
  it looking for testAreaId in whoami's answer, where it isn't (report_setup's has it). A setup that has reported gets
74
197
  the same calls without whoami's way out (its whoami says app.setUp is true); a test card answered says who picks it
@@ -144,7 +267,7 @@
144
267
 
145
268
  ## 0.11.0
146
269
 
147
- - One step for the coding agents on a computer (Darin, 2026-10-04): `npx -y pendingyou@latest init` finds Claude Code
270
+ - One step for the coding agents on a computer: `npx -y pendingyou@latest init` finds Claude Code
148
271
  and Codex (`--app claude-code,codex` names some), says what it found and which it can't set up (an agent too old for
149
272
  it is named, with the version it needs), and sets each up with one approval. Then: restart the agent, and its first
150
273
  session finishes setting up by itself (whoami, report_setup, a test card). Asked for by name and not installed, or
@@ -222,10 +345,10 @@
222
345
  - The next-message hook looks for this folder's answers with every message, not only while a hold had left an entry
223
346
  behind: one request, as before, given up after 2 seconds, inside the hook's 3-second deadline. `state.json` no
224
347
  longer keeps holds' waits.
225
- - A computer with no browser (a devbox or server you SSH into): one code approved on your phone sets Claude Code up.
348
+ - A computer with no browser (a server you SSH into): one code approved on your phone sets Claude Code up.
226
349
  `/mcp`'s Authenticate opened a browser on that machine, which nobody could reach, and `login --device` only signed
227
350
  this command in. Now `init` there (an SSH session, or Linux with no display; `--device` forces it, `--browser` the
228
- browser) signs the machine in by code as a Claude Code connection of its own ("Claude Code on devbox" on your
351
+ browser) signs the machine in by code as a Claude Code connection of its own ("Claude Code on build-01" on your
229
352
  Assistants page, named by `--name`, else Tailscale's name for it, else its hostname; `login --name` with a new name
230
353
  renames the connection with one more approval, which `login` suggests for a cloud's own name like `ip-10-42-1-252`;
231
354
  QR code of the link at a terminal), then adds the MCP server with a `headersHelper` instead of OAuth
@@ -239,8 +362,8 @@
239
362
  PATH), each with the command that fixes it, `--origin` included off production. `uninstall` takes the helper out and
240
363
  says the connection stays on your Assistants page. Needs Pending You's Worker from the same release.
241
364
  - `login` over SSH signs in by code without `--device`. A sign-in by code names the computer, and the phone asks "Let
242
- devbox hear your answers for the assistants running on it?"; in the browser, "Let this computer hear…". Neither offers
243
- a choice of assistant any more (Darin, 2026-10-04): a computer hears for the assistants running on it. A sign-in
365
+ build-01 hear your answers for the assistants running on it?"; in the browser, "Let this computer hear…". Neither
366
+ offers a choice of assistant any more: a computer hears for the assistants running on it. A sign-in
244
367
  pointed at one assistant before then keeps hearing it, so `watch` for a Muse or another app can't be set up anew.
245
368
  - Routing by sender: the hooks and `hold` ask Pending You for Claude Code's cards only (`source=claude-code`), so a card
246
369
  Codex or another app posted from the same folder never reaches a Claude Code session, even with a sign-in pointed at
package/README.md CHANGED
@@ -24,10 +24,12 @@ Run it once on each computer, in a terminal. It finds the agents on the computer
24
24
  Pi reads Codex's, else `~/.pi/agent/skills/pendingyou/SKILL.md`),
25
25
  - adds the hooks that hand it your answers: a new session picks up answers that arrived while nothing was running,
26
26
  your next message brings a waiting answer with it, and a check when it finishes a turn catches what it left you only
27
- in chat ([below](#nothing-left-only-in-chat)); and, for Claude Code, a card when it waits for your OK to run something
28
- ([below](#when-claude-code-waits-for-your-ok)),
27
+ in chat ([below](#nothing-left-only-in-chat)); for Claude Code and Codex, a card when it waits for your OK to run
28
+ something ([below](#when-claude-code-waits-for-your-ok)); and they tell Pending You which sessions are open
29
+ ([below](#which-sessions-are-open)),
29
30
  - and wakes it when you answer: Claude Code 2.1.287 or later through the wake mod, Codex through `codex queue`, OpenCode
30
- through its plugin and Pi through its extension (which also do the hooks' work there).
31
+ through its plugin and Pi through its extension (which also do the hooks' work there); and when you hand it a
32
+ question from another of your assistants ([below](#when-you-hand-an-agent-a-question)).
31
33
 
32
34
  Then it says what to do next, and the one sentence to say if it's needed:
33
35
 
@@ -53,10 +55,11 @@ in each, every other byte as it was), and running it again changes nothing it do
53
55
  **A pendingyou server that's there already** is decided on before anyone signs in. One at another Pending You
54
56
  (staging, say) is named and replaced only once you say so (`--yes` replaces it without asking); keep it and that agent
55
57
  is left as it is. One at this Pending You that signs in by itself (Claude Code's `/mcp` → Authenticate, Codex's
56
- `codex mcp login`, or the Pending You plugin for Claude) is moved to this computer's sign-in: without asking where
57
- there's no browser (it can't finish there anyway), on Yes (the default) at a terminal, or with `--yes`; with nobody to
58
- ask it's kept, and `init` prints the command that switches it. The connection it had stays on your Assistants page for
59
- you to remove, and answers to the cards it asked through that one won't reach the agent by themselves. A kept plugin
58
+ `codex mcp login`, or the Pending You plugin for Claude) is moved to this computer's sign-in: where there's no browser
59
+ (it can't finish there anyway) without asking, saying what it switched; on Yes (the default) at a terminal; or with
60
+ `--yes`; with nobody to ask it's kept, and `init` prints the command that switches it. Once the agent shows up on this
61
+ computer's sign-in, Pending You moves the old sign-in's agents over to it, with their cards and the questions you
62
+ delegated to them, and signs the old one out. A kept plugin
60
63
  carries the skill, so none is saved. `--oauth` keeps every agent signing in by itself, as before 0.11.0.
61
64
 
62
65
  The hooks run a copy of this version kept in `~/.config/pendingyou/cli/<version>/`, through a small script with a path
@@ -93,10 +96,13 @@ http_headers_helper = "/Users/ann/.config/pendingyou/bin/pendingyou-mcp-headers
93
96
  default_tools_approval_mode = "approve"
94
97
  ```
95
98
 
96
- and four hooks in `~/.codex/hooks.json`: a session start, each message, Pending You's card tools (so the Stop check
97
- knows a turn posted, and so the thread is woken), and the Stop check. **Codex runs a hook only once you trust it**: in
98
- Codex CLI, start `codex` and choose “Trust all and continue” (or trust them in `/hooks`); in the Codex app, click
99
- Review hooks › Allow all (or Settings › Hooks › Trust all). `status` says which are trusted. A `pendingyou` server or
99
+ and hooks in `~/.codex/hooks.json`: a session start, each message, Pending You's card tools (so the Stop check knows a
100
+ turn posted, and so the thread is woken), and the Stop check; and (0.15.0) Codex's permission prompt and every tool
101
+ call that ran (a card when Codex waits for your OK, [below](#when-claude-code-waits-for-your-ok)) and the session's end
102
+ ([below](#which-sessions-are-open)). **Codex runs a hook only once you trust it**: in Codex CLI, start `codex` and
103
+ choose “Trust all and continue” (or trust them in `/hooks`); in the Codex app, click Review hooks › Allow all (or
104
+ Settings › Hooks › Trust all). `status` says which are trusted. Upgrading from 0.14.0 adds three hooks, which Codex
105
+ skips until you trust them once; `init` says so, and the ones you trusted stay trusted. A `pendingyou` server or
100
106
  hooks Codex's `/import` copied from Claude Code (without its sign-in) are replaced.
101
107
 
102
108
  **The Codex app** (on a Mac, in the ChatGPT app) shares all of this with Codex CLI: the same `~/.codex`, server, hooks
@@ -111,7 +117,8 @@ it with `codex queue`: “Pending You: your person answered “…” (req_…).
111
117
  answer itself never goes in a command's arguments, where other programs could see it). Codex starts a turn at
112
118
  once when the thread is open and idle, after the current one when it's working, or when you next open it. In the Codex
113
119
  app, a chat it has open is woken while the app runs (one it let go, idle 3 hours or behind 10 newer idle chats, when
114
- you open it again).
120
+ you open it again). One more listener, for the whole app, queues a question you hand a thread from another assistant
121
+ ([below](#when-you-hand-an-agent-a-question)).
115
122
 
116
123
  ## OpenCode
117
124
 
@@ -139,7 +146,8 @@ does in OpenCode what the hooks do elsewhere, through the same hooks' script: ea
139
146
  reminder and any answer waiting (hidden in OpenCode's view, read by the model), a session's first message also says
140
147
  what setup has left, a session that left you a question in chat is asked once to put it on a card (with a toast saying
141
148
  why), and when you answer a card a session posted, the plugin starts a turn in that session: “Pending You: your person
142
- answered “…” (req_…). Call get_request …” (never the answer itself). That works while OpenCode is open, idle or busy.
149
+ answered “…” (req_…). Call get_request …” (never the answer itself). That works while OpenCode is open, idle or busy,
150
+ and for a question you hand the session from another assistant too ([below](#when-you-hand-an-agent-a-question)).
143
151
 
144
152
  Then restart OpenCode (`/exit`, then `opencode --continue`): it finishes setting up the first time you write to it.
145
153
 
@@ -167,7 +175,9 @@ Pi has no hooks, so `init` gives it an extension: `~/.config/pendingyou/pi/pendi
167
175
  the same hooks' script as a session starts, with each message, after Pending You's card tools and as a run ends (a run
168
176
  that leaves you something only in chat goes on once), and when you answer a card a session posted it starts a turn in
169
177
  that session, idle or busy: “Pending You: your person answered “…” (req_…). Call get_request …” (never the answer
170
- itself). In Pi's terminal the first session finishes setting up by itself, once Pending You's tools are there.
178
+ itself), and the same for a question you hand it from another assistant
179
+ ([below](#when-you-hand-an-agent-a-question)). In Pi's terminal the first session finishes setting up by itself, once
180
+ Pending You's tools are there.
171
181
 
172
182
  Then run `/reload` in each Pi session that's open, or start `pi`. Pi's shell waits for a command to end, so in Pi
173
183
  `npx pendingyou hold` answers at once instead of waiting.
@@ -208,6 +218,13 @@ replied on a card comes from Codex's own hook for Pending You's tools in that tu
208
218
  action card titled “Confirm in Codex on <computer>: <step>” when it asked you to type something there. OpenCode's plugin
209
219
  and Pi's extension (0.12.0) do the same in theirs.
210
220
 
221
+ A question you hand an agent from another assistant (0.14.0) is never asked to go on a card of its own: answering it
222
+ (`answer_delegated`) or giving it back (`hand_back`) puts it in front of you, so it counts as the turn's card call. A
223
+ turn that such a question started, and that left you something in chat without doing either, is asked once to answer
224
+ it or hand it back instead (about 45 tokens). Codex's hook for Pending You's tools is trusted by its exact list of
225
+ tools, which doesn't name those two, so there the check knows only that the turn started with one (when Codex gives the
226
+ message to its next-message hook), and says “unless you already did”.
227
+
211
228
  **On 0.2.0 with another origin?** Its hooks blocked every message (`There's no "--origin" command`). Run
212
229
  `npx pendingyou@latest init --origin <url>` again to repair them; see [CHANGELOG.md](CHANGELOG.md).
213
230
 
@@ -233,6 +250,32 @@ It needs Claude Code 2.1.119 or later, signing in through this computer's sign-i
233
250
  Windows yet. Don't want it? `npx pendingyou init --no-permission-cards` (later runs keep it off; `--permission-cards`
234
251
  turns it back on). `npx pendingyou status` says which.
235
252
 
253
+ **Codex** (0.15.0) gets the same: when Codex stops to ask for your approval (a command that needs more than its sandbox
254
+ gives, a patch, an MCP tool) and you don't answer within 10 seconds, a card says “Codex is waiting for your OK: git push
255
+ origin main” (“…: edit src/app.ts and 1 more” for a patch), with Codex's reason for asking, blanked like the rest. Its
256
+ PermissionRequest hook prints nothing, so Codex asks exactly as before; the card goes once the call runs, you write to
257
+ Codex, the turn ends or the thread closes. It needs Codex signing in through this computer's sign-in, and trusting the
258
+ new hooks once. The Codex app's browser-use prompts don't run that hook, so they get no card.
259
+
260
+ ## Which sessions are open
261
+
262
+ Since 0.15.0 Pending You hears which of your agents' sessions are open right now, so it can say so and offer an open
263
+ one a question. Each app tells it with its own connection here: the session's id, the name it goes by with Pending You,
264
+ its folder (under `~` only) and `live` or `closed`. Pending You counts a session open for 15 minutes after its last
265
+ `live`, so an open session says so every 5 minutes, with no model involved:
266
+
267
+ - **Claude Code**: as a session starts (its session-start hook, and the wake mod), then every 5 minutes from the wake
268
+ mod while it's open (without the mod, with your messages); closed as it ends (a SessionEnd hook).
269
+ - **Codex**: as a thread starts or you write to it, a small process in the background says so every 5 minutes while
270
+ Codex (or the Codex app) runs, and closed once it's gone; its SessionEnd hook (a conversation archived or closed, or
271
+ idle 30 minutes) says closed.
272
+ - **OpenCode and Pi**: the plugin and the extension, as a session starts and every 5 minutes while the app runs, and
273
+ closed as it goes.
274
+
275
+ It's never in your way: the hooks only start it in the background, each send gives up after 3 seconds, and a Pending
276
+ You from before presence is taken quietly. A sign-in that only hears (`--oauth`) sends nothing. `npx pendingyou status`
277
+ has a Presence line for each agent.
278
+
236
279
  ## How Claude Code hears your answer
237
280
 
238
281
  **Claude Code 2.1.287 or later: the wake mod.** There's nothing to run. In each session the mod learns the cards
@@ -269,10 +312,38 @@ allowed and the server has connected). When it isn't, it starts one turn once th
269
312
  Pending You: it’s connected here already.” and exactly what to do. Once per session, in at most three sessions, never
270
313
  two within ten minutes, and never again once Pending You says it's set up.
271
314
 
315
+ ## When you hand an agent a question
316
+
317
+ In Pending You you can hand a card to another of your assistants, a coding task included (Delegate: the card's link,
318
+ or D on the desk): it answers for you (“Answer freely”), or its answer comes back to you first (“Keep me in the loop”),
319
+ or it hands the card back with what it checked. Since 0.14.0 the task you picked hears it the way it hears your
320
+ answers, once the 5-second window to undo is over:
321
+
322
+ - **Which session**: the one that goes by the task's name in its folder (the most recent, when several do); else the
323
+ session in that folder that used Pending You most recently; never the one that asked, and never two. A sign-in that
324
+ only hears (`--oauth`) hears every computer's, so it leaves one Pending You says went to another computer (by its
325
+ name or its hostname); one that names no computer goes by the folder.
326
+ - **Claude Code**: the wake mod looks for questions handed to the session's name (`list_pending`, every minute while
327
+ the session used Pending You in the last half hour, then every 3 minutes, for 12 hours), reads each with
328
+ `get_request`, and starts a turn: “Pending You: your person handed you “Which AWS account for staging?” (req_…), a
329
+ question from billing-webhooks (answer freely: your answer goes straight to billing-webhooks). Call get_request
330
+ (requestId req_…, name “infra”) to read it and their note. If you know, answer_delegated with your answer and how you
331
+ know; if not, hand_back with what you checked.” Among sessions by one name, the one working in the task's folder
332
+ takes it (as `list_pending` gives it, `~/…` or a full path), one in another folder never does, and one that can't
333
+ tell its folder waits 5 minutes.
334
+ - **Codex**: one listener for the app queues it into the thread it's for (`codex queue`, the same words).
335
+ - **OpenCode and Pi**: the session's listener starts the turn through the plugin or extension; a session that used
336
+ Pending You's card tools keeps listening for 12 hours, while the app is open.
337
+ - **Otherwise**: a session start or your next message in that folder brings it, with your note: at once in the
338
+ session that goes by the task's name, after 5 minutes in any other.
339
+
340
+ Your note never goes in a program's arguments, where any other program on the computer could read it: the agent reads
341
+ it with `get_request`.
342
+
272
343
  ## A computer with no browser
273
344
 
274
- On a devbox or server you reach over SSH, an agent's own sign-in opens a browser on that machine, which you can't
275
- reach. `init` signs in with a code there instead: SSH in, and at the prompt there run
345
+ On a computer you reach over SSH (a build server, a machine in the cloud), an agent's own sign-in opens a browser on
346
+ that machine, which you can't reach. `init` signs in with a code there instead: SSH in, and at the prompt there run
276
347
 
277
348
  ```sh
278
349
  npx -y pendingyou@latest init
@@ -289,15 +360,15 @@ hooks find Node anyway when the agents run without your shell's PATH (a systemd
289
360
  Remote Control): on PATH, nvm's default, the one `init` ran with, or where Node usually is.
290
361
 
291
362
  The machine is called by its Tailscale name when Tailscale is installed and running (as `tailscale status` shows it),
292
- else by its hostname, or by `--name devbox` on `init` or `login`; a Mac by its own name (Sharing in System Settings). A
293
- cloud's own hostname (AWS's `ip-10-42-1-252`) is kept when there's nothing better, and `login` says how to change it:
363
+ else by its hostname, or by `--name build-01` on `init` or `login`; a Mac by its own name (Sharing in System Settings).
364
+ A cloud's own hostname (AWS's `ip-10-42-1-252`) is kept when there's nothing better, and `login` says how to change it:
294
365
  `npx pendingyou login --name <name>` asks for one more approval and renames the connection, which stays the same one.
295
366
  If an agent's sign-in ends (30 days, or you remove it on your Assistants page), `npx pendingyou login` (`--app codex`
296
367
  for Codex's) approves a new code and keeps the same connection.
297
368
 
298
369
  Only want this command's own sign-in (for `watch`, below)? `npx pendingyou login --device` signs it in with a code,
299
- hearing answers only, and adds no connection. Your phone asks "Let devbox hear your answers for the assistants running
300
- on it?", with no choice of assistant.
370
+ hearing answers only, and adds no connection. Your phone asks "Let build-01 hear your answers for the assistants
371
+ running on it?", with no choice of assistant.
301
372
 
302
373
  ## Always-on scripts
303
374
 
@@ -309,10 +380,11 @@ npx pendingyou watch -- ./on-answer.sh
309
380
 
310
381
  It hears your Claude Code assistants' answers. Signing in asks no choice of assistant (since 2026-10-04: "Let pi hear
311
382
  your answers for the assistants running on it?"); a sign-in pointed at one assistant before then keeps hearing that
312
- one. The command runs without a shell, once per answer, message or fallback that comes due, oldest first. It
313
- gets the request as JSON on stdin, and `PENDINGYOU_REQUEST_ID`, `PENDINGYOU_EVENT` (`answer_ready`, `message_ready` or
314
- `fallback_due`), `PENDINGYOU_VERSION` and `PENDINGYOU_ORIGIN` in its environment. A failing command is tried twice
315
- more, then skipped. `--once` stops after the first.
383
+ one. The command runs without a shell, once per answer, message or fallback that comes due, or (0.14.0) question you
384
+ hand the assistant from another one, oldest first. It gets the request as JSON on stdin (a handed question with
385
+ `delegated`: who asked, how its answer travels, your note), and `PENDINGYOU_REQUEST_ID`, `PENDINGYOU_EVENT`
386
+ (`answer_ready`, `message_ready`, `fallback_due` or `delegated_ready`), `PENDINGYOU_VERSION` and `PENDINGYOU_ORIGIN` in
387
+ its environment. A failing command is tried twice more, then skipped. `--once` stops after the first.
316
388
 
317
389
  ## Commands
318
390
 
@@ -345,24 +417,32 @@ Pending You (Claude Code says which server it's for; Codex's is the one `init` w
345
417
  sign-in each agent's connection used, so signing in again keeps it.
346
418
 
347
419
  `state.json` beside them remembers which answers were already handed over (ids only) and how many session starts
348
- reminded an unfinished setup, and when; `permission-cards/` each Claude Code session's prompts still waiting on your OK
349
- and its card (ids, hashes and the card's redacted words: never what the tool was given), gone once nothing waits;
420
+ reminded an unfinished setup, and when; `permission-cards/` each Claude Code session's (and Codex thread's) prompts
421
+ still waiting on your OK and its card (ids, hashes and the card's redacted words: never what the tool was given), gone once nothing waits;
422
+ `presence/` the last presence each session said (ids, states and times), and Codex threads' keepers;
350
423
  `claude-code.json`, `codex.json`, `opencode.json` and `pi.json` what `init`
351
424
  added; `codex-threads.json`, `opencode-threads.json` and `pi-threads.json` which cards each Codex thread or OpenCode or
352
- Pi session waits on (ids, names and titles: never an answer); `pi/` Pi's extension; and `cli/` the hooks' copy of the
425
+ Pi session waits on, and (0.14.0) the names it went by and its folder, kept 12 hours for a question you may hand it
426
+ (ids, names, folders and titles: never an answer or a note); `pi/` Pi's extension; and `cli/` the hooks' copy of the
353
427
  command line. `init`, `status` and the hooks read `~/.claude.json` (and a folder's `.mcp.json`, and
354
428
  `~/.claude/settings.json`'s enabled plugins), `~/.codex/config.toml`, OpenCode's config and Pi's `mcp.json` only to see
355
- whether the agent has a `pendingyou` MCP server and where it points. For a permission prompt's card, the worker reads
429
+ whether the agent has a `pendingyou` MCP server and where it points. With a sign-in that only hears, the hooks and
430
+ listeners tell this computer's handed questions from another's by its names: its hostname, the names its sign-ins by
431
+ code were given, a Mac's own name (`scutil --get ComputerName`) and, outside a hook, Tailscale's (`tailscale status`). For a permission prompt's card, the worker reads
356
432
  the end of the Claude Code session's transcript only for the name the session gave Pending You, and `git remote
357
- get-url origin` in its folder (sent without any user name or password in it). Nothing is sent anywhere but Pending
358
- You. OpenCode's plugin reads its session's messages only when it goes idle, for the check that
433
+ get-url origin` in its folder (sent without any user name or password in it). To tell when a Codex thread's Codex has quit, its
434
+ keeper asks `ps` about the Codex process its hooks ran under (its id and program name, nothing else). Nothing is sent
435
+ anywhere but Pending You. OpenCode's plugin reads its session's messages only when it goes idle, for the check that
359
436
  nothing was left in chat, and hands the hooks' script just that turn's words and tool names.
360
437
 
361
438
  The wake mod runs inside Claude Code and reaches Pending You only through the session's own connection, with
362
- `get_request` on the session's own cards (and, in `init`'s copy, `whoami` to see whether setup is finished). It keeps
363
- each session's cards (id, title, the agent's name, where each stands) in Claude Code's store for mods,
364
- `~/.claude/plugins/store/`, and forgets them once they close or after 7 days. It marks the copy acting for a session in
365
- that session's environment (`PENDINGYOU_WAKE_OWNER`).
439
+ `get_request` on the session's own cards (and, in `init`'s copy, `whoami` to see whether setup is finished), and (0.14.0)
440
+ `list_pending` with the session's name and `get_request` on a question you handed it. It keeps each session's cards (id,
441
+ title, the agent's name, where each stands), and which handed questions a session was told of (ids and when), in Claude
442
+ Code's store for mods, `~/.claude/plugins/store/`, and forgets them once they close or after 7 days. It marks the copy
443
+ acting for a session in that session's environment (`PENDINGYOU_WAKE_OWNER`). `init`'s copy (0.15.0) also reads
444
+ `~/.config/pendingyou/claude-code.json` for the Pending You it's for, and runs the hooks' script to say the session is
445
+ open.
366
446
 
367
447
  ## Development
368
448
 
package/dist/api.js CHANGED
@@ -1,32 +1,11 @@
1
- // Calls to Pending You with the command line's sign-in: a fresh access token (refreshed under the credentials lock,
2
- // so a hold and a hook never spend the same refresh token), one retry after a 401, and plain errors.
3
- //
4
- // Hooks never refresh in their own process. A hook has a hard deadline (main.ts), and a refresh cut off halfway can
5
- // end the sign-in: the server rotates the refresh token as it answers, and keeps exactly one earlier token valid
6
- // (@cloudflare/workers-oauth-provider's previousRefreshTokenId). A refresh abandoned before its answer arrives is
7
- // harmless on its own (the stored token is that earlier one, and still works), but if the server finishes it after a
8
- // later refresh has stored a newer token, that newer token stops working and the next refresh gets invalid_grant. So
9
- // a hook that needs a new token starts `pendingyou refresh` detached (background), which runs to the end under the
10
- // lock whatever happens to the hook, and waits for it only briefly; until it lands the hook uses the token it has, or
11
- // gives up and prints its plain fallback.
12
- //
13
- // Since 0.11.0 each app has its own sign-in (credentials.ts), and every call names the app it's for: its sign-in is
14
- // the one used, and a session's request names it as `source` (routing by sender, 2026-10-04), so Pending You returns
15
- // only that app's cards, never one Codex posted from the same folder to a Claude Code session there. `watch` names no
16
- // app: it hands answers to a script, not a session, with Claude Code's sign-in as before.
17
1
  import { DEFAULT_APP } from "./apps/ids.js";
18
2
  import { credentialsPath, readCredential, updateCredential, } from "./credentials.js";
19
3
  import { isLocked } from "./files.js";
20
4
  import { OAuthError, refresh } from "./oauth.js";
21
- /** Refresh this long before the access token runs out. */
22
5
  const EARLY_MS = 60_000;
23
- /** A hook starts a background refresh this long before the access token runs out, and goes on with the old one. */
24
6
  export const SOON_MS = 5 * 60_000;
25
- /** How long a hook waits for a background refresh when it has no working token. */
26
7
  export const REFRESH_WAIT_MS = 1200;
27
- /** An access token with less than this left isn't worth sending. */
28
8
  const USABLE_MS = 10_000;
29
- /** Not signed in on this computer (for this origin), or the sign-in ended: the person runs `pendingyou login`. */
30
9
  export class SignInNeeded extends Error {
31
10
  name = 'SignInNeeded';
32
11
  ended;
@@ -35,26 +14,19 @@ export class SignInNeeded extends Error {
35
14
  this.ended = ended;
36
15
  }
37
16
  }
38
- /** Pending You couldn't be reached, or had a problem: worth trying again later. */
39
17
  export class Unavailable extends Error {
40
18
  name = 'Unavailable';
41
- /** Seconds the server asked to wait, when it said. */
42
19
  retryAfter;
43
20
  constructor(message, retryAfter) {
44
21
  super(message);
45
22
  this.retryAfter = retryAfter;
46
23
  }
47
24
  }
48
- /**
49
- * Refreshes the sign-in under the lock unless it no longer needs it: a token with more than `earlyMs` left (or, when
50
- * `force`d after a 401, a refresh token another process already replaced) is used as it is.
51
- */
52
25
  async function refreshStored(io, origin, stored, options, app) {
53
26
  let ended = false;
54
27
  const updated = await updateCredential(io, origin, async (current) => {
55
28
  if (!current)
56
29
  return null;
57
- // Another process may have refreshed while this one waited for the lock.
58
30
  if (current.refreshToken !== stored.refreshToken && current.expiresAt - EARLY_MS > io.now())
59
31
  return current;
60
32
  if (!options.force && current.expiresAt - options.earlyMs > io.now())
@@ -84,7 +56,6 @@ async function freshToken(io, origin, force, app) {
84
56
  return stored.accessToken;
85
57
  return (await refreshStored(io, origin, stored, { force, earlyMs: EARLY_MS }, app)).accessToken;
86
58
  }
87
- /** What a process starts to refresh an app's sign-in in the background: `--app` only for an app but Claude Code. */
88
59
  export const refreshArgs = (origin, app, force = false) => [
89
60
  'refresh',
90
61
  '--origin',
@@ -92,10 +63,6 @@ export const refreshArgs = (origin, app, force = false) => [
92
63
  ...(app === DEFAULT_APP ? [] : ['--app', app]),
93
64
  ...(force ? ['--force'] : []),
94
65
  ];
95
- /**
96
- * `pendingyou refresh`, what a hook starts in the background: refreshes the sign-in when it runs out within SOON_MS
97
- * (or `force`, after a 401), under the lock, to the end. Quiet; true when there's a sign-in afterwards.
98
- */
99
66
  export async function refreshInBackground(io, origin, force, app = DEFAULT_APP) {
100
67
  const stored = await readCredential(io, origin, app);
101
68
  if (!stored)
@@ -105,7 +72,6 @@ export async function refreshInBackground(io, origin, force, app = DEFAULT_APP)
105
72
  await refreshStored(io, origin, stored, { force, earlyMs: SOON_MS }, app);
106
73
  return true;
107
74
  }
108
- /** A hook's token: never refreshed here (see the top of this file). */
109
75
  async function hookToken(io, origin, force, signal, app) {
110
76
  const stored = await readCredential(io, origin, app);
111
77
  if (!stored)
@@ -113,10 +79,8 @@ async function hookToken(io, origin, force, signal, app) {
113
79
  const left = stored.expiresAt - io.now();
114
80
  if (!force && left > SOON_MS)
115
81
  return stored.accessToken;
116
- // One refresher at a time: a lock held now is a refresh (or a write) already under way.
117
82
  if (!(await isLocked(credentialsPath(io))))
118
83
  io.background(refreshArgs(origin, app, force));
119
- // Still good for a few seconds: this request goes with it, the next one with the new token.
120
84
  if (!force && left > USABLE_MS)
121
85
  return stored.accessToken;
122
86
  const until = io.now() + REFRESH_WAIT_MS;
@@ -130,14 +94,6 @@ async function hookToken(io, origin, force, signal, app) {
130
94
  }
131
95
  throw new Unavailable('The sign-in is being refreshed in the background.');
132
96
  }
133
- /**
134
- * The token `pendingyou mcp-headers` hands an app's MCP server (remote.ts), and the stdio bridge sends with each
135
- * message (bridge.ts). The app runs the helper each time it connects and gives it 10 seconds, and the bridge may be
136
- * stopped at any moment, so, as for a hook, a refresh is never made in this process: within 5 minutes of expiry it
137
- * starts `pendingyou refresh` detached and waits for its token, up to `waitMs`, then uses the old one while it still
138
- * works. A token handed over has at least 10 seconds left; the app runs the helper again on a 401. `force` (the bridge,
139
- * after a 401) refreshes whatever the expiry says, and hands over only the new token.
140
- */
141
97
  export async function helperToken(io, origin, waitMs, app = DEFAULT_APP, force = false) {
142
98
  const stored = await readCredential(io, origin, app);
143
99
  if (!stored)
@@ -159,7 +115,6 @@ export async function helperToken(io, origin, waitMs, app = DEFAULT_APP, force =
159
115
  return stored.accessToken;
160
116
  throw new Unavailable('Pending You couldn’t be reached to refresh the sign-in.');
161
117
  }
162
- /** Aborts on `signal` or after `ms`, whichever comes first (AbortSignal.any needs Node 20.3). */
163
118
  function either(signal, ms) {
164
119
  const timeout = AbortSignal.timeout(ms);
165
120
  if (!signal)
@@ -172,11 +127,6 @@ function either(signal, ms) {
172
127
  timeout.addEventListener('abort', abort, { once: true });
173
128
  return controller.signal;
174
129
  }
175
- /**
176
- * GETs `path` on `origin` as the command line, with an app's sign-in (Claude Code's unless `app` says), and returns the
177
- * JSON body with its status. 401 after a refresh means the sign-in is over (revoked, or 30 days passed). Network
178
- * failures and 5xx are `Unavailable`.
179
- */
180
130
  export async function getJson(io, origin, path, timeoutMs, signal, options = {}) {
181
131
  const app = options.app ?? DEFAULT_APP;
182
132
  for (let attempt = 0;; attempt++) {
@@ -222,3 +172,30 @@ export async function getJson(io, origin, path, timeoutMs, signal, options = {})
222
172
  return { status: response.status, body };
223
173
  }
224
174
  }
175
+ export async function postJson(io, origin, path, body, timeoutMs, options = {}) {
176
+ const app = options.app ?? DEFAULT_APP;
177
+ const token = await hookToken(io, origin, options.force === true, options.signal, app);
178
+ let response;
179
+ try {
180
+ response = await io.fetch(`${origin}${path}`, {
181
+ method: 'POST',
182
+ headers: {
183
+ authorization: `Bearer ${token}`,
184
+ 'content-type': 'application/json',
185
+ accept: 'application/json',
186
+ },
187
+ body: JSON.stringify(body),
188
+ signal: either(options.signal, timeoutMs),
189
+ redirect: 'manual',
190
+ });
191
+ }
192
+ catch {
193
+ throw new Unavailable('Pending You couldn’t be reached.');
194
+ }
195
+ await response.body?.cancel().catch(() => { });
196
+ const retry = Number(response.headers.get('retry-after'));
197
+ return {
198
+ status: response.status,
199
+ ...(Number.isFinite(retry) && retry > 0 ? { retryAfter: retry } : {}),
200
+ };
201
+ }