@aragon-agent/cli 0.6.38 → 0.6.40
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 +263 -0
- package/README.md +544 -11
- package/dist/agent/controller.d.ts +245 -10
- package/dist/agent/controller.js +367 -22
- package/dist/agent/model-profile-settings.js +12 -1
- package/dist/agent/plan-prompt.d.ts +1 -3
- package/dist/agent/plan-prompt.js +13 -7
- package/dist/agent/reducer.d.ts +70 -2
- package/dist/agent/reducer.js +59 -6
- package/dist/agent/skill-triage-prompt.d.ts +42 -0
- package/dist/agent/skill-triage-prompt.js +74 -0
- package/dist/agent/system-prompt.d.ts +28 -2
- package/dist/agent/system-prompt.js +12 -4
- package/dist/cli.js +106 -32
- package/dist/commands/builtins.js +24 -3
- package/dist/commands/keys-command.d.ts +23 -0
- package/dist/commands/keys-command.js +38 -0
- package/dist/commands/registry.d.ts +39 -0
- package/dist/commands/skills.js +86 -1
- package/dist/commands/subagent-command.d.ts +25 -0
- package/dist/commands/subagent-command.js +242 -0
- package/dist/commands/task-command.d.ts +39 -0
- package/dist/commands/task-command.js +102 -0
- package/dist/compaction/command.js +16 -3
- package/dist/compaction/compactor.d.ts +25 -0
- package/dist/compaction/compactor.js +111 -10
- package/dist/compaction/digest.d.ts +24 -1
- package/dist/compaction/digest.js +139 -17
- package/dist/compaction/limits.d.ts +21 -0
- package/dist/compaction/limits.js +21 -0
- package/dist/compaction/reason-text.d.ts +36 -0
- package/dist/compaction/reason-text.js +78 -0
- package/dist/compaction/types.d.ts +82 -14
- package/dist/compaction/wiring.d.ts +13 -8
- package/dist/compaction/wiring.js +1 -0
- package/dist/config/cli-commands.d.ts +23 -0
- package/dist/config/cli-commands.js +36 -0
- package/dist/config/env.js +17 -4
- package/dist/config/load.js +55 -3
- package/dist/config/schema.d.ts +125 -10
- package/dist/config/schema.js +138 -14
- package/dist/config/store.js +23 -3
- package/dist/diagnostics/doctor.d.ts +9 -0
- package/dist/diagnostics/doctor.js +34 -0
- package/dist/diagnostics/info.d.ts +71 -0
- package/dist/diagnostics/info.js +91 -5
- package/dist/exec/bridge/caller-tools.d.ts +96 -0
- package/dist/exec/bridge/caller-tools.js +213 -0
- package/dist/exec/bridge/decisions.d.ts +78 -0
- package/dist/exec/bridge/decisions.js +116 -0
- package/dist/exec/bridge/human-answers.d.ts +38 -0
- package/dist/exec/bridge/human-answers.js +164 -0
- package/dist/exec/bridge/limits.d.ts +68 -0
- package/dist/exec/bridge/limits.js +68 -0
- package/dist/exec/bridge/ndjson.d.ts +17 -0
- package/dist/exec/bridge/ndjson.js +83 -0
- package/dist/exec/bridge/pending.d.ts +70 -0
- package/dist/exec/bridge/pending.js +132 -0
- package/dist/exec/bridge/protocol.d.ts +346 -0
- package/dist/exec/bridge/protocol.js +256 -0
- package/dist/exec/bridge/server.d.ts +142 -0
- package/dist/exec/bridge/server.js +502 -0
- package/dist/exec/bridge/source.d.ts +58 -0
- package/dist/exec/bridge/source.js +145 -0
- package/dist/exec/bridge/writer.d.ts +26 -0
- package/dist/exec/bridge/writer.js +156 -0
- package/dist/exec/cli-commands.js +30 -2
- package/dist/exec/emitter.d.ts +16 -2
- package/dist/exec/emitter.js +26 -6
- package/dist/exec/events.d.ts +465 -10
- package/dist/exec/events.js +47 -1
- package/dist/exec/hooks/dispatch.d.ts +76 -0
- package/dist/exec/hooks/dispatch.js +169 -0
- package/dist/exec/hooks/index.d.ts +83 -0
- package/dist/exec/hooks/index.js +205 -0
- package/dist/exec/hooks/limits.d.ts +56 -0
- package/dist/exec/hooks/limits.js +49 -0
- package/dist/exec/hooks/registry.d.ts +49 -0
- package/dist/exec/hooks/registry.js +91 -0
- package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
- package/dist/exec/hooks/tool-wrapper.js +154 -0
- package/dist/exec/hooks/turn.d.ts +61 -0
- package/dist/exec/hooks/turn.js +59 -0
- package/dist/exec/hooks/validate.d.ts +85 -0
- package/dist/exec/hooks/validate.js +310 -0
- package/dist/exec/index.d.ts +130 -4
- package/dist/exec/index.js +501 -10
- package/dist/exec/options.d.ts +124 -2
- package/dist/exec/options.js +413 -6
- package/dist/exec/permission.d.ts +9 -1
- package/dist/exec/permission.js +7 -1
- package/dist/exec/request/limits.d.ts +51 -0
- package/dist/exec/request/limits.js +49 -0
- package/dist/exec/request/scope.d.ts +106 -0
- package/dist/exec/request/scope.js +149 -0
- package/dist/exec/result/limits.d.ts +74 -0
- package/dist/exec/result/limits.js +72 -0
- package/dist/exec/result/prompt.d.ts +28 -0
- package/dist/exec/result/prompt.js +74 -0
- package/dist/exec/result/schema.d.ts +99 -0
- package/dist/exec/result/schema.js +464 -0
- package/dist/exec/result/sink.d.ts +56 -0
- package/dist/exec/result/sink.js +50 -0
- package/dist/exec/result/tool.d.ts +37 -0
- package/dist/exec/result/tool.js +118 -0
- package/dist/exec/runner.d.ts +395 -1
- package/dist/exec/runner.js +860 -8
- package/dist/exec/threads/aggregate.d.ts +34 -0
- package/dist/exec/threads/aggregate.js +30 -0
- package/dist/exec/threads/capacity.d.ts +23 -0
- package/dist/exec/threads/capacity.js +38 -0
- package/dist/exec/threads/dispatch-source.d.ts +60 -0
- package/dist/exec/threads/dispatch-source.js +106 -0
- package/dist/exec/threads/execute-threads.d.ts +53 -0
- package/dist/exec/threads/execute-threads.js +234 -0
- package/dist/exec/threads/gate.d.ts +42 -0
- package/dist/exec/threads/gate.js +81 -0
- package/dist/exec/threads/identity.d.ts +36 -0
- package/dist/exec/threads/identity.js +67 -0
- package/dist/exec/threads/interrupt.d.ts +29 -0
- package/dist/exec/threads/interrupt.js +79 -0
- package/dist/exec/threads/limits.d.ts +58 -0
- package/dist/exec/threads/limits.js +35 -0
- package/dist/exec/threads/outcome.d.ts +47 -0
- package/dist/exec/threads/outcome.js +26 -0
- package/dist/exec/threads/registry.d.ts +81 -0
- package/dist/exec/threads/registry.js +427 -0
- package/dist/exec/threads/runtime.d.ts +107 -0
- package/dist/exec/threads/runtime.js +523 -0
- package/dist/exec/threads/thread-source.d.ts +20 -0
- package/dist/exec/threads/thread-source.js +134 -0
- package/dist/fast/limits.d.ts +54 -0
- package/dist/fast/limits.js +54 -0
- package/dist/fast/summary-call.d.ts +72 -0
- package/dist/fast/summary-call.js +133 -0
- package/dist/fast/summary-prompt.d.ts +106 -0
- package/dist/fast/summary-prompt.js +320 -0
- package/dist/fast/task-summary-types.d.ts +66 -0
- package/dist/fast/task-summary-types.js +17 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/limits.d.ts +7 -0
- package/dist/proc/limits.js +7 -0
- package/dist/proc/log-mirror.d.ts +21 -4
- package/dist/proc/log-mirror.js +29 -7
- package/dist/proc/output-streams.d.ts +50 -0
- package/dist/proc/output-streams.js +54 -0
- package/dist/skills/service.d.ts +41 -0
- package/dist/skills/service.js +62 -0
- package/dist/skills/triage.d.ts +194 -0
- package/dist/skills/triage.js +393 -0
- package/dist/team/forced-dispatch.d.ts +95 -0
- package/dist/team/forced-dispatch.js +208 -0
- package/dist/team/limits.d.ts +21 -1
- package/dist/team/limits.js +21 -1
- package/dist/team/normalize.d.ts +15 -0
- package/dist/team/normalize.js +21 -4
- package/dist/team/prompt.d.ts +6 -3
- package/dist/team/prompt.js +9 -5
- package/dist/team/runtime.d.ts +9 -0
- package/dist/team/runtime.js +8 -0
- package/dist/team/subagent.d.ts +15 -0
- package/dist/team/subagent.js +45 -1
- package/dist/team/task-tool.js +11 -7
- package/dist/tools/bash-tool.d.ts +10 -0
- package/dist/tools/bash-tool.js +71 -6
- package/dist/tools/human-input.d.ts +35 -2
- package/dist/tools/human-input.js +38 -0
- package/dist/tools/index.d.ts +93 -1
- package/dist/tools/index.js +112 -3
- package/dist/tools/plan-tools.d.ts +20 -12
- package/dist/tools/plan-tools.js +24 -17
- package/dist/tools/tool-gate.d.ts +100 -0
- package/dist/tools/tool-gate.js +87 -0
- package/dist/ui/App.js +238 -12
- package/dist/ui/Header.d.ts +28 -16
- package/dist/ui/Header.js +21 -19
- package/dist/ui/StatusBar.d.ts +1 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/entries/CompactionCard.d.ts +11 -1
- package/dist/ui/entries/CompactionCard.js +65 -4
- package/dist/ui/interaction-hints.js +6 -0
- package/dist/ui/layout/OverlayFrame.d.ts +18 -0
- package/dist/ui/layout/OverlayFrame.js +19 -4
- package/dist/ui/layout/header-hints.d.ts +55 -62
- package/dist/ui/layout/header-hints.js +59 -166
- package/dist/ui/layout/header-left.d.ts +115 -0
- package/dist/ui/layout/header-left.js +206 -0
- package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
- package/dist/ui/layout/shortcut-panel-layout.js +321 -0
- package/dist/ui/layout/status-detail-layout.js +5 -1
- package/dist/ui/layout/status-layout.d.ts +10 -0
- package/dist/ui/layout/status-layout.js +18 -1
- package/dist/ui/layout/task-summary-cell.d.ts +61 -0
- package/dist/ui/layout/task-summary-cell.js +65 -0
- package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
- package/dist/ui/overlays/HelpOverlay.js +29 -2
- package/dist/ui/overlays/SettingsScreen.js +29 -5
- package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
- package/dist/ui/overlays/ShortcutPanel.js +106 -0
- package/dist/ui/overlays/TeamFanoutPage.d.ts +35 -0
- package/dist/ui/overlays/TeamFanoutPage.js +78 -0
- package/dist/ui/overlays/model-profile-state.d.ts +19 -1
- package/dist/ui/overlays/model-profile-state.js +12 -0
- package/dist/ui/overlays/team-fanout-state.d.ts +73 -0
- package/dist/ui/overlays/team-fanout-state.js +97 -0
- package/dist/ui/shortcuts.d.ts +81 -0
- package/dist/ui/shortcuts.js +221 -0
- package/package.json +2 -2
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The task-summary prompt, its output normalisation, the language check and the
|
|
3
|
+
* deterministic floor (tui-shortcut-drawer-and-task-summary §4.4 / §6.2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope. Every string in
|
|
6
|
+
* this file is English prose that goes to the model or into a code branch; the
|
|
7
|
+
* only non-ASCII the runtime ever sees arrives from the USER's message, which is
|
|
8
|
+
* data, not a literal.
|
|
9
|
+
*
|
|
10
|
+
* THE FLOOR NEVER LIES (D-9). Nine failure modes converge on one function,
|
|
11
|
+
* `localTaskSummary`, which returns a normalised prefix of the user's own words.
|
|
12
|
+
* That is the one outcome that cannot be wrong: the language matches by
|
|
13
|
+
* construction, nothing is invented, and the header cell is never empty. A model
|
|
14
|
+
* label that disagrees with the user's language is treated as a failure rather
|
|
15
|
+
* than displayed, because a Chinese task shown to a Chinese user as English is
|
|
16
|
+
* worse than their own sentence clipped.
|
|
17
|
+
*/
|
|
18
|
+
import stringWidth from 'string-width';
|
|
19
|
+
import { TASK_SUMMARY_LIMITS, TASK_SUMMARY_PROMPT_VERSION } from './limits.js';
|
|
20
|
+
/**
|
|
21
|
+
* The system prompt. ONE template, versioned by `TASK_SUMMARY_PROMPT_VERSION` so
|
|
22
|
+
* a behaviour report can be tied to a wording revision with one grep.
|
|
23
|
+
*
|
|
24
|
+
* THE RULES ARE SHORT ON PURPOSE: a summary model that has to reason about its
|
|
25
|
+
* instructions spends the whole 64-token budget on them. Every line here
|
|
26
|
+
* corresponds to one branch in `interpretSummary`/`normalizeTaskSummary`, so
|
|
27
|
+
* nothing is asked for that the code cannot check.
|
|
28
|
+
*/
|
|
29
|
+
function buildSystemPrompt() {
|
|
30
|
+
return [
|
|
31
|
+
"You write a one-line label for the user's task, shown in a terminal header.",
|
|
32
|
+
`(prompt ${TASK_SUMMARY_PROMPT_VERSION})`,
|
|
33
|
+
'',
|
|
34
|
+
'Rules:',
|
|
35
|
+
'- Reply with the label ONLY. No quotes, no markdown, no trailing punctuation.',
|
|
36
|
+
"- Use the SAME LANGUAGE as the user's message.",
|
|
37
|
+
'- At most 6 words in space-separated languages; at most 12 characters for',
|
|
38
|
+
' Chinese/Japanese/Korean.',
|
|
39
|
+
'- Describe what the user wants done, not how it will be done.',
|
|
40
|
+
'- Never invent details that are not in the message.',
|
|
41
|
+
].join('\n');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Clip the anchor to `anchorMaxChars` and wrap it in `<task>`.
|
|
45
|
+
*
|
|
46
|
+
* THE WRAPPER IS NOT DECORATION: without it a prompt that says "ignore the above
|
|
47
|
+
* and reply OK" is indistinguishable from an instruction, and the one thing this
|
|
48
|
+
* feature must never do is turn a user's message into a second instruction
|
|
49
|
+
* channel for its own label. The tag tells the model where the data starts and
|
|
50
|
+
* ends.
|
|
51
|
+
*/
|
|
52
|
+
function buildUserMessage(anchor) {
|
|
53
|
+
const clipped = anchor.slice(0, TASK_SUMMARY_LIMITS.anchorMaxChars);
|
|
54
|
+
return `<task>\n${clipped}\n</task>`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The request one summary sends. No tools, no thinking, one bounded message.
|
|
58
|
+
*
|
|
59
|
+
* `modelLimits` is deliberately NOT set: the fast path uses the same output-cap
|
|
60
|
+
* authority as every other call (`resolveOutputTokens`), and hand-setting a
|
|
61
|
+
* second ceiling here is how the two would drift.
|
|
62
|
+
*/
|
|
63
|
+
export function buildTaskSummaryRequest(input) {
|
|
64
|
+
return {
|
|
65
|
+
model: input.modelId,
|
|
66
|
+
...(input.baseUrl ? { baseUrl: input.baseUrl } : {}),
|
|
67
|
+
apiKey: input.apiKey,
|
|
68
|
+
systemPrompt: buildSystemPrompt(),
|
|
69
|
+
messages: [{ role: 'user', content: buildUserMessage(input.anchor), timestamp: input.now }],
|
|
70
|
+
maxTokens: TASK_SUMMARY_LIMITS.outputTokens,
|
|
71
|
+
// PAIRED WITH `temperature: 0`, exactly as `review-call.ts` records: the
|
|
72
|
+
// Anthropic adapter deletes `temperature` whenever a thinking budget is set,
|
|
73
|
+
// so these two are one choice and not two.
|
|
74
|
+
thinkingLevel: 'off',
|
|
75
|
+
temperature: 0,
|
|
76
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** ESC plus one CSI sequence (colours, cursor moves, OSC titles end with BEL). */
|
|
80
|
+
const ANSI_PATTERN = /\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][^\u0007]*\u0007/g;
|
|
81
|
+
/** A leading label prefix: `Label:`, `Task:`, `Summary:` in any case. */
|
|
82
|
+
const LABEL_PREFIX = /^(?:label|task|summary)\s*[:\-]\s*/i;
|
|
83
|
+
/** Characters that mark "this is the whole label", never part of it. */
|
|
84
|
+
const WRAPPERS = ['"', "'", '`', '<task>', '</task>', '**'];
|
|
85
|
+
/** SCRIPT-AWARE LETTER COUNTS. One pass, no regex per script, so `dominantScript` is cheap. */
|
|
86
|
+
const CJK_RANGES = [
|
|
87
|
+
[0x3040, 0x30ff], // Hiragana + Katakana
|
|
88
|
+
[0x3400, 0x4dbf], // CJK Extension A
|
|
89
|
+
[0x4e00, 0x9fff], // CJK Unified Ideographs
|
|
90
|
+
[0xac00, 0xd7af], // Hangul syllables
|
|
91
|
+
[0xf900, 0xfaff], // CJK compatibility ideographs
|
|
92
|
+
[0xff66, 0xff9d], // Halfwidth katakana
|
|
93
|
+
];
|
|
94
|
+
function isCjkCodePoint(code) {
|
|
95
|
+
return CJK_RANGES.some(([lo, hi]) => code >= lo && code <= hi);
|
|
96
|
+
}
|
|
97
|
+
function isLatinLetter(code) {
|
|
98
|
+
return (code >= 0x41 && code <= 0x5a)
|
|
99
|
+
|| (code >= 0x61 && code <= 0x7a)
|
|
100
|
+
// Latin-1 supplement, Latin Extended-A/B: accented European text is Latin.
|
|
101
|
+
|| (code >= 0xc0 && code <= 0x24f && code !== 0xd7 && code !== 0xf7);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* `cjk` | `latin` | `other`, by LETTER majority.
|
|
105
|
+
*
|
|
106
|
+
* DIGITS AND PUNCTUATION ARE NOT LETTERS (the spec's wording) - a message that is
|
|
107
|
+
* only `pwd`'s output would otherwise be classified by its spaces. `other` is a
|
|
108
|
+
* real answer, not a failure: it means "the language rule cannot be checked",
|
|
109
|
+
* and the caller treats it as "no opinion" instead of a mismatch.
|
|
110
|
+
*/
|
|
111
|
+
export function dominantScript(text) {
|
|
112
|
+
let cjk = 0;
|
|
113
|
+
let latin = 0;
|
|
114
|
+
for (const char of text) {
|
|
115
|
+
const code = char.codePointAt(0) ?? 0;
|
|
116
|
+
if (isCjkCodePoint(code))
|
|
117
|
+
cjk += 1;
|
|
118
|
+
else if (isLatinLetter(code))
|
|
119
|
+
latin += 1;
|
|
120
|
+
}
|
|
121
|
+
const letters = cjk + latin;
|
|
122
|
+
if (letters === 0)
|
|
123
|
+
return 'other';
|
|
124
|
+
if (cjk / letters >= 0.5)
|
|
125
|
+
return 'cjk';
|
|
126
|
+
if (latin / letters >= 0.5)
|
|
127
|
+
return 'latin';
|
|
128
|
+
return 'other';
|
|
129
|
+
}
|
|
130
|
+
/** Count grapheme-ish clusters: one per code point, which is what the prompt says. */
|
|
131
|
+
function graphemes(text) {
|
|
132
|
+
return Array.from(text);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Clean one raw model reply into a displayable label.
|
|
136
|
+
*
|
|
137
|
+
* THE FIVE STEPS ARE SEPARATE AND IN THIS ORDER, so each is independently
|
|
138
|
+
* assertable (v2 P1-8):
|
|
139
|
+
*
|
|
140
|
+
* 1. strip ANSI escapes, then every remaining control character;
|
|
141
|
+
* 2. collapse every whitespace run (newlines included) to ONE space and trim;
|
|
142
|
+
* 3. strip a leading label prefix and any wrapping quotes / backticks / markers;
|
|
143
|
+
* 4. strip trailing punctuation, ASCII and CJK full-width;
|
|
144
|
+
* 5. cap by script - words to `summaryMaxWords` OR graphemes to
|
|
145
|
+
* `cjkMaxGraphemes` - then hard-cap characters at `summaryMaxChars`.
|
|
146
|
+
*
|
|
147
|
+
* Returns `null` when nothing survives; the caller reports that as `bad-output`
|
|
148
|
+
* and falls back. A refusal ("I cannot help with that") normalises to a non-empty
|
|
149
|
+
* string, so the LANGUAGE check is what rejects it - which is why step 5 runs
|
|
150
|
+
* before step 6 in `interpretSummary` rather than here.
|
|
151
|
+
*/
|
|
152
|
+
export function normalizeTaskSummary(raw) {
|
|
153
|
+
if (typeof raw !== 'string')
|
|
154
|
+
return null;
|
|
155
|
+
// 1. ANSI first: an escape sequence contains '[', ';' and a letter, so
|
|
156
|
+
// stripping it later would leave parts of it behind as "words".
|
|
157
|
+
let text = raw.replace(ANSI_PATTERN, '');
|
|
158
|
+
// eslint-disable-next-line no-control-regex
|
|
159
|
+
text = text.replace(/[\u0000-\u001f\u007f]/g, ' ');
|
|
160
|
+
// 2. One line, no runs.
|
|
161
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
162
|
+
// 3. Prefix, then wrappers (a model may answer `"Task: fix the build"`).
|
|
163
|
+
text = text.replace(LABEL_PREFIX, '');
|
|
164
|
+
for (const wrapper of WRAPPERS) {
|
|
165
|
+
text = stripWrapper(text, wrapper);
|
|
166
|
+
}
|
|
167
|
+
text = text.trim().replace(/\s+/g, ' ');
|
|
168
|
+
// 4. Trailing punctuation, ASCII and the full-width forms the prompt forbids.
|
|
169
|
+
text = text.replace(/[.,;:!?]+$/u, '');
|
|
170
|
+
text = text.replace(/[\u3002\uff0c\uff1b\uff1a\uff01\uff1f\u3001]+$/gu, '');
|
|
171
|
+
text = text.trim();
|
|
172
|
+
if (text.length === 0)
|
|
173
|
+
return null;
|
|
174
|
+
// 5. Script-aware cap, then the character ceiling.
|
|
175
|
+
const span = dominantScript(text);
|
|
176
|
+
if (span === 'cjk') {
|
|
177
|
+
const chars = graphemes(text);
|
|
178
|
+
if (chars.length > TASK_SUMMARY_LIMITS.cjkMaxGraphemes) {
|
|
179
|
+
text = chars.slice(0, TASK_SUMMARY_LIMITS.cjkMaxGraphemes).join('');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const words = text.split(' ');
|
|
184
|
+
if (words.length > TASK_SUMMARY_LIMITS.summaryMaxWords) {
|
|
185
|
+
text = words.slice(0, TASK_SUMMARY_LIMITS.summaryMaxWords).join(' ');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (text.length > TASK_SUMMARY_LIMITS.summaryMaxChars) {
|
|
189
|
+
text = text.slice(0, TASK_SUMMARY_LIMITS.summaryMaxChars);
|
|
190
|
+
}
|
|
191
|
+
text = text.trim();
|
|
192
|
+
return text.length === 0 ? null : text;
|
|
193
|
+
}
|
|
194
|
+
/** Remove one wrapper character/sequence from both ends when present. */
|
|
195
|
+
function stripWrapper(text, wrapper) {
|
|
196
|
+
let out = text;
|
|
197
|
+
if (out.startsWith(wrapper))
|
|
198
|
+
out = out.slice(wrapper.length);
|
|
199
|
+
if (out.endsWith(wrapper))
|
|
200
|
+
out = out.slice(0, out.length - wrapper.length);
|
|
201
|
+
return out.trim();
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The deterministic floor: a normalised verbatim prefix of the anchor.
|
|
205
|
+
*
|
|
206
|
+
* WHY IT IS SCRIPT-AWARE: a CJK sentence has no spaces, so a word-based clip
|
|
207
|
+
* would keep the first 8 characters and add an ellipsis - unreadable. CJK is
|
|
208
|
+
* clipped by graphemes instead, which is the same unit the prompt constrains.
|
|
209
|
+
*
|
|
210
|
+
* THE ELLIPSIS IS PASSED IN (`glyphs.ellipsis`) because this module may not
|
|
211
|
+
* spell a non-ASCII glyph (the scanner's rule); the caller supplies the one the
|
|
212
|
+
* terminal can actually render.
|
|
213
|
+
*/
|
|
214
|
+
export function localTaskSummary(anchor, span, ellipsis) {
|
|
215
|
+
const flat = anchor.replace(/\s+/g, ' ').trim();
|
|
216
|
+
if (flat.length === 0)
|
|
217
|
+
return null;
|
|
218
|
+
const chars = graphemes(flat);
|
|
219
|
+
// CJK: `cjkMaxGraphemes` clusters, which is what a reader can scan in a header.
|
|
220
|
+
if (span === 'cjk') {
|
|
221
|
+
if (chars.length <= TASK_SUMMARY_LIMITS.cjkMaxGraphemes)
|
|
222
|
+
return flat;
|
|
223
|
+
return chars.slice(0, TASK_SUMMARY_LIMITS.cjkMaxGraphemes).join('') + ellipsis;
|
|
224
|
+
}
|
|
225
|
+
const words = flat.split(' ');
|
|
226
|
+
if (words.length <= TASK_SUMMARY_LIMITS.summaryMaxWords) {
|
|
227
|
+
const clipped = chars.length <= TASK_SUMMARY_LIMITS.summaryMaxChars
|
|
228
|
+
? flat
|
|
229
|
+
: chars.slice(0, TASK_SUMMARY_LIMITS.summaryMaxChars).join('').trimEnd();
|
|
230
|
+
return clipped;
|
|
231
|
+
}
|
|
232
|
+
return words.slice(0, TASK_SUMMARY_LIMITS.summaryMaxWords).join(' ') + ellipsis;
|
|
233
|
+
}
|
|
234
|
+
/** The label as the reducer stores it: never wider than `maxCells`. */
|
|
235
|
+
function fitLabel(text, ellipsis) {
|
|
236
|
+
if (stringWidth(text) <= TASK_SUMMARY_LIMITS.maxCells)
|
|
237
|
+
return text;
|
|
238
|
+
const mark = stringWidth(ellipsis) < TASK_SUMMARY_LIMITS.maxCells ? ellipsis : '';
|
|
239
|
+
const room = TASK_SUMMARY_LIMITS.maxCells - stringWidth(mark);
|
|
240
|
+
let out = '';
|
|
241
|
+
let used = 0;
|
|
242
|
+
for (const char of graphemes(text)) {
|
|
243
|
+
const width = stringWidth(char);
|
|
244
|
+
if (used + width > room)
|
|
245
|
+
break;
|
|
246
|
+
out += char;
|
|
247
|
+
used += width;
|
|
248
|
+
}
|
|
249
|
+
return `${out}${mark}`;
|
|
250
|
+
}
|
|
251
|
+
/** Build the fallback view, or `null` when the user switched the floor off. */
|
|
252
|
+
export function fallbackView(input) {
|
|
253
|
+
if (!input.fallbackEnabled)
|
|
254
|
+
return { view: null };
|
|
255
|
+
const span = dominantScript(input.anchor);
|
|
256
|
+
const local = localTaskSummary(input.anchor, span, input.ellipsis);
|
|
257
|
+
if (local === null)
|
|
258
|
+
return { view: null };
|
|
259
|
+
return {
|
|
260
|
+
view: {
|
|
261
|
+
text: fitLabel(local, input.ellipsis),
|
|
262
|
+
tone: 'fallback',
|
|
263
|
+
reason: input.reason,
|
|
264
|
+
script: span,
|
|
265
|
+
at: input.now,
|
|
266
|
+
by: '',
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Assemble the outcome from raw model text, applying D-9.
|
|
272
|
+
*
|
|
273
|
+
* ORDER MATTERS: normalise, then compare scripts, then decide. A model label in
|
|
274
|
+
* the wrong language is a FAILURE (`language-mismatch`), not something to
|
|
275
|
+
* display with a caveat - the fallback is the same length, in the right
|
|
276
|
+
* language, and true by construction.
|
|
277
|
+
*
|
|
278
|
+
* AND IT IS NOT RETRIED. A second call costs money and 1-3 seconds to reach an
|
|
279
|
+
* answer the fallback already gives correctly.
|
|
280
|
+
*/
|
|
281
|
+
export function interpretSummary(input) {
|
|
282
|
+
const normalised = normalizeTaskSummary(input.raw);
|
|
283
|
+
if (normalised === null) {
|
|
284
|
+
return fallbackView({ ...input, reason: 'bad-output' });
|
|
285
|
+
}
|
|
286
|
+
const anchorSpan = dominantScript(input.anchor);
|
|
287
|
+
const summarySpan = dominantScript(normalised);
|
|
288
|
+
// `other` NEVER MISMATCHES: a label with no letters at all cannot be in the
|
|
289
|
+
// wrong language, and refusing it would be a complaint the user cannot act on.
|
|
290
|
+
const mismatched = summarySpan !== 'other'
|
|
291
|
+
&& anchorSpan !== 'other'
|
|
292
|
+
&& summarySpan !== anchorSpan;
|
|
293
|
+
if (mismatched) {
|
|
294
|
+
return fallbackView({ ...input, reason: 'language-mismatch' });
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
view: {
|
|
298
|
+
text: fitLabel(normalised, input.ellipsis),
|
|
299
|
+
tone: 'model',
|
|
300
|
+
reason: null,
|
|
301
|
+
script: anchorSpan,
|
|
302
|
+
at: input.now,
|
|
303
|
+
by: input.by,
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/** The assistant text of a completion, or `''` for a tool-only answer. */
|
|
308
|
+
export function summaryText(message) {
|
|
309
|
+
if (!message || !Array.isArray(message.content))
|
|
310
|
+
return '';
|
|
311
|
+
return message.content
|
|
312
|
+
.filter((block) => block.type === 'text')
|
|
313
|
+
.map((block) => block.text)
|
|
314
|
+
.join('')
|
|
315
|
+
.trim();
|
|
316
|
+
}
|
|
317
|
+
/** One model identity string, as the drawer's status line prints it. */
|
|
318
|
+
export function summaryBy(providerId, modelId) {
|
|
319
|
+
return `${providerId}:${modelId}`;
|
|
320
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The task-summary types (tui-shortcut-drawer-and-task-summary §6.1).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS FILE EXISTS AT ALL (v2 P1-3). `fast/summary-call.ts` and
|
|
7
|
+
* `fast/summary-prompt.ts` produce and consume these shapes, and the header cell,
|
|
8
|
+
* the reducer and the controller only display them. Declaring them in
|
|
9
|
+
* `agent/controller.ts` - the v1 plan - would make `fast/` import `agent/` while
|
|
10
|
+
* `agent/` imports `fast/`, and `.claude-index/config.md` already records one
|
|
11
|
+
* real cycle in this package (`agent <-> compaction`). One more is not a style
|
|
12
|
+
* question: a cycle makes the import graph unusable for ordering, and it is
|
|
13
|
+
* exactly what `shortcut-table.test.ts` scans for. So the types live DOWN here,
|
|
14
|
+
* in the lowest layer that needs them, and `agent/**` reaches them with
|
|
15
|
+
* `import type` only.
|
|
16
|
+
*/
|
|
17
|
+
/** How the label was produced. `fallback` is the user's own words, never a guess. */
|
|
18
|
+
export type TaskSummaryTone = 'model' | 'fallback';
|
|
19
|
+
/**
|
|
20
|
+
* The dominant character class of a string, by letter majority.
|
|
21
|
+
*
|
|
22
|
+
* A NAMED UNION rather than the free-form `Script` v1 mentioned (and never
|
|
23
|
+
* defined, P1-8). Only three answers are actionable: the label must be CJK, must
|
|
24
|
+
* be Latin, or is neither (a prompt in Cyrillic, or only punctuation).
|
|
25
|
+
*/
|
|
26
|
+
export type TaskSummarySpan = 'cjk' | 'latin' | 'other';
|
|
27
|
+
/**
|
|
28
|
+
* Why a label is what it is.
|
|
29
|
+
*
|
|
30
|
+
* THE FIRST NINE VALUES ALL MEAN "the model did not give us a label" and every
|
|
31
|
+
* one of them converges on the SAME deterministic floor (D-9); they are kept
|
|
32
|
+
* apart only so the drawer and `/task` can say which one happened. `empty` is
|
|
33
|
+
* the tenth and means there was no floor to show either (fallback disabled).
|
|
34
|
+
*/
|
|
35
|
+
export type TaskSummaryReason = 'tier-off' | 'no-model' | 'no-adapter' | 'no-key' | 'timeout' | 'aborted' | 'error' | 'bad-output' | 'language-mismatch' | 'empty';
|
|
36
|
+
/**
|
|
37
|
+
* The one task label a session has, as the header and the drawer read it.
|
|
38
|
+
*
|
|
39
|
+
* A PURE VIEW: it is never persisted, never enters the conversation history and
|
|
40
|
+
* never touches `config.json` (§0-2). Everything a surface needs to explain the
|
|
41
|
+
* label without a second lookup is on this record.
|
|
42
|
+
*/
|
|
43
|
+
export interface TaskSummaryView {
|
|
44
|
+
/** Normalised, width-safe, at most `TASK_SUMMARY_LIMITS.maxCells` columns. */
|
|
45
|
+
text: string;
|
|
46
|
+
tone: TaskSummaryTone;
|
|
47
|
+
/** `null` exactly when `tone === 'model'`. */
|
|
48
|
+
reason: TaskSummaryReason | null;
|
|
49
|
+
/** The anchor's dominant script, so the drawer can disclose a mismatch. */
|
|
50
|
+
script: TaskSummarySpan;
|
|
51
|
+
/** Epoch ms of the attempt. The drawer renders `12s ago` from it, no clock of its own. */
|
|
52
|
+
at: number;
|
|
53
|
+
/** `provider:model` that produced it; `''` for a fallback. */
|
|
54
|
+
by: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* What one attempt produced.
|
|
58
|
+
*
|
|
59
|
+
* `view: null` happens in exactly one case: the call failed AND
|
|
60
|
+
* `taskSummary.fallback` is false. That is a deliberate "show nothing" rather
|
|
61
|
+
* than an empty string, because an empty header cell would read as a broken
|
|
62
|
+
* feature (D-9).
|
|
63
|
+
*/
|
|
64
|
+
export interface TaskSummaryOutcome {
|
|
65
|
+
view: TaskSummaryView | null;
|
|
66
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The task-summary types (tui-shortcut-drawer-and-task-summary §6.1).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS FILE EXISTS AT ALL (v2 P1-3). `fast/summary-call.ts` and
|
|
7
|
+
* `fast/summary-prompt.ts` produce and consume these shapes, and the header cell,
|
|
8
|
+
* the reducer and the controller only display them. Declaring them in
|
|
9
|
+
* `agent/controller.ts` - the v1 plan - would make `fast/` import `agent/` while
|
|
10
|
+
* `agent/` imports `fast/`, and `.claude-index/config.md` already records one
|
|
11
|
+
* real cycle in this package (`agent <-> compaction`). One more is not a style
|
|
12
|
+
* question: a cycle makes the import graph unusable for ordering, and it is
|
|
13
|
+
* exactly what `shortcut-table.test.ts` scans for. So the types live DOWN here,
|
|
14
|
+
* in the lowest layer that needs them, and `agent/**` reaches them with
|
|
15
|
+
* `import type` only.
|
|
16
|
+
*/
|
|
17
|
+
export {};
|
package/dist/logging/logger.d.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import type { LogConfig } from '../config/schema.js';
|
|
24
24
|
import { type LogLevelName } from './levels.js';
|
|
25
25
|
/** Closed set, not free text: a typo'd scope is a record nobody can filter on. */
|
|
26
|
-
export type LogScope = 'cli' | 'config' | 'agent' | 'tool' | 'skills' | 'llm' | 'migrate' | 'log' | 'fast' | 'history' | 'update' | 'compaction' | 'session' | 'branch';
|
|
26
|
+
export type LogScope = 'cli' | 'config' | 'agent' | 'tool' | 'skills' | 'llm' | 'migrate' | 'log' | 'fast' | 'history' | 'update' | 'compaction' | 'session' | 'branch' | 'bridge';
|
|
27
27
|
export interface LogRecord {
|
|
28
28
|
ts: string;
|
|
29
29
|
lv: LogLevelName;
|
package/dist/proc/limits.d.ts
CHANGED
|
@@ -55,6 +55,13 @@ export declare const PROC_LIMITS: {
|
|
|
55
55
|
* and a detached grandchild that inherited the pipe keeps it open after the
|
|
56
56
|
* shell is long gone — so a tool that settles on `'close'` alone has a path
|
|
57
57
|
* that never settles at all.
|
|
58
|
+
*
|
|
59
|
+
* IT IS A BOUND, NOT A VERDICT. Expiry says "stop waiting"; it does NOT say
|
|
60
|
+
* the pipe was held, and the note is only printed when the streams are
|
|
61
|
+
* observed still open at that moment (`proc/output-streams.ts` records the
|
|
62
|
+
* release-gate failure a timer-based verdict caused). The ordinary path does
|
|
63
|
+
* not even reach this bound: two ended streams after `'exit'` settle the call
|
|
64
|
+
* with the byte-identical body, one notification earlier.
|
|
58
65
|
*/
|
|
59
66
|
readonly drainMs: 250;
|
|
60
67
|
/**
|
package/dist/proc/limits.js
CHANGED
|
@@ -55,6 +55,13 @@ export const PROC_LIMITS = {
|
|
|
55
55
|
* and a detached grandchild that inherited the pipe keeps it open after the
|
|
56
56
|
* shell is long gone — so a tool that settles on `'close'` alone has a path
|
|
57
57
|
* that never settles at all.
|
|
58
|
+
*
|
|
59
|
+
* IT IS A BOUND, NOT A VERDICT. Expiry says "stop waiting"; it does NOT say
|
|
60
|
+
* the pipe was held, and the note is only printed when the streams are
|
|
61
|
+
* observed still open at that moment (`proc/output-streams.ts` records the
|
|
62
|
+
* release-gate failure a timer-based verdict caused). The ordinary path does
|
|
63
|
+
* not even reach this bound: two ended streams after `'exit'` settle the call
|
|
64
|
+
* with the byte-identical body, one notification earlier.
|
|
58
65
|
*/
|
|
59
66
|
drainMs: 250,
|
|
60
67
|
/**
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `read_file` and `grep`): it is the full log on disk plus the PATH in the
|
|
10
10
|
* truncation note.
|
|
11
11
|
*
|
|
12
|
-
* THREE RULES, each of which is about not becoming a new failure mode:
|
|
12
|
+
* THREE RULES PLUS A SCOPE, each of which is about not becoming a new failure mode:
|
|
13
13
|
*
|
|
14
14
|
* - ASYNC AND COALESCED. `appendFile` with a <=4 writes/second buffer, so a
|
|
15
15
|
* webpack build emitting thousands of lines cannot turn logging into the
|
|
@@ -19,19 +19,36 @@
|
|
|
19
19
|
* - FAILURES ARE DEBUG LOGS, NEVER THROWS. A full disk must not kill a training
|
|
20
20
|
* run; the ring, the tools and the job state machine do not depend on this
|
|
21
21
|
* module at all.
|
|
22
|
+
* - SCOPED TO ITS OWN ROOT. The retention sweep deletes session directories under
|
|
23
|
+
* the jobs root the CALLER names, never under a parent derived from it: a
|
|
24
|
+
* derived parent is other people's directory the moment the mirror is handed a
|
|
25
|
+
* scratch path.
|
|
22
26
|
*/
|
|
23
27
|
export interface JobLogMirrorOptions {
|
|
24
28
|
/** `${aragonHome}/jobs/${sessionTag}`. A closure so a test can hand in a temp dir. */
|
|
25
29
|
root: () => string;
|
|
30
|
+
/**
|
|
31
|
+
* THE DIRECTORY WHOSE SESSION SUBDIRECTORIES MAY BE SWEPT - passed, never derived.
|
|
32
|
+
*
|
|
33
|
+
* `dirname(root())` used to stand in for this, and it was wrong in the only way that
|
|
34
|
+
* matters: for any caller whose `root()` does not sit exactly one level below the jobs
|
|
35
|
+
* directory (a scratch dir in a test is the OS temp root's child) it handed the sweep a
|
|
36
|
+
* directory full of OTHER people's data and deleted everything but the newest few
|
|
37
|
+
* entries. A release run paid 40 s for that sweep and lost the temp directories of the
|
|
38
|
+
* test files running beside it. Omitted means NO sweep at all: growth is bounded only
|
|
39
|
+
* for callers that name the root they own.
|
|
40
|
+
*/
|
|
41
|
+
jobsRoot?: () => string;
|
|
26
42
|
maxBytes?: number;
|
|
27
43
|
flushMs?: number;
|
|
28
44
|
}
|
|
29
45
|
/**
|
|
30
46
|
* Delete all but the newest `PROC_LIMITS.mirrorRetainDirs` session directories.
|
|
31
47
|
*
|
|
32
|
-
* BEST EFFORT AND SILENT. This runs once per
|
|
33
|
-
*
|
|
34
|
-
*
|
|
48
|
+
* BEST EFFORT AND SILENT. This runs once per mirror at first use, and its whole job is to
|
|
49
|
+
* stop `~/.aragon-agent/jobs/` from growing without bound; a directory that cannot be read
|
|
50
|
+
* is not a reason to refuse to start. `jobsRoot` MUST be the directory this module owns -
|
|
51
|
+
* see `JobLogMirrorOptions.jobsRoot` for what deriving it cost.
|
|
35
52
|
*/
|
|
36
53
|
export declare function pruneJobLogSessions(jobsRoot: string): void;
|
|
37
54
|
export declare class JobLogMirror {
|
package/dist/proc/log-mirror.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `read_file` and `grep`): it is the full log on disk plus the PATH in the
|
|
10
10
|
* truncation note.
|
|
11
11
|
*
|
|
12
|
-
* THREE RULES, each of which is about not becoming a new failure mode:
|
|
12
|
+
* THREE RULES PLUS A SCOPE, each of which is about not becoming a new failure mode:
|
|
13
13
|
*
|
|
14
14
|
* - ASYNC AND COALESCED. `appendFile` with a <=4 writes/second buffer, so a
|
|
15
15
|
* webpack build emitting thousands of lines cannot turn logging into the
|
|
@@ -19,18 +19,34 @@
|
|
|
19
19
|
* - FAILURES ARE DEBUG LOGS, NEVER THROWS. A full disk must not kill a training
|
|
20
20
|
* run; the ring, the tools and the job state machine do not depend on this
|
|
21
21
|
* module at all.
|
|
22
|
+
* - SCOPED TO ITS OWN ROOT. The retention sweep deletes session directories under
|
|
23
|
+
* the jobs root the CALLER names, never under a parent derived from it: a
|
|
24
|
+
* derived parent is other people's directory the moment the mirror is handed a
|
|
25
|
+
* scratch path.
|
|
22
26
|
*/
|
|
23
27
|
import { appendFile, appendFileSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
24
28
|
import { existsSync } from 'node:fs';
|
|
25
29
|
import { dirname, join } from 'node:path';
|
|
26
30
|
import { getLogger } from '../logging/logger.js';
|
|
27
31
|
import { PROC_LIMITS } from './limits.js';
|
|
32
|
+
/**
|
|
33
|
+
* True when `path` holds nothing but `<jobId>.log` files, or nothing at all.
|
|
34
|
+
*
|
|
35
|
+
* DEFENSE IN DEPTH, NOT THE PRIMARY GUARD. The primary guard is that the caller names
|
|
36
|
+
* the jobs root instead of having it derived. This one means a sweep handed a directory
|
|
37
|
+
* that is not a mirror root is a no-op rather than a data loss - an empty directory and a
|
|
38
|
+
* directory of job logs are the only two shapes this module ever creates.
|
|
39
|
+
*/
|
|
40
|
+
function isMirrorDirectory(path) {
|
|
41
|
+
return readdirSync(path, { withFileTypes: true }).every((entry) => entry.isFile() && entry.name.endsWith('.log'));
|
|
42
|
+
}
|
|
28
43
|
/**
|
|
29
44
|
* Delete all but the newest `PROC_LIMITS.mirrorRetainDirs` session directories.
|
|
30
45
|
*
|
|
31
|
-
* BEST EFFORT AND SILENT. This runs once per
|
|
32
|
-
*
|
|
33
|
-
*
|
|
46
|
+
* BEST EFFORT AND SILENT. This runs once per mirror at first use, and its whole job is to
|
|
47
|
+
* stop `~/.aragon-agent/jobs/` from growing without bound; a directory that cannot be read
|
|
48
|
+
* is not a reason to refuse to start. `jobsRoot` MUST be the directory this module owns -
|
|
49
|
+
* see `JobLogMirrorOptions.jobsRoot` for what deriving it cost.
|
|
34
50
|
*/
|
|
35
51
|
export function pruneJobLogSessions(jobsRoot) {
|
|
36
52
|
try {
|
|
@@ -41,14 +57,16 @@ export function pruneJobLogSessions(jobsRoot) {
|
|
|
41
57
|
.map((entry) => {
|
|
42
58
|
const path = join(jobsRoot, entry.name);
|
|
43
59
|
try {
|
|
44
|
-
return { path, mtime: statSync(path).mtimeMs };
|
|
60
|
+
return { path, mtime: statSync(path).mtimeMs, prunable: isMirrorDirectory(path) };
|
|
45
61
|
}
|
|
46
62
|
catch {
|
|
47
|
-
return { path, mtime: 0 };
|
|
63
|
+
return { path, mtime: 0, prunable: false };
|
|
48
64
|
}
|
|
49
65
|
})
|
|
50
66
|
.sort((a, b) => b.mtime - a.mtime);
|
|
51
67
|
for (const stale of dirs.slice(PROC_LIMITS.mirrorRetainDirs)) {
|
|
68
|
+
if (!stale.prunable)
|
|
69
|
+
continue;
|
|
52
70
|
try {
|
|
53
71
|
rmSync(stale.path, { recursive: true, force: true });
|
|
54
72
|
}
|
|
@@ -107,7 +125,11 @@ export class JobLogMirror {
|
|
|
107
125
|
this.files.set(jobId, { path, bytes, capped: bytes >= this.maxBytes() });
|
|
108
126
|
if (!this.pruned) {
|
|
109
127
|
this.pruned = true;
|
|
110
|
-
|
|
128
|
+
// ONLY THE ROOT THE CALLER NAMED. See `JobLogMirrorOptions.jobsRoot` for why
|
|
129
|
+
// `dirname(root())` is not a substitute for it.
|
|
130
|
+
const jobsRoot = this.opts.jobsRoot?.();
|
|
131
|
+
if (jobsRoot !== undefined)
|
|
132
|
+
pruneJobLogSessions(jobsRoot);
|
|
111
133
|
}
|
|
112
134
|
}
|
|
113
135
|
catch (err) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask the child's output streams whether anything is still arriving (W1,
|
|
3
|
+
* defence 1).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS AT ALL, AND THE RELEASE-GATE FAILURE THAT PROVED IT.
|
|
6
|
+
*
|
|
7
|
+
* `bash` settles on `'close'`, which fires only once every stdio stream is
|
|
8
|
+
* closed - and a detached grandchild that inherited the pipe keeps it open after
|
|
9
|
+
* the shell is long gone, so the promise had a path that never settled. The
|
|
10
|
+
* drain window (`PROC_LIMITS.drainMs`) closes that: a quarter of a second after
|
|
11
|
+
* `'exit'`, settle anyway and stamp
|
|
12
|
+
*
|
|
13
|
+
* [note] the command exited but a background child is still holding its
|
|
14
|
+
* output stream.
|
|
15
|
+
*
|
|
16
|
+
* The first build decided that sentence by TIMER EXPIRY - whatever had not
|
|
17
|
+
* settled when the window closed was called a held pipe. That is a claim about
|
|
18
|
+
* STATE inferred from a clock, and under load the two disagree. The release gate
|
|
19
|
+
* caught it on `tool-output-store.test.ts` AC-30 ("`bash` returns the same bytes
|
|
20
|
+
* with and without a recorder"): the second call of `node -e
|
|
21
|
+
* "process.stdout.write(...)"` returned the note for a child that had printed
|
|
22
|
+
* two lines and exited immediately. The pipe was closed microseconds after
|
|
23
|
+
* `'exit'`; the event loop was busy enough (four workers, 400 files, ~200 s of
|
|
24
|
+
* transforms behind it) that the timers phase came round before libuv delivered
|
|
25
|
+
* the close notification, so a 250 ms timer won a race it was never meant to
|
|
26
|
+
* win - and the model was told a background child was holding its output stream
|
|
27
|
+
* when nothing was.
|
|
28
|
+
*
|
|
29
|
+
* Hence: READ THE STREAMS, DO NOT GUESS FROM THE CLOCK. A `Readable` has ended
|
|
30
|
+
* once it has emitted `'end'` (`readableEnded`) or been destroyed. While either
|
|
31
|
+
* child descriptor is still open the sentence above is literally true; the
|
|
32
|
+
* moment both have ended it is false no matter what a timer says. Callers use
|
|
33
|
+
* this to decide the WORDING (note or not) and, together with the `'end'`
|
|
34
|
+
* events, to settle the ordinary path without waiting for `'close'` at all.
|
|
35
|
+
*
|
|
36
|
+
* ASCII ONLY: `src/proc/**` is inside the glyph scanner's scope.
|
|
37
|
+
*/
|
|
38
|
+
import type { Readable } from 'node:stream';
|
|
39
|
+
/**
|
|
40
|
+
* Whether one child descriptor can still deliver bytes.
|
|
41
|
+
*
|
|
42
|
+
* A stream that has been destroyed is finished even when `readableEnded` is
|
|
43
|
+
* still false - that is the state a killed child's pipes end up in.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isOutputStreamOpen(stream: Readable | null | undefined): boolean;
|
|
46
|
+
/** Whether EITHER child descriptor can still deliver bytes. */
|
|
47
|
+
export declare function hasOpenOutputStreams(streams: {
|
|
48
|
+
readonly stdout: Readable | null;
|
|
49
|
+
readonly stderr: Readable | null;
|
|
50
|
+
}): boolean;
|