pendingyou 0.34.1 → 0.35.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.35.0 (2026-10-09)
4
+
5
+ - Claude Code hears a question handed to it within seconds. When you delegate a card to one of your Claude Code agents
6
+ (Delegate, or a card handed across), the wake mod no longer waits for its next look (every minute, or every 3
7
+ minutes after a quiet half hour): one session on the computer keeps a wait open with Pending You,
8
+ `pendingyou listen --handed --app claude-code`, which asks for handed questions alone and returns as soon as one
9
+ comes, and every session there looks at once. The session in the card's folder takes it, about 5 to 10 seconds after
10
+ you tap (the 5-second Undo hold included). The timer stays, as the fallback. Nothing to set up: it runs through the
11
+ hooks' shim `init` already wrote.
12
+ - A session that can't tell its folder now leaves a question handed to a task in one for half a minute, not 5 minutes,
13
+ and looks again as that ends.
14
+ - When sessions look together, only one tells it: each claims the question before reading it, and the last claim
15
+ stands.
16
+
17
+ ## 0.34.2 (2026-10-09)
18
+
19
+ - Fixed: in bypassPermissions or dontAsk mode, a Claude Code dialog for a tool its agent had used before (a subagent's
20
+ second Bash) could get no card. Claude Code writes a prompted call to its transcript only once its dialog is
21
+ answered, so 20 seconds in the check for an unanswered call found the agent's earlier call of the same tool,
22
+ answered, and settled the prompt. Now only the prompt's own call counts: not in the transcript yet, it's still
23
+ waiting and its card goes up; the same command answered before the prompt was asked is an earlier call and doesn't
24
+ count; a call Claude Code denied by itself is written with its refusal at once, so it still gets no card. A
25
+ Notification picks the prompt it shows the same way, so it names the second call rather than putting up a card in
26
+ Claude Code's words.
27
+
3
28
  ## 0.34.1 (2026-10-09)
4
29
 
5
30
  - Fixed: a Claude Code permission prompt answered through the permission channel (`npx pendingyou claude`) could get
package/README.md CHANGED
@@ -435,14 +435,16 @@ answers, once the 5-second window to undo is over:
435
435
  session in that folder that used Pending You most recently; never the one that asked, and never two. A sign-in that
436
436
  only hears (`--oauth`) hears every computer's, so it leaves one Pending You says went to another computer (by its
437
437
  name or its hostname); one that names no computer goes by the folder.
438
- - **Claude Code**: the wake mod looks for questions handed to the session's name (`list_pending`, every minute while
439
- the session used Pending You in the last half hour, then every 3 minutes, for 12 hours), reads each with
438
+ - **Claude Code**: within seconds since 0.35.0. One session on the computer keeps a wait open with Pending You for a
439
+ handed question (`listen --handed --app claude-code`, through the hooks' shim), and when one comes every session
440
+ there looks at once; otherwise the wake mod looks for questions handed to the session's name (`list_pending`, every
441
+ minute while the session used Pending You in the last half hour, then every 3 minutes, for 12 hours). It reads each with
440
442
  `get_request`, and starts a turn: “Pending You: your person handed you “Which AWS account for staging?” (req_…), a
441
443
  question from billing-webhooks (answer freely: your answer goes straight to billing-webhooks). Call get_request
442
444
  (requestId req_…, name “infra”) to read it and their note. If you know, answer_delegated with your answer and how you
443
445
  know; if not, hand_back with what you checked.” Among sessions by one name, the one working in the task's folder
444
446
  takes it (as `list_pending` gives it, `~/…` or a full path), one in another folder never does, and one that can't
445
- tell its folder waits 5 minutes.
447
+ tell its folder waits half a minute (5 minutes before 0.35.0).
446
448
  - **Codex**: one listener for the app queues it into the thread it's for (`codex queue`, the same words).
447
449
  - **OpenCode and Pi**: the session's listener starts the turn through the plugin or extension; a session that used
