agent-yes 1.194.0 → 1.196.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/ts/subcommands.ts CHANGED
@@ -20,6 +20,14 @@ import { type GlobalPidRecord, readGlobalPids, updateGlobalPidStatus } from "./g
20
20
  import { buildAgentForest, flattenForest } from "./agentTree.ts";
21
21
  import { parseTaskCounts, type TaskCounts } from "./todoParse.ts";
22
22
  import { agentYesHome } from "./agentYesHome.ts";
23
+ import {
24
+ type MailParty,
25
+ type MessageRecord,
26
+ partyMatches,
27
+ readMailbox,
28
+ recordMessage,
29
+ recordOutbox,
30
+ } from "./messageLog.ts";
23
31
  import { badgeDef, matchBadges, TYPING_BADGE } from "./badges.ts";
24
32
  import {
25
33
  classifyNeedsInput,
@@ -195,6 +203,49 @@ export async function recentReadEdges(windowMs = READ_WINDOW_MS): Promise<ReadEd
195
203
  return out;
196
204
  }
197
205
 
206
+ /** Recent agent→agent MESSAGE edges (a delivered `ay send`/`key`/`select`), for
207
+ * the /rgui + /w wire view. Like {@link ReadEdge} but sourced from the per-cwd
208
+ * outbox logs and carrying the send `kind`. `by`/`target` are the sender/recipient
209
+ * pids AT SEND TIME (a restarted agent keeps a new pid — matched best-effort). */
210
+ export interface MessageEdge {
211
+ by: number;
212
+ target: number;
213
+ at: number;
214
+ kind?: "key" | "select";
215
+ }
216
+
217
+ /**
218
+ * Scan every live agent's outbox for sends within `windowMs` and return them as
219
+ * directional edges (newest `at` per by→target pair wins). Bounded by the number
220
+ * of distinct agent cwds — the outbox is per-cwd, so each dir is read once.
221
+ */
222
+ export async function recentMessageEdges(windowMs = READ_WINDOW_MS): Promise<MessageEdge[]> {
223
+ const now = Date.now();
224
+ const records = await listRecords(undefined, {
225
+ all: true,
226
+ active: false,
227
+ json: false,
228
+ latest: false,
229
+ cwdScope: null,
230
+ });
231
+ const cwds = [...new Set(records.map((r) => r.cwd).filter(Boolean))];
232
+ const best = new Map<string, MessageEdge>();
233
+ await Promise.all(
234
+ cwds.map(async (cwd) => {
235
+ for (const rec of await readMailbox(cwd, "outbox")) {
236
+ if (now - rec.at > windowMs) continue;
237
+ const by = rec.from?.pid;
238
+ const target = rec.to?.pid;
239
+ if (!by || !target || by === target) continue; // agent→agent only
240
+ const key = `${by}\0${target}`;
241
+ const prev = best.get(key);
242
+ if (!prev || rec.at > prev.at) best.set(key, { by, target, at: rec.at, kind: rec.kind });
243
+ }
244
+ }),
245
+ );
246
+ return [...best.values()];
247
+ }
248
+
198
249
  // Identify the sender. An agent launched by `ay` inherits AGENT_YES_PID=<wrapper
199
250
  // pid>; the registered agent record carries that same wrapper_pid, so we map the
200
251
  // env value back to the agent's own canonical record. Falls back to a direct pid
