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 +25 -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/main.js +8 -1
- package/dist/permission.js +12 -14
- 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,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**:
|
|
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/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/permission.js
CHANGED
|
@@ -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
|
|
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.
|
|
766
|
-
else if (block.type === 'tool_use' &&
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
7
|
+
Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.50.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.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",
|