@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
|
@@ -21,6 +21,19 @@ export declare const LOG_CONFIG_SET_KEYS: readonly ["log.level", "log.toFile", "
|
|
|
21
21
|
* `Set team.enabled = false`.
|
|
22
22
|
*/
|
|
23
23
|
export declare const TEAM_CONFIG_SET_KEYS: readonly ["team.enabled", "team.maxSubagents", "team.maxConcurrent", "team.subagentTimeoutMs", "team.dispatchTimeoutMs", "team.maxTurnsPerSubagent", "team.overseer", "team.overseerIntervalMs", "team.model", "team.modelProfileId"];
|
|
24
|
+
/**
|
|
25
|
+
* Config keys under `jobs.` that `aragon config set` accepts
|
|
26
|
+
* (long-task-jobs §6.5).
|
|
27
|
+
*
|
|
28
|
+
* THE FIRST NESTED SECTION WHOSE KEYS ARE ALL WRITABLE, so it is also the first
|
|
29
|
+
* one where `JOBS_CONFIG_SET_KEYS ⊆ CONFIG_SET_KEYS` needs its own assertion:
|
|
30
|
+
* `__tests__/config.test.ts`'s static guard matches LITERAL keys in `cli.tsx`, so
|
|
31
|
+
* a set that is spread in is invisible to it (RV-P2-8).
|
|
32
|
+
*
|
|
33
|
+
* Every key is ONE level deep on purpose — `runConfigGet` splits on a single dot,
|
|
34
|
+
* so `jobs.waitSeconds` resolves and `jobs.mirror.enabled` would not.
|
|
35
|
+
*/
|
|
36
|
+
export declare const JOBS_CONFIG_SET_KEYS: readonly ["jobs.enabled", "jobs.waitSeconds", "jobs.stallSeconds", "jobs.persistLog", "jobs.noticeSteer"];
|
|
24
37
|
/**
|
|
25
38
|
* Config keys under `retry.` that `aragon config set` accepts
|
|
26
39
|
* (llm-api-retry-backoff §8.2).
|
|
@@ -81,6 +94,20 @@ export declare const COMPACTION_CONFIG_SET_KEYS: readonly ["compaction.enabled",
|
|
|
81
94
|
* included.
|
|
82
95
|
*/
|
|
83
96
|
export declare function applyLogConfigSet(key: string, value: string): Partial<PersistedConfig> | null;
|
|
97
|
+
/**
|
|
98
|
+
* Translate a `jobs.*` dotted key into a patch, or `null` for anything else.
|
|
99
|
+
*
|
|
100
|
+
* THE TEMPLATE IS `applyTeamConfigSet`, NOT an `applyBashConfigSet` — no such
|
|
101
|
+
* function exists, and `bash.*` is not `config set`-able to this day (RV-P2-8).
|
|
102
|
+
* Every numeric key routes through `clampJobsConfig` rather than a local
|
|
103
|
+
* coercion, so the caller echoes the STORED value and an out-of-range input is
|
|
104
|
+
* visible: `config set jobs.waitSeconds 1` prints `5`.
|
|
105
|
+
*
|
|
106
|
+
* The patch is a PARTIAL section, which is only safe because
|
|
107
|
+
* `updatePersistedConfig` deep-merges `jobs` — the sixth time that trap is
|
|
108
|
+
* recorded in `store.ts`.
|
|
109
|
+
*/
|
|
110
|
+
export declare function applyJobsConfigSet(key: string, value: string): Partial<PersistedConfig> | null;
|
|
84
111
|
/**
|
|
85
112
|
* Translate a `team.*` dotted key into a patch, or `null` for anything else.
|
|
86
113
|
*
|
|
@@ -14,7 +14,7 @@ import { getConfigBackupPath, getConfigPath, getHomeRoot, getLogsDir, getPromptH
|
|
|
14
14
|
import { getHomeMigrationBreadcrumbPath } from './migrate-home.js';
|
|
15
15
|
import { loadPersistedConfig, updatePersistedConfig } from './store.js';
|
|
16
16
|
import { normalizeModelProfiles, validateModelProfiles } from './model-profiles.js';
|
|
17
|
-
import { clampFastConfig, clampRetryConfig, clampCompactionConfig, clampTeamConfig, parseThresholdInput, clampUpdateConfig, coercePositiveInt, maskSecret, DEFAULT_FAST_CONFIG, DEFAULT_RETRY_CONFIG, DEFAULT_TEAM_CONFIG, DEFAULT_COMPACTION_CONFIG, DEFAULT_UPDATE_CONFIG, } from './schema.js';
|
|
17
|
+
import { clampFastConfig, clampRetryConfig, clampCompactionConfig, clampTeamConfig, parseThresholdInput, clampUpdateConfig, coercePositiveInt, maskSecret, DEFAULT_FAST_CONFIG, DEFAULT_RETRY_CONFIG, DEFAULT_TEAM_CONFIG, DEFAULT_COMPACTION_CONFIG, DEFAULT_UPDATE_CONFIG, DEFAULT_JOBS_CONFIG, clampJobsConfig, } from './schema.js';
|
|
18
18
|
import { clampLogLevel } from '../logging/levels.js';
|
|
19
19
|
/** Config keys under `log.` that `aragon config set` accepts. */
|
|
20
20
|
export const LOG_CONFIG_SET_KEYS = [
|
|
@@ -48,6 +48,25 @@ export const TEAM_CONFIG_SET_KEYS = [
|
|
|
48
48
|
'team.model',
|
|
49
49
|
'team.modelProfileId',
|
|
50
50
|
];
|
|
51
|
+
/**
|
|
52
|
+
* Config keys under `jobs.` that `aragon config set` accepts
|
|
53
|
+
* (long-task-jobs §6.5).
|
|
54
|
+
*
|
|
55
|
+
* THE FIRST NESTED SECTION WHOSE KEYS ARE ALL WRITABLE, so it is also the first
|
|
56
|
+
* one where `JOBS_CONFIG_SET_KEYS ⊆ CONFIG_SET_KEYS` needs its own assertion:
|
|
57
|
+
* `__tests__/config.test.ts`'s static guard matches LITERAL keys in `cli.tsx`, so
|
|
58
|
+
* a set that is spread in is invisible to it (RV-P2-8).
|
|
59
|
+
*
|
|
60
|
+
* Every key is ONE level deep on purpose — `runConfigGet` splits on a single dot,
|
|
61
|
+
* so `jobs.waitSeconds` resolves and `jobs.mirror.enabled` would not.
|
|
62
|
+
*/
|
|
63
|
+
export const JOBS_CONFIG_SET_KEYS = [
|
|
64
|
+
'jobs.enabled',
|
|
65
|
+
'jobs.waitSeconds',
|
|
66
|
+
'jobs.stallSeconds',
|
|
67
|
+
'jobs.persistLog',
|
|
68
|
+
'jobs.noticeSteer',
|
|
69
|
+
];
|
|
51
70
|
/**
|
|
52
71
|
* Config keys under `retry.` that `aragon config set` accepts
|
|
53
72
|
* (llm-api-retry-backoff §8.2).
|
|
@@ -166,6 +185,40 @@ export function applyLogConfigSet(key, value) {
|
|
|
166
185
|
return null;
|
|
167
186
|
}
|
|
168
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Translate a `jobs.*` dotted key into a patch, or `null` for anything else.
|
|
190
|
+
*
|
|
191
|
+
* THE TEMPLATE IS `applyTeamConfigSet`, NOT an `applyBashConfigSet` — no such
|
|
192
|
+
* function exists, and `bash.*` is not `config set`-able to this day (RV-P2-8).
|
|
193
|
+
* Every numeric key routes through `clampJobsConfig` rather than a local
|
|
194
|
+
* coercion, so the caller echoes the STORED value and an out-of-range input is
|
|
195
|
+
* visible: `config set jobs.waitSeconds 1` prints `5`.
|
|
196
|
+
*
|
|
197
|
+
* The patch is a PARTIAL section, which is only safe because
|
|
198
|
+
* `updatePersistedConfig` deep-merges `jobs` — the sixth time that trap is
|
|
199
|
+
* recorded in `store.ts`.
|
|
200
|
+
*/
|
|
201
|
+
export function applyJobsConfigSet(key, value) {
|
|
202
|
+
const patch = (jobs) => ({ jobs });
|
|
203
|
+
const clampOne = (field) => {
|
|
204
|
+
const merged = clampJobsConfig({ ...DEFAULT_JOBS_CONFIG, [field]: value });
|
|
205
|
+
return { [field]: merged[field] };
|
|
206
|
+
};
|
|
207
|
+
switch (key) {
|
|
208
|
+
case 'jobs.enabled':
|
|
209
|
+
return patch({ enabled: isTrue(value) });
|
|
210
|
+
case 'jobs.waitSeconds':
|
|
211
|
+
return patch(clampOne('waitSeconds'));
|
|
212
|
+
case 'jobs.stallSeconds':
|
|
213
|
+
return patch(clampOne('stallSeconds'));
|
|
214
|
+
case 'jobs.persistLog':
|
|
215
|
+
return patch({ persistLog: isTrue(value) });
|
|
216
|
+
case 'jobs.noticeSteer':
|
|
217
|
+
return patch({ noticeSteer: isTrue(value) });
|
|
218
|
+
default:
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
169
222
|
/**
|
|
170
223
|
* Translate a `team.*` dotted key into a patch, or `null` for anything else.
|
|
171
224
|
*
|
package/dist/config/env.js
CHANGED
|
@@ -254,6 +254,44 @@ export function readEnvConfig() {
|
|
|
254
254
|
if (Object.keys(section).length > 0)
|
|
255
255
|
partial.bash = section;
|
|
256
256
|
}
|
|
257
|
+
const jobsEnabled = process.env.ARAGON_JOBS_ENABLED?.trim().toLowerCase();
|
|
258
|
+
const jobsWait = process.env.ARAGON_JOBS_WAIT_SECONDS?.trim();
|
|
259
|
+
const jobsStall = process.env.ARAGON_JOBS_STALL_SECONDS?.trim();
|
|
260
|
+
const jobsPersist = process.env.ARAGON_JOBS_PERSIST_LOG?.trim().toLowerCase();
|
|
261
|
+
const jobsSteer = process.env.ARAGON_JOBS_NOTICE_STEER?.trim().toLowerCase();
|
|
262
|
+
if ((jobsEnabled !== undefined && jobsEnabled.length > 0) ||
|
|
263
|
+
(jobsWait !== undefined && jobsWait.length > 0) ||
|
|
264
|
+
(jobsStall !== undefined && jobsStall.length > 0) ||
|
|
265
|
+
(jobsPersist !== undefined && jobsPersist.length > 0) ||
|
|
266
|
+
(jobsSteer !== undefined && jobsSteer.length > 0)) {
|
|
267
|
+
const section = {};
|
|
268
|
+
// ONE TRUTHINESS LADDER FOR ALL THREE BOOLEANS, because three slightly
|
|
269
|
+
// different spellings of "is this true" is how `ARAGON_JOBS_ENABLED=off`
|
|
270
|
+
// ends up enabling the feature.
|
|
271
|
+
const flag = (value) => value === '1' || value === 'true' || value === 'on' || value === 'yes';
|
|
272
|
+
if (jobsEnabled !== undefined && jobsEnabled.length > 0)
|
|
273
|
+
section.enabled = flag(jobsEnabled);
|
|
274
|
+
if (jobsPersist !== undefined && jobsPersist.length > 0)
|
|
275
|
+
section.persistLog = flag(jobsPersist);
|
|
276
|
+
if (jobsSteer !== undefined && jobsSteer.length > 0)
|
|
277
|
+
section.noticeSteer = flag(jobsSteer);
|
|
278
|
+
if (jobsWait !== undefined && jobsWait.length > 0) {
|
|
279
|
+
const parsed = Number.parseInt(jobsWait, 10);
|
|
280
|
+
// LEFT ABSENT WHEN UNPARSEABLE, so `clampJobsConfig` supplies the default
|
|
281
|
+
// rather than an env var that silently means 0.
|
|
282
|
+
if (Number.isFinite(parsed))
|
|
283
|
+
section.waitSeconds = parsed;
|
|
284
|
+
}
|
|
285
|
+
if (jobsStall !== undefined && jobsStall.length > 0) {
|
|
286
|
+
const parsed = Number.parseInt(jobsStall, 10);
|
|
287
|
+
if (Number.isFinite(parsed))
|
|
288
|
+
section.stallSeconds = parsed;
|
|
289
|
+
}
|
|
290
|
+
// A PARTIAL section, safe only because every consumer deep-merges it - the
|
|
291
|
+
// same contract `partial.skills` / `partial.log` / `partial.team` rely on.
|
|
292
|
+
if (Object.keys(section).length > 0)
|
|
293
|
+
partial.jobs = section;
|
|
294
|
+
}
|
|
257
295
|
const retry = process.env.ARAGON_RETRY?.trim().toLowerCase();
|
|
258
296
|
const retryMax = process.env.ARAGON_RETRY_MAX?.trim();
|
|
259
297
|
if ((retry !== undefined && retry.length > 0) || (retryMax !== undefined && retryMax.length > 0)) {
|
package/dist/config/load.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* legitimately long single tool run can never be watchdog-killed.
|
|
7
7
|
*/
|
|
8
8
|
import process from 'node:process';
|
|
9
|
-
import {
|
|
9
|
+
import { JOB_WAIT_MARGIN_MS } from '../proc/limits.js';
|
|
10
|
+
import { DEFAULT_CONFIG, DEFAULT_SCROLL_MODE, IDLE_TIMEOUT_MARGIN_MS, clampAskRounds, clampDensity, clampHumanTimeout, clampCompactionConfig, clampLogConfig, clampFastConfig, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampBashConfig, clampJobsConfig, clampUpdateConfig, clampTheme, clampThinkingLevel, clampContextWindow, clampMaxTokens, clampMaxRenderInterval, clampScrollMode, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAutoToken, isSkillsToolPolicyMode, legacyTeamCeilingPatch, parseThresholdInput, } from './schema.js';
|
|
10
11
|
import { loadDotenv, readEnvConfig, readEnvDisabledSkills } from './env.js';
|
|
11
12
|
import { readConfigFile, updatePersistedConfig } from './store.js';
|
|
12
13
|
import { projectModelProfiles, resolveModelProfileState, resolveModelRoleKey, } from './model-profile-resolution.js';
|
|
@@ -182,6 +183,23 @@ function resolveBashConfig(env, file) {
|
|
|
182
183
|
...(env.bash ?? {}),
|
|
183
184
|
});
|
|
184
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Merge the `jobs` section across the three layers that supply it
|
|
188
|
+
* (defaults > file > env), long-task-jobs §6.5.
|
|
189
|
+
*
|
|
190
|
+
* THREE LAYERS, NOT FOUR, and that is the same decision `resolveBashConfig`
|
|
191
|
+
* records: both switches this section carries are already reachable from
|
|
192
|
+
* `config.json` (a per-machine preference) and from the environment (the channel
|
|
193
|
+
* a container entrypoint has). A fourth spelling would be a fourth place for the
|
|
194
|
+
* answer to differ.
|
|
195
|
+
*/
|
|
196
|
+
function resolveJobsConfig(env, file) {
|
|
197
|
+
return clampJobsConfig({
|
|
198
|
+
...DEFAULT_CONFIG.jobs,
|
|
199
|
+
...(file.jobs ?? {}),
|
|
200
|
+
...(env.jobs ?? {}),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
185
203
|
/**
|
|
186
204
|
* Merge the `retry` section across all four layers (defaults › file › env ›
|
|
187
205
|
* flags), llm-api-retry-backoff §6.2.
|
|
@@ -554,10 +572,32 @@ export function loadConfig(flags = {}, options = {}) {
|
|
|
554
572
|
const planModeMaxAskRounds = clampAskRounds(file.planModeMaxAskRounds, DEFAULT_CONFIG.planModeMaxAskRounds);
|
|
555
573
|
const planModeHumanTimeoutMs = clampHumanTimeout(file.planModeHumanTimeoutMs, DEFAULT_CONFIG.planModeHumanTimeoutMs);
|
|
556
574
|
const toolTimeoutMs = coercePositiveInt(pick(flags.toolTimeout, file.toolTimeoutMs), DEFAULT_CONFIG.toolTimeoutMs);
|
|
575
|
+
// THE `jobs` SECTION IS RESOLVED HERE, ABOVE THE TIMEOUT BLOCK, and the
|
|
576
|
+
// position is the whole of RV-P1-2. The design's first draft put the I-4 clamp
|
|
577
|
+
// next to the `idleTimeoutMs` invariant below, but `jobs` is parsed inside the
|
|
578
|
+
// big config literal a hundred lines further down — a reference to it here would
|
|
579
|
+
// be a use-before-declaration, and "resolve it later and patch the object" is
|
|
580
|
+
// how the invariant gets silently skipped. Resolving it first makes the clamp
|
|
581
|
+
// impossible to omit.
|
|
582
|
+
const jobs = resolveJobsConfig(env.partial, file);
|
|
557
583
|
let idleTimeoutMs = coercePositiveInt(pick(flags.idleTimeout, file.idleTimeoutMs), DEFAULT_CONFIG.idleTimeoutMs);
|
|
558
584
|
// Timeout invariant (R1): idle must be strictly greater than the tool ceiling
|
|
559
585
|
// so a long single tool run is never aborted by the idle watchdog.
|
|
560
586
|
idleTimeoutMs = Math.max(idleTimeoutMs, toolTimeoutMs + IDLE_TIMEOUT_MARGIN_MS);
|
|
587
|
+
// INVARIANT I-4 (long-task-jobs): a wait must fit inside the tool call that
|
|
588
|
+
// hosts it, `jobs.waitSeconds * 1000 + JOB_WAIT_MARGIN_MS <= toolTimeoutMs`.
|
|
589
|
+
//
|
|
590
|
+
// `Math.min`, AND THE DIRECTION IS THE POINT. The invariant above RAISES the
|
|
591
|
+
// idle window to accommodate the tool; this one LOWERS the wait to accommodate
|
|
592
|
+
// the tool. Copying `Math.max` here would produce a wait that outlives its own
|
|
593
|
+
// tool call — which is exactly the `Command timed out` failure the feature
|
|
594
|
+
// exists to remove, re-created by its own config check.
|
|
595
|
+
//
|
|
596
|
+
// With an operator-set ceiling below the margin the floor of 1s applies rather
|
|
597
|
+
// than a negative wait: the invariant wins over the minimum, because a wait
|
|
598
|
+
// longer than its host is worse than a short wait.
|
|
599
|
+
const waitCeilingSeconds = Math.floor((toolTimeoutMs - JOB_WAIT_MARGIN_MS) / 1000);
|
|
600
|
+
jobs.waitSeconds = Math.max(1, Math.min(jobs.waitSeconds, waitCeilingSeconds));
|
|
561
601
|
// Resolve API keys: config-file key wins over the provider env var.
|
|
562
602
|
const fileKeys = file.apiKeys ?? {};
|
|
563
603
|
const apiKeys = {};
|
|
@@ -687,6 +727,10 @@ export function loadConfig(flags = {}, options = {}) {
|
|
|
687
727
|
update: resolveUpdateConfig(flags, env.partial, file),
|
|
688
728
|
compaction: resolveCompactionConfig(flags, env.partial, file),
|
|
689
729
|
bash: resolveBashConfig(env.partial, file),
|
|
730
|
+
// The already-resolved-and-converged section from the timeout block above.
|
|
731
|
+
// NOT `resolveJobsConfig(...)` a second time: that would re-apply the
|
|
732
|
+
// un-converged `waitSeconds` and silently undo I-4.
|
|
733
|
+
jobs,
|
|
690
734
|
cwd,
|
|
691
735
|
color,
|
|
692
736
|
colorLevel,
|
|
@@ -730,6 +774,11 @@ function recordResolvedConfig(config) {
|
|
|
730
774
|
// trace — so "why did my failing call sit there for three minutes" (or
|
|
731
775
|
// "why did it not retry at all") has an answer after the fact.
|
|
732
776
|
retry: config.retry.enabled ? config.retry.maxRetries : 'off',
|
|
777
|
+
// The CONVERGED value, not the configured one: after I-4 they can differ, and
|
|
778
|
+
// "my 30s wait became 10s" is answerable only if the resolved number is
|
|
779
|
+
// recorded rather than the one on disk.
|
|
780
|
+
jobWaitSeconds: config.jobs.waitSeconds,
|
|
781
|
+
jobsEnabled: config.jobs.enabled,
|
|
733
782
|
hasApiKey: Boolean(config.apiKeys[config.provider]),
|
|
734
783
|
configPath: getConfigPath(),
|
|
735
784
|
});
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -795,6 +795,62 @@ export declare const DEFAULT_BASH_CONFIG: BashConfig;
|
|
|
795
795
|
* A garbage `startupSettleMs` resolves to 4000 and throws nothing.
|
|
796
796
|
*/
|
|
797
797
|
export declare function clampBashConfig(raw: unknown): BashConfig;
|
|
798
|
+
export interface JobsConfig {
|
|
799
|
+
/**
|
|
800
|
+
* Register `job_wait` / `job_list`, splice the `<background_jobs>` block,
|
|
801
|
+
* mirror job logs. Default TRUE.
|
|
802
|
+
*
|
|
803
|
+
* READ ONCE AT CONSTRUCTION, the `bash.background` / `team.enabled` rule. Its
|
|
804
|
+
* job is to be the single switch under which the tool array, the tool
|
|
805
|
+
* descriptions and the system prompt are BYTE-IDENTICAL to the pre-feature
|
|
806
|
+
* build (I-3) - which is why it is a registration switch and not a behaviour
|
|
807
|
+
* flag.
|
|
808
|
+
*/
|
|
809
|
+
enabled: boolean;
|
|
810
|
+
/**
|
|
811
|
+
* Seconds one `job_wait` blocks by default. Default 30, range 5..60.
|
|
812
|
+
*
|
|
813
|
+
* ALSO CONVERGED AGAINST THE TOOL CEILING at load time and again at call time
|
|
814
|
+
* (I-4): a wait that outlives the tool call hosting it would turn every long
|
|
815
|
+
* job into `Command timed out`, which is the defect this feature removes.
|
|
816
|
+
*/
|
|
817
|
+
waitSeconds: number;
|
|
818
|
+
/**
|
|
819
|
+
* Seconds without output before a job is reported `[stalled]`. Default 300,
|
|
820
|
+
* range 30..3600.
|
|
821
|
+
*
|
|
822
|
+
* A REPORT, NEVER AN ACTION: nothing kills or retries on this (R-13). The
|
|
823
|
+
* default is deliberately generous, because a training epoch that prints every
|
|
824
|
+
* five minutes is healthy.
|
|
825
|
+
*/
|
|
826
|
+
stallSeconds: number;
|
|
827
|
+
/**
|
|
828
|
+
* Mirror each job's full output to `<home>/jobs/<session>/<id>.log`. Default
|
|
829
|
+
* TRUE; `false` creates no directory at all, for privacy- or IO-sensitive
|
|
830
|
+
* environments (D-6).
|
|
831
|
+
*/
|
|
832
|
+
persistLog: boolean;
|
|
833
|
+
/**
|
|
834
|
+
* Whether a completion notice may STEER the running turn. Default TRUE.
|
|
835
|
+
*
|
|
836
|
+
* NOT A NOTICE SWITCH (§6.5). With this off, notices still arrive at the end of
|
|
837
|
+
* the next tool result and before the next user turn; only the active
|
|
838
|
+
* interruption stops. Turning notifications off entirely would require running
|
|
839
|
+
* no jobs.
|
|
840
|
+
*/
|
|
841
|
+
noticeSteer: boolean;
|
|
842
|
+
}
|
|
843
|
+
export declare const DEFAULT_JOBS_CONFIG: JobsConfig;
|
|
844
|
+
/**
|
|
845
|
+
* THE single gate for every `jobs` read AND write, mirroring `clampBashConfig`
|
|
846
|
+
* and the four sections before it.
|
|
847
|
+
*
|
|
848
|
+
* THE ELEVENTH SECTION, AND THE FIRST ONE THAT `config set` CAN REACH BY
|
|
849
|
+
* NESTED KEY (§6.5). Every key is a SCALAR and the section is ONE level deep,
|
|
850
|
+
* because `runConfigGet` splits on a single dot - a `jobs.mirror.enabled` would
|
|
851
|
+
* resolve to `undefined` and the setting would silently never apply.
|
|
852
|
+
*/
|
|
853
|
+
export declare function clampJobsConfig(raw: unknown): JobsConfig;
|
|
798
854
|
/**
|
|
799
855
|
* Parse `0.9`, `90%` or `90` into a ratio, or `null` when it is neither.
|
|
800
856
|
*
|
|
@@ -955,6 +1011,8 @@ export interface PersistedConfig {
|
|
|
955
1011
|
compaction: CompactionConfig;
|
|
956
1012
|
/** Background services. The TENTH nested object; scalars only (see BashConfig). */
|
|
957
1013
|
bash: BashConfig;
|
|
1014
|
+
/** Background jobs and bounded waiting. The ELEVENTH; scalars only (see JobsConfig). */
|
|
1015
|
+
jobs: JobsConfig;
|
|
958
1016
|
}
|
|
959
1017
|
export declare const DEFAULT_CONFIG: PersistedConfig;
|
|
960
1018
|
/**
|
|
@@ -1262,6 +1320,21 @@ export interface CliConfig {
|
|
|
1262
1320
|
* either of them.
|
|
1263
1321
|
*/
|
|
1264
1322
|
bash: BashConfig;
|
|
1323
|
+
/**
|
|
1324
|
+
* Background jobs and bounded waiting (long-task-jobs §6.5). The ELEVENTH
|
|
1325
|
+
* nested object; five scalar keys, one level deep.
|
|
1326
|
+
*
|
|
1327
|
+
* ON `CliConfig` AS WELL AS `PersistedConfig`, for the reason every section
|
|
1328
|
+
* above records: `AgentController` reads `config.jobs.waitSeconds` through a
|
|
1329
|
+
* closure and `job_wait` reads it again at call time, so a key persisted and
|
|
1330
|
+
* never projected would round-trip through `config set`, show up in
|
|
1331
|
+
* `config list`, and change nothing.
|
|
1332
|
+
*
|
|
1333
|
+
* ONE LEVEL DEEP IS A HARD REQUIREMENT, NOT A PREFERENCE: `runConfigGet` splits
|
|
1334
|
+
* `jobs.waitSeconds` on a single dot, so a nested sub-object would resolve to
|
|
1335
|
+
* `undefined` and report an empty value for a setting that IS on disk.
|
|
1336
|
+
*/
|
|
1337
|
+
jobs: JobsConfig;
|
|
1265
1338
|
/**
|
|
1266
1339
|
* Lifetime submit count driving the composer hint fade (`HINT_FADE_AFTER`).
|
|
1267
1340
|
*
|
package/dist/config/schema.js
CHANGED
|
@@ -930,6 +930,34 @@ export function clampBashConfig(raw) {
|
|
|
930
930
|
readyTimeoutMs: clampInt(src.readyTimeoutMs, DEFAULT_BASH_CONFIG.readyTimeoutMs, READY_TIMEOUT_RANGE),
|
|
931
931
|
};
|
|
932
932
|
}
|
|
933
|
+
export const DEFAULT_JOBS_CONFIG = {
|
|
934
|
+
enabled: true,
|
|
935
|
+
waitSeconds: 30,
|
|
936
|
+
stallSeconds: 300,
|
|
937
|
+
persistLog: true,
|
|
938
|
+
noticeSteer: true,
|
|
939
|
+
};
|
|
940
|
+
const WAIT_SECONDS_RANGE = { min: 5, max: 60 };
|
|
941
|
+
const STALL_SECONDS_RANGE = { min: 30, max: 3600 };
|
|
942
|
+
/**
|
|
943
|
+
* THE single gate for every `jobs` read AND write, mirroring `clampBashConfig`
|
|
944
|
+
* and the four sections before it.
|
|
945
|
+
*
|
|
946
|
+
* THE ELEVENTH SECTION, AND THE FIRST ONE THAT `config set` CAN REACH BY
|
|
947
|
+
* NESTED KEY (§6.5). Every key is a SCALAR and the section is ONE level deep,
|
|
948
|
+
* because `runConfigGet` splits on a single dot - a `jobs.mirror.enabled` would
|
|
949
|
+
* resolve to `undefined` and the setting would silently never apply.
|
|
950
|
+
*/
|
|
951
|
+
export function clampJobsConfig(raw) {
|
|
952
|
+
const src = (raw && typeof raw === 'object' ? raw : {});
|
|
953
|
+
return {
|
|
954
|
+
enabled: bool(src.enabled, DEFAULT_JOBS_CONFIG.enabled),
|
|
955
|
+
waitSeconds: clampInt(src.waitSeconds, DEFAULT_JOBS_CONFIG.waitSeconds, WAIT_SECONDS_RANGE),
|
|
956
|
+
stallSeconds: clampInt(src.stallSeconds, DEFAULT_JOBS_CONFIG.stallSeconds, STALL_SECONDS_RANGE),
|
|
957
|
+
persistLog: bool(src.persistLog, DEFAULT_JOBS_CONFIG.persistLog),
|
|
958
|
+
noticeSteer: bool(src.noticeSteer, DEFAULT_JOBS_CONFIG.noticeSteer),
|
|
959
|
+
};
|
|
960
|
+
}
|
|
933
961
|
/**
|
|
934
962
|
* Parse `0.9`, `90%` or `90` into a ratio, or `null` when it is neither.
|
|
935
963
|
*
|
|
@@ -1028,6 +1056,12 @@ export const DEFAULT_CONFIG = {
|
|
|
1028
1056
|
// `undefined` would arrive with background services OFF for every existing
|
|
1029
1057
|
// user — the same direction the compaction line above must never fail in.
|
|
1030
1058
|
bash: DEFAULT_BASH_CONFIG,
|
|
1059
|
+
// And one section later again, for the sixth time, and this one would fail
|
|
1060
|
+
// in the WORSE direction: every key in `jobs` is `true` or positive by
|
|
1061
|
+
// default, so a `config.json` predating this feature resolving `config.jobs`
|
|
1062
|
+
// to `undefined` would throw on `config.jobs.waitSeconds` at construction
|
|
1063
|
+
// rather than quietly disabling something.
|
|
1064
|
+
jobs: DEFAULT_JOBS_CONFIG,
|
|
1031
1065
|
};
|
|
1032
1066
|
/**
|
|
1033
1067
|
* Runtime state that used to be stored in `config.json` by mistake
|
package/dist/config/store.js
CHANGED
|
@@ -17,7 +17,7 @@ import { getConfigPath, getSessionsDir } from './app-paths.js';
|
|
|
17
17
|
import { registerProfileSecrets, registerSecretsFrom } from '../logging/secret-registry.js';
|
|
18
18
|
import { adaptLegacyConnectionPatch, ModelProfileConfigError, readModelSettingsDisk } from './model-profile-store.js';
|
|
19
19
|
import { normalizeModelProfiles } from './model-profiles.js';
|
|
20
|
-
import { clampBashConfig, CONFIG_VERSION, DEFAULT_CONFIG, clampCompactionConfig, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampContextWindow, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
|
|
20
|
+
import { clampBashConfig, clampJobsConfig, CONFIG_VERSION, DEFAULT_CONFIG, clampCompactionConfig, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampContextWindow, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
|
|
21
21
|
/**
|
|
22
22
|
* Normalize `maxTokens` for both directions of the config file.
|
|
23
23
|
*
|
|
@@ -189,6 +189,12 @@ export function normalizePersistedConfig(input) {
|
|
|
189
189
|
// and NOT merged over the defaults, which is verbatim the failure the
|
|
190
190
|
// paragraph above says this line prevents.
|
|
191
191
|
bash: clampBashConfig({ ...DEFAULT_CONFIG.bash, ...(partial.bash ?? {}) }),
|
|
192
|
+
// And the eleventh, on the same line and for the same reason. `jobs.enabled`
|
|
193
|
+
// is `true` by default, so a user whose `config.json` predates this feature
|
|
194
|
+
// must not resolve it to `undefined` — and `waitSeconds` would be `undefined`
|
|
195
|
+
// outright, which is a crash on the first `job_wait` rather than a silently
|
|
196
|
+
// wrong setting.
|
|
197
|
+
jobs: clampJobsConfig({ ...DEFAULT_CONFIG.jobs, ...(partial.jobs ?? {}) }),
|
|
192
198
|
};
|
|
193
199
|
}
|
|
194
200
|
/**
|
|
@@ -289,6 +295,13 @@ export function mergePersistedConfig(current, patch) {
|
|
|
289
295
|
// top level and write `background` absent, silently unregistering
|
|
290
296
|
// `bash_output` / `bash_kill` from the next launch onward.
|
|
291
297
|
bash: clampBashConfig({ ...current.bash, ...(patch.bash ?? {}) }),
|
|
298
|
+
// The eleventh section, and the sixth time this file records the same trap.
|
|
299
|
+
// `aragon config set jobs.waitSeconds 45` sends `{ jobs: { waitSeconds: 45 } }`;
|
|
300
|
+
// without this line the shallow top-level spread REPLACES the section and
|
|
301
|
+
// `persistLog` / `noticeSteer` / `enabled` / `stallSeconds` are written
|
|
302
|
+
// absent — with `noticeSteer`'s default masking it on the next load, so the
|
|
303
|
+
// user would never learn that their privacy switch had been turned back on.
|
|
304
|
+
jobs: clampJobsConfig({ ...current.jobs, ...(patch.jobs ?? {}) }),
|
|
292
305
|
};
|
|
293
306
|
return merged;
|
|
294
307
|
}
|
package/dist/exec/events.d.ts
CHANGED
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
*
|
|
10
10
|
* PURE TYPES AND BUILDERS. No I/O, no process access, no imports outside
|
|
11
11
|
* `node:`-free type space, so a consumer of this module cannot accidentally
|
|
12
|
-
* evaluate the runner.
|
|
13
|
-
*
|
|
12
|
+
* evaluate the runner. The one import below is a TYPE from `proc/types.ts` for
|
|
13
|
+
* the same reason: a shape, erased at compile time.
|
|
14
|
+
*/
|
|
15
|
+
import type { ServiceStatus } from '../proc/types.js';
|
|
16
|
+
/**
|
|
14
17
|
* `EXEC_SCHEMA_VERSION` appears on `system` and on `result` and NOWHERE ELSE.
|
|
15
18
|
* ADDING A FIELD OR AN EVENT TYPE NEVER BUMPS IT; renaming or removing one does.
|
|
16
19
|
* That rule only holds if consumers keep their side of it, so the README states
|
|
@@ -370,7 +373,30 @@ export interface ExecResultEvent {
|
|
|
370
373
|
todos: ExecTodoCounts | null;
|
|
371
374
|
error: ExecErrorInfo | null;
|
|
372
375
|
}
|
|
373
|
-
|
|
376
|
+
/**
|
|
377
|
+
* A background job's lifecycle (long-task-jobs §4.10).
|
|
378
|
+
*
|
|
379
|
+
* A PURE ADDITION to schema v1: a consumer that does not know this `type`
|
|
380
|
+
* ignores it, which is the forward-compatibility contract this file's header
|
|
381
|
+
* states. `progress` is emitted only when the extracted text CHANGES and at most
|
|
382
|
+
* once a second - a 40-minute training run would otherwise write thousands of
|
|
383
|
+
* lines to a machine consumer's stdout.
|
|
384
|
+
*/
|
|
385
|
+
export interface ExecJobEvent {
|
|
386
|
+
type: 'job';
|
|
387
|
+
schemaVersion: number;
|
|
388
|
+
sessionId: string;
|
|
389
|
+
subtype: 'started' | 'progress' | 'exited' | 'stalled';
|
|
390
|
+
/** `t3` / `s1`. */
|
|
391
|
+
jobId: string;
|
|
392
|
+
kind: 'service' | 'task';
|
|
393
|
+
status: ServiceStatus;
|
|
394
|
+
elapsedMs: number;
|
|
395
|
+
/** Only on `subtype === 'progress'`, and only when the text changed. */
|
|
396
|
+
progress?: string;
|
|
397
|
+
exitCode?: number | null;
|
|
398
|
+
}
|
|
399
|
+
export type ExecEvent = ExecSystemInitEvent | ExecUserEvent | ExecTurnStateEvent | ExecProgressEvent | ExecTextDeltaEvent | ExecThinkingEvent | ExecAssistantEvent | ExecToolCallEvent | ExecToolResultEvent | ExecTodoEvent | ExecTeamEvent | ExecFastReviewEvent | ExecFastTierEvent | ExecCompactionEvent | ExecRetryEvent | ExecJobEvent | ExecErrorEvent | ExecResultEvent;
|
|
374
400
|
/** Everything `system/init` needs that the runner cannot derive for itself. */
|
|
375
401
|
export interface ExecInitParams {
|
|
376
402
|
sessionId: string;
|
package/dist/exec/events.js
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* THE CONTRACT (cli-integration-surface section 5.1).
|
|
3
|
-
*
|
|
4
|
-
* ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope
|
|
5
|
-
* (`glyphs.test.ts::inScope`, R-3 / AC-25). Every string here can reach a
|
|
6
|
-
* consumer's terminal through `--output-format text` fallbacks or a stderr
|
|
7
|
-
* warning, and the whole point of the scanner is that a legacy `cmd.exe` shows
|
|
8
|
-
* mojibake for anything else.
|
|
9
|
-
*
|
|
10
|
-
* PURE TYPES AND BUILDERS. No I/O, no process access, no imports outside
|
|
11
|
-
* `node:`-free type space, so a consumer of this module cannot accidentally
|
|
12
|
-
* evaluate the runner.
|
|
13
|
-
*
|
|
14
2
|
* `EXEC_SCHEMA_VERSION` appears on `system` and on `result` and NOWHERE ELSE.
|
|
15
3
|
* ADDING A FIELD OR AN EVENT TYPE NEVER BUMPS IT; renaming or removing one does.
|
|
16
4
|
* That rule only holds if consumers keep their side of it, so the README states
|
|
@@ -53,6 +41,9 @@ export const EXEC_CAPABILITIES = [
|
|
|
53
41
|
'session-file',
|
|
54
42
|
'turn-lifecycle',
|
|
55
43
|
'execution-progress',
|
|
44
|
+
// Background job lifecycle events (long-task-jobs §4.10). A consumer that does
|
|
45
|
+
// not know the `job` type ignores it; this string is how it asks first.
|
|
46
|
+
'background-jobs',
|
|
56
47
|
];
|
|
57
48
|
export function buildInitEvent(params) {
|
|
58
49
|
return {
|
package/dist/exec/runner.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { type FollowThroughMode } from '../todo/follow-through.js';
|
|
|
20
20
|
import type { ExecEmitter } from './emitter.js';
|
|
21
21
|
import type { FastSnapshot } from '../fast/types.js';
|
|
22
22
|
import type { ExecStopReason } from './events.js';
|
|
23
|
+
import type { ProcEvent } from '../proc/types.js';
|
|
23
24
|
/**
|
|
24
25
|
* The controller surface the runner needs.
|
|
25
26
|
*
|
|
@@ -45,6 +46,14 @@ export interface ExecRunnerController extends HeadlessController {
|
|
|
45
46
|
* it simply emits no `fast_tier` events.
|
|
46
47
|
*/
|
|
47
48
|
fastSnapshot?(): FastSnapshot | null;
|
|
49
|
+
/**
|
|
50
|
+
* The CLI-local process stream, when this session supervises processes.
|
|
51
|
+
*
|
|
52
|
+
* OPTIONAL AND CALLED WITH `?.()`, exactly like the three optional streams
|
|
53
|
+
* above: this interface is minimal so a test can satisfy it with an object
|
|
54
|
+
* literal, and a host with no supervisor has nothing to report.
|
|
55
|
+
*/
|
|
56
|
+
subscribeProc?(listener: (event: ProcEvent) => void): () => void;
|
|
48
57
|
}
|
|
49
58
|
/** Where the caller's messages come from. One turn, or an NDJSON stdin stream. */
|
|
50
59
|
export interface ExecPromptSource {
|
|
@@ -97,6 +106,10 @@ export declare class ExecRunner {
|
|
|
97
106
|
private readonly progress;
|
|
98
107
|
private controller;
|
|
99
108
|
private unsubscribers;
|
|
109
|
+
/** Per-job progress throttle state: the last text emitted, and when. */
|
|
110
|
+
private readonly jobProgressAt;
|
|
111
|
+
/** Per-job `lastOutputAt` at which `stalled` was last reported. */
|
|
112
|
+
private readonly jobStalledAt;
|
|
100
113
|
private turns;
|
|
101
114
|
private readonly usage;
|
|
102
115
|
private compactionBilling;
|
|
@@ -123,6 +136,17 @@ export declare class ExecRunner {
|
|
|
123
136
|
* multiple subscribers.
|
|
124
137
|
*/
|
|
125
138
|
attach(controller: ExecRunnerController): void;
|
|
139
|
+
/**
|
|
140
|
+
* Map a supervisor event onto the `job` channel.
|
|
141
|
+
*
|
|
142
|
+
* THROTTLING IS THE WHOLE DISCIPLINE HERE. `output` is already coalesced to
|
|
143
|
+
* <=10 Hz inside the supervisor, which is still far too fast for a machine
|
|
144
|
+
* consumer reading a 40-minute training run; `progress` is therefore emitted
|
|
145
|
+
* only when the extracted TEXT CHANGES and never more than once a second. The
|
|
146
|
+
* other three subtypes are one-shot per job, so they pass straight through.
|
|
147
|
+
*/
|
|
148
|
+
private onProcEvent;
|
|
149
|
+
private emitJob;
|
|
126
150
|
/**
|
|
127
151
|
* Drop the subscriptions and the timer.
|
|
128
152
|
*
|
package/dist/exec/runner.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { formatStreamError } from '../agent/reducer.js';
|
|
18
18
|
import { setSignalTerminator } from '../logging/install.js';
|
|
19
19
|
import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow-through.js';
|
|
20
|
+
import { EXEC_SCHEMA_VERSION } from './events.js';
|
|
20
21
|
import { ExecProgressReporter } from './progress-reporter.js';
|
|
21
22
|
export function singlePrompt(text) {
|
|
22
23
|
let served = false;
|
|
@@ -44,6 +45,10 @@ export class ExecRunner {
|
|
|
44
45
|
progress;
|
|
45
46
|
controller = null;
|
|
46
47
|
unsubscribers = [];
|
|
48
|
+
/** Per-job progress throttle state: the last text emitted, and when. */
|
|
49
|
+
jobProgressAt = new Map();
|
|
50
|
+
/** Per-job `lastOutputAt` at which `stalled` was last reported. */
|
|
51
|
+
jobStalledAt = new Map();
|
|
47
52
|
turns = 0;
|
|
48
53
|
usage = { inputTokens: 0, outputTokens: 0 };
|
|
49
54
|
compactionBilling;
|
|
@@ -218,8 +223,82 @@ export class ExecRunner {
|
|
|
218
223
|
});
|
|
219
224
|
if (compaction)
|
|
220
225
|
this.unsubscribers.push(compaction);
|
|
226
|
+
// Background jobs (long-task-jobs §4.10). SUBSCRIBED HERE, IN `attach()`, NOT
|
|
227
|
+
// IN `start()`: the follow-through chain enters `run()` more than once, and a
|
|
228
|
+
// subscription made there would be re-made every time and never released —
|
|
229
|
+
// one leaked listener per turn for the life of the process.
|
|
230
|
+
//
|
|
231
|
+
// The four-stream shape is deliberate: `?:` optional call, `unsubscribers`
|
|
232
|
+
// push, `detach()` releases. Identical to `subscribeTeam` / `subscribeTodos` /
|
|
233
|
+
// `subscribeFast` / `subscribeCompaction`, so a host without a supervisor
|
|
234
|
+
// simply emits nothing.
|
|
235
|
+
const proc = controller.subscribeProc?.((event) => this.onProcEvent(event));
|
|
236
|
+
if (proc)
|
|
237
|
+
this.unsubscribers.push(proc);
|
|
221
238
|
this.installTerminator();
|
|
222
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Map a supervisor event onto the `job` channel.
|
|
242
|
+
*
|
|
243
|
+
* THROTTLING IS THE WHOLE DISCIPLINE HERE. `output` is already coalesced to
|
|
244
|
+
* <=10 Hz inside the supervisor, which is still far too fast for a machine
|
|
245
|
+
* consumer reading a 40-minute training run; `progress` is therefore emitted
|
|
246
|
+
* only when the extracted TEXT CHANGES and never more than once a second. The
|
|
247
|
+
* other three subtypes are one-shot per job, so they pass straight through.
|
|
248
|
+
*/
|
|
249
|
+
onProcEvent(event) {
|
|
250
|
+
const job = event.service;
|
|
251
|
+
if (event.type === 'started') {
|
|
252
|
+
this.emitJob(job, 'started');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (event.type === 'exited' || event.type === 'stopped') {
|
|
256
|
+
this.jobProgressAt.delete(job.id);
|
|
257
|
+
this.jobStalledAt.delete(job.id);
|
|
258
|
+
this.emitJob(job, 'exited');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// `ready` is deliberately NOT mapped onto a second `started`: a service emits
|
|
262
|
+
// both, and a consumer that counted `started` events would see two. The
|
|
263
|
+
// readiness fact is already carried by `status` on any later event, and the
|
|
264
|
+
// four subtypes this channel publishes are one-shot per job.
|
|
265
|
+
if (event.type !== 'output')
|
|
266
|
+
return;
|
|
267
|
+
if (job.stalled === true) {
|
|
268
|
+
if (this.jobStalledAt.get(job.id) === job.lastOutputAt)
|
|
269
|
+
return;
|
|
270
|
+
this.jobStalledAt.set(job.id, job.lastOutputAt);
|
|
271
|
+
this.emitJob(job, 'stalled');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const text = job.progress?.text;
|
|
275
|
+
if (text === undefined)
|
|
276
|
+
return;
|
|
277
|
+
if (this.jobProgressAt.get(job.id)?.text === text)
|
|
278
|
+
return;
|
|
279
|
+
const now = Date.now();
|
|
280
|
+
const last = this.jobProgressAt.get(job.id)?.at ?? 0;
|
|
281
|
+
// ONE SECOND IS THE FLOOR, and the first change of a job is always emitted
|
|
282
|
+
// (no previous entry), so a consumer sees the initial figure immediately.
|
|
283
|
+
if (last !== 0 && now - last < 1000)
|
|
284
|
+
return;
|
|
285
|
+
this.jobProgressAt.set(job.id, { text, at: now });
|
|
286
|
+
this.emitJob(job, 'progress', text);
|
|
287
|
+
}
|
|
288
|
+
emitJob(job, subtype, progress) {
|
|
289
|
+
this.emit({
|
|
290
|
+
type: 'job',
|
|
291
|
+
schemaVersion: EXEC_SCHEMA_VERSION,
|
|
292
|
+
sessionId: this.sessionId,
|
|
293
|
+
subtype,
|
|
294
|
+
jobId: job.id,
|
|
295
|
+
kind: job.kind,
|
|
296
|
+
status: job.status,
|
|
297
|
+
elapsedMs: Math.max(0, (job.endedAt ?? Date.now()) - job.startedAt),
|
|
298
|
+
...(progress !== undefined ? { progress } : {}),
|
|
299
|
+
...(subtype === 'exited' ? { exitCode: job.exitCode } : {}),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
223
302
|
/**
|
|
224
303
|
* Drop the subscriptions and the timer.
|
|
225
304
|
*
|