pendingyou 0.34.2 → 0.35.1
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 +22 -0
- package/README.md +5 -3
- package/dist/apps/codex-wake.js +1 -1
- package/dist/args.js +8 -2
- package/dist/handed-wait.js +58 -0
- package/dist/herdr/answer.js +28 -3
- package/dist/main.js +8 -1
- package/dist/version.js +1 -1
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/README.md +1 -1
- package/mod/hooks/register.js +72 -5
- package/mod/hooks/wake.js +86 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.35.1 (2026-10-11)
|
|
4
|
+
|
|
5
|
+
- Fixed: the Pending You popup in Herdr no longer breaks on a kind of card it doesn't know yet (or a grouped card with
|
|
6
|
+
a question of such a kind). It shows the card and says "Answer this one in Pending You: o opens it."; reply, Later,
|
|
7
|
+
hand over and open still work.
|
|
8
|
+
- The person API client inside the command line follows the SDK's 0.2.0 types: the enums in what /v1 sends are open
|
|
9
|
+
(any string beside the known values), since /v1 adds values over time.
|
|
10
|
+
|
|
11
|
+
## 0.35.0 (2026-10-09)
|
|
12
|
+
|
|
13
|
+
- Claude Code hears a question handed to it within seconds. When you delegate a card to one of your Claude Code agents
|
|
14
|
+
(Delegate, or a card handed across), the wake mod no longer waits for its next look (every minute, or every 3
|
|
15
|
+
minutes after a quiet half hour): one session on the computer keeps a wait open with Pending You,
|
|
16
|
+
`pendingyou listen --handed --app claude-code`, which asks for handed questions alone and returns as soon as one
|
|
17
|
+
comes, and every session there looks at once. The session in the card's folder takes it, about 5 to 10 seconds after
|
|
18
|
+
you tap (the 5-second Undo hold included). The timer stays, as the fallback. Nothing to set up: it runs through the
|
|
19
|
+
hooks' shim `init` already wrote.
|
|
20
|
+
- 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,
|
|
21
|
+
and looks again as that ends.
|
|
22
|
+
- When sessions look together, only one tells it: each claims the question before reading it, and the last claim
|
|
23
|
+
stands.
|
|
24
|
+
|
|
3
25
|
## 0.34.2 (2026-10-09)
|
|
4
26
|
|
|
5
27
|
- Fixed: in bypassPermissions or dontAsk mode, a Claude Code dialog for a tool its agent had used before (a subagent's
|
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**:
|
|
439
|
-
|
|
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.
|
package/dist/apps/codex-wake.js
CHANGED
|
@@ -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
|
-
|
|
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/herdr/answer.js
CHANGED
|
@@ -22,6 +22,25 @@ export const askerOf = (card) => `${card.asker.agent?.name ?? card.asker.assista
|
|
|
22
22
|
const askerName = (card) => card.asker.agent?.name ?? card.asker.assistant.name;
|
|
23
23
|
export const HIGH_STAKES = 'High stakes: answer it in Pending You, where you hold to approve. o opens it.';
|
|
24
24
|
export const NOT_HERE = 'Answer this one in Pending You: o opens it.';
|
|
25
|
+
const KINDS = [
|
|
26
|
+
'choice',
|
|
27
|
+
'multi',
|
|
28
|
+
'approve',
|
|
29
|
+
'text',
|
|
30
|
+
'review',
|
|
31
|
+
'action',
|
|
32
|
+
'fyi',
|
|
33
|
+
'group',
|
|
34
|
+
];
|
|
35
|
+
const QUESTION_KINDS = [
|
|
36
|
+
'choice',
|
|
37
|
+
'multi',
|
|
38
|
+
'approve',
|
|
39
|
+
'text',
|
|
40
|
+
];
|
|
41
|
+
const answersHere = (card) => card.actions.includes('answer') &&
|
|
42
|
+
KINDS.includes(card.kind) &&
|
|
43
|
+
(card.questions ?? []).every((question) => QUESTION_KINDS.includes(question.kind));
|
|
25
44
|
const NO_WORDS = 'Pending You takes no words on this card from Herdr: o opens it.';
|
|
26
45
|
const NO_LATER = 'This card can’t go to Later from Herdr: o opens it.';
|
|
27
46
|
const NO_HANDING = 'This card can’t be handed over from Herdr: o opens it.';
|
|
@@ -126,7 +145,7 @@ function listKey(state, key, now) {
|
|
|
126
145
|
{ kind: 'targets', card },
|
|
127
146
|
];
|
|
128
147
|
}
|
|
129
|
-
if (!card
|
|
148
|
+
if (!answersHere(card))
|
|
130
149
|
return [{ ...state, note: NOT_HERE }, NONE];
|
|
131
150
|
return answerKey(state, card, key);
|
|
132
151
|
}
|
|
@@ -211,6 +230,8 @@ function answerKey(state, card, key) {
|
|
|
211
230
|
return [{ ...state, note: 'y: got it.' }, NONE];
|
|
212
231
|
case 'group':
|
|
213
232
|
return groupKey(state, card, key, send);
|
|
233
|
+
default:
|
|
234
|
+
return [{ ...state, note: NOT_HERE }, NONE];
|
|
214
235
|
}
|
|
215
236
|
}
|
|
216
237
|
const toggled = (chosen, id) => chosen.includes(id) ? chosen.filter((each) => each !== id) : [...chosen, id];
|
|
@@ -289,6 +310,8 @@ function groupKey(state, card, key, send) {
|
|
|
289
310
|
const suggestion = n ? question.text?.suggestions?.[n - 1] : undefined;
|
|
290
311
|
return suggestion ? answered({ text: suggestion }) : [state, NONE];
|
|
291
312
|
}
|
|
313
|
+
default:
|
|
314
|
+
return [{ ...state, note: NOT_HERE }, NONE];
|
|
292
315
|
}
|
|
293
316
|
}
|
|
294
317
|
function writeKey(state, screen, key) {
|
|
@@ -564,7 +587,7 @@ function askLines(card, state, room, now) {
|
|
|
564
587
|
lines.push(DIM(fit(`If you don’t answer ${fromNow(card.fallback.at, now)}: ${card.fallback.text}`)));
|
|
565
588
|
if (card.highStakes)
|
|
566
589
|
lines.push(...wrap(HIGH_STAKES, room, 2));
|
|
567
|
-
else if (!card
|
|
590
|
+
else if (!answersHere(card))
|
|
568
591
|
lines.push(...wrap(NOT_HERE, room, 2));
|
|
569
592
|
return lines;
|
|
570
593
|
}
|
|
@@ -603,6 +626,8 @@ function answerKeys(card, state) {
|
|
|
603
626
|
: [`1–${count(question?.options)}`];
|
|
604
627
|
return [...own, 'Tab next question', 'Enter send all'];
|
|
605
628
|
}
|
|
629
|
+
default:
|
|
630
|
+
return [];
|
|
606
631
|
}
|
|
607
632
|
}
|
|
608
633
|
export function keysLine(state, now) {
|
|
@@ -632,7 +657,7 @@ export function keysLine(state, now) {
|
|
|
632
657
|
if (card) {
|
|
633
658
|
const busy = state.sent?.cardId === card.id && now < state.sent.until;
|
|
634
659
|
if (!card.highStakes && !busy) {
|
|
635
|
-
if (card
|
|
660
|
+
if (answersHere(card))
|
|
636
661
|
keys.push(...answerKeys(card, state));
|
|
637
662
|
if (card.actions.includes('reply'))
|
|
638
663
|
keys.push('r reply');
|
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);
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.35.1';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pendingyou-wake",
|
|
3
3
|
"displayName": "Pending You",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.52.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.
|
|
7
|
+
Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.52.0; send changes there.
|
package/mod/hooks/register.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.35.1",
|
|
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",
|