agent-yes 1.196.2 → 1.197.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.
Files changed (45) hide show
  1. package/default.config.yaml +51 -12
  2. package/dist/{SUPPORTED_CLIS-BuiThZSF.js → SUPPORTED_CLIS-BY83eM58.js} +2 -2
  3. package/dist/SUPPORTED_CLIS-D4Z--hGm.js +9 -0
  4. package/dist/{agent-yes.config-ugPYTPOd.js → agent-yes.config-whqkqq9P.js} +2 -2
  5. package/dist/{agentShare-BMZVmm7c.js → agentShare-DWioPObE.js} +8 -7
  6. package/dist/cli.js +8 -8
  7. package/dist/{configShared-0MnIQ652.js → configShared-aKTg-sa5.js} +1 -1
  8. package/dist/{e2e-BeKjLhmO.js → e2e-jb0Hp43q.js} +1 -1
  9. package/dist/{expose-B3AGhVhM.js → expose-k9l6ZJDA.js} +1 -1
  10. package/dist/{forkNested-C1kKsXi5.js → forkNested-DOyDAHnC.js} +1 -1
  11. package/dist/index.js +3 -2
  12. package/dist/messageLog-CxrKJj77.js +94 -0
  13. package/dist/{nodeRuntime-DZRjxpNp.js → nodeRuntime-CwNJuwH5.js} +1 -1
  14. package/dist/{notifyDaemon-Cikn_qKr.js → notifyDaemon-CRf_27kE.js} +7 -6
  15. package/dist/{openBrowser-DQVkxCyj.js → openBrowser-CSj21pZE.js} +1 -1
  16. package/dist/{reaper-CWF2_ATd.js → reaper-BUHCyxdF.js} +1 -1
  17. package/dist/{reaper-jfSmog15.js → reaper-CJSu8UVZ.js} +1 -1
  18. package/dist/{remotes-CW-yxSgW.js → remotes-CXqYR0p8.js} +3 -3
  19. package/dist/{remotes-oSmwSYaV.js → remotes-Cim0dBU7.js} +3 -3
  20. package/dist/{rustBinary-Uxkcz3zV.js → rustBinary-5HCt7_0b.js} +2 -2
  21. package/dist/{schedule-cvu-t8uo.js → schedule-CjBcOy-b.js} +7 -6
  22. package/dist/{serve-Cp9XGHAg.js → serve-C3E8uicL.js} +45 -26
  23. package/dist/{setup-onV23tOF.js → setup-C4SKqGy1.js} +3 -3
  24. package/dist/{share-29YbzpbU.js → share-BfIU8t_h.js} +2 -2
  25. package/dist/{share-nvqy64A2.js → share-Ciw1mWVN.js} +2 -2
  26. package/dist/{spawnGate-CWbdnJ5K.js → spawnGate-BkjeS4tg.js} +1 -1
  27. package/dist/{spawnGate-DRWa_Fzj.js → spawnGate-CXjEz6Pf.js} +2 -2
  28. package/dist/{subcommands-Dwzo0rx_.js → subcommands-B7UxIJRw.js} +17 -103
  29. package/dist/{subcommands-BReg256R.js → subcommands-DzWBZ6HE.js} +6 -5
  30. package/dist/{tray-BKTZ5gIf.js → tray-BSFi35Qg.js} +1 -1
  31. package/dist/{ts-ml_9eD1Y.js → ts-B64pNQlI.js} +84 -7
  32. package/dist/{versionChecker-ufJXjxRK.js → versionChecker-DKiSuTSp.js} +2 -2
  33. package/dist/{webrtcLink-BG0Xc4-W.js → webrtcLink-B7REGtK2.js} +2 -2
  34. package/dist/{webrtcRemote-BmuVKcrx.js → webrtcRemote-CvIEfWX8.js} +3 -3
  35. package/dist/{workspaceConfig-_GtAZtsi.js → workspaceConfig-CjaRvTjf.js} +1 -1
  36. package/package.json +1 -1
  37. package/ts/autoRetry.spec.ts +90 -1
  38. package/ts/autoRetry.ts +76 -0
  39. package/ts/index.ts +37 -4
  40. package/ts/messageLog.ts +3 -2
  41. package/ts/serve.ts +4 -5
  42. package/ts/subcommands.ts +9 -5
  43. package/ts/terminalReply.spec.ts +36 -0
  44. package/ts/terminalReply.ts +17 -0
  45. package/dist/SUPPORTED_CLIS-BBTAJQVG.js +0 -8
