@aragon-agent/cli 0.6.4 → 0.6.7
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 +37 -0
- package/README.md +108 -11
- package/dist/agent/controller.d.ts +39 -2
- package/dist/agent/controller.js +125 -0
- package/dist/agent/reducer.d.ts +33 -19
- package/dist/agent/reducer.js +62 -71
- package/dist/agent/usage.d.ts +34 -0
- package/dist/agent/usage.js +43 -0
- package/dist/cli.js +48 -1
- package/dist/commands/builtins.js +9 -0
- package/dist/compaction/compactor.d.ts +60 -24
- package/dist/compaction/compactor.js +89 -61
- package/dist/compaction/context-command.d.ts +33 -0
- package/dist/compaction/context-command.js +166 -0
- package/dist/compaction/meter.d.ts +251 -0
- package/dist/compaction/meter.js +428 -0
- package/dist/compaction/pressure.d.ts +6 -0
- package/dist/compaction/pressure.js +4 -0
- package/dist/compaction/types.d.ts +41 -0
- package/dist/compaction/wiring.d.ts +47 -0
- package/dist/compaction/wiring.js +88 -27
- package/dist/config/cli-commands.js +5 -0
- package/dist/config/env.js +15 -1
- package/dist/config/load.d.ts +17 -0
- package/dist/config/load.js +45 -1
- package/dist/config/schema.d.ts +44 -0
- package/dist/config/schema.js +36 -0
- package/dist/config/store.js +27 -1
- package/dist/exec/events.d.ts +138 -1
- package/dist/exec/events.js +42 -0
- package/dist/exec/index.js +43 -6
- package/dist/exec/progress-reporter.d.ts +23 -0
- package/dist/exec/progress-reporter.js +88 -0
- package/dist/exec/runner.d.ts +34 -0
- package/dist/exec/runner.js +137 -25
- package/dist/fast/reviewer.d.ts +19 -1
- package/dist/fast/reviewer.js +22 -0
- package/dist/fast/types.d.ts +15 -0
- package/dist/fast/wiring.d.ts +10 -1
- package/dist/fast/wiring.js +18 -1
- package/dist/todo/limits.d.ts +4 -17
- package/dist/todo/limits.js +4 -17
- package/dist/todo/panel-rows.d.ts +8 -6
- package/dist/todo/panel-rows.js +19 -6
- package/dist/ui/App.js +75 -63
- package/dist/ui/AutocompletePopup.d.ts +3 -1
- package/dist/ui/AutocompletePopup.js +9 -11
- package/dist/ui/BottomStatusRow.d.ts +1 -1
- package/dist/ui/Composer.d.ts +3 -1
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/PromptInput.d.ts +4 -1
- package/dist/ui/PromptInput.js +19 -5
- package/dist/ui/StatusBar.d.ts +21 -15
- package/dist/ui/StatusBar.js +48 -9
- package/dist/ui/TeamPanel.d.ts +3 -1
- package/dist/ui/TeamPanel.js +13 -9
- package/dist/ui/TodoPanel.d.ts +5 -5
- package/dist/ui/TodoPanel.js +56 -24
- package/dist/ui/UpdateLine.d.ts +1 -1
- package/dist/ui/capabilities.js +5 -5
- package/dist/ui/exit-snapshot.d.ts +9 -0
- package/dist/ui/layout/ScrollViewport.d.ts +1 -1
- package/dist/ui/layout/autocomplete.d.ts +18 -0
- package/dist/ui/layout/autocomplete.js +26 -0
- package/dist/ui/layout/rail.d.ts +4 -53
- package/dist/ui/layout/rail.js +10 -62
- package/dist/ui/layout/team-panel.d.ts +18 -0
- package/dist/ui/layout/team-panel.js +22 -0
- package/dist/ui/layout/todo-layout.d.ts +24 -0
- package/dist/ui/layout/todo-layout.js +27 -0
- package/dist/ui/transcript-text.js +6 -2
- package/package.json +2 -2
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 { ExecProgressReporter } from './progress-reporter.js';
|
|
20
21
|
export function singlePrompt(text) {
|
|
21
22
|
let served = false;
|
|
22
23
|
return {
|
|
@@ -40,6 +41,7 @@ export class ExecRunner {
|
|
|
40
41
|
opts;
|
|
41
42
|
signals;
|
|
42
43
|
now;
|
|
44
|
+
progress;
|
|
43
45
|
controller = null;
|
|
44
46
|
unsubscribers = [];
|
|
45
47
|
turns = 0;
|
|
@@ -62,6 +64,7 @@ export class ExecRunner {
|
|
|
62
64
|
this.opts = options;
|
|
63
65
|
this.signals = options.signals ?? DEFAULT_SIGNAL_PORT;
|
|
64
66
|
this.now = options.now ?? Date.now;
|
|
67
|
+
this.progress = new ExecProgressReporter(this.sessionId, (event) => this.emit(event));
|
|
65
68
|
}
|
|
66
69
|
// -----------------------------------------------------------------------
|
|
67
70
|
// Attach / detach
|
|
@@ -103,20 +106,65 @@ export class ExecRunner {
|
|
|
103
106
|
});
|
|
104
107
|
if (todos)
|
|
105
108
|
this.unsubscribers.push(todos);
|
|
109
|
+
// FOUR BRANCHES, and the split is the whole of W2
|
|
110
|
+
// (web-use-tier-cooperation-and-control-closure §4.2.1):
|
|
111
|
+
//
|
|
112
|
+
// · `usage` — folded into the run's totals (unchanged).
|
|
113
|
+
// · `review_end` — the EXISTING `fast_review` still fires only when the
|
|
114
|
+
// critique has text, byte-for-byte as before; a
|
|
115
|
+
// `fast_tier` now fires UNCONDITIONALLY beside it, so a
|
|
116
|
+
// failed / empty / dropped review is finally visible.
|
|
117
|
+
// · `tier_changed` — a `fast_tier` with no `review` sub-object. This is the
|
|
118
|
+
// one that carries `selfDisabled: true`, because the
|
|
119
|
+
// reviewer emits it right after switching itself off.
|
|
120
|
+
// · `review_start` — DROPPED. `inFlight` on the snapshot already says it,
|
|
121
|
+
// and once every N turns it is pure noise.
|
|
106
122
|
const fast = controller.subscribeFast?.((event) => {
|
|
107
123
|
if (event.type === 'usage') {
|
|
108
124
|
this.usage.inputTokens += event.usage.inputTokens;
|
|
109
125
|
this.usage.outputTokens += event.usage.outputTokens;
|
|
110
126
|
return;
|
|
111
127
|
}
|
|
112
|
-
if (event.type
|
|
128
|
+
if (event.type === 'review_start')
|
|
129
|
+
return;
|
|
130
|
+
if (event.type === 'review_end' && event.review.text) {
|
|
131
|
+
this.emit({
|
|
132
|
+
type: 'fast_review',
|
|
133
|
+
sessionId: this.sessionId,
|
|
134
|
+
turn: event.review.turn,
|
|
135
|
+
model: event.review.model,
|
|
136
|
+
text: event.review.text,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
// `null` means this session never registered a tier, and a session with no
|
|
140
|
+
// tier has no business appearing on this channel at all.
|
|
141
|
+
const snapshot = controller.fastSnapshot?.();
|
|
142
|
+
if (!snapshot)
|
|
113
143
|
return;
|
|
114
144
|
this.emit({
|
|
115
|
-
type: '
|
|
145
|
+
type: 'fast_tier',
|
|
116
146
|
sessionId: this.sessionId,
|
|
117
|
-
turn:
|
|
118
|
-
|
|
119
|
-
|
|
147
|
+
turn: this.turns,
|
|
148
|
+
live: snapshot.live,
|
|
149
|
+
selfDisabled: snapshot.selfDisabled,
|
|
150
|
+
model: snapshot.model,
|
|
151
|
+
sameAsMain: snapshot.sameAsMain,
|
|
152
|
+
reviews: snapshot.reviews,
|
|
153
|
+
reviewBudget: snapshot.reviewBudget,
|
|
154
|
+
budgetReached: snapshot.budgetReached,
|
|
155
|
+
delegated: snapshot.delegated,
|
|
156
|
+
inFlight: snapshot.inFlight,
|
|
157
|
+
...(event.type === 'review_end'
|
|
158
|
+
? {
|
|
159
|
+
review: {
|
|
160
|
+
index: event.review.index,
|
|
161
|
+
kind: event.review.kind,
|
|
162
|
+
...(event.review.detail !== undefined ? { detail: event.review.detail } : {}),
|
|
163
|
+
durationMs: event.review.durationMs,
|
|
164
|
+
injected: event.review.injected,
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
: {}),
|
|
120
168
|
});
|
|
121
169
|
});
|
|
122
170
|
if (fast)
|
|
@@ -132,6 +180,7 @@ export class ExecRunner {
|
|
|
132
180
|
return;
|
|
133
181
|
}
|
|
134
182
|
if (event.type === 'compaction_start') {
|
|
183
|
+
this.progress.record('compaction');
|
|
135
184
|
this.emit({
|
|
136
185
|
type: 'compaction',
|
|
137
186
|
sessionId: this.sessionId,
|
|
@@ -143,6 +192,7 @@ export class ExecRunner {
|
|
|
143
192
|
}
|
|
144
193
|
if (event.type !== 'compaction_end')
|
|
145
194
|
return;
|
|
195
|
+
this.progress.record('model');
|
|
146
196
|
const r = event.record;
|
|
147
197
|
this.emit({
|
|
148
198
|
type: 'compaction',
|
|
@@ -177,6 +227,7 @@ export class ExecRunner {
|
|
|
177
227
|
if (this.detached)
|
|
178
228
|
return;
|
|
179
229
|
this.detached = true;
|
|
230
|
+
this.progress.dispose();
|
|
180
231
|
for (const off of this.unsubscribers.splice(0)) {
|
|
181
232
|
try {
|
|
182
233
|
off();
|
|
@@ -241,15 +292,42 @@ export class ExecRunner {
|
|
|
241
292
|
// nothing more to say exits 0 with `end_turn`, not 3 with `max_turns`.
|
|
242
293
|
if (next === null)
|
|
243
294
|
break;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
295
|
+
const requestSeq = ++this.requestSeq;
|
|
296
|
+
this.requestHadError = false;
|
|
297
|
+
this.progress.begin(requestSeq);
|
|
298
|
+
this.emit({ type: 'turn_state', sessionId: this.sessionId, requestSeq, phase: 'started' });
|
|
299
|
+
this.progress.started(requestSeq);
|
|
300
|
+
try {
|
|
301
|
+
if (this.isRequestCancelled())
|
|
302
|
+
break;
|
|
303
|
+
if (this.turnBudgetExhausted()) {
|
|
304
|
+
this.stopReason = 'max_turns';
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
const finished = await this.runFollowThroughChain(controller, next, followThrough);
|
|
308
|
+
if (finished)
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
this.errored = true;
|
|
313
|
+
this.recordError('agent_error', err instanceof Error ? err.message : String(err));
|
|
314
|
+
throw err;
|
|
315
|
+
}
|
|
316
|
+
finally {
|
|
317
|
+
this.progress.finish(requestSeq);
|
|
318
|
+
const phase = this.isRequestCancelled()
|
|
319
|
+
? 'cancelled'
|
|
320
|
+
: this.requestHadError ? 'failed' : 'completed';
|
|
321
|
+
this.emit({ type: 'turn_state', sessionId: this.sessionId, requestSeq, phase });
|
|
247
322
|
}
|
|
248
|
-
const finished = await this.runFollowThroughChain(controller, next, followThrough);
|
|
249
|
-
if (finished)
|
|
250
|
-
break;
|
|
251
323
|
}
|
|
252
324
|
}
|
|
325
|
+
requestSeq = 0;
|
|
326
|
+
requestHadError = false;
|
|
327
|
+
isRequestCancelled() {
|
|
328
|
+
return this.stopReason === 'interrupted' || this.stopReason === 'timeout' ||
|
|
329
|
+
this.stopReason === 'max_turns';
|
|
330
|
+
}
|
|
253
331
|
/** One caller message plus every auto-continuation it produces. */
|
|
254
332
|
async runFollowThroughChain(controller, initial, followThrough) {
|
|
255
333
|
let budget = emptyBudget();
|
|
@@ -281,7 +359,7 @@ export class ExecRunner {
|
|
|
281
359
|
// `aborted` is false here for the same structural reason `-p` gives: an
|
|
282
360
|
// interrupt sets `stopReason` and returns above, so this branch is only
|
|
283
361
|
// reached by a run that ended on its own terms.
|
|
284
|
-
runEnd: { aborted: false, errored: this.
|
|
362
|
+
runEnd: { aborted: false, errored: this.requestHadError },
|
|
285
363
|
budget,
|
|
286
364
|
interactive: false,
|
|
287
365
|
});
|
|
@@ -394,18 +472,39 @@ export class ExecRunner {
|
|
|
394
472
|
* this and add the getter.
|
|
395
473
|
*/
|
|
396
474
|
installTerminator() {
|
|
397
|
-
this.signals.setTerminator((signo) =>
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
475
|
+
this.signals.setTerminator((signo) => this.onSignal(signo));
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Exactly synonymous with a SIGINT, only it does not travel through a signal.
|
|
479
|
+
*
|
|
480
|
+
* The stdin `{"type":"interrupt"}` frame routes here. It used to route through
|
|
481
|
+
* `process.kill(process.pid, 'SIGINT')`, which is fine on POSIX and WRONG on
|
|
482
|
+
* Windows: Node does not deliver a self-directed SIGINT to its own handlers
|
|
483
|
+
* there, so the process terminates with exit code 1 having run neither the
|
|
484
|
+
* terminator below, nor the settle path, nor `persist()`, nor the `result`
|
|
485
|
+
* event. A wrapper that interrupts a turn and respawns on the same
|
|
486
|
+
* `--session-id` would silently get a conversation missing everything since the
|
|
487
|
+
* last save - and `exec/index.ts` was already designed for exactly that caller
|
|
488
|
+
* ("PERSIST FIRST, ANNOUNCE SECOND").
|
|
489
|
+
*
|
|
490
|
+
* POSIX behaviour is byte-for-byte unchanged: the same closure runs, with
|
|
491
|
+
* `signalNumber = 2`, `exitCode() = 130`, and the two-second second-signal
|
|
492
|
+
* escape hatch intact.
|
|
493
|
+
*/
|
|
494
|
+
requestInterrupt() {
|
|
495
|
+
this.onSignal(2);
|
|
496
|
+
}
|
|
497
|
+
onSignal(signo) {
|
|
498
|
+
const at = this.now();
|
|
499
|
+
const secondSignal = at - this.lastSignalAt <= SECOND_SIGNAL_WINDOW_MS;
|
|
500
|
+
if (this.detached || (this.lastSignalAt > 0 && secondSignal)) {
|
|
501
|
+
this.signals.exit(128 + signo);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
this.lastSignalAt = at;
|
|
505
|
+
this.signalNumber = signo;
|
|
506
|
+
this.stopReason = 'interrupted';
|
|
507
|
+
this.controller?.abort();
|
|
409
508
|
}
|
|
410
509
|
// -----------------------------------------------------------------------
|
|
411
510
|
// Event translation
|
|
@@ -431,6 +530,7 @@ export class ExecRunner {
|
|
|
431
530
|
this.checkTurnBudget();
|
|
432
531
|
break;
|
|
433
532
|
case 'tool_execution_start':
|
|
533
|
+
this.progress.record('tool');
|
|
434
534
|
this.emit({
|
|
435
535
|
type: 'tool_call',
|
|
436
536
|
sessionId: this.sessionId,
|
|
@@ -441,6 +541,7 @@ export class ExecRunner {
|
|
|
441
541
|
});
|
|
442
542
|
break;
|
|
443
543
|
case 'tool_execution_end':
|
|
544
|
+
this.progress.record('model');
|
|
444
545
|
this.emit({
|
|
445
546
|
type: 'tool_result',
|
|
446
547
|
sessionId: this.sessionId,
|
|
@@ -457,7 +558,7 @@ export class ExecRunner {
|
|
|
457
558
|
break;
|
|
458
559
|
case 'agent_end':
|
|
459
560
|
// A swallowed throw (missing key / empty stream) produces no `turn_end`.
|
|
460
|
-
if (!this.sawTurnEnd && !this.
|
|
561
|
+
if (!this.sawTurnEnd && !this.requestHadError && !this.isRequestCancelled()) {
|
|
461
562
|
this.errored = true;
|
|
462
563
|
this.recordError('agent_error', 'The run produced no assistant turn.');
|
|
463
564
|
}
|
|
@@ -467,6 +568,16 @@ export class ExecRunner {
|
|
|
467
568
|
}
|
|
468
569
|
}
|
|
469
570
|
onStreamEvent(se) {
|
|
571
|
+
if (se.type === 'text_delta' && se.delta)
|
|
572
|
+
this.progress.record('model');
|
|
573
|
+
if (se.type === 'thinking_delta' && se.delta)
|
|
574
|
+
this.progress.record('thinking');
|
|
575
|
+
if (se.type === 'tool_call_delta' && se.argsDelta)
|
|
576
|
+
this.progress.record('tool_arguments');
|
|
577
|
+
if (se.type === 'retry_scheduled')
|
|
578
|
+
this.progress.record('retry', se.delayMs);
|
|
579
|
+
if (se.type === 'retry_attempt')
|
|
580
|
+
this.progress.record('model');
|
|
470
581
|
if (se.type === 'text_delta') {
|
|
471
582
|
this.flushThinking();
|
|
472
583
|
if (this.opts.partialMessages) {
|
|
@@ -527,6 +638,7 @@ export class ExecRunner {
|
|
|
527
638
|
this.emit({ type: 'thinking', sessionId: this.sessionId, turn: this.turns + 1, text });
|
|
528
639
|
}
|
|
529
640
|
recordError(code, message) {
|
|
641
|
+
this.requestHadError = true;
|
|
530
642
|
if (this.errorCode)
|
|
531
643
|
return;
|
|
532
644
|
this.errorCode = code;
|
package/dist/fast/reviewer.d.ts
CHANGED
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
import type { AgentEvent, AssistantMessage, LLMRequest, TokenUsage } from '@aragon-agent/core';
|
|
60
60
|
import type { FastConfig } from '../config/schema.js';
|
|
61
61
|
import type { NoticeLevel } from '../agent/reducer.js';
|
|
62
|
-
import type { FastEvent, FastTier } from './types.js';
|
|
62
|
+
import type { FastEvent, FastSnapshot, FastTier } from './types.js';
|
|
63
63
|
/**
|
|
64
64
|
* Everything the reviewer needs from the rest of the world.
|
|
65
65
|
*
|
|
@@ -93,6 +93,14 @@ export interface FastReviewerDeps {
|
|
|
93
93
|
available(): boolean;
|
|
94
94
|
getApiKey(providerId: string): string | undefined;
|
|
95
95
|
emit(event: FastEvent): void;
|
|
96
|
+
/**
|
|
97
|
+
* `FastWiring.snapshot()`, so the self-disable can announce itself.
|
|
98
|
+
*
|
|
99
|
+
* The reviewer cannot build a `FastSnapshot` on its own — `live`,
|
|
100
|
+
* `sameAsMain`, `delegated` and `pricingUnknown` all belong to the wiring —
|
|
101
|
+
* and the two existing `tier_changed` emitters read it the same way.
|
|
102
|
+
*/
|
|
103
|
+
snapshot(): FastSnapshot;
|
|
96
104
|
notify(level: NoticeLevel, text: string): void;
|
|
97
105
|
/** Injected clock. A test that reads the wall clock fails on a slow machine
|
|
98
106
|
* and nowhere else - the reason `TodoStore` and `TeamPanel` take one. */
|
|
@@ -155,6 +163,16 @@ export declare class FastReviewer {
|
|
|
155
163
|
isBudgetReached(): boolean;
|
|
156
164
|
sessionUsage(): TokenUsage;
|
|
157
165
|
isReviewInFlight(): boolean;
|
|
166
|
+
/**
|
|
167
|
+
* Whether this reviewer switched itself off after
|
|
168
|
+
* `FAST_LIMITS.maxConsecutiveFailures` non-transient failures in a row.
|
|
169
|
+
*
|
|
170
|
+
* Carried onto `FastSnapshot` so a wrapper can say "the fast reviews stopped,
|
|
171
|
+
* and here is why" instead of showing a tier that reads as healthy while not
|
|
172
|
+
* one review ever arrives. Same shape as `reviewCount()` / `isBudgetReached()`
|
|
173
|
+
* — a read-only accessor, no side effects.
|
|
174
|
+
*/
|
|
175
|
+
isSelfDisabled(): boolean;
|
|
158
176
|
/**
|
|
159
177
|
* `AgentController.abort()`.
|
|
160
178
|
*
|
package/dist/fast/reviewer.js
CHANGED
|
@@ -141,6 +141,18 @@ export class FastReviewer {
|
|
|
141
141
|
isReviewInFlight() {
|
|
142
142
|
return this.inFlight;
|
|
143
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Whether this reviewer switched itself off after
|
|
146
|
+
* `FAST_LIMITS.maxConsecutiveFailures` non-transient failures in a row.
|
|
147
|
+
*
|
|
148
|
+
* Carried onto `FastSnapshot` so a wrapper can say "the fast reviews stopped,
|
|
149
|
+
* and here is why" instead of showing a tier that reads as healthy while not
|
|
150
|
+
* one review ever arrives. Same shape as `reviewCount()` / `isBudgetReached()`
|
|
151
|
+
* — a read-only accessor, no side effects.
|
|
152
|
+
*/
|
|
153
|
+
isSelfDisabled() {
|
|
154
|
+
return this.selfDisabled;
|
|
155
|
+
}
|
|
144
156
|
/**
|
|
145
157
|
* `AgentController.abort()`.
|
|
146
158
|
*
|
|
@@ -561,6 +573,16 @@ export class FastReviewer {
|
|
|
561
573
|
this.selfDisabled = true;
|
|
562
574
|
this.deps.notify('warn', `Fast review disabled for this session after ${FAST_LIMITS.maxConsecutiveFailures} ` +
|
|
563
575
|
`failures. Last error: ${errorText(err)}`);
|
|
576
|
+
// THE ONE LINE THAT MAKES THE FLAG OBSERVABLE OUTSIDE THIS PROCESS.
|
|
577
|
+
//
|
|
578
|
+
// `emitReview({kind:'failed'})` above already fired, carrying a snapshot
|
|
579
|
+
// in which `selfDisabled` was still `false`, and after this point the
|
|
580
|
+
// reviewer emits NOTHING EVER AGAIN (`shouldReview()` opens with
|
|
581
|
+
// `if (this.selfDisabled) return false;`). Without this extra
|
|
582
|
+
// `tier_changed`, the field exists, the accessor exists, the snapshot line
|
|
583
|
+
// exists — and no consumer can ever see it as `true`. The `App` chip also
|
|
584
|
+
// consumes `tier_changed`; one more refresh is free.
|
|
585
|
+
this.deps.emit({ type: 'tier_changed', snapshot: this.deps.snapshot() });
|
|
564
586
|
}
|
|
565
587
|
}
|
|
566
588
|
// -----------------------------------------------------------------------
|
package/dist/fast/types.d.ts
CHANGED
|
@@ -78,6 +78,21 @@ export interface FastReview {
|
|
|
78
78
|
export interface FastSnapshot {
|
|
79
79
|
/** The tier resolved AND the live switch is on. */
|
|
80
80
|
live: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* The reviewer switched ITSELF off after `maxConsecutiveFailures` in a row
|
|
83
|
+
* (`FastReviewer.onReviewFailure`).
|
|
84
|
+
*
|
|
85
|
+
* ORTHOGONAL TO `live`, AND NEITHER IMPLIES THE OTHER. `live` answers "did
|
|
86
|
+
* this tier resolve at all" and is `registered && enabled && tier.ok`; the
|
|
87
|
+
* self-disable touches none of those three, so `live` stays `true` after it.
|
|
88
|
+
* A consumer that wants to report "the fast reviews stopped happening" MUST
|
|
89
|
+
* read this field: deriving it as `live === false` is a predicate that can
|
|
90
|
+
* never be true, and a hand-written fixture makes its test green for ever.
|
|
91
|
+
*
|
|
92
|
+
* `available()` is deliberately NOT widened to include it — see the note on
|
|
93
|
+
* `FastWiring.available`.
|
|
94
|
+
*/
|
|
95
|
+
selfDisabled: boolean;
|
|
81
96
|
model: string;
|
|
82
97
|
sameAsMain: boolean;
|
|
83
98
|
reviews: number;
|
package/dist/fast/wiring.d.ts
CHANGED
|
@@ -115,7 +115,16 @@ export declare class FastWiring {
|
|
|
115
115
|
private getFastRegistry;
|
|
116
116
|
isRegistered(): boolean;
|
|
117
117
|
isEnabled(): boolean;
|
|
118
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* §3.3's live predicate. THE THIRD TERM IS THE POINT - see the header.
|
|
120
|
+
*
|
|
121
|
+
* `FastReviewer.isSelfDisabled()` is DELIBERATELY NOT a fourth term. Folding
|
|
122
|
+
* it in would look tidier and would break something else: `delegationAvailable()`
|
|
123
|
+
* is `available() && config.fast.delegate === true`, so three consecutive
|
|
124
|
+
* REVIEW failures would silently switch DELEGATION off as well - turning a
|
|
125
|
+
* diagnostic signal into a behaviour change for every user. The two facts are
|
|
126
|
+
* reported separately on `FastSnapshot` instead (`live` / `selfDisabled`).
|
|
127
|
+
*/
|
|
119
128
|
available(): boolean;
|
|
120
129
|
/**
|
|
121
130
|
* Whether `model: "fast"` on `task` is honoured right now.
|
package/dist/fast/wiring.js
CHANGED
|
@@ -53,6 +53,9 @@ export function offFastStatus() {
|
|
|
53
53
|
tier: { ok: false, reason: 'disabled' },
|
|
54
54
|
snapshot: {
|
|
55
55
|
live: false,
|
|
56
|
+
// A tier that never existed was never switched off by its reviewer, so
|
|
57
|
+
// `false` here is the truth and not a placeholder.
|
|
58
|
+
selfDisabled: false,
|
|
56
59
|
model: '',
|
|
57
60
|
sameAsMain: false,
|
|
58
61
|
reviews: 0,
|
|
@@ -113,6 +116,9 @@ export class FastWiring {
|
|
|
113
116
|
available: () => this.available(),
|
|
114
117
|
getApiKey: deps.getApiKey,
|
|
115
118
|
emit: (event) => this.emit(event),
|
|
119
|
+
// The reviewer announces its own self-disable with a `tier_changed`, and
|
|
120
|
+
// only the wiring can build the snapshot that carries it.
|
|
121
|
+
snapshot: () => this.snapshot(),
|
|
116
122
|
notify: deps.notify,
|
|
117
123
|
});
|
|
118
124
|
}
|
|
@@ -158,7 +164,16 @@ export class FastWiring {
|
|
|
158
164
|
isEnabled() {
|
|
159
165
|
return this.enabled;
|
|
160
166
|
}
|
|
161
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* §3.3's live predicate. THE THIRD TERM IS THE POINT - see the header.
|
|
169
|
+
*
|
|
170
|
+
* `FastReviewer.isSelfDisabled()` is DELIBERATELY NOT a fourth term. Folding
|
|
171
|
+
* it in would look tidier and would break something else: `delegationAvailable()`
|
|
172
|
+
* is `available() && config.fast.delegate === true`, so three consecutive
|
|
173
|
+
* REVIEW failures would silently switch DELEGATION off as well - turning a
|
|
174
|
+
* diagnostic signal into a behaviour change for every user. The two facts are
|
|
175
|
+
* reported separately on `FastSnapshot` instead (`live` / `selfDisabled`).
|
|
176
|
+
*/
|
|
162
177
|
available() {
|
|
163
178
|
return this.registered && this.enabled && this.tier.ok;
|
|
164
179
|
}
|
|
@@ -272,6 +287,8 @@ export class FastWiring {
|
|
|
272
287
|
const ref = this.tier.ok ? this.tier.ref : null;
|
|
273
288
|
return {
|
|
274
289
|
live: this.available(),
|
|
290
|
+
// ORTHOGONAL TO `live`, never derivable from it (see `FastSnapshot`).
|
|
291
|
+
selfDisabled: this.reviewer.isSelfDisabled(),
|
|
275
292
|
model: ref ? ref.modelId : '',
|
|
276
293
|
sameAsMain: this.tier.ok ? this.tier.sameAsMain : false,
|
|
277
294
|
// THE NUMERATOR, THE DENOMINATOR AND THE FLAG ARE READ IN ONE EXPRESSION,
|
package/dist/todo/limits.d.ts
CHANGED
|
@@ -32,13 +32,10 @@ export declare const TODO_LIMITS: {
|
|
|
32
32
|
* there (§3.3 step 2).
|
|
33
33
|
*/
|
|
34
34
|
readonly minFreshItems: 2;
|
|
35
|
-
/**
|
|
36
|
-
readonly
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
* the rail is not mounted at all.
|
|
40
|
-
*/
|
|
41
|
-
readonly panelMinRows: 6;
|
|
35
|
+
/** Minimum shared middle rows for the compact rail. */
|
|
36
|
+
readonly panelMinRows: 3;
|
|
37
|
+
/** Full rail includes a blank separator and separate overflow markers. */
|
|
38
|
+
readonly panelFullRows: 6;
|
|
42
39
|
/** Below this many terminal columns the status-bar counter degrades to `[3/7]`. */
|
|
43
40
|
readonly statusCompactCols: 100;
|
|
44
41
|
/**
|
|
@@ -51,16 +48,6 @@ export declare const TODO_LIMITS: {
|
|
|
51
48
|
* component was a third policy or a typo for one of them.
|
|
52
49
|
*/
|
|
53
50
|
readonly stripDoneCols: 80;
|
|
54
|
-
/**
|
|
55
|
-
* Rows `todoRailRows()` subtracts while a team dispatch is live (§3.9 / P1-3).
|
|
56
|
-
*
|
|
57
|
-
* `TEAM_LIMITS.panelMaxRows` (5) + header + `+N more` + the mail line = 8, and
|
|
58
|
-
* this is deliberately the FULL worst case rather than a measurement: an
|
|
59
|
-
* over-subtraction costs one item row, an under-subtraction eats the `+N
|
|
60
|
-
* below` marker - the one row whose absence is indistinguishable from "the
|
|
61
|
-
* list is short" - and those two mistakes are not equally bad.
|
|
62
|
-
*/
|
|
63
|
-
readonly railReservedRows: 8;
|
|
64
51
|
/**
|
|
65
52
|
* Consecutive user turns an UNFINISHED list survives without a `todo_write`
|
|
66
53
|
* before `beginUserTurn()` drops it (§3.2 / P1-7). Three keeps the common
|
package/dist/todo/limits.js
CHANGED
|
@@ -32,13 +32,10 @@ export const TODO_LIMITS = {
|
|
|
32
32
|
* there (§3.3 step 2).
|
|
33
33
|
*/
|
|
34
34
|
minFreshItems: 2,
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
* the rail is not mounted at all.
|
|
40
|
-
*/
|
|
41
|
-
panelMinRows: 6,
|
|
35
|
+
/** Minimum shared middle rows for the compact rail. */
|
|
36
|
+
panelMinRows: 3,
|
|
37
|
+
/** Full rail includes a blank separator and separate overflow markers. */
|
|
38
|
+
panelFullRows: 6,
|
|
42
39
|
/** Below this many terminal columns the status-bar counter degrades to `[3/7]`. */
|
|
43
40
|
statusCompactCols: 100,
|
|
44
41
|
/**
|
|
@@ -51,16 +48,6 @@ export const TODO_LIMITS = {
|
|
|
51
48
|
* component was a third policy or a typo for one of them.
|
|
52
49
|
*/
|
|
53
50
|
stripDoneCols: 80,
|
|
54
|
-
/**
|
|
55
|
-
* Rows `todoRailRows()` subtracts while a team dispatch is live (§3.9 / P1-3).
|
|
56
|
-
*
|
|
57
|
-
* `TEAM_LIMITS.panelMaxRows` (5) + header + `+N more` + the mail line = 8, and
|
|
58
|
-
* this is deliberately the FULL worst case rather than a measurement: an
|
|
59
|
-
* over-subtraction costs one item row, an under-subtraction eats the `+N
|
|
60
|
-
* below` marker - the one row whose absence is indistinguishable from "the
|
|
61
|
-
* list is short" - and those two mistakes are not equally bad.
|
|
62
|
-
*/
|
|
63
|
-
railReservedRows: 8,
|
|
64
51
|
/**
|
|
65
52
|
* Consecutive user turns an UNFINISHED list survives without a `todo_write`
|
|
66
53
|
* before `beginUserTurn()` drops it (§3.2 / P1-7). Three keeps the common
|
|
@@ -37,17 +37,19 @@ export interface TodoRowSelection {
|
|
|
37
37
|
export declare function todoAnchorIndex(items: TodoItem[]): number;
|
|
38
38
|
/**
|
|
39
39
|
* @param maxRows Rows available for the item rows AND the two overflow markers
|
|
40
|
-
* together.
|
|
41
|
-
* honest while a team dispatch is live (I-10 / P1-3).
|
|
40
|
+
* together. The shared layout subtracts actual team/menu rows.
|
|
42
41
|
*
|
|
43
42
|
* THE ANCHOR IS ALWAYS VISIBLE, and that beats the row bound when the two
|
|
44
43
|
* conflict. They can only conflict below `maxRows` 3 with overflow on BOTH
|
|
45
44
|
* sides — one item plus two markers is three rows — where this returns the
|
|
46
45
|
* anchor and lets the caller's `overflow="hidden"` clip a marker. Hiding the
|
|
47
46
|
* one row the user is watching in order to keep a `+N above` would be the wrong
|
|
48
|
-
* trade.
|
|
49
|
-
*
|
|
50
|
-
* caller with a smaller budget should read this paragraph before assuming
|
|
51
|
-
* otherwise.
|
|
47
|
+
* trade. The full rail has at least four item rows. The compact rail uses
|
|
48
|
+
* `selectCompactTodoRows` with a single merged footer instead.
|
|
52
49
|
*/
|
|
53
50
|
export declare function selectTodoRows(items: TodoItem[], maxRows: number): TodoRowSelection;
|
|
51
|
+
/** Select an anchored contiguous window without reserving overflow-marker rows.
|
|
52
|
+
* maxItemRows is a row count; invalid/nonpositive values return an empty window
|
|
53
|
+
* with every item counted below it. Does not mutate the list or throw.
|
|
54
|
+
*/
|
|
55
|
+
export declare function selectCompactTodoRows(items: TodoItem[], maxItemRows: number): TodoRowSelection;
|
package/dist/todo/panel-rows.js
CHANGED
|
@@ -58,18 +58,15 @@ function windowFor(count, anchor, slots) {
|
|
|
58
58
|
}
|
|
59
59
|
/**
|
|
60
60
|
* @param maxRows Rows available for the item rows AND the two overflow markers
|
|
61
|
-
* together.
|
|
62
|
-
* honest while a team dispatch is live (I-10 / P1-3).
|
|
61
|
+
* together. The shared layout subtracts actual team/menu rows.
|
|
63
62
|
*
|
|
64
63
|
* THE ANCHOR IS ALWAYS VISIBLE, and that beats the row bound when the two
|
|
65
64
|
* conflict. They can only conflict below `maxRows` 3 with overflow on BOTH
|
|
66
65
|
* sides — one item plus two markers is three rows — where this returns the
|
|
67
66
|
* anchor and lets the caller's `overflow="hidden"` clip a marker. Hiding the
|
|
68
67
|
* one row the user is watching in order to keep a `+N above` would be the wrong
|
|
69
|
-
* trade.
|
|
70
|
-
*
|
|
71
|
-
* caller with a smaller budget should read this paragraph before assuming
|
|
72
|
-
* otherwise.
|
|
68
|
+
* trade. The full rail has at least four item rows. The compact rail uses
|
|
69
|
+
* `selectCompactTodoRows` with a single merged footer instead.
|
|
73
70
|
*/
|
|
74
71
|
export function selectTodoRows(items, maxRows) {
|
|
75
72
|
const count = items.length;
|
|
@@ -101,3 +98,19 @@ export function selectTodoRows(items, maxRows) {
|
|
|
101
98
|
visible.push({ item: items[i], index: i });
|
|
102
99
|
return { visible, hiddenAbove: start, hiddenBelow: count - end };
|
|
103
100
|
}
|
|
101
|
+
/** Select an anchored contiguous window without reserving overflow-marker rows.
|
|
102
|
+
* maxItemRows is a row count; invalid/nonpositive values return an empty window
|
|
103
|
+
* with every item counted below it. Does not mutate the list or throw.
|
|
104
|
+
*/
|
|
105
|
+
export function selectCompactTodoRows(items, maxItemRows) {
|
|
106
|
+
const slots = Number.isFinite(maxItemRows) ? Math.max(0, Math.floor(maxItemRows)) : 0;
|
|
107
|
+
if (slots === 0 || items.length === 0) {
|
|
108
|
+
return { visible: [], hiddenAbove: 0, hiddenBelow: items.length };
|
|
109
|
+
}
|
|
110
|
+
const { start, end } = windowFor(items.length, todoAnchorIndex(items), slots);
|
|
111
|
+
return {
|
|
112
|
+
visible: items.slice(start, end).map((item, offset) => ({ item, index: start + offset })),
|
|
113
|
+
hiddenAbove: start,
|
|
114
|
+
hiddenBelow: items.length - end,
|
|
115
|
+
};
|
|
116
|
+
}
|