pendingyou 0.11.0 → 0.12.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 +67 -0
- package/README.md +84 -18
- package/dist/api.js +10 -8
- package/dist/apps/codex-wake.js +158 -65
- package/dist/apps/ids.js +3 -1
- package/dist/apps/opencode-plugin.js +440 -0
- package/dist/apps/opencode.js +734 -0
- package/dist/apps/pi-extension.js +391 -0
- package/dist/apps/pi.js +679 -0
- package/dist/apps/registry.js +3 -1
- package/dist/args.js +17 -11
- package/dist/bridge.js +566 -0
- package/dist/environment.js +9 -2
- package/dist/hold.js +15 -0
- package/dist/init.js +48 -19
- package/dist/io.js +34 -0
- package/dist/json.js +281 -0
- package/dist/jsonc.js +315 -0
- package/dist/main.js +6 -5
- package/dist/pickup.js +10 -5
- package/dist/remote.js +24 -10
- package/dist/setup.js +13 -4
- package/dist/state.js +7 -2
- package/dist/stopcheck.js +68 -5
- package/dist/version.js +1 -1
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/README.md +1 -1
- package/package.json +2 -2
package/dist/apps/codex-wake.js
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
|
-
// Codex hearing answers (0.11.0)
|
|
2
|
-
// the thread when one is answered.
|
|
1
|
+
// Codex hearing answers (0.11.0), and OpenCode and Pi the same way (0.12.0): what an app's hooks remember about the
|
|
2
|
+
// cards a thread (OpenCode, Pi: a session) posted, and the listener that wakes the thread when one is answered.
|
|
3
3
|
//
|
|
4
4
|
// - `pendingyou posted --app codex` is Codex's PostToolUse hook for Pending You's card tools (post_request,
|
|
5
5
|
// update_request, reply_in_thread, cancel_request; Codex runs it only for a call that didn't fail). It records that
|
|
6
6
|
// the turn put something in front of the person (with the card's words, for the Stop check: stopcheck.ts), and which
|
|
7
7
|
// thread is waiting on which card (`session_id` is the thread's id), then makes sure that thread has a listener.
|
|
8
|
+
// OpenCode's plugin runs it from `tool.execute.after` (apps/opencode-plugin.ts), and Pi's extension from Pi's
|
|
9
|
+
// `tool_result` (apps/pi-extension.ts), with the same fields.
|
|
8
10
|
// - `pendingyou listen --app codex --thread <id>` is that listener: one per thread (a lease file), detached, holding no
|
|
9
|
-
// hook's output (Codex would wait for it otherwise). It long-polls Pending You with
|
|
10
|
-
// does, and when one of the thread's cards is the agent's move (an answer, a message, a fallback that ran) it
|
|
11
|
-
// `codex queue --thread <id> --message "Pending You: your person answered …"`, in the words the Claude Code wake
|
|
12
|
-
// uses. Never the answer itself: a program's arguments are there for any other program on the computer to read,
|
|
13
|
-
// the agent reads the answer with get_request, as the message says. Codex starts a turn at once when the thread is
|
|
14
|
-
// loaded and idle, after the current one when it's working, or when it's next resumed. It stops when the thread has
|
|
15
|
-
// hours; the next session start or message starts it again while one is (pickup.ts).
|
|
11
|
+
// hook's output (Codex would wait for it otherwise). It long-polls Pending You with the app's own connection, as
|
|
12
|
+
// `watch` does, and when one of the thread's cards is the agent's move (an answer, a message, a fallback that ran) it
|
|
13
|
+
// runs `codex queue --thread <id> --message "Pending You: your person answered …"`, in the words the Claude Code wake
|
|
14
|
+
// mod uses. Never the answer itself: a program's arguments are there for any other program on the computer to read,
|
|
15
|
+
// so the agent reads the answer with get_request, as the message says. Codex starts a turn at once when the thread is
|
|
16
|
+
// loaded and idle, after the current one when it's working, or when it's next resumed. It stops when the thread has
|
|
17
|
+
// no card left waiting, or after 8 hours; the next session start or message starts it again while one is (pickup.ts).
|
|
18
|
+
// - OpenCode's listener is the plugin's child, not detached, since only the plugin can start a turn
|
|
19
|
+
// (client.session.promptAsync): it writes each wake as one JSON line on stdout, `{"wake": "<the words>",
|
|
20
|
+
// "requests": ["req_…"]}`, and the plugin answers on its stdin, `{"delivered": true}` once the turn is started (or
|
|
21
|
+
// false). Only then is it marked handed over. It stops when its stdin closes (OpenCode went), as well as when Codex's
|
|
22
|
+
// would. Pi's listener is its extension's child in just the same way (`pi.sendMessage` starts the turn).
|
|
16
23
|
//
|
|
17
|
-
// Ids, names and titles only, in ~/.config/pendingyou
|
|
24
|
+
// Ids, names and titles only, in ~/.config/pendingyou/<app>-threads.json: never an answer.
|
|
18
25
|
import { randomBytes } from 'node:crypto';
|
|
19
26
|
import { mkdir, readFile, rm, stat, utimes, writeFile } from 'node:fs/promises';
|
|
20
27
|
import { join } from 'node:path';
|
|
@@ -37,14 +44,37 @@ const LEASE_STALE_MS = 90_000;
|
|
|
37
44
|
const PATIENT_AFTER_MS = 30 * 60_000;
|
|
38
45
|
const PATIENT_PAUSE_MS = 30_000;
|
|
39
46
|
const BACKOFF_MS = [2000, 5000, 10_000, 20_000, 30_000, 60_000];
|
|
47
|
+
/** How long OpenCode's listener waits for the plugin to say whether it started the turn. */
|
|
48
|
+
const REPLY_MS = 60_000;
|
|
40
49
|
const REQUEST_ID = /^req_[A-Za-z0-9-]{1,40}$/;
|
|
41
|
-
/**
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const
|
|
50
|
+
/** A thread's id: Codex's (a UUID) or an OpenCode session's (`ses_…`). */
|
|
51
|
+
export const THREAD = /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/;
|
|
52
|
+
/** Pending You's card tools as Codex and Pi name them: `mcp__pendingyou__post_request`. */
|
|
53
|
+
const MCP_CARD_TOOL = /^mcp__([A-Za-z0-9_]*pendingyou[A-Za-z0-9_]*)__(post_request|update_request|reply_in_thread|cancel_request)$/i;
|
|
54
|
+
/**
|
|
55
|
+
* The card calls the hook reads, on a Pending You server: Codex and Pi name a tool `mcp__pendingyou__post_request`,
|
|
56
|
+
* OpenCode `pendingyou_post_request` (the server's name, `_`, the tool's).
|
|
57
|
+
*/
|
|
58
|
+
const CARD_TOOLS = {
|
|
59
|
+
codex: MCP_CARD_TOOL,
|
|
60
|
+
opencode: /^([A-Za-z0-9_-]*pendingyou[A-Za-z0-9_-]*)_(post_request|update_request|reply_in_thread|cancel_request)$/i,
|
|
61
|
+
pi: MCP_CARD_TOOL,
|
|
62
|
+
};
|
|
63
|
+
/** The card tool a call was, for an app whose hooks read them; null for any other tool. */
|
|
64
|
+
export function cardTool(app, name) {
|
|
65
|
+
const pattern = CARD_TOOLS[app];
|
|
66
|
+
return pattern && typeof name === 'string' ? (pattern.exec(name)?.[2] ?? null) : null;
|
|
67
|
+
}
|
|
68
|
+
const statePath = (io, app) => join(configDir(io), `${app}-threads.json`);
|
|
69
|
+
const leasePath = (io, app, thread) => join(configDir(io), `${app}-listen`, `${thread}.lease`);
|
|
70
|
+
/** What uninstall takes away for an app: its threads' record and its listeners' leases. */
|
|
71
|
+
export const threadFiles = (io, app) => [
|
|
72
|
+
statePath(io, app),
|
|
73
|
+
join(configDir(io), `${app}-listen`),
|
|
74
|
+
];
|
|
45
75
|
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
46
|
-
async function load(io) {
|
|
47
|
-
const file = await readJson(statePath(io)).catch(() => null);
|
|
76
|
+
async function load(io, app) {
|
|
77
|
+
const file = await readJson(statePath(io, app)).catch(() => null);
|
|
48
78
|
return {
|
|
49
79
|
version: 1,
|
|
50
80
|
threads: isObject(file?.threads) ? file.threads : {},
|
|
@@ -52,10 +82,10 @@ async function load(io) {
|
|
|
52
82
|
};
|
|
53
83
|
}
|
|
54
84
|
/** Changes the state under its lock, dropping what's old. */
|
|
55
|
-
async function change(io, edit) {
|
|
56
|
-
const path = statePath(io);
|
|
85
|
+
async function change(io, app, edit) {
|
|
86
|
+
const path = statePath(io, app);
|
|
57
87
|
return withLock(path, io, async () => {
|
|
58
|
-
const state = await load(io);
|
|
88
|
+
const state = await load(io, app);
|
|
59
89
|
edit(state);
|
|
60
90
|
const now = io.now();
|
|
61
91
|
for (const [id, turn] of Object.entries(state.turns))
|
|
@@ -115,26 +145,26 @@ function nameOf(input) {
|
|
|
115
145
|
return text(input.name, 120) ?? text(session?.label, 120);
|
|
116
146
|
}
|
|
117
147
|
/**
|
|
118
|
-
* `pendingyou posted --app
|
|
119
|
-
*
|
|
120
|
-
* never fails.
|
|
148
|
+
* `pendingyou posted --app <app>`: Codex's PostToolUse hook, and what OpenCode's plugin runs after a card call.
|
|
149
|
+
* Remembers what the turn put in front of the person and which cards the thread waits on, then (Codex) makes sure the
|
|
150
|
+
* thread has a listener; OpenCode's plugin starts its own. Prints nothing (Codex ignores it anyway); never fails.
|
|
121
151
|
*/
|
|
122
152
|
export async function posted(io, options) {
|
|
153
|
+
const { app } = options;
|
|
123
154
|
try {
|
|
124
155
|
const input = JSON.parse(await io.readStdin(1000));
|
|
125
156
|
if (!isObject(input))
|
|
126
157
|
return 0;
|
|
127
|
-
const
|
|
158
|
+
const call = cardTool(app, input.tool_name);
|
|
128
159
|
const thread = typeof input.session_id === 'string' ? input.session_id : null;
|
|
129
160
|
const turn = typeof input.turn_id === 'string' ? input.turn_id : null;
|
|
130
|
-
if (!
|
|
161
|
+
if (!call || !thread)
|
|
131
162
|
return 0;
|
|
132
|
-
const call = tool[2];
|
|
133
163
|
const args = isObject(input.tool_input) ? input.tool_input : {};
|
|
134
164
|
const output = resultOf(input.tool_response);
|
|
135
165
|
const requestId = [output?.requestId, args.requestId].find((id) => typeof id === 'string' && REQUEST_ID.test(id));
|
|
136
166
|
const now = io.now();
|
|
137
|
-
const state = await change(io, (state) => {
|
|
167
|
+
const state = await change(io, app, (state) => {
|
|
138
168
|
if (turn && call !== 'cancel_request') {
|
|
139
169
|
const record = state.turns[turn] ?? { at: now, cards: [] };
|
|
140
170
|
record.cards = [...record.cards, wordsIn(args)].slice(-10);
|
|
@@ -162,7 +192,7 @@ export async function posted(io, options) {
|
|
|
162
192
|
waiting.at = now;
|
|
163
193
|
state.threads[thread] = waiting;
|
|
164
194
|
});
|
|
165
|
-
if (Object.keys(state.threads[thread]?.cards ?? {}).length > 0)
|
|
195
|
+
if (app === 'codex' && Object.keys(state.threads[thread]?.cards ?? {}).length > 0)
|
|
166
196
|
await keepListening(io, options.origin, thread);
|
|
167
197
|
return 0;
|
|
168
198
|
}
|
|
@@ -171,31 +201,32 @@ export async function posted(io, options) {
|
|
|
171
201
|
}
|
|
172
202
|
}
|
|
173
203
|
/** Whether this turn put something in front of the person, the words of each card, and whether it was nudged. */
|
|
174
|
-
export async function turnRecord(io, turn) {
|
|
175
|
-
const record = (await load(io)).turns[turn];
|
|
204
|
+
export async function turnRecord(io, turn, app = 'codex') {
|
|
205
|
+
const record = (await load(io, app)).turns[turn];
|
|
176
206
|
return record ? { cards: record.cards, nudged: record.nudged === true } : null;
|
|
177
207
|
}
|
|
178
208
|
/** Remembers that the Stop check asked this turn to go on: at most once a turn. */
|
|
179
|
-
export async function markNudged(io, turn) {
|
|
209
|
+
export async function markNudged(io, turn, app = 'codex') {
|
|
180
210
|
const now = io.now();
|
|
181
|
-
await change(io, (state) => {
|
|
211
|
+
await change(io, app, (state) => {
|
|
182
212
|
state.turns[turn] = { ...(state.turns[turn] ?? { cards: [] }), at: now, nudged: true };
|
|
183
213
|
});
|
|
184
214
|
}
|
|
185
215
|
/** Whether a listener holds this thread now: its lease, touched within LEASE_STALE_MS. */
|
|
186
|
-
async function listening(io, thread) {
|
|
187
|
-
return stat(leasePath(io, thread)).then((info) => Date.now() - info.mtimeMs < LEASE_STALE_MS, () => false);
|
|
216
|
+
async function listening(io, app, thread) {
|
|
217
|
+
return stat(leasePath(io, app, thread)).then((info) => Date.now() - info.mtimeMs < LEASE_STALE_MS, () => false);
|
|
188
218
|
}
|
|
189
219
|
/**
|
|
190
|
-
* Makes sure a thread with cards waiting on the person has a listener: starts one, detached, unless one is there
|
|
191
|
-
* Codex isn't signed in. Quick (three small reads) and never throws: the session-start and next-message hooks call
|
|
220
|
+
* Makes sure a Codex thread with cards waiting on the person has a listener: starts one, detached, unless one is there
|
|
221
|
+
* or Codex isn't signed in. Quick (three small reads) and never throws: the session-start and next-message hooks call
|
|
222
|
+
* it too.
|
|
192
223
|
*/
|
|
193
224
|
export async function keepListening(io, origin, thread) {
|
|
194
225
|
try {
|
|
195
|
-
if (
|
|
226
|
+
if (!THREAD.test(thread))
|
|
196
227
|
return;
|
|
197
|
-
const cards = (await load(io)).threads[thread]?.cards ?? {};
|
|
198
|
-
if (Object.keys(cards).length === 0 || (await listening(io, thread)))
|
|
228
|
+
const cards = (await load(io, 'codex')).threads[thread]?.cards ?? {};
|
|
229
|
+
if (Object.keys(cards).length === 0 || (await listening(io, 'codex', thread)))
|
|
199
230
|
return;
|
|
200
231
|
if (!(await readCredential(io, origin, 'codex')))
|
|
201
232
|
return;
|
|
@@ -265,10 +296,10 @@ export function wakeMessage(ready) {
|
|
|
265
296
|
* Takes the thread's lease, a file holding this listener's own id: null when a live listener holds it. Two listeners
|
|
266
297
|
* starting at once may both write it; the one whose id isn't there at its next renewal stops.
|
|
267
298
|
*/
|
|
268
|
-
async function takeLease(io, thread) {
|
|
269
|
-
const path = leasePath(io, thread);
|
|
299
|
+
async function takeLease(io, app, thread) {
|
|
300
|
+
const path = leasePath(io, app, thread);
|
|
270
301
|
await mkdir(join(path, '..'), { recursive: true, mode: 0o700 });
|
|
271
|
-
if (await listening(io, thread))
|
|
302
|
+
if (await listening(io, app, thread))
|
|
272
303
|
return null;
|
|
273
304
|
const id = randomBytes(8).toString('hex');
|
|
274
305
|
await rm(path, { recursive: true, force: true });
|
|
@@ -281,36 +312,98 @@ async function takeLease(io, thread) {
|
|
|
281
312
|
}
|
|
282
313
|
}
|
|
283
314
|
/** Renews the lease while it's still this listener's (`id`); false once another listener has taken it. */
|
|
284
|
-
async function renew(io, thread, id) {
|
|
285
|
-
const path = leasePath(io, thread);
|
|
315
|
+
async function renew(io, app, thread, id) {
|
|
316
|
+
const path = leasePath(io, app, thread);
|
|
286
317
|
if ((await readFile(path, 'utf8').catch(() => null)) !== id)
|
|
287
318
|
return false;
|
|
288
319
|
const now = new Date();
|
|
289
320
|
await utimes(path, now, now).catch(() => { });
|
|
290
321
|
return true;
|
|
291
322
|
}
|
|
323
|
+
/** Codex: `codex queue`, which starts the turn itself. */
|
|
324
|
+
const byCodexQueue = (io, thread) => async (message) => (await io.run('codex', ['queue', '--thread', thread, '--message', message], 30_000)).code === 0;
|
|
325
|
+
/**
|
|
326
|
+
* OpenCode and Pi: the plugin (Pi: the extension) that started this listener reads the wake on stdout and starts the
|
|
327
|
+
* turn, then says on stdin whether it did. stdin closing means the plugin (the app) is gone: `gone` stops the listener.
|
|
328
|
+
*/
|
|
329
|
+
function byPlugin(io, gone) {
|
|
330
|
+
const queue = [];
|
|
331
|
+
let waiting = null;
|
|
332
|
+
let closed = false;
|
|
333
|
+
const settle = (line) => {
|
|
334
|
+
const resolve = waiting;
|
|
335
|
+
waiting = null;
|
|
336
|
+
resolve?.(line);
|
|
337
|
+
};
|
|
338
|
+
void (async () => {
|
|
339
|
+
try {
|
|
340
|
+
for await (const line of io.lines()) {
|
|
341
|
+
if (waiting)
|
|
342
|
+
settle(line);
|
|
343
|
+
else
|
|
344
|
+
queue.push(line);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch { }
|
|
348
|
+
closed = true;
|
|
349
|
+
gone();
|
|
350
|
+
settle(null);
|
|
351
|
+
})();
|
|
352
|
+
const reply = () => {
|
|
353
|
+
if (queue.length)
|
|
354
|
+
return Promise.resolve(queue.shift());
|
|
355
|
+
if (closed)
|
|
356
|
+
return Promise.resolve(null);
|
|
357
|
+
return new Promise((resolve) => {
|
|
358
|
+
const timer = setTimeout(() => settle(null), REPLY_MS);
|
|
359
|
+
timer.unref?.();
|
|
360
|
+
waiting = (line) => {
|
|
361
|
+
clearTimeout(timer);
|
|
362
|
+
resolve(line);
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
};
|
|
366
|
+
return async (message, requests) => {
|
|
367
|
+
io.out(`${JSON.stringify({ wake: message, requests })}\n`);
|
|
368
|
+
const answered = await reply();
|
|
369
|
+
try {
|
|
370
|
+
return answered !== null && JSON.parse(answered).delivered === true;
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
292
377
|
/**
|
|
293
|
-
* `pendingyou listen --app
|
|
294
|
-
*
|
|
295
|
-
* the sign-in ends,
|
|
378
|
+
* `pendingyou listen --app <app> --thread <id>`: waits on Pending You for the thread's cards, and wakes the thread
|
|
379
|
+
* (Codex: `codex queue`; OpenCode and Pi: through the plugin or extension) when one is the agent's move. Quiet but for
|
|
380
|
+
* the wake lines it hands a plugin; exits when the thread has none left, the app can't take the wake, the sign-in ends,
|
|
381
|
+
* or after LISTEN_MS.
|
|
296
382
|
*/
|
|
297
383
|
export async function listen(io, options) {
|
|
298
|
-
const { origin, thread } = options;
|
|
299
|
-
const lease = await takeLease(io, thread);
|
|
384
|
+
const { origin, app, thread } = options;
|
|
385
|
+
const lease = await takeLease(io, app, thread);
|
|
300
386
|
if (!lease)
|
|
301
387
|
return 0;
|
|
388
|
+
const controller = new AbortController();
|
|
389
|
+
const stop = () => controller.abort();
|
|
390
|
+
if (io.signal.aborted)
|
|
391
|
+
stop();
|
|
392
|
+
io.signal.addEventListener('abort', stop, { once: true });
|
|
393
|
+
const signal = controller.signal;
|
|
394
|
+
const deliver = app === 'codex' ? byCodexQueue(io, thread) : byPlugin(io, stop);
|
|
302
395
|
try {
|
|
303
396
|
const started = io.now();
|
|
304
397
|
let since;
|
|
305
398
|
let failures = 0;
|
|
306
|
-
while (io.now() - started < LISTEN_MS && !
|
|
307
|
-
const cards = (await load(io)).threads[thread]?.cards ?? {};
|
|
399
|
+
while (io.now() - started < LISTEN_MS && !signal.aborted) {
|
|
400
|
+
const cards = (await load(io, app)).threads[thread]?.cards ?? {};
|
|
308
401
|
if (Object.keys(cards).length === 0)
|
|
309
402
|
return 0;
|
|
310
|
-
if (!(await renew(io, thread, lease)))
|
|
403
|
+
if (!(await renew(io, app, thread, lease)))
|
|
311
404
|
return 0;
|
|
312
405
|
const query = new URLSearchParams({
|
|
313
|
-
source:
|
|
406
|
+
source: app,
|
|
314
407
|
limit: '20',
|
|
315
408
|
wait: String(LONG_POLL_SECONDS),
|
|
316
409
|
});
|
|
@@ -318,19 +411,19 @@ export async function listen(io, options) {
|
|
|
318
411
|
query.set('since', since);
|
|
319
412
|
let requests;
|
|
320
413
|
try {
|
|
321
|
-
const { status, body } = await getJson(io, origin, `/mcp/cli/answers?${query}`, (LONG_POLL_SECONDS + 20) * 1000,
|
|
414
|
+
const { status, body } = await getJson(io, origin, `/mcp/cli/answers?${query}`, (LONG_POLL_SECONDS + 20) * 1000, signal, { app });
|
|
322
415
|
if (status !== 200)
|
|
323
416
|
throw new Unavailable(`Pending You answered ${status}.`);
|
|
324
417
|
requests = Array.isArray(body.requests) ? body.requests : [];
|
|
325
418
|
failures = 0;
|
|
326
419
|
}
|
|
327
420
|
catch (error) {
|
|
328
|
-
if (error instanceof SignInNeeded ||
|
|
421
|
+
if (error instanceof SignInNeeded || signal.aborted)
|
|
329
422
|
return 0;
|
|
330
423
|
// Renewed first: no pause here outlasts the lease.
|
|
331
|
-
if (!(await renew(io, thread, lease)))
|
|
424
|
+
if (!(await renew(io, app, thread, lease)))
|
|
332
425
|
return 0;
|
|
333
|
-
await io.sleep(BACKOFF_MS[Math.min(failures++, BACKOFF_MS.length - 1)],
|
|
426
|
+
await io.sleep(BACKOFF_MS[Math.min(failures++, BACKOFF_MS.length - 1)], signal);
|
|
334
427
|
continue;
|
|
335
428
|
}
|
|
336
429
|
for (const heard of requests)
|
|
@@ -342,7 +435,7 @@ export async function listen(io, options) {
|
|
|
342
435
|
const card = cards[heard.requestId];
|
|
343
436
|
if (!card || !heard.ready)
|
|
344
437
|
continue;
|
|
345
|
-
// Handed over already (the person wrote in
|
|
438
|
+
// Handed over already (the person wrote in the app since): nothing to wake it for.
|
|
346
439
|
if (await wasHanded(io, origin, heard.requestId, momentOf(heard)))
|
|
347
440
|
done.push(heard.requestId);
|
|
348
441
|
else
|
|
@@ -350,10 +443,9 @@ export async function listen(io, options) {
|
|
|
350
443
|
}
|
|
351
444
|
const message = wakeMessage(ready);
|
|
352
445
|
if (message) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
await change(io, (state) => {
|
|
446
|
+
// The app doesn't know the thread (archived, gone), or can't be reached from here: the hooks hand answers over.
|
|
447
|
+
if (!(await deliver(message, ready.map(({ heard }) => heard.requestId)))) {
|
|
448
|
+
await change(io, app, (state) => {
|
|
357
449
|
delete state.threads[thread];
|
|
358
450
|
});
|
|
359
451
|
return 0;
|
|
@@ -367,20 +459,21 @@ export async function listen(io, options) {
|
|
|
367
459
|
if (cards[heard.requestId] && isClosed(heard) && !done.includes(heard.requestId))
|
|
368
460
|
done.push(heard.requestId);
|
|
369
461
|
if (done.length)
|
|
370
|
-
await change(io, (state) => {
|
|
462
|
+
await change(io, app, (state) => {
|
|
371
463
|
const waiting = state.threads[thread];
|
|
372
464
|
if (waiting)
|
|
373
465
|
for (const id of done)
|
|
374
466
|
delete waiting.cards[id];
|
|
375
467
|
});
|
|
376
468
|
if (io.now() - started > PATIENT_AFTER_MS)
|
|
377
|
-
await io.sleep(PATIENT_PAUSE_MS,
|
|
469
|
+
await io.sleep(PATIENT_PAUSE_MS, signal);
|
|
378
470
|
}
|
|
379
471
|
return 0;
|
|
380
472
|
}
|
|
381
473
|
finally {
|
|
474
|
+
io.signal.removeEventListener('abort', stop);
|
|
382
475
|
// Only its own: a listener that took over keeps the thread.
|
|
383
|
-
if ((await readFile(leasePath(io, thread), 'utf8').catch(() => null)) === lease)
|
|
384
|
-
await rm(leasePath(io, thread), { force: true }).catch(() => { });
|
|
476
|
+
if ((await readFile(leasePath(io, app, thread), 'utf8').catch(() => null)) === lease)
|
|
477
|
+
await rm(leasePath(io, app, thread), { force: true }).catch(() => { });
|
|
385
478
|
}
|
|
386
479
|
}
|
package/dist/apps/ids.js
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
// (registry.ts lists them; docs/apps.md says what a module does). Kept apart from the modules so the argument parser can
|
|
4
4
|
// read the list without loading them.
|
|
5
5
|
/** Every app with a module, in the order init sets them up and status lists them. */
|
|
6
|
-
export const APP_IDS = ['claude-code', 'codex'];
|
|
6
|
+
export const APP_IDS = ['claude-code', 'codex', 'opencode', 'pi'];
|
|
7
7
|
/** What people call each app: "Claude Code on devbox" is a connection's name on the Assistants page. */
|
|
8
8
|
export const APP_NAMES = {
|
|
9
9
|
'claude-code': 'Claude Code',
|
|
10
10
|
codex: 'Codex',
|
|
11
|
+
opencode: 'OpenCode',
|
|
12
|
+
pi: 'Pi',
|
|
11
13
|
};
|
|
12
14
|
/**
|
|
13
15
|
* The app a command acts for when it names none: Claude Code, the only app before 0.11.0, so hook lines and helpers
|