package/ts/autoRetry.ts CHANGED
@@ -37,3 +37,79 @@ export function shouldFireRetry(
37
37
  ): boolean {
38
38
  return !working && ready && idleMs >= minIdleMs;
39
39
  }
40
+
41
+ /** Fallback reason when no autoRetry match was captured before firing. */
42
+ export const AUTO_RETRY_REASON_FALLBACK = "a transient server-side error";
43
+
44
+ /**
45
+ * All reason strings `classifyAutoRetryReason` can produce — used by the
46
+ * self-trigger guard spec to prove every possible typed message is inert
47
+ * against every CLI's screen-scrape patterns. Mirrors Rust's RETRY_REASONS.
48
+ */
49
+ export const AUTO_RETRY_REASONS = [
50
+ "the model backend reported it is temporarily busy",
51
+ "the response stream stalled mid-way",
52
+ "the connection dropped mid-response",
53
+ "a usage cap was reached (it may need time to reset)",
54
+ "requests are being throttled by the server",
55
+ AUTO_RETRY_REASON_FALLBACK,
56
+ ] as const;
57
+
58
+ /**
59
+ * Paraphrase the matched recoverable-error banner into a short reason.
60
+ *
61
+ * Deliberately NEVER echoes the raw banner wording ("API Error", "Overloaded",
62
+ * "rate limit", …): the built message is typed into the PTY and stays visible
63
+ * in the transcript, where raw wording would re-match the autoRetry patterns —
64
+ * keeping the error state true forever, which blocks the streak reset on
65
+ * recovery. Mirrors Rust's classify_retry_reason.
66
+ */
67
+ export function classifyAutoRetryReason(screen: string): string {
68
+ const s = screen.toLowerCase();
69
+ if (s.includes("overload")) return AUTO_RETRY_REASONS[0];
70
+ if (s.includes("stalled")) return AUTO_RETRY_REASONS[1];
71
+ if (s.includes("connection closed")) return AUTO_RETRY_REASONS[2];
72
+ if (s.includes("usage limit") || s.includes("session limit")) return AUTO_RETRY_REASONS[3];
73
+ if (s.includes("rate") && s.includes("limit")) return AUTO_RETRY_REASONS[4];
74
+ return AUTO_RETRY_REASON_FALLBACK;
75
+ }
76
+
77
+ /** Human-readable duration: "45s", "3m20s", "1h04m", "8h". Mirrors Rust's fmt_dur_secs. */
78
+ export function formatDurationSecs(total: number): string {
79
+ const t = Math.max(0, Math.floor(total));
80
+ if (t < 60) return `${t}s`;
81
+ if (t < 3600) {
82
+ const m = Math.floor(t / 60);
83
+ const s = t % 60;
84
+ return s === 0 ? `${m}m` : `${m}m${String(s).padStart(2, "0")}s`;
85
+ }
86
+ const h = Math.floor(t / 3600);
87
+ const m = Math.floor((t % 3600) / 60);
88
+ return m === 0 ? `${h}h` : `${h}h${String(m).padStart(2, "0")}m`;
89
+ }
90
+
91
+ /**
92
+ * The single line typed into the agent's prompt instead of a bare "retry":
93
+ * says WHO is nudging (agent-yes, automated), WHY (paraphrased reason), and
94
+ * the backoff state (attempt #, elapsed, next delay, give-up horizon), plus an
95
+ * explicit "ignore if nothing failed" clause so an agent that already
96
+ * recovered — or is mid-question — doesn't burn a turn asking what "retry"
97
+ * refers to. Must stay one line (it is submitted with a single "\r").
98
+ * Mirrors Rust's build_retry_message in rs/src/context.rs.
99
+ */
100
+ export function buildAutoRetryMessage(
101
+ attempt: number,
102
+ reason: string,
103
+ sinceFirstSecs: number,
104
+ nextBackoffSecs: number,
105
+ ): string {
106
+ const since = formatDurationSecs(sinceFirstSecs);
107
+ const next = formatDurationSecs(nextBackoffSecs);
108
+ const giveUp = formatDurationSecs(AUTO_RETRY_GIVE_UP_MS / 1000);
109
+ return (
110
+ `retry [auto-retry #${attempt} by agent-yes: ${reason}; first seen ${since} ago; ` +
111
+ `if this attempt fails too, the next nudge comes in ${next} (giving up after ${giveUp}). ` +
112
+ `This is an automated recovery nudge - if no request actually failed, ignore it and ` +
113
+ `simply continue your previous task.]`
114
+ );
115
+ }
package/ts/index.ts CHANGED
@@ -21,9 +21,14 @@ import { sendEnter, sendMessage } from "./core/messaging.ts";
21
21
  import {
22
22
  AUTO_RETRY_GIVE_UP_MS,
23
23
  AUTO_RETRY_MIN_IDLE_MS,
24
+ AUTO_RETRY_REASON_FALLBACK,
24
25
  autoRetryBackoffMs,
26
+ buildAutoRetryMessage,
27
+ classifyAutoRetryReason,
28
+ formatDurationSecs,
25
29
  shouldFireRetry,
26
30
  } from "./autoRetry.ts";
