agent-yes 1.265.4 → 1.266.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/dist/{SUPPORTED_CLIS-C3tcRetU.js → SUPPORTED_CLIS-AFV5pjSx.js} +2 -2
- package/dist/{SUPPORTED_CLIS-CY3SaV0D.js → SUPPORTED_CLIS-DRVuSo48.js} +3 -3
- package/dist/{agentShare-g6N1A6A_.js → agentShare-qdFSp9Uq.js} +6 -5
- package/dist/{callback-L4k-v2GG.js → callback-Da7MRLh6.js} +6 -5
- package/dist/{callback-z-w0ND5z.js → callback-DfNFdZin.js} +5 -5
- package/dist/{callbackCore-Cv4EOGjE.js → callbackCore-DcptaZwH.js} +1 -1
- package/dist/{callbackCore-Cg3Ye0ZS.js → callbackCore-K99x3C75.js} +1 -1
- package/dist/{channels-BwpN_8mu.js → channels-DQaZbare.js} +2 -2
- package/dist/cli.js +11 -11
- package/dist/{cwdConflictWarn-DZTxTGXq.js → cwdConflictWarn-CtQhl-vF.js} +1 -1
- package/dist/{cwdPassthroughHint-DDVkhBiD.js → cwdPassthroughHint-apaX_O7p.js} +1 -1
- package/dist/{expose-BG17GV7D.js → expose-BXn6My6A.js} +1 -1
- package/dist/{forkNested-VroZS-nk.js → forkNested-CDfWPiHw.js} +12 -2
- package/dist/{hist-CoXBBjJt.js → hist-BpocAe1N.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/initMsg-BjRseolL.js +3 -0
- package/dist/initMsg-C-w4A_eF.js +85 -0
- package/dist/needsInput-103zhZ90.js +79 -0
- package/dist/{nodeRuntime-CT0w6zsB.js → nodeRuntime-CRoEMWkp.js} +1 -1
- package/dist/{notifyDaemon-BL1tmxUK.js → notifyDaemon-BFzqcdN1.js} +32 -5
- package/dist/notifyStore-CEDiVyis.js +4 -0
- package/dist/notifyStore-Cbw2ZJ0X.js +559 -0
- package/dist/{openBrowser-Cw8hg7ok.js → openBrowser-DhQ9pJRi.js} +1 -1
- package/dist/parentLink-B798ASCW.js +5 -0
- package/dist/parentLink-C0cIRjfL.js +53 -0
- package/dist/parentPingLoop-BIKxzbPH.js +469 -0
- package/dist/{remotes-DWVw-Q7V.js → remotes-Dg_JscKk.js} +2 -2
- package/dist/{remotes-iIvqiFWK.js → remotes-iMhdZytp.js} +3 -3
- package/dist/{rustBinary-DSVRhDzO.js → rustBinary-JGNROWfY.js} +2 -2
- package/dist/{schedule-i4o5SJ_h.js → schedule-DbLpjgl5.js} +6 -6
- package/dist/{serve-CpGJD2Kt.js → serve-ucVXOci1.js} +31 -30
- package/dist/{setup-B-iQKsQW.js → setup-De7uYRZb.js} +4 -4
- package/dist/{share-DOnynkf4.js → share-Db_zb6gH.js} +1 -1
- package/dist/{share-B-e_9WBx.js → share-NUhi-Wdd.js} +1 -1
- package/dist/{spawnGate-S6EoFdzX.js → spawnGate-BvaZeHE7.js} +1 -1
- package/dist/{spawnGate-BmsTv340.js → spawnGate-CEXVmTtv.js} +2 -2
- package/dist/subcommands-C04exkIM.js +11 -0
- package/dist/{subcommands-CQPnT5qu.js → subcommands-DtF31RoW.js} +41 -656
- package/dist/{systemPathLink-CjLmiPbB.js → systemPathLink-7TFPwJna.js} +1 -1
- package/dist/{terminal-CybRgMZu.js → terminal-Dm32SXU-.js} +2 -2
- package/dist/{todoCli-DZmOmcvd.js → todoCli-c_R7D7Y-.js} +1 -1
- package/dist/{tray-Cjm3a8JH.js → tray-DZnGjJBK.js} +1 -1
- package/dist/trayApp-CZbwLK9t.js +5 -0
- package/dist/{trayApp-UglHbk_m.js → trayApp-CfkPG0wy.js} +2 -2
- package/dist/{ts-nuUnrVcf.js → ts-CGtoPER5.js} +43 -10
- package/dist/{versionChecker-CMOLbEEi.js → versionChecker-CJgW9DHY.js} +2 -2
- package/dist/{webrtcLink-CS4D-3G1.js → webrtcLink-DyCj380i.js} +1 -1
- package/dist/{webrtcRemote-zQybN9IR.js → webrtcRemote-BDg0jjTs.js} +2 -2
- package/dist/{widget-y5Bpvwsa.js → widget-DXtaX3QR.js} +3 -3
- package/dist/{workspaceConfig-BDqp7fYl.js → workspaceConfig-HiWQzq6A.js} +1 -1
- package/dist/{ws-CH8GzcQn.js → ws-CqlS5JhN.js} +5 -4
- package/dist/{ws-BrLV-AlH.js → ws-aKRZpI9b.js} +9 -11
- package/package.json +1 -1
- package/ts/forkNested.spec.ts +22 -0
- package/ts/forkNested.ts +19 -0
- package/ts/index.ts +77 -12
- package/ts/initMsg.spec.ts +114 -0
- package/ts/initMsg.ts +107 -0
- package/ts/ownerHeartbeat.ts +40 -1
- package/ts/parentLink.spec.ts +64 -0
- package/ts/parentLink.ts +58 -0
- package/ts/parentPing.spec.ts +176 -0
- package/ts/parentPing.ts +195 -0
- package/ts/parentPingLoop.delivery.spec.ts +236 -0
- package/ts/parentPingLoop.spec.ts +79 -0
- package/ts/parentPingLoop.ts +196 -0
- package/ts/parentPingSend.spec.ts +195 -0
- package/ts/parentPingSend.ts +148 -0
- package/ts/parentWatching.spec.ts +61 -0
- package/ts/parentWatching.ts +89 -0
- package/ts/subcommands.ts +11 -2
- package/ts/trayApp.spec.ts +31 -4
- package/ts/ws.ts +17 -12
- package/dist/subcommands-Cgt2-kjg.js +0 -10
- package/dist/trayApp-CmwwBg8v.js +0 -5
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import { t as invokedCliName } from "./invokedCli-uqM2YYA7.js";
|
|
2
|
-
import { n as logger } from "./logger-CDIsZ-Pp.js";
|
|
3
2
|
import { t as agentYesHome } from "./agentYesHome-CtmsY6I-.js";
|
|
4
3
|
import { a as updateGlobalPidStatus, i as readGlobalPids } from "./globalPidIndex-Dq4KY2IV.js";
|
|
5
4
|
import { a as recordOutbox, i as recordMessage, n as readMailbox, t as partyMatches } from "./messageLog-BYFwioFY.js";
|
|
6
5
|
import { t as loadSharedCliDefaults } from "./configShared-DtHMBCl7.js";
|
|
7
|
-
import { n as
|
|
8
|
-
import { a as
|
|
6
|
+
import { n as isWorkingScreen, r as parseMenu, t as classifyNeedsInput } from "./needsInput-103zhZ90.js";
|
|
7
|
+
import { S as postmortemStartedAt, _ as filterSinceSeq, a as getCursor, b as maxSeq, f as readInbox, o as heartbeatWatcher, p as setCursor, r as clearWatcher, s as hostId, v as filterSinceTs, y as filterUnread } from "./notifyStore-Cbw2ZJ0X.js";
|
|
8
|
+
import { n as isWebrtcSpec } from "./webrtcLink-DyCj380i.js";
|
|
9
|
+
import { a as resolveRemoteSpec, i as readRemotes } from "./remotes-iMhdZytp.js";
|
|
9
10
|
import { spawn } from "child_process";
|
|
10
11
|
import ms from "ms";
|
|
11
12
|
import yargs from "yargs";
|
|
12
|
-
import { appendFile, mkdir, open, readFile,
|
|
13
|
+
import { appendFile, mkdir, open, readFile, stat, writeFile } from "fs/promises";
|
|
13
14
|
import { homedir } from "os";
|
|
14
15
|
import path from "path";
|
|
15
|
-
import { randomUUID } from "node:crypto";
|
|
16
16
|
import { readFileSync } from "node:fs";
|
|
17
17
|
import path$1 from "node:path";
|
|
18
|
-
import
|
|
18
|
+
import { homedir as homedir$1, hostname as hostname$1, userInfo as userInfo$1 } from "node:os";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
|
-
import { randomBytes
|
|
20
|
+
import { randomBytes } from "crypto";
|
|
21
21
|
|
|
22
22
|
//#region ts/identity.ts
|
|
23
23
|
/**
|
|
@@ -357,83 +357,6 @@ function badgeLabel(id, defs = BADGE_DEFS) {
|
|
|
357
357
|
return def.label.includes("$1") ? def.label.replace("$1", arg) : def.label;
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
//#endregion
|
|
361
|
-
//#region ts/needsInput.ts
|
|
362
|
-
function reTest(re, s) {
|
|
363
|
-
return (re.global || re.sticky ? new RegExp(re.source, re.flags.replace(/[gy]/g, "")) : re).test(s);
|
|
364
|
-
}
|
|
365
|
-
function isChromeLine(s) {
|
|
366
|
-
const t = s.trim();
|
|
367
|
-
return !t || /^─+$/.test(t) || /^esc to (interrupt|cancel)/i.test(t) || /\? for shortcuts/.test(t) || /\d+%\s*until auto-compact/i.test(t);
|
|
368
|
-
}
|
|
369
|
-
/**
|
|
370
|
-
* Returns a NeedsInput when the screen shows an unresolved selection menu, else
|
|
371
|
-
* null. `cfg.working` short-circuits to null (an actively-working agent isn't
|
|
372
|
-
* blocked). Pure + synchronous so it's trivially unit-testable.
|
|
373
|
-
*/
|
|
374
|
-
function classifyNeedsInput(lines, cfg) {
|
|
375
|
-
const patterns = cfg.needsInput ?? [];
|
|
376
|
-
if (patterns.length === 0) return null;
|
|
377
|
-
const text = lines.join("\n");
|
|
378
|
-
if ((cfg.working ?? []).some((re) => reTest(re, text))) return null;
|
|
379
|
-
if (!patterns.some((re) => reTest(re, text))) return null;
|
|
380
|
-
let last = -1;
|
|
381
|
-
for (let i = 0; i < lines.length; i++) if (patterns.some((re) => reTest(re, lines[i]))) last = i;
|
|
382
|
-
const start = Math.max(0, last - 6);
|
|
383
|
-
const end = Math.min(lines.length, last + 6);
|
|
384
|
-
return { question: lines.slice(start, end).map((l) => l.trim()).filter((l) => l && !isChromeLine(l)).join(" • ").slice(0, 400) };
|
|
385
|
-
}
|
|
386
|
-
const OPTION_LINE = /^[\s❯›>▶◉○●·│*-]*?(\d+)\.\s/;
|
|
387
|
-
/**
|
|
388
|
-
* Parse the selection menu a `needs_input` agent is parked on into a cursor
|
|
389
|
-
* position + the available option numbers, so a caller can compute how far the
|
|
390
|
-
* cursor must move (Down/Up) to reach a target option. Returns null when the
|
|
391
|
-
* screen isn't a menu (delegates that judgement to {@link classifyNeedsInput},
|
|
392
|
-
* so `working` still wins) or no numbered cursor line is found. Pure — the
|
|
393
|
-
* `ay select` action reuses the exact detection `ay ls` renders with.
|
|
394
|
-
*/
|
|
395
|
-
function parseMenu(lines, cfg) {
|
|
396
|
-
const ni = classifyNeedsInput(lines, cfg);
|
|
397
|
-
if (!ni) return null;
|
|
398
|
-
const patterns = cfg.needsInput ?? [];
|
|
399
|
-
let cursorLine = -1;
|
|
400
|
-
for (let i = 0; i < lines.length; i++) if (patterns.some((re) => reTest(re, lines[i]))) cursorLine = i;
|
|
401
|
-
if (cursorLine < 0) return null;
|
|
402
|
-
const cm = /(\d+)\./.exec(lines[cursorLine]);
|
|
403
|
-
if (!cm) return null;
|
|
404
|
-
const cursor = parseInt(cm[1], 10);
|
|
405
|
-
const start = Math.max(0, cursorLine - 12);
|
|
406
|
-
const end = Math.min(lines.length, cursorLine + 12);
|
|
407
|
-
const options = [];
|
|
408
|
-
for (let i = start; i < end; i++) {
|
|
409
|
-
const m = OPTION_LINE.exec(lines[i]);
|
|
410
|
-
if (m) {
|
|
411
|
-
const v = parseInt(m[1], 10);
|
|
412
|
-
if (!options.includes(v)) options.push(v);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
if (!options.includes(cursor)) options.push(cursor);
|
|
416
|
-
options.sort((a, b) => a - b);
|
|
417
|
-
return {
|
|
418
|
-
cursor,
|
|
419
|
-
options,
|
|
420
|
-
question: ni.question
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
/**
|
|
424
|
-
* True when the rendered screen still shows a "busy" marker (config `working`,
|
|
425
|
-
* e.g. claude's `esc to interrupt`). Paired with a long-quiet log this is the
|
|
426
|
-
* `stuck` signal: a live spinner writes to the log every frame, so a busy marker
|
|
427
|
-
* on screen WITHOUT recent output means the agent wedged mid-stream (a silent
|
|
428
|
-
* API stream stall) rather than finishing. Pure + synchronous like the rest of
|
|
429
|
-
* this module so it's trivially unit-testable.
|
|
430
|
-
*/
|
|
431
|
-
function isWorkingScreen(lines, working) {
|
|
432
|
-
if (!working?.length) return false;
|
|
433
|
-
const text = lines.join("\n");
|
|
434
|
-
return working.some((re) => reTest(re, text));
|
|
435
|
-
}
|
|
436
|
-
|
|
437
360
|
//#endregion
|
|
438
361
|
//#region ts/lsWatch.ts
|
|
439
362
|
/**
|
|
@@ -490,556 +413,6 @@ function toEvent(a, ts) {
|
|
|
490
413
|
};
|
|
491
414
|
}
|
|
492
415
|
|
|
493
|
-
//#endregion
|
|
494
|
-
//#region ts/notifyInbox.ts
|
|
495
|
-
/**
|
|
496
|
-
* Pure, fs-free core for the subagent→parent notification inbox — the storage
|
|
497
|
-
* and watermark layer under `ay notify` / `ay notifyd`.
|
|
498
|
-
*
|
|
499
|
-
* Motivation (real incident): a parent agent fanned out sub-agents that finished
|
|
500
|
-
* their work but sat at an idle `❯` prompt WITHOUT exiting (claude-yes does not
|
|
501
|
-
* exit on idle). Claude Code's background-task notification only fires on process
|
|
502
|
-
* EXIT, so the parent never learned the children went idle and left them parked
|
|
503
|
-
* 16 minutes. `ay ls --watch` already streams transitions, but it is PULL: the
|
|
504
|
-
* parent must run the watch loop. The whole point of this pain is the parent is
|
|
505
|
-
* NOT watching. So we accumulate qualifying edges into a per-parent append-only
|
|
506
|
-
* inbox that the parent drains on its own schedule (its Monitor loop), with a
|
|
507
|
-
* persisted cursor so a parent that restarts reads only the unread edges.
|
|
508
|
-
*
|
|
509
|
-
* This module is the pure part (path math, NDJSON (de)serialization, seq/cursor
|
|
510
|
-
* filtering, retention decisions) so it is trivially unit-testable, mirroring
|
|
511
|
-
* `lsWatch.ts` / `needsInput.ts` / `resultEnvelope.ts`. The fs read/write + lock
|
|
512
|
-
* + CLI live in `subcommands.ts`; the detection/debounce lives in
|
|
513
|
-
* `notifyRouter.ts`.
|
|
514
|
-
*/
|
|
515
|
-
/** Root dir for all notification state. */
|
|
516
|
-
function notifyDir() {
|
|
517
|
-
return path.join(agentYesHome(), "notify");
|
|
518
|
-
}
|
|
519
|
-
/** Per-host inbox directory (pids are only unique within a host). */
|
|
520
|
-
function inboxDir(host) {
|
|
521
|
-
return path.join(notifyDir(), "inbox", sanitizeHost(host));
|
|
522
|
-
}
|
|
523
|
-
/** Absolute path of one parent's append-only NDJSON inbox. */
|
|
524
|
-
function inboxPath(host, parentPid) {
|
|
525
|
-
return path.join(inboxDir(host), `${parentPid}.ndjson`);
|
|
526
|
-
}
|
|
527
|
-
/** Sidecar seq counter for a parent inbox (last allocated seq). */
|
|
528
|
-
function seqPath(host, parentPid) {
|
|
529
|
-
return path.join(inboxDir(host), `${parentPid}.seq`);
|
|
530
|
-
}
|
|
531
|
-
/** Per-consumer cursor directory for a parent inbox. */
|
|
532
|
-
function cursorDir(host, parentPid) {
|
|
533
|
-
return path.join(notifyDir(), "cursors", sanitizeHost(host), String(parentPid));
|
|
534
|
-
}
|
|
535
|
-
/** Absolute path of one consumer's cursor file for a parent inbox. */
|
|
536
|
-
function cursorPath(host, parentPid, consumer = "parent") {
|
|
537
|
-
return path.join(cursorDir(host, parentPid), `${sanitizeConsumer(consumer)}.json`);
|
|
538
|
-
}
|
|
539
|
-
/** The daemon singleton lock dir (mkdir-based, like pidStore). */
|
|
540
|
-
function daemonLockDir() {
|
|
541
|
-
return path.join(notifyDir(), "notifyd.lock");
|
|
542
|
-
}
|
|
543
|
-
/**
|
|
544
|
-
* Owner metadata inside the lock dir ({pid, started_at, ts}) — the SINGLE source
|
|
545
|
-
* of truth for the running daemon's identity, used for stale-lock steal and for
|
|
546
|
-
* `notifyd status`/`stop` (a running daemon refreshes `ts` each tick).
|
|
547
|
-
*/
|
|
548
|
-
function daemonLockOwnerPath() {
|
|
549
|
-
return path.join(daemonLockDir(), "owner.json");
|
|
550
|
-
}
|
|
551
|
-
/** Registry dir of parents currently running `ay notify watch` (heartbeats). */
|
|
552
|
-
function watchersDir() {
|
|
553
|
-
return path.join(notifyDir(), "watchers");
|
|
554
|
-
}
|
|
555
|
-
/** Heartbeat file for one watching parent. */
|
|
556
|
-
function watcherPath(parentPid) {
|
|
557
|
-
return path.join(watchersDir(), `${parentPid}.json`);
|
|
558
|
-
}
|
|
559
|
-
/** A watcher heartbeat is "live" if refreshed within this window. */
|
|
560
|
-
const WATCHER_TTL_MS = 15e3;
|
|
561
|
-
function sanitizeSegment(s, fallback) {
|
|
562
|
-
const cleaned = s.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
563
|
-
return cleaned === "" || cleaned === "." || cleaned === ".." ? fallback : cleaned;
|
|
564
|
-
}
|
|
565
|
-
function sanitizeHost(host) {
|
|
566
|
-
return sanitizeSegment(host, "localhost");
|
|
567
|
-
}
|
|
568
|
-
function sanitizeConsumer(consumer) {
|
|
569
|
-
return sanitizeSegment(consumer, "parent");
|
|
570
|
-
}
|
|
571
|
-
/** Serialize one event to its NDJSON line (no trailing newline). */
|
|
572
|
-
function serializeEvent(ev) {
|
|
573
|
-
return JSON.stringify(ev);
|
|
574
|
-
}
|
|
575
|
-
/**
|
|
576
|
-
* Parse an inbox's raw text into events. Tolerant by design: a torn final line
|
|
577
|
-
* (a writer mid-append) or any unparseable line is skipped, never throws — a
|
|
578
|
-
* reader must survive a concurrent writer. Order is file order (= seq order,
|
|
579
|
-
* since a single locked writer appends in increasing seq).
|
|
580
|
-
*/
|
|
581
|
-
function parseInboxText(text) {
|
|
582
|
-
const out = [];
|
|
583
|
-
for (const line of text.split("\n")) {
|
|
584
|
-
const t = line.trim();
|
|
585
|
-
if (!t) continue;
|
|
586
|
-
try {
|
|
587
|
-
const ev = JSON.parse(t);
|
|
588
|
-
if (isValidEvent(ev)) out.push(ev);
|
|
589
|
-
} catch {}
|
|
590
|
-
}
|
|
591
|
-
return out;
|
|
592
|
-
}
|
|
593
|
-
const EDGES = new Set([
|
|
594
|
-
"needs_input",
|
|
595
|
-
"idle",
|
|
596
|
-
"exited"
|
|
597
|
-
]);
|
|
598
|
-
/**
|
|
599
|
-
* Full shape validation at the storage boundary — a torn/partial line missing
|
|
600
|
-
* the correlation fields must never flow through to a consumer's output. Every
|
|
601
|
-
* event we surface has the fields a reader relies on.
|
|
602
|
-
*/
|
|
603
|
-
function isValidEvent(ev) {
|
|
604
|
-
if (!ev || typeof ev !== "object") return false;
|
|
605
|
-
const e = ev;
|
|
606
|
-
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");
|
|
607
|
-
}
|
|
608
|
-
/** Next seq to allocate given the last stored seq counter (or the inbox max). */
|
|
609
|
-
function nextSeq(lastSeq) {
|
|
610
|
-
return (Number.isFinite(lastSeq) && lastSeq > 0 ? lastSeq : 0) + 1;
|
|
611
|
-
}
|
|
612
|
-
/** Highest seq present in a parsed inbox (0 when empty). */
|
|
613
|
-
function maxSeq(events) {
|
|
614
|
-
let m = 0;
|
|
615
|
-
for (const e of events) if (e.seq > m) m = e.seq;
|
|
616
|
-
return m;
|
|
617
|
-
}
|
|
618
|
-
/**
|
|
619
|
-
* Filter events for `--since <seq>`: strictly-greater than the given seq. A
|
|
620
|
-
* `since` of 0/undefined returns everything.
|
|
621
|
-
*/
|
|
622
|
-
function filterSinceSeq(events, sinceSeq) {
|
|
623
|
-
if (!sinceSeq || sinceSeq <= 0) return events.slice();
|
|
624
|
-
return events.filter((e) => e.seq > sinceSeq);
|
|
625
|
-
}
|
|
626
|
-
/** Filter events after a wall-clock `--since <ts>` (inclusive lower bound). */
|
|
627
|
-
function filterSinceTs(events, sinceTs) {
|
|
628
|
-
if (!sinceTs || sinceTs <= 0) return events.slice();
|
|
629
|
-
return events.filter((e) => e.ts >= sinceTs);
|
|
630
|
-
}
|
|
631
|
-
/** Unread = seq strictly greater than the consumer's cursor. */
|
|
632
|
-
function filterUnread(events, cursorSeq) {
|
|
633
|
-
return events.filter((e) => e.seq > (cursorSeq > 0 ? cursorSeq : 0));
|
|
634
|
-
}
|
|
635
|
-
/** Parse a cursor file's text; a missing/garbage cursor reads as seq 0. */
|
|
636
|
-
function parseCursor(text) {
|
|
637
|
-
if (!text) return { seq: 0 };
|
|
638
|
-
try {
|
|
639
|
-
const c = JSON.parse(text);
|
|
640
|
-
return { seq: Number.isFinite(c?.seq) && c.seq > 0 ? c.seq : 0 };
|
|
641
|
-
} catch {
|
|
642
|
-
return { seq: 0 };
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
function serializeCursor(seq) {
|
|
646
|
-
return JSON.stringify({ seq: Math.max(0, Math.floor(seq)) });
|
|
647
|
-
}
|
|
648
|
-
/**
|
|
649
|
-
* Which incarnation of a parent pid a POSTMORTEM read should show (pure).
|
|
650
|
-
*
|
|
651
|
-
* The normal read path resolves the parent's `started_at` from the registry and
|
|
652
|
-
* fails closed when there is no live record — deliberately, so a recycled pid
|
|
653
|
-
* can't read a prior session's inbox. That also makes a FINISHED parent's inbox
|
|
654
|
-
* permanently uninspectable, which is exactly when an operator wants to see what
|
|
655
|
-
* the children reported. Postmortem keeps the identity guard but takes the
|
|
656
|
-
* identity from the inbox's own `parent_started_at` stamps instead.
|
|
657
|
-
*
|
|
658
|
-
* - An explicit choice always wins.
|
|
659
|
-
* - One distinct stamp is unambiguous — use it.
|
|
660
|
-
* - Several mean the pid was reused across sessions and the file holds more than
|
|
661
|
-
* one agent's edges: refuse and list them rather than guess, because guessing
|
|
662
|
-
* would show one agent's children under another's name.
|
|
663
|
-
* - None (a legacy, identity-less inbox) yields 0 = "no filter"; there is nothing
|
|
664
|
-
* to disambiguate and nothing to mis-attribute.
|
|
665
|
-
*
|
|
666
|
-
* Issue #169 item 6.
|
|
667
|
-
*/
|
|
668
|
-
function postmortemStartedAt(events, explicit) {
|
|
669
|
-
if (Number.isFinite(explicit) && explicit > 0) return explicit;
|
|
670
|
-
const distinct = [...new Set(events.map((e) => e.parent_started_at ?? 0).filter((v) => v > 0))].sort((a, b) => a - b);
|
|
671
|
-
if (distinct.length === 0) return 0;
|
|
672
|
-
if (distinct.length === 1) return distinct[0];
|
|
673
|
-
throw new Error(`inbox holds edges from ${distinct.length} incarnations of this pid (started_at ${distinct.join(", ")}) — pass --started-at <ms> to pick one.`);
|
|
674
|
-
}
|
|
675
|
-
/** Byte size one event occupies in the inbox file (its line + newline). */
|
|
676
|
-
function eventBytes(events) {
|
|
677
|
-
let n = 0;
|
|
678
|
-
for (const e of events) n += serializeEvent(e).length + 1;
|
|
679
|
-
return n;
|
|
680
|
-
}
|
|
681
|
-
/**
|
|
682
|
-
* EMERGENCY rotation (pure): keep the newest events that fit `capBytes`,
|
|
683
|
-
* IGNORING the un-acked watermark that {@link rotateKeep} protects.
|
|
684
|
-
*
|
|
685
|
-
* This is the escape hatch for the one way an inbox can grow without bound: a
|
|
686
|
-
* parent that watches WITHOUT `--ack` (the default, at-least-once) never
|
|
687
|
-
* advances its cursor, so every event stays protected and normal rotation can
|
|
688
|
-
* trim nothing. That is the right default — an unacked edge must not be dropped
|
|
689
|
-
* — but "right" stops being right once the file threatens the disk. Past a hard
|
|
690
|
-
* cap well above the soft one, dropping the OLDEST unacked events (loudly) beats
|
|
691
|
-
* unbounded growth.
|
|
692
|
-
*
|
|
693
|
-
* `minKeep` still floors the result, so the newest edges — the ones a parent is
|
|
694
|
-
* most likely to still act on — are never the ones sacrificed.
|
|
695
|
-
*
|
|
696
|
-
* Issue #169 item 3.
|
|
697
|
-
*/
|
|
698
|
-
function emergencyKeep(events, capBytes, minKeep = 100) {
|
|
699
|
-
if (events.length <= minKeep) return events.slice();
|
|
700
|
-
const kept = [];
|
|
701
|
-
let bytes = 0;
|
|
702
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
703
|
-
const e = events[i];
|
|
704
|
-
bytes += serializeEvent(e).length + 1;
|
|
705
|
-
if (kept.length < minKeep || bytes <= capBytes) kept.push(e);
|
|
706
|
-
else break;
|
|
707
|
-
}
|
|
708
|
-
kept.reverse();
|
|
709
|
-
return kept;
|
|
710
|
-
}
|
|
711
|
-
/**
|
|
712
|
-
* Rotation decision (pure): given an inbox's events, a byte cap, and the minimum
|
|
713
|
-
* un-acked cursor across ALL consumers, return the events to KEEP after
|
|
714
|
-
* rotating. Critically, an event with `seq > protectAboveSeq` is NEVER dropped —
|
|
715
|
-
* that is the at-least-once guarantee: we must not evict an edge no consumer has
|
|
716
|
-
* acknowledged yet, even under the byte cap. Below that watermark we keep the
|
|
717
|
-
* newest events that fit the cap (and at least `minKeep`).
|
|
718
|
-
*/
|
|
719
|
-
function rotateKeep(events, capBytes, protectAboveSeq = 0, minKeep = 100) {
|
|
720
|
-
if (events.length <= minKeep) return events.slice();
|
|
721
|
-
const kept = [];
|
|
722
|
-
let bytes = 0;
|
|
723
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
724
|
-
const e = events[i];
|
|
725
|
-
const line = serializeEvent(e) + "\n";
|
|
726
|
-
bytes += line.length;
|
|
727
|
-
if (e.seq > protectAboveSeq || kept.length < minKeep || bytes <= capBytes) kept.push(e);
|
|
728
|
-
else break;
|
|
729
|
-
}
|
|
730
|
-
kept.reverse();
|
|
731
|
-
return kept;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
//#endregion
|
|
735
|
-
//#region ts/notifyStore.ts
|
|
736
|
-
/**
|
|
737
|
-
* Filesystem side of the notification inbox — the impure half of the pure
|
|
738
|
-
* `notifyInbox.ts` path/serialization core. Handles the locked append (so
|
|
739
|
-
* concurrent daemon ticks / future writers never interleave a seq), inbox and
|
|
740
|
-
* cursor reads/writes, and retention GC.
|
|
741
|
-
*
|
|
742
|
-
* The lock is a mkdir-based advisory lock (same primitive `pidStore` uses), held
|
|
743
|
-
* only for the microseconds of a read-counter → append-line → write-counter, so
|
|
744
|
-
* contention is negligible even with many parents.
|
|
745
|
-
*/
|
|
746
|
-
/** Stable local host id — the pid namespace is per-host. */
|
|
747
|
-
function hostId() {
|
|
748
|
-
return os$1.hostname() || "localhost";
|
|
749
|
-
}
|
|
750
|
-
function pidAlive(pid) {
|
|
751
|
-
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
752
|
-
try {
|
|
753
|
-
process.kill(pid, 0);
|
|
754
|
-
return true;
|
|
755
|
-
} catch (e) {
|
|
756
|
-
return e.code === "EPERM";
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
/**
|
|
760
|
-
* Pure steal decision for a per-inbox lock (extracted so it's unit-testable):
|
|
761
|
-
* steal the lock ONLY when its holder is provably gone (torn/missing owner on a
|
|
762
|
-
* lock that has EXISTED longer than the grace, or a dead pid) or its heartbeat is
|
|
763
|
-
* stale — NEVER merely because the contender has waited a while.
|
|
764
|
-
*
|
|
765
|
-
* Crucially, the torn-owner grace is measured from the LOCK INSTANCE's age
|
|
766
|
-
* (`lockAgeMs` = now − lockDir mtime), NOT from how long the contender has been
|
|
767
|
-
* waiting. Otherwise a contender that waited out one holder could instantly steal
|
|
768
|
-
* the NEXT holder's freshly-created lock during its mkdir→write-owner window
|
|
769
|
-
* (torn but brand-new) — letting two writers into the critical section.
|
|
770
|
-
*/
|
|
771
|
-
function shouldStealLock(ownerRaw, now, opts) {
|
|
772
|
-
const graceMs = opts.graceMs ?? 1e3;
|
|
773
|
-
let ownerPid = 0;
|
|
774
|
-
let ownerTs = 0;
|
|
775
|
-
let parsed = false;
|
|
776
|
-
try {
|
|
777
|
-
const o = JSON.parse(ownerRaw);
|
|
778
|
-
ownerPid = o?.pid ?? 0;
|
|
779
|
-
ownerTs = o?.ts ?? 0;
|
|
780
|
-
parsed = true;
|
|
781
|
-
} catch {}
|
|
782
|
-
if (!parsed || ownerPid <= 0 || ownerTs <= 0) return opts.lockAgeMs > graceMs;
|
|
783
|
-
const holderDead = ownerPid !== opts.selfPid && !opts.isAlive(ownerPid);
|
|
784
|
-
const holderStale = now - ownerTs > opts.staleMs;
|
|
785
|
-
return holderDead || holderStale;
|
|
786
|
-
}
|
|
787
|
-
async function ensureDir(dir) {
|
|
788
|
-
await mkdir(dir, { recursive: true }).catch(() => {});
|
|
789
|
-
}
|
|
790
|
-
/**
|
|
791
|
-
* Atomically overwrite a single file (temp + rename) so a concurrent reader never
|
|
792
|
-
* observes a torn/partial write — the sweep invariant for every single-file
|
|
793
|
-
* mutation (cursor, watcher heartbeat, lock owner). Returns whether it landed.
|
|
794
|
-
*/
|
|
795
|
-
async function atomicWrite(file, data) {
|
|
796
|
-
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
797
|
-
try {
|
|
798
|
-
await writeFile(tmp, data);
|
|
799
|
-
await rename(tmp, file);
|
|
800
|
-
return true;
|
|
801
|
-
} catch {
|
|
802
|
-
await rm(tmp, { force: true }).catch(() => {});
|
|
803
|
-
return false;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
/**
|
|
807
|
-
* Acquire a per-inbox lock; returns a release fn. Ownership is proven ONLY by a
|
|
808
|
-
* successful `mkdir(recursive:false)` (atomic exclusive create), and stealing is
|
|
809
|
-
* driven by HOLDER LIVENESS, not elapsed wait time: the holder writes an
|
|
810
|
-
* `owner` file ({pid, ts}) on acquire AND heartbeats its ts for the whole time
|
|
811
|
-
* it holds the lock, so a contender steals ONLY when that holder is dead or its
|
|
812
|
-
* heartbeat is stale. A live holder whose critical section legitimately runs long
|
|
813
|
-
* (a big GC rewrite, a slow disk) is never robbed — its heartbeat stays fresh;
|
|
814
|
-
* and a wedged holder stops heartbeating, so staleMs still reclaims it. There is
|
|
815
|
-
* deliberately NO wall-clock backstop: elapsed wait time alone never steals, so a
|
|
816
|
-
* live holder is inviolable.
|
|
817
|
-
*/
|
|
818
|
-
function isTransientLockMkdirError(code, platform = process.platform) {
|
|
819
|
-
if (code === "EEXIST") return true;
|
|
820
|
-
return platform === "win32" && (code === "EPERM" || code === "EBUSY" || code === "ENOTEMPTY" || code === "EACCES");
|
|
821
|
-
}
|
|
822
|
-
async function acquireLock(lockDir, staleMs = 3e4) {
|
|
823
|
-
const ownerFile = path.join(lockDir, "owner");
|
|
824
|
-
const token = randomUUID();
|
|
825
|
-
const readOwnerToken = async () => {
|
|
826
|
-
try {
|
|
827
|
-
return JSON.parse(await readFile(ownerFile, "utf8")).token ?? null;
|
|
828
|
-
} catch {
|
|
829
|
-
return null;
|
|
830
|
-
}
|
|
831
|
-
};
|
|
832
|
-
const stampOwner = async () => {
|
|
833
|
-
const tmp = `${ownerFile}.${token}.tmp`;
|
|
834
|
-
try {
|
|
835
|
-
await writeFile(tmp, JSON.stringify({
|
|
836
|
-
pid: process.pid,
|
|
837
|
-
ts: Date.now(),
|
|
838
|
-
token
|
|
839
|
-
}));
|
|
840
|
-
await rename(tmp, ownerFile);
|
|
841
|
-
return true;
|
|
842
|
-
} catch {
|
|
843
|
-
await rm(tmp, { force: true }).catch(() => {});
|
|
844
|
-
return false;
|
|
845
|
-
}
|
|
846
|
-
};
|
|
847
|
-
const lockAgeMs = async () => {
|
|
848
|
-
const m = await stat(lockDir).then((s) => s.mtimeMs).catch(() => Date.now());
|
|
849
|
-
return Date.now() - m;
|
|
850
|
-
};
|
|
851
|
-
let transientErrors = 0;
|
|
852
|
-
for (;;) try {
|
|
853
|
-
await mkdir(lockDir, { recursive: false });
|
|
854
|
-
if (!await stampOwner()) {
|
|
855
|
-
await rm(lockDir, {
|
|
856
|
-
recursive: true,
|
|
857
|
-
force: true
|
|
858
|
-
}).catch(() => {});
|
|
859
|
-
await new Promise((r) => setTimeout(r, 15));
|
|
860
|
-
continue;
|
|
861
|
-
}
|
|
862
|
-
const beat = setInterval(() => {
|
|
863
|
-
(async () => {
|
|
864
|
-
if (await readOwnerToken() !== token) {
|
|
865
|
-
clearInterval(beat);
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
await stampOwner();
|
|
869
|
-
})();
|
|
870
|
-
}, Math.max(500, Math.floor(staleMs / 3)));
|
|
871
|
-
if (typeof beat.unref === "function") beat.unref();
|
|
872
|
-
return async () => {
|
|
873
|
-
clearInterval(beat);
|
|
874
|
-
if (await readOwnerToken() === token) await rm(lockDir, {
|
|
875
|
-
recursive: true,
|
|
876
|
-
force: true
|
|
877
|
-
}).catch(() => {});
|
|
878
|
-
};
|
|
879
|
-
} catch (e) {
|
|
880
|
-
const code = e.code;
|
|
881
|
-
if (!isTransientLockMkdirError(code)) throw e;
|
|
882
|
-
if (code !== "EEXIST") {
|
|
883
|
-
if (++transientErrors > 200) throw e;
|
|
884
|
-
await new Promise((r) => setTimeout(r, 15));
|
|
885
|
-
continue;
|
|
886
|
-
}
|
|
887
|
-
if (shouldStealLock(await readFile(ownerFile, "utf8").catch(() => ""), Date.now(), {
|
|
888
|
-
staleMs,
|
|
889
|
-
lockAgeMs: await lockAgeMs(),
|
|
890
|
-
selfPid: process.pid,
|
|
891
|
-
isAlive: pidAlive
|
|
892
|
-
})) {
|
|
893
|
-
await rm(lockDir, {
|
|
894
|
-
recursive: true,
|
|
895
|
-
force: true
|
|
896
|
-
}).catch(() => {});
|
|
897
|
-
continue;
|
|
898
|
-
}
|
|
899
|
-
await new Promise((r) => setTimeout(r, 15));
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
/**
|
|
903
|
-
* Append one event to a parent's inbox under lock, allocating the next seq. The
|
|
904
|
-
* caller passes the event WITHOUT seq (we stamp it). Returns the stamped seq.
|
|
905
|
-
*/
|
|
906
|
-
async function appendEvent(parentPid, ev) {
|
|
907
|
-
const host = ev.host;
|
|
908
|
-
const dir = inboxDir(host);
|
|
909
|
-
await ensureDir(dir);
|
|
910
|
-
const release = await acquireLock(path.join(dir, `${parentPid}.lock`));
|
|
911
|
-
try {
|
|
912
|
-
let last = 0;
|
|
913
|
-
const counterRaw = await readFile(seqPath(host, parentPid), "utf8").catch(() => "");
|
|
914
|
-
const parsed = parseInt(counterRaw.trim(), 10);
|
|
915
|
-
if (Number.isFinite(parsed) && parsed > 0) last = parsed;
|
|
916
|
-
else last = maxSeq(parseInboxText(await readFile(inboxPath(host, parentPid), "utf8").catch(() => "")));
|
|
917
|
-
const seq = nextSeq(last);
|
|
918
|
-
const full = {
|
|
919
|
-
...ev,
|
|
920
|
-
seq
|
|
921
|
-
};
|
|
922
|
-
await writeFile(seqPath(host, parentPid), String(seq));
|
|
923
|
-
await appendFile(inboxPath(host, parentPid), serializeEvent(full) + "\n");
|
|
924
|
-
return seq;
|
|
925
|
-
} finally {
|
|
926
|
-
await release();
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
/** Read + parse a parent's inbox (empty array when none). */
|
|
930
|
-
async function readInbox(host, parentPid) {
|
|
931
|
-
return parseInboxText(await readFile(inboxPath(host, parentPid), "utf8").catch(() => ""));
|
|
932
|
-
}
|
|
933
|
-
/** All parent pids that currently have an inbox on this host. */
|
|
934
|
-
async function listInboxParents(host) {
|
|
935
|
-
const names = await readdir(inboxDir(host)).catch(() => []);
|
|
936
|
-
const pids = [];
|
|
937
|
-
for (const n of names) {
|
|
938
|
-
const m = /^(\d+)\.ndjson$/.exec(n);
|
|
939
|
-
if (m) pids.push(parseInt(m[1], 10));
|
|
940
|
-
}
|
|
941
|
-
return pids;
|
|
942
|
-
}
|
|
943
|
-
async function getCursor(host, parentPid, consumer = "parent") {
|
|
944
|
-
return parseCursor(await readFile(cursorPath(host, parentPid, consumer), "utf8").catch(() => null)).seq;
|
|
945
|
-
}
|
|
946
|
-
/**
|
|
947
|
-
* Minimum cursor across ALL consumers of a parent inbox — the watermark below
|
|
948
|
-
* which rotation may evict events (nothing above it has been acked by every
|
|
949
|
-
* reader). Returns 0 when there are no consumers — which PROTECTS EVERYTHING:
|
|
950
|
-
* with a 0 watermark, `rotateKeep` treats every event as unacked and evicts
|
|
951
|
-
* nothing (an inbox nobody reads is never trimmed).
|
|
952
|
-
*/
|
|
953
|
-
async function minConsumerCursor(host, parentPid) {
|
|
954
|
-
const cursors = (await readdir(cursorDir(host, parentPid)).catch(() => [])).filter((n) => n.endsWith(".json")).map((n) => n.slice(0, -5));
|
|
955
|
-
if (cursors.length === 0) return 0;
|
|
956
|
-
let min = Infinity;
|
|
957
|
-
for (const c of cursors) min = Math.min(min, await getCursor(host, parentPid, c));
|
|
958
|
-
return Number.isFinite(min) ? min : 0;
|
|
959
|
-
}
|
|
960
|
-
async function setCursor(host, parentPid, seq, consumer = "parent") {
|
|
961
|
-
const p = cursorPath(host, parentPid, consumer);
|
|
962
|
-
await ensureDir(path.dirname(p));
|
|
963
|
-
await atomicWrite(p, serializeCursor(seq));
|
|
964
|
-
}
|
|
965
|
-
/**
|
|
966
|
-
* Retention: delete inboxes (+ their seq counter + cursors) for parents that are
|
|
967
|
-
* both dead and unreferenced by any live child. `capBytes` also rotates a live
|
|
968
|
-
* inbox that has grown past the cap, keeping the newest events.
|
|
969
|
-
*/
|
|
970
|
-
async function gcInboxes(host, livePids, liveChildParentPids, capBytes = 10 * 1024 * 1024, hardCapBytes = capBytes * 4) {
|
|
971
|
-
const parents = await listInboxParents(host);
|
|
972
|
-
for (const p of parents) {
|
|
973
|
-
const release = await acquireLock(path.join(inboxDir(host), `${p}.lock`));
|
|
974
|
-
try {
|
|
975
|
-
if (!livePids.has(p) && !liveChildParentPids.has(p)) {
|
|
976
|
-
await rm(inboxPath(host, p), { force: true }).catch(() => {});
|
|
977
|
-
await rm(seqPath(host, p), { force: true }).catch(() => {});
|
|
978
|
-
await rm(cursorDir(host, p), {
|
|
979
|
-
recursive: true,
|
|
980
|
-
force: true
|
|
981
|
-
}).catch(() => {});
|
|
982
|
-
continue;
|
|
983
|
-
}
|
|
984
|
-
const size = await stat(inboxPath(host, p)).then((s) => s.size).catch(() => 0);
|
|
985
|
-
if (size <= capBytes) continue;
|
|
986
|
-
const events = await readInbox(host, p);
|
|
987
|
-
let kept = rotateKeep(events, capBytes, await minConsumerCursor(host, p));
|
|
988
|
-
let dropped = [];
|
|
989
|
-
if (eventBytes(kept) > hardCapBytes) {
|
|
990
|
-
const before = kept;
|
|
991
|
-
kept = emergencyKeep(kept, capBytes);
|
|
992
|
-
dropped = before.slice(0, before.length - kept.length);
|
|
993
|
-
}
|
|
994
|
-
if (kept.length < events.length) {
|
|
995
|
-
await writeFile(inboxPath(host, p), kept.map(serializeEvent).join("\n") + "\n");
|
|
996
|
-
if (dropped.length > 0) logger.error(`[notify] EMERGENCY TRIM: inbox for parent ${p} exceeded the hard cap ${hardCapBytes} bytes — dropped ${dropped.length} UNACKED events (seq ${dropped[0].seq}..${dropped[dropped.length - 1].seq}). The consumer's cursor is not advancing: run \`ay notify watch --ack\` (or \`ay notify cursor set <seq>\`) for parent ${p}.`);
|
|
997
|
-
} else logger.warn(`[notify] inbox for parent ${p} is ${size} bytes (> soft cap ${capBytes}) but all events are unacked — cursor not advancing?`);
|
|
998
|
-
} finally {
|
|
999
|
-
await release();
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
/** Register / refresh this parent's watch heartbeat (atomically, so a concurrent
|
|
1004
|
-
* liveWatchers never reads a torn file and transiently drops the watcher). */
|
|
1005
|
-
async function heartbeatWatcher(parentPid, startedAt) {
|
|
1006
|
-
await ensureDir(watchersDir());
|
|
1007
|
-
await atomicWrite(watcherPath(parentPid), JSON.stringify({
|
|
1008
|
-
pid: parentPid,
|
|
1009
|
-
started_at: startedAt,
|
|
1010
|
-
ts: Date.now(),
|
|
1011
|
-
watch_pid: process.pid
|
|
1012
|
-
}));
|
|
1013
|
-
}
|
|
1014
|
-
/** Remove this parent's watch heartbeat (on watch exit). */
|
|
1015
|
-
async function clearWatcher(parentPid) {
|
|
1016
|
-
await rm(watcherPath(parentPid), { force: true }).catch(() => {});
|
|
1017
|
-
}
|
|
1018
|
-
/**
|
|
1019
|
-
* Live watchers as a map of parent pid → the parent's own `started_at` (its
|
|
1020
|
-
* authoritative self-reported start time, so the daemon never has to guess it
|
|
1021
|
-
* from the registry and stamp a 0). A watcher counts as live ONLY if its
|
|
1022
|
-
* heartbeat is fresh AND its process is actually alive — so a crashed `watch`
|
|
1023
|
-
* whose heartbeat lingers for the TTL doesn't keep the daemon writing to a dead
|
|
1024
|
-
* parent's inbox (which would violate "nothing happens unless you watch"). A
|
|
1025
|
-
* heartbeat with a missing/non-positive `started_at` is treated as NOT live: the
|
|
1026
|
-
* daemon's cross-session scope guard keys off a positive parent start time, so a
|
|
1027
|
-
* 0 would defeat it — fail closed.
|
|
1028
|
-
*/
|
|
1029
|
-
async function liveWatchers(now = Date.now()) {
|
|
1030
|
-
const names = await readdir(watchersDir()).catch(() => []);
|
|
1031
|
-
const live = /* @__PURE__ */ new Map();
|
|
1032
|
-
for (const n of names) {
|
|
1033
|
-
if (!n.endsWith(".json")) continue;
|
|
1034
|
-
const raw = await readFile(path.join(watchersDir(), n), "utf8").catch(() => "");
|
|
1035
|
-
try {
|
|
1036
|
-
const o = JSON.parse(raw);
|
|
1037
|
-
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) && (typeof o.watch_pid !== "number" || o.watch_pid <= 0 || pidAlive(o.watch_pid))) live.set(o.pid, o.started_at);
|
|
1038
|
-
} catch {}
|
|
1039
|
-
}
|
|
1040
|
-
return live;
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
416
|
//#endregion
|
|
1044
417
|
//#region ts/resultEnvelope.ts
|
|
1045
418
|
/**
|
|
@@ -1531,7 +904,7 @@ async function runSubcommand(argv) {
|
|
|
1531
904
|
case "tail": return await cmdRead(rest, { mode: "tail" });
|
|
1532
905
|
case "head": return await cmdRead(rest, { mode: "head" });
|
|
1533
906
|
case "hist":
|
|
1534
|
-
case "history": return await (await import("./hist-
|
|
907
|
+
case "history": return await (await import("./hist-BpocAe1N.js")).cmdHist(rest);
|
|
1535
908
|
case "send": return await cmdSend(rest);
|
|
1536
909
|
case "msgs": return await cmdMsgs(rest);
|
|
1537
910
|
case "key": return await cmdKey(rest);
|
|
@@ -1543,56 +916,56 @@ async function runSubcommand(argv) {
|
|
|
1543
916
|
case "restart": return await cmdRestart(rest);
|
|
1544
917
|
case "note": return await cmdNote(rest);
|
|
1545
918
|
case "todo": {
|
|
1546
|
-
const { runTodoSubcommand } = await import("./todoCli-
|
|
919
|
+
const { runTodoSubcommand } = await import("./todoCli-c_R7D7Y-.js");
|
|
1547
920
|
return runTodoSubcommand(rest);
|
|
1548
921
|
}
|
|
1549
922
|
case "ch":
|
|
1550
923
|
case "channels": {
|
|
1551
|
-
const { cmdCh } = await import("./channels-
|
|
924
|
+
const { cmdCh } = await import("./channels-DQaZbare.js");
|
|
1552
925
|
return cmdCh(rest);
|
|
1553
926
|
}
|
|
1554
927
|
case "term": {
|
|
1555
|
-
const { cmdTerm } = await import("./terminal-
|
|
928
|
+
const { cmdTerm } = await import("./terminal-Dm32SXU-.js");
|
|
1556
929
|
return cmdTerm(rest);
|
|
1557
930
|
}
|
|
1558
931
|
case "widget": {
|
|
1559
|
-
const { cmdWidget } = await import("./widget-
|
|
932
|
+
const { cmdWidget } = await import("./widget-DXtaX3QR.js");
|
|
1560
933
|
return cmdWidget(rest);
|
|
1561
934
|
}
|
|
1562
935
|
case "mint": {
|
|
1563
|
-
const { cmdMint } = await import("./widget-
|
|
936
|
+
const { cmdMint } = await import("./widget-DXtaX3QR.js");
|
|
1564
937
|
return cmdMint(rest);
|
|
1565
938
|
}
|
|
1566
939
|
case "serve": {
|
|
1567
|
-
const { cmdServe } = await import("./serve-
|
|
940
|
+
const { cmdServe } = await import("./serve-ucVXOci1.js");
|
|
1568
941
|
return cmdServe(rest);
|
|
1569
942
|
}
|
|
1570
943
|
case "tray": {
|
|
1571
|
-
const { cmdTray } = await import("./trayApp-
|
|
944
|
+
const { cmdTray } = await import("./trayApp-CZbwLK9t.js");
|
|
1572
945
|
return cmdTray(rest);
|
|
1573
946
|
}
|
|
1574
947
|
case "setup": {
|
|
1575
|
-
const { cmdSetup } = await import("./setup-
|
|
948
|
+
const { cmdSetup } = await import("./setup-De7uYRZb.js");
|
|
1576
949
|
return cmdSetup(rest);
|
|
1577
950
|
}
|
|
1578
951
|
case "ws": {
|
|
1579
|
-
const { cmdWs } = await import("./ws-
|
|
952
|
+
const { cmdWs } = await import("./ws-CqlS5JhN.js");
|
|
1580
953
|
return cmdWs(rest);
|
|
1581
954
|
}
|
|
1582
955
|
case "schedule": {
|
|
1583
|
-
const { cmdSchedule } = await import("./schedule-
|
|
956
|
+
const { cmdSchedule } = await import("./schedule-DbLpjgl5.js");
|
|
1584
957
|
return cmdSchedule(rest);
|
|
1585
958
|
}
|
|
1586
959
|
case "remote": {
|
|
1587
|
-
const { cmdRemote } = await import("./remotes-
|
|
960
|
+
const { cmdRemote } = await import("./remotes-Dg_JscKk.js");
|
|
1588
961
|
return cmdRemote(rest);
|
|
1589
962
|
}
|
|
1590
963
|
case "expose": {
|
|
1591
|
-
const { cmdExpose } = await import("./expose-
|
|
964
|
+
const { cmdExpose } = await import("./expose-BXn6My6A.js");
|
|
1592
965
|
return cmdExpose(rest);
|
|
1593
966
|
}
|
|
1594
967
|
case "callback": {
|
|
1595
|
-
const { cmdCallback } = await import("./callback-
|
|
968
|
+
const { cmdCallback } = await import("./callback-Da7MRLh6.js");
|
|
1596
969
|
return cmdCallback(rest);
|
|
1597
970
|
}
|
|
1598
971
|
case "reap":
|
|
@@ -1656,7 +1029,19 @@ async function buildAgentContextSection(self) {
|
|
|
1656
1029
|
latest: false,
|
|
1657
1030
|
cwdScope: null
|
|
1658
1031
|
})).find((r) => r.wrapper_pid === self.parent_pid) : void 0;
|
|
1659
|
-
|
|
1032
|
+
const whoAmI = `You are agent pid ${self.pid} (${self.cli}) in ${shortenPath(self.cwd)}.`;
|
|
1033
|
+
const parentLine = !hasParentPid ? `Top-level agent — no parent (started from a human shell or scheduler).` : parent ? `Spawned by agent pid ${parent.pid} (${parent.cli}) in ${shortenPath(parent.cwd)}.` : `Nested under a parent (wrapper pid ${self.parent_pid}) whose record isn't in the local registry.`;
|
|
1034
|
+
const dutyLine = parent ? ` You owe it a report: \`ay send ${parent.agent_id || parent.pid} "..."\` when you finish, and\n when you are blocked. It is not watching your terminal.\n` : ``;
|
|
1035
|
+
return `You are running inside an agent:\n ${whoAmI}\n ${parentLine}\n` + dutyLine + `
|
|
1036
|
+
As an agent, you can:
|
|
1037
|
+
Spawn a sub-agent:
|
|
1038
|
+
ay <cli> -- "<prompt>" auto-links as your child
|
|
1039
|
+
ay claude --model sonnet --advisor opus -- "<prompt>" routine task
|
|
1040
|
+
ay claude --model opus --advisor fable -- "<prompt>" complex task
|
|
1041
|
+
(pick --model by task complexity so easy tasks don't cost like hard ones;
|
|
1042
|
+
--advisor is a claude-cli flag — only takes effect for claude/cy)
|
|
1043
|
+
List agents (your children nest under your own pid in the tree):
|
|
1044
|
+
ay ls --cwd ${shortenPath(self.cwd)}\n Watch agent state changes, scoped to your workspace:\n ay ls --watch --cwd ${shortenPath(self.cwd)}\n (NDJSON stream of state changes across every matched agent — one watcher\n for the whole fan-out instead of N \`ay status --watch\`es)\n Read one sub-agent's output:\n ay tail -f <pid> follow live output (no single command tails\n many agents' content at once yet — loop\n \`ay ls --json\` pids into per-pid \`ay tail\`)\n\n`;
|
|
1660
1045
|
}
|
|
1661
1046
|
async function cmdHelp(managerCommands = true) {
|
|
1662
1047
|
const setupLine = managerCommands ? ` ay setup guided setup: pick a workspace, share to agent-yes.com\n` : ``;
|
|
@@ -2043,7 +1428,7 @@ async function fetchRemoteRecordsRaw(url, token, opts) {
|
|
|
2043
1428
|
let base = url;
|
|
2044
1429
|
let bearer = token;
|
|
2045
1430
|
if (isWebrtcSpec(url)) {
|
|
2046
|
-
const { startWebrtcBridge } = await import("./webrtcRemote-
|
|
1431
|
+
const { startWebrtcBridge } = await import("./webrtcRemote-BDg0jjTs.js");
|
|
2047
1432
|
bridge = await startWebrtcBridge(url);
|
|
2048
1433
|
base = bridge.baseUrl;
|
|
2049
1434
|
bearer = bridge.token;
|
|
@@ -3482,7 +2867,7 @@ async function cmdSend(rest) {
|
|
|
3482
2867
|
let suffix = "";
|
|
3483
2868
|
let nonce;
|
|
3484
2869
|
if (sender.agent && !isSlashCommand(body) && !raw) {
|
|
3485
|
-
nonce = randomBytes
|
|
2870
|
+
nonce = randomBytes(4).toString("hex");
|
|
3486
2871
|
const identity = formatIdentity({
|
|
3487
2872
|
cwd: sender.agent.cwd,
|
|
3488
2873
|
pid: sender.agent.pid
|
|
@@ -4080,7 +3465,7 @@ async function cmdAttach(rest) {
|
|
|
4080
3465
|
const winsizeDir = path.join(ayHome, "winsize");
|
|
4081
3466
|
await mkdir(winsizeDir, { recursive: true });
|
|
4082
3467
|
const winsizePath = path.join(winsizeDir, String(record.pid));
|
|
4083
|
-
const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-
|
|
3468
|
+
const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-ucVXOci1.js");
|
|
4084
3469
|
const daemonBase = await resolveDaemonHttpBase().catch(() => null);
|
|
4085
3470
|
const daemonToken = daemonBase ? await loadTokenReadOnly().catch(() => null) ?? "" : "";
|
|
4086
3471
|
const capViewer = `attach:${process.pid}`;
|
|
@@ -4790,7 +4175,7 @@ async function cmdNotify(rest) {
|
|
|
4790
4175
|
}
|
|
4791
4176
|
const ensure = async () => {
|
|
4792
4177
|
if (!argv["ensure-daemon"]) return;
|
|
4793
|
-
const { ensureDaemon } = await import("./notifyDaemon-
|
|
4178
|
+
const { ensureDaemon } = await import("./notifyDaemon-BFzqcdN1.js");
|
|
4794
4179
|
await ensureDaemon().catch(() => null);
|
|
4795
4180
|
};
|
|
4796
4181
|
await heartbeatWatcher(parent, selfStartedAt);
|
|
@@ -4864,7 +4249,7 @@ async function cmdNotifyCursor(args) {
|
|
|
4864
4249
|
}
|
|
4865
4250
|
async function cmdNotifyd(rest) {
|
|
4866
4251
|
const sub = rest[0] ?? "status";
|
|
4867
|
-
const daemon = await import("./notifyDaemon-
|
|
4252
|
+
const daemon = await import("./notifyDaemon-BFzqcdN1.js");
|
|
4868
4253
|
switch (sub) {
|
|
4869
4254
|
case "run": return daemon.runDaemon();
|
|
4870
4255
|
case "once": return daemon.runDaemon({ once: true });
|
|
@@ -4890,5 +4275,5 @@ async function cmdNotifyd(rest) {
|
|
|
4890
4275
|
}
|
|
4891
4276
|
|
|
4892
4277
|
//#endregion
|
|
4893
|
-
export {
|
|
4894
|
-
//# sourceMappingURL=subcommands-
|
|
4278
|
+
export { menuSelectKeys as A, resolveOne as B, isSubcommand as C, lastStdinAt as D, lastReadAt as E, recentMessageEdges as F, snapshotStatus as G, resolveResumeArgs as H, recentReadEdges as I, submitAndConfirm as J, stdinActivityPath as K, renderLogTailLines as L, readLogForRender as M, readNotes as N, listRecords as O, readPtysize as P, TYPING_BADGE as Q, renderRawLog as R, isSlashCommand as S, isUserTyping as T, restartHintLines as U, resolveReadWindow as V, runSubcommand as W, writeKeysPaced as X, waitForLogQuiet as Y, writeToIpc as Z, finalizedLines as _, TYPING_WINDOW_MS as a, isExitRequest as b, cmdHelp as c, deriveLiveState as d, deriveLiveStatus as f, extractTaskCounts as g, extractNeedsInput as h, READ_WINDOW_MS as i, readAgentPtysize as j, matchKeyword as k, controlCodeFromName as l, extractMenu as m, MAX_RENDER_BYTES as n, backoffWhileTyping as o, extractBadges as p, stopTipForCli as q, READ_PAGE_DEFAULT as r, cmdDeepseek as s, GRACEFUL_EXIT_COMMANDS as t, cursorAbs as u, isAgentStuck as v, isUnknownManagerToken as w, isPidAlive as x, isBareManagerInvocation as y, renderRawLogLines as z };
|
|
4279
|
+
//# sourceMappingURL=subcommands-DtF31RoW.js.map
|