@aragon-agent/cli 0.6.31 → 0.6.32
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/CHANGELOG.md +100 -0
- package/README.md +2957 -2826
- package/dist/agent/controller.d.ts +46 -0
- package/dist/agent/controller.js +175 -7
- package/dist/agent/job-notices.d.ts +143 -0
- package/dist/agent/job-notices.js +182 -0
- package/dist/agent/reducer.d.ts +21 -1
- package/dist/agent/reducer.js +7 -0
- package/dist/agent/system-prompt.d.ts +12 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +232 -8
- package/dist/commands/builtins.js +23 -8
- package/dist/commands/perf.d.ts +54 -1
- package/dist/commands/perf.js +28 -4
- package/dist/config/cli-commands.d.ts +27 -0
- package/dist/config/cli-commands.js +54 -1
- package/dist/config/env.js +38 -0
- package/dist/config/load.js +50 -1
- package/dist/config/schema.d.ts +73 -0
- package/dist/config/schema.js +34 -0
- package/dist/config/store.js +14 -1
- package/dist/exec/events.d.ts +29 -3
- package/dist/exec/events.js +3 -12
- package/dist/exec/runner.d.ts +24 -0
- package/dist/exec/runner.js +79 -0
- package/dist/input/cursor-report.d.ts +61 -0
- package/dist/input/cursor-report.js +97 -0
- package/dist/input/stdin-filter.d.ts +22 -0
- package/dist/input/stdin-filter.js +48 -3
- package/dist/proc/limits.d.ts +76 -2
- package/dist/proc/limits.js +83 -2
- package/dist/proc/log-mirror.d.ts +59 -0
- package/dist/proc/log-mirror.js +203 -0
- package/dist/proc/log-ring.d.ts +22 -0
- package/dist/proc/log-ring.js +20 -1
- package/dist/proc/notices.d.ts +57 -0
- package/dist/proc/notices.js +101 -0
- package/dist/proc/progress.d.ts +40 -0
- package/dist/proc/progress.js +135 -0
- package/dist/proc/prompt.d.ts +24 -2
- package/dist/proc/prompt.js +51 -2
- package/dist/proc/supervisor.d.ts +35 -2
- package/dist/proc/supervisor.js +142 -43
- package/dist/proc/types.d.ts +89 -1
- package/dist/proc/wait.d.ts +57 -0
- package/dist/proc/wait.js +133 -0
- package/dist/session/persist.js +3 -1
- package/dist/session/validate-session.js +17 -0
- package/dist/tools/bash-tool.d.ts +12 -1
- package/dist/tools/bash-tool.js +100 -13
- package/dist/tools/index.d.ts +26 -2
- package/dist/tools/index.js +43 -2
- package/dist/tools/job-tools.d.ts +54 -0
- package/dist/tools/job-tools.js +329 -0
- package/dist/tools/proc-tools.d.ts +1 -1
- package/dist/tools/proc-tools.js +49 -17
- package/dist/ui/ActivityLine.js +7 -0
- package/dist/ui/App.js +64 -6
- package/dist/ui/ScrollbackShell.d.ts +30 -4
- package/dist/ui/ScrollbackShell.js +8 -3
- package/dist/ui/StatusBar.d.ts +5 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/Transcript.d.ts +14 -0
- package/dist/ui/Transcript.js +14 -2
- package/dist/ui/entries/ServiceCard.d.ts +17 -1
- package/dist/ui/entries/ServiceCard.js +39 -4
- package/dist/ui/layout/status-layout.d.ts +13 -1
- package/dist/ui/layout/status-layout.js +10 -0
- package/dist/ui/layout/virtual-window.js +14 -1
- package/dist/ui/run-status-row.d.ts +12 -0
- package/dist/ui/screen.d.ts +17 -4
- package/dist/ui/screen.js +14 -6
- package/dist/ui/scrollback/LiveTail.d.ts +64 -0
- package/dist/ui/scrollback/LiveTail.js +27 -0
- package/dist/ui/scrollback/band.d.ts +105 -0
- package/dist/ui/scrollback/band.js +109 -0
- package/dist/ui/scrollback/budget.d.ts +32 -3
- package/dist/ui/scrollback/budget.js +43 -6
- package/dist/ui/scrollback/canvas.d.ts +99 -0
- package/dist/ui/scrollback/canvas.js +113 -0
- package/dist/ui/scrollback/cursor-probe.d.ts +71 -0
- package/dist/ui/scrollback/cursor-probe.js +133 -0
- package/dist/ui/scrollback/limits.d.ts +49 -0
- package/dist/ui/scrollback/limits.js +49 -0
- package/dist/ui/scrollback/scrollback-writer.d.ts +104 -16
- package/dist/ui/scrollback/scrollback-writer.js +368 -82
- package/dist/ui/scrollback/use-scrollback-view.d.ts +11 -0
- package/dist/ui/scrollback/use-scrollback-view.js +37 -6
- package/dist/ui/scrollback/writer-shapes.d.ts +68 -2
- package/dist/ui/scrollback/writer-shapes.js +53 -8
- package/package.json +3 -3
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor Position Report parsing (tui-bottom-anchored-live-region §4.5).
|
|
3
|
+
*
|
|
4
|
+
* The bottom anchor needs one number the writer cannot compute: how many rows are
|
|
5
|
+
* left below the cursor. `ESC[6n` is the only way to ask, and the terminal answers
|
|
6
|
+
* with `ESC[<row>;<col>R` (DECXCPR adds a `?`: `ESC[?<row>;<col>R`) on STDIN —
|
|
7
|
+
* the same stream the user types on. Two things follow, and both live here:
|
|
8
|
+
*
|
|
9
|
+
* 1. THE ANSWER MUST BE STRIPPED BEFORE INK SEES IT. A report that reaches Ink
|
|
10
|
+
* travels the keystroke path: `[` is 0x5B, so `PromptInput`'s control-sequence
|
|
11
|
+
* guard passes it through and the user's draft gets `[12;1R` typed into it (the
|
|
12
|
+
* same class of accident the mouse filter exists to prevent).
|
|
13
|
+
* 2. THE ANSWER CAN ARRIVE IN TWO CHUNKS. ConPTY, tmux and a loaded SSH session
|
|
14
|
+
* all split reads at arbitrary points, and a half report (`ESC[12`) is worse
|
|
15
|
+
* than a whole one: `parse-keypress` has no branch for it.
|
|
16
|
+
*
|
|
17
|
+
* Pure functions only, mirroring `mouse-events.ts`: the filter owns the buffering
|
|
18
|
+
* and the timers, this module owns the shapes.
|
|
19
|
+
*/
|
|
20
|
+
/** The query for a Cursor Position Report (DSR 6). ASCII by construction. */
|
|
21
|
+
export declare const CURSOR_REPORT_QUERY = "\u001B[6n";
|
|
22
|
+
/** One parsed report. `row`/`col` are 1-based, exactly as the terminal sent them. */
|
|
23
|
+
export interface CursorReport {
|
|
24
|
+
row: number;
|
|
25
|
+
col: number;
|
|
26
|
+
}
|
|
27
|
+
export interface CursorReportSplit {
|
|
28
|
+
/** Text that survives stripping — everything that is not a report. */
|
|
29
|
+
text: string;
|
|
30
|
+
/** Reports found in this chunk, in order. */
|
|
31
|
+
reports: CursorReport[];
|
|
32
|
+
/** Trailing bytes that could still become a report; hand them to the next chunk. */
|
|
33
|
+
pending: string;
|
|
34
|
+
}
|
|
35
|
+
/** True when `s` is a non-empty proper prefix of a possible report. */
|
|
36
|
+
export declare function isCursorReportPrefix(s: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Split the reports out of one chunk.
|
|
39
|
+
*
|
|
40
|
+
* `pending` carries the tail that might still complete into a report. It is only
|
|
41
|
+
* ever the LAST fragment: the scan is left to right, so anything before a complete
|
|
42
|
+
* report is settled text.
|
|
43
|
+
*/
|
|
44
|
+
export declare function splitCursorReports(text: string): CursorReportSplit;
|
|
45
|
+
/**
|
|
46
|
+
* Length of the trailing run of `text` that is a proper prefix of a report, or 0.
|
|
47
|
+
*
|
|
48
|
+
* LONGEST MATCH WINS, unlike the mouse family's shortest-wins rule. The families are
|
|
49
|
+
* combined with `max()`, so only the longest answer ever survives, and a longer hold
|
|
50
|
+
* is never wrong here: `ESC[1` can still become `ESC[12;1R`, while the shorter
|
|
51
|
+
* answer `ESC` could not describe it. The bound is the longest report the terminals
|
|
52
|
+
* we target emit (DECXCPR with a `?` and three-digit coordinates).
|
|
53
|
+
*/
|
|
54
|
+
export declare function trailingCprPrefixLength(text: string): number;
|
|
55
|
+
/** The query bytes, as a function so call sites read as an action rather than a literal. */
|
|
56
|
+
export declare function formatCursorReportQuery(): string;
|
|
57
|
+
/**
|
|
58
|
+
* Rows left below a reported cursor row on a terminal of `rows` rows.
|
|
59
|
+
* `0` means the cursor is already on the last row; `null` when the height is unknown.
|
|
60
|
+
*/
|
|
61
|
+
export declare function spaceBelowFromReport(report: CursorReport, rows: number | undefined): number | null;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor Position Report parsing (tui-bottom-anchored-live-region §4.5).
|
|
3
|
+
*
|
|
4
|
+
* The bottom anchor needs one number the writer cannot compute: how many rows are
|
|
5
|
+
* left below the cursor. `ESC[6n` is the only way to ask, and the terminal answers
|
|
6
|
+
* with `ESC[<row>;<col>R` (DECXCPR adds a `?`: `ESC[?<row>;<col>R`) on STDIN —
|
|
7
|
+
* the same stream the user types on. Two things follow, and both live here:
|
|
8
|
+
*
|
|
9
|
+
* 1. THE ANSWER MUST BE STRIPPED BEFORE INK SEES IT. A report that reaches Ink
|
|
10
|
+
* travels the keystroke path: `[` is 0x5B, so `PromptInput`'s control-sequence
|
|
11
|
+
* guard passes it through and the user's draft gets `[12;1R` typed into it (the
|
|
12
|
+
* same class of accident the mouse filter exists to prevent).
|
|
13
|
+
* 2. THE ANSWER CAN ARRIVE IN TWO CHUNKS. ConPTY, tmux and a loaded SSH session
|
|
14
|
+
* all split reads at arbitrary points, and a half report (`ESC[12`) is worse
|
|
15
|
+
* than a whole one: `parse-keypress` has no branch for it.
|
|
16
|
+
*
|
|
17
|
+
* Pure functions only, mirroring `mouse-events.ts`: the filter owns the buffering
|
|
18
|
+
* and the timers, this module owns the shapes.
|
|
19
|
+
*/
|
|
20
|
+
/** The query for a Cursor Position Report (DSR 6). ASCII by construction. */
|
|
21
|
+
export const CURSOR_REPORT_QUERY = '\x1b[6n';
|
|
22
|
+
/**
|
|
23
|
+
* `ESC[<row>;<col>R`, with the DECXCPR `?` accepted. Anchored on `ESC[` plus digits
|
|
24
|
+
* and one `;`, which is what keeps it from matching a private-mode write such as
|
|
25
|
+
* `ESC[?25l` (that one ends in a letter this pattern cannot accept).
|
|
26
|
+
*
|
|
27
|
+
* Built per call rather than shared: a module-level `/g` regex carries `lastIndex`
|
|
28
|
+
* between calls, which turns a pure function into a stateful one.
|
|
29
|
+
*/
|
|
30
|
+
const reportPattern = () => /\x1b\[\??(\d+);(\d+)R/g;
|
|
31
|
+
/**
|
|
32
|
+
* A PROPER prefix of a report: `ESC`, `ESC[`, then an optional `?`, then digits, at
|
|
33
|
+
* most one `;`, then more digits. Nothing a keyboard produces matches past `ESC[`
|
|
34
|
+
* (arrow keys end in a letter), so the filter's 12 ms hold stays a rare path.
|
|
35
|
+
*/
|
|
36
|
+
const PREFIX = /^\x1b(\[(\?(\d*(;\d*)?)?|\d*(;\d*)?)?)?$/;
|
|
37
|
+
/** True when `s` is a non-empty proper prefix of a possible report. */
|
|
38
|
+
export function isCursorReportPrefix(s) {
|
|
39
|
+
return s.length > 0 && s.length < 24 && PREFIX.test(s);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Split the reports out of one chunk.
|
|
43
|
+
*
|
|
44
|
+
* `pending` carries the tail that might still complete into a report. It is only
|
|
45
|
+
* ever the LAST fragment: the scan is left to right, so anything before a complete
|
|
46
|
+
* report is settled text.
|
|
47
|
+
*/
|
|
48
|
+
export function splitCursorReports(text) {
|
|
49
|
+
const reports = [];
|
|
50
|
+
const pattern = reportPattern();
|
|
51
|
+
let out = '';
|
|
52
|
+
let index = 0;
|
|
53
|
+
for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
|
|
54
|
+
out += text.slice(index, match.index);
|
|
55
|
+
reports.push({ row: Number(match[1]), col: Number(match[2]) });
|
|
56
|
+
index = match.index + match[0].length;
|
|
57
|
+
}
|
|
58
|
+
out += text.slice(index);
|
|
59
|
+
const pendingLength = trailingCprPrefixLength(out);
|
|
60
|
+
if (pendingLength === 0)
|
|
61
|
+
return { text: out, reports, pending: '' };
|
|
62
|
+
return {
|
|
63
|
+
text: out.slice(0, out.length - pendingLength),
|
|
64
|
+
reports,
|
|
65
|
+
pending: out.slice(out.length - pendingLength),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Length of the trailing run of `text` that is a proper prefix of a report, or 0.
|
|
70
|
+
*
|
|
71
|
+
* LONGEST MATCH WINS, unlike the mouse family's shortest-wins rule. The families are
|
|
72
|
+
* combined with `max()`, so only the longest answer ever survives, and a longer hold
|
|
73
|
+
* is never wrong here: `ESC[1` can still become `ESC[12;1R`, while the shorter
|
|
74
|
+
* answer `ESC` could not describe it. The bound is the longest report the terminals
|
|
75
|
+
* we target emit (DECXCPR with a `?` and three-digit coordinates).
|
|
76
|
+
*/
|
|
77
|
+
export function trailingCprPrefixLength(text) {
|
|
78
|
+
const max = Math.min(text.length, 24);
|
|
79
|
+
for (let k = max; k >= 1; k -= 1) {
|
|
80
|
+
if (isCursorReportPrefix(text.slice(text.length - k)))
|
|
81
|
+
return k;
|
|
82
|
+
}
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
/** The query bytes, as a function so call sites read as an action rather than a literal. */
|
|
86
|
+
export function formatCursorReportQuery() {
|
|
87
|
+
return CURSOR_REPORT_QUERY;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Rows left below a reported cursor row on a terminal of `rows` rows.
|
|
91
|
+
* `0` means the cursor is already on the last row; `null` when the height is unknown.
|
|
92
|
+
*/
|
|
93
|
+
export function spaceBelowFromReport(report, rows) {
|
|
94
|
+
if (rows === undefined || !Number.isFinite(rows) || rows < 1)
|
|
95
|
+
return null;
|
|
96
|
+
return Math.max(0, Math.floor(rows) - 1 - (report.row - 1));
|
|
97
|
+
}
|
|
@@ -69,11 +69,33 @@ export interface StdinFilterFeatures {
|
|
|
69
69
|
* translates for a session whose mode push actually happened.
|
|
70
70
|
*/
|
|
71
71
|
readonly enhancedKeys?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Strip the Cursor Position Report a terminal sends back for `ESC[6n`
|
|
74
|
+
* (tui-bottom-anchored-live-region §4.5).
|
|
75
|
+
*
|
|
76
|
+
* TOLD, never derived, like the three above: `cli.tsx` passes
|
|
77
|
+
* `plan.differ === 'scrollback'`, so an `app`-mode session stays byte-identical to
|
|
78
|
+
* the build without this feature (I-A8). The report MUST NOT reach Ink -- `[` is
|
|
79
|
+
* 0x5B, so `PromptInput`'s control-sequence guard passes it and the reply gets
|
|
80
|
+
* typed into the draft -- and a report split across two reads is worse than a
|
|
81
|
+
* whole one, because `parse-keypress` has no branch for a half sequence.
|
|
82
|
+
*/
|
|
83
|
+
readonly cursorReport?: boolean;
|
|
72
84
|
}
|
|
73
85
|
export interface StdinFilter {
|
|
74
86
|
/** Hand this to Ink: `render(<App/>, { stdin: filter.stdin })`. */
|
|
75
87
|
readonly stdin: NodeJS.ReadStream;
|
|
76
88
|
readonly source: MouseSource;
|
|
89
|
+
/**
|
|
90
|
+
* The probe is waiting for an answer: hold a possible report prefix until it
|
|
91
|
+
* settles instead of flushing it after `PENDING_FLUSH_MS`.
|
|
92
|
+
*
|
|
93
|
+
* ConPTY can split `ESC[12;1R` across two reads further apart than 12 ms, and a
|
|
94
|
+
* half report that reaches Ink lands on an undefined branch of its key parser.
|
|
95
|
+
* Turning this off flushes whatever is still held as ordinary text -- the
|
|
96
|
+
* correct degradation for a terminal that answered with something else (R3).
|
|
97
|
+
*/
|
|
98
|
+
setExpectingCursorReport(on: boolean): void;
|
|
77
99
|
/** Idempotent. Detaches from the real stdin and clears every timer (I-7). */
|
|
78
100
|
dispose(): void;
|
|
79
101
|
}
|
|
@@ -36,6 +36,7 @@ import { MAX_PENDING_MOUSE_CHARS, splitMouseEvents, isMousePrefix, } from './mou
|
|
|
36
36
|
import { ENTER_NEWLINE_FRAME, INDEX_KEY_FRAME, PASTE_ASSEMBLY_MAX_MS, PASTE_BURST_MS, PASTE_CLOSE, PASTE_MARKER_RESCAN_TTL_MS, PASTE_MAX_BYTES, PASTE_OPEN, formatPasteSize, } from './limits.js';
|
|
37
37
|
import { PASTE_BEGIN_MARK, PASTE_END_MARK, classifyChunk, isPastePrefix, sanitisePaste, trailingPastePrefixLength, } from './paste-parse.js';
|
|
38
38
|
import { hasPasteOnlySequence } from './control-sequences.js';
|
|
39
|
+
import { splitCursorReports, trailingCprPrefixLength } from './cursor-report.js';
|
|
39
40
|
import { splitEnterSequences, trailingEnterPrefixLength } from './enter-sequences.js';
|
|
40
41
|
import { splitCsiUKeys, trailingCsiUPrefixLength, translateCsiUKey } from './csiu-keys.js';
|
|
41
42
|
import { splitWin32KeySequences, trailingWin32PrefixLength, translateWin32Key, } from './win32-input-mode.js';
|
|
@@ -95,6 +96,12 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
95
96
|
let disposed = false;
|
|
96
97
|
let finished = false;
|
|
97
98
|
let paste = { kind: 'idle' };
|
|
99
|
+
/**
|
|
100
|
+
* The probe window (R3). While true a possible report prefix is held until the
|
|
101
|
+
* probe settles instead of being flushed after `PENDING_FLUSH_MS`, because a
|
|
102
|
+
* half `ESC[12` reaching Ink hits an undefined branch of its key parser.
|
|
103
|
+
*/
|
|
104
|
+
let expectingCursorReport = false;
|
|
98
105
|
let discardBracketed = false;
|
|
99
106
|
let outputBatch = null;
|
|
100
107
|
/**
|
|
@@ -266,6 +273,26 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
266
273
|
burstTimer = arm(flushBurst, PASTE_BURST_MS);
|
|
267
274
|
return;
|
|
268
275
|
}
|
|
276
|
+
// FOURTH PREFIX FAMILY: the terminal's answer to `ESC[6n`
|
|
277
|
+
// (tui-bottom-anchored-live-region §4.5). AFTER the paste tests, and that order
|
|
278
|
+
// is the specification: a user who pastes a terminal log containing `ESC[12;1R`
|
|
279
|
+
// is pasting DATA, and a paste body is the one place such a sequence is kept
|
|
280
|
+
// verbatim (I-3, the same rule that suspends mouse parsing inside a body).
|
|
281
|
+
if (features.cursorReport) {
|
|
282
|
+
const report = splitCursorReports(text);
|
|
283
|
+
text = report.text;
|
|
284
|
+
if (report.pending.length > 0) {
|
|
285
|
+
// Not text yet: it joins the cross-chunk buffer the mouse family uses.
|
|
286
|
+
// `holdTail` false means a paste marker follows in this very chunk, so no
|
|
287
|
+
// later byte can complete the prefix and holding it would reorder it (I-5).
|
|
288
|
+
if (holdTail)
|
|
289
|
+
pending += report.pending;
|
|
290
|
+
else
|
|
291
|
+
writeInput(report.pending);
|
|
292
|
+
}
|
|
293
|
+
if (text.length === 0)
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
269
296
|
// Ink conflates DEL Backspace with CSI Delete. Normalize only keyboard
|
|
270
297
|
// text, after burst classification, preserving ESC for Alt+Backspace.
|
|
271
298
|
text = text.replace(/\x7f/g, '\x08');
|
|
@@ -414,7 +441,7 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
414
441
|
// the ESC / ESC-[ head bytes, which the max keeps for whoever needs
|
|
415
442
|
// them. The existing 12 ms flush and the 32-char ceiling bound the
|
|
416
443
|
// hold, so no new timer is needed.
|
|
417
|
-
const keep = Math.max(trailingPastePrefixLength(rest), features.mouse ? trailingMousePrefixLength(rest) : 0, trailingEnterPrefixLength(rest), enhancedKeys ? trailingWin32PrefixLength(rest) : 0, enhancedKeys ? trailingCsiUPrefixLength(rest) : 0);
|
|
444
|
+
const keep = Math.max(trailingPastePrefixLength(rest), features.mouse ? trailingMousePrefixLength(rest) : 0, features.cursorReport ? trailingCprPrefixLength(rest) : 0, trailingEnterPrefixLength(rest), enhancedKeys ? trailingWin32PrefixLength(rest) : 0, enhancedKeys ? trailingCsiUPrefixLength(rest) : 0);
|
|
418
445
|
const tail = rest.slice(rest.length - keep);
|
|
419
446
|
handleOutsidePaste(rest.slice(0, rest.length - keep), true);
|
|
420
447
|
pending += tail;
|
|
@@ -450,6 +477,10 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
450
477
|
flushTimer = clearTimer(flushTimer);
|
|
451
478
|
if (pending.length === 0 || disposed)
|
|
452
479
|
return;
|
|
480
|
+
// The probe owes us an answer: releasing a half report now would type it into
|
|
481
|
+
// the draft. `setExpectingCursorReport(false)` flushes whatever is left.
|
|
482
|
+
if (expectingCursorReport)
|
|
483
|
+
return;
|
|
453
484
|
const text = pending;
|
|
454
485
|
pending = '';
|
|
455
486
|
// A tail that never completed is ordinary text; it goes through the same
|
|
@@ -526,9 +557,12 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
526
557
|
wrapper.write(text);
|
|
527
558
|
if (paste.kind === 'bracketed' || pending.length === 0)
|
|
528
559
|
return;
|
|
529
|
-
if (pending.length > MAX_PENDING_CHARS) {
|
|
560
|
+
if (pending.length > MAX_PENDING_CHARS && !expectingCursorReport) {
|
|
530
561
|
// A malformed sequence that looks like a prefix forever must never wedge
|
|
531
|
-
// input: past the ceiling the tail becomes ordinary text.
|
|
562
|
+
// input: past the ceiling the tail becomes ordinary text. NOT DURING THE
|
|
563
|
+
// PROBE WINDOW THOUGH (L8): there the ceiling could push half a report into
|
|
564
|
+
// Ink, which is the failure this whole family exists to prevent. The window
|
|
565
|
+
// is bounded by the probe timeout, after which the ceiling applies again.
|
|
532
566
|
const text = pending;
|
|
533
567
|
pending = '';
|
|
534
568
|
flushTailAsText(text);
|
|
@@ -574,6 +608,17 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
574
608
|
return () => listeners.delete(listener);
|
|
575
609
|
},
|
|
576
610
|
},
|
|
611
|
+
setExpectingCursorReport(on) {
|
|
612
|
+
if (expectingCursorReport === on)
|
|
613
|
+
return;
|
|
614
|
+
expectingCursorReport = on;
|
|
615
|
+
if (on || disposed)
|
|
616
|
+
return;
|
|
617
|
+
// Settled: nothing that is still held was a report. Release it as text, so a
|
|
618
|
+
// probe that never got an answer costs its own timeout and not a dead keyboard.
|
|
619
|
+
flushTimer = clearTimer(flushTimer);
|
|
620
|
+
flushPending();
|
|
621
|
+
},
|
|
577
622
|
dispose() {
|
|
578
623
|
if (disposed)
|
|
579
624
|
return;
|
package/dist/proc/limits.d.ts
CHANGED
|
@@ -34,14 +34,19 @@ export declare const PROC_LIMITS: {
|
|
|
34
34
|
/** Rows a snapshot (and therefore a card) carries. */
|
|
35
35
|
readonly cardTailRows: 8;
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
37
|
+
* JOB RECORDS one session may hold — services AND tasks share the number.
|
|
38
|
+
*
|
|
39
|
+
* NAMED `maxJobs`, NOT `maxServices` (long-task-jobs D-7). A constant that
|
|
40
|
+
* counts two kinds of job while being called `maxServices` is the next edit's
|
|
41
|
+
* bug: the limit and its refusal message would go on saying "service" while
|
|
42
|
+
* the model is holding a training run.
|
|
38
43
|
*
|
|
39
44
|
* Eviction only ever removes a record in a TERMINAL state
|
|
40
45
|
* (`exited | failed | stopped`). When all 16 are live `start()` refuses and
|
|
41
46
|
* names the oldest ids rather than evicting: evicting a live record would
|
|
42
47
|
* orphan a running server that nothing can any longer name or stop.
|
|
43
48
|
*/
|
|
44
|
-
readonly
|
|
49
|
+
readonly maxJobs: 16;
|
|
45
50
|
/**
|
|
46
51
|
* How long `bash` waits after `'exit'` for the stdio streams to close before
|
|
47
52
|
* settling anyway.
|
|
@@ -76,9 +81,78 @@ export declare const PROC_LIMITS: {
|
|
|
76
81
|
readonly stopGraceMs: 3000;
|
|
77
82
|
/** Below this many terminal columns the status chip degrades to `[N]`. */
|
|
78
83
|
readonly statusCompactCols: 92;
|
|
84
|
+
/**
|
|
85
|
+
* Rows of new output one `job_wait` result carries, in addition to its status,
|
|
86
|
+
* progress, cursor, `[note]`/`[stalled]` and `[next]` lines.
|
|
87
|
+
*
|
|
88
|
+
* THE NUMBER IS THE FEATURE. One wait must cost ~150 tokens, not the ~6k a
|
|
89
|
+
* 200-row tail costs - that economy is the whole reason `job_wait` exists
|
|
90
|
+
* instead of a seventh `bash_output` poll.
|
|
91
|
+
*/
|
|
92
|
+
readonly waitTailRows: 6;
|
|
93
|
+
/** Seconds a single wait may block, as a STRUCTURAL floor and ceiling (`jobs.waitSeconds` is clamped into this range). */
|
|
94
|
+
readonly waitMinSeconds: 5;
|
|
95
|
+
readonly waitMaxSeconds: 60;
|
|
96
|
+
/**
|
|
97
|
+
* Characters one extracted progress line may occupy.
|
|
98
|
+
*
|
|
99
|
+
* Also the width the card's progress row is assumed to take, which is why
|
|
100
|
+
* `ui/layout/virtual-window.ts` can count it as exactly one row.
|
|
101
|
+
*/
|
|
102
|
+
readonly waitProgressChars: 80;
|
|
103
|
+
/**
|
|
104
|
+
* Rows `extractProgress` scans, from the tail, before giving up.
|
|
105
|
+
*
|
|
106
|
+
* Eight because a tqdm bar plus its ETA plus the loss line above it is the
|
|
107
|
+
* realistic worst case, and because a wider window is how a stale `100%` from
|
|
108
|
+
* an earlier phase wins over the live one.
|
|
109
|
+
*/
|
|
110
|
+
readonly progressScanRows: 8;
|
|
111
|
+
/** Notices one `<job_notices>` block may render. Bounded so the block stays ~8 lines. */
|
|
112
|
+
readonly maxNotices: 8;
|
|
113
|
+
/** Log rows a failed job's notice carries. Two: the message and the frame above it. */
|
|
114
|
+
readonly noticeTailRows: 2;
|
|
115
|
+
/** Rows `job_list` prints before it collapses the rest into `+N more`. */
|
|
116
|
+
readonly listRows: 8;
|
|
117
|
+
/**
|
|
118
|
+
* Consecutive waits with no new row and no progress change before the
|
|
119
|
+
* `[guard]` line is prepended (R-3).
|
|
120
|
+
*
|
|
121
|
+
* A HINT, NEVER A REFUSAL: the seventh wait still waits. Refusing would take
|
|
122
|
+
* the decision away from the model at exactly the point where it has the most
|
|
123
|
+
* context.
|
|
124
|
+
*/
|
|
125
|
+
readonly maxConsecutiveWaits: 6;
|
|
126
|
+
/** Bytes one mirrored job log may occupy before it stops appending. */
|
|
127
|
+
readonly logMirrorMaxBytes: 2000000;
|
|
128
|
+
/** Coalescing window for the log mirror's appends (<=4 writes/second). */
|
|
129
|
+
readonly mirrorFlushMs: 250;
|
|
130
|
+
/** Session directories the mirror keeps, newest by mtime. */
|
|
131
|
+
readonly mirrorRetainDirs: 5;
|
|
79
132
|
};
|
|
80
133
|
/**
|
|
81
134
|
* Bumped whenever the wording of the `<background_services>` block changes, so a
|
|
82
135
|
* behaviour report can be tied to a block revision with one grep.
|
|
83
136
|
*/
|
|
84
137
|
export declare const BACKGROUND_SERVICES_BLOCK_VERSION = "v1";
|
|
138
|
+
/**
|
|
139
|
+
* The same discipline for the `<background_jobs>` block. A SEPARATE CONSTANT,
|
|
140
|
+
* not a bump of the one above: the two blocks are spliced by different flags
|
|
141
|
+
* (`bash.background` and `jobs.enabled`), so one version number covering both
|
|
142
|
+
* would make "which block changed" unanswerable from a report.
|
|
143
|
+
*/
|
|
144
|
+
export declare const JOBS_BLOCK_VERSION = "v1-2026-10";
|
|
145
|
+
/**
|
|
146
|
+
* Head-room a single wait leaves below the tool ceiling (I-4).
|
|
147
|
+
*
|
|
148
|
+
* `jobs.waitSeconds * 1000 + JOB_WAIT_MARGIN_MS <= toolTimeoutMs` is the whole
|
|
149
|
+
* reason a wait never needs to pause the idle watchdog (D-3): a 60 s wait inside
|
|
150
|
+
* a 180 s tool call in a 210 s idle window cannot be mistaken for a hang.
|
|
151
|
+
*
|
|
152
|
+
* IT LIVES HERE RATHER THAN IN `config/schema.ts`, where the design document put
|
|
153
|
+
* it, because the TOOL layer needs the same number for its call-time convergence
|
|
154
|
+
* (AC-14) and nothing in `tools/` may import `config/` - a tool takes its policy
|
|
155
|
+
* through closures. One constant, two readers, no second copy to drift. Recorded
|
|
156
|
+
* in `docs/plans/long-task-jobs/spec.md` under `## 实施过程发现的方案缺陷` (I-2).
|
|
157
|
+
*/
|
|
158
|
+
export declare const JOB_WAIT_MARGIN_MS = 30000;
|
package/dist/proc/limits.js
CHANGED
|
@@ -34,14 +34,19 @@ export const PROC_LIMITS = {
|
|
|
34
34
|
/** Rows a snapshot (and therefore a card) carries. */
|
|
35
35
|
cardTailRows: 8,
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
37
|
+
* JOB RECORDS one session may hold — services AND tasks share the number.
|
|
38
|
+
*
|
|
39
|
+
* NAMED `maxJobs`, NOT `maxServices` (long-task-jobs D-7). A constant that
|
|
40
|
+
* counts two kinds of job while being called `maxServices` is the next edit's
|
|
41
|
+
* bug: the limit and its refusal message would go on saying "service" while
|
|
42
|
+
* the model is holding a training run.
|
|
38
43
|
*
|
|
39
44
|
* Eviction only ever removes a record in a TERMINAL state
|
|
40
45
|
* (`exited | failed | stopped`). When all 16 are live `start()` refuses and
|
|
41
46
|
* names the oldest ids rather than evicting: evicting a live record would
|
|
42
47
|
* orphan a running server that nothing can any longer name or stop.
|
|
43
48
|
*/
|
|
44
|
-
|
|
49
|
+
maxJobs: 16,
|
|
45
50
|
/**
|
|
46
51
|
* How long `bash` waits after `'exit'` for the stdio streams to close before
|
|
47
52
|
* settling anyway.
|
|
@@ -76,9 +81,85 @@ export const PROC_LIMITS = {
|
|
|
76
81
|
stopGraceMs: 3000,
|
|
77
82
|
/** Below this many terminal columns the status chip degrades to `[N]`. */
|
|
78
83
|
statusCompactCols: 92,
|
|
84
|
+
// --- long-task-jobs: bounded waiting, progress, notices, log mirror --------
|
|
85
|
+
//
|
|
86
|
+
// EVERY ONE OF THESE IS STRUCTURAL, not policy. The user-tunable knobs are the
|
|
87
|
+
// five `jobs.*` keys in `config/schema.ts` (how many seconds to wait, when
|
|
88
|
+
// silence becomes `[stalled]`, whether to mirror the log, whether notices may
|
|
89
|
+
// steer). What a result can PHYSICALLY carry lives here, for the reason this
|
|
90
|
+
// file's header records at length.
|
|
91
|
+
/**
|
|
92
|
+
* Rows of new output one `job_wait` result carries, in addition to its status,
|
|
93
|
+
* progress, cursor, `[note]`/`[stalled]` and `[next]` lines.
|
|
94
|
+
*
|
|
95
|
+
* THE NUMBER IS THE FEATURE. One wait must cost ~150 tokens, not the ~6k a
|
|
96
|
+
* 200-row tail costs - that economy is the whole reason `job_wait` exists
|
|
97
|
+
* instead of a seventh `bash_output` poll.
|
|
98
|
+
*/
|
|
99
|
+
waitTailRows: 6,
|
|
100
|
+
/** Seconds a single wait may block, as a STRUCTURAL floor and ceiling (`jobs.waitSeconds` is clamped into this range). */
|
|
101
|
+
waitMinSeconds: 5,
|
|
102
|
+
waitMaxSeconds: 60,
|
|
103
|
+
/**
|
|
104
|
+
* Characters one extracted progress line may occupy.
|
|
105
|
+
*
|
|
106
|
+
* Also the width the card's progress row is assumed to take, which is why
|
|
107
|
+
* `ui/layout/virtual-window.ts` can count it as exactly one row.
|
|
108
|
+
*/
|
|
109
|
+
waitProgressChars: 80,
|
|
110
|
+
/**
|
|
111
|
+
* Rows `extractProgress` scans, from the tail, before giving up.
|
|
112
|
+
*
|
|
113
|
+
* Eight because a tqdm bar plus its ETA plus the loss line above it is the
|
|
114
|
+
* realistic worst case, and because a wider window is how a stale `100%` from
|
|
115
|
+
* an earlier phase wins over the live one.
|
|
116
|
+
*/
|
|
117
|
+
progressScanRows: 8,
|
|
118
|
+
/** Notices one `<job_notices>` block may render. Bounded so the block stays ~8 lines. */
|
|
119
|
+
maxNotices: 8,
|
|
120
|
+
/** Log rows a failed job's notice carries. Two: the message and the frame above it. */
|
|
121
|
+
noticeTailRows: 2,
|
|
122
|
+
/** Rows `job_list` prints before it collapses the rest into `+N more`. */
|
|
123
|
+
listRows: 8,
|
|
124
|
+
/**
|
|
125
|
+
* Consecutive waits with no new row and no progress change before the
|
|
126
|
+
* `[guard]` line is prepended (R-3).
|
|
127
|
+
*
|
|
128
|
+
* A HINT, NEVER A REFUSAL: the seventh wait still waits. Refusing would take
|
|
129
|
+
* the decision away from the model at exactly the point where it has the most
|
|
130
|
+
* context.
|
|
131
|
+
*/
|
|
132
|
+
maxConsecutiveWaits: 6,
|
|
133
|
+
/** Bytes one mirrored job log may occupy before it stops appending. */
|
|
134
|
+
logMirrorMaxBytes: 2_000_000,
|
|
135
|
+
/** Coalescing window for the log mirror's appends (<=4 writes/second). */
|
|
136
|
+
mirrorFlushMs: 250,
|
|
137
|
+
/** Session directories the mirror keeps, newest by mtime. */
|
|
138
|
+
mirrorRetainDirs: 5,
|
|
79
139
|
};
|
|
80
140
|
/**
|
|
81
141
|
* Bumped whenever the wording of the `<background_services>` block changes, so a
|
|
82
142
|
* behaviour report can be tied to a block revision with one grep.
|
|
83
143
|
*/
|
|
84
144
|
export const BACKGROUND_SERVICES_BLOCK_VERSION = 'v1';
|
|
145
|
+
/**
|
|
146
|
+
* The same discipline for the `<background_jobs>` block. A SEPARATE CONSTANT,
|
|
147
|
+
* not a bump of the one above: the two blocks are spliced by different flags
|
|
148
|
+
* (`bash.background` and `jobs.enabled`), so one version number covering both
|
|
149
|
+
* would make "which block changed" unanswerable from a report.
|
|
150
|
+
*/
|
|
151
|
+
export const JOBS_BLOCK_VERSION = 'v1-2026-10';
|
|
152
|
+
/**
|
|
153
|
+
* Head-room a single wait leaves below the tool ceiling (I-4).
|
|
154
|
+
*
|
|
155
|
+
* `jobs.waitSeconds * 1000 + JOB_WAIT_MARGIN_MS <= toolTimeoutMs` is the whole
|
|
156
|
+
* reason a wait never needs to pause the idle watchdog (D-3): a 60 s wait inside
|
|
157
|
+
* a 180 s tool call in a 210 s idle window cannot be mistaken for a hang.
|
|
158
|
+
*
|
|
159
|
+
* IT LIVES HERE RATHER THAN IN `config/schema.ts`, where the design document put
|
|
160
|
+
* it, because the TOOL layer needs the same number for its call-time convergence
|
|
161
|
+
* (AC-14) and nothing in `tools/` may import `config/` - a tool takes its policy
|
|
162
|
+
* through closures. One constant, two readers, no second copy to drift. Recorded
|
|
163
|
+
* in `docs/plans/long-task-jobs/spec.md` under `## 实施过程发现的方案缺陷` (I-2).
|
|
164
|
+
*/
|
|
165
|
+
export const JOB_WAIT_MARGIN_MS = 30_000;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The optional on-disk log mirror — the escape hatch (long-task-jobs §4.8).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/proc/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* WHY IT EXISTS: the ring holds 200 rows, so the `ModuleNotFoundError` from eight
|
|
7
|
+
* minutes ago is gone exactly when the model needs it. The answer is not a bigger
|
|
8
|
+
* ring (that is a bigger context) and not a new tool (the model already has
|
|
9
|
+
* `read_file` and `grep`): it is the full log on disk plus the PATH in the
|
|
10
|
+
* truncation note.
|
|
11
|
+
*
|
|
12
|
+
* THREE RULES, each of which is about not becoming a new failure mode:
|
|
13
|
+
*
|
|
14
|
+
* - ASYNC AND COALESCED. `appendFile` with a <=4 writes/second buffer, so a
|
|
15
|
+
* webpack build emitting thousands of lines cannot turn logging into the
|
|
16
|
+
* bottleneck of the build it is logging.
|
|
17
|
+
* - BOUNDED. `PROC_LIMITS.logMirrorMaxBytes` (2 MB) per job, then one
|
|
18
|
+
* `[mirror truncated]` line and silence.
|
|
19
|
+
* - FAILURES ARE DEBUG LOGS, NEVER THROWS. A full disk must not kill a training
|
|
20
|
+
* run; the ring, the tools and the job state machine do not depend on this
|
|
21
|
+
* module at all.
|
|
22
|
+
*/
|
|
23
|
+
export interface JobLogMirrorOptions {
|
|
24
|
+
/** `${aragonHome}/jobs/${sessionTag}`. A closure so a test can hand in a temp dir. */
|
|
25
|
+
root: () => string;
|
|
26
|
+
maxBytes?: number;
|
|
27
|
+
flushMs?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Delete all but the newest `PROC_LIMITS.mirrorRetainDirs` session directories.
|
|
31
|
+
*
|
|
32
|
+
* BEST EFFORT AND SILENT. This runs once per process at first use, and its whole
|
|
33
|
+
* job is to stop `~/.aragon-agent/jobs/` from growing without bound; a directory
|
|
34
|
+
* that cannot be read is not a reason to refuse to start.
|
|
35
|
+
*/
|
|
36
|
+
export declare function pruneJobLogSessions(jobsRoot: string): void;
|
|
37
|
+
export declare class JobLogMirror {
|
|
38
|
+
private readonly opts;
|
|
39
|
+
private readonly files;
|
|
40
|
+
private readonly buffers;
|
|
41
|
+
private flushTimer;
|
|
42
|
+
private pruned;
|
|
43
|
+
private disposed;
|
|
44
|
+
constructor(opts: JobLogMirrorOptions);
|
|
45
|
+
/** Lazy creation: the directory appears only when a job actually prints. */
|
|
46
|
+
open(jobId: string): void;
|
|
47
|
+
/** Buffer one chunk. Never throws, never awaits, never blocks the child. */
|
|
48
|
+
write(jobId: string, chunk: string): void;
|
|
49
|
+
pathOf(jobId: string): string | undefined;
|
|
50
|
+
/** Flush what is buffered and release the timer. Idempotent. */
|
|
51
|
+
dispose(): void;
|
|
52
|
+
private maxBytes;
|
|
53
|
+
private flushMs;
|
|
54
|
+
private scheduleFlush;
|
|
55
|
+
private flushAll;
|
|
56
|
+
private flushOne;
|
|
57
|
+
/** One place that writes, so the sync and async paths cannot drift. */
|
|
58
|
+
private writeOut;
|
|
59
|
+
}
|