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.
- package/default.config.yaml +51 -12
- package/dist/{SUPPORTED_CLIS-BuiThZSF.js → SUPPORTED_CLIS-BY83eM58.js} +2 -2
- package/dist/SUPPORTED_CLIS-D4Z--hGm.js +9 -0
- package/dist/{agent-yes.config-ugPYTPOd.js → agent-yes.config-whqkqq9P.js} +2 -2
- package/dist/{agentShare-BMZVmm7c.js → agentShare-DWioPObE.js} +8 -7
- package/dist/cli.js +8 -8
- package/dist/{configShared-0MnIQ652.js → configShared-aKTg-sa5.js} +1 -1
- package/dist/{e2e-BeKjLhmO.js → e2e-jb0Hp43q.js} +1 -1
- package/dist/{expose-B3AGhVhM.js → expose-k9l6ZJDA.js} +1 -1
- package/dist/{forkNested-C1kKsXi5.js → forkNested-DOyDAHnC.js} +1 -1
- package/dist/index.js +3 -2
- package/dist/messageLog-CxrKJj77.js +94 -0
- package/dist/{nodeRuntime-DZRjxpNp.js → nodeRuntime-CwNJuwH5.js} +1 -1
- package/dist/{notifyDaemon-Cikn_qKr.js → notifyDaemon-CRf_27kE.js} +7 -6
- package/dist/{openBrowser-DQVkxCyj.js → openBrowser-CSj21pZE.js} +1 -1
- package/dist/{reaper-CWF2_ATd.js → reaper-BUHCyxdF.js} +1 -1
- package/dist/{reaper-jfSmog15.js → reaper-CJSu8UVZ.js} +1 -1
- package/dist/{remotes-CW-yxSgW.js → remotes-CXqYR0p8.js} +3 -3
- package/dist/{remotes-oSmwSYaV.js → remotes-Cim0dBU7.js} +3 -3
- package/dist/{rustBinary-Uxkcz3zV.js → rustBinary-5HCt7_0b.js} +2 -2
- package/dist/{schedule-cvu-t8uo.js → schedule-CjBcOy-b.js} +7 -6
- package/dist/{serve-Cp9XGHAg.js → serve-C3E8uicL.js} +45 -26
- package/dist/{setup-onV23tOF.js → setup-C4SKqGy1.js} +3 -3
- package/dist/{share-29YbzpbU.js → share-BfIU8t_h.js} +2 -2
- package/dist/{share-nvqy64A2.js → share-Ciw1mWVN.js} +2 -2
- package/dist/{spawnGate-CWbdnJ5K.js → spawnGate-BkjeS4tg.js} +1 -1
- package/dist/{spawnGate-DRWa_Fzj.js → spawnGate-CXjEz6Pf.js} +2 -2
- package/dist/{subcommands-Dwzo0rx_.js → subcommands-B7UxIJRw.js} +17 -103
- package/dist/{subcommands-BReg256R.js → subcommands-DzWBZ6HE.js} +6 -5
- package/dist/{tray-BKTZ5gIf.js → tray-BSFi35Qg.js} +1 -1
- package/dist/{ts-ml_9eD1Y.js → ts-B64pNQlI.js} +84 -7
- package/dist/{versionChecker-ufJXjxRK.js → versionChecker-DKiSuTSp.js} +2 -2
- package/dist/{webrtcLink-BG0Xc4-W.js → webrtcLink-B7REGtK2.js} +2 -2
- package/dist/{webrtcRemote-BmuVKcrx.js → webrtcRemote-CvIEfWX8.js} +3 -3
- package/dist/{workspaceConfig-_GtAZtsi.js → workspaceConfig-CjaRvTjf.js} +1 -1
- package/package.json +1 -1
- package/ts/autoRetry.spec.ts +90 -1
- package/ts/autoRetry.ts +76 -0
- package/ts/index.ts +37 -4
- package/ts/messageLog.ts +3 -2
- package/ts/serve.ts +4 -5
- package/ts/subcommands.ts +9 -5
- package/ts/terminalReply.spec.ts +36 -0
- package/ts/terminalReply.ts +17 -0
- 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
|
-
|
|
826
|
-
|
|
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("
|
|
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 +
|
|
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`)
|
|
43
|
-
|
|
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
|
-
//
|
|
1569
|
-
//
|
|
1570
|
-
//
|
|
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
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
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 };
|