@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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Killing a process TREE, on both platform families
|
|
3
|
+
* (background-service-supervision §3.5 / P1-1 / P1-2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/proc/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* ONE OWNER FOR BOTH KINDS OF CHILD. This was `bash-tool.ts`'s private helper;
|
|
8
|
+
* hoisting it is what lets a foreground `bash` child and a supervised service be
|
|
9
|
+
* reaped by the same code, which is the only way "no service outlives the CLI"
|
|
10
|
+
* can be true on every exit path.
|
|
11
|
+
*
|
|
12
|
+
* THE POSIX BRANCH IS A PROCESS-GROUP KILL, NOT `child.kill()` (P1-1). With
|
|
13
|
+
* `shell: true` the direct child is the shell wrapper, and `npm run dev` re-execs
|
|
14
|
+
* into node children that survive a signal aimed at the shell alone — which is
|
|
15
|
+
* why every `npm run dev` grandchild used to survive today's `killTree` on
|
|
16
|
+
* POSIX. `process.kill(-pid, sig)` reaches the whole group, and a group only
|
|
17
|
+
* EXISTS because `supervisor.ts` spawns with `detached: true` there. On Windows
|
|
18
|
+
* the flag is irrelevant to reaping: `taskkill /t` walks the parent-child table.
|
|
19
|
+
*
|
|
20
|
+
* THE SYNC VARIANT EXISTS BECAUSE `spawn` IS ASYNCHRONOUS (P1-2). A process that
|
|
21
|
+
* is already exiting will not live long enough for an async `taskkill`, and
|
|
22
|
+
* `process.on('exit')` permits only synchronous work — so the reaper an exit hook
|
|
23
|
+
* calls must be `spawnSync` end to end.
|
|
24
|
+
*/
|
|
25
|
+
/** Is the pid still there? `signal 0` tests for existence without delivering. */
|
|
26
|
+
export declare function isAlive(pid: number | undefined): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Kill the tree rooted at `pid`, asynchronously. Best-effort; never throws.
|
|
29
|
+
*
|
|
30
|
+
* `signal` is honoured on POSIX (so a graceful `SIGTERM` -> `SIGKILL` ladder is
|
|
31
|
+
* possible); Windows `taskkill /f` has no gentle mode, so a `SIGTERM` there is a
|
|
32
|
+
* plain `taskkill` without `/f` and the escalation adds it.
|
|
33
|
+
*/
|
|
34
|
+
export declare function killTree(pid: number | undefined, signal?: NodeJS.Signals): void;
|
|
35
|
+
/**
|
|
36
|
+
* Kill the tree rooted at `pid`, SYNCHRONOUSLY. Never throws, never awaits.
|
|
37
|
+
*
|
|
38
|
+
* The only kill primitive a signal handler, `process.on('exit')` or
|
|
39
|
+
* `handleFatal` may reach (I-9). No escalation ladder and no logging: a reaper
|
|
40
|
+
* that can block or throw is worse than one that misses a child.
|
|
41
|
+
*/
|
|
42
|
+
export declare function killTreeSync(pid: number | undefined): void;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Killing a process TREE, on both platform families
|
|
3
|
+
* (background-service-supervision §3.5 / P1-1 / P1-2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/proc/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* ONE OWNER FOR BOTH KINDS OF CHILD. This was `bash-tool.ts`'s private helper;
|
|
8
|
+
* hoisting it is what lets a foreground `bash` child and a supervised service be
|
|
9
|
+
* reaped by the same code, which is the only way "no service outlives the CLI"
|
|
10
|
+
* can be true on every exit path.
|
|
11
|
+
*
|
|
12
|
+
* THE POSIX BRANCH IS A PROCESS-GROUP KILL, NOT `child.kill()` (P1-1). With
|
|
13
|
+
* `shell: true` the direct child is the shell wrapper, and `npm run dev` re-execs
|
|
14
|
+
* into node children that survive a signal aimed at the shell alone — which is
|
|
15
|
+
* why every `npm run dev` grandchild used to survive today's `killTree` on
|
|
16
|
+
* POSIX. `process.kill(-pid, sig)` reaches the whole group, and a group only
|
|
17
|
+
* EXISTS because `supervisor.ts` spawns with `detached: true` there. On Windows
|
|
18
|
+
* the flag is irrelevant to reaping: `taskkill /t` walks the parent-child table.
|
|
19
|
+
*
|
|
20
|
+
* THE SYNC VARIANT EXISTS BECAUSE `spawn` IS ASYNCHRONOUS (P1-2). A process that
|
|
21
|
+
* is already exiting will not live long enough for an async `taskkill`, and
|
|
22
|
+
* `process.on('exit')` permits only synchronous work — so the reaper an exit hook
|
|
23
|
+
* calls must be `spawnSync` end to end.
|
|
24
|
+
*/
|
|
25
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
26
|
+
import process from 'node:process';
|
|
27
|
+
/** Is the pid still there? `signal 0` tests for existence without delivering. */
|
|
28
|
+
export function isAlive(pid) {
|
|
29
|
+
if (pid === undefined || !Number.isFinite(pid) || pid <= 0)
|
|
30
|
+
return false;
|
|
31
|
+
try {
|
|
32
|
+
process.kill(pid, 0);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
// EPERM means it exists and belongs to somebody else, which for our purposes
|
|
37
|
+
// is still "alive" - reporting it dead would make the honest
|
|
38
|
+
// "may have left a detached child" warning silently unreachable.
|
|
39
|
+
return err?.code === 'EPERM';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Kill the tree rooted at `pid`, asynchronously. Best-effort; never throws.
|
|
44
|
+
*
|
|
45
|
+
* `signal` is honoured on POSIX (so a graceful `SIGTERM` -> `SIGKILL` ladder is
|
|
46
|
+
* possible); Windows `taskkill /f` has no gentle mode, so a `SIGTERM` there is a
|
|
47
|
+
* plain `taskkill` without `/f` and the escalation adds it.
|
|
48
|
+
*/
|
|
49
|
+
export function killTree(pid, signal = 'SIGKILL') {
|
|
50
|
+
if (pid === undefined || !Number.isFinite(pid) || pid <= 0)
|
|
51
|
+
return;
|
|
52
|
+
if (process.platform === 'win32') {
|
|
53
|
+
const args = ['/pid', String(pid), '/t'];
|
|
54
|
+
if (signal === 'SIGKILL')
|
|
55
|
+
args.push('/f');
|
|
56
|
+
try {
|
|
57
|
+
const child = spawn('taskkill', args, { windowsHide: true, stdio: 'ignore' });
|
|
58
|
+
// A reaper must never be the reason the CLI does not exit (I-3), and it
|
|
59
|
+
// must never crash it either: `taskkill` missing from PATH emits 'error'.
|
|
60
|
+
child.unref();
|
|
61
|
+
child.on('error', () => { });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* ignore */
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// THE GROUP FIRST, THE PID SECOND. The group is what reaches the grandchild;
|
|
69
|
+
// the single-pid fallback covers a child that was NOT spawned detached (a
|
|
70
|
+
// foreground `bash` child), where `-pid` is not a group at all.
|
|
71
|
+
try {
|
|
72
|
+
process.kill(-pid, signal);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* fall through to the single-process attempt */
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
process.kill(pid, signal);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
/* already gone */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Kill the tree rooted at `pid`, SYNCHRONOUSLY. Never throws, never awaits.
|
|
87
|
+
*
|
|
88
|
+
* The only kill primitive a signal handler, `process.on('exit')` or
|
|
89
|
+
* `handleFatal` may reach (I-9). No escalation ladder and no logging: a reaper
|
|
90
|
+
* that can block or throw is worse than one that misses a child.
|
|
91
|
+
*/
|
|
92
|
+
export function killTreeSync(pid) {
|
|
93
|
+
if (pid === undefined || !Number.isFinite(pid) || pid <= 0)
|
|
94
|
+
return;
|
|
95
|
+
if (process.platform === 'win32') {
|
|
96
|
+
try {
|
|
97
|
+
spawnSync('taskkill', ['/pid', String(pid), '/t', '/f'], {
|
|
98
|
+
windowsHide: true,
|
|
99
|
+
stdio: 'ignore',
|
|
100
|
+
timeout: 2000,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
/* ignore */
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
process.kill(-pid, 'SIGKILL');
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
try {
|
|
113
|
+
process.kill(pid, 'SIGKILL');
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
/* already gone */
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PROC_LIMITS — the single authority on every structural bound the process
|
|
3
|
+
* supervisor enforces (background-service-supervision §6).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/proc/**` is inside the glyph scanner's scope
|
|
6
|
+
* (`glyphs.test.ts::inScope`), whose hardcoded directory regex this feature
|
|
7
|
+
* extends. A NEW TREE IS INVISIBLE TO THAT SCAN UNTIL IT IS NAMED THERE, and a
|
|
8
|
+
* guard rail that silently stops guarding is worse than no guard rail — this
|
|
9
|
+
* package has now paid for that edit eleven times.
|
|
10
|
+
*
|
|
11
|
+
* TWO KINDS OF NUMBER, AND THEY ARE NOT INTERCHANGEABLE — the distinction
|
|
12
|
+
* `TODO_LIMITS` and `TEAM_LIMITS` each record at length:
|
|
13
|
+
*
|
|
14
|
+
* - Everything below is STRUCTURAL: how many rows a tail can physically carry,
|
|
15
|
+
* how many children one session may hold, how long a kill may take before it
|
|
16
|
+
* stops being a kill. A user has no business tuning them, so they are
|
|
17
|
+
* constants.
|
|
18
|
+
* - The four `bash.*` config keys (`config/schema.ts`) are POLICY: register the
|
|
19
|
+
* tools at all, let the classifier fire, how long a launch may block the
|
|
20
|
+
* turn, how long to wait for readiness. They are clamped, persisted and
|
|
21
|
+
* user-facing.
|
|
22
|
+
*/
|
|
23
|
+
export declare const PROC_LIMITS: {
|
|
24
|
+
/**
|
|
25
|
+
* Rows the per-service ring retains.
|
|
26
|
+
*
|
|
27
|
+
* DELIBERATELY A DIFFERENT NUMBER FROM `cardTailRows` BELOW, and from
|
|
28
|
+
* `LIVE_TAIL_ROWS` (8) in `tools/tool-output-store.ts`. They bound unrelated
|
|
29
|
+
* things — what `bash_output` can page back through, versus what one card
|
|
30
|
+
* draws — and a shared literal is how a later edit collapses two bounds into
|
|
31
|
+
* one.
|
|
32
|
+
*/
|
|
33
|
+
readonly serviceTailRows: 200;
|
|
34
|
+
/** Rows a snapshot (and therefore a card) carries. */
|
|
35
|
+
readonly cardTailRows: 8;
|
|
36
|
+
/**
|
|
37
|
+
* Service RECORDS one session may hold.
|
|
38
|
+
*
|
|
39
|
+
* Eviction only ever removes a record in a TERMINAL state
|
|
40
|
+
* (`exited | failed | stopped`). When all 16 are live `start()` refuses and
|
|
41
|
+
* names the oldest ids rather than evicting: evicting a live record would
|
|
42
|
+
* orphan a running server that nothing can any longer name or stop.
|
|
43
|
+
*/
|
|
44
|
+
readonly maxServices: 16;
|
|
45
|
+
/**
|
|
46
|
+
* How long `bash` waits after `'exit'` for the stdio streams to close before
|
|
47
|
+
* settling anyway.
|
|
48
|
+
*
|
|
49
|
+
* This number IS defence 1. `'close'` fires only once every stream is closed,
|
|
50
|
+
* and a detached grandchild that inherited the pipe keeps it open after the
|
|
51
|
+
* shell is long gone — so a tool that settles on `'close'` alone has a path
|
|
52
|
+
* that never settles at all.
|
|
53
|
+
*/
|
|
54
|
+
readonly drainMs: 250;
|
|
55
|
+
/**
|
|
56
|
+
* How long an aborted/timed-out `bash` waits after the kill before settling
|
|
57
|
+
* WHETHER OR NOT the child ever reported exit.
|
|
58
|
+
*
|
|
59
|
+
* A pending promise after an abort is the bug; a possibly-premature result is
|
|
60
|
+
* not.
|
|
61
|
+
*/
|
|
62
|
+
readonly killGraceMs: 2000;
|
|
63
|
+
/** Readiness port probe interval. */
|
|
64
|
+
readonly probeIntervalMs: 500;
|
|
65
|
+
/** Socket timeout for one readiness probe. */
|
|
66
|
+
readonly probeSocketTimeoutMs: 1000;
|
|
67
|
+
/**
|
|
68
|
+
* Output-event coalescing window inside the supervisor.
|
|
69
|
+
*
|
|
70
|
+
* At the SOURCE rather than in the view, because the same event is also
|
|
71
|
+
* consumed by `/bg` and by `bash_output`. A webpack build emitting thousands
|
|
72
|
+
* of lines a second must not become thousands of React renders.
|
|
73
|
+
*/
|
|
74
|
+
readonly outputCoalesceMs: 100;
|
|
75
|
+
/** `SIGTERM` -> `SIGKILL` escalation window for a graceful stop. */
|
|
76
|
+
readonly stopGraceMs: 3000;
|
|
77
|
+
/** Below this many terminal columns the status chip degrades to `[N]`. */
|
|
78
|
+
readonly statusCompactCols: 92;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Bumped whenever the wording of the `<background_services>` block changes, so a
|
|
82
|
+
* behaviour report can be tied to a block revision with one grep.
|
|
83
|
+
*/
|
|
84
|
+
export declare const BACKGROUND_SERVICES_BLOCK_VERSION = "v1";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PROC_LIMITS — the single authority on every structural bound the process
|
|
3
|
+
* supervisor enforces (background-service-supervision §6).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/proc/**` is inside the glyph scanner's scope
|
|
6
|
+
* (`glyphs.test.ts::inScope`), whose hardcoded directory regex this feature
|
|
7
|
+
* extends. A NEW TREE IS INVISIBLE TO THAT SCAN UNTIL IT IS NAMED THERE, and a
|
|
8
|
+
* guard rail that silently stops guarding is worse than no guard rail — this
|
|
9
|
+
* package has now paid for that edit eleven times.
|
|
10
|
+
*
|
|
11
|
+
* TWO KINDS OF NUMBER, AND THEY ARE NOT INTERCHANGEABLE — the distinction
|
|
12
|
+
* `TODO_LIMITS` and `TEAM_LIMITS` each record at length:
|
|
13
|
+
*
|
|
14
|
+
* - Everything below is STRUCTURAL: how many rows a tail can physically carry,
|
|
15
|
+
* how many children one session may hold, how long a kill may take before it
|
|
16
|
+
* stops being a kill. A user has no business tuning them, so they are
|
|
17
|
+
* constants.
|
|
18
|
+
* - The four `bash.*` config keys (`config/schema.ts`) are POLICY: register the
|
|
19
|
+
* tools at all, let the classifier fire, how long a launch may block the
|
|
20
|
+
* turn, how long to wait for readiness. They are clamped, persisted and
|
|
21
|
+
* user-facing.
|
|
22
|
+
*/
|
|
23
|
+
export const PROC_LIMITS = {
|
|
24
|
+
/**
|
|
25
|
+
* Rows the per-service ring retains.
|
|
26
|
+
*
|
|
27
|
+
* DELIBERATELY A DIFFERENT NUMBER FROM `cardTailRows` BELOW, and from
|
|
28
|
+
* `LIVE_TAIL_ROWS` (8) in `tools/tool-output-store.ts`. They bound unrelated
|
|
29
|
+
* things — what `bash_output` can page back through, versus what one card
|
|
30
|
+
* draws — and a shared literal is how a later edit collapses two bounds into
|
|
31
|
+
* one.
|
|
32
|
+
*/
|
|
33
|
+
serviceTailRows: 200,
|
|
34
|
+
/** Rows a snapshot (and therefore a card) carries. */
|
|
35
|
+
cardTailRows: 8,
|
|
36
|
+
/**
|
|
37
|
+
* Service RECORDS one session may hold.
|
|
38
|
+
*
|
|
39
|
+
* Eviction only ever removes a record in a TERMINAL state
|
|
40
|
+
* (`exited | failed | stopped`). When all 16 are live `start()` refuses and
|
|
41
|
+
* names the oldest ids rather than evicting: evicting a live record would
|
|
42
|
+
* orphan a running server that nothing can any longer name or stop.
|
|
43
|
+
*/
|
|
44
|
+
maxServices: 16,
|
|
45
|
+
/**
|
|
46
|
+
* How long `bash` waits after `'exit'` for the stdio streams to close before
|
|
47
|
+
* settling anyway.
|
|
48
|
+
*
|
|
49
|
+
* This number IS defence 1. `'close'` fires only once every stream is closed,
|
|
50
|
+
* and a detached grandchild that inherited the pipe keeps it open after the
|
|
51
|
+
* shell is long gone — so a tool that settles on `'close'` alone has a path
|
|
52
|
+
* that never settles at all.
|
|
53
|
+
*/
|
|
54
|
+
drainMs: 250,
|
|
55
|
+
/**
|
|
56
|
+
* How long an aborted/timed-out `bash` waits after the kill before settling
|
|
57
|
+
* WHETHER OR NOT the child ever reported exit.
|
|
58
|
+
*
|
|
59
|
+
* A pending promise after an abort is the bug; a possibly-premature result is
|
|
60
|
+
* not.
|
|
61
|
+
*/
|
|
62
|
+
killGraceMs: 2000,
|
|
63
|
+
/** Readiness port probe interval. */
|
|
64
|
+
probeIntervalMs: 500,
|
|
65
|
+
/** Socket timeout for one readiness probe. */
|
|
66
|
+
probeSocketTimeoutMs: 1000,
|
|
67
|
+
/**
|
|
68
|
+
* Output-event coalescing window inside the supervisor.
|
|
69
|
+
*
|
|
70
|
+
* At the SOURCE rather than in the view, because the same event is also
|
|
71
|
+
* consumed by `/bg` and by `bash_output`. A webpack build emitting thousands
|
|
72
|
+
* of lines a second must not become thousands of React renders.
|
|
73
|
+
*/
|
|
74
|
+
outputCoalesceMs: 100,
|
|
75
|
+
/** `SIGTERM` -> `SIGKILL` escalation window for a graceful stop. */
|
|
76
|
+
stopGraceMs: 3000,
|
|
77
|
+
/** Below this many terminal columns the status chip degrades to `[N]`. */
|
|
78
|
+
statusCompactCols: 92,
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Bumped whenever the wording of the `<background_services>` block changes, so a
|
|
82
|
+
* behaviour report can be tied to a block revision with one grep.
|
|
83
|
+
*/
|
|
84
|
+
export const BACKGROUND_SERVICES_BLOCK_VERSION = 'v1';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bounded, sanitised output tail one supervised service keeps
|
|
3
|
+
* (background-service-supervision §4).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/proc/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* DELIBERATELY NOT `tools/tool-output-store.ts` (P2-4), which is the closest
|
|
8
|
+
* existing thing and would be the reflex merge. Three differences, each
|
|
9
|
+
* load-bearing:
|
|
10
|
+
*
|
|
11
|
+
* - that store keeps 8 rows, this keeps 200 — one is what a card DRAWS, the
|
|
12
|
+
* other is what `bash_output` can page back through;
|
|
13
|
+
* - that store is cleared at `tool_execution_end`, and a service outlives by
|
|
14
|
+
* minutes or hours the tool call that started it;
|
|
15
|
+
* - that store has no cursor, and paging is the whole point of `since`.
|
|
16
|
+
*
|
|
17
|
+
* It reuses `sanitizeChunk` for the same reason that store does — the contract
|
|
18
|
+
* is "terminal-safe rows", and no consumer can forget — and it follows the same
|
|
19
|
+
* "two bounds are two different literals" rule.
|
|
20
|
+
*/
|
|
21
|
+
export interface LogRingPage {
|
|
22
|
+
rows: readonly string[];
|
|
23
|
+
/** Pass as the next `since`. */
|
|
24
|
+
cursor: number;
|
|
25
|
+
/** Rows between `since` and the first row returned were evicted. */
|
|
26
|
+
truncated: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A per-service ring.
|
|
30
|
+
*
|
|
31
|
+
* `rowsSeen` is the MONOTONIC count of completed rows ever appended, which is
|
|
32
|
+
* both the paging cursor and the transcript's revision term. It is deliberately
|
|
33
|
+
* not `rows.length`: the ring evicts while appending, so the length can be
|
|
34
|
+
* identical across two different tails.
|
|
35
|
+
*/
|
|
36
|
+
export declare class LogRing {
|
|
37
|
+
private readonly capacity;
|
|
38
|
+
private readonly rows;
|
|
39
|
+
private carry;
|
|
40
|
+
private seen;
|
|
41
|
+
private evicted;
|
|
42
|
+
constructor(capacity?: number);
|
|
43
|
+
/**
|
|
44
|
+
* Append a raw chunk; returns the rows it completed.
|
|
45
|
+
*
|
|
46
|
+
* The IN-PROGRESS line is NOT a row and is not counted: a command whose output
|
|
47
|
+
* is one continuously-rewritten line (every progress bar there is) would
|
|
48
|
+
* otherwise burn the whole ring on one logical line. It is rendered through
|
|
49
|
+
* `toDisplayRow` by `tail()` instead, so the display still shows it.
|
|
50
|
+
*/
|
|
51
|
+
append(chunk: string): readonly string[];
|
|
52
|
+
/** The last `n` display rows, in-progress line included when non-empty. */
|
|
53
|
+
tail(n: number): readonly string[];
|
|
54
|
+
/** Every completed row plus the in-progress line. */
|
|
55
|
+
all(): readonly string[];
|
|
56
|
+
/** Completed rows ever appended — the paging cursor and the revision term. */
|
|
57
|
+
get rowsSeen(): number;
|
|
58
|
+
/** The ring has dropped at least one row. */
|
|
59
|
+
get truncated(): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Rows after cursor `since`, or the whole tail when `since` is omitted.
|
|
62
|
+
*
|
|
63
|
+
* `truncated` reports that the cursor FELL OFF the ring — the caller asked for
|
|
64
|
+
* row 3 and the oldest row still held is 57 — which is a real loss the model
|
|
65
|
+
* is entitled to be told about rather than have silently papered over.
|
|
66
|
+
*/
|
|
67
|
+
page(since?: number): LogRingPage;
|
|
68
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bounded, sanitised output tail one supervised service keeps
|
|
3
|
+
* (background-service-supervision §4).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/proc/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* DELIBERATELY NOT `tools/tool-output-store.ts` (P2-4), which is the closest
|
|
8
|
+
* existing thing and would be the reflex merge. Three differences, each
|
|
9
|
+
* load-bearing:
|
|
10
|
+
*
|
|
11
|
+
* - that store keeps 8 rows, this keeps 200 — one is what a card DRAWS, the
|
|
12
|
+
* other is what `bash_output` can page back through;
|
|
13
|
+
* - that store is cleared at `tool_execution_end`, and a service outlives by
|
|
14
|
+
* minutes or hours the tool call that started it;
|
|
15
|
+
* - that store has no cursor, and paging is the whole point of `since`.
|
|
16
|
+
*
|
|
17
|
+
* It reuses `sanitizeChunk` for the same reason that store does — the contract
|
|
18
|
+
* is "terminal-safe rows", and no consumer can forget — and it follows the same
|
|
19
|
+
* "two bounds are two different literals" rule.
|
|
20
|
+
*/
|
|
21
|
+
import { sanitizeChunk, toDisplayRow } from '../tools/terminal-output.js';
|
|
22
|
+
import { PROC_LIMITS } from './limits.js';
|
|
23
|
+
/**
|
|
24
|
+
* A per-service ring.
|
|
25
|
+
*
|
|
26
|
+
* `rowsSeen` is the MONOTONIC count of completed rows ever appended, which is
|
|
27
|
+
* both the paging cursor and the transcript's revision term. It is deliberately
|
|
28
|
+
* not `rows.length`: the ring evicts while appending, so the length can be
|
|
29
|
+
* identical across two different tails.
|
|
30
|
+
*/
|
|
31
|
+
export class LogRing {
|
|
32
|
+
capacity;
|
|
33
|
+
rows = [];
|
|
34
|
+
carry = '';
|
|
35
|
+
seen = 0;
|
|
36
|
+
evicted = 0;
|
|
37
|
+
constructor(capacity = PROC_LIMITS.serviceTailRows) {
|
|
38
|
+
this.capacity = capacity;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Append a raw chunk; returns the rows it completed.
|
|
42
|
+
*
|
|
43
|
+
* The IN-PROGRESS line is NOT a row and is not counted: a command whose output
|
|
44
|
+
* is one continuously-rewritten line (every progress bar there is) would
|
|
45
|
+
* otherwise burn the whole ring on one logical line. It is rendered through
|
|
46
|
+
* `toDisplayRow` by `tail()` instead, so the display still shows it.
|
|
47
|
+
*/
|
|
48
|
+
append(chunk) {
|
|
49
|
+
const { rows, carry } = sanitizeChunk(this.carry, chunk);
|
|
50
|
+
this.carry = carry;
|
|
51
|
+
if (rows.length === 0)
|
|
52
|
+
return [];
|
|
53
|
+
this.rows.push(...rows);
|
|
54
|
+
this.seen += rows.length;
|
|
55
|
+
if (this.rows.length > this.capacity) {
|
|
56
|
+
const drop = this.rows.length - this.capacity;
|
|
57
|
+
this.rows.splice(0, drop);
|
|
58
|
+
this.evicted += drop;
|
|
59
|
+
}
|
|
60
|
+
return rows;
|
|
61
|
+
}
|
|
62
|
+
/** The last `n` display rows, in-progress line included when non-empty. */
|
|
63
|
+
tail(n) {
|
|
64
|
+
const live = toDisplayRow(this.carry);
|
|
65
|
+
const all = live.length > 0 ? [...this.rows, live] : this.rows;
|
|
66
|
+
return Object.freeze(all.length > n ? all.slice(-n) : [...all]);
|
|
67
|
+
}
|
|
68
|
+
/** Every completed row plus the in-progress line. */
|
|
69
|
+
all() {
|
|
70
|
+
return this.tail(this.capacity + 1);
|
|
71
|
+
}
|
|
72
|
+
/** Completed rows ever appended — the paging cursor and the revision term. */
|
|
73
|
+
get rowsSeen() {
|
|
74
|
+
return this.seen;
|
|
75
|
+
}
|
|
76
|
+
/** The ring has dropped at least one row. */
|
|
77
|
+
get truncated() {
|
|
78
|
+
return this.evicted > 0;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Rows after cursor `since`, or the whole tail when `since` is omitted.
|
|
82
|
+
*
|
|
83
|
+
* `truncated` reports that the cursor FELL OFF the ring — the caller asked for
|
|
84
|
+
* row 3 and the oldest row still held is 57 — which is a real loss the model
|
|
85
|
+
* is entitled to be told about rather than have silently papered over.
|
|
86
|
+
*/
|
|
87
|
+
page(since) {
|
|
88
|
+
const oldest = this.seen - this.rows.length;
|
|
89
|
+
if (since === undefined || since < 0) {
|
|
90
|
+
return { rows: this.all(), cursor: this.seen, truncated: this.truncated };
|
|
91
|
+
}
|
|
92
|
+
const from = Math.max(since, oldest);
|
|
93
|
+
const rows = this.rows.slice(from - oldest);
|
|
94
|
+
const live = toDisplayRow(this.carry);
|
|
95
|
+
const withLive = live.length > 0 ? [...rows, live] : rows;
|
|
96
|
+
return {
|
|
97
|
+
rows: Object.freeze(withLive),
|
|
98
|
+
cursor: this.seen,
|
|
99
|
+
truncated: since < oldest,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `<background_services>` system-prompt block
|
|
3
|
+
* (background-service-supervision §5.2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/proc/**` is inside the glyph scanner's scope, and this string
|
|
6
|
+
* also reaches a model that may be running in a `cmd.exe` terminal.
|
|
7
|
+
*
|
|
8
|
+
* SPLICED CONDITIONALLY by `buildSystemPrompt`, which is what preserves I-2:
|
|
9
|
+
* with `bash.background: false` the prompt is BYTE-IDENTICAL to the pre-feature
|
|
10
|
+
* output for a fixed tool array. An unconditional guidance line anywhere here
|
|
11
|
+
* would quietly break that, `--no-todo`, `--no-team` and `--no-skills` all at
|
|
12
|
+
* once.
|
|
13
|
+
*
|
|
14
|
+
* IT NAMES THE `[note]` SENTENCE VERBATIM. That line is the whole of defence 1
|
|
15
|
+
* as the model experiences it, and telling it what the words mean is the
|
|
16
|
+
* difference between "re-run with background: true" and reaching for
|
|
17
|
+
* `Start-Process` - which is exactly what produced the reported screenshot.
|
|
18
|
+
*
|
|
19
|
+
* `BACKGROUND_SERVICES_BLOCK_VERSION` exists so a change to this wording is
|
|
20
|
+
* greppable from a behaviour report.
|
|
21
|
+
*/
|
|
22
|
+
import { BACKGROUND_SERVICES_BLOCK_VERSION } from './limits.js';
|
|
23
|
+
export { BACKGROUND_SERVICES_BLOCK_VERSION };
|
|
24
|
+
export declare function buildBackgroundServicesBlock(): string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `<background_services>` system-prompt block
|
|
3
|
+
* (background-service-supervision §5.2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/proc/**` is inside the glyph scanner's scope, and this string
|
|
6
|
+
* also reaches a model that may be running in a `cmd.exe` terminal.
|
|
7
|
+
*
|
|
8
|
+
* SPLICED CONDITIONALLY by `buildSystemPrompt`, which is what preserves I-2:
|
|
9
|
+
* with `bash.background: false` the prompt is BYTE-IDENTICAL to the pre-feature
|
|
10
|
+
* output for a fixed tool array. An unconditional guidance line anywhere here
|
|
11
|
+
* would quietly break that, `--no-todo`, `--no-team` and `--no-skills` all at
|
|
12
|
+
* once.
|
|
13
|
+
*
|
|
14
|
+
* IT NAMES THE `[note]` SENTENCE VERBATIM. That line is the whole of defence 1
|
|
15
|
+
* as the model experiences it, and telling it what the words mean is the
|
|
16
|
+
* difference between "re-run with background: true" and reaching for
|
|
17
|
+
* `Start-Process` - which is exactly what produced the reported screenshot.
|
|
18
|
+
*
|
|
19
|
+
* `BACKGROUND_SERVICES_BLOCK_VERSION` exists so a change to this wording is
|
|
20
|
+
* greppable from a behaviour report.
|
|
21
|
+
*/
|
|
22
|
+
import { BACKGROUND_SERVICES_BLOCK_VERSION } from './limits.js';
|
|
23
|
+
export { BACKGROUND_SERVICES_BLOCK_VERSION };
|
|
24
|
+
export function buildBackgroundServicesBlock() {
|
|
25
|
+
return [
|
|
26
|
+
'<background_services>',
|
|
27
|
+
'Some commands never exit on their own: dev servers, watchers, `docker compose up`,',
|
|
28
|
+
'`tail -f`. Running one in the foreground stalls the turn until the tool timeout',
|
|
29
|
+
'kills it, and you learn nothing.',
|
|
30
|
+
'',
|
|
31
|
+
'Rules:',
|
|
32
|
+
'- Set `background: true` on `bash` for anything that does not exit on its own.',
|
|
33
|
+
' The call returns within a few seconds with a service id, a status, and a log tail.',
|
|
34
|
+
'- NEVER detach a command yourself with `Start-Process`, `&`, `nohup` or `screen`.',
|
|
35
|
+
' A self-detached process cannot be reported on, read from, or stopped.',
|
|
36
|
+
'- A background result is PROVISIONAL. `ready` means a port answered or a URL was',
|
|
37
|
+
' printed - it does not mean the app works. Verify with `bash_output({ service })`',
|
|
38
|
+
' AND with an actual request against the URL (for example `curl -s <url>`).',
|
|
39
|
+
'- Read more output at any time with `bash_output({ service, since })`; `since` is the',
|
|
40
|
+
' cursor returned by the previous call.',
|
|
41
|
+
'- Stop a service with `bash_kill({ service })`, or `bash_kill({ service: "all" })`,',
|
|
42
|
+
' when you are finished with it - unless the user asked for it to stay up.',
|
|
43
|
+
'- If a foreground result ends with',
|
|
44
|
+
' `[note] the command exited but a background child is still holding its output stream.`',
|
|
45
|
+
' then the command detached something. Re-run it with `background: true`.',
|
|
46
|
+
'- Services are stopped when this CLI exits. They do not survive the session.',
|
|
47
|
+
'</background_services>',
|
|
48
|
+
].join('\n');
|
|
49
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Readiness detection (background-service-supervision §3.5).
|
|
3
|
+
*
|
|
4
|
+
* Two detectors, raced, first one wins:
|
|
5
|
+
*
|
|
6
|
+
* - the URL detector, which is PURE and therefore testable without a socket;
|
|
7
|
+
* - the port probe, which is one TCP connect and only runs when the command
|
|
8
|
+
* text named a port.
|
|
9
|
+
*
|
|
10
|
+
* ASCII ONLY: `src/proc/**` is inside the glyph scanner's scope.
|
|
11
|
+
*
|
|
12
|
+
* EVERY TIMER AND SOCKET HERE IS `unref()`d (I-3). Readiness detection must
|
|
13
|
+
* never be the reason the CLI does not exit; only the child processes themselves
|
|
14
|
+
* hold the loop open, and they are reaped on exit.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* A loopback/wildcard `http(s)` URL, with an optional port and path.
|
|
18
|
+
*
|
|
19
|
+
* ONE ANCHORED SOURCE OF TRUTH, and written out rather than assembled, because
|
|
20
|
+
* the obvious shorthand is not a regex at all: `[::1]` inside a pattern is a
|
|
21
|
+
* CHARACTER CLASS matching `:` or `1`, and unescaped dots match any character.
|
|
22
|
+
* Both mistakes read as correct.
|
|
23
|
+
*/
|
|
24
|
+
export declare const READY_URL_RE: RegExp;
|
|
25
|
+
export interface ReadyUrlMatch {
|
|
26
|
+
/** Normalised so a human can click it; absent for a bare-port phrase. */
|
|
27
|
+
url?: string;
|
|
28
|
+
port?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Normalise a wildcard/IPv6-loopback host to something clickable.
|
|
32
|
+
*
|
|
33
|
+
* `0.0.0.0` means "every interface", which a browser cannot open; `::1` and
|
|
34
|
+
* `[::1]` are correct but unfamiliar. All three become `127.0.0.1`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function normalizeHost(url: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* The first readiness candidate in `rows`, or `undefined`.
|
|
39
|
+
*
|
|
40
|
+
* A MATCH IS A CANDIDATE, NOT A VERDICT (P2-2). A loopback URL printed in a
|
|
41
|
+
* banner or a `--help` blurb appears BEFORE anything binds, and a bare pattern
|
|
42
|
+
* would call that service ready in the first 40 ms. Two guards make the trade
|
|
43
|
+
* explicit, and neither lives here:
|
|
44
|
+
*
|
|
45
|
+
* 1. only rows emitted AFTER spawn are scanned (the ring starts empty, so this
|
|
46
|
+
* is free), and
|
|
47
|
+
* 2. when the match yields a PORT, `ReadinessWatcher` confirms with one TCP
|
|
48
|
+
* connect before the status flips.
|
|
49
|
+
*
|
|
50
|
+
* When the match yields no port the URL is accepted as-is: there is nothing to
|
|
51
|
+
* probe, and a hostname-only loopback URL in a banner is rare enough to trade
|
|
52
|
+
* against the complexity of guessing 80 vs 443.
|
|
53
|
+
*/
|
|
54
|
+
export declare function detectReadyUrl(rows: readonly string[]): ReadyUrlMatch | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* One TCP connect. Resolves `true` when something answered.
|
|
57
|
+
*
|
|
58
|
+
* Never rejects, and the socket is destroyed on every path — a probe that leaked
|
|
59
|
+
* a socket would hold the event loop open exactly as long as the service runs.
|
|
60
|
+
*/
|
|
61
|
+
export declare function probePort(port: number, host?: string, timeoutMs?: 1000): Promise<boolean>;
|
|
62
|
+
export interface ReadinessResult {
|
|
63
|
+
url?: string;
|
|
64
|
+
port?: number;
|
|
65
|
+
detectedBy: 'url' | 'probe';
|
|
66
|
+
}
|
|
67
|
+
export interface ReadinessWatcherOptions {
|
|
68
|
+
/** From `extractPortHint`, and only when the pre-flight probe found it FREE. */
|
|
69
|
+
portHint?: number;
|
|
70
|
+
readyTimeoutMs: number;
|
|
71
|
+
onReady: (result: ReadinessResult) => void;
|
|
72
|
+
/** Alive past `readyTimeoutMs` with nothing detected (`tsc --watch` et al). */
|
|
73
|
+
onTimeout: () => void;
|
|
74
|
+
/** Injected for the tests; production passes nothing. */
|
|
75
|
+
probe?: (port: number) => Promise<boolean>;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Races the URL detector against the port probe and stops at the first hit.
|
|
79
|
+
*
|
|
80
|
+
* ONE SHOT. `stop()` is idempotent and is called from every terminal path, so a
|
|
81
|
+
* service that exits during the window leaves nothing behind.
|
|
82
|
+
*/
|
|
83
|
+
export declare class ReadinessWatcher {
|
|
84
|
+
private readonly opts;
|
|
85
|
+
private timer;
|
|
86
|
+
private deadline;
|
|
87
|
+
private finished;
|
|
88
|
+
private probing;
|
|
89
|
+
private readonly probeFn;
|
|
90
|
+
constructor(opts: ReadinessWatcherOptions);
|
|
91
|
+
start(): void;
|
|
92
|
+
/**
|
|
93
|
+
* Offer the rows emitted since the last call.
|
|
94
|
+
*
|
|
95
|
+
* A MATCH WITH A PORT IS CONFIRMED BY ONE CONNECT before the status flips; a
|
|
96
|
+
* refused connect leaves the service `starting` and the row is simply not
|
|
97
|
+
* re-considered on the next call, because the caller passes only NEW rows.
|
|
98
|
+
*/
|
|
99
|
+
offerRows(rows: readonly string[]): void;
|
|
100
|
+
stop(): void;
|
|
101
|
+
private settle;
|
|
102
|
+
}
|