@dreb/coding-agent 2.70.1 → 2.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/cli/args.d.ts +6 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +17 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts +3 -9
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +19 -51
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/output-guard.d.ts +6 -0
- package/dist/core/output-guard.d.ts.map +1 -1
- package/dist/core/output-guard.js +86 -13
- package/dist/core/output-guard.js.map +1 -1
- package/dist/core/thinking.d.ts +2 -0
- package/dist/core/thinking.d.ts.map +1 -1
- package/dist/core/thinking.js +20 -14
- package/dist/core/thinking.js.map +1 -1
- package/dist/core/tools/subagent.d.ts +1 -0
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +13 -0
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +13 -10
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +9 -5
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/index.d.ts +2 -2
- package/dist/modes/rpc/index.d.ts.map +1 -1
- package/dist/modes/rpc/index.js +1 -1
- package/dist/modes/rpc/index.js.map +1 -1
- package/dist/modes/rpc/rpc-event-projection.d.ts +14 -9
- package/dist/modes/rpc/rpc-event-projection.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-event-projection.js +16 -11
- package/dist/modes/rpc/rpc-event-projection.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +10 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +29 -15
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +28 -2
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/agent-models.md +1 -1
- package/docs/models.md +1 -1
- package/docs/providers.md +26 -0
- package/docs/rpc.md +25 -13
- package/package.json +1 -1
|
@@ -5,6 +5,12 @@ export declare function isStdoutTakenOver(): boolean;
|
|
|
5
5
|
export declare const MAX_QUEUED_STDOUT_BYTES: number;
|
|
6
6
|
/** Abort if the queue stays above the cap with no drain progress for this long. */
|
|
7
7
|
export declare const MAX_NO_DRAIN_GRACE_MS = 30000;
|
|
8
|
+
/**
|
|
9
|
+
* True once a fatal stdout failure has started exiting the process. Graceful
|
|
10
|
+
* shutdown paths must not exit 0 (or exit at all) while this is set: the fatal
|
|
11
|
+
* path owns the exit and will terminate with code 1 once its diagnostic flushes.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isFatalExitPending(): boolean;
|
|
8
14
|
export declare function writeRawStdout(text: string): void;
|
|
9
15
|
export declare function flushRawStdout(): Promise<void>;
|
|
10
16
|
/**
|
|
@@ -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;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"]}
|
|
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;AAiF5C;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAgGD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAUjD;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAyBpD;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAY/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\n// ---------------------------------------------------------------------------\n// Stdout stream errors (issues 454, 535)\n//\n// A failed pipe write (EPIPE when the reader is gone, ENOBUFS when the kernel\n// cannot buffer the write, ...) surfaces asynchronously as an 'error' event on\n// process.stdout. Without a listener Node rethrows it as an unhandled 'error'\n// event and the process dies with only a stack trace. Both the direct and the\n// takeover/raw-writer routes write to the same process.stdout socket, so one\n// listener covers both; per-write callbacks route the same failure through the\n// same fatal path. There is no recovery: stdout is this process's only output\n// channel, so a write failure ends the run loudly with a nonzero exit.\n// ---------------------------------------------------------------------------\n\nlet stdoutErrorListener: ((error: Error) => void) | undefined;\nlet fatalExitStarted = false;\n\nfunction ensureStdoutErrorListener(): void {\n\tif (stdoutErrorListener) return;\n\tstdoutErrorListener = (error: Error) => abortForStdoutError(error);\n\tprocess.stdout.on(\"error\", stdoutErrorListener);\n}\n\nfunction onStdoutWriteResult(error?: Error | null): void {\n\tif (error) abortForStdoutError(error);\n}\n\nfunction abortForStdoutError(error: Error): void {\n\tconst code = (error as NodeJS.ErrnoException).code ?? \"unknown\";\n\tconst reason =\n\t\tcode === \"EPIPE\"\n\t\t\t? \"the consumer closed its end of the pipe\"\n\t\t\t: code === \"ENOBUFS\"\n\t\t\t\t? \"the OS ran out of pipe buffer space\"\n\t\t\t\t: \"the stream reported an error\";\n\tfatalExit(\n\t\t`Fatal: stdout write failed (${code}: ${error.message}); ${reason}. ` +\n\t\t\t\"Output from this process can no longer be delivered. Aborting.\\n\",\n\t);\n}\n\n/**\n * Write a diagnostic to stderr and exit 1. Idempotent: a write callback error\n * and the stream 'error' event can both fire for the same failure. The exit is\n * bounded by a timeout in case stderr is itself broken; stderr failures are\n * swallowed here (never routed back into the stdout guard) because the process\n * is already exiting nonzero.\n */\nfunction fatalExit(diagnostic: string): void {\n\tif (fatalExitStarted) return;\n\tfatalExitStarted = true;\n\t// Record the failure immediately so any exit path that races the diagnostic\n\t// (natural loop drain, or a shutdown that consults isFatalExitPending())\n\t// still reports failure.\n\tprocess.exitCode = 1;\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\ttry {\n\t\tprocess.stderr.write(diagnostic, () => exit());\n\t} catch {\n\t\texit();\n\t}\n}\n\n/**\n * True once a fatal stdout failure has started exiting the process. Graceful\n * shutdown paths must not exit 0 (or exit at all) while this is set: the fatal\n * path owns the exit and will terminate with code 1 once its diagnostic flushes.\n */\nexport function isFatalExitPending(): boolean {\n\treturn fatalExitStarted;\n}\n\nfunction writeToStdout(text: string): boolean {\n\tensureStdoutErrorListener();\n\tif (stdoutTakeoverState) {\n\t\treturn stdoutTakeoverState.rawStdoutWrite(text, onStdoutWriteResult);\n\t}\n\treturn process.stdout.write(text, onStdoutWriteResult);\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\tfatalExit(diagnostic);\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\tensureStdoutErrorListener();\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\tif (stdoutErrorListener) {\n\t\tprocess.stdout.off(\"error\", stdoutErrorListener);\n\t\tstdoutErrorListener = undefined;\n\t}\n\tif (fatalExitStarted) process.exitCode = undefined;\n\tfatalExitStarted = false;\n}\n"]}
|
|
@@ -54,11 +54,86 @@ let stdoutBackpressured = false;
|
|
|
54
54
|
let stdoutDrainListening = false;
|
|
55
55
|
let stdoutDrainWaiters = [];
|
|
56
56
|
let noDrainAbortTimer;
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Stdout stream errors (issues 454, 535)
|
|
59
|
+
//
|
|
60
|
+
// A failed pipe write (EPIPE when the reader is gone, ENOBUFS when the kernel
|
|
61
|
+
// cannot buffer the write, ...) surfaces asynchronously as an 'error' event on
|
|
62
|
+
// process.stdout. Without a listener Node rethrows it as an unhandled 'error'
|
|
63
|
+
// event and the process dies with only a stack trace. Both the direct and the
|
|
64
|
+
// takeover/raw-writer routes write to the same process.stdout socket, so one
|
|
65
|
+
// listener covers both; per-write callbacks route the same failure through the
|
|
66
|
+
// same fatal path. There is no recovery: stdout is this process's only output
|
|
67
|
+
// channel, so a write failure ends the run loudly with a nonzero exit.
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
let stdoutErrorListener;
|
|
70
|
+
let fatalExitStarted = false;
|
|
71
|
+
function ensureStdoutErrorListener() {
|
|
72
|
+
if (stdoutErrorListener)
|
|
73
|
+
return;
|
|
74
|
+
stdoutErrorListener = (error) => abortForStdoutError(error);
|
|
75
|
+
process.stdout.on("error", stdoutErrorListener);
|
|
76
|
+
}
|
|
77
|
+
function onStdoutWriteResult(error) {
|
|
78
|
+
if (error)
|
|
79
|
+
abortForStdoutError(error);
|
|
80
|
+
}
|
|
81
|
+
function abortForStdoutError(error) {
|
|
82
|
+
const code = error.code ?? "unknown";
|
|
83
|
+
const reason = code === "EPIPE"
|
|
84
|
+
? "the consumer closed its end of the pipe"
|
|
85
|
+
: code === "ENOBUFS"
|
|
86
|
+
? "the OS ran out of pipe buffer space"
|
|
87
|
+
: "the stream reported an error";
|
|
88
|
+
fatalExit(`Fatal: stdout write failed (${code}: ${error.message}); ${reason}. ` +
|
|
89
|
+
"Output from this process can no longer be delivered. Aborting.\n");
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Write a diagnostic to stderr and exit 1. Idempotent: a write callback error
|
|
93
|
+
* and the stream 'error' event can both fire for the same failure. The exit is
|
|
94
|
+
* bounded by a timeout in case stderr is itself broken; stderr failures are
|
|
95
|
+
* swallowed here (never routed back into the stdout guard) because the process
|
|
96
|
+
* is already exiting nonzero.
|
|
97
|
+
*/
|
|
98
|
+
function fatalExit(diagnostic) {
|
|
99
|
+
if (fatalExitStarted)
|
|
100
|
+
return;
|
|
101
|
+
fatalExitStarted = true;
|
|
102
|
+
// Record the failure immediately so any exit path that races the diagnostic
|
|
103
|
+
// (natural loop drain, or a shutdown that consults isFatalExitPending())
|
|
104
|
+
// still reports failure.
|
|
105
|
+
process.exitCode = 1;
|
|
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
|
+
try {
|
|
117
|
+
process.stderr.write(diagnostic, () => exit());
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
exit();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* True once a fatal stdout failure has started exiting the process. Graceful
|
|
125
|
+
* shutdown paths must not exit 0 (or exit at all) while this is set: the fatal
|
|
126
|
+
* path owns the exit and will terminate with code 1 once its diagnostic flushes.
|
|
127
|
+
*/
|
|
128
|
+
export function isFatalExitPending() {
|
|
129
|
+
return fatalExitStarted;
|
|
130
|
+
}
|
|
57
131
|
function writeToStdout(text) {
|
|
132
|
+
ensureStdoutErrorListener();
|
|
58
133
|
if (stdoutTakeoverState) {
|
|
59
|
-
return stdoutTakeoverState.rawStdoutWrite(text);
|
|
134
|
+
return stdoutTakeoverState.rawStdoutWrite(text, onStdoutWriteResult);
|
|
60
135
|
}
|
|
61
|
-
return process.stdout.write(text);
|
|
136
|
+
return process.stdout.write(text, onStdoutWriteResult);
|
|
62
137
|
}
|
|
63
138
|
function requestDrainFlush() {
|
|
64
139
|
if (stdoutDrainListening)
|
|
@@ -103,17 +178,7 @@ function abortForStalledConsumer() {
|
|
|
103
178
|
const diagnostic = `Fatal: stdout write queue exceeded ${MAX_QUEUED_STDOUT_BYTES} bytes with no drain progress ` +
|
|
104
179
|
`for ${MAX_NO_DRAIN_GRACE_MS} ms. The consumer of this process's stdout is not reading; ` +
|
|
105
180
|
"refusing unbounded memory growth. Aborting.\n";
|
|
106
|
-
|
|
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);
|
|
181
|
+
fatalExit(diagnostic);
|
|
117
182
|
}
|
|
118
183
|
function flushStdoutQueue() {
|
|
119
184
|
stdoutBackpressured = false;
|
|
@@ -165,6 +230,7 @@ export function writeRawStdout(text) {
|
|
|
165
230
|
}
|
|
166
231
|
}
|
|
167
232
|
export async function flushRawStdout() {
|
|
233
|
+
ensureStdoutErrorListener();
|
|
168
234
|
// Wait for any queued output to drain so flushes observe true end-of-stream.
|
|
169
235
|
if (stdoutBackpressured || stdoutQueue.length > 0) {
|
|
170
236
|
await new Promise((resolve) => {
|
|
@@ -202,5 +268,12 @@ export function resetOutputGuardForTests() {
|
|
|
202
268
|
stdoutBackpressured = false;
|
|
203
269
|
stdoutDrainWaiters.length = 0;
|
|
204
270
|
disarmNoDrainAbort();
|
|
271
|
+
if (stdoutErrorListener) {
|
|
272
|
+
process.stdout.off("error", stdoutErrorListener);
|
|
273
|
+
stdoutErrorListener = undefined;
|
|
274
|
+
}
|
|
275
|
+
if (fatalExitStarted)
|
|
276
|
+
process.exitCode = undefined;
|
|
277
|
+
fatalExitStarted = false;
|
|
205
278
|
}
|
|
206
279
|
//# 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: 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"]}
|
|
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\n// ---------------------------------------------------------------------------\n// Stdout stream errors (issues 454, 535)\n//\n// A failed pipe write (EPIPE when the reader is gone, ENOBUFS when the kernel\n// cannot buffer the write, ...) surfaces asynchronously as an 'error' event on\n// process.stdout. Without a listener Node rethrows it as an unhandled 'error'\n// event and the process dies with only a stack trace. Both the direct and the\n// takeover/raw-writer routes write to the same process.stdout socket, so one\n// listener covers both; per-write callbacks route the same failure through the\n// same fatal path. There is no recovery: stdout is this process's only output\n// channel, so a write failure ends the run loudly with a nonzero exit.\n// ---------------------------------------------------------------------------\n\nlet stdoutErrorListener: ((error: Error) => void) | undefined;\nlet fatalExitStarted = false;\n\nfunction ensureStdoutErrorListener(): void {\n\tif (stdoutErrorListener) return;\n\tstdoutErrorListener = (error: Error) => abortForStdoutError(error);\n\tprocess.stdout.on(\"error\", stdoutErrorListener);\n}\n\nfunction onStdoutWriteResult(error?: Error | null): void {\n\tif (error) abortForStdoutError(error);\n}\n\nfunction abortForStdoutError(error: Error): void {\n\tconst code = (error as NodeJS.ErrnoException).code ?? \"unknown\";\n\tconst reason =\n\t\tcode === \"EPIPE\"\n\t\t\t? \"the consumer closed its end of the pipe\"\n\t\t\t: code === \"ENOBUFS\"\n\t\t\t\t? \"the OS ran out of pipe buffer space\"\n\t\t\t\t: \"the stream reported an error\";\n\tfatalExit(\n\t\t`Fatal: stdout write failed (${code}: ${error.message}); ${reason}. ` +\n\t\t\t\"Output from this process can no longer be delivered. Aborting.\\n\",\n\t);\n}\n\n/**\n * Write a diagnostic to stderr and exit 1. Idempotent: a write callback error\n * and the stream 'error' event can both fire for the same failure. The exit is\n * bounded by a timeout in case stderr is itself broken; stderr failures are\n * swallowed here (never routed back into the stdout guard) because the process\n * is already exiting nonzero.\n */\nfunction fatalExit(diagnostic: string): void {\n\tif (fatalExitStarted) return;\n\tfatalExitStarted = true;\n\t// Record the failure immediately so any exit path that races the diagnostic\n\t// (natural loop drain, or a shutdown that consults isFatalExitPending())\n\t// still reports failure.\n\tprocess.exitCode = 1;\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\ttry {\n\t\tprocess.stderr.write(diagnostic, () => exit());\n\t} catch {\n\t\texit();\n\t}\n}\n\n/**\n * True once a fatal stdout failure has started exiting the process. Graceful\n * shutdown paths must not exit 0 (or exit at all) while this is set: the fatal\n * path owns the exit and will terminate with code 1 once its diagnostic flushes.\n */\nexport function isFatalExitPending(): boolean {\n\treturn fatalExitStarted;\n}\n\nfunction writeToStdout(text: string): boolean {\n\tensureStdoutErrorListener();\n\tif (stdoutTakeoverState) {\n\t\treturn stdoutTakeoverState.rawStdoutWrite(text, onStdoutWriteResult);\n\t}\n\treturn process.stdout.write(text, onStdoutWriteResult);\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\tfatalExit(diagnostic);\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\tensureStdoutErrorListener();\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\tif (stdoutErrorListener) {\n\t\tprocess.stdout.off(\"error\", stdoutErrorListener);\n\t\tstdoutErrorListener = undefined;\n\t}\n\tif (fatalExitStarted) process.exitCode = undefined;\n\tfatalExitStarted = false;\n}\n"]}
|
package/dist/core/thinking.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export type ThinkingLevelValidation = {
|
|
|
21
21
|
* fails before spawn instead of silently changing levels in the child.
|
|
22
22
|
*/
|
|
23
23
|
export declare function validateThinkingLevelForModel(model: Model<any> | undefined, thinkingLevel: AgentThinkingLevel): ThinkingLevelValidation;
|
|
24
|
+
/** Error text for an explicit `max` on a model without native `max`. */
|
|
25
|
+
export declare function unsupportedMaxError(model: Model<any>): string;
|
|
24
26
|
/**
|
|
25
27
|
* Resolve the thinkingDisplay option for a session/subagent.
|
|
26
28
|
* Default-on policy: adaptive-thinking models (Opus 4.7+ default to "omitted" at the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thinking.d.ts","sourceRoot":"","sources":["../../src/core/thinking.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,EACN,KAAK,aAAa,IAAI,eAAe,
|
|
1
|
+
{"version":3,"file":"thinking.d.ts","sourceRoot":"","sources":["../../src/core/thinking.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,EACN,KAAK,aAAa,IAAI,eAAe,EAErC,KAAK,KAAK,EAKV,MAAM,UAAU,CAAC;AAGlB;;;GAGG;AACH,wBAAgB,6BAA6B,CAC5C,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,EAC7B,aAAa,EAAE,kBAAkB,GAAG,SAAS,EAC7C,oBAAoB,GAAE,kBAA2C,GAC/D,kBAAkB,CAEpB;AAED,4FAA4F;AAC5F,wBAAgB,wBAAwB,CAAC,aAAa,EAAE,kBAAkB,GAAG,eAAe,GAAG,SAAS,CAEvG;AAED,MAAM,MAAM,uBAAuB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAElF;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC5C,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,EAC7B,aAAa,EAAE,kBAAkB,GAC/B,uBAAuB,CAiCzB;AAED,wEAAwE;AACxE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,CAG7D;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACrC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,EAC7B,cAAc,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,GAClD,YAAY,GAAG,SAAS,GAAG,SAAS,CAGtC","sourcesContent":["import type { ThinkingLevel as AgentThinkingLevel } from \"@dreb/agent-core\";\nimport {\n\ttype ThinkingLevel as AiThinkingLevel,\n\tclampThinkingLevel,\n\ttype Model,\n\tsupportsAdaptiveThinking,\n\tsupportsMax,\n\tsupportsThinkingOff,\n\tsupportsXhigh,\n} from \"@dreb/ai\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\n\n/**\n * Resolve the effective thinking level for a model using the same capability\n * clamp as normal coding-agent sessions.\n */\nexport function resolveEffectiveThinkingLevel(\n\tmodel: Model<any> | undefined,\n\tthinkingLevel: AgentThinkingLevel | undefined,\n\tdefaultThinkingLevel: AgentThinkingLevel = DEFAULT_THINKING_LEVEL,\n): AgentThinkingLevel {\n\treturn clampThinkingLevel(model, thinkingLevel ?? defaultThinkingLevel);\n}\n\n/** Convert an effective thinking level into the reasoning option passed to streamSimple. */\nexport function thinkingLevelToReasoning(thinkingLevel: AgentThinkingLevel): AiThinkingLevel | undefined {\n\treturn thinkingLevel === \"off\" ? undefined : (thinkingLevel as AiThinkingLevel);\n}\n\nexport type ThinkingLevelValidation = { ok: true } | { ok: false; error: string };\n\n/**\n * Validate an explicit thinking override against a resolved model.\n *\n * Normal session defaults are still capability-clamped for backward compatibility.\n * Explicit subagent/arbiter choices use this stricter path so an unsupported request\n * fails before spawn instead of silently changing levels in the child.\n */\nexport function validateThinkingLevelForModel(\n\tmodel: Model<any> | undefined,\n\tthinkingLevel: AgentThinkingLevel,\n): ThinkingLevelValidation {\n\tif (!model) {\n\t\tif (thinkingLevel === \"off\") return { ok: true };\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: `Cannot validate thinking level \"${thinkingLevel}\" because no concrete child model was resolved.`,\n\t\t};\n\t}\n\tconst modelRef = `${model.provider}/${model.id}`;\n\tif (thinkingLevel === \"off\") {\n\t\tif (!model.reasoning || supportsThinkingOff(model)) return { ok: true };\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: `Thinking level \"off\" is not supported by model \"${modelRef}\" because its thinking cannot be turned off. Use \"minimal\" or \"low\" for the least thinking.`,\n\t\t};\n\t}\n\tif (!model.reasoning) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: `Thinking level \"${thinkingLevel}\" is not supported by non-reasoning model \"${modelRef}\". Use \"off\" or choose a reasoning model.`,\n\t\t};\n\t}\n\tif (thinkingLevel === \"max\" && !supportsMax(model)) {\n\t\treturn { ok: false, error: unsupportedMaxError(model) };\n\t}\n\tif (thinkingLevel === \"xhigh\" && !supportsXhigh(model)) {\n\t\tconst suggestion = supportsMax(model) ? '\"max\" or \"high\"' : '\"high\"';\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: `Thinking level \"xhigh\" is not supported by model \"${modelRef}\". Use ${suggestion} or choose an xhigh-capable model.`,\n\t\t};\n\t}\n\treturn { ok: true };\n}\n\n/** Error text for an explicit `max` on a model without native `max`. */\nexport function unsupportedMaxError(model: Model<any>): string {\n\tconst suggestion = supportsXhigh(model) ? '\"xhigh\"' : '\"high\"';\n\treturn `Thinking level \"max\" is not supported by model \"${model.provider}/${model.id}\". Use ${suggestion} or choose a max-capable model.`;\n}\n\n/**\n * Resolve the thinkingDisplay option for a session/subagent.\n * Default-on policy: adaptive-thinking models (Opus 4.7+ default to \"omitted\" at the\n * API) get \"summarized\" so thinking is visible, unless the user stored an override for\n * this model id. Non-adaptive models return undefined (the AI layer ignores the field\n * for them anyway). Keyed by model id, so the main session and any subagent using the\n * same model resolve identically from shared settings.\n */\nexport function resolveThinkingDisplay(\n\tmodel: Model<any> | undefined,\n\tstoredOverride: \"summarized\" | \"omitted\" | undefined,\n): \"summarized\" | \"omitted\" | undefined {\n\tif (!model || !supportsAdaptiveThinking(model)) return undefined;\n\treturn storedOverride ?? \"summarized\";\n}\n"]}
|
package/dist/core/thinking.js
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import { supportsAdaptiveThinking, supportsMax, supportsXhigh, } from "@dreb/ai";
|
|
1
|
+
import { clampThinkingLevel, supportsAdaptiveThinking, supportsMax, supportsThinkingOff, supportsXhigh, } from "@dreb/ai";
|
|
2
2
|
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
|
3
3
|
/**
|
|
4
4
|
* Resolve the effective thinking level for a model using the same capability
|
|
5
5
|
* clamp as normal coding-agent sessions.
|
|
6
6
|
*/
|
|
7
7
|
export function resolveEffectiveThinkingLevel(model, thinkingLevel, defaultThinkingLevel = DEFAULT_THINKING_LEVEL) {
|
|
8
|
-
|
|
9
|
-
if (!model?.reasoning)
|
|
10
|
-
return "off";
|
|
11
|
-
if (effectiveThinkingLevel === "max" && !supportsMax(model))
|
|
12
|
-
return supportsXhigh(model) ? "xhigh" : "high";
|
|
13
|
-
return effectiveThinkingLevel === "xhigh" && !supportsXhigh(model) ? "high" : effectiveThinkingLevel;
|
|
8
|
+
return clampThinkingLevel(model, thinkingLevel ?? defaultThinkingLevel);
|
|
14
9
|
}
|
|
15
10
|
/** Convert an effective thinking level into the reasoning option passed to streamSimple. */
|
|
16
11
|
export function thinkingLevelToReasoning(thinkingLevel) {
|
|
@@ -24,35 +19,46 @@ export function thinkingLevelToReasoning(thinkingLevel) {
|
|
|
24
19
|
* fails before spawn instead of silently changing levels in the child.
|
|
25
20
|
*/
|
|
26
21
|
export function validateThinkingLevelForModel(model, thinkingLevel) {
|
|
27
|
-
if (thinkingLevel === "off")
|
|
28
|
-
return { ok: true };
|
|
29
22
|
if (!model) {
|
|
23
|
+
if (thinkingLevel === "off")
|
|
24
|
+
return { ok: true };
|
|
30
25
|
return {
|
|
31
26
|
ok: false,
|
|
32
27
|
error: `Cannot validate thinking level "${thinkingLevel}" because no concrete child model was resolved.`,
|
|
33
28
|
};
|
|
34
29
|
}
|
|
35
30
|
const modelRef = `${model.provider}/${model.id}`;
|
|
36
|
-
if (
|
|
31
|
+
if (thinkingLevel === "off") {
|
|
32
|
+
if (!model.reasoning || supportsThinkingOff(model))
|
|
33
|
+
return { ok: true };
|
|
37
34
|
return {
|
|
38
35
|
ok: false,
|
|
39
|
-
error: `Thinking level "
|
|
36
|
+
error: `Thinking level "off" is not supported by model "${modelRef}" because its thinking cannot be turned off. Use "minimal" or "low" for the least thinking.`,
|
|
40
37
|
};
|
|
41
38
|
}
|
|
42
|
-
if (
|
|
39
|
+
if (!model.reasoning) {
|
|
43
40
|
return {
|
|
44
41
|
ok: false,
|
|
45
|
-
error: `Thinking level "
|
|
42
|
+
error: `Thinking level "${thinkingLevel}" is not supported by non-reasoning model "${modelRef}". Use "off" or choose a reasoning model.`,
|
|
46
43
|
};
|
|
47
44
|
}
|
|
45
|
+
if (thinkingLevel === "max" && !supportsMax(model)) {
|
|
46
|
+
return { ok: false, error: unsupportedMaxError(model) };
|
|
47
|
+
}
|
|
48
48
|
if (thinkingLevel === "xhigh" && !supportsXhigh(model)) {
|
|
49
|
+
const suggestion = supportsMax(model) ? '"max" or "high"' : '"high"';
|
|
49
50
|
return {
|
|
50
51
|
ok: false,
|
|
51
|
-
error: `Thinking level "xhigh" is not supported by model "${modelRef}". Use
|
|
52
|
+
error: `Thinking level "xhigh" is not supported by model "${modelRef}". Use ${suggestion} or choose an xhigh-capable model.`,
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
55
|
return { ok: true };
|
|
55
56
|
}
|
|
57
|
+
/** Error text for an explicit `max` on a model without native `max`. */
|
|
58
|
+
export function unsupportedMaxError(model) {
|
|
59
|
+
const suggestion = supportsXhigh(model) ? '"xhigh"' : '"high"';
|
|
60
|
+
return `Thinking level "max" is not supported by model "${model.provider}/${model.id}". Use ${suggestion} or choose a max-capable model.`;
|
|
61
|
+
}
|
|
56
62
|
/**
|
|
57
63
|
* Resolve the thinkingDisplay option for a session/subagent.
|
|
58
64
|
* Default-on policy: adaptive-thinking models (Opus 4.7+ default to "omitted" at the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thinking.js","sourceRoot":"","sources":["../../src/core/thinking.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"thinking.js","sourceRoot":"","sources":["../../src/core/thinking.ts"],"names":[],"mappings":"AACA,OAAO,EAEN,kBAAkB,EAElB,wBAAwB,EACxB,WAAW,EACX,mBAAmB,EACnB,aAAa,GACb,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAEvD;;;GAGG;AACH,MAAM,UAAU,6BAA6B,CAC5C,KAA6B,EAC7B,aAA6C,EAC7C,oBAAoB,GAAuB,sBAAsB,EAC5C;IACrB,OAAO,kBAAkB,CAAC,KAAK,EAAE,aAAa,IAAI,oBAAoB,CAAC,CAAC;AAAA,CACxE;AAED,4FAA4F;AAC5F,MAAM,UAAU,wBAAwB,CAAC,aAAiC,EAA+B;IACxG,OAAO,aAAa,KAAK,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAE,aAAiC,CAAC;AAAA,CAChF;AAID;;;;;;GAMG;AACH,MAAM,UAAU,6BAA6B,CAC5C,KAA6B,EAC7B,aAAiC,EACP;IAC1B,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,IAAI,aAAa,KAAK,KAAK;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;QACjD,OAAO;YACN,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,mCAAmC,aAAa,iDAAiD;SACxG,CAAC;IACH,CAAC;IACD,MAAM,QAAQ,GAAG,GAAG,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,EAAE,EAAE,CAAC;IACjD,IAAI,aAAa,KAAK,KAAK,EAAE,CAAC;QAC7B,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,mBAAmB,CAAC,KAAK,CAAC;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;QACxE,OAAO;YACN,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,mDAAmD,QAAQ,6FAA6F;SAC/J,CAAC;IACH,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;QACtB,OAAO;YACN,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,mBAAmB,aAAa,8CAA8C,QAAQ,2CAA2C;SACxI,CAAC;IACH,CAAC;IACD,IAAI,aAAa,KAAK,KAAK,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;QACpD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,CAAC,KAAK,CAAC,EAAE,CAAC;IACzD,CAAC;IACD,IAAI,aAAa,KAAK,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;QACxD,MAAM,UAAU,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,QAAQ,CAAC;QACrE,OAAO;YACN,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,qDAAqD,QAAQ,UAAU,UAAU,oCAAoC;SAC5H,CAAC;IACH,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AAAA,CACpB;AAED,wEAAwE;AACxE,MAAM,UAAU,mBAAmB,CAAC,KAAiB,EAAU;IAC9D,MAAM,UAAU,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC/D,OAAO,mDAAmD,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,EAAE,UAAU,UAAU,iCAAiC,CAAC;AAAA,CAC1I;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CACrC,KAA6B,EAC7B,cAAoD,EACb;IACvC,IAAI,CAAC,KAAK,IAAI,CAAC,wBAAwB,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACjE,OAAO,cAAc,IAAI,YAAY,CAAC;AAAA,CACtC","sourcesContent":["import type { ThinkingLevel as AgentThinkingLevel } from \"@dreb/agent-core\";\nimport {\n\ttype ThinkingLevel as AiThinkingLevel,\n\tclampThinkingLevel,\n\ttype Model,\n\tsupportsAdaptiveThinking,\n\tsupportsMax,\n\tsupportsThinkingOff,\n\tsupportsXhigh,\n} from \"@dreb/ai\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\n\n/**\n * Resolve the effective thinking level for a model using the same capability\n * clamp as normal coding-agent sessions.\n */\nexport function resolveEffectiveThinkingLevel(\n\tmodel: Model<any> | undefined,\n\tthinkingLevel: AgentThinkingLevel | undefined,\n\tdefaultThinkingLevel: AgentThinkingLevel = DEFAULT_THINKING_LEVEL,\n): AgentThinkingLevel {\n\treturn clampThinkingLevel(model, thinkingLevel ?? defaultThinkingLevel);\n}\n\n/** Convert an effective thinking level into the reasoning option passed to streamSimple. */\nexport function thinkingLevelToReasoning(thinkingLevel: AgentThinkingLevel): AiThinkingLevel | undefined {\n\treturn thinkingLevel === \"off\" ? undefined : (thinkingLevel as AiThinkingLevel);\n}\n\nexport type ThinkingLevelValidation = { ok: true } | { ok: false; error: string };\n\n/**\n * Validate an explicit thinking override against a resolved model.\n *\n * Normal session defaults are still capability-clamped for backward compatibility.\n * Explicit subagent/arbiter choices use this stricter path so an unsupported request\n * fails before spawn instead of silently changing levels in the child.\n */\nexport function validateThinkingLevelForModel(\n\tmodel: Model<any> | undefined,\n\tthinkingLevel: AgentThinkingLevel,\n): ThinkingLevelValidation {\n\tif (!model) {\n\t\tif (thinkingLevel === \"off\") return { ok: true };\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: `Cannot validate thinking level \"${thinkingLevel}\" because no concrete child model was resolved.`,\n\t\t};\n\t}\n\tconst modelRef = `${model.provider}/${model.id}`;\n\tif (thinkingLevel === \"off\") {\n\t\tif (!model.reasoning || supportsThinkingOff(model)) return { ok: true };\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: `Thinking level \"off\" is not supported by model \"${modelRef}\" because its thinking cannot be turned off. Use \"minimal\" or \"low\" for the least thinking.`,\n\t\t};\n\t}\n\tif (!model.reasoning) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: `Thinking level \"${thinkingLevel}\" is not supported by non-reasoning model \"${modelRef}\". Use \"off\" or choose a reasoning model.`,\n\t\t};\n\t}\n\tif (thinkingLevel === \"max\" && !supportsMax(model)) {\n\t\treturn { ok: false, error: unsupportedMaxError(model) };\n\t}\n\tif (thinkingLevel === \"xhigh\" && !supportsXhigh(model)) {\n\t\tconst suggestion = supportsMax(model) ? '\"max\" or \"high\"' : '\"high\"';\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: `Thinking level \"xhigh\" is not supported by model \"${modelRef}\". Use ${suggestion} or choose an xhigh-capable model.`,\n\t\t};\n\t}\n\treturn { ok: true };\n}\n\n/** Error text for an explicit `max` on a model without native `max`. */\nexport function unsupportedMaxError(model: Model<any>): string {\n\tconst suggestion = supportsXhigh(model) ? '\"xhigh\"' : '\"high\"';\n\treturn `Thinking level \"max\" is not supported by model \"${model.provider}/${model.id}\". Use ${suggestion} or choose a max-capable model.`;\n}\n\n/**\n * Resolve the thinkingDisplay option for a session/subagent.\n * Default-on policy: adaptive-thinking models (Opus 4.7+ default to \"omitted\" at the\n * API) get \"summarized\" so thinking is visible, unless the user stored an override for\n * this model id. Non-adaptive models return undefined (the AI layer ignores the field\n * for them anyway). Keyed by model id, so the main session and any subagent using the\n * same model resolve identically from shared settings.\n */\nexport function resolveThinkingDisplay(\n\tmodel: Model<any> | undefined,\n\tstoredOverride: \"summarized\" | \"omitted\" | undefined,\n): \"summarized\" | \"omitted\" | undefined {\n\tif (!model || !supportsAdaptiveThinking(model)) return undefined;\n\treturn storedOverride ?? \"summarized\";\n}\n"]}
|
|
@@ -53,6 +53,7 @@ export interface SubagentResult {
|
|
|
53
53
|
/** Path to the persisted session JSONL file, if available */
|
|
54
54
|
sessionFile?: string;
|
|
55
55
|
}
|
|
56
|
+
export declare function setSubagentRpcFullMessageUpdates(enabled: boolean): void;
|
|
56
57
|
/**
|
|
57
58
|
* Filter a comma-separated tools string, removing any tools in SUBAGENT_EXCLUDED_TOOLS.
|
|
58
59
|
* Returns the filtered tools as a comma-separated string (always non-empty — falls
|