@aragon-agent/cli 0.6.21 → 0.6.23
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/README.md +32 -12
- package/dist/agent/controller.js +22 -1
- package/dist/agent/reducer.js +8 -6
- package/dist/commands/builtins.js +2 -1
- package/dist/config/cli-commands.d.ts +1 -1
- package/dist/config/cli-commands.js +3 -0
- package/dist/config/load.js +42 -3
- package/dist/config/schema.d.ts +53 -12
- package/dist/config/schema.js +42 -0
- package/dist/team/limits.d.ts +73 -13
- package/dist/team/limits.js +82 -15
- package/dist/team/normalize.js +7 -2
- package/dist/team/overseer.d.ts +158 -18
- package/dist/team/overseer.js +356 -49
- package/dist/team/report.d.ts +9 -0
- package/dist/team/report.js +32 -4
- package/dist/team/runtime.d.ts +51 -9
- package/dist/team/runtime.js +498 -74
- package/dist/team/subagent.d.ts +10 -0
- package/dist/team/subagent.js +20 -6
- package/dist/team/task-tool.d.ts +7 -0
- package/dist/team/task-tool.js +2 -0
- package/dist/team/types.d.ts +44 -2
- package/dist/ui/App.js +14 -0
- package/dist/ui/TeamPanel.d.ts +22 -0
- package/dist/ui/TeamPanel.js +71 -2
- package/dist/ui/layout/team-panel.js +4 -1
- package/package.json +2 -2
package/dist/team/runtime.js
CHANGED
|
@@ -28,30 +28,48 @@
|
|
|
28
28
|
* pre-feature behaviour.
|
|
29
29
|
*/
|
|
30
30
|
import { TeamBus } from './bus.js';
|
|
31
|
-
import { TEAM_LIMITS } from './limits.js';
|
|
32
|
-
import { TeamOverseer, buildReplacementPrompt } from './overseer.js';
|
|
31
|
+
import { TEAM_AGGREGATE_LABEL, TEAM_LIMITS } from './limits.js';
|
|
32
|
+
import { TeamOverseer, buildReplacementPrompt, nextOverseerLadderStepMs, resolveOverseerFirstCheckMs, } from './overseer.js';
|
|
33
33
|
import { shouldRetryColdStart } from './retry.js';
|
|
34
34
|
import { createSubagent, defaultSubagentFactory, } from './subagent.js';
|
|
35
35
|
/**
|
|
36
|
-
* The
|
|
37
|
-
*
|
|
36
|
+
* The pseudo-label aggregate supervisor notices travel under (D-5 / R-P1-6):
|
|
37
|
+
* `TEAM_AGGREGATE_LABEL`, RESERVED at normalization so no child slug can be
|
|
38
|
+
* `team`. The reservation is what makes this label a safe namespace - a
|
|
39
|
+
* colliding child would have the aggregate notice attributed to it in the
|
|
40
|
+
* ledger and its own quiet note swallowed by the report's aggregate count.
|
|
41
|
+
*/
|
|
42
|
+
const MONITOR_TEAM_LABEL = TEAM_AGGREGATE_LABEL;
|
|
43
|
+
const MONITOR_EXHAUSTED_DECISION = {
|
|
44
|
+
action: 'wait',
|
|
45
|
+
reason: 'dispatch look budget exhausted; supervision quiet for the rest of the dispatch',
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The wait a DEGRADED tick announces per child (D-4 / D-10). One shared
|
|
49
|
+
* object, not a fresh literal per event: it is the same fact every time and
|
|
50
|
+
* identity makes that obvious to any consumer that looks.
|
|
51
|
+
*/
|
|
52
|
+
const DEGRADED_WAIT_DECISION = {
|
|
53
|
+
action: 'wait',
|
|
54
|
+
reason: 'fast tier unavailable; waiting unassisted',
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The per-child SILENCE windows (team-overseer §3.2; subagent-overseer-v2
|
|
58
|
+
* keeps only this half). Timers only — no state, no decisions; the runtime
|
|
59
|
+
* applies what the supervisor decides. The wall-clock half moved to
|
|
60
|
+
* `MonitorLoop`, which owns the cadence ladder for the whole roster.
|
|
38
61
|
*
|
|
39
62
|
* Every timer is `unref`'d (the `backoff` argument): a pending supervision
|
|
40
63
|
* look must not hold the process open on exit.
|
|
41
64
|
*/
|
|
42
65
|
class ChildSupervisor {
|
|
43
66
|
deps;
|
|
44
|
-
clocks = new Map();
|
|
45
67
|
silences = new Map();
|
|
46
|
-
/** The interval used when a decision did not name one. */
|
|
47
|
-
defaultCheckMs;
|
|
48
68
|
constructor(deps) {
|
|
49
69
|
this.deps = deps;
|
|
50
|
-
this.defaultCheckMs = Math.max(TEAM_LIMITS.overseerNextCheckMinMs, deps.firstCheckMs);
|
|
51
70
|
}
|
|
52
|
-
/** Arm (or re-arm)
|
|
71
|
+
/** Arm (or re-arm) the silence window for a child starting an attempt. */
|
|
53
72
|
start(label) {
|
|
54
|
-
this.armClock(label, this.deps.firstCheckMs);
|
|
55
73
|
this.armSilence(label);
|
|
56
74
|
}
|
|
57
75
|
/** Reset the silence window. A no-op for a child nobody is supervising. */
|
|
@@ -62,33 +80,16 @@ class ChildSupervisor {
|
|
|
62
80
|
rearmSilence(label) {
|
|
63
81
|
this.armSilence(label);
|
|
64
82
|
}
|
|
65
|
-
rearmClock(label, ms) {
|
|
66
|
-
this.armClock(label, ms);
|
|
67
|
-
}
|
|
68
83
|
clear(label) {
|
|
69
|
-
const clock = this.clocks.get(label);
|
|
70
|
-
if (clock !== undefined)
|
|
71
|
-
clearTimeout(clock);
|
|
72
84
|
const silence = this.silences.get(label);
|
|
73
85
|
if (silence !== undefined)
|
|
74
86
|
clearTimeout(silence);
|
|
75
|
-
this.clocks.delete(label);
|
|
76
87
|
this.silences.delete(label);
|
|
77
88
|
}
|
|
78
89
|
clearAll() {
|
|
79
|
-
for (const label of [...this.clocks.keys()])
|
|
80
|
-
this.clear(label);
|
|
81
90
|
for (const label of [...this.silences.keys()])
|
|
82
91
|
this.clear(label);
|
|
83
92
|
}
|
|
84
|
-
armClock(label, ms) {
|
|
85
|
-
const previous = this.clocks.get(label);
|
|
86
|
-
if (previous !== undefined)
|
|
87
|
-
clearTimeout(previous);
|
|
88
|
-
const timer = setTimeout(() => this.fire(label, 'clock'), ms);
|
|
89
|
-
timer.unref?.();
|
|
90
|
-
this.clocks.set(label, timer);
|
|
91
|
-
}
|
|
92
93
|
armSilence(label) {
|
|
93
94
|
const previous = this.silences.get(label);
|
|
94
95
|
if (previous !== undefined)
|
|
@@ -101,17 +102,191 @@ class ChildSupervisor {
|
|
|
101
102
|
this.armSilence(label);
|
|
102
103
|
return;
|
|
103
104
|
}
|
|
104
|
-
this.
|
|
105
|
+
this.silences.delete(label);
|
|
106
|
+
this.deps.onFire(label, 'silence');
|
|
105
107
|
}, this.deps.idleMs);
|
|
106
108
|
timer.unref?.();
|
|
107
109
|
this.silences.set(label, timer);
|
|
108
110
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The cadence ladder (subagent-overseer-v2 D-2): one timer for the whole
|
|
114
|
+
* roster, per-child `nextAt` / `ladderStep` entries armed at each child's
|
|
115
|
+
* ACTUAL start (R-P1-5), and a strict one-batch-in-flight rule.
|
|
116
|
+
*
|
|
117
|
+
* A SKIPPED LABEL RE-SCHEULES WITHOUT TOUCHING ITS LADDER STEP: the
|
|
118
|
+
* single-flight guard (R-P1-4) can leave a due `nextAt` in the past while a
|
|
119
|
+
* silence-triggered inspection of that child is still settling, and a timer
|
|
120
|
+
* that re-fired at `max(0, due - now)` would busy-loop until it settles. The
|
|
121
|
+
* recheck delay is invisible to the protocol - no look is spent, the ladder
|
|
122
|
+
* value does not move - it only keeps the event loop idle.
|
|
123
|
+
*/
|
|
124
|
+
const MONITOR_RECHECK_MS = 500;
|
|
125
|
+
class MonitorLoop {
|
|
126
|
+
deps;
|
|
127
|
+
nextAt = new Map();
|
|
128
|
+
ladderStep = new Map();
|
|
129
|
+
timer = null;
|
|
130
|
+
ticking = false;
|
|
131
|
+
stopped = false;
|
|
132
|
+
constructor(deps) {
|
|
133
|
+
this.deps = deps;
|
|
134
|
+
}
|
|
135
|
+
/** Arm the FIRST rung for a child that just started (R-P1-5). */
|
|
136
|
+
arm(label) {
|
|
137
|
+
const now = this.deps.now();
|
|
138
|
+
this.nextAt.set(label, now + this.deps.firstCheckMs);
|
|
139
|
+
this.ladderStep.set(label, this.deps.firstCheckMs);
|
|
140
|
+
this.schedule();
|
|
141
|
+
}
|
|
142
|
+
/** A wait decision's `nextCheckMs` OVERRIDES the rung and becomes it (D-2). */
|
|
143
|
+
override(label, ms) {
|
|
144
|
+
const now = this.deps.now();
|
|
145
|
+
this.nextAt.set(label, now + ms);
|
|
146
|
+
this.ladderStep.set(label, ms);
|
|
147
|
+
this.schedule();
|
|
148
|
+
}
|
|
149
|
+
/** Any ACTION resets that child's ladder to the base rung (D-2). */
|
|
150
|
+
reset(label) {
|
|
151
|
+
this.arm(label);
|
|
152
|
+
}
|
|
153
|
+
/** Grow the rung and schedule the next visit (D-2). */
|
|
154
|
+
advance(label) {
|
|
155
|
+
const now = this.deps.now();
|
|
156
|
+
const step = nextOverseerLadderStepMs(this.ladderStep.get(label) ?? this.deps.firstCheckMs);
|
|
157
|
+
this.ladderStep.set(label, step);
|
|
158
|
+
this.nextAt.set(label, now + step);
|
|
159
|
+
this.schedule();
|
|
160
|
+
}
|
|
161
|
+
/** Stop visiting one child (settled, quieted, or its verdict applied). */
|
|
162
|
+
clear(label) {
|
|
163
|
+
this.nextAt.delete(label);
|
|
164
|
+
this.ladderStep.delete(label);
|
|
165
|
+
this.schedule();
|
|
166
|
+
}
|
|
167
|
+
/** Stop the whole loop; the dispatch is over or the total budget is spent. */
|
|
168
|
+
stop() {
|
|
169
|
+
this.stopped = true;
|
|
170
|
+
if (this.timer !== null)
|
|
171
|
+
clearTimeout(this.timer);
|
|
172
|
+
this.timer = null;
|
|
173
|
+
this.nextAt.clear();
|
|
174
|
+
this.ladderStep.clear();
|
|
175
|
+
}
|
|
176
|
+
dispose() {
|
|
177
|
+
this.stop();
|
|
178
|
+
}
|
|
179
|
+
/** The earliest due time among live labels, or null when nothing is armed. */
|
|
180
|
+
earliestDue() {
|
|
181
|
+
let earliest = null;
|
|
182
|
+
for (const [label, at] of this.nextAt) {
|
|
183
|
+
if (!this.deps.isLive(label))
|
|
184
|
+
continue;
|
|
185
|
+
earliest = earliest === null ? at : Math.min(earliest, at);
|
|
186
|
+
}
|
|
187
|
+
return earliest;
|
|
188
|
+
}
|
|
189
|
+
schedule() {
|
|
190
|
+
if (this.timer !== null) {
|
|
191
|
+
clearTimeout(this.timer);
|
|
192
|
+
this.timer = null;
|
|
193
|
+
}
|
|
194
|
+
if (this.stopped || this.ticking)
|
|
195
|
+
return;
|
|
196
|
+
const earliest = this.earliestDue();
|
|
197
|
+
if (earliest === null)
|
|
198
|
+
return;
|
|
199
|
+
const delay = Math.max(0, earliest - this.deps.now());
|
|
200
|
+
const timer = setTimeout(() => {
|
|
201
|
+
this.timer = null;
|
|
202
|
+
void this.tick();
|
|
203
|
+
}, delay);
|
|
204
|
+
timer.unref?.();
|
|
205
|
+
this.timer = timer;
|
|
206
|
+
}
|
|
207
|
+
async tick() {
|
|
208
|
+
if (this.ticking || this.stopped)
|
|
209
|
+
return;
|
|
210
|
+
const now = this.deps.now();
|
|
211
|
+
const due = [];
|
|
212
|
+
for (const [label, at] of [...this.nextAt]) {
|
|
213
|
+
if (!this.deps.isLive(label)) {
|
|
214
|
+
// Settled while armed; R-P1-5 also means a re-queued replacement body
|
|
215
|
+
// re-arms itself at its own start.
|
|
216
|
+
this.nextAt.delete(label);
|
|
217
|
+
this.ladderStep.delete(label);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (now >= at)
|
|
221
|
+
due.push(label);
|
|
222
|
+
}
|
|
223
|
+
if (due.length === 0) {
|
|
224
|
+
this.schedule();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// DEGRADED TICK (D-4 / D-10): no fast tier right now. The children wait
|
|
228
|
+
// unassisted - announced per child, never killed - and the cadence keeps
|
|
229
|
+
// running so a `/fast on` restores assistance at the next rung.
|
|
230
|
+
if (!this.deps.isActive()) {
|
|
231
|
+
this.deps.onDegraded(due);
|
|
232
|
+
for (const label of due)
|
|
233
|
+
this.advance(label);
|
|
234
|
+
this.schedule();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
// PER-CHILD QUIET (D-5): budget spent says so once, then stop visiting.
|
|
238
|
+
const inspectable = [];
|
|
239
|
+
for (const label of due) {
|
|
240
|
+
if (this.deps.isExhausted(label)) {
|
|
241
|
+
this.clear(label);
|
|
242
|
+
this.deps.onQuiet(label);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
inspectable.push(label);
|
|
246
|
+
}
|
|
247
|
+
if (inspectable.length === 0) {
|
|
248
|
+
this.schedule();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
this.ticking = true;
|
|
252
|
+
let decisions;
|
|
253
|
+
try {
|
|
254
|
+
decisions = await this.deps.inspectBatch(inspectable);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Defence in depth: `inspectBatch` is fail-soft by contract. Treat the
|
|
258
|
+
// tick as if every child had answered wait.
|
|
259
|
+
decisions = new Map();
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
this.ticking = false;
|
|
263
|
+
}
|
|
264
|
+
const after = this.deps.now();
|
|
265
|
+
for (const label of inspectable) {
|
|
266
|
+
const decision = decisions.get(label);
|
|
267
|
+
if (decision === undefined) {
|
|
268
|
+
// Skipped for single-flight (R-P1-4): no decision, no look, and the
|
|
269
|
+
// ladder does not move - only the timer must not busy-loop.
|
|
270
|
+
this.nextAt.set(label, after + MONITOR_RECHECK_MS);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (decision.action === 'wait' && decision.nextCheckMs !== undefined) {
|
|
274
|
+
this.override(label, decision.nextCheckMs);
|
|
275
|
+
}
|
|
276
|
+
else if (decision.action === 'wait') {
|
|
277
|
+
this.advance(label);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
this.reset(label);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (this.deps.dispatchExhausted()) {
|
|
284
|
+
// The fool-proof total (R-P1-6). Stop the whole loop, once, loudly.
|
|
285
|
+
this.stop();
|
|
286
|
+
this.deps.onExhausted();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.schedule();
|
|
115
290
|
}
|
|
116
291
|
}
|
|
117
292
|
export class TeamRuntime {
|
|
@@ -137,15 +312,30 @@ export class TeamRuntime {
|
|
|
137
312
|
*/
|
|
138
313
|
pendingBackoffs = new Set();
|
|
139
314
|
/**
|
|
140
|
-
* The dispatch supervisor, LIVE for the current dispatch only
|
|
141
|
-
* Null when the provider is
|
|
142
|
-
*
|
|
315
|
+
* The dispatch supervisor, LIVE for the current dispatch only
|
|
316
|
+
* (team-overseer / subagent-overseer-v2). Null when the provider is not
|
|
317
|
+
* wired or the policy is off, and reset to null at every dispatch end so
|
|
318
|
+
* nothing supervision-shaped leaks across dispatches.
|
|
319
|
+
*
|
|
320
|
+
* THE GATE IS "PROVIDER PRESENT AND POLICY ON" (R-P0-2), deliberately NOT
|
|
321
|
+
* the provider's live `active()`: a dispatch that STARTS without a fast
|
|
322
|
+
* tier must still get a MonitorLoop, because the promise this feature
|
|
323
|
+
* makes is "timeout is never a death sentence" - an unavailable tier
|
|
324
|
+
* degrades the tick to an announced unassisted wait rather than falling
|
|
325
|
+
* back to the legacy hard abort. `active()` is re-read every tick.
|
|
143
326
|
*/
|
|
144
327
|
overseer = null;
|
|
145
|
-
/** The
|
|
328
|
+
/** The per-child silence windows, same lifetime as `overseer`. */
|
|
146
329
|
supervision = null;
|
|
330
|
+
/** The cadence ladder (subagent-overseer-v2 D-2), same lifetime. */
|
|
331
|
+
monitor = null;
|
|
147
332
|
/** Applied interventions for the in-flight dispatch's outcome. */
|
|
148
333
|
interventions = [];
|
|
334
|
+
/** Labels already told the supervisor went quiet on them (D-5, once). */
|
|
335
|
+
quietNotified = new Set();
|
|
336
|
+
/** Edge-latched "a tick ran unassisted" (D-10), per dispatch. */
|
|
337
|
+
overseerDegraded = false;
|
|
338
|
+
overseerDegradedTicks = 0;
|
|
149
339
|
constructor(deps) {
|
|
150
340
|
this.deps = deps;
|
|
151
341
|
}
|
|
@@ -177,29 +367,48 @@ export class TeamRuntime {
|
|
|
177
367
|
this.busy = true;
|
|
178
368
|
this.aborted = false;
|
|
179
369
|
this.interventions = [];
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
//
|
|
370
|
+
this.quietNotified.clear();
|
|
371
|
+
this.overseerDegraded = false;
|
|
372
|
+
this.overseerDegradedTicks = 0;
|
|
373
|
+
// THE SUPERVISOR IS CREATED PER DISPATCH (R-P0-2): provider wired AND
|
|
374
|
+
// policy on. `TeamOverseer.isActive()` re-checks the provider per
|
|
375
|
+
// inspection and per tick, so a mid-dispatch `/fast off` degrades the
|
|
376
|
+
// NEXT look to an unassisted wait instead of unwinding anything already
|
|
377
|
+
// applied - and a dispatch that started without the tier is still
|
|
378
|
+
// supervised, in that degraded, never-kill sense.
|
|
379
|
+
const supervised = this.deps.overseer !== undefined && team.overseer;
|
|
184
380
|
this.overseer =
|
|
185
|
-
this.deps.overseer !== undefined
|
|
186
|
-
this.deps.overseer.active() &&
|
|
187
|
-
team.overseer
|
|
381
|
+
supervised && this.deps.overseer !== undefined
|
|
188
382
|
? new TeamOverseer(this.deps.overseer, { startedAt })
|
|
189
383
|
: null;
|
|
190
384
|
this.supervision =
|
|
191
385
|
this.overseer !== null
|
|
192
386
|
? new ChildSupervisor({
|
|
193
387
|
idleMs: config.idleTimeoutMs,
|
|
194
|
-
firstCheckMs: team.subagentTimeoutMs > 0
|
|
195
|
-
? team.subagentTimeoutMs
|
|
196
|
-
: TEAM_LIMITS.overseerDefaultCheckMs,
|
|
197
388
|
isLegitimatelyBlocked: (label) => this.isBlocked(label),
|
|
198
389
|
onFire: (label, trigger) => {
|
|
199
390
|
void this.inspectAndApply(dispatchId, label, trigger);
|
|
200
391
|
},
|
|
201
392
|
})
|
|
202
393
|
: null;
|
|
394
|
+
this.monitor =
|
|
395
|
+
this.overseer !== null
|
|
396
|
+
? new MonitorLoop({
|
|
397
|
+
// D-8: `overseerIntervalMs` wins, then a positive
|
|
398
|
+
// `subagentTimeoutMs` (compat), then the structural default.
|
|
399
|
+
firstCheckMs: resolveOverseerFirstCheckMs(team),
|
|
400
|
+
now: () => Date.now(),
|
|
401
|
+
// R-P1-5: only STARTED, UNSETTLED children tick.
|
|
402
|
+
isLive: (label) => this.isMonitorLive(label),
|
|
403
|
+
isActive: () => this.overseer?.isActive() === true,
|
|
404
|
+
isExhausted: (label) => this.overseer?.looksExhausted(label) === true,
|
|
405
|
+
dispatchExhausted: () => this.overseer?.dispatchLooksExhausted() === true,
|
|
406
|
+
inspectBatch: (labels) => this.monitorInspectBatch(dispatchId, labels),
|
|
407
|
+
onDegraded: (labels) => this.noteDegradedTick(dispatchId, labels),
|
|
408
|
+
onQuiet: (label) => this.noteQuiet(dispatchId, label),
|
|
409
|
+
onExhausted: () => this.noteDispatchExhausted(dispatchId),
|
|
410
|
+
})
|
|
411
|
+
: null;
|
|
203
412
|
// DECLARED BEFORE THE BUS, ASSIGNED AFTER THE HANDLES. `canSend` closes over
|
|
204
413
|
// this binding so it reads the LIVE array: F-4 replaces `handles[index]`
|
|
205
414
|
// mid-dispatch, and a captured array or a captured handle would answer for
|
|
@@ -279,6 +488,18 @@ export class TeamRuntime {
|
|
|
279
488
|
// ONE KICK PER REAL AGENT EVENT, unthrottled (I-OV3): the silence
|
|
280
489
|
// window resets on evidence, not on a coalesced UI frame.
|
|
281
490
|
onActivity: () => this.supervision?.kick(spec.label),
|
|
491
|
+
// A'2: under supervision the turn cap is ONE inspection, fired from the
|
|
492
|
+
// child's own `turn_end` at the crossing turn. Async-and-forgotten
|
|
493
|
+
// exactly like the silence timer's `onFire`: nothing in the child's
|
|
494
|
+
// event path may block or throw. ABSENT in the legacy regime, which
|
|
495
|
+
// keeps the hard stop - the hook's presence IS the gate.
|
|
496
|
+
...(this.overseer !== null
|
|
497
|
+
? {
|
|
498
|
+
onTurnCapReached: (label) => {
|
|
499
|
+
void this.inspectAndApply(dispatchId, label, 'turncap');
|
|
500
|
+
},
|
|
501
|
+
}
|
|
502
|
+
: {}),
|
|
282
503
|
});
|
|
283
504
|
// ASSIGNMENT, not a declaration: a `const` here would shadow the `let` above
|
|
284
505
|
// the bus, `canSend` would see the empty array forever, and every peer would
|
|
@@ -311,15 +532,24 @@ export class TeamRuntime {
|
|
|
311
532
|
// its slowest child and only an Esc ends it early. The lead's idle
|
|
312
533
|
// watchdog is paused for the whole dispatch either way, exactly as it is
|
|
313
534
|
// for the lead's own long turns.
|
|
535
|
+
//
|
|
536
|
+
// D-4, NOW FOR THE WHOLE DISPATCH TOO: with the supervisor gate open the
|
|
537
|
+
// ceiling NEVER arms a hard abort - the cadence ladder is what looks at a
|
|
538
|
+
// long dispatch, exactly as it already looks at a long child. The legacy
|
|
539
|
+
// hard `abortAll()` exists ONLY under `team.overseer: false`, the same
|
|
540
|
+
// regime line `runCtx.subagentTimeoutMs` below draws.
|
|
541
|
+
const dispatchCeilingMs = team.overseer ? 0 : team.dispatchTimeoutMs;
|
|
314
542
|
this.dispatchTimer =
|
|
315
|
-
|
|
316
|
-
? setTimeout(() => this.abortAll(), team.dispatchTimeoutMs)
|
|
317
|
-
: null;
|
|
543
|
+
dispatchCeilingMs > 0 ? setTimeout(() => this.abortAll(), dispatchCeilingMs) : null;
|
|
318
544
|
const runCtx = {
|
|
319
545
|
dispatchId,
|
|
320
546
|
handles,
|
|
321
547
|
makeHandle,
|
|
322
|
-
|
|
548
|
+
// D-4: with the supervisor gate open, `subagentTimeoutMs` NEVER arms a
|
|
549
|
+
// hard abort - 0 here arms no timer, so the key's only remaining role
|
|
550
|
+
// is the ladder's compat first-check (D-8). The legacy hard abort
|
|
551
|
+
// exists ONLY under `team.overseer: false`.
|
|
552
|
+
subagentTimeoutMs: team.overseer ? 0 : team.subagentTimeoutMs,
|
|
323
553
|
};
|
|
324
554
|
const cursor = { next: 0 };
|
|
325
555
|
const workers = Array.from({ length: Math.max(1, Math.min(team.maxConcurrent, handles.length)) }, () => this.worker(runCtx, cursor));
|
|
@@ -330,8 +560,17 @@ export class TeamRuntime {
|
|
|
330
560
|
signal?.removeEventListener('abort', onAbort);
|
|
331
561
|
for (const handle of handles)
|
|
332
562
|
handle.unsubscribe();
|
|
563
|
+
this.monitor?.dispose();
|
|
564
|
+
this.monitor = null;
|
|
333
565
|
this.supervision?.clearAll();
|
|
334
566
|
this.supervision = null;
|
|
567
|
+
// CAPTURED BEFORE THE FIELD IS NULLED: the outcome carries what the
|
|
568
|
+
// supervisor spent (D-7) and whether any tick ran unassisted (D-10),
|
|
569
|
+
// both read off the per-dispatch state being torn down here.
|
|
570
|
+
const overseerUsage = this.overseer?.usage();
|
|
571
|
+
const overseerCalls = this.overseer?.calls();
|
|
572
|
+
const overseerDegraded = this.overseerDegraded;
|
|
573
|
+
const overseerDegradedTicks = this.overseerDegradedTicks;
|
|
335
574
|
this.overseer = null;
|
|
336
575
|
// ONE TOTAL, AT THE LEAD'S TABLE. Every child runs the lead's own model
|
|
337
576
|
// (main-agent parity), so there is exactly one tier to sum and one price to
|
|
@@ -350,6 +589,13 @@ export class TeamRuntime {
|
|
|
350
589
|
leadMail: bus.leadMail(),
|
|
351
590
|
interventions: this.interventions,
|
|
352
591
|
usage,
|
|
592
|
+
// Honest supervision accounting (D-7 / D-10). `overseerCalls > 0`
|
|
593
|
+
// with zero usage is a provider that never reported usage - the
|
|
594
|
+
// report then omits the spend line rather than pricing nothing.
|
|
595
|
+
...(overseerUsage !== undefined ? { overseerUsage } : {}),
|
|
596
|
+
...(overseerCalls !== undefined ? { overseerCalls } : {}),
|
|
597
|
+
...(overseerDegraded ? { overseerDegraded: true } : {}),
|
|
598
|
+
...(overseerDegradedTicks > 0 ? { overseerDegradedTicks } : {}),
|
|
353
599
|
};
|
|
354
600
|
this.live = [];
|
|
355
601
|
this.bus = null;
|
|
@@ -400,13 +646,20 @@ export class TeamRuntime {
|
|
|
400
646
|
run.phase = 'starting';
|
|
401
647
|
run.startedAt = Date.now();
|
|
402
648
|
this.publish(dispatchId, run);
|
|
403
|
-
// WITH
|
|
404
|
-
//
|
|
405
|
-
// watchdog is the silence backstop.
|
|
406
|
-
//
|
|
649
|
+
// WITH THE SUPERVISOR GATE OPEN there is no hard wall clock on the
|
|
650
|
+
// child at all (D-4): the cadence ladder looks and decides, and the
|
|
651
|
+
// child's own (lengthened) idle watchdog is the silence backstop. The
|
|
652
|
+
// rung and the silence window are both armed HERE, at the attempt's
|
|
653
|
+
// actual start (R-P1-5) - a `queued` child has neither. WITHOUT the
|
|
654
|
+
// gate - `team.overseer: false`, the legacy regime - the hard timer is
|
|
655
|
+
// armed exactly as before, `0` meaning no ceiling. A provider that is
|
|
656
|
+
// wired but INACTIVE arms no timer either: the tick degrades to an
|
|
657
|
+
// announced unassisted wait, never a kill (R-P0-2).
|
|
407
658
|
const supervised = this.overseer !== null;
|
|
408
|
-
if (supervised)
|
|
659
|
+
if (supervised) {
|
|
409
660
|
this.supervision?.start(run.label);
|
|
661
|
+
this.monitor?.arm(run.label);
|
|
662
|
+
}
|
|
410
663
|
let timedOut = false;
|
|
411
664
|
const timer = !supervised && subagentTimeoutMs > 0
|
|
412
665
|
? setTimeout(() => {
|
|
@@ -462,6 +715,7 @@ export class TeamRuntime {
|
|
|
462
715
|
// wants diagnosed, and replace-after-death is exactly re-assignment.
|
|
463
716
|
await this.maybePostMortem(ctx, index, handle, verdict);
|
|
464
717
|
this.supervision?.clear(run.label);
|
|
718
|
+
this.monitor?.clear(run.label);
|
|
465
719
|
return;
|
|
466
720
|
}
|
|
467
721
|
this.replaceChild(dispatchId, handles, index, makeHandle, handle, null);
|
|
@@ -534,6 +788,7 @@ export class TeamRuntime {
|
|
|
534
788
|
const retries = (handle.run.retries ?? 0) + (verdict === null ? 1 : 0);
|
|
535
789
|
const replacements = (handle.run.replacements ?? 0) + (verdict !== null ? 1 : 0);
|
|
536
790
|
const interventions = handle.run.interventions;
|
|
791
|
+
const lastIntervention = handle.run.lastIntervention;
|
|
537
792
|
handle.unsubscribe();
|
|
538
793
|
try {
|
|
539
794
|
handle.abort();
|
|
@@ -562,6 +817,10 @@ export class TeamRuntime {
|
|
|
562
817
|
fresh.run.replacements = replacements;
|
|
563
818
|
if (interventions !== undefined)
|
|
564
819
|
fresh.run.interventions = interventions;
|
|
820
|
+
// The badge survives a rebuild (`replaced 5s`) - the panel row does not
|
|
821
|
+
// reset just because the body under it did.
|
|
822
|
+
if (lastIntervention !== undefined)
|
|
823
|
+
fresh.run.lastIntervention = lastIntervention;
|
|
565
824
|
if (verdict !== null)
|
|
566
825
|
this.overseer?.noteReplacement(handle.run.label);
|
|
567
826
|
handles[index] = fresh;
|
|
@@ -583,12 +842,14 @@ export class TeamRuntime {
|
|
|
583
842
|
return this.deps.humanQueue?.isWaiting(label) === true;
|
|
584
843
|
}
|
|
585
844
|
/**
|
|
586
|
-
* One trigger handled: look, then apply. Fired from a supervision
|
|
587
|
-
* therefore async-and-forgotten; nothing here may throw.
|
|
845
|
+
* One SILENCE trigger handled: look, then apply. Fired from a supervision
|
|
846
|
+
* timer and therefore async-and-forgotten; nothing here may throw.
|
|
588
847
|
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
848
|
+
* A NULL DECISION HAS THREE DISTINCT QUIETS: the look budget is spent ->
|
|
849
|
+
* say so ONCE and stop supervising this child (D-5); an inspection of this
|
|
850
|
+
* child is already in flight -> re-arm and let it settle (R-P1-4); the
|
|
851
|
+
* tier is gone -> clear quietly, because the cadence tick - not the
|
|
852
|
+
* silence path - owns the degraded announcements (D-4).
|
|
592
853
|
*/
|
|
593
854
|
async inspectAndApply(dispatchId, label, trigger) {
|
|
594
855
|
const overseer = this.overseer;
|
|
@@ -609,7 +870,17 @@ export class TeamRuntime {
|
|
|
609
870
|
childAlive: true,
|
|
610
871
|
});
|
|
611
872
|
if (decision === null || this.aborted) {
|
|
612
|
-
|
|
873
|
+
if (!this.aborted && overseer.looksExhausted(label)) {
|
|
874
|
+
this.noteQuiet(dispatchId, label);
|
|
875
|
+
supervision.clear(label);
|
|
876
|
+
this.monitor?.clear(label);
|
|
877
|
+
}
|
|
878
|
+
else if (!this.aborted && overseer.isInFlight(label)) {
|
|
879
|
+
supervision.rearmSilence(label);
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
supervision.clear(label);
|
|
883
|
+
}
|
|
613
884
|
return;
|
|
614
885
|
}
|
|
615
886
|
// The child may have settled while the call was in flight; an
|
|
@@ -621,9 +892,10 @@ export class TeamRuntime {
|
|
|
621
892
|
this.emit({ type: 'overseer', dispatchId, label, decision, trigger });
|
|
622
893
|
if (settled) {
|
|
623
894
|
supervision.clear(label);
|
|
895
|
+
this.monitor?.clear(label);
|
|
624
896
|
return;
|
|
625
897
|
}
|
|
626
|
-
this.applyDecision(
|
|
898
|
+
this.applyDecision(handle, label, decision);
|
|
627
899
|
}
|
|
628
900
|
catch {
|
|
629
901
|
// Defence in depth around an async timer callback: an unexpected throw
|
|
@@ -631,49 +903,88 @@ export class TeamRuntime {
|
|
|
631
903
|
supervision.rearmSilence(label);
|
|
632
904
|
}
|
|
633
905
|
}
|
|
634
|
-
applyDecision(
|
|
906
|
+
applyDecision(handle, label, decision) {
|
|
907
|
+
const supervision = this.supervision;
|
|
908
|
+
const overseer = this.overseer;
|
|
909
|
+
if (supervision === null || overseer === null)
|
|
910
|
+
return;
|
|
635
911
|
switch (decision.action) {
|
|
636
912
|
case 'wait':
|
|
637
|
-
|
|
913
|
+
// A model-chosen interval OVERRIDES the rung and becomes it (D-2);
|
|
914
|
+
// without one the rung is untouched - the silence window is the only
|
|
915
|
+
// clock this path re-arms.
|
|
916
|
+
if (decision.nextCheckMs !== undefined) {
|
|
917
|
+
this.monitor?.override(label, decision.nextCheckMs);
|
|
918
|
+
}
|
|
638
919
|
supervision.rearmSilence(label);
|
|
639
920
|
break;
|
|
640
|
-
case 'nudge':
|
|
921
|
+
case 'nudge': {
|
|
922
|
+
// THE NUDGE BUDGET IS CHECKED BEFORE STEERING (D-5): a fourth nudge
|
|
923
|
+
// is not advice, it is the supervisor refusing to decide - it waits.
|
|
924
|
+
if (!overseer.nudgesLeft(label)) {
|
|
925
|
+
supervision.rearmSilence(label);
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
641
928
|
// I-OV4: never steer into an abort already requested — the same
|
|
642
929
|
// guard-1 fact the child's fast reviewer reads. Queued steering is
|
|
643
930
|
// drained at the safe top-of-loop checkpoint by core.
|
|
644
931
|
if (!handle.isAborted() && decision.guidance !== undefined) {
|
|
645
932
|
handle.agent.steer(decision.guidance);
|
|
933
|
+
overseer.noteNudge(label);
|
|
646
934
|
}
|
|
647
|
-
|
|
935
|
+
this.monitor?.reset(label);
|
|
648
936
|
supervision.rearmSilence(label);
|
|
649
937
|
break;
|
|
938
|
+
}
|
|
650
939
|
case 'replace': {
|
|
651
|
-
//
|
|
940
|
+
// Budgets checked BEFORE the verdict is set, so two near-simultaneous
|
|
652
941
|
// replace decisions cannot both rebuild (the second becomes an
|
|
653
942
|
// abandon with the budget stated in the reason).
|
|
654
|
-
|
|
655
|
-
|
|
943
|
+
if (overseer.replacementsLeft(label)) {
|
|
944
|
+
handle.overseerVerdict = {
|
|
656
945
|
action: 'replace',
|
|
657
946
|
reason: decision.reason,
|
|
658
947
|
...(decision.guidance !== undefined ? { guidance: decision.guidance } : {}),
|
|
659
|
-
}
|
|
660
|
-
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
else if (overseer.abandonsLeft(label)) {
|
|
951
|
+
handle.overseerVerdict = {
|
|
952
|
+
action: 'abandon',
|
|
953
|
+
reason: `replacement budget exhausted; ${decision.reason}`,
|
|
954
|
+
};
|
|
955
|
+
overseer.noteAbandon(label);
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
// Both mutation budgets spent: the honest reading is wait (I-OV2).
|
|
959
|
+
supervision.rearmSilence(label);
|
|
960
|
+
this.monitor?.advance(label);
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
661
963
|
supervision.clear(label);
|
|
964
|
+
this.monitor?.clear(label);
|
|
662
965
|
handle.abort();
|
|
663
966
|
break;
|
|
664
967
|
}
|
|
665
968
|
case 'abandon':
|
|
969
|
+
if (!overseer.abandonsLeft(label)) {
|
|
970
|
+
supervision.rearmSilence(label);
|
|
971
|
+
this.monitor?.advance(label);
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
666
974
|
handle.overseerVerdict = { action: 'abandon', reason: decision.reason };
|
|
975
|
+
overseer.noteAbandon(label);
|
|
667
976
|
supervision.clear(label);
|
|
977
|
+
this.monitor?.clear(label);
|
|
668
978
|
handle.abort();
|
|
669
979
|
break;
|
|
670
980
|
}
|
|
671
981
|
}
|
|
672
982
|
/** Record one applied decision on the outcome, the run and the event stream. */
|
|
673
983
|
recordIntervention(dispatchId, label, trigger, decision) {
|
|
984
|
+
const at = Date.now();
|
|
674
985
|
this.interventions.push({
|
|
675
986
|
label,
|
|
676
|
-
at
|
|
987
|
+
at,
|
|
677
988
|
action: decision.action,
|
|
678
989
|
reason: decision.reason,
|
|
679
990
|
trigger,
|
|
@@ -681,9 +992,116 @@ export class TeamRuntime {
|
|
|
681
992
|
const handle = this.live.find((h) => h.run.label === label);
|
|
682
993
|
if (handle !== undefined) {
|
|
683
994
|
handle.run.interventions = (handle.run.interventions ?? 0) + 1;
|
|
995
|
+
// The panel's badge (AC-5): ABSOLUTE `at`, same units as `startedAt`
|
|
996
|
+
// (R-P2-3), so rendering is one subtraction.
|
|
997
|
+
handle.run.lastIntervention = {
|
|
998
|
+
action: decision.action,
|
|
999
|
+
at,
|
|
1000
|
+
reasonHead: decision.reason.slice(0, 60),
|
|
1001
|
+
};
|
|
684
1002
|
this.publish(dispatchId, handle.run);
|
|
685
1003
|
}
|
|
686
1004
|
}
|
|
1005
|
+
// -------------------------------------------------------------------------
|
|
1006
|
+
// The cadence loop's callbacks (subagent-overseer-v2 D-2 / D-3 / D-4)
|
|
1007
|
+
// -------------------------------------------------------------------------
|
|
1008
|
+
/** R-P1-5: a label ticks only once STARTED and not yet settled. */
|
|
1009
|
+
isMonitorLive(label) {
|
|
1010
|
+
const handle = this.live.find((h) => h.run.label === label);
|
|
1011
|
+
if (handle === undefined || handle.run.startedAt === undefined)
|
|
1012
|
+
return false;
|
|
1013
|
+
const phase = handle.run.phase;
|
|
1014
|
+
return phase !== 'done' && phase !== 'failed' && phase !== 'aborted';
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* One ASSISTED cadence tick: build the batch over the due labels, apply
|
|
1018
|
+
* every returned decision through the same `applyDecision` the silence
|
|
1019
|
+
* path uses (D-3's single application path), and hand the map back so the
|
|
1020
|
+
* loop can move the ladders. Labels the batch skipped (single-flight,
|
|
1021
|
+
* R-P1-4) are absent from the map.
|
|
1022
|
+
*/
|
|
1023
|
+
async monitorInspectBatch(dispatchId, labels) {
|
|
1024
|
+
const overseer = this.overseer;
|
|
1025
|
+
if (overseer === null || this.aborted)
|
|
1026
|
+
return new Map();
|
|
1027
|
+
const reqs = [];
|
|
1028
|
+
for (const label of labels) {
|
|
1029
|
+
const handle = this.live.find((h) => h.run.label === label);
|
|
1030
|
+
if (handle === undefined)
|
|
1031
|
+
continue;
|
|
1032
|
+
reqs.push({
|
|
1033
|
+
run: handle.run,
|
|
1034
|
+
messages: handle.agent.state.messages,
|
|
1035
|
+
trigger: 'clock',
|
|
1036
|
+
childAlive: true,
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
const decisions = await overseer.inspectBatch(reqs);
|
|
1040
|
+
for (const [label, decision] of decisions) {
|
|
1041
|
+
if (this.aborted)
|
|
1042
|
+
break;
|
|
1043
|
+
const handle = this.live.find((h) => h.run.label === label);
|
|
1044
|
+
const settled = handle === undefined ||
|
|
1045
|
+
handle.run.phase === 'done' ||
|
|
1046
|
+
handle.run.phase === 'failed' ||
|
|
1047
|
+
handle.run.phase === 'aborted';
|
|
1048
|
+
this.recordIntervention(dispatchId, label, 'clock', decision);
|
|
1049
|
+
this.emit({ type: 'overseer', dispatchId, label, decision, trigger: 'clock' });
|
|
1050
|
+
if (settled) {
|
|
1051
|
+
this.monitor?.clear(label);
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
this.applyDecision(handle, label, decision);
|
|
1055
|
+
}
|
|
1056
|
+
return decisions;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* One DEGRADED cadence tick (D-4 / D-10): the fast tier is missing right
|
|
1060
|
+
* now. The children wait unassisted - announced per child through the same
|
|
1061
|
+
* event shape, never killed - and the fact is edge-latched here, NOT in
|
|
1062
|
+
* the interventions ledger.
|
|
1063
|
+
*/
|
|
1064
|
+
noteDegradedTick(dispatchId, labels) {
|
|
1065
|
+
this.overseerDegraded = true;
|
|
1066
|
+
this.overseerDegradedTicks += 1;
|
|
1067
|
+
for (const label of labels) {
|
|
1068
|
+
this.emit({
|
|
1069
|
+
type: 'overseer',
|
|
1070
|
+
dispatchId,
|
|
1071
|
+
label,
|
|
1072
|
+
decision: DEGRADED_WAIT_DECISION,
|
|
1073
|
+
trigger: 'clock',
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
/** The supervisor goes quiet on one child, ONCE (D-5 / D-10). */
|
|
1078
|
+
noteQuiet(dispatchId, label) {
|
|
1079
|
+
if (this.quietNotified.has(label))
|
|
1080
|
+
return;
|
|
1081
|
+
this.quietNotified.add(label);
|
|
1082
|
+
const decision = {
|
|
1083
|
+
action: 'wait',
|
|
1084
|
+
reason: 'look budget exhausted; supervision quiet for this child',
|
|
1085
|
+
};
|
|
1086
|
+
this.recordIntervention(dispatchId, label, 'quiet', decision);
|
|
1087
|
+
this.emit({ type: 'overseer', dispatchId, label, decision, trigger: 'quiet' });
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* The dispatch-wide fool-proof total is spent (R-P1-6): stop the whole
|
|
1091
|
+
* loop and emit ONE aggregate notice under a pseudo-label that is never a
|
|
1092
|
+
* child slug.
|
|
1093
|
+
*/
|
|
1094
|
+
noteDispatchExhausted(dispatchId) {
|
|
1095
|
+
this.monitor?.stop();
|
|
1096
|
+
this.recordIntervention(dispatchId, MONITOR_TEAM_LABEL, 'quiet', MONITOR_EXHAUSTED_DECISION);
|
|
1097
|
+
this.emit({
|
|
1098
|
+
type: 'overseer',
|
|
1099
|
+
dispatchId,
|
|
1100
|
+
label: MONITOR_TEAM_LABEL,
|
|
1101
|
+
decision: MONITOR_EXHAUSTED_DECISION,
|
|
1102
|
+
trigger: 'quiet',
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
687
1105
|
/**
|
|
688
1106
|
* The final phase of an attempt, in a FIXED ORDER: aborted, then timed out,
|
|
689
1107
|
* then error, then no-summary, then done.
|
|
@@ -755,6 +1173,10 @@ export class TeamRuntime {
|
|
|
755
1173
|
}
|
|
756
1174
|
this.bus?.cancelAllWaits();
|
|
757
1175
|
this.supervision?.clearAll();
|
|
1176
|
+
// The cadence loop too: every child is being aborted, so no tick has
|
|
1177
|
+
// anything left to look at (an in-flight batch's decisions are dropped
|
|
1178
|
+
// by the aborted guard in `monitorInspectBatch`).
|
|
1179
|
+
this.monitor?.stop();
|
|
758
1180
|
// Every in-flight retry backoff, not just the last one registered: a
|
|
759
1181
|
// provider-wide failure puts up to `maxConcurrent` children here at once.
|
|
760
1182
|
for (const release of [...this.pendingBackoffs])
|
|
@@ -766,6 +1188,8 @@ export class TeamRuntime {
|
|
|
766
1188
|
if (this.dispatchTimer)
|
|
767
1189
|
clearTimeout(this.dispatchTimer);
|
|
768
1190
|
this.dispatchTimer = null;
|
|
1191
|
+
this.monitor?.dispose();
|
|
1192
|
+
this.monitor = null;
|
|
769
1193
|
this.supervision?.clearAll();
|
|
770
1194
|
this.supervision = null;
|
|
771
1195
|
this.overseer = null;
|