brl-subagent 2.3.9 → 2.5.0
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/AGENT.md +5 -0
- package/README.md +20 -8
- package/package.json +11 -9
- package/src/concurrency.ts +1 -0
- package/src/diff.ts +1 -0
- package/src/event-bus.ts +8 -2
- package/src/git.ts +68 -0
- package/src/history.ts +59 -1
- package/src/index.ts +244 -406
- package/src/kill-escalation.ts +94 -0
- package/src/logging.ts +58 -4
- package/src/messaging.ts +1 -0
- package/src/metrics.ts +1 -0
- package/src/model-availability.ts +1 -0
- package/src/notify-completion.ts +1 -0
- package/src/params.ts +1 -0
- package/src/paths.ts +1 -0
- package/src/preflight.ts +30 -14
- package/src/prelude.ts +1 -0
- package/src/presets.ts +1 -0
- package/src/proc.ts +140 -0
- package/src/prompt.ts +1 -0
- package/src/recovery-engine.ts +262 -0
- package/src/recovery.ts +325 -0
- package/src/reports.ts +64 -8
- package/src/router.ts +1 -0
- package/src/run-registry.ts +269 -0
- package/src/runner.ts +207 -16
- package/src/sanitize.ts +54 -3
- package/src/scheduler.ts +1 -0
- package/src/schema.ts +254 -0
- package/src/session-manager.ts +142 -111
- package/src/state.ts +52 -7
- package/src/templates.ts +1 -0
- package/src/transcript-path.ts +41 -0
- package/src/transcript-tail.ts +1 -0
- package/src/transcript.ts +14 -12
- package/src/tui-format.ts +1 -0
- package/src/tui.ts +231 -296
- package/src/types.ts +130 -2
- package/src/unit-run.ts +9 -0
- package/src/validate.ts +16 -2
package/AGENT.md
CHANGED
|
@@ -66,6 +66,11 @@ delegation-heavy work.
|
|
|
66
66
|
- Background fan-out: `tasks: [...], background: true` — one background agent per task, one id per task in task order, one wake per agent.
|
|
67
67
|
- retryRunId: pass a failed run's id to re-run it with its recorded task and params; explicit parameters override the originals.
|
|
68
68
|
|
|
69
|
+
## Deadlines and retries
|
|
70
|
+
|
|
71
|
+
- `timeout` (ms) is a **deadline, not a ceiling** — an explicit value is honored as given; 30 minutes is only the default for a background run that passes none. Background expiry aborts the session (the run ends `stopped`); foreground expiry kills the process.
|
|
72
|
+
- `retryOnTimeout` applies to **foreground runs only**. A timed-out background run is not auto-retried: re-dispatch it with `retryRunId` if that is the right call.
|
|
73
|
+
|
|
69
74
|
## Mechanics pointer
|
|
70
75
|
|
|
71
76
|
Presets and templates are documented in the schema, not here. Judgment only:
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Multi-agent orchestration for [pi](https://github.com/earendil-works/pi) — chain, parallel, and dependency-graph delegation to isolated subagents with per-step model routing, preset-driven tool scoping, thinking-level control, and background execution with live monitoring, real abort, and per-agent timeouts.
|
|
4
4
|
|
|
5
|
-
**Version:** 2.
|
|
5
|
+
**Version:** 2.5.0 · **Author:** Azmeen Afandi / [Beeroo Labs](https://beeroolabs.com) · **License:** MIT
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
`brl-subagent` is **one of the most capable subagent orchestration extensions for pi** — one `delegate_task` tool that spawns fully isolated subagent processes, each with its own model, context window, tool permissions, and thinking level.
|
|
12
12
|
|
|
13
13
|
- **Multi-step delegation, natively** — chain, parallel, and dependency-graph modes with per-step model routing, so a complex task fans out exactly as you design it.
|
|
14
|
-
- **True background execution** — live monitor, real abort (`stop_subagent`), per-agent timeouts, and
|
|
14
|
+
- **True background execution** — live monitor, real abort (`stop_subagent`), per-agent timeouts, and a 30-minute default deadline for runs that don't set one. Nothing orphans; nothing leaks.
|
|
15
15
|
- **Preset-driven tool scoping** — every subagent runs with exactly the tools its job needs, restricted by preset or per-call `tools`/`excludeTools`, with auto-route that picks the right preset when you don't.
|
|
16
16
|
- **Templates with slots** — saved, file-backed task templates with `${param}` placeholders for workflows you run again and again.
|
|
17
|
-
- **Safety by default** — task-fence injection protection, sanitized error paths, owner-only persistence, and a
|
|
17
|
+
- **Safety by default** — task-fence injection protection, sanitized error paths, owner-only persistence, and a 1300+ test suite pinning every contract against the real pi SDK.
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -59,11 +59,18 @@ pi remove npm:brl-subagent # uninstall
|
|
|
59
59
|
| `/brl-subagent history` | Browse past subagent runs |
|
|
60
60
|
| `/brl-subagent monitor` | Live monitor running subagents |
|
|
61
61
|
| `/brl-subagent dashboard` | Live observability dashboard |
|
|
62
|
+
| `/brl-subagent compliance` | Compliance reports (file access, secrets exposure, full summary) |
|
|
62
63
|
| `/brl-subagent retry` | Browse failed runs to retry |
|
|
63
64
|
| `/brl-subagent reset` | Reset all configuration |
|
|
64
65
|
|
|
65
66
|
All settings persist across sessions.
|
|
66
67
|
|
|
68
|
+
### Environment variables
|
|
69
|
+
|
|
70
|
+
- `BRL_LOG_LEVEL` — minimum level written to the log file. One of `debug`,
|
|
71
|
+
`info` (default), `warn`, `error`. Read once at module load; unrecognised
|
|
72
|
+
values fall back to `info`. Runtime changes go through `setLogLevel()`.
|
|
73
|
+
|
|
67
74
|
---
|
|
68
75
|
|
|
69
76
|
## delegate_task parameters
|
|
@@ -82,7 +89,7 @@ All settings persist across sessions.
|
|
|
82
89
|
| `inheritSystemPrompt` | boolean | `true` | Whether to inherit the main agent's system prompt. Set `false` to save tokens. |
|
|
83
90
|
| `thinkingLevel` | string | — | `off` / `minimal` / `low` / `medium` / `high` / `xhigh`. Capped at user's configured max. |
|
|
84
91
|
| `outputFile` | string | — | Path for the subagent to write full findings. Returns only a summary. |
|
|
85
|
-
| `timeout` | number | — | Max milliseconds.
|
|
92
|
+
| `timeout` | number | — | Max milliseconds. Foreground: exceeded → SIGTERM (5s grace) → SIGKILL. Background: the session is aborted and the run ends `stopped` with the timeout reason. A background run with no explicit `timeout` gets the 30-minute default deadline — an explicit timeout is honored as given (a default, not a ceiling). A raw value `≥ 2^31-1` normalizes to no timeout (for a background run, the 30-minute default then applies). |
|
|
86
93
|
| `cwd` | string | — | Working directory. Defaults to conductor's cwd. |
|
|
87
94
|
| `background` | boolean | `false` | Spawn as an independent background session; returns an ID immediately. With `tasks` it fans out (one background agent per task, one ID per task), and the conductor is woken once per agent as each finishes. See [Background execution](#background-execution). |
|
|
88
95
|
| `priority` | string | — | Concurrency priority: `critical` / `high` / `normal` / `low`. Defaults to `normal`; higher-priority delegations queue ahead. `tasks[]` / `graph[]` steps can set `priority` per unit (see below). |
|
|
@@ -98,7 +105,7 @@ All settings persist across sessions.
|
|
|
98
105
|
| `approvalMode` | string | — | `auto` / `writes` / `always`. Default is user config (`/brl-subagent approval`). `always` is rejected for background runs. |
|
|
99
106
|
| `force` | boolean | `false` | Dispatch anyway past a capability pre-flight block; the block is downgraded to a warning. |
|
|
100
107
|
| `retryRunId` | string | — | Re-run a previously failed run with its recorded task and params. |
|
|
101
|
-
| `retryOnTimeout` | boolean | `false` | Retry once if
|
|
108
|
+
| `retryOnTimeout` | boolean | `false` | Retry once if a **foreground** subagent times out. The second timeout is a final failure. Background runs are not auto-retried — re-dispatch with `retryRunId`. |
|
|
102
109
|
|
|
103
110
|
**Capability pre-flight:** before spawning, the extension checks that the resolved toolset can actually do the task — a run/execute/test/compile/benchmark task with no `bash`, or an exploration task (search/grep/find/list/locate/glob) with none of `find`/`ls`/`grep`/`bash`. Such a dispatch is **rejected** by default; `force: true` downgrades the rejection to a **warning**. `force` never suppresses an `outputFile`-without-write conflict — that stays a hard error. Warnings are surfaced in the returned result in every mode.
|
|
104
111
|
|
|
@@ -108,7 +115,7 @@ All settings persist across sessions.
|
|
|
108
115
|
|
|
109
116
|
**Fan-out origins are the exception.** Parallel/chain/graph units are recorded per unit, and those records deliberately snapshot **no** `background`, so retrying a unit is a **single run in the foreground**. Pass `background: true` explicitly if you want a background retry.
|
|
110
117
|
|
|
111
|
-
**Not restored:** `chain` / `tasks` / `graph` and `params` are discarded, so retrying a multi-step run silently degrades to a single task — re-issue it fresh for those shapes. `retryOnTimeout` is **explicit-only**: it is never restored from the recorded run and takes effect only when you pass it on the retry call (it arms a deadline for *this* call).
|
|
118
|
+
**Not restored:** `chain` / `tasks` / `graph` and `params` are discarded, so retrying a multi-step run silently degrades to a single task — re-issue it fresh for those shapes. `retryOnTimeout` is **explicit-only**: it is never restored from the recorded run and takes effect only when you pass it on the retry call (foreground only; it arms a deadline for *this* call).
|
|
112
119
|
|
|
113
120
|
**`template` is asymmetric:** the original run's template is **not** restored, but a `template` you supply explicitly on the retry call **does** take effect (it is resolved before the retry merge, issue #175) — its body wins over the recorded `task`.
|
|
114
121
|
|
|
@@ -273,7 +280,7 @@ The built-in `code-review` template's slot is `${target}` (its companion `securi
|
|
|
273
280
|
|
|
274
281
|
## Background execution
|
|
275
282
|
|
|
276
|
-
Set `background: true` to spawn the subagent as an independent session that returns an ID immediately and keeps running. Its progress is tracked via `getAgent` and the live monitor while the main session continues.
|
|
283
|
+
Set `background: true` to spawn the subagent as an independent session that returns an ID immediately and keeps running. Its progress is tracked via `getAgent` and the live monitor while the main session continues. Steering via `steerAgent` delivers the message into the agent's live session — it is queued while the agent runs and delivered after the current tool call finishes, before the next model call. Repeat steers work, and a delivery failure surfaces as a real tool error rather than a recorded-but-undelivered steer.
|
|
277
284
|
|
|
278
285
|
**System prompt semantics:** background sessions are spawned with the same built prompt as foreground subagents (base prompt when `inheritSystemPrompt: true`, custom prompt, preset guidance, and subagent instructions). Like pi's own `--append-system-prompt`, supplying this prompt **replaces** any discovered `.pi/APPEND_SYSTEM.md` / global `APPEND_SYSTEM.md` content — this matches foreground subagent behavior exactly. With `inheritSystemPrompt: true` (the default) your conductor session's instructions — including its own appended content — are inherited through the base prompt.
|
|
279
286
|
|
|
@@ -281,7 +288,7 @@ Set `background: true` to spawn the subagent as an independent session that retu
|
|
|
281
288
|
|
|
282
289
|
**Background safety controls (issue #28):** background agents honor the same safety controls as foreground runs — no more unattended sessions that bypass approval, git isolation, deadlines, or cost:
|
|
283
290
|
|
|
284
|
-
- **Per-agent timeout** — the deadline is armed immediately after the prompt is issued, so in-prompt preflight (auth, model resolution) counts toward it.
|
|
291
|
+
- **Per-agent timeout** — the deadline is armed immediately after the prompt is issued, so in-prompt preflight (auth, model resolution) counts toward it. A background run with no explicit `timeout` gets a 30-minute default deadline (the issue #28 orphan protection); an explicit timeout is honored as given — 30 minutes is the default, not a ceiling. On expiry the session is aborted and the agent ends with status `stopped` and the timeout reason. Timeout values are normalized (`0`/negative/`NaN`/`Infinity`/`≥ 2^31` → no timeout, which for a background run means the 30-minute default) and a double-fire guard prevents the timer from acting on an already-settled agent.
|
|
285
292
|
- **Session cost limit (R5)** — the cost check runs before the background spawn, so a session at its limit cannot bypass it by delegating to background. For a background fan-out the whole batch is checked up front as per-task estimate × N.
|
|
286
293
|
- **Approval mode** — `approvalMode: 'always'` is rejected for background agents (there is no interactive dialog to approve a diff while running unattended); `'writes'` silently auto-approves with a warning logged. For a fan-out, `'always'` rejects the whole batch before any spawn and `'writes'` warns once for the batch.
|
|
287
294
|
- **gitMode branch isolation** — with `gitMode: 'branch'` a work branch is created before the run, the agent's changes are committed at teardown so the diff is real, the diff is captured and surfaced via `get_subagent_result`, and the branch is then switched away from and deleted. This requires a clean working tree — a dirty tree is refused loudly rather than risking the base branch. `gitMode: 'branch'` is rejected for background fan-out — see [Background fan-out](#background-fan-out).
|
|
@@ -296,6 +303,11 @@ Three checks reject the fan-out up front, before any spawn: `approvalMode: 'alwa
|
|
|
296
303
|
|
|
297
304
|
If a spawn fails mid-loop, fan-out stops and reports the failed task plus the IDs already started; an abort mid-loop stops further spawns. Either way, the agents already started stay detached and still wake the conductor. Everything else is unchanged: single background, foreground parallel, the `MAX_PARALLEL_TASKS` cap (8), a live-monitor row per agent, `get_subagent_result` / `steer_subagent` / `stop_subagent` by agent ID, per-agent timeouts, and a single `'writes'` approval warning for the batch.
|
|
298
305
|
|
|
306
|
+
## Contributing
|
|
307
|
+
|
|
308
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, the branch model (`main` = releases, `dev` = integration),
|
|
309
|
+
the worktree workflow, and the test gates. Development docs live in [`.development/`](.development/).
|
|
310
|
+
|
|
299
311
|
## Changelog
|
|
300
312
|
|
|
301
313
|
See [CHANGELOG.md](CHANGELOG.md) for the release history.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brl-subagent",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Subagent
|
|
3
|
+
"version": "2.5.0",
|
|
4
|
+
"description": "Subagent delegation for pi — blocking or background runs, chain/parallel/graph modes, presets and task templates, live monitor, and dispatch capability guards.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
7
7
|
],
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
]
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
|
-
"typecheck": "tsc --noEmit"
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"docs:arch": "node scripts/docs-arch.mjs --write",
|
|
30
|
+
"docs:decisions": "node scripts/docs-decisions.mjs --write"
|
|
29
31
|
},
|
|
30
32
|
"peerDependencies": {
|
|
31
33
|
"@earendil-works/pi-agent-core": "*",
|
|
@@ -35,13 +37,13 @@
|
|
|
35
37
|
"typebox": "*"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
38
|
-
"@earendil-works/pi-agent-core": "^
|
|
39
|
-
"@earendil-works/pi-ai": "^
|
|
40
|
-
"@earendil-works/pi-coding-agent": "^
|
|
41
|
-
"@earendil-works/pi-tui": "^
|
|
40
|
+
"@earendil-works/pi-agent-core": "^1.1.0",
|
|
41
|
+
"@earendil-works/pi-ai": "^1.1.0",
|
|
42
|
+
"@earendil-works/pi-coding-agent": "^1.1.0",
|
|
43
|
+
"@earendil-works/pi-tui": "^1.1.0",
|
|
42
44
|
"jiti": "^2.7.0",
|
|
43
|
-
"typebox": "^1.3.
|
|
44
|
-
"typescript": "
|
|
45
|
+
"typebox": "^1.3.34",
|
|
46
|
+
"typescript": "7.0.2",
|
|
45
47
|
"vitest": "^5.0.0"
|
|
46
48
|
},
|
|
47
49
|
"overrides": {
|
package/src/concurrency.ts
CHANGED
package/src/diff.ts
CHANGED
package/src/event-bus.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
// Purpose: In-memory lifecycle pub/sub (`subagent:*` events) for extensions and the completion wake.
|
|
1
2
|
import type { SubagentEvent, SubagentEventType, SubagentEventListener } from './types';
|
|
3
|
+
import { createLogger } from './logging';
|
|
4
|
+
|
|
5
|
+
// Issue #265: listener failures route through the logger (file-only by default)
|
|
6
|
+
// so raw console.error writes cannot corrupt the pi TUI.
|
|
7
|
+
const log = createLogger('event-bus');
|
|
2
8
|
|
|
3
9
|
// Event listeners by type
|
|
4
10
|
const listeners = new Map<SubagentEventType, Set<SubagentEventListener>>();
|
|
@@ -48,7 +54,7 @@ export function emit(event: SubagentEvent): void {
|
|
|
48
54
|
listener(event);
|
|
49
55
|
} catch (err) {
|
|
50
56
|
// Don't let listener errors break the event loop
|
|
51
|
-
|
|
57
|
+
log.error(`Listener error for ${event.type}`, { error: err instanceof Error ? err.message : String(err) });
|
|
52
58
|
}
|
|
53
59
|
}
|
|
54
60
|
}
|
|
@@ -58,7 +64,7 @@ export function emit(event: SubagentEvent): void {
|
|
|
58
64
|
try {
|
|
59
65
|
listener(event);
|
|
60
66
|
} catch (err) {
|
|
61
|
-
|
|
67
|
+
log.error('Global listener error', { error: err instanceof Error ? err.message : String(err) });
|
|
62
68
|
}
|
|
63
69
|
}
|
|
64
70
|
}
|
package/src/git.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Purpose: Branch-based git workflow for worktree runs: branch creation, diff capture, switch-back, cleanup.
|
|
1
2
|
/**
|
|
2
3
|
* brl-subagent — Git Integration (P3)
|
|
3
4
|
*
|
|
@@ -16,6 +17,11 @@ function gitOpts(cwd: string) {
|
|
|
16
17
|
cwd,
|
|
17
18
|
encoding: "utf-8" as const,
|
|
18
19
|
timeout: 10_000,
|
|
20
|
+
// Capture child stderr instead of inheriting it: expected git failures
|
|
21
|
+
// (e.g. deleting a checked-out branch) are caught and returned as
|
|
22
|
+
// {ok:false, error}; execFileSync still puts the captured stderr in the
|
|
23
|
+
// thrown error's message, so real failures stay diagnosable.
|
|
24
|
+
stdio: ["ignore", "pipe", "pipe"] as ["ignore", "pipe", "pipe"],
|
|
19
25
|
};
|
|
20
26
|
}
|
|
21
27
|
|
|
@@ -32,6 +38,46 @@ export function getCurrentBranch(cwd: string): string {
|
|
|
32
38
|
return output.trim();
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
/**
|
|
42
|
+
* The starting state of HEAD, captured at spawn so teardown can restore it.
|
|
43
|
+
*
|
|
44
|
+
* `getCurrentBranch` alone is NOT enough to restore a detached HEAD: it
|
|
45
|
+
* returns the "HEAD" pseudo-ref, so a `git checkout HEAD` teardown is a no-op
|
|
46
|
+
* that stays on the work branch (stranding it and leaking the branch in CI PR
|
|
47
|
+
* builds, which check out a detached HEAD). The sha + attached-ness must be
|
|
48
|
+
* captured together.
|
|
49
|
+
*
|
|
50
|
+
* `detached` also governs DATA SAFETY at teardown (#308): a detached start has
|
|
51
|
+
* no branch of its own to retain a merged work branch, so the auto-approve path
|
|
52
|
+
* MUST preserve the work branch as the durable ref rather than delete it.
|
|
53
|
+
*/
|
|
54
|
+
export interface HeadState {
|
|
55
|
+
/** Commit HEAD pointed at when captured. */
|
|
56
|
+
sha: string;
|
|
57
|
+
/** True when HEAD was detached (no symbolic ref) at capture time. */
|
|
58
|
+
detached: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Branch name when attached; the literal "HEAD" pseudo-ref when detached.
|
|
61
|
+
* Attached callers can pass it straight to createWorkBranch/captureDiff.
|
|
62
|
+
*/
|
|
63
|
+
branch: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Capture the current HEAD state (sha + attached-ness) for later restoration.
|
|
68
|
+
* Throws if the cwd is not a git repository.
|
|
69
|
+
*/
|
|
70
|
+
export function getHeadState(cwd: string): HeadState {
|
|
71
|
+
const sha = execFileSync("git", ["rev-parse", "HEAD"], gitOpts(cwd)).trim();
|
|
72
|
+
try {
|
|
73
|
+
// Exits non-zero when HEAD is detached — that is the discriminator.
|
|
74
|
+
const branch = execFileSync("git", ["symbolic-ref", "--short", "-q", "HEAD"], gitOpts(cwd)).trim();
|
|
75
|
+
return { sha, detached: false, branch };
|
|
76
|
+
} catch {
|
|
77
|
+
return { sha, detached: true, branch: "HEAD" };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
35
81
|
/**
|
|
36
82
|
* Resolve the repository root that contains the given directory.
|
|
37
83
|
* Returns undefined when the directory is not inside a repository, or when
|
|
@@ -104,6 +150,28 @@ export function switchToBranch(
|
|
|
104
150
|
}
|
|
105
151
|
}
|
|
106
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Restore HEAD to the state captured by getHeadState.
|
|
155
|
+
* Detached starts are re-detached at the captured sha (`git checkout
|
|
156
|
+
* --detach <sha>`) instead of switching to the useless "HEAD" pseudo-ref;
|
|
157
|
+
* attached starts switch back to the original branch.
|
|
158
|
+
* Returns { ok: true } on success, { ok: false, error } on failure.
|
|
159
|
+
*/
|
|
160
|
+
export function restoreHead(
|
|
161
|
+
cwd: string,
|
|
162
|
+
state: HeadState,
|
|
163
|
+
): { ok: true } | { ok: false; error: string } {
|
|
164
|
+
try {
|
|
165
|
+
const args = state.detached
|
|
166
|
+
? ["checkout", "--detach", state.sha]
|
|
167
|
+
: ["checkout", state.branch];
|
|
168
|
+
execFileSync("git", args, gitOpts(cwd));
|
|
169
|
+
return { ok: true };
|
|
170
|
+
} catch (err) {
|
|
171
|
+
return { ok: false, error: (err as Error).message };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
107
175
|
/**
|
|
108
176
|
* Merge a work branch into the current branch.
|
|
109
177
|
* Uses --no-edit to prevent opening an editor.
|
package/src/history.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Purpose: Run-record store: creation, finalization, retry lookup, and history pruning.
|
|
1
2
|
/**
|
|
2
3
|
* brl-subagent — Run History
|
|
3
4
|
*
|
|
@@ -6,7 +7,9 @@
|
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type { SubagentRun, SubagentResult } from "./types";
|
|
9
|
-
import { isSubagentError, getFinalOutput, isSubagentRunShape, classifyError, coherentFailureReason, CUSTOM_ENTRY_TYPES, MAX_RUN_HISTORY_ENTRIES } from "./types";
|
|
10
|
+
import { isSubagentError, getFinalOutput, isSubagentRunShape, classifyError, coherentFailureReason, CUSTOM_ENTRY_TYPES, MAX_RUN_HISTORY_ENTRIES, DEFAULT_OUTPUT_CAP_BYTES } from "./types";
|
|
11
|
+
import { isOutputTruncated } from "./sanitize";
|
|
12
|
+
import { transcriptDisplayPath } from "./transcript-path";
|
|
10
13
|
import type { ExtensionContext, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
11
14
|
|
|
12
15
|
// ---------------------------------------------------------------------------
|
|
@@ -199,3 +202,58 @@ export function formatRunDuration(ms: number): string {
|
|
|
199
202
|
const secs = Math.round((ms % 60_000) / 1000);
|
|
200
203
|
return `${min}m ${secs}s`;
|
|
201
204
|
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Full-output honesty lines (issue #261)
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Human-readable size for an output cap, e.g. `100 * 1024` → "100 KB".
|
|
212
|
+
* Byte caps are whole-KB by construction; sub-KB values (mostly tests) render
|
|
213
|
+
* as plain bytes.
|
|
214
|
+
*/
|
|
215
|
+
function formatCapSize(capBytes: number): string {
|
|
216
|
+
if (capBytes > 0 && capBytes % 1024 === 0) return `${capBytes / 1024} KB`;
|
|
217
|
+
return `${capBytes} B`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Honest disclosure lines for a settled run's full-output view (issue #261).
|
|
222
|
+
*
|
|
223
|
+
* Always returns the transcript pointer. When the stored output hit the
|
|
224
|
+
* `capBytes` cap it also adds a disclosure that points at the transcript for
|
|
225
|
+
* the rest. Detection is deliberately two-signal:
|
|
226
|
+
*
|
|
227
|
+
* 1. `isOutputTruncated(run.fullOutput)` — the exact `capOutput` notice, true
|
|
228
|
+
* only when the stored text was genuinely truncated;
|
|
229
|
+
* 2. `Buffer.byteLength` at/over `capBytes` — the length fallback, so an
|
|
230
|
+
* UNCAPPED record (background/crash paths store raw `liveOutput`) that
|
|
231
|
+
* already sits at or over the cap budget is still disclosed instead of
|
|
232
|
+
* pretending the panel can show more than the cap's worth.
|
|
233
|
+
*
|
|
234
|
+
* Boundary precision: `capOutput` truncates only when the byte length is
|
|
235
|
+
* STRICTLY greater than the cap, so an output of exactly `capBytes` takes the
|
|
236
|
+
* length branch and is disclosed as "at or over" without carrying the notice.
|
|
237
|
+
* The notice branch alone is exact for "was truncated".
|
|
238
|
+
*
|
|
239
|
+
* Pure and pi-tui-free, so it is unit-testable in isolation.
|
|
240
|
+
*/
|
|
241
|
+
export function buildOutputHonestyLines(
|
|
242
|
+
run: Pick<SubagentRun, "id" | "fullOutput">,
|
|
243
|
+
capBytes: number = DEFAULT_OUTPUT_CAP_BYTES,
|
|
244
|
+
): string[] {
|
|
245
|
+
const transcriptPath = transcriptDisplayPath(run.id);
|
|
246
|
+
const lines = [`Transcript: ${transcriptPath}`];
|
|
247
|
+
const output = run.fullOutput;
|
|
248
|
+
const size = formatCapSize(capBytes);
|
|
249
|
+
if (isOutputTruncated(output)) {
|
|
250
|
+
lines.push(
|
|
251
|
+
`Output was truncated at the ${size} cap — full text in the transcript: ${transcriptPath}`,
|
|
252
|
+
);
|
|
253
|
+
} else if (output && Buffer.byteLength(output, "utf8") >= capBytes) {
|
|
254
|
+
lines.push(
|
|
255
|
+
`Stored output is at or over the ${size} cap — full transcript: ${transcriptPath}`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return lines;
|
|
259
|
+
}
|