pendingyou 0.32.1 → 0.32.2

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.32.2 (2026-10-08)
4
+
5
+ - A Claude Code dialog in bypassPermissions or dontAsk mode always gets its card. A subagent's `rm -rf $S/*` met Claude
6
+ Code's “Dangerous rm operation” check and its dialog waited an hour with no card: its prompt was written down, but
7
+ its card waited for a Notification hook that never named it (one comes per dialog, it doesn't say which agent's, and
8
+ with several subagents at work it could mean another's prompt). Now the session's worker also looks at every such
9
+ prompt after 20 seconds: if the asking agent's transcript has no result for that call yet, its dialog is on screen
10
+ and the card goes up; if it has one (a call Claude Code denied by itself), it's settled with no card. A Notification
11
+ names the newest prompt whose call is still unanswered, and a card it makes due goes up within a second. A
12
+ subagent's card says “A subagent (general-purpose) of Claude Code … needs your permission to run a command”, its
13
+ title ends “(a subagent)”, and it goes once the person answers in Claude Code.
14
+
3
15
  ## 0.32.1 (2026-10-08)
4
16
 
5
17
  - The Claude Code wake mod never announces a card that's already picked up. It checks cards while a turn runs but tells
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
2
  import { open, readdir, rm, stat } from 'node:fs/promises';
3
- import { basename, join } from 'node:path';
3
+ import { basename, dirname, join } from 'node:path';
4
4
  import { threadNames } from "./apps/codex-wake.js";
5
5
  import { APP_NAMES } from "./apps/ids.js";
6
6
  import { DEFAULT_ORIGIN } from "./args.js";
@@ -13,8 +13,9 @@ import { machineOf } from "./remote.js";
13
13
  export const GRACE_MS = 10_000;
14
14
  export const TITLE_MAX = 90;
15
15
  export const WAITING = 'Claude Code is waiting for your OK';
16
- export const BACKGROUND_AGENT = ' (a background agent)';
16
+ export const SUBAGENT = ' (a subagent)';
17
17
  const QUIET_MODES = new Set(['bypassPermissions', 'dontAsk']);
18
+ export const QUIET_GRACE_MS = 20_000;
18
19
  export const NOTICE_MS = 4000;
19
20
  const BATCH_MS = 1000;
20
21
  export const PROMPT_APPS = ['claude-code', 'codex'];
@@ -28,7 +29,7 @@ export const DEFAULT_NAME = 'Claude Code';
28
29
  const defaultName = (app) => APP_NAMES[app];
29
30
  const LEASE_STALE_MS = 120_000;
30
31
  const WORKER_MS = 60 * 60_000;
31
- const NAP_MS = 20_000;
32
+ const NAP_MS = 1000;
32
33
  const CALL_MS = 15_000;
33
34
  const BACKOFF_MS = [2000, 5000, 10_000, 20_000];
34
35
  const MAX_PROMPTS = 20;
@@ -139,7 +140,7 @@ export function promptWords(tool, input, home, app = CLAUDE) {
139
140
  }
140
141
  return said(toolName(tool), `to use ${toolName(tool)}`);
141
142
  }
142
- export const fromAgent = (title) => `${cut(title, TITLE_MAX - [...BACKGROUND_AGENT].length)}${BACKGROUND_AGENT}`;
143
+ export const fromAgent = (title) => `${cut(title, TITLE_MAX - [...SUBAGENT].length)}${SUBAGENT}`;
143
144
  const noticeWords = (message) => redactText(text(message), 120);
144
145
  function noticeOf(message, now) {
145
146
  return {
@@ -171,7 +172,8 @@ async function change(io, session, edit) {
171
172
  });
172
173
  }
173
174
  const alive = (state, now) => Boolean(state.worker && now - state.worker.at < LEASE_STALE_MS);
