agent-yes 1.169.0 → 1.170.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.
Files changed (62) hide show
  1. package/dist/{SUPPORTED_CLIS-D1WUoZA9.js → SUPPORTED_CLIS-ByppVvWO.js} +2 -2
  2. package/dist/SUPPORTED_CLIS-C_c097RZ.js +8 -0
  3. package/dist/{agent-yes.config-Bl08kMpl.js → agent-yes.config-ugPYTPOd.js} +2 -2
  4. package/dist/{agentYesHome-_eJa3DaX.js → agentYesHome-CtHb5b71.js} +1 -1
  5. package/dist/cli.js +10 -212
  6. package/dist/{configShared-CEyhl0WH.js → configShared-0MnIQ652.js} +1 -1
  7. package/dist/{e2e-BeKjLhmO.js → e2e-jb0Hp43q.js} +1 -1
  8. package/dist/{forkNested-UCKPEgSI.js → forkNested-DhJxa4q4.js} +2 -2
  9. package/dist/{globalPidIndex-DlmmJlO8.js → globalPidIndex-CoNr7tS8.js} +1 -1
  10. package/dist/index.js +4 -4
  11. package/dist/notifyDaemon-D5jtpCAR.js +591 -0
  12. package/dist/{openBrowser-CCF1iuQK.js → openBrowser-ChR4llYa.js} +1 -1
  13. package/dist/{pidStore-BfoBWUjc.js → pidStore-BIvsBQ8X.js} +3 -3
  14. package/dist/pidStore-CLxwp0qI.js +5 -0
  15. package/dist/{reaper-C-eWAxIj.js → reaper-CWF2_ATd.js} +27 -3
  16. package/dist/{reaper-BQgDb85W.js → reaper-jfSmog15.js} +1 -1
  17. package/dist/{remotes-cx_GDFPj.js → remotes-BQMr4_En.js} +3 -3
  18. package/dist/{remotes-BmAPylU_.js → remotes-CC-4GuJb.js} +3 -3
  19. package/dist/{runningLock-BobVW1_A.js → runningLock-CNMl13dC.js} +1 -1
  20. package/dist/rustBinary-DsO0IlNq.js +228 -0
  21. package/dist/{schedule-DTHFMzDj.js → schedule-ChTPIcXf.js} +7 -7
  22. package/dist/{serve-D-joIHrL.js → serve-De1gAWNI.js} +120 -26
  23. package/dist/{setup-K6VTmirE.js → setup-nlMqyEPB.js} +3 -3
  24. package/dist/{share-D_e2Fwiy.js → share-QregR8a_.js} +2 -2
  25. package/dist/spawnGate-BFhva-2F.js +5 -0
  26. package/dist/{spawnGate-DzPfa1PZ.js → spawnGate-IJDByl2U.js} +3 -3
  27. package/dist/{subcommands-BZPW0UVn.js → subcommands-BFlpV23b.js} +1049 -44
  28. package/dist/subcommands-DLDpwrfG.js +9 -0
  29. package/dist/{tray-CWUpaZF4.js → tray-DXr7iK3E.js} +2 -2
  30. package/dist/{ts-DeahYDN3.js → ts-DsxejviJ.js} +10 -10
  31. package/dist/{versionChecker-C7KP-aoh.js → versionChecker-Czk3aHqy.js} +2 -2
  32. package/dist/{webrtcLink-BG0Xc4-W.js → webrtcLink-B7REGtK2.js} +2 -2
  33. package/dist/{webrtcRemote-SybKurg9.js → webrtcRemote-Bx-eD_0I.js} +3 -3
  34. package/dist/{workspaceConfig-BC03X4Y1.js → workspaceConfig-BgqK-31W.js} +37 -6
  35. package/lab/ui/console-logic.js +79 -10
  36. package/lab/ui/index.html +174 -8
  37. package/lab/ui/landing.html +11 -1
  38. package/package.json +1 -1
  39. package/scripts/build-rs.ts +9 -5
  40. package/ts/badges.spec.ts +51 -1
  41. package/ts/badges.ts +35 -1
  42. package/ts/index.ts +13 -4
  43. package/ts/notifyDaemon.spec.ts +276 -0
  44. package/ts/notifyDaemon.ts +698 -0
  45. package/ts/notifyInbox.spec.ts +192 -0
  46. package/ts/notifyInbox.ts +294 -0
  47. package/ts/notifyRouter.spec.ts +355 -0
  48. package/ts/notifyRouter.ts +288 -0
  49. package/ts/notifyStore.spec.ts +308 -0
  50. package/ts/notifyStore.ts +439 -0
  51. package/ts/reaper.spec.ts +18 -1
  52. package/ts/reaper.ts +33 -1
  53. package/ts/rustBinary.ts +30 -0
  54. package/ts/serve.ts +180 -14
  55. package/ts/subcommands.spec.ts +341 -0
  56. package/ts/subcommands.ts +764 -29
  57. package/ts/workspaceConfig.spec.ts +48 -0
  58. package/ts/workspaceConfig.ts +46 -3
  59. package/dist/SUPPORTED_CLIS-BFV10E3f.js +0 -8
  60. package/dist/pidStore-5qyLX3Fh.js +0 -5
  61. package/dist/spawnGate-CQ1Il3Xk.js +0 -5
  62. package/dist/subcommands-DuGwtmz5.js +0 -9
@@ -1,15 +1,485 @@
1
1
  import { t as invokedCliName } from "./invokedCli-uqM2YYA7.js";
2
- import { t as agentYesHome } from "./agentYesHome-_eJa3DaX.js";
3
- import { a as updateGlobalPidStatus, i as readGlobalPids } from "./globalPidIndex-DlmmJlO8.js";
4
- import { t as loadSharedCliDefaults } from "./configShared-CEyhl0WH.js";
5
- import { n as isWebrtcSpec } from "./webrtcLink-BG0Xc4-W.js";
6
- import { a as resolveRemoteSpec, i as readRemotes } from "./remotes-BmAPylU_.js";
2
+ import { n as logger } from "./logger-CDIsZ-Pp.js";
3
+ import { t as agentYesHome } from "./agentYesHome-CtHb5b71.js";
4
+ import { a as updateGlobalPidStatus, i as readGlobalPids } from "./globalPidIndex-CoNr7tS8.js";
5
+ import { t as loadSharedCliDefaults } from "./configShared-0MnIQ652.js";
6
+ import { n as isWebrtcSpec } from "./webrtcLink-B7REGtK2.js";
7
+ import { a as resolveRemoteSpec, i as readRemotes } from "./remotes-CC-4GuJb.js";
7
8
  import ms from "ms";
8
9
  import yargs from "yargs";
