@aragon-agent/cli 0.6.3 → 0.6.4
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 +105 -0
- package/README.md +95 -2
- package/dist/agent/controller.d.ts +119 -0
- package/dist/agent/controller.js +275 -1
- package/dist/agent/reducer.d.ts +69 -0
- package/dist/agent/reducer.js +100 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +84 -22
- package/dist/commands/builtins.js +92 -1
- package/dist/compaction/child.js +5 -0
- package/dist/compaction/command.js +14 -1
- package/dist/compaction/compactor.d.ts +34 -0
- package/dist/compaction/compactor.js +158 -37
- package/dist/compaction/types.d.ts +36 -0
- package/dist/compaction/wiring.d.ts +9 -0
- package/dist/compaction/wiring.js +22 -1
- package/dist/config/env.d.ts +1 -1
- package/dist/config/env.js +48 -1
- package/dist/config/load.d.ts +11 -0
- package/dist/config/load.js +39 -1
- package/dist/config/schema.d.ts +85 -0
- package/dist/config/schema.js +33 -0
- package/dist/config/store.js +21 -1
- package/dist/input/limits.d.ts +72 -0
- package/dist/input/limits.js +63 -0
- package/dist/input/paste-parse.d.ts +71 -0
- package/dist/input/paste-parse.js +95 -0
- package/dist/input/stdin-filter.d.ts +81 -0
- package/dist/input/stdin-filter.js +379 -0
- package/dist/logging/install.d.ts +2 -0
- package/dist/logging/install.js +53 -1
- package/dist/proc/classify.d.ts +52 -0
- package/dist/proc/classify.js +143 -0
- package/dist/proc/kill-tree.d.ts +42 -0
- package/dist/proc/kill-tree.js +119 -0
- package/dist/proc/limits.d.ts +84 -0
- package/dist/proc/limits.js +84 -0
- package/dist/proc/log-ring.d.ts +68 -0
- package/dist/proc/log-ring.js +102 -0
- package/dist/proc/prompt.d.ts +24 -0
- package/dist/proc/prompt.js +49 -0
- package/dist/proc/readiness.d.ts +102 -0
- package/dist/proc/readiness.js +219 -0
- package/dist/proc/supervisor.d.ts +103 -0
- package/dist/proc/supervisor.js +465 -0
- package/dist/proc/types.d.ts +175 -0
- package/dist/proc/types.js +15 -0
- package/dist/session/persist.js +36 -1
- package/dist/tools/bash-tool.d.ts +51 -2
- package/dist/tools/bash-tool.js +321 -51
- package/dist/tools/fs-tools.d.ts +17 -0
- package/dist/tools/index.d.ts +55 -2
- package/dist/tools/index.js +39 -1
- package/dist/tools/proc-tools.d.ts +25 -0
- package/dist/tools/proc-tools.js +102 -0
- package/dist/ui/App.d.ts +15 -2
- package/dist/ui/App.js +256 -7
- package/dist/ui/Composer.d.ts +54 -1
- package/dist/ui/Composer.js +50 -6
- package/dist/ui/PromptInput.d.ts +49 -3
- package/dist/ui/PromptInput.js +184 -52
- package/dist/ui/StatusBar.d.ts +19 -0
- package/dist/ui/StatusBar.js +5 -2
- package/dist/ui/Transcript.js +29 -1
- package/dist/ui/composer-limits.d.ts +31 -0
- package/dist/ui/composer-limits.js +50 -0
- package/dist/ui/composer-rows.d.ts +73 -0
- package/dist/ui/composer-rows.js +201 -0
- package/dist/ui/editor-reducer.d.ts +37 -0
- package/dist/ui/editor-reducer.js +88 -12
- package/dist/ui/entries/ServiceCard.d.ts +66 -0
- package/dist/ui/entries/ServiceCard.js +116 -0
- package/dist/ui/entries/UserEntry.d.ts +17 -3
- package/dist/ui/entries/UserEntry.js +34 -5
- package/dist/ui/layout/budget.d.ts +34 -6
- package/dist/ui/layout/budget.js +32 -7
- package/dist/ui/layout/virtual-window.js +51 -1
- package/dist/ui/overlays/HelpOverlay.js +3 -0
- package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
- package/dist/ui/overlays/QuestionOverlay.js +5 -1
- package/dist/ui/overlays/SettingsScreen.js +14 -1
- package/dist/ui/paste-frames.d.ts +87 -0
- package/dist/ui/paste-frames.js +112 -0
- package/dist/ui/paste-tokens.d.ts +90 -0
- package/dist/ui/paste-tokens.js +147 -0
- package/dist/ui/screen.d.ts +16 -0
- package/dist/ui/screen.js +45 -2
- package/dist/ui/selection/selection-controller.d.ts +1 -1
- package/dist/ui/transcript-text.js +49 -0
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- package/package.json +2 -2
- package/dist/input/stdin-mouse-filter.d.ts +0 -47
- package/dist/input/stdin-mouse-filter.js +0 -153
package/dist/cli.js
CHANGED
|
@@ -44,7 +44,7 @@ import { decideRenderMode } from './ui/layout/frame.js';
|
|
|
44
44
|
import { createFrameDiffer } from './ui/frame-differ.js';
|
|
45
45
|
import { wrapStdoutForFrames } from './ui/stdout-frame-writer.js';
|
|
46
46
|
import { setFrameStatsProvider } from './commands/perf.js';
|
|
47
|
-
import {
|
|
47
|
+
import { tryCreateStdinFilter } from './input/stdin-filter.js';
|
|
48
48
|
import { enterAltScreen, writeExitTranscript } from './ui/screen.js';
|
|
49
49
|
import { supportsWindowsVtInput } from './ui/win-vt-input.js';
|
|
50
50
|
import { forceWindowsVtInput } from './ui/win-vt-force.js';
|
|
@@ -113,6 +113,11 @@ function toFlags(opts) {
|
|
|
113
113
|
// on `CliFlags`, AND a line here; `config.test.ts` is the regression pin that
|
|
114
114
|
// exists because `--no-mouse` shipped inert once already.
|
|
115
115
|
mouseSelect: opts.mouseSelect,
|
|
116
|
+
// Site three of three for `--paste` / `--no-paste` -- TEN features, one
|
|
117
|
+
// trap. A flag needs the `.option()` declaration below, a field on
|
|
118
|
+
// `CliFlags`, AND a line here; `config.test.ts` is the regression pin that
|
|
119
|
+
// exists because `--no-mouse` shipped inert once already.
|
|
120
|
+
paste: opts.paste,
|
|
116
121
|
skills: opts.skills,
|
|
117
122
|
skill: opts.skill,
|
|
118
123
|
skillsYes: opts.skillsYes,
|
|
@@ -416,7 +421,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
416
421
|
// before it reaches this process, so `\x1b[?1000h` is a pure no-op and the
|
|
417
422
|
// filter has nothing to filter. Gating `wantMouse` — rather than the three
|
|
418
423
|
// things downstream of it — is what keeps the three in agreement: the
|
|
419
|
-
// `?1000h` write (`enterAltScreen(..., { mouse:
|
|
424
|
+
// `?1000h` write (`enterAltScreen(..., { mouse: mouseOn })`), the
|
|
420
425
|
// filter itself, and the `mouseSource` prop that decides whether the user is
|
|
421
426
|
// advised about a mode that is not running. Wiring any of them separately
|
|
422
427
|
// re-opens the gap where the app claims a capability it does not have.
|
|
@@ -434,10 +439,36 @@ function runInteractive(flags, extras = {}) {
|
|
|
434
439
|
!!process.stdout.isTTY &&
|
|
435
440
|
!!process.stdin.isTTY &&
|
|
436
441
|
vtInputSupported;
|
|
437
|
-
|
|
438
|
-
|
|
442
|
+
// --- Paste handling (tui-paste-handling section 5.1.1 / D-13 / D-17). ----
|
|
443
|
+
//
|
|
444
|
+
// THE GATE IS WIDER THAN THE MOUSE'S, AND `paste` DEFAULTS TO TRUE. Paste
|
|
445
|
+
// needs no `vtInputSupported` and no full-screen: Tier 2 is a heuristic on
|
|
446
|
+
// chunk shape and needs no terminal cooperation at all, which is exactly why
|
|
447
|
+
// it exists for the consoles that never deliver `\x1b[200~` (R-11).
|
|
448
|
+
//
|
|
449
|
+
// WHAT THIS WIDENING BREAKS IF IT IS NOT PAIRED WITH `mouseOn` BELOW (P0-1).
|
|
450
|
+
// `filter !== null` used to be the answer to FIVE questions, only one of which
|
|
451
|
+
// is really about the stream. With paste able to build a filter on its own,
|
|
452
|
+
// `aragon --no-mouse` would get a non-null handle and every one of those five
|
|
453
|
+
// would flip true: `?1000h`/`?1006h`/`?1002h` written, terminal selection taken
|
|
454
|
+
// away (the ONLY reason anyone passes `--no-mouse`), and a startup notice
|
|
455
|
+
// advising the user about a mode they just disabled.
|
|
456
|
+
const wantPaste = config.paste && !!process.stdin.isTTY;
|
|
457
|
+
const pasteBridge = { notify: null };
|
|
458
|
+
const filter = wantMouse || wantPaste
|
|
459
|
+
? tryCreateStdinFilter(process.stdin, { mouse: wantMouse, paste: wantPaste }, (reason) => logger.warn('cli', 'stdin_filter_failed', { reason }), pasteBridge)
|
|
439
460
|
: null;
|
|
440
|
-
|
|
461
|
+
/**
|
|
462
|
+
* THE ONE EXPRESSION EVERY MOUSE SITE READS (I-11 / D-17).
|
|
463
|
+
*
|
|
464
|
+
* `filter !== null` now answers exactly one question -- "is a stream wrapped?"
|
|
465
|
+
* -- and the only site allowed to keep reading it is the `stdin` handed to
|
|
466
|
+
* `render()`.
|
|
467
|
+
*/
|
|
468
|
+
const mouseOn = wantMouse && filter !== null;
|
|
469
|
+
/** Same discipline for DEC 2004: already decided here, never re-derived. */
|
|
470
|
+
const pasteOn = wantPaste && filter !== null;
|
|
471
|
+
const disposeStdinFilter = () => filter?.dispose();
|
|
441
472
|
// --- Drag-select (tui-selection-and-scroll-follow §4.4). ------------------
|
|
442
473
|
//
|
|
443
474
|
// ONE EXPRESSION, READ BY FOUR THINGS, and that is the same discipline
|
|
@@ -446,7 +477,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
446
477
|
// disagree about whether drag-select is running. With it false the session is
|
|
447
478
|
// byte-identical to a pre-feature build — no extra escape on the wire (AC-8),
|
|
448
479
|
// no controller, no `decorate`, no mirror.
|
|
449
|
-
const wantSelect =
|
|
480
|
+
const wantSelect = mouseOn && config.mouseSelect;
|
|
450
481
|
const selectionBridge = {
|
|
451
482
|
controller: null,
|
|
452
483
|
onCopied: null,
|
|
@@ -526,11 +557,21 @@ function runInteractive(flags, extras = {}) {
|
|
|
526
557
|
}));
|
|
527
558
|
};
|
|
528
559
|
if (mode === 'fullscreen') {
|
|
529
|
-
// `mouse` here means "a filter is installed", NOT "the user
|
|
530
|
-
// support" — see `AltScreenOptions`. `screen.ts` must never
|
|
531
|
-
// itself, or a later refactor re-opens the gap I-8 closes.
|
|
532
|
-
// the same discipline for `?1002h
|
|
533
|
-
|
|
560
|
+
// `mouse` here means "a mouse-parsing filter is installed", NOT "the user
|
|
561
|
+
// wants mouse support" — see `AltScreenOptions`. `screen.ts` must never
|
|
562
|
+
// derive it for itself, or a later refactor re-opens the gap I-8 closes.
|
|
563
|
+
// `motion` carries the same discipline for `?1002h`, and `bracketedPaste`
|
|
564
|
+
// for `?2004h`: all three are already decided, none is re-derived.
|
|
565
|
+
//
|
|
566
|
+
// IT IS `mouseOn`, NOT `filter !== null` (P0-1). A `--no-mouse` session with
|
|
567
|
+
// paste at its default `true` still builds a filter, and passing the handle
|
|
568
|
+
// here would write `?1000h` at a user who disabled the mouse precisely to
|
|
569
|
+
// keep their terminal's own selection.
|
|
570
|
+
screen = enterAltScreen(process.stdout, {
|
|
571
|
+
mouse: mouseOn,
|
|
572
|
+
motion: wantSelect,
|
|
573
|
+
bracketedPaste: pasteOn && mode === 'fullscreen',
|
|
574
|
+
});
|
|
534
575
|
const restore = () => screen?.restore();
|
|
535
576
|
// Four idempotent restore paths (§4.4). The signal hook is NOT redundant:
|
|
536
577
|
// - `waitUntilExit().then()` is a microtask and may never be reached when
|
|
@@ -613,10 +654,10 @@ function runInteractive(flags, extras = {}) {
|
|
|
613
654
|
// than inside `screen.ts`'s closure because `App` needs to READ it and the
|
|
614
655
|
// handle's own flag is private (it has to be — `restore()` reads it, and
|
|
615
656
|
// exposing a setter would be a second way to get the two out of step).
|
|
616
|
-
let mouseCaptured =
|
|
617
|
-
if (wantSelect &&
|
|
657
|
+
let mouseCaptured = mouseOn;
|
|
658
|
+
if (wantSelect && filter) {
|
|
618
659
|
selectionBridge.controller = createSelectionController({
|
|
619
|
-
source:
|
|
660
|
+
source: filter.source,
|
|
620
661
|
// The frame writer is absent under `--no-diff-render`, where there is no
|
|
621
662
|
// cache to repaint from at all; the controller then falls back to asking
|
|
622
663
|
// `App` for a React redraw on every drag flush, which is slower and
|
|
@@ -656,7 +697,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
656
697
|
const terminalBridge = mode === 'fullscreen' || writeForeign
|
|
657
698
|
? {
|
|
658
699
|
mouseSelect: wantSelect,
|
|
659
|
-
...(
|
|
700
|
+
...(mouseOn
|
|
660
701
|
? {
|
|
661
702
|
setMouseCapture: (on) => {
|
|
662
703
|
screen?.setMouseCapture(on);
|
|
@@ -682,13 +723,21 @@ function runInteractive(flags, extras = {}) {
|
|
|
682
723
|
!!process.stdin.isTTY &&
|
|
683
724
|
!process.env.CI &&
|
|
684
725
|
config.update.mode !== 'off';
|
|
685
|
-
const instance = render(_jsx(App, { controller: controller, version: VERSION, mode: mode, confirmBridge: confirmBridge, humanInputBridge: humanInputBridge, initialPrompt: extras.initialPrompt, initialOverlay: extras.initialOverlay,
|
|
726
|
+
const instance = render(_jsx(App, { controller: controller, version: VERSION, mode: mode, confirmBridge: confirmBridge, humanInputBridge: humanInputBridge, initialPrompt: extras.initialPrompt, initialOverlay: extras.initialOverlay,
|
|
727
|
+
// `mouseOn`, NOT the handle (I-11). `App.tsx:186-188` documents this
|
|
728
|
+
// prop's contract as "absent under `--no-mouse`, inline, or non-TTY", and
|
|
729
|
+
// it feeds `enabled: !!mouseSource` on a wheel subscription that would
|
|
730
|
+
// otherwise be a channel no event can ever arrive on.
|
|
731
|
+
mouseSource: mouseOn && filter ? filter.source : undefined,
|
|
686
732
|
// Present ONLY when this console swallows `CSI Z` and mouse reports.
|
|
687
733
|
// Passed in rather than probed inside `App` so that every test renders
|
|
688
734
|
// the same tree on every machine — reading `process.versions.node` from
|
|
689
735
|
// a component would make the notice appear or vanish according to which
|
|
690
736
|
// Node the developer happened to have installed.
|
|
691
|
-
vtInputWarning: vtInputSupported ? undefined : { nodeVersion: process.versions.node }, updateBridge: updateEligible ? updateBridge : undefined,
|
|
737
|
+
vtInputWarning: vtInputSupported ? undefined : { nodeVersion: process.versions.node }, updateBridge: updateEligible ? updateBridge : undefined,
|
|
738
|
+
// Absent when nothing can refuse a paste, so a `--no-paste` tree is
|
|
739
|
+
// byte-identical to a build without this feature.
|
|
740
|
+
pasteBridge: pasteOn ? pasteBridge : undefined, terminal: terminalBridge }),
|
|
692
741
|
// Full-screen owns console.* itself (I-4): Ink's patchConsole writes
|
|
693
742
|
// straight to stdout and permanently shifts the fixed frame's accounting.
|
|
694
743
|
{
|
|
@@ -696,7 +745,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
696
745
|
patchConsole: mode === 'inline',
|
|
697
746
|
// I-5: with mouse support off the REAL stdin is handed over unwrapped, so
|
|
698
747
|
// every existing path is provably unchanged.
|
|
699
|
-
stdin:
|
|
748
|
+
stdin: filter?.stdin ?? process.stdin,
|
|
700
749
|
// SPREAD, not `frameWriter?.stdout ?? process.stdout`. This call passed no
|
|
701
750
|
// `stdout` at all before, and Ink defaults it to `process.stdout`; omitting
|
|
702
751
|
// the key entirely when the writer is off keeps the options object the same
|
|
@@ -757,12 +806,12 @@ function runInteractive(flags, extras = {}) {
|
|
|
757
806
|
.waitUntilExit()
|
|
758
807
|
.then(() => {
|
|
759
808
|
screen?.restore();
|
|
760
|
-
// BEFORE `
|
|
809
|
+
// BEFORE `disposeStdinFilter()`, which clears the listener set it subscribed to,
|
|
761
810
|
// and after `restore()` for the reason that call records: the terminal must
|
|
762
811
|
// stop reporting first, and anything already in flight is still consumed by
|
|
763
812
|
// a live filter.
|
|
764
813
|
disposeSelection();
|
|
765
|
-
|
|
814
|
+
disposeStdinFilter();
|
|
766
815
|
// BOTH BRANCHES, the same discipline `controller.dispose()` and
|
|
767
816
|
// `updateService?.dispose()` already follow. It drops the `'resize'`
|
|
768
817
|
// listener of I-8 and detaches the `/perf` provider so a later `/perf` in a
|
|
@@ -786,12 +835,12 @@ function runInteractive(flags, extras = {}) {
|
|
|
786
835
|
})
|
|
787
836
|
.catch(() => {
|
|
788
837
|
screen?.restore();
|
|
789
|
-
// BEFORE `
|
|
838
|
+
// BEFORE `disposeStdinFilter()`, which clears the listener set it subscribed to,
|
|
790
839
|
// and after `restore()` for the reason that call records: the terminal must
|
|
791
840
|
// stop reporting first, and anything already in flight is still consumed by
|
|
792
841
|
// a live filter.
|
|
793
842
|
disposeSelection();
|
|
794
|
-
|
|
843
|
+
disposeStdinFilter();
|
|
795
844
|
disposeFrameWriter();
|
|
796
845
|
controller.dispose();
|
|
797
846
|
updateService?.dispose();
|
|
@@ -911,6 +960,10 @@ const CONFIG_SET_KEYS = new Set([
|
|
|
911
960
|
// records: a key listed here but absent there writes nothing and still prints
|
|
912
961
|
// `Set mouseSelect = false`.
|
|
913
962
|
'mouseSelect',
|
|
963
|
+
// Listed here AND cased in the switch below, for the reason the note above
|
|
964
|
+
// `mouse` records: a key present here but absent there falls through every
|
|
965
|
+
// case, writes nothing, and still prints `Set paste = false`.
|
|
966
|
+
'paste',
|
|
914
967
|
'scrollResumeMs',
|
|
915
968
|
'historyEnabled',
|
|
916
969
|
'planModeDefault',
|
|
@@ -1109,6 +1162,9 @@ function runConfigSet(key, value) {
|
|
|
1109
1162
|
case 'mouseSelect':
|
|
1110
1163
|
patch.mouseSelect = value === 'true' || value === '1';
|
|
1111
1164
|
break;
|
|
1165
|
+
case 'paste':
|
|
1166
|
+
patch.paste = value === 'true' || value === '1';
|
|
1167
|
+
break;
|
|
1112
1168
|
// Clamped rather than rejected, for the same reason `planModeDefault` below
|
|
1113
1169
|
// records: hardening only the READ path leaves a bad value on disk that
|
|
1114
1170
|
// reverts to the default on every launch, which presents as "my setting
|
|
@@ -1381,6 +1437,12 @@ function buildProgram() {
|
|
|
1381
1437
|
// `false` on every run that passes no flag at all.
|
|
1382
1438
|
.option('--mouse-select', 'Drag with the mouse to select text; releasing copies it')
|
|
1383
1439
|
.option('--no-mouse-select', 'Keep wheel scrolling, but leave drag-select off')
|
|
1440
|
+
// Positive form FIRST, same tri-state reason as `--mouse-select` above:
|
|
1441
|
+
// `paste` is persisted and defaults to `true`, so a lone `--no-paste` would
|
|
1442
|
+
// make commander default `opts.paste` to `true` -- indistinguishable from
|
|
1443
|
+
// silence, and silently overriding a stored `false` on every run.
|
|
1444
|
+
.option('--paste', 'Collapse large pastes into a placeholder (the default)')
|
|
1445
|
+
.option('--no-paste', 'Treat pasted bytes as keystrokes (pre-0.6.3 behavior)')
|
|
1384
1446
|
// Declaring `--fullscreen` BEFORE `--no-fullscreen` keeps the default
|
|
1385
1447
|
// `undefined` instead of `true`, which is what makes the tri-state work:
|
|
1386
1448
|
// "no opinion" must stay distinguishable from "force it".
|
|
@@ -7,7 +7,7 @@ import { existsSync, statSync } from 'node:fs';
|
|
|
7
7
|
import { isAbsolute, resolve } from 'node:path';
|
|
8
8
|
import { resolveOutputTokens } from '@aragon-agent/core';
|
|
9
9
|
import { clampThinkingLevel, clampTheme, isAdapterProvider, isThemeName, parseMaxTokensInput, ADAPTER_PROVIDERS, DEFAULT_MAX_TOKENS, HARD_MAX_RETRIES, LEGACY_DARK_THEME, THEME_NAMES, THINKING_LEVELS, } from '../config/schema.js';
|
|
10
|
-
import { computeCost, formatCost, formatTokens } from '../agent/usage.js';
|
|
10
|
+
import { computeCost, formatCost, formatDuration, formatTokens } from '../agent/usage.js';
|
|
11
11
|
import { TODO_FOLLOW_LIMITS } from '../todo/limits.js';
|
|
12
12
|
import { buildContinuationMessage } from '../todo/follow-through.js';
|
|
13
13
|
import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
|
|
@@ -1048,6 +1048,95 @@ const COMMANDS = [
|
|
|
1048
1048
|
// render layer, and threading `ui/render-cache.js` into this file would make
|
|
1049
1049
|
// every other command's import graph pull the renderer in with it.
|
|
1050
1050
|
perfCommand,
|
|
1051
|
+
{
|
|
1052
|
+
name: 'bg',
|
|
1053
|
+
description: 'Background services: list | logs <id> [n] | stop <id|all> | status',
|
|
1054
|
+
/**
|
|
1055
|
+
* THE USER'S HANDLE ON WHAT THE AGENT LEFT RUNNING (§5.3).
|
|
1056
|
+
*
|
|
1057
|
+
* It is the LIVE view, and that is why it exists at all: the transcript
|
|
1058
|
+
* records EVENTS about a service (D-11) — a card printed into `<Static>`
|
|
1059
|
+
* cannot be rewritten — so "what is running right now" lives here, in the
|
|
1060
|
+
* status chip, and in `bash_output`. Without this command a user whose
|
|
1061
|
+
* server card scrolled away an hour ago has no way to name it.
|
|
1062
|
+
*/
|
|
1063
|
+
run: async (ctx) => {
|
|
1064
|
+
const arg = ctx.args.trim();
|
|
1065
|
+
const [verb = 'list', value, count] = arg.split(/\s+/);
|
|
1066
|
+
const controller = ctx.controller;
|
|
1067
|
+
if (!controller.isBackgroundRegistered()) {
|
|
1068
|
+
ctx.notify('info', 'Background services are off for this session (bash.background).');
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
const services = controller.listServices();
|
|
1072
|
+
if (verb === 'status') {
|
|
1073
|
+
const live = controller.liveServiceCount();
|
|
1074
|
+
ctx.notify('info', live === 0
|
|
1075
|
+
? 'No background services running.'
|
|
1076
|
+
: `${live} background service${live === 1 ? '' : 's'} running.`);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
if (verb === 'logs') {
|
|
1080
|
+
if (!value) {
|
|
1081
|
+
ctx.notify('warn', 'Usage: /bg logs <id> [n]');
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
const service = controller.getServiceSnapshot(value);
|
|
1085
|
+
if (!service) {
|
|
1086
|
+
ctx.notify('warn', `No service "${value}".`);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const page = controller.readServiceLog(value);
|
|
1090
|
+
const n = count ? Number.parseInt(count, 10) : DEFAULT_BG_LOG_ROWS;
|
|
1091
|
+
const rows = page?.rows ?? [];
|
|
1092
|
+
const take = Number.isFinite(n) && n > 0 ? n : DEFAULT_BG_LOG_ROWS;
|
|
1093
|
+
const shown = rows.slice(-take);
|
|
1094
|
+
ctx.notify('info', shown.length === 0
|
|
1095
|
+
? `service ${service.id}: no output yet.`
|
|
1096
|
+
: `service ${service.id} (last ${shown.length} rows)\n${shown.join('\n')}`);
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (verb === 'stop') {
|
|
1100
|
+
if (!value) {
|
|
1101
|
+
ctx.notify('warn', 'Usage: /bg stop <id|all>');
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
// AWAITED, unlike `doExit`'s force path: a slash command runs on a live
|
|
1105
|
+
// event loop, so the graceful SIGTERM -> SIGKILL ladder can run and a
|
|
1106
|
+
// dev server gets its chance to release its port cleanly.
|
|
1107
|
+
const stopped = await controller.stopService(value);
|
|
1108
|
+
if (stopped.length === 0) {
|
|
1109
|
+
ctx.notify('info', 'Nothing to stop.');
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const leaked = stopped.filter((svc) => svc.killIncomplete);
|
|
1113
|
+
ctx.toast('success', `Stopped ${stopped.length} service${stopped.length === 1 ? '' : 's'}.`);
|
|
1114
|
+
if (leaked.length > 0) {
|
|
1115
|
+
// AN HONEST WARNING BEATS A SILENT LEAK (R-2 / P2-6). `taskkill /t`
|
|
1116
|
+
// cannot reach a grandchild that re-parented itself, and saying so is
|
|
1117
|
+
// the difference between a puzzle and a `netstat` the user knows to run.
|
|
1118
|
+
ctx.notify('warn', `${leaked.map((svc) => svc.id).join(', ')} may have left a detached child - ` +
|
|
1119
|
+
'check with netstat/lsof if a port is still held.');
|
|
1120
|
+
}
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (verb !== 'list' && verb !== '') {
|
|
1124
|
+
ctx.notify('warn', `Unknown argument "${verb}" - use /bg [list|logs|stop|status].`);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
if (services.length === 0) {
|
|
1128
|
+
ctx.notify('info', 'No background services this session.');
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
const now = Date.now();
|
|
1132
|
+
const lines = services.map((svc) => {
|
|
1133
|
+
const uptime = formatDuration((svc.endedAt ?? now) - svc.startedAt);
|
|
1134
|
+
const where = svc.url ? ` ${svc.url}` : '';
|
|
1135
|
+
return `${svc.id} ${svc.status} ${uptime}${where} $ ${svc.command}`;
|
|
1136
|
+
});
|
|
1137
|
+
ctx.notify('info', `Background services:\n${lines.join('\n')}`);
|
|
1138
|
+
},
|
|
1139
|
+
},
|
|
1051
1140
|
{
|
|
1052
1141
|
name: 'exit',
|
|
1053
1142
|
aliases: ['quit'],
|
|
@@ -1055,6 +1144,8 @@ const COMMANDS = [
|
|
|
1055
1144
|
run: (ctx) => ctx.exit(),
|
|
1056
1145
|
},
|
|
1057
1146
|
];
|
|
1147
|
+
/** Rows `/bg logs <id>` shows when the user names no count. */
|
|
1148
|
+
const DEFAULT_BG_LOG_ROWS = 40;
|
|
1058
1149
|
export function registerBuiltinCommands(registry) {
|
|
1059
1150
|
for (const command of COMMANDS)
|
|
1060
1151
|
registry.register(command);
|
package/dist/compaction/child.js
CHANGED
|
@@ -85,6 +85,11 @@ export function createChildContextManager(req, deps) {
|
|
|
85
85
|
}
|
|
86
86
|
if (event.type !== 'compaction_end')
|
|
87
87
|
return;
|
|
88
|
+
// DEFENCE IN DEPTH SINCE quiet-noop D-8: a child compaction that declines
|
|
89
|
+
// before committing to work no longer emits this event at all, so this
|
|
90
|
+
// filter is now unreachable on that path. It is retained deliberately -
|
|
91
|
+
// it is this tree's own record that a no-op compaction is a non-event, and
|
|
92
|
+
// deleting it would make the child depend on that rule holding forever.
|
|
88
93
|
if (!event.record.applied && event.record.mode === 'none')
|
|
89
94
|
return;
|
|
90
95
|
req.onCompacted();
|
|
@@ -66,9 +66,22 @@ export function formatCompactionStatus(ctx) {
|
|
|
66
66
|
lines.push(` Summarizer: ${snapshot.model || '(none)'}` +
|
|
67
67
|
(config.useFastTier ? ' (fast tier preferred)' : ' (fast tier not used)'));
|
|
68
68
|
lines.push(` On summarize failure: ${config.onFailure}`);
|
|
69
|
+
// THE PARENTHETICAL IS BUILT FROM WHATEVER APPLIES, AND OMITTED WHEN NOTHING
|
|
70
|
+
// DOES (quiet-noop D-9). `declined` counts checkpoints that fired and chose to
|
|
71
|
+
// do nothing - the only human-readable trace of a decline once the transcript
|
|
72
|
+
// stopped carrying a card for one. It renders ONLY when non-zero: a counter
|
|
73
|
+
// that says `(0 checkpoints declined)` in every healthy session would have
|
|
74
|
+
// replaced one piece of noise with another.
|
|
75
|
+
const sessionNotes = [];
|
|
76
|
+
if (snapshot.generation > 0)
|
|
77
|
+
sessionNotes.push(`generation ${snapshot.generation}`);
|
|
78
|
+
const declined = snapshot.declined ?? 0;
|
|
79
|
+
if (declined > 0) {
|
|
80
|
+
sessionNotes.push(`${declined} ${declined === 1 ? 'checkpoint' : 'checkpoints'} declined`);
|
|
81
|
+
}
|
|
69
82
|
lines.push(` This session: ${snapshot.compactions} ` +
|
|
70
83
|
`${snapshot.compactions === 1 ? 'compaction' : 'compactions'}` +
|
|
71
|
-
(
|
|
84
|
+
(sessionNotes.length > 0 ? ` (${sessionNotes.join(', ')})` : '') +
|
|
72
85
|
`, ${formatTokens(snapshot.tokensReclaimed)} tokens reclaimed`);
|
|
73
86
|
const cost = snapshot.pricingUnknown
|
|
74
87
|
? `unknown (no price table for ${snapshot.model || 'the summarizer'})`
|
|
@@ -78,6 +78,15 @@ export declare class Compactor implements ContextManager {
|
|
|
78
78
|
private compactionsThisRun;
|
|
79
79
|
private lastCompactionTurn;
|
|
80
80
|
private compactions;
|
|
81
|
+
/**
|
|
82
|
+
* How many `compact()` calls returned WITHOUT announcing (quiet-noop D-9).
|
|
83
|
+
*
|
|
84
|
+
* A SESSION TOTAL, LIKE `compactions`, and reset by neither `onRunStart()` nor
|
|
85
|
+
* `clearSelfDisable()`: `/compact on` overrides a guard, it does not rewrite
|
|
86
|
+
* history. `/compact status` is its only reader, and it is the whole of what
|
|
87
|
+
* keeps a decline reportable once the transcript stops carrying one.
|
|
88
|
+
*/
|
|
89
|
+
private declined;
|
|
81
90
|
private tokensReclaimed;
|
|
82
91
|
private sessionUsage;
|
|
83
92
|
private pricingUnknown;
|
|
@@ -175,6 +184,29 @@ export declare class Compactor implements ContextManager {
|
|
|
175
184
|
*/
|
|
176
185
|
shouldCompact(probe: CompactionProbe): boolean;
|
|
177
186
|
compact(ctx: CompactionContext): Promise<CompactionOutcome>;
|
|
187
|
+
/**
|
|
188
|
+
* Open the card (quiet-noop §3.2).
|
|
189
|
+
*
|
|
190
|
+
* THE ONLY EMITTER OF `compaction_start`, AND THE ONLY PLACE `this.compactions`
|
|
191
|
+
* ADVANCES. A compaction that never reaches an announce point is a DECLINE: it
|
|
192
|
+
* changed nothing, called nothing and spent nothing, and it is deliberately
|
|
193
|
+
* indistinguishable - on every human surface except `/compact status`'s
|
|
194
|
+
* `declined` count (D-9) - from a turn at which `shouldCompact` returned false.
|
|
195
|
+
* `finish` is the other half of that rule.
|
|
196
|
+
*
|
|
197
|
+
* IT RETURNS THE ANNOUNCEMENT RATHER THAN STORING IT (P1-1), and the caller
|
|
198
|
+
* keeps it in a LOCAL for the lifetime of one `runCompaction` call - the same
|
|
199
|
+
* slot `index` occupied before this change. An instance field would have needed
|
|
200
|
+
* a no-re-entrancy argument, and the obvious one is WRONG: guard 1 lives in
|
|
201
|
+
* `shouldCompact`, while the idle `/compact` path calls `compact()` straight
|
|
202
|
+
* through `wiring.compactNow()` without consulting it. What actually keeps two
|
|
203
|
+
* compactions apart is that `command.ts` QUEUES instead of calling `compactNow`
|
|
204
|
+
* while the loop runs - a property of a different file, which is exactly why
|
|
205
|
+
* this one must not depend on it. A clobbered field would leave `finish` unable
|
|
206
|
+
* to settle the card it opened, and a card that never settles pins
|
|
207
|
+
* `Transcript`'s monotonic boundary for the rest of the session.
|
|
208
|
+
*/
|
|
209
|
+
private announce;
|
|
178
210
|
private runCompaction;
|
|
179
211
|
/** Tail relief against this session's window (hardening §3.3.3 / W2). */
|
|
180
212
|
private relieveInPlace;
|
|
@@ -237,6 +269,8 @@ export declare class Compactor implements ContextManager {
|
|
|
237
269
|
recordApplied(tokensBefore: number, tokensAfter: number): void;
|
|
238
270
|
sessionTotals(): {
|
|
239
271
|
compactions: number;
|
|
272
|
+
/** Checkpoints that declined before committing to any work (quiet-noop D-9). */
|
|
273
|
+
declined: number;
|
|
240
274
|
tokensReclaimed: number;
|
|
241
275
|
usage: TokenUsage;
|
|
242
276
|
pricingUnknown: boolean;
|