448
450
  Pending You's card tools keeps listening for 12 hours, while the app is open.
@@ -330,7 +330,7 @@ export async function computerNames(io, origin, options) {
330
330
  }
331
331
  return names;
332
332
  }
333
- async function elsewhereFor(io, origin, app) {
333
+ export async function elsewhereFor(io, origin, app) {
334
334
  if (!(await hearsOnly(io, origin, app)))
335
335
  return () => false;
336
336
  const names = await computerNames(io, origin, { tailscale: true });
package/dist/args.js CHANGED
@@ -22,6 +22,7 @@ export class UsageError extends Error {
22
22
  name = 'UsageError';
23
23
  }
24
24
  const REQUEST_ID = /^req_[A-Za-z0-9-]{1,40}$/;
25
+ const ISO_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/;
25
26
  export function parseDuration(text) {
26
27
  const match = /^(\d+(?:\.\d+)?)(s|m|h)?$/.exec(text.trim());
27
28
  if (!match)
@@ -108,7 +109,7 @@ const FLAGS = {
108
109
  handoff: { origin: true, app: true },
109
110
  stopcheck: { origin: true, app: true },
110
111
  posted: { origin: true, app: true },
111
- listen: { origin: true, app: true, thread: true, handed: false },
112
+ listen: { origin: true, app: true, thread: true, handed: false, since: true },
112
113
  permission: { origin: true, app: true },
113
114
  'permission-done': { origin: true, app: true },
114
115
  notify: { origin: true, app: true },
@@ -360,11 +361,16 @@ export function parseArgs(argv, env = {}) {
360
361
  case 'listen': {
361
362
  none();
362
363
  const thread = text('thread');
364
+ const since = text('since');
363
365
  if (flags.has('handed')) {
364
366
  if (thread !== undefined)
365
367
  throw new UsageError('Use --thread or --handed, not both.');
366
- return { name: 'listen', origin, ...app(), handed: true };
368
+ if (since !== undefined && !ISO_TIME.test(since))
369
+ throw new UsageError('--since needs a time, like: --since 2026-10-09T12:00:00.000Z');
370
+ return { name: 'listen', origin, ...app(), handed: true, ...(since ? { since } : {}) };
367
371
  }
372
+ if (since !== undefined)
373
+ throw new UsageError('--since goes with --handed.');
368
374
  if (thread === undefined || !THREAD.test(thread))
369
375
  throw new UsageError('listen needs the thread it wakes, like: --thread 0199a1b2-…');
370
376
  return { name: 'listen', origin, ...app(), thread };
@@ -0,0 +1,58 @@
1
+ import { getJson, SignInNeeded } from "./api.js";
2
+ import { elsewhereFor } from "./apps/codex-wake.js";
3
+ import { delegatedOf } from "./format.js";
4
+ export const WAIT_MS = 2 * 60_000;
5
+ export const POLL_SECONDS = 25;
6
+ const RETRY_MS = 5000;
7
+ const after = (at, since) => !since || Date.parse(at) > Date.parse(since);
8
+ export async function waitHanded(io, options) {
9
+ const said = await listenOnce(io, options);
10
+ io.out(`${JSON.stringify(said)}\n`);
11
+ return 0;
12
+ }
13
+ async function listenOnce(io, options) {
14
+ const { origin } = options;
15
+ let since = options.since;
16
+ const started = io.now();
17
+ const elsewhere = await elsewhereFor(io, origin, 'claude-code');
18
+ let failures = 0;
19
+ while (io.now() - started < WAIT_MS && !io.signal.aborted) {
20
+ const query = new URLSearchParams({
21
+ source: 'claude-code',
22
+ handed: '1',
23
+ limit: '20',
24
+ wait: String(POLL_SECONDS),
25
+ });
26
+ if (since)
27
+ query.set('since', since);
28
+ let requests;
29
+ try {
30
+ const { status, body } = await getJson(io, origin, `/mcp/cli/answers?${query}`, (POLL_SECONDS + 20) * 1000, io.signal, { app: 'claude-code' });
31
+ if (status !== 200)
32
+ throw new Error(`status ${status}`);
33
+ requests = Array.isArray(body.requests) ? body.requests : [];
34
+ failures = 0;
35
+ }
36
+ catch (error) {
37
+ if (error instanceof SignInNeeded)
38
+ return { error: 'signin' };
39
+ if (io.signal.aborted)
40
+ break;
41
+ if (++failures >= 2)
42
+ return { error: 'unavailable', ...(since ? { since } : {}) };
43
+ await io.sleep(RETRY_MS, io.signal);
44
+ continue;
45
+ }
46
+ const handed = requests.filter((heard) => delegatedOf(heard) !== null &&
47
+ heard.ready === true &&
48
+ typeof heard.changedAt === 'string' &&
49
+ after(heard.changedAt, since) &&
50
+ !elsewhere(heard));
51
+ for (const heard of requests)
52
+ if (typeof heard.changedAt === 'string' && after(heard.changedAt, since))
53
+ since = heard.changedAt;
54
+ if (handed.length)
55
+ return { handed: handed.map((heard) => heard.requestId), ...(since ? { since } : {}) };
56
+ }
57
+ return { handed: [], ...(since ? { since } : {}) };
58
+ }
package/dist/main.js CHANGED
@@ -9,6 +9,7 @@ import { channel } from "./channel.js";
9
9
  import { claudeWithChannel } from "./claude.js";
10
10
  import { ANSWER_DEADLINE_MARGIN_MS, readAnswerWait } from "./codex-answers.js";
11
11
  import { PlainError } from "./errors.js";
12
+ import { waitHanded } from "./handed-wait.js";
12
13
  import { hold } from "./hold.js";
13
14
  import { init, status, uninstall } from "./init.js";
14
15
  import { machine } from "./machine-command.js";
@@ -78,8 +79,14 @@ async function run(io, command, hook = { fallback: '' }) {
78
79
  catch { }
79
80
  return 0;
80
81
  case 'listen':
81
- if ('handed' in command)
82
+ if ('handed' in command) {
83
+ if (command.app === 'claude-code')
84
+ return waitHanded(io, {
85
+ origin: command.origin,
86
+ ...(command.since ? { since: command.since } : {}),
87
+ });
82
88
  return listenHanded(io, { origin: command.origin, app: command.app ?? DEFAULT_APP });
89
+ }
83
90
  return listen(io, { ...command, app: command.app ?? DEFAULT_APP });
84
91
  case 'init':
85
92
  return init(io, command);
@@ -740,8 +740,7 @@ async function callState(calls, prompt) {
740
740
  const size = (await file.stat()).size;
741
741
  let position = size;
742
742
  let carry = Buffer.alloc(0);
743
- const results = new Set();
744
- let sameTool = null;
743
+ const results = new Map();
745
744
  let found = null;
746
745
  const read = (line) => {
747
746
  if (!line.includes('tool_'))
@@ -762,13 +761,16 @@ async function callState(calls, prompt) {
762
761
  continue;
763
762
  const id = text(block.tool_use_id) || text(block.id);
764
763
  if (block.type === 'tool_result' && id)
765
- results.add(id);
766
- else if (block.type === 'tool_use' && entry.type === 'assistant' && found === null) {
767
- const tool = text(block.name);
768
- if (promptKey(tool, block.input, prompt.agent) === prompt.key)
769
- found = results.has(id) ? 'answered' : 'waiting';
770
- else if (sameTool === null && prompt.tool && tool === prompt.tool)
771
- sameTool = id;
764
+ results.set(id, Date.parse(text(entry.timestamp)));
765
+ else if (block.type === 'tool_use' &&
766
+ entry.type === 'assistant' &&
767
+ found === null &&
768
+ promptKey(text(block.name), block.input, prompt.agent) === prompt.key) {
769
+ const answered = results.get(id);
770
+ if (answered === undefined)
771
+ found = 'waiting';
772
+ else if (!(answered < prompt.at))
773
+ found = 'answered';
772
774
  }
773
775
  }
774
776
  };
@@ -788,11 +790,7 @@ async function callState(calls, prompt) {
788
790
  }
789
791
  if (found === null && position === 0)
790
792
  read(carry.toString('utf8'));
791
- if (found !== null)
792
- return found;
793
- if (sameTool !== null)
794
- return results.has(sameTool) ? 'answered' : 'waiting';
795
- return 'waiting';
793
+ return found ?? 'waiting';
796
794
  }
797
795
  catch {
798
796
  return null;
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.34.1';
1
+ export const VERSION = '0.35.0';
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pendingyou-wake",
3
3
  "displayName": "Pending You",
4
- "version": "2.49.0",
4
+ "version": "2.50.0",
5
5
  "description": "Wakes Claude Code when your person answers a Pending You card it posted, even while it sits idle. Installed by `npx pendingyou init`.",
6
6
  "author": {
7
7
  "name": "RecordPlane",
package/mod/README.md CHANGED
@@ -4,4 +4,4 @@ The pendingyou command line’s copy of the Pending You plugin’s wake mod, for
4
4
 
5
5
  In each session it learns the cards the session posts to Pending You, checks them through the session’s own Pending You connection, and starts a turn when your person answers, writes back or a fallback comes due, or hands the session a question from another of their assistants (CLI 0.14.0), even while it sits idle. It answers `npx pendingyou hold` itself, so nothing waits in the background, and shows “2 waiting on you” under the prompt, after the plugin’s name. With the Pending You plugin installed too, only one copy acts.
6
6
 
7
- Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.49.0; send changes there.
7
+ Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.50.0; send changes there.
@@ -1,7 +1,8 @@
1
1
  // Generated from packages/claude-plugin/src/wake/register.ts in recordplane/pendingyou: edit that, then run
2
2
  // `pnpm --filter @pendingyou/claude-plugin generate`.
3
3
 
4
- import { argumentsOf, blockedNote, CLAIM_PREFIX, CLI_COPY, cardTool, checked, claimLease, cliConfigOf, connectionOf, dueCards, emptyBook, failed, folderOf, HANDED_FAST_MS, HANDED_READS, HERDR_EVERY_MS, HERDR_RUN_MS, handedDue, handedNext, handedOf, handedText, handedView, herdrArgv, herdrPayload, holdOf, holdReply, inHerdr, isFresh, isRefusal, isStale, isWaitingOnYou, KEEP_MS, learn, leaseHolder, loadedNote, loadedPath, momentOf, originOf, PRESENCE_EVERY_MS, PRESENCE_RUN_MS, presenceArgv, prompted, put, readBook, readClaim, readSetup, readSince, resultObject, SETUP_AFTER_MS, SETUP_RETRY_MS, SETUP_SERVER, SETUP_TRIES, settle, setUpOf, setupNeeded, setupPrompt, stateOf, statusText, TICK_MS, takesHanded, told, toTell, untold, wakeText, } from "./wake.js";
4
+ import { argumentsOf, blockedNote, CLAIM_PREFIX, CLI_COPY, cardTool, checked, claimedElsewhere, claimHeld, claimLease, cliConfigOf, connectionOf, dueCards, emptyBook, failed, folderOf, graceEnds, HANDED_FAST_MS, HANDED_READS, HERDR_EVERY_MS, HERDR_RUN_MS, handedDue, handedNext, handedOf, handedText, handedView, herdrArgv, herdrPayload, holdOf, holdReply, inHerdr, isFresh, isRefusal, isStale, isWaitingOnYou, KEEP_MS, LISTEN_RUN_MS, LISTENER_KEY, learn, leaseFor, leaseHolder, listenArgv, listened, loadedNote, loadedPath, mayListen, momentOf, originOf, PRESENCE_EVERY_MS, PRESENCE_RUN_MS, presenceArgv, prompted, put, readBook, readClaim, readListener, readSetup, readSignal, readSince, readWait, resultObject, SETUP_AFTER_MS, SETUP_RETRY_MS, SETUP_SERVER, SETUP_TRIES, SIGNAL_KEY, settle, setUpOf, setupNeeded, setupPrompt, stateOf, statusText, TICK_MS, takesHanded, told, toTell, untold, wakeText, } from "./wake.js";
5
+ const UNHEARD = { error: 'unavailable' };
5
6
  const RETRY_MS = 60_000;
6
7
  const live = {
7
8
  me: '',
@@ -29,6 +30,8 @@ const live = {
29
30
  herdr: null,
30
31
  herdrSaid: undefined,
31
32
  herdrTimer: null,
33
+ waiting: false,
34
+ signalSeen: 0,
32
35
  };
33
36
  const keyOf = (sessionId) => `session:${sessionId}`;
34
37
  function blockedServer() {
@@ -181,28 +184,41 @@ async function deliverHanded($) {
181
184
  }
182
185
  const cards = [];
183
186
  const claims = [];
187
+ const me = live.sessionId;
184
188
  for (const handed of listed) {
185
189
  const known = live.handedTold.get(handed.requestId);
186
190
  if (readSince(known, handed.updatedAt) || known === handed.updatedAt)
187
191
  continue;
188
- if (!takesHanded(handed, live.folders, live.home, now))
192
+ if (!takesHanded(handed, live.folders, live.home, now)) {
193
+ const again = graceEnds(handed, live.folders);
194
+ if (again !== null && again > now)
195
+ live.handedAt = Math.min(live.handedAt, again);
189
196
  continue;
197
+ }
190
198
  const key = `${CLAIM_PREFIX}${handed.requestId}`;
191
- if (readClaim(await $.store.get(key))?.moment === handed.updatedAt) {
199
+ if (claimedElsewhere(await $.store.get(key), handed.updatedAt, me)) {
192
200
  live.handedTold.set(handed.requestId, handed.updatedAt);
193
201
  continue;
194
202
  }
195
203
  const args = { requestId: handed.requestId, name };
196
204
  if (!(await allowed($, server, args)))
197
205
  continue;
206
+ await $.store.set(key, { moment: handed.updatedAt, at: now, by: me });
198
207
  let view = null;
199
208
  try {
200
209
  view = handedView(resultObject(await $.mcp.call(server, 'get_request', args)), handed, name);
201
210
  }
202
211
  catch { }
203
- if (!view)
212
+ const claim = await $.store.get(key);
213
+ if (!view) {
214
+ if (claimHeld(claim, handed.updatedAt, me))
215
+ await $.store.delete(key);
216
+ continue;
217
+ }
218
+ if (!claimHeld(claim, handed.updatedAt, me)) {
219
+ live.handedTold.set(handed.requestId, handed.updatedAt);
204
220
  continue;
205
- await $.store.set(key, { moment: handed.updatedAt, at: now });
221
+ }
206
222
  live.handedTold.set(handed.requestId, handed.updatedAt);
207
223
  claims.push(key);
208
224
  cards.push(view);
@@ -217,6 +233,55 @@ async function deliverHanded($) {
217
233
  live.handedAt = (await $.clock.now()) + RETRY_MS;
218
234
  });
219
235
  }
236
+ async function listenHanded($) {
237
+ const presence = live.presence;
238
+ if (!presence || live.waiting || !live.sessionId)
239
+ return;
240
+ const now = await $.clock.now();
241
+ const me = live.sessionId;
242
+ const record = readListener(await $.store.get(LISTENER_KEY));
243
+ if (!mayListen(record, me, live.seenAt, now))
244
+ return;
245
+ const lease = leaseFor(record, me, now);
246
+ await $.store.set(LISTENER_KEY, lease);
247
+ if (readListener(await $.store.get(LISTENER_KEY))?.by !== me)
248
+ return;
249
+ const argv = listenArgv(presence.config, presence.origin, lease.since);
250
+ let run;
251
+ try {
252
+ run = $.process.run(argv, { timeoutMs: LISTEN_RUN_MS });
253
+ }
254
+ catch {
255
+ await $.store.set(LISTENER_KEY, listened(lease, readWait(''), now));
256
+ return;
257
+ }
258
+ live.waiting = true;
259
+ run
260
+ .then((ran) => (ran.exitCode === 0 ? readWait(ran.stdout) : UNHEARD), () => UNHEARD)
261
+ .then(async (heard) => {
262
+ const at = await $.clock.now();
263
+ const current = readListener(await $.store.get(LISTENER_KEY));
264
+ if (!current || current.by === me)
265
+ await $.store.set(LISTENER_KEY, listened(lease, heard, at));
266
+ if ('handed' in heard && heard.handed.length) {
267
+ await $.store.set(SIGNAL_KEY, { at });
268
+ live.signalSeen = at;
269
+ live.handedAt = 0;
270
+ }
271
+ })
272
+ .catch(() => { })
273
+ .finally(() => {
274
+ live.waiting = false;
275
+ soon($);
276
+ });
277
+ }
278
+ async function heedSignal($) {
279
+ const at = readSignal(await $.store.get(SIGNAL_KEY));
280
+ if (at <= live.signalSeen)
281
+ return;
282
+ live.signalSeen = at;
283
+ live.handedAt = 0;
284
+ }
220
285
  async function prune($) {
221
286
  const now = await $.clock.now();
222
287
  for (const key of await $.store.keys()) {
@@ -298,6 +363,8 @@ async function tick($) {
298
363
  }
299
364
  for (const card of dueCards(live.book, now))
300
365
  await check($, card.requestId);
366
+ await listenHanded($).catch(() => { });
367
+ await heedSignal($).catch(() => { });
301
368
  if (!(await deliver($)))
302
369
  await deliverHanded($);
303
370
  showCards($);
package/mod/hooks/wake.js CHANGED
@@ -592,7 +592,7 @@ export function claimLease(value, me, now) {
592
592
  export const HANDED_FAST_MS = 60_000;
593
593
  export const HANDED_SLOW_MS = 3 * 60_000;
594
594
  export const HANDED_FOR_MS = 12 * 60 * 60_000;
595
- export const HANDED_GRACE_MS = 5 * 60_000;
595
+ export const HANDED_GRACE_MS = 30_000;
596
596
  export const CLAIM_PREFIX = 'handed:';
597
597
  export const HANDED_PREFIX = 'Pending You: your person handed you';
598
598
  export const HANDED_REPLY = 'on the question handed to you';
@@ -670,6 +670,12 @@ export function takesHanded(handed, folders, home, now) {
670
670
  return now - Date.parse(handed.updatedAt) >= HANDED_GRACE_MS;
671
671
  return inTaskFolder(folders, handed.cwd, home);
672
672
  }
673
+ export function graceEnds(handed, folders) {
674
+ if (handed.byName || !handed.cwd || folders.length > 0)
675
+ return null;
676
+ const at = Date.parse(handed.updatedAt);
677
+ return Number.isFinite(at) ? at + HANDED_GRACE_MS : null;
678
+ }
673
679
  export function folderOf(tool, input) {
674
680
  if (tool !== 'post_request')
675
681
  return undefined;
@@ -698,8 +704,86 @@ export function handedView(output, handed, name) {
698
704
  export function readClaim(value) {
699
705
  if (!isObject(value) || typeof value.moment !== 'string')
700
706
  return null;
701
- return { moment: value.moment, at: count(value.at) ?? 0 };
707
+ const by = text(value.by, 200);
708
+ return { moment: value.moment, at: count(value.at) ?? 0, ...(by ? { by } : {}) };
709
+ }
710
+ export function claimedElsewhere(value, moment, me) {
711
+ const claim = readClaim(value);
712
+ return claim?.moment === moment && claim.by !== me;
713
+ }
714
+ export function claimHeld(value, moment, me) {
715
+ const claim = readClaim(value);
716
+ return claim?.moment === moment && claim.by === me;
717
+ }
718
+ export const LISTENER_KEY = 'handed-listener';
719
+ export const SIGNAL_KEY = 'handed-signal';
720
+ export const LISTEN_RUN_MS = 4 * 60_000;
721
+ export const LISTENER_LEASE_MS = LISTEN_RUN_MS + 30_000;
722
+ export const LISTENER_KEEP_MS = 15_000;
723
+ export const LISTEN_RETRY_MS = 2 * 60_000;
724
+ export const LISTEN_QUIET_MS = 30 * 60_000;
725
+ const ISO_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/;
726
+ const isoOf = (value) => typeof value === 'string' && ISO_TIME.test(value) ? value : undefined;
727
+ export function readListener(value) {
728
+ if (!isObject(value))
729
+ return null;
730
+ const by = text(value.by, 200);
731
+ const until = count(value.until);
732
+ if (!by || until === undefined)
733
+ return null;
734
+ const since = isoOf(value.since);
735
+ const quietUntil = count(value.quietUntil);
736
+ return { by, until, ...(since ? { since } : {}), ...(quietUntil ? { quietUntil } : {}) };
737
+ }
738
+ export function mayListen(record, me, seenAt, now) {
739
+ if (seenAt <= 0 || now - seenAt >= HANDED_FOR_MS)
740
+ return false;
741
+ if (record?.quietUntil && now < record.quietUntil)
742
+ return false;
743
+ return !record || record.by === me || now >= record.until;
744
+ }
745
+ export const leaseFor = (record, me, now) => ({
746
+ by: me,
747
+ until: now + LISTENER_LEASE_MS,
748
+ ...(record?.since ? { since: record.since } : {}),
749
+ });
750
+ export function readWait(stdout) {
751
+ const line = stdout.trim().split('\n').at(-1) ?? '';
752
+ const parsed = parseJson(line);
753
+ if (!isObject(parsed))
754
+ return { error: 'unknown' };
755
+ const since = isoOf(parsed.since);
756
+ if (parsed.error === 'signin' || parsed.error === 'unavailable')
757
+ return { error: parsed.error, ...(since ? { since } : {}) };
758
+ if (!Array.isArray(parsed.handed))
759
+ return { error: 'unknown' };
760
+ const handed = parsed.handed.filter((id) => typeof id === 'string' && REQUEST_ID.test(id));
761
+ return { handed, ...(since ? { since } : {}) };
762
+ }
763
+ export function listened(record, heard, now) {
764
+ const since = heard.since ?? record.since;
765
+ const quiet = 'error' in heard
766
+ ? now + (heard.error === 'unavailable' ? LISTEN_RETRY_MS : LISTEN_QUIET_MS)
767
+ : undefined;
768
+ return {
769
+ by: record.by,
770
+ until: now + LISTENER_KEEP_MS,
771
+ ...(since ? { since } : {}),
772
+ ...(quiet ? { quietUntil: quiet } : {}),
773
+ };
774
+ }
775
+ export function listenArgv(config, origin, since) {
776
+ return [
777
+ `${config}/bin/pendingyou-hook`,
778
+ 'listen',
779
+ '--handed',
780
+ '--app',
781
+ 'claude-code',
782
+ ...(since ? ['--since', since] : []),
783
+ ...(origin === PRODUCTION ? [] : ['--origin', origin]),
784
+ ];
702
785
  }
786
+ export const readSignal = (value) => (isObject(value) ? (count(value.at) ?? 0) : 0);
703
787
  function cut(value, max) {
704
788
  const chars = [...value.replace(/\s+/g, ' ').trim()];
705
789
  return chars.length <= max
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pendingyou",
3
- "version": "0.34.1",
3
+ "version": "0.35.0",
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",