@@ -296,6 +347,7 @@ const SUBCOMMANDS = new Set([
296
347
  "tail",
297
348
  "head",
298
349
  "send",
350
+ "msgs",
299
351
  "key",
300
352
  "select",
301
353
  "spawn",
@@ -397,6 +449,8 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
397
449
  return await cmdRead(rest, { mode: "head" });
398
450
  case "send":
399
451
  return await cmdSend(rest);
452
+ case "msgs":
453
+ return await cmdMsgs(rest);
400
454
  case "key":
401
455
  return await cmdKey(rest);
402
456
  case "select":
@@ -534,6 +588,7 @@ export async function cmdHelp(managerCommands = true): Promise<number> {
534
588
  ` ay cat <keyword> full log\n` +
535
589
  ` ay head <keyword> first N lines\n` +
536
590
  ` ay send <keyword> <msg> send a message\n` +
591
+ ` ay msgs [keyword] [--in|--out] inter-agent message log (sent + received)\n` +
537
592
  ` ay key <keyword> <key...> send raw keystrokes (down/up/enter/esc/…) — drives menus\n` +
538
593
  ` ay select <keyword> <N> pick option N of a needs_input selection menu\n` +
539
594
  ` ay attach <keyword> interactive attach (detach: Ctrl-\\)\n` +
@@ -979,13 +1034,48 @@ async function runRemoteSend(remote: ResolvedRemote, msg: string, code: string):
979
1034
  process.stderr.write("remote send requires a keyword (e.g. token@host:port:keyword)\n");
980
1035
  return 1;
981
1036
  }
982
- const res = await remotePost(remote, "/api/send", { keyword, msg, code });
1037
+ // Attribute the send so the remote can record its recipient's inbox with a real
1038
+ // sender (not an anonymous cross-wire write). Human shell → no `from`.
1039
+ const sender = await senderContext();
1040
+ const from = sender.agent
1041
+ ? {
1042
+ pid: sender.agent.pid,
1043
+ cli: sender.agent.cli,
1044
+ cwd: sender.agent.cwd,
1045
+ agent_id: sender.agent.agent_id,
1046
+ }
1047
+ : null;
1048
+ const res = await remotePost(remote, "/api/send", { keyword, msg, code, from });
983
1049
  if (!res.ok) {
984
1050
  process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
985
1051
  return 1;
986
1052
  }
987
- const data = (await res.json()) as any;
1053
+ const data = (await res.json()) as {
1054
+ pid: number;
1055
+ cli?: string;
1056
+ cwd?: string;
1057
+ agentId?: string;
1058
+ };
988
1059
  process.stdout.write(`sent to remote pid ${data.pid} (${remote.url} ${keyword})\n`);
1060
+ // Record the sender's half of the exchange locally (the recipient's inbox is
1061
+ // recorded on the remote host by its /api/send handler). Only real bodies.
1062
+ if (msg && msg !== "-") {
1063
+ await recordOutbox({
1064
+ at: Date.now(),
1065
+ from,
1066
+ to: {
1067
+ pid: data.pid,
1068
+ cli: data.cli ?? keyword,
1069
+ cwd: data.cwd ?? "",
1070
+ agent_id: data.agentId,
1071
+ },
1072
+ body: msg,
1073
+ code: code.toLowerCase() === "enter" ? undefined : code.toLowerCase(),
1074
+ confirmed: true,
1075
+ wrapped: false,
1076
+ remote: remote.url,
1077
+ });
1078
+ }
989
1079
  return 0;
990
1080
  }
991
1081
 
@@ -2975,8 +3065,9 @@ async function cmdSend(rest: string[]): Promise<number> {
2975
3065
  const replyTarget = sender.agent?.agent_id || sender.agent?.pid;
2976
3066
  let prefix = "";
2977
3067
  let suffix = "";
3068
+ let nonce: string | undefined;
2978
3069
  if (sender.agent && !isSlashCommand(body)) {
2979
- const nonce = randomBytes(4).toString("hex");
3070
+ nonce = randomBytes(4).toString("hex");
2980
3071
  prefix = `<ay-msg ${nonce} from ${sender.agent.cli} #${sender.agent.pid} @ ${shortenPath(sender.agent.cwd)} — reply: ay send ${replyTarget} "...">\n`;
2981
3072
  suffix = `\n</ay-msg ${nonce}>`;
2982
3073
  }
@@ -3031,6 +3122,35 @@ async function cmdSend(rest: string[]): Promise<number> {
3031
3122
  process.stdout.write(
3032
3123
  `${status} to pid ${record.pid} (${record.cli}): ${truncate(payload, 80)}\n`,
3033
3124
  );
3125
+
3126
+ // Persist a durable record of the exchange from both ends' point of view (the
3127
+ // sender's outbox + the recipient's inbox). Only real message bodies are
3128
+ // logged — a bare control code (esc/ctrl-c with no body) isn't a "message".
3129
+ // Best-effort: recordMessage swallows its own errors so it never breaks send.
3130
+ if (body) {
3131
+ await recordMessage({
3132
+ at: Date.now(),
3133
+ nonce,
3134
+ from: sender.agent
3135
+ ? {
3136
+ pid: sender.agent.pid,
3137
+ cli: sender.agent.cli,
3138
+ cwd: sender.agent.cwd,
3139
+ agent_id: sender.agent.agent_id,
3140
+ }
3141
+ : null,
3142
+ to: {
3143
+ pid: record.pid,
3144
+ cli: record.cli,
3145
+ cwd: record.cwd,
3146
+ agent_id: record.agent_id,
3147
+ },
3148
+ body,
3149
+ code: trailing === "\r" ? undefined : codeName,
3150
+ confirmed,
3151
+ wrapped: Boolean(nonce),
3152
+ });
3153
+ }
3034
3154
  if (!confirmed) {
3035
3155
  process.stderr.write(
3036
3156
  `\nwarning: couldn't confirm the CLI acted on it after ${SEND_SUBMIT_MAX_RETRIES + 1} attempt(s) — ` +
@@ -3059,6 +3179,119 @@ async function cmdSend(rest: string[]): Promise<number> {
3059
3179
  return confirmed ? 0 : 1;
3060
3180
  }
3061
3181
 
3182
+ // ---------------------------------------------------------------------------
3183
+ // ay msgs — read the durable inter-agent message log
3184
+ // ---------------------------------------------------------------------------
3185
+
3186
+ /** A mailbox record annotated with the direction it takes from the owner's POV. */
3187
+ interface DirectedMessage {
3188
+ dir: "in" | "out";
3189
+ rec: MessageRecord;
3190
+ }
3191
+
3192
+ /**
3193
+ * `ay msgs [keyword]` — show the inter-agent messages an agent sent and
3194
+ * received. With no keyword it uses THIS caller's context (the agent running
3195
+ * `ay msgs`, or the human shell's cwd); with a keyword it resolves one agent and
3196
+ * reads that agent's mailboxes. Newest last, like a chat log.
3197
+ */
3198
+ async function cmdMsgs(rest: string[]): Promise<number> {
3199
+ const y = yargs(rest)
3200
+ .usage("Usage: ay msgs [keyword] [--in|--out] [-n N] [--json]")
3201
+ .option("in", { type: "boolean", default: false, description: "Only received messages" })
3202
+ .option("out", { type: "boolean", default: false, description: "Only sent messages" })
3203
+ .option("n", { type: "number", description: "Show the last N messages (default 50)" })
3204
+ .option("json", { type: "boolean", default: false, description: "Emit raw JSONL records" })
3205
+ .option("all", { type: "boolean", default: false, description: "Include exited agents" })
3206
+ .option("latest", {
3207
+ type: "boolean",
3208
+ default: false,
3209
+ description: "Use most recent match when multiple match",
3210
+ })
3211
+ .option("cwd", { type: "string", description: "Restrict to agents under this dir" })
3212
+ .help(false)
3213
+ .version(false)
3214
+ .exitProcess(false);
3215
+
3216
+ const argv = await y.parseAsync();
3217
+ const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
3218
+
3219
+ // Whose mailbox: an explicit keyword names a target agent; otherwise the
3220
+ // calling agent (or, for a human shell, its own cwd with no agent filter).
3221
+ let ownerCwd: string;
3222
+ let ownerAgentId: string | null | undefined;
3223
+ let ownerPid: number | null | undefined;
3224
+ let ownerLabel: string;
3225
+ if (keyword) {
3226
+ const opts: CommonOpts = {
3227
+ all: argv.all,
3228
+ active: false,
3229
+ json: false,
3230
+ latest: argv.latest,
3231
+ cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null,
3232
+ };
3233
+ const record = await resolveOne(keyword, opts);
3234
+ ownerCwd = record.cwd;
3235
+ ownerAgentId = record.agent_id;
3236
+ ownerPid = record.pid;
3237
+ ownerLabel = `pid ${record.pid} (${record.cli})`;
3238
+ } else {
3239
+ const sender = await senderContext();
3240
+ ownerCwd = sender.agent?.cwd ?? process.cwd();
3241
+ ownerAgentId = sender.agent?.agent_id;
3242
+ ownerPid = sender.agent?.pid;
3243
+ ownerLabel = sender.agent ? `pid ${sender.agent.pid} (${sender.agent.cli})` : "this shell";
3244
+ }
3245
+
3246
+ const isOwner = (party: MailParty | null): boolean =>
3247
+ // A human shell (no agent context) owns only the messages it sent, which
3248
+ // carry `from: null`; match those so `ay msgs` in a plain terminal works.
3249
+ ownerAgentId || ownerPid ? partyMatches(party, ownerAgentId, ownerPid) : party === null;
3250
+
3251
+ const messages: DirectedMessage[] = [];
3252
+ if (!argv.in) {
3253
+ for (const rec of await readMailbox(ownerCwd, "outbox")) {
3254
+ if (isOwner(rec.from)) messages.push({ dir: "out", rec });
3255
+ }
3256
+ }
3257
+ if (!argv.out) {
3258
+ for (const rec of await readMailbox(ownerCwd, "inbox")) {
3259
+ if (isOwner(rec.to)) messages.push({ dir: "in", rec });
3260
+ }
3261
+ }
3262
+ messages.sort((a, b) => a.rec.at - b.rec.at);
3263
+
3264
+ const limit =
3265
+ argv.n !== undefined && Number.isFinite(argv.n) && argv.n! > 0 ? Math.floor(argv.n!) : 50;
3266
+ const shown = messages.slice(-limit);
3267
+
3268
+ if (argv.json) {
3269
+ for (const { dir, rec } of shown) {
3270
+ process.stdout.write(JSON.stringify({ dir, ...rec }) + "\n");
3271
+ }
3272
+ return 0;
3273
+ }
3274
+
3275
+ if (shown.length === 0) {
3276
+ process.stderr.write(`no messages for ${ownerLabel}.\n`);
3277
+ return 0;
3278
+ }
3279
+
3280
+ for (const { dir, rec } of shown) {
3281
+ const when = new Date(rec.at).toLocaleTimeString();
3282
+ const peer =
3283
+ dir === "out"
3284
+ ? `→ ${rec.to.cli} #${rec.to.pid}`
3285
+ : `← ${rec.from ? `${rec.from.cli} #${rec.from.pid}` : "human"}`;
3286
+ const via = rec.remote ? ` (via ${rec.remote})` : "";
3287
+ const flag = rec.confirmed === false ? " (unconfirmed)" : "";
3288
+ const tag = rec.kind ? `[${rec.kind}] ` : "";
3289
+ const line = tag + truncate(rec.body.replace(/\s+/g, " "), 100);
3290
+ process.stdout.write(`${when} ${peer.padEnd(20)}${via}${flag} ${line}\n`);
3291
+ }
3292
+ return 0;
3293
+ }
3294
+
3062
3295
  // Resolve a keyword to one agent and return it with a writable FIFO, or throw
3063
3296
  // with the same guidance cmdSend gives. Shared by `ay key` / `ay select`.
3064
3297
  async function resolveWritableAgent(keyword: string, opts: CommonOpts): Promise<GlobalPidRecord> {
@@ -3071,6 +3304,36 @@ async function resolveWritableAgent(keyword: string, opts: CommonOpts): Promise<
3071
3304
  return record;
3072
3305
  }
3073
3306
 
3307
+ /**
3308
+ * Record an `ay key` / `ay select` stdin write in the message log, same as a
3309
+ * text send but tagged with its `kind` (and `body` = the keystroke names /
3310
+ * chosen option). Key/select are local-only (no remote wire), so both mailboxes
3311
+ * are on this host — recordMessage writes both. Best-effort.
3312
+ */
3313
+ async function recordKeyEvent(
3314
+ sender: { agent: GlobalPidRecord | null },
3315
+ record: GlobalPidRecord,
3316
+ kind: "key" | "select",
3317
+ body: string,
3318
+ ): Promise<void> {
3319
+ await recordMessage({
3320
+ at: Date.now(),
3321
+ from: sender.agent
3322
+ ? {
3323
+ pid: sender.agent.pid,
3324
+ cli: sender.agent.cli,
3325
+ cwd: sender.agent.cwd,
3326
+ agent_id: sender.agent.agent_id,
3327
+ }
3328
+ : null,
3329
+ to: { pid: record.pid, cli: record.cli, cwd: record.cwd, agent_id: record.agent_id },
3330
+ kind,
3331
+ body,
3332
+ confirmed: true,
3333
+ wrapped: false,
3334
+ });
3335
+ }
3336
+
3074
3337
  async function cmdKey(rest: string[]): Promise<number> {
3075
3338
  const y = yargs(rest)
3076
3339
  .usage(
@@ -3118,10 +3381,11 @@ async function cmdKey(rest: string[]): Promise<number> {
3118
3381
  };
3119
3382
  const record = await resolveWritableAgent(keyword, opts);
3120
3383
  const force = Boolean(argv.force) || process.env.AGENT_YES_FORCE_SEND === "1";
3121
- await enforceSendGuards(record, force);
3384
+ const sender = await enforceSendGuards(record, force);
3122
3385
 
3123
3386
  await writeKeysPaced(record.fifo_file!, byteSeqs, Math.max(0, argv.pace));
3124
3387
  process.stdout.write(`sent to pid ${record.pid} (${record.cli}): ${keyNames.join(" ")}\n`);
3388
+ await recordKeyEvent(sender, record, "key", keyNames.join(" "));
3125
3389
  return 0;
3126
3390
  }
3127
3391
 
@@ -3178,7 +3442,7 @@ async function cmdSelect(rest: string[]): Promise<number> {
3178
3442
  );
3179
3443
  }
3180
3444
  const force = Boolean(argv.force) || process.env.AGENT_YES_FORCE_SEND === "1";
3181
- await enforceSendGuards(record, force);
3445
+ const sender = await enforceSendGuards(record, force);
3182
3446
 
3183
3447
  const menu = await extractMenu(record.log_file, record.cli);
3184
3448
  if (!menu) {
@@ -3202,6 +3466,7 @@ async function cmdSelect(rest: string[]): Promise<number> {
3202
3466
  process.stdout.write(
3203
3467
  `pid ${record.pid} (${record.cli}): selected option ${n} (${moved} + enter)\n`,
3204
3468
  );
3469
+ await recordKeyEvent(sender, record, "select", `option ${n}`);
3205
3470
 
3206
3471
  if (argv.wait) {
3207
3472
  const ok = await waitForNeedsInputClear(record, Math.max(1, argv.timeout) * 1000);
@@ -1,8 +0,0 @@
1
- import "./ts-6JcbtJh1.js";
2
- import "./logger-CDIsZ-Pp.js";
3
- import "./versionChecker-DDNKcQRx.js";
4
- import "./pidStore-BIvsBQ8X.js";
5
- import "./globalPidIndex-CoNr7tS8.js";
6
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS--WH2XfM2.js";
7
-
8
- export { SUPPORTED_CLIS };
@@ -1,9 +0,0 @@
1
- import "./logger-CDIsZ-Pp.js";
2
- import "./globalPidIndex-CoNr7tS8.js";
3
- import "./configShared-0MnIQ652.js";
4
- import { A as renderRawLogLines, B as waitForLogQuiet, C as matchKeyword, D as recentReadEdges, E as readPtysize, F as runSubcommand, H as writeToIpc, I as snapshotStatus, L as stdinActivityPath, M as resolveReadWindow, N as resolveResumeArgs, O as renderLogTailLines, P as restartHintLines, R as stopTipForCli, S as listRecords, T as readNotes, V as writeKeysPaced, _ as isPidAlive, a as cmdHelp, b as isUserTyping, c as deriveLiveState, d as extractMenu, f as extractNeedsInput, g as isExitRequest, h as isAgentStuck, i as backoffWhileTyping, j as resolveOne, k as renderRawLog, l as deriveLiveStatus, m as finalizedLines, n as READ_PAGE_DEFAULT, o as controlCodeFromName, p as extractTaskCounts, r as TYPING_WINDOW_MS, s as cursorAbs, t as GRACEFUL_EXIT_COMMANDS, u as extractBadges, v as isSlashCommand, w as menuSelectKeys, x as lastStdinAt, y as isSubcommand, z as submitAndConfirm } from "./subcommands-Ba0V9EEH.js";
5
- import "./e2e-BeKjLhmO.js";
6
- import "./webrtcLink-BG0Xc4-W.js";
7
- import "./remotes-oSmwSYaV.js";
8
-
9
- export { cmdHelp, isSubcommand, runSubcommand };