@aragon-agent/cli 0.6.46 → 0.6.47
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 +15 -0
- package/README.md +53 -6
- package/dist/agent/coalesce.d.ts +4 -0
- package/dist/agent/coalesce.js +13 -0
- package/dist/agent/controller.d.ts +21 -0
- package/dist/agent/controller.js +56 -3
- package/dist/agent/reducer.d.ts +20 -0
- package/dist/agent/reducer.js +31 -2
- package/dist/agent/user-attention.d.ts +17 -0
- package/dist/agent/user-attention.js +32 -0
- package/dist/cli.js +2 -3
- package/dist/config/load.js +4 -25
- package/dist/config/schema.d.ts +4 -4
- package/dist/config/schema.js +7 -1
- package/dist/proc/limits.d.ts +27 -13
- package/dist/proc/limits.js +27 -13
- package/dist/proc/progress.d.ts +7 -0
- package/dist/proc/progress.js +35 -7
- package/dist/proc/prompt.js +7 -4
- package/dist/proc/supervisor.js +7 -1
- package/dist/proc/types.d.ts +6 -0
- package/dist/proc/wait-plan.d.ts +39 -0
- package/dist/proc/wait-plan.js +77 -0
- package/dist/proc/wait-view.d.ts +65 -0
- package/dist/proc/wait-view.js +66 -0
- package/dist/proc/wait.d.ts +36 -3
- package/dist/proc/wait.js +67 -4
- package/dist/session/checkpoint.js +2 -0
- package/dist/session/persist.js +19 -2
- package/dist/session/validate-session.js +3 -0
- package/dist/tools/job-tools.d.ts +13 -2
- package/dist/tools/job-tools.js +156 -33
- package/dist/ui/ActivityLine.js +5 -2
- package/dist/ui/App.js +34 -0
- package/dist/ui/StatusBar.d.ts +1 -0
- package/dist/ui/Transcript.js +7 -0
- package/dist/ui/entries/ToolCard.js +11 -2
- package/dist/ui/entries/WaitCard.d.ts +37 -0
- package/dist/ui/entries/WaitCard.js +92 -0
- package/dist/ui/interaction-copy.d.ts +2 -0
- package/dist/ui/interaction-copy.js +3 -0
- package/dist/ui/layout/status-layout.d.ts +1 -0
- package/dist/ui/layout/status-layout.js +10 -1
- package/dist/ui/layout/virtual-window.js +7 -1
- package/dist/ui/layout/wait-bar.d.ts +34 -0
- package/dist/ui/layout/wait-bar.js +48 -0
- package/dist/ui/run-status-row.d.ts +4 -2
- package/dist/ui/scrollback/live-preview.js +12 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,21 @@ were written.
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
|
+
- **Long waits: `job_wait` now waits up to 20 minutes, and the wait looks like it.**
|
|
12
|
+
`timeoutSeconds` takes any integer from 5 to 1200 (it used to stop at 60, and at 150 with the
|
|
13
|
+
default `toolTimeoutMs`, because the tool ceiling silently pulled it down). `job_wait` gets its own
|
|
14
|
+
executor ceiling (1230 s) and pauses the idle watchdog for the whole wait, so `toolTimeoutMs` no
|
|
15
|
+
longer limits it and `jobs.waitSeconds` accepts 5-1200. **Typing a message ends the wait at once**
|
|
16
|
+
(result line `[woke] ...`, the job keeps running, no `Esc` needed); reviewer notes and completion
|
|
17
|
+
notices do not. While waiting, the tool card is replaced by a fixed five-row card - a progress bar
|
|
18
|
+
(the job's own progress when it prints any, otherwise the wait window), both clocks, the job's
|
|
19
|
+
newest output with "N s ago", a calm "not a failure" note after `jobs.stallSeconds` of silence, and
|
|
20
|
+
a hint that a message ends the wait - and the status bar reads `Waiting on t3 4m12s/10m00s`. New:
|
|
21
|
+
`job: "any"` (return on the first job to end), a `[next]` suggestion with a `timeoutSeconds` (and the
|
|
22
|
+
job's ETA) so the model prefers one long wait to many short ones, and `[note]` when a request is
|
|
23
|
+
clamped to the ceiling. No new config keys; `jobs.enabled=false` is byte-identical to before.
|
|
24
|
+
Honest limit: while a wait runs the idle check is off, so a stuck wait is ended by its own 20-minute
|
|
25
|
+
bound, the executor backstop, or you.
|
|
11
26
|
- **Governance mode - a multi-agent "legion" collaboration posture.** `Shift+Tab` (and `Ctrl+N`)
|
|
12
27
|
now cycle `BUILD -> PLAN -> GOVERN -> PLAN+GOVERN -> BUILD`; `/gov` goes to any of them
|
|
13
28
|
directly. In `GOVERN` the main agent becomes an **Orchestrator** that cannot write files or run
|
package/README.md
CHANGED
|
@@ -2068,13 +2068,60 @@ longer reads "not ready yet, still coming up" for its first minute.
|
|
|
2068
2068
|
Two tools manage them:
|
|
2069
2069
|
|
|
2070
2070
|
- `job_wait({ job, timeoutSeconds?, since? })` — block up to a bound (default 30s,
|
|
2071
|
-
at most
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2071
|
+
**at most 20 minutes**: `timeoutSeconds` takes any integer from 5 to 1200) and
|
|
2072
|
+
return one status line, one progress line, at most six new output rows and a
|
|
2073
|
+
cursor. It returns immediately if the job already finished, and it also ends
|
|
2074
|
+
the moment **you send a message** (see below). `job` is an id, `"all"` (until
|
|
2075
|
+
every live job ends) or `"any"` (until the first one ends). This is the point
|
|
2076
|
+
of the feature: waiting 30 seconds used to cost six `bash_output` calls, six
|
|
2077
|
+
model turns and six 200-row tails; it now costs one call and about 150 tokens,
|
|
2078
|
+
and a 40-minute training run costs a handful of calls instead of dozens.
|
|
2076
2079
|
- `job_list()` — one line per job: id, kind, status, elapsed, progress.
|
|
2077
2080
|
|
|
2081
|
+
### Long waits
|
|
2082
|
+
|
|
2083
|
+
A wait can now last up to 20 minutes, so three things keep it safe and visible:
|
|
2084
|
+
|
|
2085
|
+
- **You can always interrupt it by typing.** Send a message while the agent is
|
|
2086
|
+
waiting and the wait ends at once; the result says `[woke] you sent a message...`,
|
|
2087
|
+
your message is delivered next, and the job keeps running. You do not need
|
|
2088
|
+
`Esc`, which would stop the whole run. (Automatic notes, such as a reviewer's
|
|
2089
|
+
or a completion notice, do not end a wait. If the agent had queued more tool
|
|
2090
|
+
calls in the same step, the engine skips them when your message arrives, as it
|
|
2091
|
+
does for any message sent mid-step.)
|
|
2092
|
+
- **The wait is drawn as a five-row card** instead of a bare "running":
|
|
2093
|
+
|
|
2094
|
+
```
|
|
2095
|
+
⠹ Waiting for t3 python train.py --epochs 10 4m12s of 10m00s
|
|
2096
|
+
│ ████████░░░░░░░░░░░░░░░░░░░░ 30% epoch 3/10 ETA 13m
|
|
2097
|
+
│ job running 42m04s · this wait ends in 5m48s, or sooner if the job finishes
|
|
2098
|
+
│ step 912/1000 loss 0.4098 3s ago
|
|
2099
|
+
│ Send a message to end the wait early · Esc x2 stops the whole run
|
|
2100
|
+
```
|
|
2101
|
+
|
|
2102
|
+
The bar shows the job's own progress when it prints any, and otherwise the
|
|
2103
|
+
wait window itself (the words say which). After `jobs.stallSeconds` of silence
|
|
2104
|
+
the output row becomes a calm
|
|
2105
|
+
`quiet for 6m02s - the job is still alive; this is not a failure`. The status
|
|
2106
|
+
bar and activity line carry the same clock (`Waiting on t3 4m12s/10m00s`).
|
|
2107
|
+
The card is always five rows, uses ASCII glyphs on terminals without Unicode,
|
|
2108
|
+
and keeps its numbers moving under reduced motion. It disappears when the wait
|
|
2109
|
+
ends and leaves the usual one-line history row.
|
|
2110
|
+
- **The idle watchdog is paused for the length of the wait**, and `toolTimeoutMs`
|
|
2111
|
+
no longer limits it: `job_wait` gets its own executor ceiling (20 minutes plus
|
|
2112
|
+
30 seconds). The honest cost is that a wait which is itself stuck is only
|
|
2113
|
+
caught by that ceiling and by you (`Esc x2`), not by the 3.5-minute idle check.
|
|
2114
|
+
|
|
2115
|
+
Results suggest the next wait: when the job reports an ETA or has been running for
|
|
2116
|
+
a while, the `[next]` line carries a `timeoutSeconds` worth using (for example
|
|
2117
|
+
`timeoutSeconds: 900 ... (ETA about 13m)`) so the agent prefers one long wait to
|
|
2118
|
+
many short ones. It is advice, never enforced. A request over 1200 seconds is
|
|
2119
|
+
clamped and says so (`[note] timeoutSeconds 3600 exceeds the 1200s ceiling`).
|
|
2120
|
+
|
|
2121
|
+
Cost note: a wait longer than your provider's prompt-cache lifetime (often five
|
|
2122
|
+
minutes) means the next request re-reads the context uncached. That is still far
|
|
2123
|
+
cheaper than the dozens of model turns short polling would have spent.
|
|
2124
|
+
|
|
2078
2125
|
Nothing else tells the model a job finished. When one ends while it is busy, a
|
|
2079
2126
|
`<job_notices>` block appears at the end of the next tool result:
|
|
2080
2127
|
|
|
@@ -2114,7 +2161,7 @@ to use it — `grep` and `read_file` already exist.
|
|
|
2114
2161
|
| Key | Default | Meaning |
|
|
2115
2162
|
|---|---|---|
|
|
2116
2163
|
| `jobs.enabled` | `true` | Register `job_wait`/`job_list`, splice the prompt block. `false` restores the pre-feature build byte for byte. |
|
|
2117
|
-
| `jobs.waitSeconds` | `30` | Default bound for one wait, 5–
|
|
2164
|
+
| `jobs.waitSeconds` | `30` | Default bound for one wait, 5–1200. No longer lowered by `toolTimeoutMs`. |
|
|
2118
2165
|
| `jobs.stallSeconds` | `300` | Silence before `[stalled]`, 30–3600. |
|
|
2119
2166
|
| `jobs.persistLog` | `true` | Mirror each job's output to disk. `false` creates no directory at all. |
|
|
2120
2167
|
| `jobs.noticeSteer` | `true` | Whether a completion notice may interrupt a running turn. `false` keeps the notice for the next tool result or the next user turn — it is **not** a notification off switch. |
|
package/dist/agent/coalesce.d.ts
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
* concatenating, and a differing id starts a new pending action because merging
|
|
17
17
|
* across ids would paint one command's output onto another's card.
|
|
18
18
|
*
|
|
19
|
+
* A FOURTH ARRIVED WITH THE LONG WAIT (extended-job-wait RV-6): `waitUpdate`
|
|
20
|
+
* carries the WHOLE view, so it merges exactly like the tool tail - same
|
|
21
|
+
* `toolCallId` keeps the last, a different id or any other action flushes first.
|
|
22
|
+
*
|
|
19
23
|
* ONE PENDING SLOT, DISCRIMINATED, NEVER TWO (P1-5). A second, independent slot
|
|
20
24
|
* for the object-shaped payload would let a chunk overtake a token and reorder
|
|
21
25
|
* the two streams — exactly what I-L4-1 (`ui/render-governor.ts:14-19`) forbids:
|
package/dist/agent/coalesce.js
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
* concatenating, and a differing id starts a new pending action because merging
|
|
17
17
|
* across ids would paint one command's output onto another's card.
|
|
18
18
|
*
|
|
19
|
+
* A FOURTH ARRIVED WITH THE LONG WAIT (extended-job-wait RV-6): `waitUpdate`
|
|
20
|
+
* carries the WHOLE view, so it merges exactly like the tool tail - same
|
|
21
|
+
* `toolCallId` keeps the last, a different id or any other action flushes first.
|
|
22
|
+
*
|
|
19
23
|
* ONE PENDING SLOT, DISCRIMINATED, NEVER TWO (P1-5). A second, independent slot
|
|
20
24
|
* for the object-shaped payload would let a chunk overtake a token and reorder
|
|
21
25
|
* the two streams — exactly what I-L4-1 (`ui/render-governor.ts:14-19`) forbids:
|
|
@@ -28,6 +32,9 @@ function isTextDelta(action) {
|
|
|
28
32
|
function isToolOutputDelta(action) {
|
|
29
33
|
return action.type === 'toolOutputDelta';
|
|
30
34
|
}
|
|
35
|
+
function isWaitUpdate(action) {
|
|
36
|
+
return action.type === 'waitUpdate';
|
|
37
|
+
}
|
|
31
38
|
/** Merge consecutive same-kind deltas; flush on kind change or a non-delta. */
|
|
32
39
|
export function mergeDeltas(actions) {
|
|
33
40
|
const out = [];
|
|
@@ -70,6 +77,12 @@ export function mergeDeltas(actions) {
|
|
|
70
77
|
// increment, so merging means replacing (D-30).
|
|
71
78
|
pending = { kind: 'tool', action };
|
|
72
79
|
}
|
|
80
|
+
else if (isWaitUpdate(action)) {
|
|
81
|
+
const merges = prev !== null && prev.kind === 'wait' && prev.action.toolCallId === action.toolCallId;
|
|
82
|
+
if (!merges)
|
|
83
|
+
flush();
|
|
84
|
+
pending = { kind: 'wait', action };
|
|
85
|
+
}
|
|
73
86
|
else {
|
|
74
87
|
flush();
|
|
75
88
|
out.push(action);
|
|
@@ -28,6 +28,7 @@ import type { SubagentModelResolution } from '../team/model.js';
|
|
|
28
28
|
import type { ForcedTeamState } from '../team/forced-mode.js';
|
|
29
29
|
import type { TeamEvent, TeamSnapshot } from '../team/types.js';
|
|
30
30
|
import { ProcSupervisor } from '../proc/supervisor.js';
|
|
31
|
+
import type { WaitEvent } from '../proc/wait-view.js';
|
|
31
32
|
import type { ProcEventListener, ServiceSnapshot } from '../proc/types.js';
|
|
32
33
|
import { type FastStatus } from '../fast/wiring.js';
|
|
33
34
|
import type { FastEventListener, FastSnapshot } from '../fast/types.js';
|
|
@@ -350,6 +351,10 @@ export declare class AgentController {
|
|
|
350
351
|
*/
|
|
351
352
|
private readonly toolOutputs;
|
|
352
353
|
private readonly outputListeners;
|
|
354
|
+
/** `job_wait` live-view listeners (extended-job-wait); see `subscribeWait`. */
|
|
355
|
+
private readonly waitListeners;
|
|
356
|
+
/** "The user just spoke": ends a running `job_wait` early. */
|
|
357
|
+
private readonly userAttention;
|
|
353
358
|
private readonly fast;
|
|
354
359
|
private readonly compaction;
|
|
355
360
|
private settingsRevision;
|
|
@@ -768,6 +773,12 @@ export declare class AgentController {
|
|
|
768
773
|
* rather than a counter, so a nested call's `finally` re-arms the watchdog
|
|
769
774
|
* while the outer wait is still going. That is why a child's `--confirm`
|
|
770
775
|
* dialog does NOT route through this (see the constructor's `humanQueue`).
|
|
776
|
+
*
|
|
777
|
+
* `job_wait` ALSO DEPENDS ON THAT (extended-job-wait I-3): it holds the pause for
|
|
778
|
+
* up to 20 minutes and is safe only because the engine runs a batch's tools
|
|
779
|
+
* one at a time. If tool execution ever becomes parallel, make this a COUNTER
|
|
780
|
+
* first - otherwise the first tool to finish re-arms the watchdog under a
|
|
781
|
+
* still-waiting sibling, which then kills the run at the idle timeout.
|
|
771
782
|
*/
|
|
772
783
|
withPausedWatchdog<T>(fn: () => Promise<T>): Promise<T>;
|
|
773
784
|
/** @see withPausedWatchdog — the name the plan tools and `--confirm` use. */
|
|
@@ -1026,6 +1037,16 @@ export declare class AgentController {
|
|
|
1026
1037
|
* `warn` because a listener that throws once throws on every chunk.
|
|
1027
1038
|
*/
|
|
1028
1039
|
private emitToolOutput;
|
|
1040
|
+
/**
|
|
1041
|
+
* Subscribe to `job_wait`'s live view (`begin` / 1 Hz `update` / `end`).
|
|
1042
|
+
*
|
|
1043
|
+
* NOT GATED ON `liveToolOutput` (extended-job-wait D-9): that switch bounds a
|
|
1044
|
+
* high-frequency byte stream; this channel is capped at 1 Hz, and switching it
|
|
1045
|
+
* off would bring back the 20-minute black box.
|
|
1046
|
+
*/
|
|
1047
|
+
subscribeWait(listener: (event: WaitEvent) => void): () => void;
|
|
1048
|
+
/** Fan out one wait event; a throwing listener is logged and skipped (I-6). */
|
|
1049
|
+
private emitWait;
|
|
1029
1050
|
/**
|
|
1030
1051
|
* Subscribe to the CLI-local live tool-output stream (§3.1.3).
|
|
1031
1052
|
*
|
package/dist/agent/controller.js
CHANGED
|
@@ -27,7 +27,8 @@ import { buildForcedTeamBlock } from '../team/forced-prompt.js';
|
|
|
27
27
|
import { buildFastBlock } from '../fast/prompt.js';
|
|
28
28
|
import { ProcSupervisor } from '../proc/supervisor.js';
|
|
29
29
|
import { buildBackgroundJobsBlock, buildBackgroundServicesBlock } from '../proc/prompt.js';
|
|
30
|
-
import { PROC_LIMITS } from '../proc/limits.js';
|
|
30
|
+
import { JOB_WAIT_TOOL_CEILING_MS, PROC_LIMITS } from '../proc/limits.js';
|
|
31
|
+
import { createUserAttention } from './user-attention.js';
|
|
31
32
|
import { JobLogMirror } from '../proc/log-mirror.js';
|
|
32
33
|
import { JobNoticeQueue } from '../proc/notices.js';
|
|
33
34
|
import { makeBashKill, makeBashOutput } from '../tools/proc-tools.js';
|
|
@@ -261,6 +262,10 @@ export class AgentController {
|
|
|
261
262
|
*/
|
|
262
263
|
toolOutputs;
|
|
263
264
|
outputListeners = new Set();
|
|
265
|
+
/** `job_wait` live-view listeners (extended-job-wait); see `subscribeWait`. */
|
|
266
|
+
waitListeners = new Set();
|
|
267
|
+
/** "The user just spoke": ends a running `job_wait` early. */
|
|
268
|
+
userAttention = createUserAttention();
|
|
264
269
|
// -----------------------------------------------------------------------
|
|
265
270
|
// Fast model tier (fast-model-tier §3.1 / §3.3)
|
|
266
271
|
//
|
|
@@ -735,15 +740,19 @@ export class AgentController {
|
|
|
735
740
|
supervisor: this.procs,
|
|
736
741
|
waitSeconds: () => this.config.jobs.waitSeconds,
|
|
737
742
|
stallSeconds: () => this.config.jobs.stallSeconds,
|
|
738
|
-
toolTimeoutMs: () => this.config.toolTimeoutMs,
|
|
739
743
|
maxConsecutiveWaits: PROC_LIMITS.maxConsecutiveWaits,
|
|
740
744
|
terminalRecord: (id) => this.jobRelay?.terminalRecord(id),
|
|
745
|
+
// THE THREE LONG-WAIT PORTS (extended-job-wait). `holdWatchdog` wraps the
|
|
746
|
+
// WHOLE wait because tools run one at a time (I-3); `attention` is
|
|
747
|
+
// subscribe-only (RV-1); the view listener never throws back (I-6).
|
|
748
|
+
onWait: (event) => this.emitWait(event),
|
|
749
|
+
holdWatchdog: (fn) => this.withPausedWatchdog(fn),
|
|
750
|
+
attention: { subscribe: (listener) => this.userAttention.subscribe(listener) },
|
|
741
751
|
}),
|
|
742
752
|
makeJobList({
|
|
743
753
|
supervisor: this.procs,
|
|
744
754
|
waitSeconds: () => this.config.jobs.waitSeconds,
|
|
745
755
|
stallSeconds: () => this.config.jobs.stallSeconds,
|
|
746
|
-
toolTimeoutMs: () => this.config.toolTimeoutMs,
|
|
747
756
|
maxConsecutiveWaits: PROC_LIMITS.maxConsecutiveWaits,
|
|
748
757
|
terminalRecord: (id) => this.jobRelay?.terminalRecord(id),
|
|
749
758
|
}),
|
|
@@ -961,6 +970,13 @@ export class AgentController {
|
|
|
961
970
|
// outlived a mid-session `Settings > Team` switch to the supervisor
|
|
962
971
|
// and killed the dispatch the supervisor was meant to look after.
|
|
963
972
|
...(this.teamRegistered ? { task: 0 } : {}),
|
|
973
|
+
// THE EXECUTOR'S OWN CLOCK FOR A LONG WAIT (extended-job-wait I-2 / I-4').
|
|
974
|
+
// Without it `job_wait` falls onto `toolTimeoutMs` (180 s) and a 20-minute
|
|
975
|
+
// wait dies at three; with `0` the wait would have no independent backstop
|
|
976
|
+
// while the watchdog is paused. A finite value derived from the one
|
|
977
|
+
// authority, `PROC_LIMITS.waitMaxSeconds`, is both. `jobsRegistered` is
|
|
978
|
+
// assigned in the constructor before this table is built.
|
|
979
|
+
...(this.jobsRegistered ? { job_wait: JOB_WAIT_TOOL_CEILING_MS } : {}),
|
|
964
980
|
// P0 (governance-mode D-21 / RV-0): `gov_dispatch` runs a whole governed stage,
|
|
965
981
|
// routinely longer than `toolTimeoutMs`, and it listens to `ctx.signal` (it hands
|
|
966
982
|
// it to `TeamRuntime.dispatch`), which is what makes `0` safe. Registered in the SAME
|
|
@@ -1587,6 +1603,10 @@ export class AgentController {
|
|
|
1587
1603
|
this.pendingUserSteering.delete(id);
|
|
1588
1604
|
throw error;
|
|
1589
1605
|
}
|
|
1606
|
+
// AFTER a successful steer, and ONLY for user input (I-7): a reviewer or a job
|
|
1607
|
+
// notice must not cut short the wait the user chose.
|
|
1608
|
+
if (user)
|
|
1609
|
+
this.userAttention.notify();
|
|
1590
1610
|
return id;
|
|
1591
1611
|
}
|
|
1592
1612
|
followUp(text) {
|
|
@@ -1948,6 +1968,12 @@ export class AgentController {
|
|
|
1948
1968
|
* rather than a counter, so a nested call's `finally` re-arms the watchdog
|
|
1949
1969
|
* while the outer wait is still going. That is why a child's `--confirm`
|
|
1950
1970
|
* dialog does NOT route through this (see the constructor's `humanQueue`).
|
|
1971
|
+
*
|
|
1972
|
+
* `job_wait` ALSO DEPENDS ON THAT (extended-job-wait I-3): it holds the pause for
|
|
1973
|
+
* up to 20 minutes and is safe only because the engine runs a batch's tools
|
|
1974
|
+
* one at a time. If tool execution ever becomes parallel, make this a COUNTER
|
|
1975
|
+
* first - otherwise the first tool to finish re-arms the watchdog under a
|
|
1976
|
+
* still-waiting sibling, which then kills the run at the idle timeout.
|
|
1951
1977
|
*/
|
|
1952
1978
|
async withPausedWatchdog(fn) {
|
|
1953
1979
|
this.agent.pauseIdleWatchdog();
|
|
@@ -2501,6 +2527,33 @@ export class AgentController {
|
|
|
2501
2527
|
}
|
|
2502
2528
|
}
|
|
2503
2529
|
}
|
|
2530
|
+
/**
|
|
2531
|
+
* Subscribe to `job_wait`'s live view (`begin` / 1 Hz `update` / `end`).
|
|
2532
|
+
*
|
|
2533
|
+
* NOT GATED ON `liveToolOutput` (extended-job-wait D-9): that switch bounds a
|
|
2534
|
+
* high-frequency byte stream; this channel is capped at 1 Hz, and switching it
|
|
2535
|
+
* off would bring back the 20-minute black box.
|
|
2536
|
+
*/
|
|
2537
|
+
subscribeWait(listener) {
|
|
2538
|
+
this.waitListeners.add(listener);
|
|
2539
|
+
return () => {
|
|
2540
|
+
this.waitListeners.delete(listener);
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
/** Fan out one wait event; a throwing listener is logged and skipped (I-6). */
|
|
2544
|
+
emitWait(event) {
|
|
2545
|
+
for (const listener of this.waitListeners) {
|
|
2546
|
+
try {
|
|
2547
|
+
listener(event);
|
|
2548
|
+
}
|
|
2549
|
+
catch (err) {
|
|
2550
|
+
getLogger().debug('tool', 'wait_listener_threw', {
|
|
2551
|
+
toolCallId: event.toolCallId,
|
|
2552
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2504
2557
|
/**
|
|
2505
2558
|
* Subscribe to the CLI-local live tool-output stream (§3.1.3).
|
|
2506
2559
|
*
|
package/dist/agent/reducer.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { FastReview, FastSnapshot } from '../fast/types.js';
|
|
|
19
19
|
import type { CompactionMode, CompactionPhase, CompactionRecord, CompactionSnapshot, CompactionUiTrigger, ContextUsageSnapshot } from '../compaction/types.js';
|
|
20
20
|
import type { AcceptanceSnapshot, TodoItem, TodoSnapshot } from '../todo/types.js';
|
|
21
21
|
import type { JobKind, ServiceSnapshot, ServiceStatus } from '../proc/types.js';
|
|
22
|
+
import type { WaitView } from '../proc/wait-view.js';
|
|
22
23
|
import type { RetryPhase } from './retry-view.js';
|
|
23
24
|
import type { FilePatch } from '../tools/patch.js';
|
|
24
25
|
import type { TaskSummaryView } from '../fast/task-summary-types.js';
|
|
@@ -140,6 +141,16 @@ export type Entry = {
|
|
|
140
141
|
liveSeq?: number;
|
|
141
142
|
/** Epoch ms of the last output row, for the stall row (§3.3.4). */
|
|
142
143
|
lastOutputAt?: number;
|
|
144
|
+
/**
|
|
145
|
+
* The live view of a running `job_wait` (extended-job-wait §3.8.1), refreshed
|
|
146
|
+
* at 1 Hz. RUNTIME ONLY, and it lives and dies with `live` (I-9): cleared by
|
|
147
|
+
* `toolExecEnd` AND by every idle path through `releaseLiveTails`, and never
|
|
148
|
+
* persisted. `job_wait` never streams, so `live` is always empty for it - which
|
|
149
|
+
* is exactly why the idle clean-up cannot key off `live` alone.
|
|
150
|
+
*/
|
|
151
|
+
wait?: WaitView;
|
|
152
|
+
/** Monotonic counter: the revision term that invalidates the render cache. */
|
|
153
|
+
waitSeq?: number;
|
|
143
154
|
} | {
|
|
144
155
|
id: string;
|
|
145
156
|
kind: 'notice';
|
|
@@ -580,6 +591,15 @@ export type ViewAction = {
|
|
|
580
591
|
toolCallId: string;
|
|
581
592
|
rows: readonly string[];
|
|
582
593
|
at: number;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* `job_wait`'s 1 Hz view (extended-job-wait). Carries the WHOLE view, so the
|
|
597
|
+
* coalescer keeps only the last per `toolCallId`, like `toolOutputDelta`.
|
|
598
|
+
*/
|
|
599
|
+
| {
|
|
600
|
+
type: 'waitUpdate';
|
|
601
|
+
toolCallId: string;
|
|
602
|
+
view: WaitView;
|
|
583
603
|
} | {
|
|
584
604
|
type: 'turnEnd';
|
|
585
605
|
usage: TokenUsage;
|
package/dist/agent/reducer.js
CHANGED
|
@@ -400,12 +400,22 @@ function settleRetryCard(state, entries, phase) {
|
|
|
400
400
|
function releaseLiveTails(entries) {
|
|
401
401
|
let touched = false;
|
|
402
402
|
const next = entries.map((e) => {
|
|
403
|
-
|
|
403
|
+
// `!e.live && !e.wait`, NOT `!e.live` (extended-job-wait RV-2): a `job_wait`
|
|
404
|
+
// never streams, so its `live` is always empty and the old guard would skip it,
|
|
405
|
+
// leaving a wait card that says "running" in the transcript forever.
|
|
406
|
+
if (e.kind !== 'tool' || (!e.live && !e.wait))
|
|
404
407
|
return e;
|
|
405
408
|
if (e.status !== 'running' && e.status !== 'pending')
|
|
406
409
|
return e;
|
|
407
410
|
touched = true;
|
|
408
|
-
return {
|
|
411
|
+
return {
|
|
412
|
+
...e,
|
|
413
|
+
live: undefined,
|
|
414
|
+
lastOutputAt: undefined,
|
|
415
|
+
liveSeq: (e.liveSeq ?? 0) + 1,
|
|
416
|
+
wait: undefined,
|
|
417
|
+
waitSeq: (e.waitSeq ?? 0) + 1,
|
|
418
|
+
};
|
|
409
419
|
});
|
|
410
420
|
return touched ? next : entries;
|
|
411
421
|
}
|
|
@@ -669,6 +679,23 @@ export function viewReducer(state, action) {
|
|
|
669
679
|
: e),
|
|
670
680
|
};
|
|
671
681
|
}
|
|
682
|
+
/**
|
|
683
|
+
* ONLY A RUNNING, MATCHING ENTRY. `begin` travels from the controller and can
|
|
684
|
+
* beat `tool_execution_start` through the agent-event pipe, so a miss is
|
|
685
|
+
* normal and is dropped on purpose: the next 1 Hz beat repairs it (RV-5).
|
|
686
|
+
*/
|
|
687
|
+
case 'waitUpdate': {
|
|
688
|
+
const id = findToolEntryId(state, action.toolCallId);
|
|
689
|
+
if (!id)
|
|
690
|
+
return state;
|
|
691
|
+
const target = state.entries.find((e) => e.id === id);
|
|
692
|
+
if (!target || target.kind !== 'tool' || target.status !== 'running')
|
|
693
|
+
return state;
|
|
694
|
+
return {
|
|
695
|
+
...state,
|
|
696
|
+
entries: mapEntry(state.entries, id, (e) => e.kind === 'tool' ? { ...e, wait: action.view, waitSeq: (e.waitSeq ?? 0) + 1 } : e),
|
|
697
|
+
};
|
|
698
|
+
}
|
|
672
699
|
case 'toolExecEnd': {
|
|
673
700
|
const id = findToolEntryId(state, action.toolCallId);
|
|
674
701
|
if (!id)
|
|
@@ -689,6 +716,8 @@ export function viewReducer(state, action) {
|
|
|
689
716
|
// `bashBadge`'s last-line parse (`ToolPreview.tsx:35-54`).
|
|
690
717
|
live: undefined,
|
|
691
718
|
lastOutputAt: undefined,
|
|
719
|
+
wait: undefined,
|
|
720
|
+
waitSeq: undefined,
|
|
692
721
|
}
|
|
693
722
|
: e),
|
|
694
723
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "The user just spoke" as a subscribable edge (extended-job-wait §3.4).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/agent/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* EDGE-TRIGGERED ON PURPOSE: there is no "is anything pending" probe. A wait that
|
|
7
|
+
* could be woken by pre-existing state would return at once whenever that state
|
|
8
|
+
* went stale, and a model that gets an instant return from every `job_wait` burns
|
|
9
|
+
* tokens in a hot loop. Only a `notify()` that happens after `subscribe()` counts.
|
|
10
|
+
*/
|
|
11
|
+
export interface UserAttention {
|
|
12
|
+
/** Announce one new user input to every current subscriber. */
|
|
13
|
+
notify(): void;
|
|
14
|
+
/** Register a listener; returns its unsubscribe. */
|
|
15
|
+
subscribe(listener: () => void): () => void;
|
|
16
|
+
}
|
|
17
|
+
export declare function createUserAttention(): UserAttention;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "The user just spoke" as a subscribable edge (extended-job-wait §3.4).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/agent/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* EDGE-TRIGGERED ON PURPOSE: there is no "is anything pending" probe. A wait that
|
|
7
|
+
* could be woken by pre-existing state would return at once whenever that state
|
|
8
|
+
* went stale, and a model that gets an instant return from every `job_wait` burns
|
|
9
|
+
* tokens in a hot loop. Only a `notify()` that happens after `subscribe()` counts.
|
|
10
|
+
*/
|
|
11
|
+
export function createUserAttention() {
|
|
12
|
+
const listeners = new Set();
|
|
13
|
+
return {
|
|
14
|
+
notify() {
|
|
15
|
+
// Snapshot first: a listener may unsubscribe itself while we iterate.
|
|
16
|
+
for (const listener of [...listeners]) {
|
|
17
|
+
try {
|
|
18
|
+
listener();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// One broken listener must not deprive the others of the wake-up.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
subscribe(listener) {
|
|
26
|
+
listeners.add(listener);
|
|
27
|
+
return () => {
|
|
28
|
+
listeners.delete(listener);
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1765,9 +1765,8 @@ function runConfigSetValue(key, value) {
|
|
|
1765
1765
|
const jobsPatch = applyJobsConfigSet(key, value);
|
|
1766
1766
|
if (jobsPatch) {
|
|
1767
1767
|
// Echoed from the STORED value, so `config set jobs.waitSeconds 1` prints 5
|
|
1768
|
-
// and `jobs.waitSeconds 9999` prints
|
|
1769
|
-
// states.
|
|
1770
|
-
// tool ceiling by I-4, so the stored value is the only honest answer.
|
|
1768
|
+
// and `jobs.waitSeconds 9999` prints 1200 — the rule every section above
|
|
1769
|
+
// states.
|
|
1771
1770
|
const merged = updatePersistedConfig(jobsPatch);
|
|
1772
1771
|
const field = key.slice('jobs.'.length);
|
|
1773
1772
|
process.stdout.write(`Set ${key} = ${String(merged.jobs[field])}\n`);
|
package/dist/config/load.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* legitimately long single tool run can never be watchdog-killed.
|
|
7
7
|
*/
|
|
8
8
|
import process from 'node:process';
|
|
9
|
-
import { JOB_WAIT_MARGIN_MS } from '../proc/limits.js';
|
|
10
9
|
import { DEFAULT_CONFIG, DEFAULT_SCROLL_MODE, IDLE_TIMEOUT_MARGIN_MS, clampDensity, clampHumanTimeout, clampCompactionConfig, clampLogConfig, clampFastConfig, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampBashConfig, clampJobsConfig, clampPromptsConfig, clampTaskSummaryConfig, clampUpdateConfig, clampTheme, clampThinkingLevel, clampContextWindow, clampMaxTokens, clampMaxRenderInterval, clampScrollMode, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAutoToken, isSkillsToolPolicyMode, legacyTeamCeilingPatch, legacyTeamFanoutPatch, LEGACY_TEAM_FANOUT_DEFAULTS, parseThresholdInput, } from './schema.js';
|
|
11
10
|
import { loadDotenv, readEnvConfig, readEnvDisabledSkills } from './env.js';
|
|
12
11
|
import { readConfigFile, updatePersistedConfig } from './store.js';
|
|
@@ -622,32 +621,14 @@ export function loadConfig(flags = {}, options = {}) {
|
|
|
622
621
|
DEFAULT_CONFIG.planModeDefault;
|
|
623
622
|
const planModeHumanTimeoutMs = clampHumanTimeout(file.planModeHumanTimeoutMs, DEFAULT_CONFIG.planModeHumanTimeoutMs);
|
|
624
623
|
const toolTimeoutMs = coercePositiveInt(pick(flags.toolTimeout, file.toolTimeoutMs), DEFAULT_CONFIG.toolTimeoutMs);
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
// big config literal a hundred lines further down — a reference to it here would
|
|
629
|
-
// be a use-before-declaration, and "resolve it later and patch the object" is
|
|
630
|
-
// how the invariant gets silently skipped. Resolving it first makes the clamp
|
|
631
|
-
// impossible to omit.
|
|
624
|
+
// `jobs.waitSeconds` is NOT converged against `toolTimeoutMs` (extended-job-wait
|
|
625
|
+
// I-4'): `job_wait` has its own executor ceiling and pauses the idle watchdog,
|
|
626
|
+
// so the section is resolved once, as-is.
|
|
632
627
|
const jobs = resolveJobsConfig(env.partial, file);
|
|
633
628
|
let idleTimeoutMs = coercePositiveInt(pick(flags.idleTimeout, file.idleTimeoutMs), DEFAULT_CONFIG.idleTimeoutMs);
|
|
634
629
|
// Timeout invariant (R1): idle must be strictly greater than the tool ceiling
|
|
635
630
|
// so a long single tool run is never aborted by the idle watchdog.
|
|
636
631
|
idleTimeoutMs = Math.max(idleTimeoutMs, toolTimeoutMs + IDLE_TIMEOUT_MARGIN_MS);
|
|
637
|
-
// INVARIANT I-4 (long-task-jobs): a wait must fit inside the tool call that
|
|
638
|
-
// hosts it, `jobs.waitSeconds * 1000 + JOB_WAIT_MARGIN_MS <= toolTimeoutMs`.
|
|
639
|
-
//
|
|
640
|
-
// `Math.min`, AND THE DIRECTION IS THE POINT. The invariant above RAISES the
|
|
641
|
-
// idle window to accommodate the tool; this one LOWERS the wait to accommodate
|
|
642
|
-
// the tool. Copying `Math.max` here would produce a wait that outlives its own
|
|
643
|
-
// tool call — which is exactly the `Command timed out` failure the feature
|
|
644
|
-
// exists to remove, re-created by its own config check.
|
|
645
|
-
//
|
|
646
|
-
// With an operator-set ceiling below the margin the floor of 1s applies rather
|
|
647
|
-
// than a negative wait: the invariant wins over the minimum, because a wait
|
|
648
|
-
// longer than its host is worse than a short wait.
|
|
649
|
-
const waitCeilingSeconds = Math.floor((toolTimeoutMs - JOB_WAIT_MARGIN_MS) / 1000);
|
|
650
|
-
jobs.waitSeconds = Math.max(1, Math.min(jobs.waitSeconds, waitCeilingSeconds));
|
|
651
632
|
// Resolve API keys: config-file key wins over the provider env var.
|
|
652
633
|
const fileKeys = file.apiKeys ?? {};
|
|
653
634
|
const apiKeys = {};
|
|
@@ -798,9 +779,7 @@ export function loadConfig(flags = {}, options = {}) {
|
|
|
798
779
|
update: resolveUpdateConfig(flags, env.partial, file),
|
|
799
780
|
compaction: resolveCompactionConfig(flags, env.partial, file),
|
|
800
781
|
bash: resolveBashConfig(env.partial, file),
|
|
801
|
-
// The already-resolved
|
|
802
|
-
// NOT `resolveJobsConfig(...)` a second time: that would re-apply the
|
|
803
|
-
// un-converged `waitSeconds` and silently undo I-4.
|
|
782
|
+
// The already-resolved section from the top of this function.
|
|
804
783
|
jobs,
|
|
805
784
|
// The quick-prompt library's three policy scalars (quick-prompt-library D-19).
|
|
806
785
|
// Resolved through the SAME clamp the read and write paths use, so an env-only
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -878,11 +878,11 @@ export interface JobsConfig {
|
|
|
878
878
|
*/
|
|
879
879
|
enabled: boolean;
|
|
880
880
|
/**
|
|
881
|
-
* Seconds one `job_wait` blocks by default. Default 30, range 5..
|
|
881
|
+
* Seconds one `job_wait` blocks by default. Default 30, range 5..1200.
|
|
882
882
|
*
|
|
883
|
-
*
|
|
884
|
-
*
|
|
885
|
-
*
|
|
883
|
+
* NOT CONVERGED AGAINST `toolTimeoutMs` ANY MORE (extended-job-wait I-4'): the
|
|
884
|
+
* executor gives `job_wait` its own ceiling and the idle watchdog is paused for
|
|
885
|
+
* the whole wait, so a long default no longer risks `Command timed out`.
|
|
886
886
|
*/
|
|
887
887
|
waitSeconds: number;
|
|
888
888
|
/**
|
package/dist/config/schema.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { isAbsolute } from 'node:path';
|
|
9
9
|
import { ABSOLUTE_MAX_OUTPUT_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_RETRY_POLICY, MIN_MAX_OUTPUT_TOKENS, RETRY_LIMITS, } from '@aragon-agent/core';
|
|
10
10
|
import { clampLogLevel } from '../logging/levels.js';
|
|
11
|
+
import { PROC_LIMITS } from '../proc/limits.js';
|
|
11
12
|
import { TEAM_LIMITS } from '../team/limits.js';
|
|
12
13
|
import { DEFAULT_GOVERNANCE_CONFIG } from './governance-config.js';
|
|
13
14
|
// Governance mode (governance-mode 11.1): defined in its own file so this one does not grow.
|
|
@@ -1013,7 +1014,12 @@ export const DEFAULT_JOBS_CONFIG = {
|
|
|
1013
1014
|
persistLog: true,
|
|
1014
1015
|
noticeSteer: true,
|
|
1015
1016
|
};
|
|
1016
|
-
|
|
1017
|
+
// DERIVED, NOT RESTATED (extended-job-wait I-2): a second copy of the ceiling is
|
|
1018
|
+
// how the config layer and the tool layer end up disagreeing without an error.
|
|
1019
|
+
const WAIT_SECONDS_RANGE = {
|
|
1020
|
+
min: PROC_LIMITS.waitMinSeconds,
|
|
1021
|
+
max: PROC_LIMITS.waitMaxSeconds,
|
|
1022
|
+
};
|
|
1017
1023
|
const STALL_SECONDS_RANGE = { min: 30, max: 3600 };
|
|
1018
1024
|
/**
|
|
1019
1025
|
* THE single gate for every `jobs` read AND write, mirroring `clampBashConfig`
|
package/dist/proc/limits.d.ts
CHANGED
|
@@ -132,9 +132,26 @@ export declare const PROC_LIMITS: {
|
|
|
132
132
|
* instead of a seventh `bash_output` poll.
|
|
133
133
|
*/
|
|
134
134
|
readonly waitTailRows: 6;
|
|
135
|
-
/**
|
|
135
|
+
/**
|
|
136
|
+
* Seconds a single wait may block, as a STRUCTURAL floor and ceiling (`jobs.waitSeconds` is clamped into this range).
|
|
137
|
+
*
|
|
138
|
+
* THE ONE AUTHORITY FOR THE CEILING (extended-job-wait I-2): the executor
|
|
139
|
+
* override (`JOB_WAIT_TOOL_CEILING_MS`), `WAIT_SECONDS_RANGE.max`, the tool
|
|
140
|
+
* description and the prompt block all derive from, or are asserted equal to,
|
|
141
|
+
* `waitMaxSeconds`. Changing it here and missing another reader is how a wait
|
|
142
|
+
* is silently cut short by a layer nobody remembered.
|
|
143
|
+
*/
|
|
136
144
|
readonly waitMinSeconds: 5;
|
|
137
|
-
readonly waitMaxSeconds:
|
|
145
|
+
readonly waitMaxSeconds: 1200;
|
|
146
|
+
/**
|
|
147
|
+
* Cadence of the in-wait heartbeat, in ms.
|
|
148
|
+
*
|
|
149
|
+
* ONE HZ because the reader needs "a few seconds ago" precision, not a stream:
|
|
150
|
+
* subscribing to the supervisor's 100 ms output coalescing would mean ~12000
|
|
151
|
+
* pointless view updates across a 20-minute wait. The same beat also carries the
|
|
152
|
+
* wall-clock deadline re-check (see `proc/wait.ts`).
|
|
153
|
+
*/
|
|
154
|
+
readonly waitHeartbeatMs: 1000;
|
|
138
155
|
/**
|
|
139
156
|
* Characters one extracted progress line may occupy.
|
|
140
157
|
*
|
|
@@ -183,18 +200,15 @@ export declare const BACKGROUND_SERVICES_BLOCK_VERSION = "v1";
|
|
|
183
200
|
* (`bash.background` and `jobs.enabled`), so one version number covering both
|
|
184
201
|
* would make "which block changed" unanswerable from a report.
|
|
185
202
|
*/
|
|
186
|
-
export declare const JOBS_BLOCK_VERSION = "
|
|
203
|
+
export declare const JOBS_BLOCK_VERSION = "v2-2026-10";
|
|
187
204
|
/**
|
|
188
|
-
* Head-room
|
|
189
|
-
*
|
|
190
|
-
* `jobs.waitSeconds * 1000 + JOB_WAIT_MARGIN_MS <= toolTimeoutMs` is the whole
|
|
191
|
-
* reason a wait never needs to pause the idle watchdog (D-3): a 60 s wait inside
|
|
192
|
-
* a 180 s tool call in a 210 s idle window cannot be mistaken for a hang.
|
|
205
|
+
* Head-room the executor's own `job_wait` timer leaves above the longest wait.
|
|
193
206
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* in `docs/plans/long-task-jobs/spec.md` under `## 实施过程发现的方案缺陷` (I-2).
|
|
207
|
+
* It is a SECOND, INDEPENDENT clock, not a coupling to `toolTimeoutMs`
|
|
208
|
+
* (extended-job-wait I-4'): the idle watchdog is paused for the whole wait, so
|
|
209
|
+
* when `waitForJob` itself misbehaves this is the only thing left that can end
|
|
210
|
+
* the call. `toolTimeoutMs` no longer constrains a wait at all.
|
|
199
211
|
*/
|
|
200
212
|
export declare const JOB_WAIT_MARGIN_MS = 30000;
|
|
213
|
+
/** The executor ceiling for `job_wait`: the longest wait plus its margin (1230 s). */
|
|
214
|
+
export declare const JOB_WAIT_TOOL_CEILING_MS: number;
|