174
- const seen = (prompt) => !prompt.quiet || prompt.shown !== undefined;
175
+ const seen = (prompt) => !prompt.quiet || prompt.shown !== undefined || prompt.waited !== undefined;
176
+ const unchecked = (prompt) => !seen(prompt) && !prompt.notice;
175
177
  function claim(state, now) {
176
178
  if (alive(state, now))
177
179
  return null;
@@ -234,7 +236,7 @@ async function asked(io, origin, input, app) {
234
236
  const words = promptWords(tool, isObject(input.tool_input) ? input.tool_input : {}, io.home, app);
235
237
  const prompt = {
236
238
  key: promptKey(tool, keyInput(app, input.tool_input), subagent || undefined),
237
- ...(app === 'codex' ? { tool: tool.slice(0, 100) } : {}),
239
+ tool: tool.slice(0, 100),
238
240
  ...(reasonOf(app, input.tool_input)
239
241
  ? { reason: reasonOf(app, input.tool_input) }
240
242
  : {}),
@@ -271,15 +273,13 @@ async function asked(io, origin, input, app) {
271
273
  const kept = app === 'claude-code'
272
274
  ? next.prompts.filter((each) => !wentOn(agent, now)(each))
273
275
  : next.prompts;
274
- const dropped = kept.length < next.prompts.length;
275
276
  next.prompts = kept;
276
277
  if (!next.prompts.some((each) => each.key === prompt.key))
277
278
  next.prompts.push(prompt);
278
279
  next.prompts = next.prompts.slice(-MAX_PROMPTS);
279
280
  delete next.settled;
280
281
  next.at = now;
281
- if (!quiet || (dropped && next.card))
282
- worker = claim(next, now);
282
+ worker = claim(next, now);
283
283
  return next;
284
284
  });
285
285
  if (worker)
@@ -302,8 +302,8 @@ async function settle(io, session, which, how) {
302
302
  state.prompts = left;
303
303
  state.settled = how;
304
304
  state.at = now;
305
- if (!state.card && !left.some(seen))
306
- return left.length > 0 || alive(state, now) ? state : null;
305
+ if (!state.card && left.length === 0)
306
+ return alive(state, now) ? state : null;
307
307
  worker = claim(state, now);
308
308
  return state;
309
309
  });
@@ -368,6 +368,16 @@ async function notified(io, origin, input) {
368
368
  const message = noticeWords(input.message);
369
369
  const cwd = text(input.cwd) || io.cwd;
370
370
  const link = sessionLink(io.env);
371
+ const known = await load(io, where.session);
372
+ let picked = null;
373
+ for (const prompt of (known?.prompts ?? []).slice().reverse()) {
374
+ if (prompt.shown !== undefined || prompt.notice || prompt.at > before)
375
+ continue;
376
+ if (await stillAsked(known, prompt)) {
377
+ picked = promptId(prompt);
378
+ break;
379
+ }
380
+ }
371
381
  let worker = null;
372
382
  await change(io, where.session, (state) => {
373
383
  const next = state ?? {
@@ -385,9 +395,9 @@ async function notified(io, origin, input) {
385
395
  next.transcript = where.transcript;
386
396
  if (link)
387
397
  next.link = link;
388
- const shows = next.prompts
389
- .filter((prompt) => prompt.shown === undefined && !prompt.notice && prompt.at <= before)
390
- .at(-1);
398
+ const shows = picked
399
+ ? next.prompts.find((prompt) => prompt.shown === undefined && promptId(prompt) === picked)
400
+ : undefined;
391
401
  if (shows) {
392
402
  shows.shown = now;
393
403
  delete shows.failed;
@@ -434,9 +444,23 @@ export async function permissionHook(io, options) {
434
444
  catch { }
435
445
  return 0;
436
446
  }
437
- const dueOf = (prompt, grace) => Math.min(prompt.quiet ? Number.POSITIVE_INFINITY : prompt.at + grace, prompt.shown ?? Number.POSITIVE_INFINITY);
447
+ const dueOf = (prompt, grace) => Math.min(prompt.quiet ? (prompt.waited ?? Number.POSITIVE_INFINITY) : prompt.at + grace, prompt.shown ?? Number.POSITIVE_INFINITY);
438
448
  const shownOf = (prompts) => (prompts.filter((prompt) => !prompt.notice).at(-1) ?? prompts.at(-1));
439
- export function planFor(state, now, grace) {
449
+ export function planFor(state, now, grace, quietGrace = QUIET_GRACE_MS) {
450
+ const plan = cardPlan(state, now, grace);
451
+ const pending = state.prompts.filter(unchecked);
452
+ if (pending.length === 0)
453
+ return plan;
454
+ const check = Math.min(...pending.map((prompt) => prompt.at + quietGrace)) - now;
455
+ if (check <= 0)
456
+ return { kind: 'check' };
457
+ if (plan.kind === 'done')
458
+ return { kind: 'wait', ms: check };
459
+ if (plan.kind === 'wait')
460
+ return { kind: 'wait', ms: Math.min(plan.ms, check) };
461
+ return plan;
462
+ }
463
+ function cardPlan(state, now, grace) {
440
464
  const prompts = state.prompts.filter(seen);
441
465
  if (prompts.length === 0)
442
466
  return state.card ? { kind: 'cancel' } : { kind: 'done' };
@@ -615,6 +639,104 @@ export async function sessionPosted(path) {
615
639
  await file.close().catch(() => { });
616
640
  }
617
641
  }
642
+ const AGENT_ID = /^[A-Za-z0-9_-]{1,100}$/;
643
+ function callsOf(state, prompt) {
644
+ const transcript = state.transcript;
645
+ if (!transcript)
646
+ return [];
647
+ const agent = prompt.agent;
648
+ if (!agent)
649
+ return [{ path: transcript, mine: (entry) => entry.isSidechain !== true }];
650
+ if (!AGENT_ID.test(agent))
651
+ return [];
652
+ const folder = join(dirname(transcript), basename(transcript, '.jsonl'));
653
+ return [
654
+ { path: join(folder, 'subagents', `agent-${agent}.jsonl`), mine: () => true },
655
+ { path: transcript, mine: (entry) => entry.isSidechain === true && entry.agentId === agent },
656
+ ];
657
+ }
658
+ async function callState(calls, prompt) {
659
+ let file;
660
+ try {
661
+ file = await open(calls.path, 'r');
662
+ }
663
+ catch {
664
+ return null;
665
+ }
666
+ try {
667
+ const size = (await file.stat()).size;
668
+ let position = size;
669
+ let carry = Buffer.alloc(0);
670
+ const results = new Set();
671
+ let sameTool = null;
672
+ let found = null;
673
+ const read = (line) => {
674
+ if (!line.includes('tool_'))
675
+ return;
676
+ let entry;
677
+ try {
678
+ entry = JSON.parse(line);
679
+ }
680
+ catch {
681
+ return;
682
+ }
683
+ if (!isObject(entry) || !calls.mine(entry))
684
+ return;
685
+ const message = isObject(entry.message) ? entry.message : {};
686
+ const blocks = Array.isArray(message.content) ? message.content : [];
687
+ for (const block of [...blocks].reverse()) {
688
+ if (!isObject(block))
689
+ continue;
690
+ const id = text(block.tool_use_id) || text(block.id);
691
+ if (block.type === 'tool_result' && id)
692
+ results.add(id);
693
+ else if (block.type === 'tool_use' && entry.type === 'assistant' && found === null) {
694
+ const tool = text(block.name);
695
+ if (promptKey(tool, block.input, prompt.agent) === prompt.key)
696
+ found = results.has(id) ? 'answered' : 'waiting';
697
+ else if (sameTool === null && prompt.tool && tool === prompt.tool)
698
+ sameTool = id;
699
+ }
700
+ }
701
+ };
702
+ while (found === null && position > 0 && size - position < TRANSCRIPT_CAP) {
703
+ const length = Math.min(64 * 1024, position);
704
+ position -= length;
705
+ const chunk = Buffer.alloc(length);
706
+ await file.read(chunk, 0, length, position);
707
+ let buffer = Buffer.concat([chunk, carry]);
708
+ let newline = buffer.lastIndexOf(10);
709
+ while (found === null && newline !== -1) {
710
+ read(buffer.subarray(newline + 1).toString('utf8'));
711
+ buffer = buffer.subarray(0, newline);
712
+ newline = buffer.lastIndexOf(10);
713
+ }
714
+ carry = buffer;
715
+ }
716
+ if (found === null && position === 0)
717
+ read(carry.toString('utf8'));
718
+ if (found !== null)
719
+ return found;
720
+ if (sameTool !== null)
721
+ return results.has(sameTool) ? 'answered' : 'waiting';
722
+ return 'waiting';
723
+ }
724
+ catch {
725
+ return null;
726
+ }
727
+ finally {
728
+ await file.close().catch(() => { });
729
+ }
730
+ }
731
+ async function stillAsked(state, prompt) {
732
+ for (const calls of callsOf(state, prompt)) {
733
+ const found = await callState(calls, prompt);
734
+ if (found)
735
+ return found === 'waiting';
736
+ }
737
+ return true;
738
+ }
739
+ const promptId = (prompt) => `${prompt.key}:${prompt.at}`;
618
740
  export function remoteOf(url) {
619
741
  const plain = url
620
742
  .trim()
@@ -675,7 +797,7 @@ const byAgent = (state, prompt) => appOf(state) === 'claude-code' && Boolean(pro
675
797
  function changeNoteOf(state, prompt) {
676
798
  if (prompt.doing === 'to answer its question')
677
799
  return cut(`Now waiting for your answer: ${prompt.what}`, 120);
678
- const agent = byAgent(state, prompt) ? BACKGROUND_AGENT : '';
800
+ const agent = byAgent(state, prompt) ? SUBAGENT : '';
679
801
  return `${cut(`Now waiting for your OK: ${prompt.what}`, 120 - [...agent].length)}${agent}`;
680
802
  }
681
803
  function cardWords(io, state, prompt, name, machine) {
@@ -686,10 +808,16 @@ function cardWords(io, state, prompt, name, machine) {
686
808
  : `is waiting for your OK${prompt.doing ? ` ${prompt.doing}` : ''}`;
687
809
  const why = prompt.reason ? ` It says: “${prompt.reason}”.` : '';
688
810
  const where = `in ${tilde(io, state.cwd)} on ${machine}`;
689
- const asker = byAgent(state, prompt)
690
- ? `A background agent${prompt.agentType ? ` (${prompt.agentType})` : ''} of ${who}, ${where},`
811
+ const subagent = byAgent(state, prompt);
812
+ const asker = subagent
813
+ ? `A subagent${prompt.agentType ? ` (${prompt.agentType})` : ''} of ${who}, ${where},`
691
814
  : `${who} ${where}`;
692
- const summary = cut(`${asker} ${waiting}.${why} It goes on once you answer it there.`, 400);
815
+ const needs = !subagent
816
+ ? waiting
817
+ : prompt.doing === 'to answer its question'
818
+ ? 'needs your answer to its question'
819
+ : `needs your permission${prompt.doing ? ` ${prompt.doing}` : ''}`;
820
+ const summary = cut(`${asker} ${needs}.${why} It goes on once you answer it there.`, 400);
693
821
  const place = `${APP_NAMES[app]} on ${machine}`;
694
822
  const linked = state.link ? `${place}, ${state.link}` : place;
695
823
  return {
@@ -817,6 +945,24 @@ async function withdraw(io, client, card, state, how) {
817
945
  .catch(() => { });
818
946
  }
819
947
  }
948
+ async function check(io, state, worker) {
949
+ const now = io.now();
950
+ const due = state.prompts.filter((prompt) => unchecked(prompt) && prompt.at + QUIET_GRACE_MS <= now);
951
+ const waiting = new Map();
952
+ for (const prompt of due)
953
+ waiting.set(promptId(prompt), await stillAsked(state, prompt));
954
+ await record(io, state.session, worker, (state) => {
955
+ const left = state.prompts.filter((prompt) => waiting.get(promptId(prompt)) !== false);
956
+ if (left.length < state.prompts.length) {
957
+ state.prompts = left;
958
+ state.settled = 'answered';
959
+ state.at = now;
960
+ }
961
+ for (const prompt of left)
962
+ if (unchecked(prompt) && waiting.get(promptId(prompt)))
963
+ prompt.waited = now;
964
+ });
965
+ }
820
966
  const renew = (io, session, worker) => record(io, session, worker, () => { });
821
967
  async function letGo(io, session, worker, grace) {
822
968
  let done = true;
@@ -871,6 +1017,10 @@ export async function permissionWorker(io, options) {
871
1017
  await io.sleep(Math.min(plan.ms, NAP_MS), io.signal);
872
1018
  continue;
873
1019
  }
1020
+ if (plan.kind === 'check') {
1021
+ await check(io, state, worker);
1022
+ continue;
1023
+ }
874
1024
  try {
875
1025
  client ??= await mcpClient(io, state.origin, appOf(state), CALL_MS);
876
1026
  if (plan.kind === 'post')
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.32.1';
1
+ export const VERSION = '0.32.2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pendingyou",
3
- "version": "0.32.1",
3
+ "version": "0.32.2",
4
4
  "description": "Pending You for your coding agents: one command sets up Claude Code, Codex, OpenCode and Pi to ask you through Pending You and hear your answers, even while they sit idle.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://www.pendingyou.com",