9
- import { appendFile, mkdir, open, readFile, stat, writeFile } from "fs/promises";
10
+ import { appendFile, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "fs/promises";
10
11
  import { homedir } from "os";
11
12
  import path from "path";
13
+ import { randomUUID } from "node:crypto";
14
+ import os from "node:os";
12
15
 
16
+ //#region ts/notifyInbox.ts
17
+ /**
18
+ * Pure, fs-free core for the subagent→parent notification inbox — the storage
19
+ * and watermark layer under `ay notify` / `ay notifyd`.
20
+ *
21
+ * Motivation (real incident): a parent agent fanned out sub-agents that finished
22
+ * their work but sat at an idle `❯` prompt WITHOUT exiting (claude-yes does not
23
+ * exit on idle). Claude Code's background-task notification only fires on process
24
+ * EXIT, so the parent never learned the children went idle and left them parked
25
+ * 16 minutes. `ay ls --watch` already streams transitions, but it is PULL: the
26
+ * parent must run the watch loop. The whole point of this pain is the parent is
27
+ * NOT watching. So we accumulate qualifying edges into a per-parent append-only
28
+ * inbox that the parent drains on its own schedule (its Monitor loop), with a
29
+ * persisted cursor so a parent that restarts reads only the unread edges.
30
+ *
31
+ * This module is the pure part (path math, NDJSON (de)serialization, seq/cursor
32
+ * filtering, retention decisions) so it is trivially unit-testable, mirroring
33
+ * `lsWatch.ts` / `needsInput.ts` / `resultEnvelope.ts`. The fs read/write + lock
34
+ * + CLI live in `subcommands.ts`; the detection/debounce lives in
35
+ * `notifyRouter.ts`.
36
+ */
37
+ /** Root dir for all notification state. */
38
+ function notifyDir() {
39
+ return path.join(agentYesHome(), "notify");
40
+ }
41
+ /** Per-host inbox directory (pids are only unique within a host). */
42
+ function inboxDir(host) {
43
+ return path.join(notifyDir(), "inbox", sanitizeHost(host));
44
+ }
45
+ /** Absolute path of one parent's append-only NDJSON inbox. */
46
+ function inboxPath(host, parentPid) {
47
+ return path.join(inboxDir(host), `${parentPid}.ndjson`);
48
+ }
49
+ /** Sidecar seq counter for a parent inbox (last allocated seq). */
50
+ function seqPath(host, parentPid) {
51
+ return path.join(inboxDir(host), `${parentPid}.seq`);
52
+ }
53
+ /** Per-consumer cursor directory for a parent inbox. */
54
+ function cursorDir(host, parentPid) {
55
+ return path.join(notifyDir(), "cursors", sanitizeHost(host), String(parentPid));
56
+ }
57
+ /** Absolute path of one consumer's cursor file for a parent inbox. */
58
+ function cursorPath(host, parentPid, consumer = "parent") {
59
+ return path.join(cursorDir(host, parentPid), `${sanitizeConsumer(consumer)}.json`);
60
+ }
61
+ /** The daemon singleton lock dir (mkdir-based, like pidStore). */
62
+ function daemonLockDir() {
63
+ return path.join(notifyDir(), "notifyd.lock");
64
+ }
65
+ /**
66
+ * Owner metadata inside the lock dir ({pid, started_at, ts}) — the SINGLE source
67
+ * of truth for the running daemon's identity, used for stale-lock steal and for
68
+ * `notifyd status`/`stop` (a running daemon refreshes `ts` each tick).
69
+ */
70
+ function daemonLockOwnerPath() {
71
+ return path.join(daemonLockDir(), "owner.json");
72
+ }
73
+ /** Registry dir of parents currently running `ay notify watch` (heartbeats). */
74
+ function watchersDir() {
75
+ return path.join(notifyDir(), "watchers");
76
+ }
77
+ /** Heartbeat file for one watching parent. */
78
+ function watcherPath(parentPid) {
79
+ return path.join(watchersDir(), `${parentPid}.json`);
80
+ }
81
+ /** A watcher heartbeat is "live" if refreshed within this window. */
82
+ const WATCHER_TTL_MS = 15e3;
83
+ function sanitizeSegment(s, fallback) {
84
+ const cleaned = s.replace(/[^A-Za-z0-9._-]/g, "_");
85
+ return cleaned === "" || cleaned === "." || cleaned === ".." ? fallback : cleaned;
86
+ }
87
+ function sanitizeHost(host) {
88
+ return sanitizeSegment(host, "localhost");
89
+ }
90
+ function sanitizeConsumer(consumer) {
91
+ return sanitizeSegment(consumer, "parent");
92
+ }
93
+ /** Serialize one event to its NDJSON line (no trailing newline). */
94
+ function serializeEvent(ev) {
95
+ return JSON.stringify(ev);
96
+ }
97
+ /**
98
+ * Parse an inbox's raw text into events. Tolerant by design: a torn final line
99
+ * (a writer mid-append) or any unparseable line is skipped, never throws — a
100
+ * reader must survive a concurrent writer. Order is file order (= seq order,
101
+ * since a single locked writer appends in increasing seq).
102
+ */
103
+ function parseInboxText(text) {
104
+ const out = [];
105
+ for (const line of text.split("\n")) {
106
+ const t = line.trim();
107
+ if (!t) continue;
108
+ try {
109
+ const ev = JSON.parse(t);
110
+ if (isValidEvent(ev)) out.push(ev);
111
+ } catch {}
112
+ }
113
+ return out;
114
+ }
115
+ const EDGES = new Set([
116
+ "needs_input",
117
+ "idle",
118
+ "exited"
119
+ ]);
120
+ /**
121
+ * Full shape validation at the storage boundary — a torn/partial line missing
122
+ * the correlation fields must never flow through to a consumer's output. Every
123
+ * event we surface has the fields a reader relies on.
124
+ */
125
+ function isValidEvent(ev) {
126
+ if (!ev || typeof ev !== "object") return false;
127
+ const e = ev;
128
+ return typeof e.seq === "number" && typeof e.ts === "number" && typeof e.host === "string" && typeof e.parent_pid === "number" && typeof e.child_pid === "number" && typeof e.cli === "string" && typeof e.cwd === "string" && typeof e.edge === "string" && EDGES.has(e.edge) && typeof e.state === "string" && (e.question === null || typeof e.question === "string");
129
+ }
130
+ /** Next seq to allocate given the last stored seq counter (or the inbox max). */
131
+ function nextSeq(lastSeq) {
132
+ return (Number.isFinite(lastSeq) && lastSeq > 0 ? lastSeq : 0) + 1;
133
+ }
134
+ /** Highest seq present in a parsed inbox (0 when empty). */
135
+ function maxSeq(events) {
136
+ let m = 0;
137
+ for (const e of events) if (e.seq > m) m = e.seq;
138
+ return m;
139
+ }
140
+ /**
141
+ * Filter events for `--since <seq>`: strictly-greater than the given seq. A
142
+ * `since` of 0/undefined returns everything.
143
+ */
144
+ function filterSinceSeq(events, sinceSeq) {
145
+ if (!sinceSeq || sinceSeq <= 0) return events.slice();
146
+ return events.filter((e) => e.seq > sinceSeq);
147
+ }
148
+ /** Filter events after a wall-clock `--since <ts>` (inclusive lower bound). */
149
+ function filterSinceTs(events, sinceTs) {
150
+ if (!sinceTs || sinceTs <= 0) return events.slice();
151
+ return events.filter((e) => e.ts >= sinceTs);
152
+ }
153
+ /** Unread = seq strictly greater than the consumer's cursor. */
154
+ function filterUnread(events, cursorSeq) {
155
+ return events.filter((e) => e.seq > (cursorSeq > 0 ? cursorSeq : 0));
156
+ }
157
+ /** Parse a cursor file's text; a missing/garbage cursor reads as seq 0. */
158
+ function parseCursor(text) {
159
+ if (!text) return { seq: 0 };
160
+ try {
161
+ const c = JSON.parse(text);
162
+ return { seq: Number.isFinite(c?.seq) && c.seq > 0 ? c.seq : 0 };
163
+ } catch {
164
+ return { seq: 0 };
165
+ }
166
+ }
167
+ function serializeCursor(seq) {
168
+ return JSON.stringify({ seq: Math.max(0, Math.floor(seq)) });
169
+ }
170
+ /**
171
+ * Rotation decision (pure): given an inbox's events, a byte cap, and the minimum
172
+ * un-acked cursor across ALL consumers, return the events to KEEP after
173
+ * rotating. Critically, an event with `seq > protectAboveSeq` is NEVER dropped —
174
+ * that is the at-least-once guarantee: we must not evict an edge no consumer has
175
+ * acknowledged yet, even under the byte cap. Below that watermark we keep the
176
+ * newest events that fit the cap (and at least `minKeep`).
177
+ */
178
+ function rotateKeep(events, capBytes, protectAboveSeq = 0, minKeep = 100) {
179
+ if (events.length <= minKeep) return events.slice();
180
+ const kept = [];
181
+ let bytes = 0;
182
+ for (let i = events.length - 1; i >= 0; i--) {
183
+ const e = events[i];
184
+ const line = serializeEvent(e) + "\n";
185
+ bytes += line.length;
186
+ if (e.seq > protectAboveSeq || kept.length < minKeep || bytes <= capBytes) kept.push(e);
187
+ else break;
188
+ }
189
+ kept.reverse();
190
+ return kept;
191
+ }
192
+
193
+ //#endregion
194
+ //#region ts/notifyStore.ts
195
+ /**
196
+ * Filesystem side of the notification inbox — the impure half of the pure
197
+ * `notifyInbox.ts` path/serialization core. Handles the locked append (so
198
+ * concurrent daemon ticks / future writers never interleave a seq), inbox and
199
+ * cursor reads/writes, and retention GC.
200
+ *
201
+ * The lock is a mkdir-based advisory lock (same primitive `pidStore` uses), held
202
+ * only for the microseconds of a read-counter → append-line → write-counter, so
203
+ * contention is negligible even with many parents.
204
+ */
205
+ /** Stable local host id — the pid namespace is per-host. */
206
+ function hostId() {
207
+ return os.hostname() || "localhost";
208
+ }
209
+ function pidAlive(pid) {
210
+ if (!Number.isFinite(pid) || pid <= 0) return false;
211
+ try {
212
+ process.kill(pid, 0);
213
+ return true;
214
+ } catch (e) {
215
+ return e.code === "EPERM";
216
+ }
217
+ }
218
+ /**
219
+ * Pure steal decision for a per-inbox lock (extracted so it's unit-testable):
220
+ * steal the lock ONLY when its holder is provably gone (torn/missing owner on a
221
+ * lock that has EXISTED longer than the grace, or a dead pid) or its heartbeat is
222
+ * stale — NEVER merely because the contender has waited a while.
223
+ *
224
+ * Crucially, the torn-owner grace is measured from the LOCK INSTANCE's age
225
+ * (`lockAgeMs` = now − lockDir mtime), NOT from how long the contender has been
226
+ * waiting. Otherwise a contender that waited out one holder could instantly steal
227
+ * the NEXT holder's freshly-created lock during its mkdir→write-owner window
228
+ * (torn but brand-new) — letting two writers into the critical section.
229
+ */
230
+ function shouldStealLock(ownerRaw, now, opts) {
231
+ const graceMs = opts.graceMs ?? 1e3;
232
+ let ownerPid = 0;
233
+ let ownerTs = 0;
234
+ let parsed = false;
235
+ try {
236
+ const o = JSON.parse(ownerRaw);
237
+ ownerPid = o?.pid ?? 0;
238
+ ownerTs = o?.ts ?? 0;
239
+ parsed = true;
240
+ } catch {}
241
+ if (!parsed || ownerPid <= 0 || ownerTs <= 0) return opts.lockAgeMs > graceMs;
242
+ const holderDead = ownerPid !== opts.selfPid && !opts.isAlive(ownerPid);
243
+ const holderStale = now - ownerTs > opts.staleMs;
244
+ return holderDead || holderStale;
245
+ }
246
+ async function ensureDir(dir) {
247
+ await mkdir(dir, { recursive: true }).catch(() => {});
248
+ }
249
+ /**
250
+ * Atomically overwrite a single file (temp + rename) so a concurrent reader never
251
+ * observes a torn/partial write — the sweep invariant for every single-file
252
+ * mutation (cursor, watcher heartbeat, lock owner). Returns whether it landed.
253
+ */
254
+ async function atomicWrite(file, data) {
255
+ const tmp = `${file}.${randomUUID()}.tmp`;
256
+ try {
257
+ await writeFile(tmp, data);
258
+ await rename(tmp, file);
259
+ return true;
260
+ } catch {
261
+ await rm(tmp, { force: true }).catch(() => {});
262
+ return false;
263
+ }
264
+ }
265
+ /**
266
+ * Acquire a per-inbox lock; returns a release fn. Ownership is proven ONLY by a
267
+ * successful `mkdir(recursive:false)` (atomic exclusive create), and stealing is
268
+ * driven by HOLDER LIVENESS, not elapsed wait time: the holder writes an
269
+ * `owner` file ({pid, ts}) on acquire AND heartbeats its ts for the whole time
270
+ * it holds the lock, so a contender steals ONLY when that holder is dead or its
271
+ * heartbeat is stale. A live holder whose critical section legitimately runs long
272
+ * (a big GC rewrite, a slow disk) is never robbed — its heartbeat stays fresh;
273
+ * and a wedged holder stops heartbeating, so staleMs still reclaims it. There is
274
+ * deliberately NO wall-clock backstop: elapsed wait time alone never steals, so a
275
+ * live holder is inviolable.
276
+ */
277
+ async function acquireLock(lockDir, staleMs = 3e4) {
278
+ const ownerFile = path.join(lockDir, "owner");
279
+ const token = randomUUID();
280
+ const readOwnerToken = async () => {
281
+ try {
282
+ return JSON.parse(await readFile(ownerFile, "utf8")).token ?? null;
283
+ } catch {
284
+ return null;
285
+ }
286
+ };
287
+ const stampOwner = async () => {
288
+ const tmp = `${ownerFile}.${token}.tmp`;
289
+ try {
290
+ await writeFile(tmp, JSON.stringify({
291
+ pid: process.pid,
292
+ ts: Date.now(),
293
+ token
294
+ }));
295
+ await rename(tmp, ownerFile);
296
+ return true;
297
+ } catch {
298
+ await rm(tmp, { force: true }).catch(() => {});
299
+ return false;
300
+ }
301
+ };
302
+ const lockAgeMs = async () => {
303
+ const m = await stat(lockDir).then((s) => s.mtimeMs).catch(() => Date.now());
304
+ return Date.now() - m;
305
+ };
306
+ for (;;) try {
307
+ await mkdir(lockDir, { recursive: false });
308
+ if (!await stampOwner()) {
309
+ await rm(lockDir, {
310
+ recursive: true,
311
+ force: true
312
+ }).catch(() => {});
313
+ await new Promise((r) => setTimeout(r, 15));
314
+ continue;
315
+ }
316
+ const beat = setInterval(() => {
317
+ (async () => {
318
+ if (await readOwnerToken() !== token) {
319
+ clearInterval(beat);
320
+ return;
321
+ }
322
+ await stampOwner();
323
+ })();
324
+ }, Math.max(500, Math.floor(staleMs / 3)));
325
+ if (typeof beat.unref === "function") beat.unref();
326
+ return async () => {
327
+ clearInterval(beat);
328
+ if (await readOwnerToken() === token) await rm(lockDir, {
329
+ recursive: true,
330
+ force: true
331
+ }).catch(() => {});
332
+ };
333
+ } catch (e) {
334
+ if (e.code !== "EEXIST") throw e;
335
+ if (shouldStealLock(await readFile(ownerFile, "utf8").catch(() => ""), Date.now(), {
336
+ staleMs,
337
+ lockAgeMs: await lockAgeMs(),
338
+ selfPid: process.pid,
339
+ isAlive: pidAlive
340
+ })) {
341
+ await rm(lockDir, {
342
+ recursive: true,
343
+ force: true
344
+ }).catch(() => {});
345
+ continue;
346
+ }
347
+ await new Promise((r) => setTimeout(r, 15));
348
+ }
349
+ }
350
+ /**
351
+ * Append one event to a parent's inbox under lock, allocating the next seq. The
352
+ * caller passes the event WITHOUT seq (we stamp it). Returns the stamped seq.
353
+ */
354
+ async function appendEvent(parentPid, ev) {
355
+ const host = ev.host;
356
+ const dir = inboxDir(host);
357
+ await ensureDir(dir);
358
+ const release = await acquireLock(path.join(dir, `${parentPid}.lock`));
359
+ try {
360
+ let last = 0;
361
+ const counterRaw = await readFile(seqPath(host, parentPid), "utf8").catch(() => "");
362
+ const parsed = parseInt(counterRaw.trim(), 10);
363
+ if (Number.isFinite(parsed) && parsed > 0) last = parsed;
364
+ else last = maxSeq(parseInboxText(await readFile(inboxPath(host, parentPid), "utf8").catch(() => "")));
365
+ const seq = nextSeq(last);
366
+ const full = {
367
+ ...ev,
368
+ seq
369
+ };
370
+ await writeFile(seqPath(host, parentPid), String(seq));
371
+ await appendFile(inboxPath(host, parentPid), serializeEvent(full) + "\n");
372
+ return seq;
373
+ } finally {
374
+ await release();
375
+ }
376
+ }
377
+ /** Read + parse a parent's inbox (empty array when none). */
378
+ async function readInbox(host, parentPid) {
379
+ return parseInboxText(await readFile(inboxPath(host, parentPid), "utf8").catch(() => ""));
380
+ }
381
+ /** All parent pids that currently have an inbox on this host. */
382
+ async function listInboxParents(host) {
383
+ const names = await readdir(inboxDir(host)).catch(() => []);
384
+ const pids = [];
385
+ for (const n of names) {
386
+ const m = /^(\d+)\.ndjson$/.exec(n);
387
+ if (m) pids.push(parseInt(m[1], 10));
388
+ }
389
+ return pids;
390
+ }
391
+ async function getCursor(host, parentPid, consumer = "parent") {
392
+ return parseCursor(await readFile(cursorPath(host, parentPid, consumer), "utf8").catch(() => null)).seq;
393
+ }
394
+ /**
395
+ * Minimum cursor across ALL consumers of a parent inbox — the watermark below
396
+ * which rotation may evict events (nothing above it has been acked by every
397
+ * reader). Returns 0 when there are no consumers — which PROTECTS EVERYTHING:
398
+ * with a 0 watermark, `rotateKeep` treats every event as unacked and evicts
399
+ * nothing (an inbox nobody reads is never trimmed).
400
+ */
401
+ async function minConsumerCursor(host, parentPid) {
402
+ const cursors = (await readdir(cursorDir(host, parentPid)).catch(() => [])).filter((n) => n.endsWith(".json")).map((n) => n.slice(0, -5));
403
+ if (cursors.length === 0) return 0;
404
+ let min = Infinity;
405
+ for (const c of cursors) min = Math.min(min, await getCursor(host, parentPid, c));
406
+ return Number.isFinite(min) ? min : 0;
407
+ }
408
+ async function setCursor(host, parentPid, seq, consumer = "parent") {
409
+ const p = cursorPath(host, parentPid, consumer);
410
+ await ensureDir(path.dirname(p));
411
+ await atomicWrite(p, serializeCursor(seq));
412
+ }
413
+ /**
414
+ * Retention: delete inboxes (+ their seq counter + cursors) for parents that are
415
+ * both dead and unreferenced by any live child. `capBytes` also rotates a live
416
+ * inbox that has grown past the cap, keeping the newest events.
417
+ */
418
+ async function gcInboxes(host, livePids, liveChildParentPids, capBytes = 10 * 1024 * 1024) {
419
+ const parents = await listInboxParents(host);
420
+ for (const p of parents) {
421
+ const release = await acquireLock(path.join(inboxDir(host), `${p}.lock`));
422
+ try {
423
+ if (!livePids.has(p) && !liveChildParentPids.has(p)) {
424
+ await rm(inboxPath(host, p), { force: true }).catch(() => {});
425
+ await rm(seqPath(host, p), { force: true }).catch(() => {});
426
+ await rm(cursorDir(host, p), {
427
+ recursive: true,
428
+ force: true
429
+ }).catch(() => {});
430
+ continue;
431
+ }
432
+ const size = await stat(inboxPath(host, p)).then((s) => s.size).catch(() => 0);
433
+ if (size <= capBytes) continue;
434
+ const events = await readInbox(host, p);
435
+ const kept = rotateKeep(events, capBytes, await minConsumerCursor(host, p));
436
+ if (kept.length < events.length) await writeFile(inboxPath(host, p), kept.map(serializeEvent).join("\n") + "\n");
437
+ else logger.warn(`[notify] inbox for parent ${p} is ${size} bytes (> soft cap ${capBytes}) but all events are unacked — cursor not advancing?`);
438
+ } finally {
439
+ await release();
440
+ }
441
+ }
442
+ }
443
+ /** Register / refresh this parent's watch heartbeat (atomically, so a concurrent
444
+ * liveWatchers never reads a torn file and transiently drops the watcher). */
445
+ async function heartbeatWatcher(parentPid, startedAt) {
446
+ await ensureDir(watchersDir());
447
+ await atomicWrite(watcherPath(parentPid), JSON.stringify({
448
+ pid: parentPid,
449
+ started_at: startedAt,
450
+ ts: Date.now()
451
+ }));
452
+ }
453
+ /** Remove this parent's watch heartbeat (on watch exit). */
454
+ async function clearWatcher(parentPid) {
455
+ await rm(watcherPath(parentPid), { force: true }).catch(() => {});
456
+ }
457
+ /**
458
+ * Live watchers as a map of parent pid → the parent's own `started_at` (its
459
+ * authoritative self-reported start time, so the daemon never has to guess it
460
+ * from the registry and stamp a 0). A watcher counts as live ONLY if its
461
+ * heartbeat is fresh AND its process is actually alive — so a crashed `watch`
462
+ * whose heartbeat lingers for the TTL doesn't keep the daemon writing to a dead
463
+ * parent's inbox (which would violate "nothing happens unless you watch"). A
464
+ * heartbeat with a missing/non-positive `started_at` is treated as NOT live: the
465
+ * daemon's cross-session scope guard keys off a positive parent start time, so a
466
+ * 0 would defeat it — fail closed.
467
+ */
468
+ async function liveWatchers(now = Date.now()) {
469
+ const names = await readdir(watchersDir()).catch(() => []);
470
+ const live = /* @__PURE__ */ new Map();
471
+ for (const n of names) {
472
+ if (!n.endsWith(".json")) continue;
473
+ const raw = await readFile(path.join(watchersDir(), n), "utf8").catch(() => "");
474
+ try {
475
+ const o = JSON.parse(raw);
476
+ if (typeof o?.pid === "number" && typeof o?.started_at === "number" && o.started_at > 0 && typeof o?.ts === "number" && now - o.ts <= WATCHER_TTL_MS && pidAlive(o.pid)) live.set(o.pid, o.started_at);
477
+ } catch {}
478
+ }
479
+ return live;
480
+ }
481
+
482
+ //#endregion
13
483
  //#region ts/agentTree.ts
14
484
  /**
15
485
  * Link records into a forest via parent_pid === wrapper_pid. Records whose
@@ -159,12 +629,26 @@ function parseTaskCounts(lines) {
159
629
 
160
630
  //#endregion
161
631
  //#region ts/badges.ts
162
- const BADGE_DEFS = [{
163
- id: "goal-active",
164
- label: "goal",
165
- title: "A /goal Stop-hook loop is active on this agent",
166
- pattern: /\/goal active/i
167
- }];
632
+ const BADGE_DEFS = [
633
+ {
634
+ id: "goal-active",
635
+ label: "goal",
636
+ title: "A /goal Stop-hook loop is active on this agent",
637
+ pattern: /\/goal active/i
638
+ },
639
+ {
640
+ id: "session-limit",
641
+ label: "limit",
642
+ title: "Usage session limit hit — waiting for the reset time shown on screen",
643
+ pattern: /you['’]?ve hit your session limit/i
644
+ },
645
+ {
646
+ id: "retrying",
647
+ label: "retry",
648
+ title: "Waiting for the API — the CLI is auto-retrying on its own backoff (no action needed)",
649
+ pattern: /Waiting for API response[\s\S]{0,40}will retry in \d/i
650
+ }
651
+ ];
168
652
  /**
169
653
  * Which badge ids match the given rendered screen lines. Pure so it's
170
654
  * unit-tested without a live PTY/log file.
@@ -173,6 +657,22 @@ function matchBadges(lines, defs = BADGE_DEFS) {
173
657
  const text = lines.join("\n");
174
658
  return defs.filter((d) => d.pattern.test(text)).map((d) => d.id);
175
659
  }
660
+ /**
661
+ * A time-derived flag (NOT screen-matched): lit when the user typed at this
662
+ * agent's terminal within the last few seconds. The ls code appends its id from
663
+ * the Rust runner's stdin-activity marker, so it resolves through `badgeDef`
664
+ * like any other chip — but `matchBadges` never produces it (it isn't in
665
+ * BADGE_DEFS, and its pattern can't match), keeping screen matching pure.
666
+ */
667
+ const TYPING_BADGE = {
668
+ id: "typing",
669
+ label: "typing",
670
+ title: "The user is typing at this agent's terminal — ay send backs off until they pause",
671
+ pattern: /(?!)/
672
+ };
673
+ function badgeDef(id, defs = BADGE_DEFS) {
674
+ return defs.find((d) => d.id === id) ?? (id === TYPING_BADGE.id ? TYPING_BADGE : void 0);
675
+ }
176
676
 
177
677
  //#endregion
178
678
  //#region ts/needsInput.ts
@@ -549,6 +1049,8 @@ const SUBCOMMANDS = new Set([
549
1049
  "ps",
550
1050
  "status",
551
1051
  "result",
1052
+ "notify",
1053
+ "notifyd",
552
1054
  "read",
553
1055
  "cat",
554
1056
  "tail",
@@ -574,6 +1076,14 @@ const STUCK_THRESHOLD_MS = (() => {
574
1076
  const n = Number(process.env.AGENT_YES_STUCK_MS);
575
1077
  return Number.isFinite(n) && n > 0 ? n : 300 * 1e3;
576
1078
  })();
1079
+ const SEND_SETTLE_QUIET_MS = 150;
1080
+ const SEND_SETTLE_MAX_MS = 1500;
1081
+ const SEND_CONFIRM_QUIET_MS = 400;
1082
+ const SEND_CONFIRM_MAX_MS = 1200;
1083
+ const SEND_CONFIRM_MIN_GROWTH_BYTES = 8;
1084
+ const SEND_SUBMIT_MAX_RETRIES = 2;
1085
+ const SEND_TYPING_POLL_MS = 200;
1086
+ const SEND_TYPING_MAX_WAIT_MS = 1e4;
577
1087
  /**
578
1088
  * Whether `name` is a subcommand. `managerCommands` (default true, for the
579
1089
  * generic `ay`/`agent-yes` entry) additionally admits manager-only commands
@@ -600,6 +1110,8 @@ async function runSubcommand(argv) {
600
1110
  case "ps": return await cmdLs(rest);
601
1111
  case "status": return await cmdStatus(rest);
602
1112
  case "result": return await cmdResult(rest);
1113
+ case "notify": return await cmdNotify(rest);
1114
+ case "notifyd": return await cmdNotifyd(rest);
603
1115
  case "read":
604
1116
  case "cat": return await cmdRead(rest, { mode: "cat" });
605
1117
  case "tail": return await cmdRead(rest, { mode: "tail" });
@@ -614,23 +1126,23 @@ async function runSubcommand(argv) {
614
1126
  case "restart": return await cmdRestart(rest);
615
1127
  case "note": return await cmdNote(rest);
616
1128
  case "serve": {
617
- const { cmdServe } = await import("./serve-D-joIHrL.js");
1129
+ const { cmdServe } = await import("./serve-De1gAWNI.js");
618
1130
  return cmdServe(rest);
619
1131
  }
620
1132
  case "setup": {
621
- const { cmdSetup } = await import("./setup-K6VTmirE.js");
1133
+ const { cmdSetup } = await import("./setup-nlMqyEPB.js");
622
1134
  return cmdSetup(rest);
623
1135
  }
624
1136
  case "schedule": {
625
- const { cmdSchedule } = await import("./schedule-DTHFMzDj.js");
1137
+ const { cmdSchedule } = await import("./schedule-ChTPIcXf.js");
626
1138
  return cmdSchedule(rest);
627
1139
  }
628
1140
  case "remote": {
629
- const { cmdRemote } = await import("./remotes-cx_GDFPj.js");
1141
+ const { cmdRemote } = await import("./remotes-BQMr4_En.js");
630
1142
  return cmdRemote(rest);
631
1143
  }
632
1144
  case "reap":
633
- await (await import("./reaper-BQgDb85W.js")).sweep();
1145
+ await (await import("./reaper-jfSmog15.js")).sweep();
634
1146
  return 0;
635
1147
  case "help": return cmdHelp(managerCommands);
636
1148
  default: return null;
@@ -1019,7 +1531,7 @@ async function fetchRemoteRecordsRaw(url, token, opts) {
1019
1531
  let base = url;
1020
1532
  let bearer = token;
1021
1533
  if (isWebrtcSpec(url)) {
1022
- const { startWebrtcBridge } = await import("./webrtcRemote-SybKurg9.js");
1534
+ const { startWebrtcBridge } = await import("./webrtcRemote-Bx-eD_0I.js");
1023
1535
  bridge = await startWebrtcBridge(url);
1024
1536
  base = bridge.baseUrl;
1025
1537
  bearer = bridge.token;
@@ -1052,41 +1564,85 @@ async function runAllRemotesLs(opts) {
1052
1564
  host: alias,
1053
1565
  records
1054
1566
  })))]);
1055
- const rows = [];
1056
- if (localResult.status === "fulfilled") for (const r of localResult.value.records) rows.push({
1057
- host: "local",
1058
- rec: r
1059
- });
1060
- for (const res of remoteResults) if (res.status === "fulfilled") for (const r of res.value.records) rows.push({
1567
+ const byHost = [];
1568
+ const gitRootCache = /* @__PURE__ */ new Map();
1569
+ const gitInfoCache = /* @__PURE__ */ new Map();
1570
+ if (localResult.status === "fulfilled") {
1571
+ const enriched = await Promise.all(localResult.value.records.map(async (r) => {
1572
+ const { state, question } = await deriveLiveState(r);
1573
+ const [tasks, badges, git, typing] = state !== "stopped" ? await Promise.all([
1574
+ r.log_file ? extractTaskCounts(r.log_file) : Promise.resolve(null),
1575
+ r.log_file ? extractBadges(r.log_file) : Promise.resolve([]),
1576
+ gitStatusOnce(r.cwd, gitRootCache, gitInfoCache),
1577
+ isUserTyping(r.pid)
1578
+ ]) : [
1579
+ null,
1580
+ [],
1581
+ null,
1582
+ false
1583
+ ];
1584
+ return {
1585
+ ...r,
1586
+ status: state,
1587
+ question,
1588
+ last_active_at: await deriveLastActiveAt(r),
1589
+ tasks,
1590
+ badges: typing ? [...badges, TYPING_BADGE.id] : badges,
1591
+ git
1592
+ };
1593
+ }));
1594
+ byHost.push({
1595
+ host: "local",
1596
+ records: enriched
1597
+ });
1598
+ }
1599
+ for (const res of remoteResults) if (res.status === "fulfilled") byHost.push({
1061
1600
  host: res.value.host,
1062
- rec: r
1601
+ records: res.value.records
1602
+ });
1603
+ const rows = [];
1604
+ for (const { host, records } of byHost) for (const { record, prefix } of flattenForest(buildAgentForest(records))) rows.push({
1605
+ host,
1606
+ rec: record,
1607
+ prefix
1063
1608
  });
1064
1609
  if (rows.length === 0) {
1065
1610
  process.stderr.write("no running agents\n");
1066
1611
  return 0;
1067
1612
  }
1068
1613
  const termWidth = process.stdout.columns ?? 120;
1614
+ const now = Date.now();
1615
+ const ageOf = (rec) => humanizeAge(now - (rec.last_active_at ?? rec.started_at));
1616
+ const badgeOf = (rec) => rec.tasks ? `${rec.tasks.done}/${rec.tasks.total} ` : "";
1069
1617
  const hostW = Math.max(4, ...rows.map((r) => r.host.length));
1070
1618
  const pidW = Math.max(3, ...rows.map((r) => String(r.rec.pid).length));
1071
1619
  const cliW = Math.max(3, ...rows.map((r) => String(r.rec.cli).length));
1072
1620
  const statusW = Math.max(6, ...rows.map((r) => String(r.rec.status).length));
1621
+ const ageW = Math.max(3, ...rows.map((r) => ageOf(r.rec).length));
1073
1622
  const cwdW = Math.max(3, ...rows.map((r) => shortenPath(String(r.rec.cwd)).length));
1074
- const promptBudget = Math.max(20, termWidth - hostW - pidW - cliW - statusW - cwdW - 10 - 1);
1623
+ const promptBudget = Math.max(20, termWidth - hostW - pidW - cliW - statusW - ageW - cwdW - 12 - 1);
1075
1624
  process.stdout.write([
1076
1625
  "HOST".padEnd(hostW),
1077
1626
  "PID".padEnd(pidW),
1078
1627
  "CLI".padEnd(cliW),
1079
1628
  "STATUS".padEnd(statusW),
1629
+ "AGE".padEnd(ageW),
1080
1630
  "CWD".padEnd(cwdW),
1081
1631
  "PROMPT"
1082
1632
  ].join(" ") + "\n");
1083
- for (const { host, rec } of rows) {
1084
- const label = rec.prompt ? truncate(`→ ${rec.prompt}`, promptBudget) : "";
1633
+ for (const { host, rec, prefix } of rows) {
1634
+ const flagStr = badgeLabels(rec.badges);
1635
+ const branchStr = branchLabel(rec.git);
1636
+ const gitStr = gitLabel(rec.git);
1637
+ const deco = badgeOf(rec) + (flagStr ? flagStr + " " : "") + (branchStr ? branchStr + " " : "") + (gitStr ? gitStr + " " : "");
1638
+ const budget = Math.max(8, promptBudget - prefix.length - deco.length);
1639
+ const label = prefix + deco + (rec.prompt ? truncate(`→ ${rec.prompt}`, budget) : "");
1085
1640
  process.stdout.write([
1086
1641
  host.padEnd(hostW),
1087
1642
  String(rec.pid).padEnd(pidW),
1088
1643
  String(rec.cli).padEnd(cliW),
1089
1644
  String(rec.status).padEnd(statusW),
1645
+ ageOf(rec).padEnd(ageW),
1090
1646
  shortenPath(String(rec.cwd)).padEnd(cwdW),
1091
1647
  label
1092
1648
  ].join(" ") + "\n");
@@ -1142,6 +1698,133 @@ async function deriveLiveState(r) {
1142
1698
  question: null
1143
1699
  };
1144
1700
  }
1701
+ /**
1702
+ * When the agent last wrote stdout — the log file's mtime, falling back to
1703
+ * started_at when there's no log yet (freshly spawned). Mirrors serve.ts's
1704
+ * `last_active_at`, so the `ay ls` AGE column measures STALENESS (time since the
1705
+ * agent last produced output) rather than lifetime — matching the console's
1706
+ * left-panel age. A long-lived but quiet agent then reads as stale, not "new".
1707
+ */
1708
+ async function deriveLastActiveAt(r) {
1709
+ if (!r.log_file) return r.started_at;
1710
+ return stat(r.log_file).then((s) => s.mtimeMs).catch(() => r.started_at);
1711
+ }
1712
+ /**
1713
+ * Format a GitInfo into the console's compact tag: "±3" changed files, "⑂2"
1714
+ * submodule pin-bumps, "⊙1" submodule internal dirt, "↑1" ahead, "↓2" behind.
1715
+ * Mirrors gitLabel() in lab/ui/console-logic.js so `ay ls` and the web panel's
1716
+ * left rail read identically. "" when clean / in sync / not a repo.
1717
+ */
1718
+ function gitLabel(g) {
1719
+ if (!g) return "";
1720
+ const parts = [];
1721
+ if (g.changed > 0) parts.push("±" + g.changed);
1722
+ if (g.pins > 0) parts.push("⑂" + g.pins);
1723
+ if (g.subDirty > 0) parts.push("⊙" + g.subDirty);
1724
+ if (g.ahead > 0) parts.push("↑" + g.ahead);
1725
+ if (g.behind > 0) parts.push("↓" + g.behind);
1726
+ return parts.join(" ");
1727
+ }
1728
+ /**
1729
+ * The checked-out branch as "⎇<name>" (⎇ = the branch/alt-key glyph). Shows the
1730
+ * ACTUAL git branch, which can differ from the worktree folder in the cwd (a
1731
+ * feature branch checked out in .../tree/main, a detached HEAD, etc.). "" when
1732
+ * detached / not a repo. Kept separate from gitLabel so gitLabel stays a mirror
1733
+ * of the web console's tag.
1734
+ */
1735
+ function branchLabel(g) {
1736
+ return g?.branch ? "⎇" + g.branch : "";
1737
+ }
1738
+ /**
1739
+ * Short status-flag chips ("goal", "retry", "limit") for a list of badge ids —
1740
+ * the same flags the console shows, resolved to their labels via badges.ts.
1741
+ * "" when none. (Remote records carry `badges` from /api/ls; local ones are
1742
+ * matched here via extractBadges.)
1743
+ */
1744
+ function badgeLabels(ids) {
1745
+ if (!ids || ids.length === 0) return "";
1746
+ return ids.map((id) => badgeDef(id)?.label ?? id).join(" ");
1747
+ }
1748
+ function parseGitStatus(out) {
1749
+ let branch = null;
1750
+ let ahead = 0;
1751
+ let behind = 0;
1752
+ let changed = 0;
1753
+ let pins = 0;
1754
+ let subDirty = 0;
1755
+ for (const line of out.split("\n")) {
1756
+ if (line.length === 0) continue;
1757
+ if (line[0] === "#") {
1758
+ const head = /^# branch\.head (.+)$/.exec(line);
1759
+ if (head) {
1760
+ branch = head[1] === "(detached)" ? null : head[1];
1761
+ continue;
1762
+ }
1763
+ const ab = /^# branch\.ab \+(\d+) -(\d+)/.exec(line);
1764
+ if (ab) {
1765
+ ahead = Number(ab[1]);
1766
+ behind = Number(ab[2]);
1767
+ }
1768
+ continue;
1769
+ }
1770
+ const type = line[0];
1771
+ if (type === "?" || type === "u") changed++;
1772
+ else if (type === "1" || type === "2") {
1773
+ const sub = line.split(" ")[2] ?? "N...";
1774
+ if (sub[0] === "S") if (sub[1] === "C") pins++;
1775
+ else subDirty++;
1776
+ else changed++;
1777
+ }
1778
+ }
1779
+ return {
1780
+ branch,
1781
+ dirty: changed > 0,
1782
+ changed,
1783
+ pins,
1784
+ subDirty,
1785
+ ahead,
1786
+ behind
1787
+ };
1788
+ }
1789
+ async function runGitCli(args, cwd) {
1790
+ try {
1791
+ const proc = Bun.spawn(["git", ...args], {
1792
+ cwd,
1793
+ stdout: "pipe",
1794
+ stderr: "ignore",
1795
+ signal: AbortSignal.timeout(2e3)
1796
+ });
1797
+ const out = await new Response(proc.stdout).text();
1798
+ await proc.exited;
1799
+ return proc.exitCode === 0 ? out : null;
1800
+ } catch {
1801
+ return null;
1802
+ }
1803
+ }
1804
+ /**
1805
+ * One-shot git status for the `ay ls` CLI. serve.ts keeps a per-repo watcher so
1806
+ * its request path never spawns git; a one-shot CLI has no watcher, so it spawns
1807
+ * `git status` directly — but deduped per repo root via the two caches, so N
1808
+ * agents sharing a repo (or its submodules/subdirs) cost ONE `git status`.
1809
+ */
1810
+ async function gitStatusOnce(cwd, rootCache, infoCache) {
1811
+ if (!cwd) return null;
1812
+ let root = rootCache.get(cwd);
1813
+ if (root === void 0) {
1814
+ root = (await runGitCli(["rev-parse", "--show-toplevel"], cwd) ?? "").trim();
1815
+ rootCache.set(cwd, root);
1816
+ }
1817
+ if (!root) return null;
1818
+ if (infoCache.has(root)) return infoCache.get(root);
1819
+ const out = await runGitCli([
1820
+ "status",
1821
+ "--porcelain=v2",
1822
+ "--branch"
1823
+ ], root);
1824
+ const info = out != null ? parseGitStatus(out) : null;
1825
+ infoCache.set(root, info);
1826
+ return info;
1827
+ }
1145
1828
  async function cmdLs(rest) {
1146
1829
  const y = yargs(rest).usage("Usage: ay ls [keyword] [options]\n ay list [keyword] [options]\n ay ps [keyword] [options]\n\nList running agents. Optionally filter by keyword (pid, cwd substring, or prompt substring).").option("all", {
1147
1830
  type: "boolean",
@@ -1259,37 +1942,57 @@ async function cmdLs(rest) {
1259
1942
  return 0;
1260
1943
  }
1261
1944
  const termWidth = process.stdout.columns ?? 120;
1945
+ const now = Date.now();
1946
+ const lastActive = new Map(await Promise.all(records.map(async (r) => [r.pid, await deriveLastActiveAt(r)])));
1947
+ const ageOf = (r) => humanizeAge(now - (lastActive.get(r.pid) ?? r.started_at));
1262
1948
  const rawCwds = records.map((r) => shortenPath(r.cwd));
1263
1949
  const widths = {
1264
1950
  pid: Math.max(3, ...records.map((r) => String(r.pid).length)),
1265
1951
  cli: Math.max(3, ...records.map((r) => r.cli.length)),
1266
1952
  status: Math.max(6, ...records.map((r) => r.status.length)),
1267
- age: Math.max(3, ...records.map((r) => humanizeAge(Date.now() - r.started_at).length)),
1953
+ age: Math.max(3, ...records.map((r) => ageOf(r).length)),
1268
1954
  cwd: Math.max(3, ...rawCwds.map((c) => c.length))
1269
1955
  };
1270
1956
  const fixedWidth = widths.pid + widths.cli + widths.status + widths.age + widths.cwd + 10;
1271
1957
  const promptBudget = Math.max(20, termWidth - fixedWidth - 1);
1272
1958
  const forestRows = flattenForest(buildAgentForest(records));
1273
1959
  const notes = await readNotes();
1960
+ const gitRootCache = /* @__PURE__ */ new Map();
1961
+ const gitInfoCache = /* @__PURE__ */ new Map();
1274
1962
  const rows = await Promise.all(forestRows.map(async ({ record: r, prefix }) => {
1275
1963
  const displayStatus = (await deriveLiveState(r)).state;
1964
+ const alive = displayStatus !== "stopped";
1276
1965
  const note = notes.get(r.pid);
1277
- const tasks = displayStatus !== "stopped" && r.log_file ? await extractTaskCounts(r.log_file) : null;
1278
- const badge = tasks ? `${tasks.done}/${tasks.total} ` : "";
1279
- const budget = Math.max(8, promptBudget - prefix.length - badge.length);
1966
+ const [tasks, flags, git, typing] = alive ? await Promise.all([
1967
+ r.log_file ? extractTaskCounts(r.log_file) : Promise.resolve(null),
1968
+ r.log_file ? extractBadges(r.log_file) : Promise.resolve([]),
1969
+ gitStatusOnce(r.cwd, gitRootCache, gitInfoCache),
1970
+ isUserTyping(r.pid)
1971
+ ]) : [
1972
+ null,
1973
+ [],
1974
+ null,
1975
+ false
1976
+ ];
1977
+ const taskBadge = tasks ? `${tasks.done}/${tasks.total} ` : "";
1978
+ const flagStr = badgeLabels(typing ? [...flags, TYPING_BADGE.id] : flags);
1979
+ const branchStr = branchLabel(git);
1980
+ const gitStr = gitLabel(git);
1981
+ const deco = taskBadge + (flagStr ? flagStr + " " : "") + (branchStr ? branchStr + " " : "") + (gitStr ? gitStr + " " : "");
1982
+ const budget = Math.max(8, promptBudget - prefix.length - deco.length);
1280
1983
  let label;
1281
1984
  let hasNote = false;
1282
1985
  if (note) {
1283
1986
  label = truncate(note, budget);
1284
1987
  hasNote = true;
1285
- } else if (r.log_file && displayStatus !== "stopped") label = truncate(await extractActivity(r.log_file) ?? (r.prompt ? `→ ${r.prompt}` : ""), budget);
1988
+ } else if (r.log_file && alive) label = truncate(await extractActivity(r.log_file) ?? (r.prompt ? `→ ${r.prompt}` : ""), budget);
1286
1989
  else label = truncate(r.prompt ? `→ ${r.prompt}` : "", budget);
1287
- label = prefix + (hasNote ? "* " : "") + badge + label;
1990
+ label = prefix + (hasNote ? "* " : "") + deco + label;
1288
1991
  return {
1289
1992
  pid: String(r.pid),
1290
1993
  cli: r.cli,
1291
1994
  status: displayStatus,
1292
- age: humanizeAge(Date.now() - r.started_at),
1995
+ age: ageOf(r),
1293
1996
  cwd: shortenPath(r.cwd),
1294
1997
  label,
1295
1998
  hasNote,
@@ -1894,6 +2597,20 @@ async function extractBadges(logPath) {
1894
2597
  if (!lines) return [];
1895
2598
  return matchBadges(lines);
1896
2599
  }
2600
+ const TYPING_WINDOW_MS = 3e3;
2601
+ function stdinActivityPath(pid) {
2602
+ return path.resolve(agentYesHome(), "activity", `${pid}.stdin`);
2603
+ }
2604
+ async function lastStdinAt(pid) {
2605
+ const raw = await readFile(stdinActivityPath(pid), "utf-8").catch(() => null);
2606
+ if (raw === null) return null;
2607
+ const ms = Number(raw.trim());
2608
+ return Number.isFinite(ms) && ms > 0 ? ms : null;
2609
+ }
2610
+ async function isUserTyping(pid, windowMs = TYPING_WINDOW_MS) {
2611
+ const at = await lastStdinAt(pid);
2612
+ return at !== null && Date.now() - at <= windowMs;
2613
+ }
1897
2614
  /**
1898
2615
  * Whether an alive agent is wedged: its log has been silent for at least
1899
2616
  * STUCK_THRESHOLD_MS yet its screen still shows a `working` busy marker (a live
@@ -2038,6 +2755,82 @@ async function extractMenu(logPath, cli) {
2038
2755
  working: cfg.working
2039
2756
  });
2040
2757
  }
2758
+ /**
2759
+ * Poll `logFile`'s size until it goes `quietMs` without changing, or `maxWaitMs`
2760
+ * total elapses (whichever first). Returns the final observed size, or null if
2761
+ * the file can't be stat'd. Used by `ay send` both to wait out a paste's render
2762
+ * (before submitting) and to detect whether a submit actually produced output
2763
+ * (after submitting).
2764
+ */
2765
+ async function waitForLogQuiet(logFile, quietMs, maxWaitMs) {
2766
+ const pollMs = 50;
2767
+ let lastSize = null;
2768
+ let lastChangeAt = Date.now();
2769
+ const deadline = Date.now() + maxWaitMs;
2770
+ while (Date.now() < deadline) {
2771
+ const size = await stat(logFile).then((s) => s.size).catch(() => null);
2772
+ if (size === null) return null;
2773
+ if (size !== lastSize) {
2774
+ lastSize = size;
2775
+ lastChangeAt = Date.now();
2776
+ } else if (Date.now() - lastChangeAt >= quietMs) return lastSize;
2777
+ await new Promise((r) => setTimeout(r, pollMs));
2778
+ }
2779
+ return lastSize;
2780
+ }
2781
+ /**
2782
+ * Block while the user is typing at `pid`'s terminal, so `ay send` doesn't inject
2783
+ * mid-line. Polls the stdin-activity marker every SEND_TYPING_POLL_MS until the
2784
+ * user pauses (last keystroke older than the typing window) or `maxWaitMs`
2785
+ * elapses. Returns `{ clear, waitedMs }`: `clear` is true if they paused, false
2786
+ * if still typing at the deadline (caller sends anyway, with a warning).
2787
+ */
2788
+ async function backoffWhileTyping(pid, maxWaitMs) {
2789
+ const start = Date.now();
2790
+ const deadline = start + maxWaitMs;
2791
+ let waited = false;
2792
+ while (Date.now() < deadline) {
2793
+ if (!await isUserTyping(pid)) return {
2794
+ clear: true,
2795
+ waitedMs: waited ? Date.now() - start : 0
2796
+ };
2797
+ waited = true;
2798
+ await new Promise((r) => setTimeout(r, SEND_TYPING_POLL_MS));
2799
+ }
2800
+ return {
2801
+ clear: false,
2802
+ waitedMs: Date.now() - start
2803
+ };
2804
+ }
2805
+ /**
2806
+ * Send the trailing submit code and confirm the CLI actually acted on it —
2807
+ * either a `working` busy marker appears, or the log grows meaningfully. Retries
2808
+ * (re-sending just the trailing code) up to SEND_SUBMIT_MAX_RETRIES times when
2809
+ * neither shows, since a swallowed Enter looks identical to a slow one until we
2810
+ * check. Returns whether submission was confirmed, plus the final rendered tail
2811
+ * (for a caller to show the user when it wasn't).
2812
+ */
2813
+ async function submitAndConfirm(record, fifoPath, trailing) {
2814
+ const logFile = record.log_file;
2815
+ const cfg = (await cliDefaults())[record.cli];
2816
+ let screen = [];
2817
+ for (let attempt = 0; attempt <= SEND_SUBMIT_MAX_RETRIES; attempt++) {
2818
+ const sizeBefore = await stat(logFile).then((s) => s.size).catch(() => null) ?? 0;
2819
+ const wasAlreadyWorking = isWorkingScreen(await renderLogTailLines(logFile, 40) ?? [], cfg?.working);
2820
+ await writeToIpc(fifoPath, trailing);
2821
+ const sizeAfter = await waitForLogQuiet(logFile, SEND_CONFIRM_QUIET_MS, SEND_CONFIRM_MAX_MS);
2822
+ screen = await renderLogTailLines(logFile, 40) ?? [];
2823
+ const grew = sizeAfter !== null && sizeAfter >= sizeBefore + SEND_CONFIRM_MIN_GROWTH_BYTES;
2824
+ if (isWorkingScreen(screen, cfg?.working) && !wasAlreadyWorking || grew) return {
2825
+ confirmed: true,
2826
+ screen
2827
+ };
2828
+ }
2829
+ return {
2830
+ confirmed: false,
2831
+ screen
2832
+ };
2833
+ }
2041
2834
  /** Poll until the agent is no longer parked on a menu (selection accepted → it
2042
2835
  * resumed / moved on) or the deadline passes. Returns true if it cleared. */
2043
2836
  async function waitForNeedsInputClear(record, timeoutMs) {
@@ -2049,7 +2842,7 @@ async function waitForNeedsInputClear(record, timeoutMs) {
2049
2842
  return false;
2050
2843
  }
2051
2844
  async function cmdSend(rest) {
2052
- const argv = await yargs(rest).usage("Usage: ay send <keyword> <msg|-> [options]").option("code", {
2845
+ const argv = await yargs(rest).parserConfiguration({ "boolean-negation": false }).usage("Usage: ay send <keyword> <msg|-> [options]").option("code", {
2053
2846
  type: "string",
2054
2847
  default: "enter",
2055
2848
  description: "Trailing control code (enter|esc|ctrl-c|ctrl-y|tab|none)"
@@ -2067,7 +2860,12 @@ async function cmdSend(rest) {
2067
2860
  }).option("force", {
2068
2861
  type: "boolean",
2069
2862
  default: false,
2070
- description: "Skip the 'tailed recently' safety check (also: AGENT_YES_FORCE_SEND=1)"
2863
+ description: "Skip the 'tailed recently' safety check and the wait-while-user-typing backoff (also: AGENT_YES_FORCE_SEND=1)"
2864
+ }).option("no-wait", {
2865
+ type: "boolean",
2866
+ default: false,
2867
+ alias: "async",
2868
+ description: "Fire-and-forget: skip the paste-settle wait and submit confirmation, don't retry a swallowed Enter (also: AGENT_YES_SEND_NO_WAIT=1)"
2071
2869
  }).help(false).version(false).exitProcess(false).parseAsync();
2072
2870
  const opts = {
2073
2871
  all: argv.all,
@@ -2094,7 +2892,8 @@ async function cmdSend(rest) {
2094
2892
  for await (const chunk of process.stdin) chunks.push(chunk);
2095
2893
  body = Buffer.concat(chunks).toString("utf-8").trimEnd();
2096
2894
  } else body = rawMessage;
2097
- const sender = await enforceSendGuards(record, Boolean(argv.force) || process.env.AGENT_YES_FORCE_SEND === "1");
2895
+ const force = Boolean(argv.force) || process.env.AGENT_YES_FORCE_SEND === "1";
2896
+ const sender = await enforceSendGuards(record, force);
2098
2897
  if (isExitRequest(body)) {
2099
2898
  const reason = sender.agent ? `requested by ${sender.agent.cli} #${sender.agent.pid} @ ${shortenPath(sender.agent.cwd)}` : `requested via 'ay send ${keyword} exit'`;
2100
2899
  const { strategy } = await gracefulExitAgent(record, reason);
@@ -2102,20 +2901,36 @@ async function cmdSend(rest) {
2102
2901
  return 0;
2103
2902
  }
2104
2903
  const fullBody = (sender.agent && !isSlashCommand(body) ? `[from ${sender.agent.cli} #${sender.agent.pid} @ ${shortenPath(sender.agent.cwd)} — reply: ay send ${sender.agent.pid} "..."]\n` : "") + body;
2904
+ const noWait = Boolean(argv.noWait) || process.env.AGENT_YES_SEND_NO_WAIT === "1";
2905
+ if (fullBody && !noWait && !force) {
2906
+ const { clear, waitedMs } = await backoffWhileTyping(record.pid, SEND_TYPING_MAX_WAIT_MS);
2907
+ if (!clear) process.stderr.write(`warning: user still typing at pid ${record.pid} after ${Math.round(waitedMs / 1e3)}s — sending anyway (may interleave with their line). Use --force to skip this wait.\n`);
2908
+ else if (waitedMs > 0) process.stderr.write(`waited ${Math.round(waitedMs / 1e3)}s for the user to pause typing before sending.\n`);
2909
+ }
2910
+ const canConfirm = trailing === "\r" && Boolean(fullBody) && !noWait;
2911
+ let confirmed = true;
2912
+ let lastScreen = [];
2105
2913
  if (fullBody && trailing) {
2106
2914
  await writeToIpc(fifoPath, fullBody);
2107
- await new Promise((r) => setTimeout(r, 200));
2108
- await writeToIpc(fifoPath, trailing);
2915
+ if (canConfirm && record.log_file) {
2916
+ await waitForLogQuiet(record.log_file, SEND_SETTLE_QUIET_MS, SEND_SETTLE_MAX_MS);
2917
+ ({confirmed, screen: lastScreen} = await submitAndConfirm(record, fifoPath, trailing));
2918
+ } else {
2919
+ await new Promise((r) => setTimeout(r, 200));
2920
+ await writeToIpc(fifoPath, trailing);
2921
+ }
2109
2922
  } else await writeToIpc(fifoPath, fullBody + trailing);
2110
2923
  const payload = body + trailing;
2111
- process.stdout.write(`sent to pid ${record.pid} (${record.cli}): ${truncate(payload, 80)}\n`);
2924
+ const status = confirmed ? "sent" : "sent but NOT confirmed submitted";
2925
+ process.stdout.write(`${status} to pid ${record.pid} (${record.cli}): ${truncate(payload, 80)}\n`);
2926
+ if (!confirmed) process.stderr.write(`\nwarning: couldn't confirm the CLI acted on it after ${SEND_SUBMIT_MAX_RETRIES + 1} attempt(s) — it may still be sitting unsubmitted in the prompt. Last screen:\n` + lastScreen.slice(-8).map((l) => ` ${l}`).join("\n") + "\n");
2112
2927
  const replyHint = sender.agent ? ` ay send ${sender.agent.pid} "..." # reply to sender\n` : "";
2113
2928
  process.stderr.write(`\n` + replyHint + ` ay tail ${record.pid} # watch output\n ay ls # list all agents\n`);
2114
2929
  if (codeName === "ctrl-c" || codeName === "ctrlc") {
2115
2930
  const tip = stopTipForCli(record.cli, record.pid);
2116
2931
  if (tip) process.stderr.write(tip);
2117
2932
  }
2118
- return 0;
2933
+ return confirmed ? 0 : 1;
2119
2934
  }
2120
2935
  async function resolveWritableAgent(keyword, opts) {
2121
2936
  const record = await resolveOne(keyword, opts);
@@ -3056,7 +3871,197 @@ async function cmdResultSet(rest) {
3056
3871
  process.stdout.write(`result envelope written for pid ${pid}\n`);
3057
3872
  return 0;
3058
3873
  }
3874
+ /** Resolve the parent pid a `ay notify` invocation is draining. */
3875
+ function resolveParentPid(explicit) {
3876
+ if (Number.isFinite(explicit) && explicit > 0) return explicit;
3877
+ const self = Number(process.env.AGENT_YES_PID);
3878
+ if (Number.isFinite(self) && self > 0) return self;
3879
+ throw new Error("ay notify: not running inside an agent (no AGENT_YES_PID) — pass --parent <pid>");
3880
+ }
3881
+ function printNotifyEvents(events, json) {
3882
+ if (json) {
3883
+ for (const e of events) process.stdout.write(JSON.stringify(e) + "\n");
3884
+ return;
3885
+ }
3886
+ for (const e of events) {
3887
+ const tag = `[${e.edge}]`;
3888
+ const head = `[${e.seq}] ${tag} pid ${e.child_pid} (${e.cli}) ${e.cwd}`;
3889
+ process.stdout.write(head + "\n");
3890
+ if (e.git_head) process.stdout.write(` HEAD ${e.git_head}\n`);
3891
+ if (e.question) process.stdout.write(` Q: ${e.question}\n`);
3892
+ if (e.tail) process.stdout.write(e.tail.split("\n").map((l) => ` | ${l}`).join("\n") + "\n");
3893
+ }
3894
+ }
3895
+ async function cmdNotify(rest) {
3896
+ const verb = rest[0];
3897
+ const args = rest.slice(1);
3898
+ if (verb === "cursor") return cmdNotifyCursor(args);
3899
+ if (verb !== "read" && verb !== "watch") {
3900
+ process.stderr.write("usage: ay notify <read|watch|cursor> [--parent <pid>] [--since <seq>] [--since-ts <ms>] [--unread] [--ack] [--json]\n");
3901
+ return 1;
3902
+ }
3903
+ const argv = await yargs(args).option("parent", {
3904
+ type: "number",
3905
+ description: "Parent pid whose inbox to drain (default: $AGENT_YES_PID)"
3906
+ }).option("since", {
3907
+ type: "number",
3908
+ description: "Only edges with seq greater than this"
3909
+ }).option("since-ts", {
3910
+ type: "number",
3911
+ description: "Only edges at/after this epoch-ms"
3912
+ }).option("unread", {
3913
+ type: "boolean",
3914
+ default: false,
3915
+ description: "Only edges past the saved cursor"
3916
+ }).option("ack", {
3917
+ type: "boolean",
3918
+ default: false,
3919
+ description: "Advance the cursor past what's shown (at-least-once: off by default)"
3920
+ }).option("json", {
3921
+ type: "boolean",
3922
+ default: false,
3923
+ description: "Emit raw NDJSON events"
3924
+ }).option("consumer", {
3925
+ type: "string",
3926
+ default: "parent",
3927
+ description: "Cursor identity (for multiple readers)"
3928
+ }).option("interval", {
3929
+ type: "number",
3930
+ default: 2,
3931
+ description: "Poll interval in seconds (watch)"
3932
+ }).option("ensure-daemon", {
3933
+ type: "boolean",
3934
+ default: true,
3935
+ description: "Start the notifyd singleton if not running (watch)"
3936
+ }).help(false).version(false).exitProcess(false).parseAsync();
3937
+ const parent = resolveParentPid(argv.parent);
3938
+ const host = hostId();
3939
+ const consumer = String(argv.consumer);
3940
+ const selfStartedAt = await resolveParentStartedAt(parent);
3941
+ if (selfStartedAt <= 0) throw new Error(`cannot resolve identity for pid ${parent} (no live agent record) — refusing to open the notification path (pass --parent for a live agent).`);
3942
+ const drain = async (sinceSeqOverride) => {
3943
+ let events = await readInbox(host, parent);
3944
+ if (selfStartedAt > 0) events = events.filter((e) => e.parent_started_at === selfStartedAt);
3945
+ if (argv.unread) {
3946
+ const cursor = await getCursor(host, parent, consumer);
3947
+ events = filterUnread(events, sinceSeqOverride ?? cursor);
3948
+ } else {
3949
+ if (sinceSeqOverride !== void 0) events = filterSinceSeq(events, sinceSeqOverride);
3950
+ else if (Number.isFinite(argv.since)) events = filterSinceSeq(events, argv.since);
3951
+ if (Number.isFinite(argv["since-ts"])) events = filterSinceTs(events, argv["since-ts"]);
3952
+ }
3953
+ printNotifyEvents(events, argv.json);
3954
+ return maxSeq(events);
3955
+ };
3956
+ const ackTo = async (seq) => {
3957
+ if (seq > await getCursor(host, parent, consumer)) await setCursor(host, parent, seq, consumer);
3958
+ };
3959
+ if (verb === "read") {
3960
+ const top = await drain();
3961
+ if (argv.ack && top > 0) await ackTo(top);
3962
+ return 0;
3963
+ }
3964
+ const ensure = async () => {
3965
+ if (!argv["ensure-daemon"]) return;
3966
+ const { ensureDaemon } = await import("./notifyDaemon-D5jtpCAR.js");
3967
+ await ensureDaemon().catch(() => null);
3968
+ };
3969
+ await heartbeatWatcher(parent, selfStartedAt);
3970
+ await ensure();
3971
+ const intervalMs = Math.max(500, (Number.isFinite(argv.interval) ? argv.interval : 2) * 1e3);
3972
+ let lastSeq = argv.unread ? await getCursor(host, parent, consumer) : Number.isFinite(argv.since) ? argv.since : maxSeq(await readInbox(host, parent));
3973
+ let acked = lastSeq;
3974
+ let stop = false;
3975
+ const onSig = () => {
3976
+ stop = true;
3977
+ clearWatcher(parent).finally(() => process.exit(0));
3978
+ };
3979
+ process.on("SIGINT", onSig);
3980
+ process.on("SIGTERM", onSig);
3981
+ try {
3982
+ while (!stop) {
3983
+ await heartbeatWatcher(parent, selfStartedAt);
3984
+ await ensure();
3985
+ const top = await drain(lastSeq);
3986
+ if (top > lastSeq) lastSeq = top;
3987
+ if (argv.ack && lastSeq > acked) {
3988
+ await ackTo(lastSeq);
3989
+ acked = lastSeq;
3990
+ }
3991
+ await new Promise((r) => setTimeout(r, intervalMs));
3992
+ }
3993
+ } finally {
3994
+ await clearWatcher(parent);
3995
+ }
3996
+ return 0;
3997
+ }
3998
+ /**
3999
+ * Resolve the started_at of the LIVE agent whose wrapper pid is `parent`. Returns
4000
+ * 0 (→ the caller fails closed) when there is no such record, when the matching
4001
+ * record is stale (exited / pid not alive), or when the match is ambiguous
4002
+ * (>1 live record) — so a leftover stale record can't make a recycled parent pid
4003
+ * resolve to a PRIOR incarnation's start time and fail-open the identity guard.
4004
+ */
4005
+ async function resolveParentStartedAt(parent) {
4006
+ const live = (await listRecords(void 0, {
4007
+ all: true,
4008
+ active: false,
4009
+ json: false,
4010
+ latest: false,
4011
+ cwdScope: null
4012
+ }).catch(() => [])).filter((r) => (r.wrapper_pid === parent || r.pid === parent) && r.status !== "exited" && isPidAlive(r.pid));
4013
+ if (live.length !== 1) return 0;
4014
+ return live[0].started_at ?? 0;
4015
+ }
4016
+ async function cmdNotifyCursor(args) {
4017
+ const action = args[0];
4018
+ const argv = await yargs(args.slice(1)).option("parent", { type: "number" }).option("consumer", {
4019
+ type: "string",
4020
+ default: "parent"
4021
+ }).help(false).version(false).exitProcess(false).parseAsync();
4022
+ const parent = resolveParentPid(argv.parent);
4023
+ const host = hostId();
4024
+ const consumer = String(argv.consumer);
4025
+ if (action === "get") {
4026
+ process.stdout.write(String(await getCursor(host, parent, consumer)) + "\n");
4027
+ return 0;
4028
+ }
4029
+ if (action === "set") {
4030
+ const seq = Number(argv._[0]);
4031
+ if (!Number.isFinite(seq) || seq < 0) throw new Error("ay notify cursor set <seq>");
4032
+ await setCursor(host, parent, seq, consumer);
4033
+ return 0;
4034
+ }
4035
+ process.stderr.write("usage: ay notify cursor <get|set <seq>> [--parent <pid>]\n");
4036
+ return 1;
4037
+ }
4038
+ async function cmdNotifyd(rest) {
4039
+ const sub = rest[0] ?? "status";
4040
+ const daemon = await import("./notifyDaemon-D5jtpCAR.js");
4041
+ switch (sub) {
4042
+ case "run": return daemon.runDaemon();
4043
+ case "once": return daemon.runDaemon({ once: true });
4044
+ case "start": {
4045
+ const pid = await daemon.ensureDaemon();
4046
+ process.stdout.write(pid ? `notifyd running (pid ${pid})\n` : "notifyd: failed to start\n");
4047
+ return pid ? 0 : 1;
4048
+ }
4049
+ case "status": {
4050
+ const pid = await daemon.daemonStatus();
4051
+ process.stdout.write(pid ? `notifyd running (pid ${pid})\n` : "notifyd: not running\n");
4052
+ return pid ? 0 : 1;
4053
+ }
4054
+ case "stop": {
4055
+ const pid = await daemon.requestDaemonStop();
4056
+ process.stdout.write(pid ? `notifyd: stop requested (pid ${pid} will exit shortly)\n` : "notifyd: not running\n");
4057
+ return 0;
4058
+ }
4059
+ default:
4060
+ process.stderr.write("usage: ay notifyd <run|once|start|status|stop>\n");
4061
+ return 1;
4062
+ }
4063
+ }
3059
4064
 
3060
4065
  //#endregion
3061
- export { stopTipForCli as A, renderRawLogLines as C, restartHintLines as D, resolveResumeArgs as E, writeToIpc as M, runSubcommand as O, renderRawLog as S, resolveReadWindow as T, listRecords as _, cursorAbs as a, readNotes as b, extractMenu as c, finalizedLines as d, isAgentStuck as f, isSubcommand as g, isSlashCommand as h, controlCodeFromName as i, writeKeysPaced as j, snapshotStatus as k, extractNeedsInput as l, isPidAlive as m, READ_PAGE_DEFAULT as n, deriveLiveStatus as o, isExitRequest as p, cmdHelp as r, extractBadges as s, GRACEFUL_EXIT_COMMANDS as t, extractTaskCounts as u, matchKeyword as v, resolveOne as w, readPtysize as x, menuSelectKeys as y };
3062
- //# sourceMappingURL=subcommands-BZPW0UVn.js.map
4066
+ export { resolveOne as A, writeKeysPaced as B, matchKeyword as C, renderLogTailLines as D, readPtysize as E, snapshotStatus as F, hostId as G, TYPING_BADGE as H, stdinActivityPath as I, readInbox as J, listInboxParents as K, stopTipForCli as L, resolveResumeArgs as M, restartHintLines as N, renderRawLog as O, runSubcommand as P, notifyDir as Q, submitAndConfirm as R, listRecords as S, readNotes as T, appendEvent as U, writeToIpc as V, gcInboxes as W, daemonLockDir as X, shouldStealLock as Y, daemonLockOwnerPath as Z, isPidAlive as _, cmdHelp as a, isUserTyping as b, deriveLiveState as c, extractMenu as d, extractNeedsInput as f, isExitRequest as g, isAgentStuck as h, backoffWhileTyping as i, resolveReadWindow as j, renderRawLogLines as k, deriveLiveStatus as l, finalizedLines as m, READ_PAGE_DEFAULT as n, controlCodeFromName as o, extractTaskCounts as p, liveWatchers as q, TYPING_WINDOW_MS as r, cursorAbs as s, GRACEFUL_EXIT_COMMANDS as t, extractBadges as u, isSlashCommand as v, menuSelectKeys as w, lastStdinAt as x, isSubcommand as y, waitForLogQuiet as z };
4067
+ //# sourceMappingURL=subcommands-BFlpV23b.js.map