@dreb/coding-agent 2.64.0 → 2.64.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/core/output-guard.d.ts +9 -1
- package/dist/core/output-guard.d.ts.map +1 -1
- package/dist/core/output-guard.js +75 -20
- package/dist/core/output-guard.js.map +1 -1
- package/dist/modes/rpc/index.d.ts +1 -0
- package/dist/modes/rpc/index.d.ts.map +1 -1
- package/dist/modes/rpc/index.js +1 -0
- package/dist/modes/rpc/index.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +1 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +21 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-event-projection.d.ts +7 -0
- package/dist/modes/rpc/rpc-event-projection.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-event-projection.js +161 -0
- package/dist/modes/rpc/rpc-event-projection.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +10 -7
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/docs/dashboard.md +15 -1
- package/docs/rpc.md +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ Or use a custom provider (corporate proxy, Bedrock, etc.) — see [Custom provid
|
|
|
81
81
|
|
|
82
82
|
Then just talk to dreb. All 13 standard built-in tools are enabled by default (unless `backgroundAgents.maxConcurrentSubagents` is `0`, which removes `subagent` from new parent sessions): `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`, `web_search`, `web_fetch`, `subagent`, `wait`, `watch_github_ci`, and `ask_user`. Use `--tools` to restrict to a subset (e.g., `--tools read,grep,find,ls` for read-only). Three additional tools — `search`, `skill`, and `tasks_update` — are always active regardless of `--tools`. `suggest_next` is active by default but excluded when `--tools` is specified. The model uses these to fulfill your requests. Add capabilities via [skills](#skills), [prompt templates](#prompt-templates), [extensions](#extensions), or [packages](#packages).
|
|
83
83
|
|
|
84
|
-
**Also available:** [`@dreb/telegram`](https://www.npmjs.com/package/@dreb/telegram) — run dreb as a Telegram bot with live tool status and visible results for user-facing tools (`npm install -g @dreb/telegram`). [`@dreb/dashboard`](https://www.npmjs.com/package/@dreb/dashboard) — run `dreb dashboard` for a browser UI with fleet overview, full chat steering, unified dismissible session banners, explicit navigation that preserves viewed closed transcripts as read-only snapshots, a TUI-parity rolling TPS indicator with long-term delta, generic fail-closed built-in slash-command discovery and execution, inline provider/API failures with partial output preserved, sanitized raster tool images plus sent user uploads retained as bounded transcript previews by default, a bounded all-agent subagent panel with drill-in, host file browser, dreb memory editor with exact-revision saves and automatic index cleanup on delete, curated appearance themes with per-browser light/dark mode and font selection (theme default, Google-hosted IBM Plex Mono, or the bundled self-hosted JetBrains Mono, Fira Code, Iosevka, OpenDyslexic, and Atkinson Hyperlegible), and resilient Tailscale/rotating-code pairing with a configurable 180-day default and advance expiry warnings (`npm install -g @dreb/dashboard`; see [docs/dashboard.md](docs/dashboard.md)). Tool images cross browser-facing transport as content-addressed references; browser-local Settings offers placeholders, bounded previews, or informed-opt-in originals, with size disclosure and confirmation above 1 MiB. Full-resolution HTML export remains self-contained. The Memories screen is dreb-only (`~/.dreb/memory` and populated active/on-disk-session project `.dreb/memory`; empty projects are omitted), shows complete indexes with a >200-line warning, opens local index links within the selected scope, replaces stale editor content with visible loading feedback, surfaces malformed entry frontmatter for repair, preserves drafts on conflicts, and does not create/rename entries or expose Claude paths. Compact SSE snapshots update live fleet cards without repeatedly fetching the cross-project inventory, and session drill-in hydrates state, messages, and background agents through one ordered snapshot request. While drill-in remains mounted, authoritative detail refreshes keep its header current, confirmed model/thinking changes are reconciled without stale snapshot rollback, and post-compaction context uses a conservative estimate until fresh provider usage arrives. Terminal provider failures show their reason on fleet cards, while transient failures clear terminal state when automatic retry begins and remain recorded inline on the failed attempt. Its top bar and persistent session header indicators report connecting, connected, retrying, resyncing, disconnected, or auth failed; bounded SSE replay plus an explicit snapshot barrier restores session state, tasks, and image references after a reload, restart, gap, backpressure disconnect, or stalled stream, while authenticated image routes recover bytes separately from authoritative transcripts.
|
|
84
|
+
**Also available:** [`@dreb/telegram`](https://www.npmjs.com/package/@dreb/telegram) — run dreb as a Telegram bot with live tool status and visible results for user-facing tools (`npm install -g @dreb/telegram`). [`@dreb/dashboard`](https://www.npmjs.com/package/@dreb/dashboard) — run `dreb dashboard` for a browser UI with fleet overview, full chat steering, unified dismissible session banners, explicit navigation that preserves viewed closed transcripts as read-only snapshots, a TUI-parity rolling TPS indicator with long-term delta, generic fail-closed built-in slash-command discovery and execution, inline provider/API failures with partial output preserved, sanitized raster tool images plus sent user uploads retained as bounded transcript previews by default, a bounded all-agent subagent panel with drill-in, host file browser, dreb memory editor with exact-revision saves and automatic index cleanup on delete, curated appearance themes with per-browser light/dark mode and font selection (theme default, Google-hosted IBM Plex Mono, or the bundled self-hosted JetBrains Mono, Fira Code, Iosevka, OpenDyslexic, and Atkinson Hyperlegible), and resilient Tailscale/rotating-code pairing with a configurable 180-day default and advance expiry warnings (`npm install -g @dreb/dashboard`; see [docs/dashboard.md](docs/dashboard.md)). Tool images cross browser-facing transport as content-addressed references; in dashboard mode each unique image crosses the RPC stdout pipe at most once per child process (later occurrences become references), and browser-local Settings offers placeholders, bounded previews, or informed-opt-in originals, with size disclosure and confirmation above 1 MiB. Full-resolution HTML export remains self-contained. The Memories screen is dreb-only (`~/.dreb/memory` and populated active/on-disk-session project `.dreb/memory`; empty projects are omitted), shows complete indexes with a >200-line warning, opens local index links within the selected scope, replaces stale editor content with visible loading feedback, surfaces malformed entry frontmatter for repair, preserves drafts on conflicts, and does not create/rename entries or expose Claude paths. Compact SSE snapshots update live fleet cards without repeatedly fetching the cross-project inventory, and session drill-in hydrates state, messages, and background agents through one ordered snapshot request. While drill-in remains mounted, authoritative detail refreshes keep its header current, confirmed model/thinking changes are reconciled without stale snapshot rollback, and post-compaction context uses a conservative estimate until fresh provider usage arrives. Terminal provider failures show their reason on fleet cards, while transient failures clear terminal state when automatic retry begins and remain recorded inline on the failed attempt. Its top bar and persistent session header indicators report connecting, connected, retrying, resyncing, disconnected, or auth failed; bounded SSE replay plus an explicit snapshot barrier restores session state, tasks, and image references after a reload, restart, gap, backpressure disconnect, or stalled stream, while authenticated image routes recover bytes separately from authoritative transcripts.
|
|
85
85
|
|
|
86
86
|
**Platform notes:** [Windows](docs/windows.md) | [Termux (Android)](docs/termux.md) | [tmux](docs/tmux.md) | [Terminal setup](docs/terminal-setup.md) | [Shell aliases](docs/shell-aliases.md)
|
|
87
87
|
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
export declare function takeOverStdout(): void;
|
|
2
2
|
export declare function restoreStdout(): void;
|
|
3
3
|
export declare function isStdoutTakenOver(): boolean;
|
|
4
|
-
/** Maximum aggregate bytes allowed for ordinary queued writes before
|
|
4
|
+
/** Maximum aggregate bytes allowed for ordinary queued writes before the no-drain window starts. */
|
|
5
5
|
export declare const MAX_QUEUED_STDOUT_BYTES: number;
|
|
6
|
+
/** Abort if the queue stays above the cap with no drain progress for this long. */
|
|
7
|
+
export declare const MAX_NO_DRAIN_GRACE_MS = 30000;
|
|
6
8
|
export declare function writeRawStdout(text: string): void;
|
|
7
9
|
export declare function flushRawStdout(): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Test-only: clear all queue state (backlog, byte count, backpressure flag,
|
|
12
|
+
* drain waiters, and the no-drain abort timer) so tests start from a clean
|
|
13
|
+
* process-global slate.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resetOutputGuardForTests(): void;
|
|
8
16
|
//# sourceMappingURL=output-guard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output-guard.d.ts","sourceRoot":"","sources":["../../src/core/output-guard.ts"],"names":[],"mappings":"AAQA,wBAAgB,cAAc,IAAI,IAAI,CAyBrC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAOpC;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;
|
|
1
|
+
{"version":3,"file":"output-guard.d.ts","sourceRoot":"","sources":["../../src/core/output-guard.ts"],"names":[],"mappings":"AAQA,wBAAgB,cAAc,IAAI,IAAI,CAyBrC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAOpC;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAkBD,oGAAoG;AACpG,eAAO,MAAM,uBAAuB,QAAmB,CAAC;AAExD,mFAAmF;AACnF,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAiH5C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAUjD;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAwBpD;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAM/C","sourcesContent":["interface StdoutTakeoverState {\n\trawStdoutWrite: (chunk: string, callback?: (error?: Error | null) => void) => boolean;\n\trawStderrWrite: (chunk: string, callback?: (error?: Error | null) => void) => boolean;\n\toriginalStdoutWrite: typeof process.stdout.write;\n}\n\nlet stdoutTakeoverState: StdoutTakeoverState | undefined;\n\nexport function takeOverStdout(): void {\n\tif (stdoutTakeoverState) {\n\t\treturn;\n\t}\n\n\tconst rawStdoutWrite = process.stdout.write.bind(process.stdout) as StdoutTakeoverState[\"rawStdoutWrite\"];\n\tconst rawStderrWrite = process.stderr.write.bind(process.stderr) as StdoutTakeoverState[\"rawStderrWrite\"];\n\tconst originalStdoutWrite = process.stdout.write;\n\n\tprocess.stdout.write = ((\n\t\tchunk: string | Uint8Array,\n\t\tencodingOrCallback?: BufferEncoding | ((error?: Error | null) => void),\n\t\tcallback?: (error?: Error | null) => void,\n\t): boolean => {\n\t\tif (typeof encodingOrCallback === \"function\") {\n\t\t\treturn rawStderrWrite(String(chunk), encodingOrCallback);\n\t\t}\n\t\treturn rawStderrWrite(String(chunk), callback);\n\t}) as typeof process.stdout.write;\n\n\tstdoutTakeoverState = {\n\t\trawStdoutWrite,\n\t\trawStderrWrite,\n\t\toriginalStdoutWrite,\n\t};\n}\n\nexport function restoreStdout(): void {\n\tif (!stdoutTakeoverState) {\n\t\treturn;\n\t}\n\n\tprocess.stdout.write = stdoutTakeoverState.originalStdoutWrite;\n\tstdoutTakeoverState = undefined;\n}\n\nexport function isStdoutTakenOver(): boolean {\n\treturn stdoutTakeoverState !== undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Backpressure-aware bounded write queue\n//\n// stream.write() returns false when the stream's internal buffer is full\n// (backpressure). Ignoring that signal during high-rate event streaming lets\n// output queue up unboundedly inside the process, which is what produced the\n// multi-thousand-event end-of-response bursts in issue 448. While the stream\n// is backpressured we queue subsequent writes and flush them in order on\n// \"drain\". The queue is byte-capped: legitimate multi-MiB bursts (for example\n// dashboard turns with several inline images, issue 495) can push the backlog\n// past the cap while the consumer is merely slow, so the cap is a grace\n// trigger, not an immediate kill — we only abort when the consumer makes no\n// drain progress for the grace window, which means the output channel is dead\n// or hopelessly behind. That keeps us from growing memory without bound.\n// ---------------------------------------------------------------------------\n\n/** Maximum aggregate bytes allowed for ordinary queued writes before the no-drain window starts. */\nexport const MAX_QUEUED_STDOUT_BYTES = 16 * 1024 * 1024; // 16 MiB\n\n/** Abort if the queue stays above the cap with no drain progress for this long. */\nexport const MAX_NO_DRAIN_GRACE_MS = 30_000;\n\nconst FATAL_DIAGNOSTIC_FLUSH_TIMEOUT_MS = 1_000;\n\nconst stdoutQueue: string[] = [];\nlet stdoutQueuedBytes = 0;\nlet stdoutBackpressured = false;\nlet stdoutDrainListening = false;\nlet stdoutDrainWaiters: Array<() => void> = [];\nlet noDrainAbortTimer: ReturnType<typeof setTimeout> | undefined;\n\nfunction writeToStdout(text: string): boolean {\n\tif (stdoutTakeoverState) {\n\t\treturn stdoutTakeoverState.rawStdoutWrite(text);\n\t}\n\treturn process.stdout.write(text);\n}\n\nfunction requestDrainFlush(): void {\n\tif (stdoutDrainListening) return;\n\tstdoutDrainListening = true;\n\tprocess.stdout.once(\"drain\", () => {\n\t\tstdoutDrainListening = false;\n\t\t// Any drain is forward progress by the consumer: reset the no-drain\n\t\t// abort window. If the backlog is still above the cap after this flush,\n\t\t// the next queued write starts a fresh window.\n\t\tdisarmNoDrainAbort();\n\t\tflushStdoutQueue();\n\t});\n}\n\n/** Start (or keep) the no-drain abort window for an over-cap backlog. */\nfunction armNoDrainAbort(): void {\n\tif (noDrainAbortTimer) return;\n\tconst timer = setTimeout(() => {\n\t\tnoDrainAbortTimer = undefined;\n\t\tabortForStalledConsumer();\n\t}, MAX_NO_DRAIN_GRACE_MS);\n\ttimer.unref();\n\tnoDrainAbortTimer = timer;\n}\n\nfunction disarmNoDrainAbort(): void {\n\tif (!noDrainAbortTimer) return;\n\tclearTimeout(noDrainAbortTimer);\n\tnoDrainAbortTimer = undefined;\n}\n\n/**\n * The consumer exceeded the queue cap and then made no drain progress for the\n * full grace window — treat the output channel as dead and abort loudly\n * instead of holding payloads in memory forever.\n */\nfunction abortForStalledConsumer(): void {\n\tif (stdoutQueuedBytes <= MAX_QUEUED_STDOUT_BYTES) {\n\t\t// The backlog drained back under the cap before the window expired.\n\t\treturn;\n\t}\n\tconst diagnostic =\n\t\t`Fatal: stdout write queue exceeded ${MAX_QUEUED_STDOUT_BYTES} bytes with no drain progress ` +\n\t\t`for ${MAX_NO_DRAIN_GRACE_MS} ms. The consumer of this process's stdout is not reading; ` +\n\t\t\"refusing unbounded memory growth. Aborting.\\n\";\n\tlet exiting = false;\n\tconst exit = (): void => {\n\t\tif (exiting) return;\n\t\texiting = true;\n\t\tclearTimeout(forceExit);\n\t\tprocess.exit(1);\n\t};\n\tconst forceExit = setTimeout(exit, FATAL_DIAGNOSTIC_FLUSH_TIMEOUT_MS);\n\tforceExit.unref();\n\tprocess.stderr.write(diagnostic, exit);\n}\n\nfunction flushStdoutQueue(): void {\n\tstdoutBackpressured = false;\n\twhile (stdoutQueue.length > 0) {\n\t\tconst next = stdoutQueue.shift() as string;\n\t\tstdoutQueuedBytes -= Buffer.byteLength(next);\n\t\t// A false return means the stream accepted the chunk but its buffer is\n\t\t// full again — stop writing and wait for the next drain.\n\t\tif (!writeToStdout(next)) {\n\t\t\tstdoutBackpressured = true;\n\t\t\trequestDrainFlush();\n\t\t\treturn;\n\t\t}\n\t}\n\tif (stdoutDrainWaiters.length > 0) {\n\t\tconst waiters = stdoutDrainWaiters;\n\t\tstdoutDrainWaiters = [];\n\t\tfor (const resolve of waiters) resolve();\n\t}\n}\n\nfunction enqueueStdout(text: string): void {\n\tconst bytes = Buffer.byteLength(text);\n\t// Bound accumulated ordinary backlog, but allow one legitimate protocol frame\n\t// larger than the cap (for example, a complete dashboard snapshot). Once that\n\t// oversized frame is queued, any subsequent write still counts against the cap.\n\tconst exceedsAggregateCap = stdoutQueuedBytes + bytes > MAX_QUEUED_STDOUT_BYTES;\n\tconst isSingleOversizedFrame = bytes > MAX_QUEUED_STDOUT_BYTES && stdoutQueuedBytes <= MAX_QUEUED_STDOUT_BYTES;\n\tif (exceedsAggregateCap && !isSingleOversizedFrame) {\n\t\t// Over the cap: keep queuing (this process has no other output channel)\n\t\t// and start the no-drain abort window. A slow-but-alive consumer — for\n\t\t// example a dashboard synchronously decoding multi-MiB image lines —\n\t\t// makes drain progress before the window expires and is not killed\n\t\t// mid-turn (issue 495); a dead one is.\n\t\tarmNoDrainAbort();\n\t}\n\tstdoutQueue.push(text);\n\tstdoutQueuedBytes += bytes;\n}\n\nexport function writeRawStdout(text: string): void {\n\t// Queue behind any backpressured/queued writes to preserve ordering.\n\tif (stdoutBackpressured || stdoutQueue.length > 0) {\n\t\tenqueueStdout(text);\n\t\treturn;\n\t}\n\tif (!writeToStdout(text)) {\n\t\tstdoutBackpressured = true;\n\t\trequestDrainFlush();\n\t}\n}\n\nexport async function flushRawStdout(): Promise<void> {\n\t// Wait for any queued output to drain so flushes observe true end-of-stream.\n\tif (stdoutBackpressured || stdoutQueue.length > 0) {\n\t\tawait new Promise<void>((resolve) => {\n\t\t\tstdoutDrainWaiters.push(resolve);\n\t\t});\n\t}\n\n\tif (stdoutTakeoverState) {\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tstdoutTakeoverState?.rawStdoutWrite(\"\", (err) => {\n\t\t\t\tif (err) reject(err);\n\t\t\t\telse resolve();\n\t\t\t});\n\t\t});\n\t\treturn;\n\t}\n\n\tawait new Promise<void>((resolve, reject) => {\n\t\tprocess.stdout.write(\"\", (err) => {\n\t\t\tif (err) reject(err);\n\t\t\telse resolve();\n\t\t});\n\t});\n}\n\n/**\n * Test-only: clear all queue state (backlog, byte count, backpressure flag,\n * drain waiters, and the no-drain abort timer) so tests start from a clean\n * process-global slate.\n */\nexport function resetOutputGuardForTests(): void {\n\tstdoutQueue.length = 0;\n\tstdoutQueuedBytes = 0;\n\tstdoutBackpressured = false;\n\tstdoutDrainWaiters.length = 0;\n\tdisarmNoDrainAbort();\n}\n"]}
|
|
@@ -36,18 +36,24 @@ export function isStdoutTakenOver() {
|
|
|
36
36
|
// output queue up unboundedly inside the process, which is what produced the
|
|
37
37
|
// multi-thousand-event end-of-response bursts in issue 448. While the stream
|
|
38
38
|
// is backpressured we queue subsequent writes and flush them in order on
|
|
39
|
-
// "drain". The queue is byte-capped:
|
|
40
|
-
//
|
|
41
|
-
//
|
|
39
|
+
// "drain". The queue is byte-capped: legitimate multi-MiB bursts (for example
|
|
40
|
+
// dashboard turns with several inline images, issue 495) can push the backlog
|
|
41
|
+
// past the cap while the consumer is merely slow, so the cap is a grace
|
|
42
|
+
// trigger, not an immediate kill — we only abort when the consumer makes no
|
|
43
|
+
// drain progress for the grace window, which means the output channel is dead
|
|
44
|
+
// or hopelessly behind. That keeps us from growing memory without bound.
|
|
42
45
|
// ---------------------------------------------------------------------------
|
|
43
|
-
/** Maximum aggregate bytes allowed for ordinary queued writes before
|
|
46
|
+
/** Maximum aggregate bytes allowed for ordinary queued writes before the no-drain window starts. */
|
|
44
47
|
export const MAX_QUEUED_STDOUT_BYTES = 16 * 1024 * 1024; // 16 MiB
|
|
48
|
+
/** Abort if the queue stays above the cap with no drain progress for this long. */
|
|
49
|
+
export const MAX_NO_DRAIN_GRACE_MS = 30_000;
|
|
45
50
|
const FATAL_DIAGNOSTIC_FLUSH_TIMEOUT_MS = 1_000;
|
|
46
51
|
const stdoutQueue = [];
|
|
47
52
|
let stdoutQueuedBytes = 0;
|
|
48
53
|
let stdoutBackpressured = false;
|
|
49
54
|
let stdoutDrainListening = false;
|
|
50
55
|
let stdoutDrainWaiters = [];
|
|
56
|
+
let noDrainAbortTimer;
|
|
51
57
|
function writeToStdout(text) {
|
|
52
58
|
if (stdoutTakeoverState) {
|
|
53
59
|
return stdoutTakeoverState.rawStdoutWrite(text);
|
|
@@ -60,9 +66,55 @@ function requestDrainFlush() {
|
|
|
60
66
|
stdoutDrainListening = true;
|
|
61
67
|
process.stdout.once("drain", () => {
|
|
62
68
|
stdoutDrainListening = false;
|
|
69
|
+
// Any drain is forward progress by the consumer: reset the no-drain
|
|
70
|
+
// abort window. If the backlog is still above the cap after this flush,
|
|
71
|
+
// the next queued write starts a fresh window.
|
|
72
|
+
disarmNoDrainAbort();
|
|
63
73
|
flushStdoutQueue();
|
|
64
74
|
});
|
|
65
75
|
}
|
|
76
|
+
/** Start (or keep) the no-drain abort window for an over-cap backlog. */
|
|
77
|
+
function armNoDrainAbort() {
|
|
78
|
+
if (noDrainAbortTimer)
|
|
79
|
+
return;
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
noDrainAbortTimer = undefined;
|
|
82
|
+
abortForStalledConsumer();
|
|
83
|
+
}, MAX_NO_DRAIN_GRACE_MS);
|
|
84
|
+
timer.unref();
|
|
85
|
+
noDrainAbortTimer = timer;
|
|
86
|
+
}
|
|
87
|
+
function disarmNoDrainAbort() {
|
|
88
|
+
if (!noDrainAbortTimer)
|
|
89
|
+
return;
|
|
90
|
+
clearTimeout(noDrainAbortTimer);
|
|
91
|
+
noDrainAbortTimer = undefined;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The consumer exceeded the queue cap and then made no drain progress for the
|
|
95
|
+
* full grace window — treat the output channel as dead and abort loudly
|
|
96
|
+
* instead of holding payloads in memory forever.
|
|
97
|
+
*/
|
|
98
|
+
function abortForStalledConsumer() {
|
|
99
|
+
if (stdoutQueuedBytes <= MAX_QUEUED_STDOUT_BYTES) {
|
|
100
|
+
// The backlog drained back under the cap before the window expired.
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const diagnostic = `Fatal: stdout write queue exceeded ${MAX_QUEUED_STDOUT_BYTES} bytes with no drain progress ` +
|
|
104
|
+
`for ${MAX_NO_DRAIN_GRACE_MS} ms. The consumer of this process's stdout is not reading; ` +
|
|
105
|
+
"refusing unbounded memory growth. Aborting.\n";
|
|
106
|
+
let exiting = false;
|
|
107
|
+
const exit = () => {
|
|
108
|
+
if (exiting)
|
|
109
|
+
return;
|
|
110
|
+
exiting = true;
|
|
111
|
+
clearTimeout(forceExit);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
};
|
|
114
|
+
const forceExit = setTimeout(exit, FATAL_DIAGNOSTIC_FLUSH_TIMEOUT_MS);
|
|
115
|
+
forceExit.unref();
|
|
116
|
+
process.stderr.write(diagnostic, exit);
|
|
117
|
+
}
|
|
66
118
|
function flushStdoutQueue() {
|
|
67
119
|
stdoutBackpressured = false;
|
|
68
120
|
while (stdoutQueue.length > 0) {
|
|
@@ -87,25 +139,16 @@ function enqueueStdout(text) {
|
|
|
87
139
|
const bytes = Buffer.byteLength(text);
|
|
88
140
|
// Bound accumulated ordinary backlog, but allow one legitimate protocol frame
|
|
89
141
|
// larger than the cap (for example, a complete dashboard snapshot). Once that
|
|
90
|
-
// oversized frame is queued, any subsequent write still
|
|
142
|
+
// oversized frame is queued, any subsequent write still counts against the cap.
|
|
91
143
|
const exceedsAggregateCap = stdoutQueuedBytes + bytes > MAX_QUEUED_STDOUT_BYTES;
|
|
92
144
|
const isSingleOversizedFrame = bytes > MAX_QUEUED_STDOUT_BYTES && stdoutQueuedBytes <= MAX_QUEUED_STDOUT_BYTES;
|
|
93
145
|
if (exceedsAggregateCap && !isSingleOversizedFrame) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return;
|
|
101
|
-
exiting = true;
|
|
102
|
-
clearTimeout(forceExit);
|
|
103
|
-
process.exit(1);
|
|
104
|
-
};
|
|
105
|
-
const forceExit = setTimeout(exit, FATAL_DIAGNOSTIC_FLUSH_TIMEOUT_MS);
|
|
106
|
-
forceExit.unref();
|
|
107
|
-
process.stderr.write(diagnostic, exit);
|
|
108
|
-
return;
|
|
146
|
+
// Over the cap: keep queuing (this process has no other output channel)
|
|
147
|
+
// and start the no-drain abort window. A slow-but-alive consumer — for
|
|
148
|
+
// example a dashboard synchronously decoding multi-MiB image lines —
|
|
149
|
+
// makes drain progress before the window expires and is not killed
|
|
150
|
+
// mid-turn (issue 495); a dead one is.
|
|
151
|
+
armNoDrainAbort();
|
|
109
152
|
}
|
|
110
153
|
stdoutQueue.push(text);
|
|
111
154
|
stdoutQueuedBytes += bytes;
|
|
@@ -148,4 +191,16 @@ export async function flushRawStdout() {
|
|
|
148
191
|
});
|
|
149
192
|
});
|
|
150
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Test-only: clear all queue state (backlog, byte count, backpressure flag,
|
|
196
|
+
* drain waiters, and the no-drain abort timer) so tests start from a clean
|
|
197
|
+
* process-global slate.
|
|
198
|
+
*/
|
|
199
|
+
export function resetOutputGuardForTests() {
|
|
200
|
+
stdoutQueue.length = 0;
|
|
201
|
+
stdoutQueuedBytes = 0;
|
|
202
|
+
stdoutBackpressured = false;
|
|
203
|
+
stdoutDrainWaiters.length = 0;
|
|
204
|
+
disarmNoDrainAbort();
|
|
205
|
+
}
|
|
151
206
|
//# sourceMappingURL=output-guard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output-guard.js","sourceRoot":"","sources":["../../src/core/output-guard.ts"],"names":[],"mappings":"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","sourcesContent":["interface StdoutTakeoverState {\n\trawStdoutWrite: (chunk: string, callback?: (error?: Error | null) => void) => boolean;\n\trawStderrWrite: (chunk: string, callback?: (error?: Error | null) => void) => boolean;\n\toriginalStdoutWrite: typeof process.stdout.write;\n}\n\nlet stdoutTakeoverState: StdoutTakeoverState | undefined;\n\nexport function takeOverStdout(): void {\n\tif (stdoutTakeoverState) {\n\t\treturn;\n\t}\n\n\tconst rawStdoutWrite = process.stdout.write.bind(process.stdout) as StdoutTakeoverState[\"rawStdoutWrite\"];\n\tconst rawStderrWrite = process.stderr.write.bind(process.stderr) as StdoutTakeoverState[\"rawStderrWrite\"];\n\tconst originalStdoutWrite = process.stdout.write;\n\n\tprocess.stdout.write = ((\n\t\tchunk: string | Uint8Array,\n\t\tencodingOrCallback?: BufferEncoding | ((error?: Error | null) => void),\n\t\tcallback?: (error?: Error | null) => void,\n\t): boolean => {\n\t\tif (typeof encodingOrCallback === \"function\") {\n\t\t\treturn rawStderrWrite(String(chunk), encodingOrCallback);\n\t\t}\n\t\treturn rawStderrWrite(String(chunk), callback);\n\t}) as typeof process.stdout.write;\n\n\tstdoutTakeoverState = {\n\t\trawStdoutWrite,\n\t\trawStderrWrite,\n\t\toriginalStdoutWrite,\n\t};\n}\n\nexport function restoreStdout(): void {\n\tif (!stdoutTakeoverState) {\n\t\treturn;\n\t}\n\n\tprocess.stdout.write = stdoutTakeoverState.originalStdoutWrite;\n\tstdoutTakeoverState = undefined;\n}\n\nexport function isStdoutTakenOver(): boolean {\n\treturn stdoutTakeoverState !== undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Backpressure-aware bounded write queue\n//\n// stream.write() returns false when the stream's internal buffer is full\n// (backpressure). Ignoring that signal during high-rate event streaming lets\n// output queue up unboundedly inside the process, which is what produced the\n// multi-thousand-event end-of-response bursts in issue 448. While the stream\n// is backpressured we queue subsequent writes and flush them in order on\n// \"drain\". The queue is byte-capped: a consumer that stalls beyond the cap\n// means this process's primary output channel is dead or hopelessly behind,\n// so we fail loudly instead of growing memory without bound.\n// ---------------------------------------------------------------------------\n\n/** Maximum aggregate bytes allowed for ordinary queued writes before aborting. */\nexport const MAX_QUEUED_STDOUT_BYTES = 16 * 1024 * 1024; // 16 MiB\n\nconst FATAL_DIAGNOSTIC_FLUSH_TIMEOUT_MS = 1_000;\n\nconst stdoutQueue: string[] = [];\nlet stdoutQueuedBytes = 0;\nlet stdoutBackpressured = false;\nlet stdoutDrainListening = false;\nlet stdoutDrainWaiters: Array<() => void> = [];\n\nfunction writeToStdout(text: string): boolean {\n\tif (stdoutTakeoverState) {\n\t\treturn stdoutTakeoverState.rawStdoutWrite(text);\n\t}\n\treturn process.stdout.write(text);\n}\n\nfunction requestDrainFlush(): void {\n\tif (stdoutDrainListening) return;\n\tstdoutDrainListening = true;\n\tprocess.stdout.once(\"drain\", () => {\n\t\tstdoutDrainListening = false;\n\t\tflushStdoutQueue();\n\t});\n}\n\nfunction flushStdoutQueue(): void {\n\tstdoutBackpressured = false;\n\twhile (stdoutQueue.length > 0) {\n\t\tconst next = stdoutQueue.shift() as string;\n\t\tstdoutQueuedBytes -= Buffer.byteLength(next);\n\t\t// A false return means the stream accepted the chunk but its buffer is\n\t\t// full again — stop writing and wait for the next drain.\n\t\tif (!writeToStdout(next)) {\n\t\t\tstdoutBackpressured = true;\n\t\t\trequestDrainFlush();\n\t\t\treturn;\n\t\t}\n\t}\n\tif (stdoutDrainWaiters.length > 0) {\n\t\tconst waiters = stdoutDrainWaiters;\n\t\tstdoutDrainWaiters = [];\n\t\tfor (const resolve of waiters) resolve();\n\t}\n}\n\nfunction enqueueStdout(text: string): void {\n\tconst bytes = Buffer.byteLength(text);\n\t// Bound accumulated ordinary backlog, but allow one legitimate protocol frame\n\t// larger than the cap (for example, a complete dashboard snapshot). Once that\n\t// oversized frame is queued, any subsequent write still fails the cap check.\n\tconst exceedsAggregateCap = stdoutQueuedBytes + bytes > MAX_QUEUED_STDOUT_BYTES;\n\tconst isSingleOversizedFrame = bytes > MAX_QUEUED_STDOUT_BYTES && stdoutQueuedBytes <= MAX_QUEUED_STDOUT_BYTES;\n\tif (exceedsAggregateCap && !isSingleOversizedFrame) {\n\t\tconst diagnostic =\n\t\t\t`Fatal: stdout write queue exceeded ${MAX_QUEUED_STDOUT_BYTES} bytes while the stream was ` +\n\t\t\t\"backpressured. The consumer of this process's stdout is not reading; refusing \" +\n\t\t\t\"unbounded memory growth. Aborting.\\n\";\n\t\tlet exiting = false;\n\t\tconst exit = (): void => {\n\t\t\tif (exiting) return;\n\t\t\texiting = true;\n\t\t\tclearTimeout(forceExit);\n\t\t\tprocess.exit(1);\n\t\t};\n\t\tconst forceExit = setTimeout(exit, FATAL_DIAGNOSTIC_FLUSH_TIMEOUT_MS);\n\t\tforceExit.unref();\n\t\tprocess.stderr.write(diagnostic, exit);\n\t\treturn;\n\t}\n\tstdoutQueue.push(text);\n\tstdoutQueuedBytes += bytes;\n}\n\nexport function writeRawStdout(text: string): void {\n\t// Queue behind any backpressured/queued writes to preserve ordering.\n\tif (stdoutBackpressured || stdoutQueue.length > 0) {\n\t\tenqueueStdout(text);\n\t\treturn;\n\t}\n\tif (!writeToStdout(text)) {\n\t\tstdoutBackpressured = true;\n\t\trequestDrainFlush();\n\t}\n}\n\nexport async function flushRawStdout(): Promise<void> {\n\t// Wait for any queued output to drain so flushes observe true end-of-stream.\n\tif (stdoutBackpressured || stdoutQueue.length > 0) {\n\t\tawait new Promise<void>((resolve) => {\n\t\t\tstdoutDrainWaiters.push(resolve);\n\t\t});\n\t}\n\n\tif (stdoutTakeoverState) {\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tstdoutTakeoverState?.rawStdoutWrite(\"\", (err) => {\n\t\t\t\tif (err) reject(err);\n\t\t\t\telse resolve();\n\t\t\t});\n\t\t});\n\t\treturn;\n\t}\n\n\tawait new Promise<void>((resolve, reject) => {\n\t\tprocess.stdout.write(\"\", (err) => {\n\t\t\tif (err) reject(err);\n\t\t\telse resolve();\n\t\t});\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"output-guard.js","sourceRoot":"","sources":["../../src/core/output-guard.ts"],"names":[],"mappings":"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","sourcesContent":["interface StdoutTakeoverState {\n\trawStdoutWrite: (chunk: string, callback?: (error?: Error | null) => void) => boolean;\n\trawStderrWrite: (chunk: string, callback?: (error?: Error | null) => void) => boolean;\n\toriginalStdoutWrite: typeof process.stdout.write;\n}\n\nlet stdoutTakeoverState: StdoutTakeoverState | undefined;\n\nexport function takeOverStdout(): void {\n\tif (stdoutTakeoverState) {\n\t\treturn;\n\t}\n\n\tconst rawStdoutWrite = process.stdout.write.bind(process.stdout) as StdoutTakeoverState[\"rawStdoutWrite\"];\n\tconst rawStderrWrite = process.stderr.write.bind(process.stderr) as StdoutTakeoverState[\"rawStderrWrite\"];\n\tconst originalStdoutWrite = process.stdout.write;\n\n\tprocess.stdout.write = ((\n\t\tchunk: string | Uint8Array,\n\t\tencodingOrCallback?: BufferEncoding | ((error?: Error | null) => void),\n\t\tcallback?: (error?: Error | null) => void,\n\t): boolean => {\n\t\tif (typeof encodingOrCallback === \"function\") {\n\t\t\treturn rawStderrWrite(String(chunk), encodingOrCallback);\n\t\t}\n\t\treturn rawStderrWrite(String(chunk), callback);\n\t}) as typeof process.stdout.write;\n\n\tstdoutTakeoverState = {\n\t\trawStdoutWrite,\n\t\trawStderrWrite,\n\t\toriginalStdoutWrite,\n\t};\n}\n\nexport function restoreStdout(): void {\n\tif (!stdoutTakeoverState) {\n\t\treturn;\n\t}\n\n\tprocess.stdout.write = stdoutTakeoverState.originalStdoutWrite;\n\tstdoutTakeoverState = undefined;\n}\n\nexport function isStdoutTakenOver(): boolean {\n\treturn stdoutTakeoverState !== undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Backpressure-aware bounded write queue\n//\n// stream.write() returns false when the stream's internal buffer is full\n// (backpressure). Ignoring that signal during high-rate event streaming lets\n// output queue up unboundedly inside the process, which is what produced the\n// multi-thousand-event end-of-response bursts in issue 448. While the stream\n// is backpressured we queue subsequent writes and flush them in order on\n// \"drain\". The queue is byte-capped: legitimate multi-MiB bursts (for example\n// dashboard turns with several inline images, issue 495) can push the backlog\n// past the cap while the consumer is merely slow, so the cap is a grace\n// trigger, not an immediate kill — we only abort when the consumer makes no\n// drain progress for the grace window, which means the output channel is dead\n// or hopelessly behind. That keeps us from growing memory without bound.\n// ---------------------------------------------------------------------------\n\n/** Maximum aggregate bytes allowed for ordinary queued writes before the no-drain window starts. */\nexport const MAX_QUEUED_STDOUT_BYTES = 16 * 1024 * 1024; // 16 MiB\n\n/** Abort if the queue stays above the cap with no drain progress for this long. */\nexport const MAX_NO_DRAIN_GRACE_MS = 30_000;\n\nconst FATAL_DIAGNOSTIC_FLUSH_TIMEOUT_MS = 1_000;\n\nconst stdoutQueue: string[] = [];\nlet stdoutQueuedBytes = 0;\nlet stdoutBackpressured = false;\nlet stdoutDrainListening = false;\nlet stdoutDrainWaiters: Array<() => void> = [];\nlet noDrainAbortTimer: ReturnType<typeof setTimeout> | undefined;\n\nfunction writeToStdout(text: string): boolean {\n\tif (stdoutTakeoverState) {\n\t\treturn stdoutTakeoverState.rawStdoutWrite(text);\n\t}\n\treturn process.stdout.write(text);\n}\n\nfunction requestDrainFlush(): void {\n\tif (stdoutDrainListening) return;\n\tstdoutDrainListening = true;\n\tprocess.stdout.once(\"drain\", () => {\n\t\tstdoutDrainListening = false;\n\t\t// Any drain is forward progress by the consumer: reset the no-drain\n\t\t// abort window. If the backlog is still above the cap after this flush,\n\t\t// the next queued write starts a fresh window.\n\t\tdisarmNoDrainAbort();\n\t\tflushStdoutQueue();\n\t});\n}\n\n/** Start (or keep) the no-drain abort window for an over-cap backlog. */\nfunction armNoDrainAbort(): void {\n\tif (noDrainAbortTimer) return;\n\tconst timer = setTimeout(() => {\n\t\tnoDrainAbortTimer = undefined;\n\t\tabortForStalledConsumer();\n\t}, MAX_NO_DRAIN_GRACE_MS);\n\ttimer.unref();\n\tnoDrainAbortTimer = timer;\n}\n\nfunction disarmNoDrainAbort(): void {\n\tif (!noDrainAbortTimer) return;\n\tclearTimeout(noDrainAbortTimer);\n\tnoDrainAbortTimer = undefined;\n}\n\n/**\n * The consumer exceeded the queue cap and then made no drain progress for the\n * full grace window — treat the output channel as dead and abort loudly\n * instead of holding payloads in memory forever.\n */\nfunction abortForStalledConsumer(): void {\n\tif (stdoutQueuedBytes <= MAX_QUEUED_STDOUT_BYTES) {\n\t\t// The backlog drained back under the cap before the window expired.\n\t\treturn;\n\t}\n\tconst diagnostic =\n\t\t`Fatal: stdout write queue exceeded ${MAX_QUEUED_STDOUT_BYTES} bytes with no drain progress ` +\n\t\t`for ${MAX_NO_DRAIN_GRACE_MS} ms. The consumer of this process's stdout is not reading; ` +\n\t\t\"refusing unbounded memory growth. Aborting.\\n\";\n\tlet exiting = false;\n\tconst exit = (): void => {\n\t\tif (exiting) return;\n\t\texiting = true;\n\t\tclearTimeout(forceExit);\n\t\tprocess.exit(1);\n\t};\n\tconst forceExit = setTimeout(exit, FATAL_DIAGNOSTIC_FLUSH_TIMEOUT_MS);\n\tforceExit.unref();\n\tprocess.stderr.write(diagnostic, exit);\n}\n\nfunction flushStdoutQueue(): void {\n\tstdoutBackpressured = false;\n\twhile (stdoutQueue.length > 0) {\n\t\tconst next = stdoutQueue.shift() as string;\n\t\tstdoutQueuedBytes -= Buffer.byteLength(next);\n\t\t// A false return means the stream accepted the chunk but its buffer is\n\t\t// full again — stop writing and wait for the next drain.\n\t\tif (!writeToStdout(next)) {\n\t\t\tstdoutBackpressured = true;\n\t\t\trequestDrainFlush();\n\t\t\treturn;\n\t\t}\n\t}\n\tif (stdoutDrainWaiters.length > 0) {\n\t\tconst waiters = stdoutDrainWaiters;\n\t\tstdoutDrainWaiters = [];\n\t\tfor (const resolve of waiters) resolve();\n\t}\n}\n\nfunction enqueueStdout(text: string): void {\n\tconst bytes = Buffer.byteLength(text);\n\t// Bound accumulated ordinary backlog, but allow one legitimate protocol frame\n\t// larger than the cap (for example, a complete dashboard snapshot). Once that\n\t// oversized frame is queued, any subsequent write still counts against the cap.\n\tconst exceedsAggregateCap = stdoutQueuedBytes + bytes > MAX_QUEUED_STDOUT_BYTES;\n\tconst isSingleOversizedFrame = bytes > MAX_QUEUED_STDOUT_BYTES && stdoutQueuedBytes <= MAX_QUEUED_STDOUT_BYTES;\n\tif (exceedsAggregateCap && !isSingleOversizedFrame) {\n\t\t// Over the cap: keep queuing (this process has no other output channel)\n\t\t// and start the no-drain abort window. A slow-but-alive consumer — for\n\t\t// example a dashboard synchronously decoding multi-MiB image lines —\n\t\t// makes drain progress before the window expires and is not killed\n\t\t// mid-turn (issue 495); a dead one is.\n\t\tarmNoDrainAbort();\n\t}\n\tstdoutQueue.push(text);\n\tstdoutQueuedBytes += bytes;\n}\n\nexport function writeRawStdout(text: string): void {\n\t// Queue behind any backpressured/queued writes to preserve ordering.\n\tif (stdoutBackpressured || stdoutQueue.length > 0) {\n\t\tenqueueStdout(text);\n\t\treturn;\n\t}\n\tif (!writeToStdout(text)) {\n\t\tstdoutBackpressured = true;\n\t\trequestDrainFlush();\n\t}\n}\n\nexport async function flushRawStdout(): Promise<void> {\n\t// Wait for any queued output to drain so flushes observe true end-of-stream.\n\tif (stdoutBackpressured || stdoutQueue.length > 0) {\n\t\tawait new Promise<void>((resolve) => {\n\t\t\tstdoutDrainWaiters.push(resolve);\n\t\t});\n\t}\n\n\tif (stdoutTakeoverState) {\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tstdoutTakeoverState?.rawStdoutWrite(\"\", (err) => {\n\t\t\t\tif (err) reject(err);\n\t\t\t\telse resolve();\n\t\t\t});\n\t\t});\n\t\treturn;\n\t}\n\n\tawait new Promise<void>((resolve, reject) => {\n\t\tprocess.stdout.write(\"\", (err) => {\n\t\t\tif (err) reject(err);\n\t\t\telse resolve();\n\t\t});\n\t});\n}\n\n/**\n * Test-only: clear all queue state (backlog, byte count, backpressure flag,\n * drain waiters, and the no-drain abort timer) so tests start from a clean\n * process-global slate.\n */\nexport function resetOutputGuardForTests(): void {\n\tstdoutQueue.length = 0;\n\tstdoutQueuedBytes = 0;\n\tstdoutBackpressured = false;\n\tstdoutDrainWaiters.length = 0;\n\tdisarmNoDrainAbort();\n}\n"]}
|
|
@@ -6,5 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export type { ModelInfo, RpcClientOptions, RpcEventListener, RpcExitInfo, RpcExitListener } from "./rpc-client.js";
|
|
8
8
|
export { RpcClient } from "./rpc-client.js";
|
|
9
|
+
export { createDashboardRpcEventProjector, projectDashboardRpcEvent, } from "./rpc-event-projection.js";
|
|
9
10
|
export type { RpcAgentTypeInfo, RpcBackgroundAgentInfo, RpcCommand, RpcCommandType, RpcContextTrustEvaluation, RpcContextTrustMutationResult, RpcDashboardSnapshot, RpcDashboardSnapshotBarrierEvent, RpcEvent, RpcExtensionUIRequest, RpcExtensionUIResponse, RpcPendingMessages, RpcPerformanceStats, RpcQueuedMessage, RpcResources, RpcResponse, RpcScopedModel, RpcSessionInfo, RpcSessionState, RpcSessionTask, RpcSettingsSetResult, RpcSettingsSnapshot, RpcSettingsUpdate, RpcSlashCommand, RpcTreeNode, RpcTrustedFolderRemovalResult, } from "./rpc-types.js";
|
|
10
11
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EAAE,SAAS,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACnH,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,YAAY,EACX,gBAAgB,EAChB,sBAAsB,EACtB,UAAU,EACV,cAAc,EACd,yBAAyB,EACzB,6BAA6B,EAC7B,oBAAoB,EACpB,gCAAgC,EAChC,QAAQ,EACR,qBAAqB,EACrB,sBAAsB,EACtB,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,cAAc,EACd,cAAc,EACd,eAAe,EACf,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,EACf,WAAW,EACX,6BAA6B,GAC7B,MAAM,gBAAgB,CAAC","sourcesContent":["/**\n * RPC client and types for programmatic access to the coding agent.\n *\n * Usage:\n * import { RpcClient } from \"@dreb/coding-agent/rpc\";\n */\n\nexport type { ModelInfo, RpcClientOptions, RpcEventListener, RpcExitInfo, RpcExitListener } from \"./rpc-client.js\";\nexport { RpcClient } from \"./rpc-client.js\";\nexport type {\n\tRpcAgentTypeInfo,\n\tRpcBackgroundAgentInfo,\n\tRpcCommand,\n\tRpcCommandType,\n\tRpcContextTrustEvaluation,\n\tRpcContextTrustMutationResult,\n\tRpcDashboardSnapshot,\n\tRpcDashboardSnapshotBarrierEvent,\n\tRpcEvent,\n\tRpcExtensionUIRequest,\n\tRpcExtensionUIResponse,\n\tRpcPendingMessages,\n\tRpcPerformanceStats,\n\tRpcQueuedMessage,\n\tRpcResources,\n\tRpcResponse,\n\tRpcScopedModel,\n\tRpcSessionInfo,\n\tRpcSessionState,\n\tRpcSessionTask,\n\tRpcSettingsSetResult,\n\tRpcSettingsSnapshot,\n\tRpcSettingsUpdate,\n\tRpcSlashCommand,\n\tRpcTreeNode,\n\tRpcTrustedFolderRemovalResult,\n} from \"./rpc-types.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EAAE,SAAS,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACnH,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EACN,gCAAgC,EAChC,wBAAwB,GACxB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EACX,gBAAgB,EAChB,sBAAsB,EACtB,UAAU,EACV,cAAc,EACd,yBAAyB,EACzB,6BAA6B,EAC7B,oBAAoB,EACpB,gCAAgC,EAChC,QAAQ,EACR,qBAAqB,EACrB,sBAAsB,EACtB,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,cAAc,EACd,cAAc,EACd,eAAe,EACf,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,EACf,WAAW,EACX,6BAA6B,GAC7B,MAAM,gBAAgB,CAAC","sourcesContent":["/**\n * RPC client and types for programmatic access to the coding agent.\n *\n * Usage:\n * import { RpcClient } from \"@dreb/coding-agent/rpc\";\n */\n\nexport type { ModelInfo, RpcClientOptions, RpcEventListener, RpcExitInfo, RpcExitListener } from \"./rpc-client.js\";\nexport { RpcClient } from \"./rpc-client.js\";\nexport {\n\tcreateDashboardRpcEventProjector,\n\tprojectDashboardRpcEvent,\n} from \"./rpc-event-projection.js\";\nexport type {\n\tRpcAgentTypeInfo,\n\tRpcBackgroundAgentInfo,\n\tRpcCommand,\n\tRpcCommandType,\n\tRpcContextTrustEvaluation,\n\tRpcContextTrustMutationResult,\n\tRpcDashboardSnapshot,\n\tRpcDashboardSnapshotBarrierEvent,\n\tRpcEvent,\n\tRpcExtensionUIRequest,\n\tRpcExtensionUIResponse,\n\tRpcPendingMessages,\n\tRpcPerformanceStats,\n\tRpcQueuedMessage,\n\tRpcResources,\n\tRpcResponse,\n\tRpcScopedModel,\n\tRpcSessionInfo,\n\tRpcSessionState,\n\tRpcSessionTask,\n\tRpcSettingsSetResult,\n\tRpcSettingsSnapshot,\n\tRpcSettingsUpdate,\n\tRpcSlashCommand,\n\tRpcTreeNode,\n\tRpcTrustedFolderRemovalResult,\n} from \"./rpc-types.js\";\n"]}
|
package/dist/modes/rpc/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/modes/rpc/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC","sourcesContent":["/**\n * RPC client and types for programmatic access to the coding agent.\n *\n * Usage:\n * import { RpcClient } from \"@dreb/coding-agent/rpc\";\n */\n\nexport type { ModelInfo, RpcClientOptions, RpcEventListener, RpcExitInfo, RpcExitListener } from \"./rpc-client.js\";\nexport { RpcClient } from \"./rpc-client.js\";\nexport type {\n\tRpcAgentTypeInfo,\n\tRpcBackgroundAgentInfo,\n\tRpcCommand,\n\tRpcCommandType,\n\tRpcContextTrustEvaluation,\n\tRpcContextTrustMutationResult,\n\tRpcDashboardSnapshot,\n\tRpcDashboardSnapshotBarrierEvent,\n\tRpcEvent,\n\tRpcExtensionUIRequest,\n\tRpcExtensionUIResponse,\n\tRpcPendingMessages,\n\tRpcPerformanceStats,\n\tRpcQueuedMessage,\n\tRpcResources,\n\tRpcResponse,\n\tRpcScopedModel,\n\tRpcSessionInfo,\n\tRpcSessionState,\n\tRpcSessionTask,\n\tRpcSettingsSetResult,\n\tRpcSettingsSnapshot,\n\tRpcSettingsUpdate,\n\tRpcSlashCommand,\n\tRpcTreeNode,\n\tRpcTrustedFolderRemovalResult,\n} from \"./rpc-types.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/modes/rpc/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EACN,gCAAgC,EAChC,wBAAwB,GACxB,MAAM,2BAA2B,CAAC","sourcesContent":["/**\n * RPC client and types for programmatic access to the coding agent.\n *\n * Usage:\n * import { RpcClient } from \"@dreb/coding-agent/rpc\";\n */\n\nexport type { ModelInfo, RpcClientOptions, RpcEventListener, RpcExitInfo, RpcExitListener } from \"./rpc-client.js\";\nexport { RpcClient } from \"./rpc-client.js\";\nexport {\n\tcreateDashboardRpcEventProjector,\n\tprojectDashboardRpcEvent,\n} from \"./rpc-event-projection.js\";\nexport type {\n\tRpcAgentTypeInfo,\n\tRpcBackgroundAgentInfo,\n\tRpcCommand,\n\tRpcCommandType,\n\tRpcContextTrustEvaluation,\n\tRpcContextTrustMutationResult,\n\tRpcDashboardSnapshot,\n\tRpcDashboardSnapshotBarrierEvent,\n\tRpcEvent,\n\tRpcExtensionUIRequest,\n\tRpcExtensionUIResponse,\n\tRpcPendingMessages,\n\tRpcPerformanceStats,\n\tRpcQueuedMessage,\n\tRpcResources,\n\tRpcResponse,\n\tRpcScopedModel,\n\tRpcSessionInfo,\n\tRpcSessionState,\n\tRpcSessionTask,\n\tRpcSettingsSetResult,\n\tRpcSettingsSnapshot,\n\tRpcSettingsUpdate,\n\tRpcSlashCommand,\n\tRpcTreeNode,\n\tRpcTrustedFolderRemovalResult,\n} from \"./rpc-types.js\";\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc-client.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/rpc-client.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * RPC Client for programmatic access to the coding agent.\n *\n * Spawns the agent in RPC mode and provides a typed API for all operations.\n */\n\nimport { type ChildProcess, spawn } from \"node:child_process\";\nimport type { AgentMessage, ThinkingLevel } from \"@dreb/agent-core\";\nimport type { ImageContent } from \"@dreb/ai\";\nimport type { SessionStats } from \"../../core/agent-session.js\";\nimport type { BashResult } from \"../../core/bash-executor.js\";\nimport type { CompactionResult } from \"../../core/compaction/index.js\";\nimport { attachJsonlLineReader, serializeJsonLine } from \"./jsonl.js\";\nimport type {\n\tRpcAgentTypeInfo,\n\tRpcBackgroundAgentInfo,\n\tRpcCommand,\n\tRpcContextTrustEvaluation,\n\tRpcContextTrustMutationResult,\n\tRpcDashboardSnapshot,\n\tRpcDashboardSnapshotBarrierEvent,\n\tRpcEvent,\n\tRpcExtensionUIRequest,\n\tRpcExtensionUIResponse,\n\tRpcPendingMessages,\n\tRpcPerformanceStats,\n\tRpcResources,\n\tRpcResponse,\n\tRpcSessionInfo,\n\tRpcSessionState,\n\tRpcSettingsSetResult,\n\tRpcSettingsSnapshot,\n\tRpcSettingsUpdate,\n\tRpcSlashCommand,\n\tRpcTreeNode,\n\tRpcTrustedFolderRemovalResult,\n} from \"./rpc-types.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Grace period (ms) start() waits for the child to initialize before treating\n * it as alive. An 'error'/'exit' event during this window rejects start() early.\n */\nconst INIT_GRACE_MS = 100;\n\n/** Distributive Omit that works with union types */\ntype DistributiveOmit<T, K extends keyof T> = T extends unknown ? Omit<T, K> : never;\n\n/** RpcCommand without the id field (for internal send) */\ntype RpcCommandBody = DistributiveOmit<RpcCommand, \"id\">;\n\nexport interface RpcClientOptions {\n\t/** Path to the CLI entry point (default: searches for dist/cli.js) */\n\tcliPath?: string;\n\t/** Working directory for the agent */\n\tcwd?: string;\n\t/** Environment variables */\n\tenv?: Record<string, string>;\n\t/** Provider to use */\n\tprovider?: string;\n\t/** Model ID to use */\n\tmodel?: string;\n\t/** Additional CLI arguments */\n\targs?: string[];\n\t/**\n\t * Run the agent child process under this OS user id.\n\t * Forwarded directly to `child_process.spawn`. The parent process must hold\n\t * `CAP_SETUID` (typically by running as root) for this to succeed; otherwise\n\t * the spawn fails and `start()` rejects. Omitted when unset (default behavior).\n\t */\n\tuid?: number;\n\t/**\n\t * Run the agent child process under this OS group id.\n\t * Forwarded directly to `child_process.spawn`. The parent process must hold\n\t * `CAP_SETGID` (typically by running as root) for this to succeed; otherwise\n\t * the spawn fails and `start()` rejects. Omitted when unset (default behavior).\n\t */\n\tgid?: number;\n}\n\nexport interface ModelInfo {\n\tprovider: string;\n\tid: string;\n\tcontextWindow: number;\n\treasoning: boolean;\n}\n\n/**\n * Listener for non-response messages streamed by the RPC server. The wire carries\n * the full {@link RpcEvent} union: session events, extension UI requests, and\n * RPC-specific ordering markers such as `dashboard_snapshot_barrier`.\n */\nexport type RpcEventListener = (event: RpcEvent) => void;\n\nexport type RpcExitInfo =\n\t| { code: number | null; signal: NodeJS.Signals | null; error?: undefined }\n\t| { code?: undefined; signal?: undefined; error: Error };\n\nexport type RpcExitListener = (info: RpcExitInfo) => void;\n\ntype JsonRecord = Record<string, unknown>;\n\nfunction isJsonRecord(value: unknown): value is JsonRecord {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isRpcResponseMessage(value: unknown): value is RpcResponse {\n\treturn isJsonRecord(value) && value.type === \"response\";\n}\n\nfunction isRpcDashboardSnapshotBarrierEvent(value: unknown): value is RpcDashboardSnapshotBarrierEvent {\n\treturn isJsonRecord(value) && value.type === \"dashboard_snapshot_barrier\" && typeof value.snapshotId === \"string\";\n}\n\nfunction isRpcExtensionUIRequestMessage(value: unknown): value is RpcExtensionUIRequest {\n\treturn (\n\t\tisJsonRecord(value) &&\n\t\tvalue.type === \"extension_ui_request\" &&\n\t\ttypeof value.id === \"string\" &&\n\t\ttypeof value.method === \"string\"\n\t);\n}\n\nfunction isRpcEvent(value: unknown): value is RpcEvent {\n\tif (!isJsonRecord(value) || typeof value.type !== \"string\" || value.type === \"response\") {\n\t\treturn false;\n\t}\n\tif (value.type === \"dashboard_snapshot_barrier\") {\n\t\treturn isRpcDashboardSnapshotBarrierEvent(value);\n\t}\n\tif (value.type === \"extension_ui_request\") {\n\t\treturn isRpcExtensionUIRequestMessage(value);\n\t}\n\treturn true;\n}\n\n// ============================================================================\n// RPC Client\n// ============================================================================\n\nexport class RpcClient {\n\tprivate process: ChildProcess | null = null;\n\tprivate stopReadingStdout: (() => void) | null = null;\n\tprivate eventListeners: RpcEventListener[] = [];\n\tprivate exitListeners = new Set<RpcExitListener>();\n\tprivate pendingRequests: Map<string, { resolve: (response: RpcResponse) => void; reject: (error: Error) => void }> =\n\t\tnew Map();\n\tprivate requestId = 0;\n\tprivate stderr = \"\";\n\tprivate _dead = false;\n\tprivate spawnError: Error | null = null;\n\n\tconstructor(private options: RpcClientOptions = {}) {}\n\n\t/**\n\t * Start the RPC agent process.\n\t */\n\tasync start(): Promise<void> {\n\t\tif (this.process) {\n\t\t\tthrow new Error(\"Client already started\");\n\t\t}\n\n\t\tconst cliPath = this.options.cliPath ?? \"dist/cli.js\";\n\t\tconst args = [\"--mode\", \"rpc\"];\n\n\t\tif (this.options.provider) {\n\t\t\targs.push(\"--provider\", this.options.provider);\n\t\t}\n\t\tif (this.options.model) {\n\t\t\targs.push(\"--model\", this.options.model);\n\t\t}\n\t\tif (this.options.args) {\n\t\t\targs.push(...this.options.args);\n\t\t}\n\n\t\tthis.process = spawn(\"node\", [cliPath, ...args], {\n\t\t\tcwd: this.options.cwd,\n\t\t\tenv: { ...process.env, ...this.options.env },\n\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t\t...(this.options.uid != null ? { uid: this.options.uid } : {}),\n\t\t\t...(this.options.gid != null ? { gid: this.options.gid } : {}),\n\t\t});\n\n\t\tthis._dead = false;\n\t\tthis.spawnError = null;\n\n\t\t// Collect stderr for debugging\n\t\tthis.process.stderr?.on(\"data\", (data) => {\n\t\t\tthis.stderr += data.toString();\n\t\t});\n\n\t\t// Detect process exit and spawn failures for the whole session lifecycle —\n\t\t// reject pending requests so callers don't hang. Capture the process\n\t\t// reference so stale handlers from a previous process don't clobber state\n\t\t// after a stop()/start() cycle.\n\t\tconst procRef = this.process;\n\t\tprocRef.on(\"exit\", (code, signal) => {\n\t\t\t// Guard: skip if this handler belongs to an old, already-stopped process\n\t\t\tif (this.process !== procRef) return;\n\t\t\tthis.failPendingRequests(`RPC process exited with code ${code}, signal ${signal}`);\n\t\t\tthis.notifyExitListeners({ code, signal });\n\t\t});\n\n\t\t// Spawn failures surface asynchronously as an 'error' event rather than a\n\t\t// thrown exception (e.g. EPERM when dropping to a uid/gid the parent lacks\n\t\t// CAP_SETUID/CAP_SETGID for, or unsupported on Windows). Without a listener\n\t\t// an 'error' event would crash the process; record the cause so start() can\n\t\t// fail loudly and a later send() can report the real reason instead of a\n\t\t// generic \"not running\".\n\t\tprocRef.on(\"error\", (err) => {\n\t\t\t// Guard: skip if this handler belongs to an old, already-stopped process\n\t\t\tif (this.process !== procRef) return;\n\t\t\tthis.spawnError = err;\n\t\t\tthis.failPendingRequests(`RPC process failed to spawn: ${err.message}`);\n\t\t\tthis.notifyExitListeners({ error: err });\n\t\t});\n\n\t\t// Set up strict JSONL reader for stdout.\n\t\tthis.stopReadingStdout = attachJsonlLineReader(this.process.stdout!, (line) => {\n\t\t\tthis.handleLine(line);\n\t\t});\n\n\t\t// Give the process a moment to initialize, but settle as soon as an\n\t\t// 'error' or 'exit' event arrives. Racing the grace period against those\n\t\t// events (instead of a blind fixed sleep) guarantees that a failed\n\t\t// privilege drop (uid/gid EPERM) rejects start() loudly no matter when\n\t\t// libuv delivers the event — a fixed sleep could expire first and let\n\t\t// start() resolve despite the child never running.\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tconst onError = (err: Error) => {\n\t\t\t\tcleanup();\n\t\t\t\treject(new Error(`Agent process failed to spawn: ${err.message}. Stderr: ${this.stderr}`));\n\t\t\t};\n\t\t\tconst onExit = (code: number | null, signal: NodeJS.Signals | null) => {\n\t\t\t\tcleanup();\n\t\t\t\treject(\n\t\t\t\t\tnew Error(\n\t\t\t\t\t\t`Agent process exited immediately with code ${code}, signal ${signal}. Stderr: ${this.stderr}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t};\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve();\n\t\t\t}, INIT_GRACE_MS);\n\t\t\tconst cleanup = () => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tprocRef.off(\"error\", onError);\n\t\t\t\tprocRef.off(\"exit\", onExit);\n\t\t\t};\n\t\t\tprocRef.once(\"error\", onError);\n\t\t\tprocRef.once(\"exit\", onExit);\n\t\t});\n\n\t\t// Belt-and-suspenders: a process that exited exactly as the grace timer\n\t\t// fired (so the race resolved instead of rejecting) still surfaces loudly.\n\t\tif (this.process.exitCode !== null) {\n\t\t\tthrow new Error(`Agent process exited immediately with code ${this.process.exitCode}. Stderr: ${this.stderr}`);\n\t\t}\n\t}\n\n\t/**\n\t * Stop the RPC agent process.\n\t */\n\tasync stop(): Promise<void> {\n\t\tif (!this.process) return;\n\n\t\tthis.stopReadingStdout?.();\n\t\tthis.stopReadingStdout = null;\n\t\tthis.process.kill(\"SIGTERM\");\n\n\t\t// Wait for process to exit\n\t\tawait new Promise<void>((resolve) => {\n\t\t\tconst timeout = setTimeout(() => {\n\t\t\t\tthis.process?.kill(\"SIGKILL\");\n\t\t\t\tresolve();\n\t\t\t}, 1000);\n\n\t\t\tthis.process?.on(\"exit\", () => {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\tresolve();\n\t\t\t});\n\t\t});\n\n\t\tthis.process = null;\n\t\tthis._dead = true;\n\t\tthis.pendingRequests.clear();\n\t}\n\n\t/**\n\t * Subscribe to agent events.\n\t */\n\tonEvent(listener: RpcEventListener): () => void {\n\t\tthis.eventListeners.push(listener);\n\t\treturn () => {\n\t\t\tconst index = this.eventListeners.indexOf(listener);\n\t\t\tif (index !== -1) {\n\t\t\t\tthis.eventListeners.splice(index, 1);\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * Subscribe to child process death/spawn-failure notifications.\n\t */\n\tonExit(listener: RpcExitListener): () => void {\n\t\tthis.exitListeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.exitListeners.delete(listener);\n\t\t};\n\t}\n\n\t/**\n\t * Get collected stderr output (useful for debugging).\n\t */\n\tgetStderr(): string {\n\t\treturn this.stderr;\n\t}\n\n\t// =========================================================================\n\t// Command Methods\n\t// =========================================================================\n\n\t/**\n\t * Send a prompt to the agent.\n\t * Returns immediately after sending; use onEvent() to receive streaming events.\n\t * Use waitForIdle() to wait for completion.\n\t */\n\tasync prompt(message: string, images?: ImageContent[]): Promise<void> {\n\t\tawait this.send({ type: \"prompt\", message, images });\n\t}\n\n\t/**\n\t * Queue a steering message to interrupt the agent mid-run.\n\t */\n\tasync steer(message: string, images?: ImageContent[]): Promise<void> {\n\t\tawait this.send({ type: \"steer\", message, images });\n\t}\n\n\t/**\n\t * Queue a follow-up message to be processed after the agent finishes.\n\t */\n\tasync followUp(message: string, images?: ImageContent[]): Promise<void> {\n\t\tawait this.send({ type: \"follow_up\", message, images });\n\t}\n\n\t/** Queue an unchanged steering message in a specific live background agent. */\n\tasync steerBackgroundAgent(agentId: string, message: string): Promise<void> {\n\t\tconst response = await this.send({ type: \"steer_background_agent\", agentId, message });\n\t\tthis.getData(response);\n\t}\n\n\t/** Read the selected live background agent's pending steering queue. */\n\tasync getBackgroundAgentPending(agentId: string): Promise<{\n\t\tsteeringMode: \"all\" | \"one-at-a-time\";\n\t\tpending: RpcPendingMessages;\n\t}> {\n\t\tconst response = await this.send({ type: \"get_background_agent_pending\", agentId });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Abort current operation.\n\t */\n\tasync abort(): Promise<void> {\n\t\tawait this.send({ type: \"abort\" });\n\t}\n\n\t/**\n\t * Start a new session, optionally with parent tracking.\n\t * @param parentSession - Optional parent session path for lineage tracking\n\t * @returns Object with `cancelled: true` if an extension cancelled the new session\n\t */\n\tasync newSession(parentSession?: string): Promise<{ cancelled: boolean }> {\n\t\tconst response = await this.send({ type: \"new_session\", parentSession });\n\t\treturn this.getData(response);\n\t}\n\n\t/** Reload extensions, skills, prompts, themes, and other session resources. */\n\tasync reload(): Promise<void> {\n\t\tawait this.send({ type: \"reload\" });\n\t}\n\n\t/** Run `/dream` or one of its backup-path forms. */\n\tasync dream(args?: string): Promise<{ message: string }> {\n\t\tconst response = await this.send({ type: \"dream\", args });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Get current session state.\n\t */\n\tasync getState(): Promise<RpcSessionState> {\n\t\tconst response = await this.send({ type: \"get_state\" });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Capture a dashboard recovery snapshot. The RPC stream emits a matching\n\t * `dashboard_snapshot_barrier` immediately before this response; consumers\n\t * need that event's EventHub sequence before applying the snapshot.\n\t */\n\tasync getDashboardSnapshot(): Promise<RpcDashboardSnapshot> {\n\t\tconst response = await this.send({ type: \"get_dashboard_snapshot\" });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Get loaded context/resources metadata (paths/names only, no file contents).\n\t */\n\tasync getResources(): Promise<RpcResources> {\n\t\tconst response = await this.send({ type: \"get_resources\" });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Get current git branch for the agent cwd.\n\t */\n\tasync getGitBranch(): Promise<string | null> {\n\t\tconst response = await this.send({ type: \"get_git_branch\" });\n\t\treturn this.getData<{ branch: string | null }>(response).branch;\n\t}\n\n\t/**\n\t * Get cached same-day cost across all sessions, primed by the server on first call.\n\t */\n\tasync getDailyCost(): Promise<number> {\n\t\tconst response = await this.send({ type: \"get_daily_cost\" });\n\t\treturn this.getData<{ cost: number }>(response).cost;\n\t}\n\n\t/**\n\t * Set model by provider and ID.\n\t */\n\tasync setModel(provider: string, modelId: string): Promise<{ provider: string; id: string }> {\n\t\tconst response = await this.send({ type: \"set_model\", provider, modelId });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Cycle to next model.\n\t */\n\tasync cycleModel(): Promise<{\n\t\tmodel: { provider: string; id: string };\n\t\tthinkingLevel: ThinkingLevel;\n\t\tisScoped: boolean;\n\t} | null> {\n\t\tconst response = await this.send({ type: \"cycle_model\" });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Get list of available models.\n\t */\n\tasync getAvailableModels(): Promise<ModelInfo[]> {\n\t\tconst response = await this.send({ type: \"get_available_models\" });\n\t\treturn this.getData<{ models: ModelInfo[] }>(response).models;\n\t}\n\n\t/**\n\t * Resolve a model pattern using the same logic as CLI/TUI.\n\t * Runs server-side so API keys never leave the process.\n\t * Returns null if no match found.\n\t */\n\tasync resolveModel(pattern: string): Promise<{ model: ModelInfo; warning?: string } | null> {\n\t\tconst response = await this.send({ type: \"resolve_model\", pattern });\n\t\treturn this.getData<{ model: ModelInfo; warning?: string } | null>(response);\n\t}\n\n\t/**\n\t * Hatch a new buddy companion. Runs inside the agent process\n\t * so API keys never leave the process boundary.\n\t */\n\tasync buddyHatch(): Promise<import(\"../../core/buddy/buddy-types.js\").BuddyState> {\n\t\tconst response = await this.send({ type: \"buddy_hatch\" });\n\t\treturn this.getData<{ state: import(\"../../core/buddy/buddy-types.js\").BuddyState }>(response).state;\n\t}\n\n\t/**\n\t * Reroll the buddy companion. Runs inside the agent process\n\t * so API keys never leave the process boundary.\n\t */\n\tasync buddyReroll(): Promise<import(\"../../core/buddy/buddy-types.js\").BuddyState> {\n\t\tconst response = await this.send({ type: \"buddy_reroll\" });\n\t\treturn this.getData<{ state: import(\"../../core/buddy/buddy-types.js\").BuddyState }>(response).state;\n\t}\n\n\t/**\n\t * Set thinking level.\n\t */\n\tasync setThinkingLevel(level: ThinkingLevel): Promise<void> {\n\t\tawait this.send({ type: \"set_thinking_level\", level });\n\t}\n\n\t/**\n\t * Cycle thinking level.\n\t */\n\tasync cycleThinkingLevel(): Promise<{ level: ThinkingLevel } | null> {\n\t\tconst response = await this.send({ type: \"cycle_thinking_level\" });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Set steering mode.\n\t */\n\tasync setSteeringMode(mode: \"all\" | \"one-at-a-time\"): Promise<void> {\n\t\tawait this.send({ type: \"set_steering_mode\", mode });\n\t}\n\n\t/**\n\t * Set follow-up mode.\n\t */\n\tasync setFollowUpMode(mode: \"all\" | \"one-at-a-time\"): Promise<void> {\n\t\tawait this.send({ type: \"set_follow_up_mode\", mode });\n\t}\n\n\t/**\n\t * Get pending steering and follow-up messages without clearing them.\n\t */\n\tasync getPendingMessages(): Promise<RpcPendingMessages> {\n\t\tconst response = await this.send({ type: \"get_pending_messages\" });\n\t\treturn this.getData<RpcPendingMessages>(response);\n\t}\n\n\t/**\n\t * Clear pending steering and follow-up messages, returning the cleared text.\n\t */\n\tasync clearPendingMessages(): Promise<RpcPendingMessages> {\n\t\tconst response = await this.send({ type: \"clear_pending_messages\" });\n\t\treturn this.getData<RpcPendingMessages>(response);\n\t}\n\n\t/**\n\t * Compact session context.\n\t */\n\tasync compact(customInstructions?: string): Promise<CompactionResult> {\n\t\tconst response = await this.send({ type: \"compact\", customInstructions });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Set auto-compaction enabled/disabled.\n\t */\n\tasync setAutoCompaction(enabled: boolean): Promise<void> {\n\t\tawait this.send({ type: \"set_auto_compaction\", enabled });\n\t}\n\n\t/**\n\t * Abort in-progress compaction.\n\t */\n\tasync abortCompaction(): Promise<void> {\n\t\tawait this.send({ type: \"abort_compaction\" });\n\t}\n\n\t/**\n\t * Set auto-retry enabled/disabled.\n\t */\n\tasync setAutoRetry(enabled: boolean): Promise<void> {\n\t\tawait this.send({ type: \"set_auto_retry\", enabled });\n\t}\n\n\t/**\n\t * Abort in-progress retry.\n\t */\n\tasync abortRetry(): Promise<void> {\n\t\tawait this.send({ type: \"abort_retry\" });\n\t}\n\n\t/**\n\t * Execute a bash command.\n\t */\n\tasync bash(command: string): Promise<BashResult> {\n\t\tconst response = await this.send({ type: \"bash\", command });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Abort running bash command.\n\t */\n\tasync abortBash(): Promise<void> {\n\t\tawait this.send({ type: \"abort_bash\" });\n\t}\n\n\t/**\n\t * Get session statistics.\n\t */\n\tasync getSessionStats(): Promise<SessionStats> {\n\t\tconst response = await this.send({ type: \"get_session_stats\" });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Get performance statistics.\n\t */\n\tasync getPerformanceStats(): Promise<RpcPerformanceStats> {\n\t\tconst response = await this.send({ type: \"get_performance_stats\" });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Export session to HTML.\n\t */\n\tasync exportHtml(outputPath?: string): Promise<{ path: string }> {\n\t\tconst response = await this.send({ type: \"export_html\", outputPath });\n\t\treturn this.getData(response);\n\t}\n\n\t/** Import a JSONL file into the current runtime. */\n\tasync importJsonl(inputPath: string): Promise<{ cancelled: boolean }> {\n\t\tconst response = await this.send({ type: \"import_jsonl\", inputPath });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Switch to a different session file.\n\t * @returns Object with `cancelled: true` if an extension cancelled the switch\n\t */\n\tasync switchSession(sessionPath: string): Promise<{ cancelled: boolean }> {\n\t\tconst response = await this.send({ type: \"switch_session\", sessionPath });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Delete a session file, trying trash first and falling back to unlink.\n\t * Throws on failure, including when attempting to delete the active session.\n\t */\n\tasync deleteSession(sessionPath: string): Promise<{ method: \"trash\" | \"unlink\" }> {\n\t\tconst response = await this.send({ type: \"delete_session\", sessionPath });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Fork from a specific message.\n\t * @returns Object with `text` (the message text) and `cancelled` (if extension cancelled)\n\t */\n\tasync fork(entryId: string): Promise<{ text: string; cancelled: boolean }> {\n\t\tconst response = await this.send({ type: \"fork\", entryId });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Get messages available for forking.\n\t */\n\tasync getForkMessages(): Promise<Array<{ entryId: string; text: string; role: \"user\" | \"assistant\" }>> {\n\t\tconst response = await this.send({ type: \"get_fork_messages\" });\n\t\treturn this.getData<{ messages: Array<{ entryId: string; text: string; role: \"user\" | \"assistant\" }> }>(response)\n\t\t\t.messages;\n\t}\n\n\t/**\n\t * Get the session tree and current leaf.\n\t */\n\tasync getTree(): Promise<{ roots: RpcTreeNode[]; leafId: string | null }> {\n\t\tconst response = await this.send({ type: \"get_tree\" });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Navigate to a session tree entry.\n\t * @param targetId Entry ID to navigate to\n\t * @param options Navigation options; timeoutMs is client-side only and is not sent over RPC\n\t * @returns Object with `cancelled: true` if navigation was cancelled, and `editorText` when re-editing a message\n\t */\n\tasync navigateTree(\n\t\ttargetId: string,\n\t\toptions?: {\n\t\t\tsummarize?: boolean;\n\t\t\tcustomInstructions?: string;\n\t\t\treplaceInstructions?: boolean;\n\t\t\tlabel?: string;\n\t\t\ttimeoutMs?: number;\n\t\t},\n\t): Promise<{ cancelled: boolean; editorText?: string }> {\n\t\tconst { timeoutMs = 300000, ...commandOptions } = options ?? {};\n\t\tconst response = await this.send({ type: \"navigate_tree\", targetId, ...commandOptions }, timeoutMs);\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Get text of last assistant message.\n\t */\n\tasync getLastAssistantText(): Promise<string | null> {\n\t\tconst response = await this.send({ type: \"get_last_assistant_text\" });\n\t\treturn this.getData<{ text: string | null }>(response).text;\n\t}\n\n\t/**\n\t * Set the session display name.\n\t */\n\tasync setSessionName(name: string): Promise<void> {\n\t\tawait this.send({ type: \"set_session_name\", name });\n\t}\n\n\t/**\n\t * Get all messages in the session.\n\t */\n\tasync getMessages(): Promise<AgentMessage[]> {\n\t\tconst response = await this.send({ type: \"get_messages\" });\n\t\treturn this.getData<{ messages: AgentMessage[] }>(response).messages;\n\t}\n\n\t/**\n\t * Get discoverable commands, including built-ins that require client-side handling.\n\t */\n\tasync getCommands(): Promise<RpcSlashCommand[]> {\n\t\tconst response = await this.send({ type: \"get_commands\" });\n\t\treturn this.getData<{ commands: RpcSlashCommand[] }>(response).commands;\n\t}\n\n\t/**\n\t * List sessions for the current working directory.\n\t * Returns sessions sorted by most recently modified first.\n\t */\n\tasync listSessions(): Promise<RpcSessionInfo[]> {\n\t\tconst response = await this.send({ type: \"list_sessions\" });\n\t\treturn this.getData<{ sessions: RpcSessionInfo[] }>(response).sessions;\n\t}\n\n\t/**\n\t * List sessions across all projects.\n\t * Returns sessions sorted by most recently modified first. May be slow with many sessions.\n\t */\n\tasync listAllSessions(): Promise<RpcSessionInfo[]> {\n\t\tconst response = await this.send({ type: \"list_all_sessions\" });\n\t\treturn this.getData<{ sessions: RpcSessionInfo[] }>(response).sessions;\n\t}\n\n\t/**\n\t * List background subagents tracked by the server's registry (running and\n\t * recently completed), including session dir/file paths where known.\n\t */\n\tasync listBackgroundAgents(): Promise<RpcBackgroundAgentInfo[]> {\n\t\tconst response = await this.send({ type: \"list_background_agents\" });\n\t\treturn this.getData<{ agents: RpcBackgroundAgentInfo[] }>(response).agents;\n\t}\n\n\t/**\n\t * List discoverable subagent types for the server's current working directory.\n\t */\n\tasync listAgentTypes(): Promise<RpcAgentTypeInfo[]> {\n\t\tconst response = await this.send({ type: \"list_agent_types\" });\n\t\treturn this.getData<{ agentTypes: RpcAgentTypeInfo[] }>(response).agentTypes;\n\t}\n\n\t/**\n\t * Answer an extension UI request (select/confirm/input/editor) previously\n\t * received as an `extension_ui_request` event. Fire-and-forget: the server\n\t * does not send a response to this message.\n\t */\n\tsendExtensionUIResponse(response: RpcExtensionUIResponse): void {\n\t\tif (this._dead || !this.process?.stdin) {\n\t\t\tconst cause = this.spawnError ? ` Spawn error: ${this.spawnError.message}.` : \"\";\n\t\t\tthrow new Error(`RPC process is not running.${cause} Stderr: ${this.stderr}`);\n\t\t}\n\t\tthis.process.stdin.write(serializeJsonLine(response));\n\t}\n\n\t/**\n\t * Get the dreb version.\n\t */\n\tasync getVersion(): Promise<string> {\n\t\tconst response = await this.send({ type: \"get_version\" });\n\t\treturn this.getData<{ version: string }>(response).version;\n\t}\n\n\t/**\n\t * Get the persistent default settings (SettingsManager-backed).\n\t * These seed fresh runtimes — for live session state use getState().\n\t */\n\tasync getSettings(): Promise<RpcSettingsSnapshot> {\n\t\tconst response = await this.send({ type: \"get_settings\" });\n\t\treturn this.getData<RpcSettingsSnapshot>(response);\n\t}\n\n\t/**\n\t * Update persistent default settings. Validates the whole payload before applying\n\t * anything (atomic: on any invalid field, nothing changes). Does NOT touch live\n\t * session state — use setModel/setThinkingLevel/etc. for that.\n\t * Returns the full settings snapshot after the write, plus any loud warnings\n\t * (for example project-level agent model overrides shadowing global writes).\n\t */\n\tasync setSettings(settings: RpcSettingsUpdate): Promise<RpcSettingsSetResult> {\n\t\tconst response = await this.send({ type: \"set_settings\", settings });\n\t\treturn this.getData<RpcSettingsSetResult>(response);\n\t}\n\n\t/** Evaluate an existing directory against the server's global context-trust policy. */\n\tasync evaluateContextTrust(path: string): Promise<RpcContextTrustEvaluation> {\n\t\tconst response = await this.send({ type: \"evaluate_context_trust\", path });\n\t\treturn this.getData<RpcContextTrustEvaluation>(response);\n\t}\n\n\t/** Add an existing directory as a canonical global trusted context root. */\n\tasync trustContextFolder(path: string): Promise<RpcContextTrustMutationResult> {\n\t\tconst response = await this.send({ type: \"trust_context_folder\", path });\n\t\treturn this.getData<RpcContextTrustMutationResult>(response);\n\t}\n\n\t/** Remove the root that currently grants the directory context trust. */\n\tasync untrustContextFolder(path: string): Promise<RpcContextTrustMutationResult> {\n\t\tconst response = await this.send({ type: \"untrust_context_folder\", path });\n\t\treturn this.getData<RpcContextTrustMutationResult>(response);\n\t}\n\n\t/** Remove a configured trusted-folder string exactly as stored. */\n\tasync removeTrustedContextFolder(path: string): Promise<RpcTrustedFolderRemovalResult> {\n\t\tconst response = await this.send({ type: \"remove_trusted_context_folder\", path });\n\t\treturn this.getData<RpcTrustedFolderRemovalResult>(response);\n\t}\n\n\t// =========================================================================\n\t// Helpers\n\t// =========================================================================\n\n\t/**\n\t * Wait for agent to become idle (no streaming).\n\t * Resolves when agent_end event is received.\n\t */\n\twaitForIdle(timeout = 60000): Promise<void> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tunsubscribe();\n\t\t\t\treject(new Error(`Timeout waiting for agent to become idle. Stderr: ${this.stderr}`));\n\t\t\t}, timeout);\n\n\t\t\tconst unsubscribe = this.onEvent((event) => {\n\t\t\t\tif (event.type === \"agent_end\") {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tunsubscribe();\n\t\t\t\t\tresolve();\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Collect events until agent becomes idle.\n\t */\n\tcollectEvents(timeout = 60000): Promise<RpcEvent[]> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst events: RpcEvent[] = [];\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tunsubscribe();\n\t\t\t\treject(new Error(`Timeout collecting events. Stderr: ${this.stderr}`));\n\t\t\t}, timeout);\n\n\t\t\tconst unsubscribe = this.onEvent((event) => {\n\t\t\t\tevents.push(event);\n\t\t\t\tif (event.type === \"agent_end\") {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tunsubscribe();\n\t\t\t\t\tresolve(events);\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Send prompt and wait for completion, returning all events.\n\t */\n\tasync promptAndWait(message: string, images?: ImageContent[], timeout = 60000): Promise<RpcEvent[]> {\n\t\tconst eventsPromise = this.collectEvents(timeout);\n\t\tawait this.prompt(message, images);\n\t\treturn eventsPromise;\n\t}\n\n\t// =========================================================================\n\t// Internal\n\t// =========================================================================\n\n\t/**\n\t * Mark the client dead and reject every in-flight request with `message`.\n\t * Shared by the 'exit' and 'error' handlers so both failure paths surface\n\t * loudly and consistently instead of leaving callers hanging.\n\t */\n\tprivate failPendingRequests(message: string): void {\n\t\tthis._dead = true;\n\t\tfor (const pending of this.pendingRequests.values()) {\n\t\t\tpending.reject(new Error(message));\n\t\t}\n\t\tthis.pendingRequests.clear();\n\t}\n\n\tprivate notifyExitListeners(info: RpcExitInfo): void {\n\t\tfor (const listener of this.exitListeners) {\n\t\t\tlistener(info);\n\t\t}\n\t}\n\n\tprivate handleLine(line: string): void {\n\t\ttry {\n\t\t\tconst data: unknown = JSON.parse(line);\n\n\t\t\t// Check if it's a response to a pending request. Unknown/unsolicited\n\t\t\t// response frames are not events and should not be forwarded to listeners.\n\t\t\tif (isRpcResponseMessage(data)) {\n\t\t\t\tif (typeof data.id === \"string\" && this.pendingRequests.has(data.id)) {\n\t\t\t\t\tconst pending = this.pendingRequests.get(data.id)!;\n\t\t\t\t\tthis.pendingRequests.delete(data.id);\n\t\t\t\t\tpending.resolve(data);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!isRpcEvent(data)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tfor (const listener of this.eventListeners) {\n\t\t\t\tlistener(data);\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore non-JSON lines\n\t\t}\n\t}\n\n\tprivate async send(command: RpcCommandBody, timeoutMs = 30000): Promise<RpcResponse> {\n\t\tif (this._dead || !this.process?.stdin) {\n\t\t\t// Surface the real spawn cause (e.g. uid/gid EPERM) if one was captured,\n\t\t\t// rather than a generic message that hides why the process is gone.\n\t\t\tthrow new Error(\n\t\t\t\tthis.spawnError ? `RPC process not running: ${this.spawnError.message}` : \"RPC process not running\",\n\t\t\t);\n\t\t}\n\n\t\tconst id = `req_${++this.requestId}`;\n\t\tconst fullCommand = { ...command, id } as RpcCommand;\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst timeout = setTimeout(() => {\n\t\t\t\tthis.pendingRequests.delete(id);\n\t\t\t\treject(new Error(`Timeout waiting for response to ${command.type}. Stderr: ${this.stderr}`));\n\t\t\t}, timeoutMs);\n\n\t\t\tthis.pendingRequests.set(id, {\n\t\t\t\tresolve: (response) => {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t\tresolve(response);\n\t\t\t\t},\n\t\t\t\treject: (error) => {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t\treject(error);\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tthis.process!.stdin!.write(serializeJsonLine(fullCommand));\n\t\t});\n\t}\n\n\tprivate getData<T>(response: RpcResponse): T {\n\t\tif (!response.success) {\n\t\t\tconst errorResponse = response as Extract<RpcResponse, { success: false }>;\n\t\t\tthrow new Error(errorResponse.error);\n\t\t}\n\t\t// Type assertion: we trust response.data matches T based on the command sent.\n\t\t// This is safe because each public method specifies the correct T for its command.\n\t\tconst successResponse = response as Extract<RpcResponse, { success: true; data: unknown }>;\n\t\treturn successResponse.data as T;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"rpc-client.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/rpc-client.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * RPC Client for programmatic access to the coding agent.\n *\n * Spawns the agent in RPC mode and provides a typed API for all operations.\n */\n\nimport { type ChildProcess, spawn } from \"node:child_process\";\nimport type { AgentMessage, ThinkingLevel } from \"@dreb/agent-core\";\nimport type { ImageContent } from \"@dreb/ai\";\nimport type { SessionStats } from \"../../core/agent-session.js\";\nimport type { BashResult } from \"../../core/bash-executor.js\";\nimport type { CompactionResult } from \"../../core/compaction/index.js\";\nimport { attachJsonlLineReader, serializeJsonLine } from \"./jsonl.js\";\nimport type {\n\tRpcAgentTypeInfo,\n\tRpcBackgroundAgentInfo,\n\tRpcCommand,\n\tRpcContextTrustEvaluation,\n\tRpcContextTrustMutationResult,\n\tRpcDashboardSnapshot,\n\tRpcDashboardSnapshotBarrierEvent,\n\tRpcEvent,\n\tRpcExtensionUIRequest,\n\tRpcExtensionUIResponse,\n\tRpcPendingMessages,\n\tRpcPerformanceStats,\n\tRpcResources,\n\tRpcResponse,\n\tRpcSessionInfo,\n\tRpcSessionState,\n\tRpcSettingsSetResult,\n\tRpcSettingsSnapshot,\n\tRpcSettingsUpdate,\n\tRpcSlashCommand,\n\tRpcTreeNode,\n\tRpcTrustedFolderRemovalResult,\n} from \"./rpc-types.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Grace period (ms) start() waits for the child to initialize before treating\n * it as alive. An 'error'/'exit' event during this window rejects start() early.\n */\nconst INIT_GRACE_MS = 100;\n/** Maximum child-stderr characters attached to exit notifications for diagnostics. */\nconst STDERR_TAIL_CHARS = 2000;\n\n/** Distributive Omit that works with union types */\ntype DistributiveOmit<T, K extends keyof T> = T extends unknown ? Omit<T, K> : never;\n\n/** RpcCommand without the id field (for internal send) */\ntype RpcCommandBody = DistributiveOmit<RpcCommand, \"id\">;\n\nexport interface RpcClientOptions {\n\t/** Path to the CLI entry point (default: searches for dist/cli.js) */\n\tcliPath?: string;\n\t/** Working directory for the agent */\n\tcwd?: string;\n\t/** Environment variables */\n\tenv?: Record<string, string>;\n\t/** Provider to use */\n\tprovider?: string;\n\t/** Model ID to use */\n\tmodel?: string;\n\t/** Additional CLI arguments */\n\targs?: string[];\n\t/**\n\t * Run the agent child process under this OS user id.\n\t * Forwarded directly to `child_process.spawn`. The parent process must hold\n\t * `CAP_SETUID` (typically by running as root) for this to succeed; otherwise\n\t * the spawn fails and `start()` rejects. Omitted when unset (default behavior).\n\t */\n\tuid?: number;\n\t/**\n\t * Run the agent child process under this OS group id.\n\t * Forwarded directly to `child_process.spawn`. The parent process must hold\n\t * `CAP_SETGID` (typically by running as root) for this to succeed; otherwise\n\t * the spawn fails and `start()` rejects. Omitted when unset (default behavior).\n\t */\n\tgid?: number;\n}\n\nexport interface ModelInfo {\n\tprovider: string;\n\tid: string;\n\tcontextWindow: number;\n\treasoning: boolean;\n}\n\n/**\n * Listener for non-response messages streamed by the RPC server. The wire carries\n * the full {@link RpcEvent} union: session events, extension UI requests, and\n * RPC-specific ordering markers such as `dashboard_snapshot_barrier`.\n */\nexport type RpcEventListener = (event: RpcEvent) => void;\n\nexport type RpcExitInfo =\n\t| { code: number | null; signal: NodeJS.Signals | null; stderrTail?: string; error?: undefined }\n\t| { code?: undefined; signal?: undefined; error: Error };\n\nexport type RpcExitListener = (info: RpcExitInfo) => void;\n\ntype JsonRecord = Record<string, unknown>;\n\nfunction isJsonRecord(value: unknown): value is JsonRecord {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isRpcResponseMessage(value: unknown): value is RpcResponse {\n\treturn isJsonRecord(value) && value.type === \"response\";\n}\n\nfunction isRpcDashboardSnapshotBarrierEvent(value: unknown): value is RpcDashboardSnapshotBarrierEvent {\n\treturn isJsonRecord(value) && value.type === \"dashboard_snapshot_barrier\" && typeof value.snapshotId === \"string\";\n}\n\nfunction isRpcExtensionUIRequestMessage(value: unknown): value is RpcExtensionUIRequest {\n\treturn (\n\t\tisJsonRecord(value) &&\n\t\tvalue.type === \"extension_ui_request\" &&\n\t\ttypeof value.id === \"string\" &&\n\t\ttypeof value.method === \"string\"\n\t);\n}\n\nfunction isRpcEvent(value: unknown): value is RpcEvent {\n\tif (!isJsonRecord(value) || typeof value.type !== \"string\" || value.type === \"response\") {\n\t\treturn false;\n\t}\n\tif (value.type === \"dashboard_snapshot_barrier\") {\n\t\treturn isRpcDashboardSnapshotBarrierEvent(value);\n\t}\n\tif (value.type === \"extension_ui_request\") {\n\t\treturn isRpcExtensionUIRequestMessage(value);\n\t}\n\treturn true;\n}\n\n// ============================================================================\n// RPC Client\n// ============================================================================\n\nexport class RpcClient {\n\tprivate process: ChildProcess | null = null;\n\tprivate stopReadingStdout: (() => void) | null = null;\n\tprivate eventListeners: RpcEventListener[] = [];\n\tprivate exitListeners = new Set<RpcExitListener>();\n\tprivate pendingRequests: Map<string, { resolve: (response: RpcResponse) => void; reject: (error: Error) => void }> =\n\t\tnew Map();\n\tprivate requestId = 0;\n\tprivate stderr = \"\";\n\tprivate _dead = false;\n\tprivate spawnError: Error | null = null;\n\n\tconstructor(private options: RpcClientOptions = {}) {}\n\n\t/**\n\t * Start the RPC agent process.\n\t */\n\tasync start(): Promise<void> {\n\t\tif (this.process) {\n\t\t\tthrow new Error(\"Client already started\");\n\t\t}\n\n\t\tconst cliPath = this.options.cliPath ?? \"dist/cli.js\";\n\t\tconst args = [\"--mode\", \"rpc\"];\n\n\t\tif (this.options.provider) {\n\t\t\targs.push(\"--provider\", this.options.provider);\n\t\t}\n\t\tif (this.options.model) {\n\t\t\targs.push(\"--model\", this.options.model);\n\t\t}\n\t\tif (this.options.args) {\n\t\t\targs.push(...this.options.args);\n\t\t}\n\n\t\tthis.process = spawn(\"node\", [cliPath, ...args], {\n\t\t\tcwd: this.options.cwd,\n\t\t\tenv: { ...process.env, ...this.options.env },\n\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t\t...(this.options.uid != null ? { uid: this.options.uid } : {}),\n\t\t\t...(this.options.gid != null ? { gid: this.options.gid } : {}),\n\t\t});\n\n\t\tthis._dead = false;\n\t\tthis.spawnError = null;\n\n\t\t// Collect stderr for debugging\n\t\tthis.process.stderr?.on(\"data\", (data) => {\n\t\t\tthis.stderr += data.toString();\n\t\t});\n\n\t\t// Detect process exit and spawn failures for the whole session lifecycle —\n\t\t// reject pending requests so callers don't hang. Capture the process\n\t\t// reference so stale handlers from a previous process don't clobber state\n\t\t// after a stop()/start() cycle.\n\t\tconst procRef = this.process;\n\t\tprocRef.on(\"exit\", (code, signal) => {\n\t\t\t// Guard: skip if this handler belongs to an old, already-stopped process\n\t\t\tif (this.process !== procRef) return;\n\t\t\tthis.failPendingRequests(`RPC process exited with code ${code}, signal ${signal}`);\n\t\t\t// Surface the child's own fatal diagnostic (e.g. the stdout\n\t\t\t// backpressure abort) in the exit notification so dashboard\n\t\t\t// diagnostics can show the real cause instead of a bare exit code.\n\t\t\tthis.notifyExitListeners({ code, signal, stderrTail: this.stderr.slice(-STDERR_TAIL_CHARS) });\n\t\t});\n\n\t\t// Child stdio pipes fail asynchronously as 'error' events (e.g. EPIPE\n\t\t// when writing a large prompt to the stdin of a dying child). An\n\t\t// unhandled pipe 'error' would crash this process — for the dashboard\n\t\t// that means every session dies at once — so record the cause and fail\n\t\t// in-flight requests the same way the 'exit' handler does. The 'exit'\n\t\t// handler still owns the exit notification; this only stops the crash.\n\t\tconst onPipeError = (stream: string) => (error: Error) => {\n\t\t\tif (this.process !== procRef) return;\n\t\t\tthis.stderr += `\\n[child ${stream} error: ${error.message}]`;\n\t\t\tthis.failPendingRequests(\n\t\t\t\t`RPC process ${stream} error: ${error.message}. Stderr: ${this.stderr.slice(-STDERR_TAIL_CHARS)}`,\n\t\t\t);\n\t\t};\n\t\tthis.process.stdin?.on(\"error\", onPipeError(\"stdin\"));\n\t\tthis.process.stdout?.on(\"error\", onPipeError(\"stdout\"));\n\t\tthis.process.stderr?.on(\"error\", onPipeError(\"stderr\"));\n\n\t\t// Spawn failures surface asynchronously as an 'error' event rather than a\n\t\t// thrown exception (e.g. EPERM when dropping to a uid/gid the parent lacks\n\t\t// CAP_SETUID/CAP_SETGID for, or unsupported on Windows). Without a listener\n\t\t// an 'error' event would crash the process; record the cause so start() can\n\t\t// fail loudly and a later send() can report the real reason instead of a\n\t\t// generic \"not running\".\n\t\tprocRef.on(\"error\", (err) => {\n\t\t\t// Guard: skip if this handler belongs to an old, already-stopped process\n\t\t\tif (this.process !== procRef) return;\n\t\t\tthis.spawnError = err;\n\t\t\tthis.failPendingRequests(`RPC process failed to spawn: ${err.message}`);\n\t\t\tthis.notifyExitListeners({ error: err });\n\t\t});\n\n\t\t// Set up strict JSONL reader for stdout.\n\t\tthis.stopReadingStdout = attachJsonlLineReader(this.process.stdout!, (line) => {\n\t\t\tthis.handleLine(line);\n\t\t});\n\n\t\t// Give the process a moment to initialize, but settle as soon as an\n\t\t// 'error' or 'exit' event arrives. Racing the grace period against those\n\t\t// events (instead of a blind fixed sleep) guarantees that a failed\n\t\t// privilege drop (uid/gid EPERM) rejects start() loudly no matter when\n\t\t// libuv delivers the event — a fixed sleep could expire first and let\n\t\t// start() resolve despite the child never running.\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tconst onError = (err: Error) => {\n\t\t\t\tcleanup();\n\t\t\t\treject(new Error(`Agent process failed to spawn: ${err.message}. Stderr: ${this.stderr}`));\n\t\t\t};\n\t\t\tconst onExit = (code: number | null, signal: NodeJS.Signals | null) => {\n\t\t\t\tcleanup();\n\t\t\t\treject(\n\t\t\t\t\tnew Error(\n\t\t\t\t\t\t`Agent process exited immediately with code ${code}, signal ${signal}. Stderr: ${this.stderr}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t};\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve();\n\t\t\t}, INIT_GRACE_MS);\n\t\t\tconst cleanup = () => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tprocRef.off(\"error\", onError);\n\t\t\t\tprocRef.off(\"exit\", onExit);\n\t\t\t};\n\t\t\tprocRef.once(\"error\", onError);\n\t\t\tprocRef.once(\"exit\", onExit);\n\t\t});\n\n\t\t// Belt-and-suspenders: a process that exited exactly as the grace timer\n\t\t// fired (so the race resolved instead of rejecting) still surfaces loudly.\n\t\tif (this.process.exitCode !== null) {\n\t\t\tthrow new Error(`Agent process exited immediately with code ${this.process.exitCode}. Stderr: ${this.stderr}`);\n\t\t}\n\t}\n\n\t/**\n\t * Stop the RPC agent process.\n\t */\n\tasync stop(): Promise<void> {\n\t\tif (!this.process) return;\n\n\t\tthis.stopReadingStdout?.();\n\t\tthis.stopReadingStdout = null;\n\t\tthis.process.kill(\"SIGTERM\");\n\n\t\t// Wait for process to exit\n\t\tawait new Promise<void>((resolve) => {\n\t\t\tconst timeout = setTimeout(() => {\n\t\t\t\tthis.process?.kill(\"SIGKILL\");\n\t\t\t\tresolve();\n\t\t\t}, 1000);\n\n\t\t\tthis.process?.on(\"exit\", () => {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\tresolve();\n\t\t\t});\n\t\t});\n\n\t\tthis.process = null;\n\t\tthis._dead = true;\n\t\tthis.pendingRequests.clear();\n\t}\n\n\t/**\n\t * Subscribe to agent events.\n\t */\n\tonEvent(listener: RpcEventListener): () => void {\n\t\tthis.eventListeners.push(listener);\n\t\treturn () => {\n\t\t\tconst index = this.eventListeners.indexOf(listener);\n\t\t\tif (index !== -1) {\n\t\t\t\tthis.eventListeners.splice(index, 1);\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * Subscribe to child process death/spawn-failure notifications.\n\t */\n\tonExit(listener: RpcExitListener): () => void {\n\t\tthis.exitListeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.exitListeners.delete(listener);\n\t\t};\n\t}\n\n\t/**\n\t * Get collected stderr output (useful for debugging).\n\t */\n\tgetStderr(): string {\n\t\treturn this.stderr;\n\t}\n\n\t// =========================================================================\n\t// Command Methods\n\t// =========================================================================\n\n\t/**\n\t * Send a prompt to the agent.\n\t * Returns immediately after sending; use onEvent() to receive streaming events.\n\t * Use waitForIdle() to wait for completion.\n\t */\n\tasync prompt(message: string, images?: ImageContent[]): Promise<void> {\n\t\tawait this.send({ type: \"prompt\", message, images });\n\t}\n\n\t/**\n\t * Queue a steering message to interrupt the agent mid-run.\n\t */\n\tasync steer(message: string, images?: ImageContent[]): Promise<void> {\n\t\tawait this.send({ type: \"steer\", message, images });\n\t}\n\n\t/**\n\t * Queue a follow-up message to be processed after the agent finishes.\n\t */\n\tasync followUp(message: string, images?: ImageContent[]): Promise<void> {\n\t\tawait this.send({ type: \"follow_up\", message, images });\n\t}\n\n\t/** Queue an unchanged steering message in a specific live background agent. */\n\tasync steerBackgroundAgent(agentId: string, message: string): Promise<void> {\n\t\tconst response = await this.send({ type: \"steer_background_agent\", agentId, message });\n\t\tthis.getData(response);\n\t}\n\n\t/** Read the selected live background agent's pending steering queue. */\n\tasync getBackgroundAgentPending(agentId: string): Promise<{\n\t\tsteeringMode: \"all\" | \"one-at-a-time\";\n\t\tpending: RpcPendingMessages;\n\t}> {\n\t\tconst response = await this.send({ type: \"get_background_agent_pending\", agentId });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Abort current operation.\n\t */\n\tasync abort(): Promise<void> {\n\t\tawait this.send({ type: \"abort\" });\n\t}\n\n\t/**\n\t * Start a new session, optionally with parent tracking.\n\t * @param parentSession - Optional parent session path for lineage tracking\n\t * @returns Object with `cancelled: true` if an extension cancelled the new session\n\t */\n\tasync newSession(parentSession?: string): Promise<{ cancelled: boolean }> {\n\t\tconst response = await this.send({ type: \"new_session\", parentSession });\n\t\treturn this.getData(response);\n\t}\n\n\t/** Reload extensions, skills, prompts, themes, and other session resources. */\n\tasync reload(): Promise<void> {\n\t\tawait this.send({ type: \"reload\" });\n\t}\n\n\t/** Run `/dream` or one of its backup-path forms. */\n\tasync dream(args?: string): Promise<{ message: string }> {\n\t\tconst response = await this.send({ type: \"dream\", args });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Get current session state.\n\t */\n\tasync getState(): Promise<RpcSessionState> {\n\t\tconst response = await this.send({ type: \"get_state\" });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Capture a dashboard recovery snapshot. The RPC stream emits a matching\n\t * `dashboard_snapshot_barrier` immediately before this response; consumers\n\t * need that event's EventHub sequence before applying the snapshot.\n\t */\n\tasync getDashboardSnapshot(): Promise<RpcDashboardSnapshot> {\n\t\tconst response = await this.send({ type: \"get_dashboard_snapshot\" });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Get loaded context/resources metadata (paths/names only, no file contents).\n\t */\n\tasync getResources(): Promise<RpcResources> {\n\t\tconst response = await this.send({ type: \"get_resources\" });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Get current git branch for the agent cwd.\n\t */\n\tasync getGitBranch(): Promise<string | null> {\n\t\tconst response = await this.send({ type: \"get_git_branch\" });\n\t\treturn this.getData<{ branch: string | null }>(response).branch;\n\t}\n\n\t/**\n\t * Get cached same-day cost across all sessions, primed by the server on first call.\n\t */\n\tasync getDailyCost(): Promise<number> {\n\t\tconst response = await this.send({ type: \"get_daily_cost\" });\n\t\treturn this.getData<{ cost: number }>(response).cost;\n\t}\n\n\t/**\n\t * Set model by provider and ID.\n\t */\n\tasync setModel(provider: string, modelId: string): Promise<{ provider: string; id: string }> {\n\t\tconst response = await this.send({ type: \"set_model\", provider, modelId });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Cycle to next model.\n\t */\n\tasync cycleModel(): Promise<{\n\t\tmodel: { provider: string; id: string };\n\t\tthinkingLevel: ThinkingLevel;\n\t\tisScoped: boolean;\n\t} | null> {\n\t\tconst response = await this.send({ type: \"cycle_model\" });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Get list of available models.\n\t */\n\tasync getAvailableModels(): Promise<ModelInfo[]> {\n\t\tconst response = await this.send({ type: \"get_available_models\" });\n\t\treturn this.getData<{ models: ModelInfo[] }>(response).models;\n\t}\n\n\t/**\n\t * Resolve a model pattern using the same logic as CLI/TUI.\n\t * Runs server-side so API keys never leave the process.\n\t * Returns null if no match found.\n\t */\n\tasync resolveModel(pattern: string): Promise<{ model: ModelInfo; warning?: string } | null> {\n\t\tconst response = await this.send({ type: \"resolve_model\", pattern });\n\t\treturn this.getData<{ model: ModelInfo; warning?: string } | null>(response);\n\t}\n\n\t/**\n\t * Hatch a new buddy companion. Runs inside the agent process\n\t * so API keys never leave the process boundary.\n\t */\n\tasync buddyHatch(): Promise<import(\"../../core/buddy/buddy-types.js\").BuddyState> {\n\t\tconst response = await this.send({ type: \"buddy_hatch\" });\n\t\treturn this.getData<{ state: import(\"../../core/buddy/buddy-types.js\").BuddyState }>(response).state;\n\t}\n\n\t/**\n\t * Reroll the buddy companion. Runs inside the agent process\n\t * so API keys never leave the process boundary.\n\t */\n\tasync buddyReroll(): Promise<import(\"../../core/buddy/buddy-types.js\").BuddyState> {\n\t\tconst response = await this.send({ type: \"buddy_reroll\" });\n\t\treturn this.getData<{ state: import(\"../../core/buddy/buddy-types.js\").BuddyState }>(response).state;\n\t}\n\n\t/**\n\t * Set thinking level.\n\t */\n\tasync setThinkingLevel(level: ThinkingLevel): Promise<void> {\n\t\tawait this.send({ type: \"set_thinking_level\", level });\n\t}\n\n\t/**\n\t * Cycle thinking level.\n\t */\n\tasync cycleThinkingLevel(): Promise<{ level: ThinkingLevel } | null> {\n\t\tconst response = await this.send({ type: \"cycle_thinking_level\" });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Set steering mode.\n\t */\n\tasync setSteeringMode(mode: \"all\" | \"one-at-a-time\"): Promise<void> {\n\t\tawait this.send({ type: \"set_steering_mode\", mode });\n\t}\n\n\t/**\n\t * Set follow-up mode.\n\t */\n\tasync setFollowUpMode(mode: \"all\" | \"one-at-a-time\"): Promise<void> {\n\t\tawait this.send({ type: \"set_follow_up_mode\", mode });\n\t}\n\n\t/**\n\t * Get pending steering and follow-up messages without clearing them.\n\t */\n\tasync getPendingMessages(): Promise<RpcPendingMessages> {\n\t\tconst response = await this.send({ type: \"get_pending_messages\" });\n\t\treturn this.getData<RpcPendingMessages>(response);\n\t}\n\n\t/**\n\t * Clear pending steering and follow-up messages, returning the cleared text.\n\t */\n\tasync clearPendingMessages(): Promise<RpcPendingMessages> {\n\t\tconst response = await this.send({ type: \"clear_pending_messages\" });\n\t\treturn this.getData<RpcPendingMessages>(response);\n\t}\n\n\t/**\n\t * Compact session context.\n\t */\n\tasync compact(customInstructions?: string): Promise<CompactionResult> {\n\t\tconst response = await this.send({ type: \"compact\", customInstructions });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Set auto-compaction enabled/disabled.\n\t */\n\tasync setAutoCompaction(enabled: boolean): Promise<void> {\n\t\tawait this.send({ type: \"set_auto_compaction\", enabled });\n\t}\n\n\t/**\n\t * Abort in-progress compaction.\n\t */\n\tasync abortCompaction(): Promise<void> {\n\t\tawait this.send({ type: \"abort_compaction\" });\n\t}\n\n\t/**\n\t * Set auto-retry enabled/disabled.\n\t */\n\tasync setAutoRetry(enabled: boolean): Promise<void> {\n\t\tawait this.send({ type: \"set_auto_retry\", enabled });\n\t}\n\n\t/**\n\t * Abort in-progress retry.\n\t */\n\tasync abortRetry(): Promise<void> {\n\t\tawait this.send({ type: \"abort_retry\" });\n\t}\n\n\t/**\n\t * Execute a bash command.\n\t */\n\tasync bash(command: string): Promise<BashResult> {\n\t\tconst response = await this.send({ type: \"bash\", command });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Abort running bash command.\n\t */\n\tasync abortBash(): Promise<void> {\n\t\tawait this.send({ type: \"abort_bash\" });\n\t}\n\n\t/**\n\t * Get session statistics.\n\t */\n\tasync getSessionStats(): Promise<SessionStats> {\n\t\tconst response = await this.send({ type: \"get_session_stats\" });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Get performance statistics.\n\t */\n\tasync getPerformanceStats(): Promise<RpcPerformanceStats> {\n\t\tconst response = await this.send({ type: \"get_performance_stats\" });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Export session to HTML.\n\t */\n\tasync exportHtml(outputPath?: string): Promise<{ path: string }> {\n\t\tconst response = await this.send({ type: \"export_html\", outputPath });\n\t\treturn this.getData(response);\n\t}\n\n\t/** Import a JSONL file into the current runtime. */\n\tasync importJsonl(inputPath: string): Promise<{ cancelled: boolean }> {\n\t\tconst response = await this.send({ type: \"import_jsonl\", inputPath });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Switch to a different session file.\n\t * @returns Object with `cancelled: true` if an extension cancelled the switch\n\t */\n\tasync switchSession(sessionPath: string): Promise<{ cancelled: boolean }> {\n\t\tconst response = await this.send({ type: \"switch_session\", sessionPath });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Delete a session file, trying trash first and falling back to unlink.\n\t * Throws on failure, including when attempting to delete the active session.\n\t */\n\tasync deleteSession(sessionPath: string): Promise<{ method: \"trash\" | \"unlink\" }> {\n\t\tconst response = await this.send({ type: \"delete_session\", sessionPath });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Fork from a specific message.\n\t * @returns Object with `text` (the message text) and `cancelled` (if extension cancelled)\n\t */\n\tasync fork(entryId: string): Promise<{ text: string; cancelled: boolean }> {\n\t\tconst response = await this.send({ type: \"fork\", entryId });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Get messages available for forking.\n\t */\n\tasync getForkMessages(): Promise<Array<{ entryId: string; text: string; role: \"user\" | \"assistant\" }>> {\n\t\tconst response = await this.send({ type: \"get_fork_messages\" });\n\t\treturn this.getData<{ messages: Array<{ entryId: string; text: string; role: \"user\" | \"assistant\" }> }>(response)\n\t\t\t.messages;\n\t}\n\n\t/**\n\t * Get the session tree and current leaf.\n\t */\n\tasync getTree(): Promise<{ roots: RpcTreeNode[]; leafId: string | null }> {\n\t\tconst response = await this.send({ type: \"get_tree\" });\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Navigate to a session tree entry.\n\t * @param targetId Entry ID to navigate to\n\t * @param options Navigation options; timeoutMs is client-side only and is not sent over RPC\n\t * @returns Object with `cancelled: true` if navigation was cancelled, and `editorText` when re-editing a message\n\t */\n\tasync navigateTree(\n\t\ttargetId: string,\n\t\toptions?: {\n\t\t\tsummarize?: boolean;\n\t\t\tcustomInstructions?: string;\n\t\t\treplaceInstructions?: boolean;\n\t\t\tlabel?: string;\n\t\t\ttimeoutMs?: number;\n\t\t},\n\t): Promise<{ cancelled: boolean; editorText?: string }> {\n\t\tconst { timeoutMs = 300000, ...commandOptions } = options ?? {};\n\t\tconst response = await this.send({ type: \"navigate_tree\", targetId, ...commandOptions }, timeoutMs);\n\t\treturn this.getData(response);\n\t}\n\n\t/**\n\t * Get text of last assistant message.\n\t */\n\tasync getLastAssistantText(): Promise<string | null> {\n\t\tconst response = await this.send({ type: \"get_last_assistant_text\" });\n\t\treturn this.getData<{ text: string | null }>(response).text;\n\t}\n\n\t/**\n\t * Set the session display name.\n\t */\n\tasync setSessionName(name: string): Promise<void> {\n\t\tawait this.send({ type: \"set_session_name\", name });\n\t}\n\n\t/**\n\t * Get all messages in the session.\n\t */\n\tasync getMessages(): Promise<AgentMessage[]> {\n\t\tconst response = await this.send({ type: \"get_messages\" });\n\t\treturn this.getData<{ messages: AgentMessage[] }>(response).messages;\n\t}\n\n\t/**\n\t * Get discoverable commands, including built-ins that require client-side handling.\n\t */\n\tasync getCommands(): Promise<RpcSlashCommand[]> {\n\t\tconst response = await this.send({ type: \"get_commands\" });\n\t\treturn this.getData<{ commands: RpcSlashCommand[] }>(response).commands;\n\t}\n\n\t/**\n\t * List sessions for the current working directory.\n\t * Returns sessions sorted by most recently modified first.\n\t */\n\tasync listSessions(): Promise<RpcSessionInfo[]> {\n\t\tconst response = await this.send({ type: \"list_sessions\" });\n\t\treturn this.getData<{ sessions: RpcSessionInfo[] }>(response).sessions;\n\t}\n\n\t/**\n\t * List sessions across all projects.\n\t * Returns sessions sorted by most recently modified first. May be slow with many sessions.\n\t */\n\tasync listAllSessions(): Promise<RpcSessionInfo[]> {\n\t\tconst response = await this.send({ type: \"list_all_sessions\" });\n\t\treturn this.getData<{ sessions: RpcSessionInfo[] }>(response).sessions;\n\t}\n\n\t/**\n\t * List background subagents tracked by the server's registry (running and\n\t * recently completed), including session dir/file paths where known.\n\t */\n\tasync listBackgroundAgents(): Promise<RpcBackgroundAgentInfo[]> {\n\t\tconst response = await this.send({ type: \"list_background_agents\" });\n\t\treturn this.getData<{ agents: RpcBackgroundAgentInfo[] }>(response).agents;\n\t}\n\n\t/**\n\t * List discoverable subagent types for the server's current working directory.\n\t */\n\tasync listAgentTypes(): Promise<RpcAgentTypeInfo[]> {\n\t\tconst response = await this.send({ type: \"list_agent_types\" });\n\t\treturn this.getData<{ agentTypes: RpcAgentTypeInfo[] }>(response).agentTypes;\n\t}\n\n\t/**\n\t * Answer an extension UI request (select/confirm/input/editor) previously\n\t * received as an `extension_ui_request` event. Fire-and-forget: the server\n\t * does not send a response to this message.\n\t */\n\tsendExtensionUIResponse(response: RpcExtensionUIResponse): void {\n\t\tif (this._dead || !this.process?.stdin) {\n\t\t\tconst cause = this.spawnError ? ` Spawn error: ${this.spawnError.message}.` : \"\";\n\t\t\tthrow new Error(`RPC process is not running.${cause} Stderr: ${this.stderr}`);\n\t\t}\n\t\tthis.process.stdin.write(serializeJsonLine(response));\n\t}\n\n\t/**\n\t * Get the dreb version.\n\t */\n\tasync getVersion(): Promise<string> {\n\t\tconst response = await this.send({ type: \"get_version\" });\n\t\treturn this.getData<{ version: string }>(response).version;\n\t}\n\n\t/**\n\t * Get the persistent default settings (SettingsManager-backed).\n\t * These seed fresh runtimes — for live session state use getState().\n\t */\n\tasync getSettings(): Promise<RpcSettingsSnapshot> {\n\t\tconst response = await this.send({ type: \"get_settings\" });\n\t\treturn this.getData<RpcSettingsSnapshot>(response);\n\t}\n\n\t/**\n\t * Update persistent default settings. Validates the whole payload before applying\n\t * anything (atomic: on any invalid field, nothing changes). Does NOT touch live\n\t * session state — use setModel/setThinkingLevel/etc. for that.\n\t * Returns the full settings snapshot after the write, plus any loud warnings\n\t * (for example project-level agent model overrides shadowing global writes).\n\t */\n\tasync setSettings(settings: RpcSettingsUpdate): Promise<RpcSettingsSetResult> {\n\t\tconst response = await this.send({ type: \"set_settings\", settings });\n\t\treturn this.getData<RpcSettingsSetResult>(response);\n\t}\n\n\t/** Evaluate an existing directory against the server's global context-trust policy. */\n\tasync evaluateContextTrust(path: string): Promise<RpcContextTrustEvaluation> {\n\t\tconst response = await this.send({ type: \"evaluate_context_trust\", path });\n\t\treturn this.getData<RpcContextTrustEvaluation>(response);\n\t}\n\n\t/** Add an existing directory as a canonical global trusted context root. */\n\tasync trustContextFolder(path: string): Promise<RpcContextTrustMutationResult> {\n\t\tconst response = await this.send({ type: \"trust_context_folder\", path });\n\t\treturn this.getData<RpcContextTrustMutationResult>(response);\n\t}\n\n\t/** Remove the root that currently grants the directory context trust. */\n\tasync untrustContextFolder(path: string): Promise<RpcContextTrustMutationResult> {\n\t\tconst response = await this.send({ type: \"untrust_context_folder\", path });\n\t\treturn this.getData<RpcContextTrustMutationResult>(response);\n\t}\n\n\t/** Remove a configured trusted-folder string exactly as stored. */\n\tasync removeTrustedContextFolder(path: string): Promise<RpcTrustedFolderRemovalResult> {\n\t\tconst response = await this.send({ type: \"remove_trusted_context_folder\", path });\n\t\treturn this.getData<RpcTrustedFolderRemovalResult>(response);\n\t}\n\n\t// =========================================================================\n\t// Helpers\n\t// =========================================================================\n\n\t/**\n\t * Wait for agent to become idle (no streaming).\n\t * Resolves when agent_end event is received.\n\t */\n\twaitForIdle(timeout = 60000): Promise<void> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tunsubscribe();\n\t\t\t\treject(new Error(`Timeout waiting for agent to become idle. Stderr: ${this.stderr}`));\n\t\t\t}, timeout);\n\n\t\t\tconst unsubscribe = this.onEvent((event) => {\n\t\t\t\tif (event.type === \"agent_end\") {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tunsubscribe();\n\t\t\t\t\tresolve();\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Collect events until agent becomes idle.\n\t */\n\tcollectEvents(timeout = 60000): Promise<RpcEvent[]> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst events: RpcEvent[] = [];\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tunsubscribe();\n\t\t\t\treject(new Error(`Timeout collecting events. Stderr: ${this.stderr}`));\n\t\t\t}, timeout);\n\n\t\t\tconst unsubscribe = this.onEvent((event) => {\n\t\t\t\tevents.push(event);\n\t\t\t\tif (event.type === \"agent_end\") {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tunsubscribe();\n\t\t\t\t\tresolve(events);\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Send prompt and wait for completion, returning all events.\n\t */\n\tasync promptAndWait(message: string, images?: ImageContent[], timeout = 60000): Promise<RpcEvent[]> {\n\t\tconst eventsPromise = this.collectEvents(timeout);\n\t\tawait this.prompt(message, images);\n\t\treturn eventsPromise;\n\t}\n\n\t// =========================================================================\n\t// Internal\n\t// =========================================================================\n\n\t/**\n\t * Mark the client dead and reject every in-flight request with `message`.\n\t * Shared by the 'exit' and 'error' handlers so both failure paths surface\n\t * loudly and consistently instead of leaving callers hanging.\n\t */\n\tprivate failPendingRequests(message: string): void {\n\t\tthis._dead = true;\n\t\tfor (const pending of this.pendingRequests.values()) {\n\t\t\tpending.reject(new Error(message));\n\t\t}\n\t\tthis.pendingRequests.clear();\n\t}\n\n\tprivate notifyExitListeners(info: RpcExitInfo): void {\n\t\tfor (const listener of this.exitListeners) {\n\t\t\tlistener(info);\n\t\t}\n\t}\n\n\tprivate handleLine(line: string): void {\n\t\ttry {\n\t\t\tconst data: unknown = JSON.parse(line);\n\n\t\t\t// Check if it's a response to a pending request. Unknown/unsolicited\n\t\t\t// response frames are not events and should not be forwarded to listeners.\n\t\t\tif (isRpcResponseMessage(data)) {\n\t\t\t\tif (typeof data.id === \"string\" && this.pendingRequests.has(data.id)) {\n\t\t\t\t\tconst pending = this.pendingRequests.get(data.id)!;\n\t\t\t\t\tthis.pendingRequests.delete(data.id);\n\t\t\t\t\tpending.resolve(data);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!isRpcEvent(data)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tfor (const listener of this.eventListeners) {\n\t\t\t\tlistener(data);\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore non-JSON lines\n\t\t}\n\t}\n\n\tprivate async send(command: RpcCommandBody, timeoutMs = 30000): Promise<RpcResponse> {\n\t\tif (this._dead || !this.process?.stdin) {\n\t\t\t// Surface the real spawn cause (e.g. uid/gid EPERM) if one was captured,\n\t\t\t// rather than a generic message that hides why the process is gone.\n\t\t\tthrow new Error(\n\t\t\t\tthis.spawnError ? `RPC process not running: ${this.spawnError.message}` : \"RPC process not running\",\n\t\t\t);\n\t\t}\n\n\t\tconst id = `req_${++this.requestId}`;\n\t\tconst fullCommand = { ...command, id } as RpcCommand;\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst timeout = setTimeout(() => {\n\t\t\t\tthis.pendingRequests.delete(id);\n\t\t\t\treject(new Error(`Timeout waiting for response to ${command.type}. Stderr: ${this.stderr}`));\n\t\t\t}, timeoutMs);\n\n\t\t\tthis.pendingRequests.set(id, {\n\t\t\t\tresolve: (response) => {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t\tresolve(response);\n\t\t\t\t},\n\t\t\t\treject: (error) => {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t\treject(error);\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tthis.process!.stdin!.write(serializeJsonLine(fullCommand));\n\t\t});\n\t}\n\n\tprivate getData<T>(response: RpcResponse): T {\n\t\tif (!response.success) {\n\t\t\tconst errorResponse = response as Extract<RpcResponse, { success: false }>;\n\t\t\tthrow new Error(errorResponse.error);\n\t\t}\n\t\t// Type assertion: we trust response.data matches T based on the command sent.\n\t\t// This is safe because each public method specifies the correct T for its command.\n\t\tconst successResponse = response as Extract<RpcResponse, { success: true; data: unknown }>;\n\t\treturn successResponse.data as T;\n\t}\n}\n"]}
|
|
@@ -13,6 +13,8 @@ import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js";
|
|
|
13
13
|
* it as alive. An 'error'/'exit' event during this window rejects start() early.
|
|
14
14
|
*/
|
|
15
15
|
const INIT_GRACE_MS = 100;
|
|
16
|
+
/** Maximum child-stderr characters attached to exit notifications for diagnostics. */
|
|
17
|
+
const STDERR_TAIL_CHARS = 2000;
|
|
16
18
|
function isJsonRecord(value) {
|
|
17
19
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
20
|
}
|
|
@@ -98,8 +100,26 @@ export class RpcClient {
|
|
|
98
100
|
if (this.process !== procRef)
|
|
99
101
|
return;
|
|
100
102
|
this.failPendingRequests(`RPC process exited with code ${code}, signal ${signal}`);
|
|
101
|
-
|
|
103
|
+
// Surface the child's own fatal diagnostic (e.g. the stdout
|
|
104
|
+
// backpressure abort) in the exit notification so dashboard
|
|
105
|
+
// diagnostics can show the real cause instead of a bare exit code.
|
|
106
|
+
this.notifyExitListeners({ code, signal, stderrTail: this.stderr.slice(-STDERR_TAIL_CHARS) });
|
|
102
107
|
});
|
|
108
|
+
// Child stdio pipes fail asynchronously as 'error' events (e.g. EPIPE
|
|
109
|
+
// when writing a large prompt to the stdin of a dying child). An
|
|
110
|
+
// unhandled pipe 'error' would crash this process — for the dashboard
|
|
111
|
+
// that means every session dies at once — so record the cause and fail
|
|
112
|
+
// in-flight requests the same way the 'exit' handler does. The 'exit'
|
|
113
|
+
// handler still owns the exit notification; this only stops the crash.
|
|
114
|
+
const onPipeError = (stream) => (error) => {
|
|
115
|
+
if (this.process !== procRef)
|
|
116
|
+
return;
|
|
117
|
+
this.stderr += `\n[child ${stream} error: ${error.message}]`;
|
|
118
|
+
this.failPendingRequests(`RPC process ${stream} error: ${error.message}. Stderr: ${this.stderr.slice(-STDERR_TAIL_CHARS)}`);
|
|
119
|
+
};
|
|
120
|
+
this.process.stdin?.on("error", onPipeError("stdin"));
|
|
121
|
+
this.process.stdout?.on("error", onPipeError("stdout"));
|
|
122
|
+
this.process.stderr?.on("error", onPipeError("stderr"));
|
|
103
123
|
// Spawn failures surface asynchronously as an 'error' event rather than a
|
|
104
124
|
// thrown exception (e.g. EPERM when dropping to a uid/gid the parent lacks
|
|
105
125
|
// CAP_SETUID/CAP_SETGID for, or unsupported on Windows). Without a listener
|