31
+ import { recordInbox } from "./messageLog.ts";
27
32
  import {
28
33
  initializeLogPaths,
29
34
  setupDebugLogging,
@@ -794,6 +799,9 @@ export default async function agentYes({
794
799
  let retryStartedAt: number | null = null;
795
800
  let retryNextAt: number | null = null;
796
801
  let autoRetryScreen = "";
802
+ // Paraphrased reason captured when the error banner was matched — folded
803
+ // into the typed retry message so the agent knows why it is being nudged.
804
+ let retryReason: string | null = null;
797
805
  const heartbeatInterval = setInterval(async () => {
798
806
  try {
799
807
  const rendered = removeControlCharacters(xtermProxy.tail(12));
@@ -822,15 +830,36 @@ export default async function agentYes({
822
830
  retryNextAt = now + 500; // busy / not at prompt / still active — re-check shortly
823
831
  } else {
824
832
  retryStreak += 1;
825
- logger.warn(`[${cli}-yes] auto-retry: typing 'retry' (attempt ${retryStreak})`);
826
- // Write "retry" + Enter atomically (mirrors rs do_send_retry); using
833
+ const nextBackoffMs = autoRetryBackoffMs(retryStreak);
834
+ const reason = retryReason ?? AUTO_RETRY_REASON_FALLBACK;
835
+ const sinceFirstSecs = retryStartedAt === null ? 0 : (now - retryStartedAt) / 1000;
836
+ const line = buildAutoRetryMessage(
837
+ retryStreak,
838
+ reason,
839
+ sinceFirstSecs,
840
+ nextBackoffMs / 1000,
841
+ );
842
+ logger.warn(
843
+ `[${cli}-yes] auto-retry: typing retry nudge (attempt ${retryStreak}, reason: ${reason})`,
844
+ );
845
+ // Write the nudge + Enter atomically (mirrors rs do_send_retry); using
827
846
  // sendMessage would split text/Enter across the fast heartbeat ticks.
828
- ctx.messageContext.shell.write("retry\r");
847
+ ctx.messageContext.shell.write(line + "\r");
829
848
  ctx.idleWaiter.ping();
849
+ // Structured trace for `ay msgs` / the console (mirrors the Rust
850
+ // runtime's record_auto_retry_inbox) — best-effort, never blocks.
851
+ void recordInbox({
852
+ at: now,
853
+ from: null,
854
+ to: { pid: process.pid, cli, cwd: workingDir },
855
+ kind: "auto-retry",
856
+ body: `${reason} (attempt ${retryStreak}, next backoff ${formatDurationSecs(nextBackoffMs / 1000)})`,
857
+ wrapped: false,
858
+ });
830
859
  // Self-schedule the next retry with escalated backoff. (Leaving nextAt
831
860
  // null and re-arming from the stdout pipeline would tight-loop while the
832
861
  // error banner stays on screen.) Reset on recovery cancels this.
833
- retryNextAt = now + autoRetryBackoffMs(retryStreak);
862
+ retryNextAt = now + nextBackoffMs;
834
863
  }
835
864
  }
836
865
  }
@@ -1185,6 +1214,10 @@ export default async function agentYes({
1185
1214
  const errVisible = conf.autoRetry.some((rx: RegExp) => rx.test(rendered));
1186
1215
  const readyVisible = conf.ready?.some((rx: RegExp) => rx.test(rendered)) ?? false;
1187
1216
  if (errVisible && readyVisible) {
1217
+ // Remember WHY (paraphrased — see classifyAutoRetryReason) so
1218
+ // the typed message can explain itself. Refresh on every match:
1219
+ // the banner may change across attempts (e.g. overload → 5xx).
1220
+ retryReason = classifyAutoRetryReason(rendered);
1188
1221
  if (retryNextAt === null) {
1189
1222
  if (retryStartedAt === null) retryStartedAt = Date.now();
1190
1223
  const delayMs = autoRetryBackoffMs(retryStreak);
package/ts/messageLog.ts CHANGED
@@ -39,8 +39,9 @@ export interface MessageRecord {
39
39
  to: MailParty;
40
40
  /** What kind of stdin write this was. Omitted for a normal `ay send` text
41
41
  * message; "key" for raw keystrokes (`ay key`), "select" for a menu pick
42
- * (`ay select`). For key/select the `body` holds the key names / choice. */
43
- kind?: "key" | "select";
42
+ * (`ay select`), "auto-retry" for the wrapper's own recoverable-error nudge
43
+ * (from is null; `body` holds the paraphrased reason + backoff state). */
44
+ kind?: "key" | "select" | "auto-retry";
44
45
  /** The message body (without the `[ay-msg …]` wrapper), or — for a key/select
45
46
  * record — the keystroke names / chosen option. */
46
47
  body: string;
package/ts/serve.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  type CommonOpts,
27
27
  } from "./subcommands.ts";
28
28
  import { TYPING_BADGE } from "./badges.ts";
29
+ import { isTerminalReply } from "./terminalReply.ts";
29
30
  import { ensureNodeRuntime, liveEnv } from "./nodeRuntime.ts";
30
31
  import { type MailParty, recordInbox } from "./messageLog.ts";
31
32
  import { updateGlobalPidStatus } from "./globalPidIndex.ts";
@@ -1565,11 +1566,9 @@ export async function cmdServe(rest: string[]): Promise<number> {
1565
1566
  // resolveLastStdinAt below and the /api/send handler that stamps these.
1566
1567
  const meaningfulStdinAt = new Map<number, number>();
1567
1568
  const anyDaemonWriteAt = new Map<number, number>();
1568
- // A payload that is PURELY a terminal auto-reply (Cursor Position Report, Device
1569
- // Attributes, Device Status Report) protocol chatter a TUI emits on redraw/resize,
1570
- // not a keystroke. Anchored so a chunk that also carries real input never matches;
1571
- // real typing (incl. arrow keys like `ESC[A`) never looks like one of these.
1572
- const isTerminalReply = (s: string) => /^\x1b\[(\d+;\d+R|\?[\d;]*c|>[\d;]*c|\d*n)$/.test(s);
1569
+ // isTerminalReply (ts/terminalReply.ts) spots payloads that are purely such
1570
+ // auto-replies incl. the DECXCPR `ESC[?r;cR` form and bursts of several
1571
+ // replies concatenated in one chunk so they never count as meaningful.
1573
1572
  // Stamp both maps after a daemon FIFO write, keyed off the FIFO's post-write mtime
1574
1573
  // (so `anyDaemonWriteAt` is exactly our write's mtime — an external write bumps it
1575
1574
  // strictly higher, which is how resolveLastStdinAt tells them apart with no clock skew).
package/ts/subcommands.ts CHANGED
@@ -211,7 +211,7 @@ export interface MessageEdge {
211
211
  by: number;
212
212
  target: number;
213
213
  at: number;
214
- kind?: "key" | "select";
214
+ kind?: "key" | "select" | "auto-retry";
215
215
  }
216
216
 
217
217
  /**
@@ -3279,10 +3279,14 @@ async function cmdMsgs(rest: string[]): Promise<number> {
3279
3279
 
3280
3280
  for (const { dir, rec } of shown) {
3281
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"}`;
3282
+ // auto-retry nudges are written by the agent's own wrapper (from is null
3283
+ // there too) — label them as agent-yes, not a human send.
3284
+ const fromLabel = rec.from
3285
+ ? `${rec.from.cli} #${rec.from.pid}`
3286
+ : rec.kind === "auto-retry"
3287
+ ? "agent-yes"
3288
+ : "human";
3289
+ const peer = dir === "out" ? `→ ${rec.to.cli} #${rec.to.pid}` : `← ${fromLabel}`;
3286
3290
  const via = rec.remote ? ` (via ${rec.remote})` : "";
3287
3291
  const flag = rec.confirmed === false ? " (unconfirmed)" : "";
3288
3292
  const tag = rec.kind ? `[${rec.kind}] ` : "";
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isTerminalReply } from "./terminalReply.ts";
3
+
4
+ describe("isTerminalReply", () => {
5
+ it("matches single auto-replies (CPR, DECXCPR, DA1, DA2, DSR)", () => {
6
+ expect(isTerminalReply("\x1b[5;10R")).toBe(true); // CPR (ESC[6n answer)
7
+ expect(isTerminalReply("\x1b[?1;1R")).toBe(true); // DECXCPR (ESC[?6n answer)
8
+ expect(isTerminalReply("\x1b[?1;1;1R")).toBe(true); // DECXCPR with page
9
+ expect(isTerminalReply("\x1b[?1;2c")).toBe(true); // DA1
10
+ expect(isTerminalReply("\x1b[>0;276;0c")).toBe(true); // DA2
11
+ expect(isTerminalReply("\x1b[0n")).toBe(true); // DSR status OK
12
+ });
13
+
14
+ it("matches a burst of replies concatenated in one chunk", () => {
15
+ // A viewer attach replays a tail full of queries; xterm answers them all
16
+ // in one onData chunk.
17
+ expect(isTerminalReply("\x1b[?1;1R\x1b[?1;1R\x1b[?1;1R")).toBe(true);
18
+ expect(isTerminalReply("\x1b[1;1R\x1b[?1;2c\x1b[0n")).toBe(true);
19
+ });
20
+
21
+ it("never matches real typing", () => {
22
+ expect(isTerminalReply("hello")).toBe(false);
23
+ expect(isTerminalReply("\r")).toBe(false);
24
+ expect(isTerminalReply("\x1b[A")).toBe(false); // arrow key
25
+ expect(isTerminalReply("\x1b[1;5C")).toBe(false); // ctrl+arrow (no R/c/n final)
26
+ expect(isTerminalReply("\x1b")).toBe(false);
27
+ expect(isTerminalReply("")).toBe(false);
28
+ expect(isTerminalReply("\x1b[R")).toBe(false); // malformed — not a reply xterm emits
29
+ });
30
+
31
+ it("never matches a chunk that mixes a reply with real input", () => {
32
+ expect(isTerminalReply("\x1b[?1;1Ry")).toBe(false);
33
+ expect(isTerminalReply("y\x1b[?1;1R")).toBe(false);
34
+ expect(isTerminalReply("\x1b[1;1R\r")).toBe(false);
35
+ });
36
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Is this /api/send payload PURELY terminal auto-reply chatter — the responses
3
+ * a viewer's xterm generates to the agent TUI's protocol queries (Cursor
4
+ * Position Report incl. the DECXCPR `?`-prefixed form, Device Attributes,
5
+ * Device Status Report) — rather than a keystroke?
6
+ *
7
+ * Used by the serve daemon to keep such writes out of `last_stdin_at`, so a
8
+ * redraw/resize (or a TUI polling `ESC[?6n` every render) can't pin the
9
+ * console's stdin-flash + stdin age at "just typed".
10
+ *
11
+ * Anchored over ONE-OR-MORE replies: a burst of queries (e.g. tail replay on
12
+ * viewer attach) is answered in a single onData chunk, so several replies
13
+ * arrive concatenated in one payload. Real typing — including arrow keys like
14
+ * `ESC[A` — never matches any alternative.
15
+ */
16
+ export const isTerminalReply = (s: string): boolean =>
17
+ /^(?:\x1b\[(?:\??\d+(?:;\d+)*R|\?[\d;]*c|>[\d;]*c|\d*n))+$/.test(s);
@@ -1,8 +0,0 @@
1
- import "./ts-ml_9eD1Y.js";
2
- import "./logger-CDIsZ-Pp.js";
3
- import "./versionChecker-ufJXjxRK.js";
4
- import "./pidStore-BIvsBQ8X.js";
5
- import "./globalPidIndex-CoNr7tS8.js";
6
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BuiThZSF.js";
7
-
8
- export { SUPPORTED_CLIS };