@dreb/coding-agent 2.55.4 → 2.55.6

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.
@@ -1,6 +1,8 @@
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 aborting. */
5
+ export declare const MAX_QUEUED_STDOUT_BYTES: number;
4
6
  export declare function writeRawStdout(text: string): void;
5
7
  export declare function flushRawStdout(): Promise<void>;
6
8
  //# 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;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAMjD;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAiBpD","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\nexport function writeRawStdout(text: string): void {\n\tif (stdoutTakeoverState) {\n\t\tstdoutTakeoverState.rawStdoutWrite(text);\n\t\treturn;\n\t}\n\tprocess.stdout.write(text);\n}\n\nexport async function flushRawStdout(): Promise<void> {\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.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;AAeD,kFAAkF;AAClF,eAAO,MAAM,uBAAuB,QAAmB,CAAC;AA0ExD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAUjD;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAwBpD","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"]}
@@ -28,14 +28,106 @@ export function restoreStdout() {
28
28
  export function isStdoutTakenOver() {
29
29
  return stdoutTakeoverState !== undefined;
30
30
  }
31
- export function writeRawStdout(text) {
31
+ // ---------------------------------------------------------------------------
32
+ // Backpressure-aware bounded write queue
33
+ //
34
+ // stream.write() returns false when the stream's internal buffer is full
35
+ // (backpressure). Ignoring that signal during high-rate event streaming lets
36
+ // output queue up unboundedly inside the process, which is what produced the
37
+ // multi-thousand-event end-of-response bursts in issue 448. While the stream
38
+ // is backpressured we queue subsequent writes and flush them in order on
39
+ // "drain". The queue is byte-capped: a consumer that stalls beyond the cap
40
+ // means this process's primary output channel is dead or hopelessly behind,
41
+ // so we fail loudly instead of growing memory without bound.
42
+ // ---------------------------------------------------------------------------
43
+ /** Maximum aggregate bytes allowed for ordinary queued writes before aborting. */
44
+ export const MAX_QUEUED_STDOUT_BYTES = 16 * 1024 * 1024; // 16 MiB
45
+ const FATAL_DIAGNOSTIC_FLUSH_TIMEOUT_MS = 1_000;
46
+ const stdoutQueue = [];
47
+ let stdoutQueuedBytes = 0;
48
+ let stdoutBackpressured = false;
49
+ let stdoutDrainListening = false;
50
+ let stdoutDrainWaiters = [];
51
+ function writeToStdout(text) {
32
52
  if (stdoutTakeoverState) {
33
- stdoutTakeoverState.rawStdoutWrite(text);
53
+ return stdoutTakeoverState.rawStdoutWrite(text);
54
+ }
55
+ return process.stdout.write(text);
56
+ }
57
+ function requestDrainFlush() {
58
+ if (stdoutDrainListening)
34
59
  return;
60
+ stdoutDrainListening = true;
61
+ process.stdout.once("drain", () => {
62
+ stdoutDrainListening = false;
63
+ flushStdoutQueue();
64
+ });
65
+ }
66
+ function flushStdoutQueue() {
67
+ stdoutBackpressured = false;
68
+ while (stdoutQueue.length > 0) {
69
+ const next = stdoutQueue.shift();
70
+ stdoutQueuedBytes -= Buffer.byteLength(next);
71
+ // A false return means the stream accepted the chunk but its buffer is
72
+ // full again — stop writing and wait for the next drain.
73
+ if (!writeToStdout(next)) {
74
+ stdoutBackpressured = true;
75
+ requestDrainFlush();
76
+ return;
77
+ }
78
+ }
79
+ if (stdoutDrainWaiters.length > 0) {
80
+ const waiters = stdoutDrainWaiters;
81
+ stdoutDrainWaiters = [];
82
+ for (const resolve of waiters)
83
+ resolve();
84
+ }
85
+ }
86
+ function enqueueStdout(text) {
87
+ const bytes = Buffer.byteLength(text);
88
+ // Bound accumulated ordinary backlog, but allow one legitimate protocol frame
89
+ // larger than the cap (for example, a complete dashboard snapshot). Once that
90
+ // oversized frame is queued, any subsequent write still fails the cap check.
91
+ const exceedsAggregateCap = stdoutQueuedBytes + bytes > MAX_QUEUED_STDOUT_BYTES;
92
+ const isSingleOversizedFrame = bytes > MAX_QUEUED_STDOUT_BYTES && stdoutQueuedBytes <= MAX_QUEUED_STDOUT_BYTES;
93
+ if (exceedsAggregateCap && !isSingleOversizedFrame) {
94
+ const diagnostic = `Fatal: stdout write queue exceeded ${MAX_QUEUED_STDOUT_BYTES} bytes while the stream was ` +
95
+ "backpressured. The consumer of this process's stdout is not reading; refusing " +
96
+ "unbounded memory growth. Aborting.\n";
97
+ let exiting = false;
98
+ const exit = () => {
99
+ if (exiting)
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;
109
+ }
110
+ stdoutQueue.push(text);
111
+ stdoutQueuedBytes += bytes;
112
+ }
113
+ export function writeRawStdout(text) {
114
+ // Queue behind any backpressured/queued writes to preserve ordering.
115
+ if (stdoutBackpressured || stdoutQueue.length > 0) {
116
+ enqueueStdout(text);
117
+ return;
118
+ }
119
+ if (!writeToStdout(text)) {
120
+ stdoutBackpressured = true;
121
+ requestDrainFlush();
35
122
  }
36
- process.stdout.write(text);
37
123
  }
38
124
  export async function flushRawStdout() {
125
+ // Wait for any queued output to drain so flushes observe true end-of-stream.
126
+ if (stdoutBackpressured || stdoutQueue.length > 0) {
127
+ await new Promise((resolve) => {
128
+ stdoutDrainWaiters.push(resolve);
129
+ });
130
+ }
39
131
  if (stdoutTakeoverState) {
40
132
  await new Promise((resolve, reject) => {
41
133
  stdoutTakeoverState?.rawStdoutWrite("", (err) => {
@@ -1 +1 @@
1
- {"version":3,"file":"output-guard.js","sourceRoot":"","sources":["../../src/core/output-guard.ts"],"names":[],"mappings":"AAMA,IAAI,mBAAoD,CAAC;AAEzD,MAAM,UAAU,cAAc,GAAS;IACtC,IAAI,mBAAmB,EAAE,CAAC;QACzB,OAAO;IACR,CAAC;IAED,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAA0C,CAAC;IAC1G,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAA0C,CAAC;IAC1G,MAAM,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;IAEjD,OAAO,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,CACvB,KAA0B,EAC1B,kBAAsE,EACtE,QAAyC,EAC/B,EAAE,CAAC;QACb,IAAI,OAAO,kBAAkB,KAAK,UAAU,EAAE,CAAC;YAC9C,OAAO,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,kBAAkB,CAAC,CAAC;QAC1D,CAAC;QACD,OAAO,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,CAAC;IAAA,CAC/C,CAAgC,CAAC;IAElC,mBAAmB,GAAG;QACrB,cAAc;QACd,cAAc;QACd,mBAAmB;KACnB,CAAC;AAAA,CACF;AAED,MAAM,UAAU,aAAa,GAAS;IACrC,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAC1B,OAAO;IACR,CAAC;IAED,OAAO,CAAC,MAAM,CAAC,KAAK,GAAG,mBAAmB,CAAC,mBAAmB,CAAC;IAC/D,mBAAmB,GAAG,SAAS,CAAC;AAAA,CAChC;AAED,MAAM,UAAU,iBAAiB,GAAY;IAC5C,OAAO,mBAAmB,KAAK,SAAS,CAAC;AAAA,CACzC;AAED,MAAM,UAAU,cAAc,CAAC,IAAY,EAAQ;IAClD,IAAI,mBAAmB,EAAE,CAAC;QACzB,mBAAmB,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;QACzC,OAAO;IACR,CAAC;IACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAAA,CAC3B;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,GAAkB;IACrD,IAAI,mBAAmB,EAAE,CAAC;QACzB,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;YAC5C,mBAAmB,EAAE,cAAc,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC;gBAChD,IAAI,GAAG;oBAAE,MAAM,CAAC,GAAG,CAAC,CAAC;;oBAChB,OAAO,EAAE,CAAC;YAAA,CACf,CAAC,CAAC;QAAA,CACH,CAAC,CAAC;QACH,OAAO;IACR,CAAC;IAED,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;QAC5C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC;YACjC,IAAI,GAAG;gBAAE,MAAM,CAAC,GAAG,CAAC,CAAC;;gBAChB,OAAO,EAAE,CAAC;QAAA,CACf,CAAC,CAAC;IAAA,CACH,CAAC,CAAC;AAAA,CACH","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\nexport function writeRawStdout(text: string): void {\n\tif (stdoutTakeoverState) {\n\t\tstdoutTakeoverState.rawStdoutWrite(text);\n\t\treturn;\n\t}\n\tprocess.stdout.write(text);\n}\n\nexport async function flushRawStdout(): Promise<void> {\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: 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"]}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Dashboard-mode RPC event projection.
3
+ *
4
+ * `message_update` events carry two cumulative copies of the growing assistant
5
+ * message: the top-level `message` field and `assistantMessageEvent.partial`.
6
+ * Serializing both for every token makes the child->parent JSONL pipe quadratic
7
+ * in response length and floods the stdout queue (see issue 448).
8
+ *
9
+ * The dashboard never reads those fields: its browser reducer consumes only
10
+ * the delta fields (`delta`, `content`, `toolCall`, `contentIndex`), and its
11
+ * authoritative transcript comes from `message_end` plus `get_dashboard_snapshot`
12
+ * RPC responses. The dashboard's own EventHub already strips the same fields at
13
+ * the browser SSE boundary (projectDashboardEvent); this module applies the same
14
+ * removal one boundary earlier, before JSONL serialization in runRpcMode.
15
+ *
16
+ * Only the quadratic `message_update` fields are removed here. Broader bounding
17
+ * (agent_end messages, tool_execution_update args, retry discardedPartial,
18
+ * images) remains the EventHub's browser-facing concern.
19
+ */
20
+ /**
21
+ * Project a single agent session event for dashboard-mode RPC transport.
22
+ *
23
+ * Unknown event types are returned exactly as received (same reference) so
24
+ * extensions and future event types remain forward-safe. Projected events are
25
+ * shallow copies — the input event is never mutated, because other session
26
+ * subscribers (and the session's own state) share the same object.
27
+ */
28
+ export declare function projectDashboardRpcEvent(event: Record<string, unknown>): Record<string, unknown>;
29
+ //# sourceMappingURL=rpc-event-projection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc-event-projection.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/rpc-event-projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAYH;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgBhG","sourcesContent":["/**\n * Dashboard-mode RPC event projection.\n *\n * `message_update` events carry two cumulative copies of the growing assistant\n * message: the top-level `message` field and `assistantMessageEvent.partial`.\n * Serializing both for every token makes the child->parent JSONL pipe quadratic\n * in response length and floods the stdout queue (see issue 448).\n *\n * The dashboard never reads those fields: its browser reducer consumes only\n * the delta fields (`delta`, `content`, `toolCall`, `contentIndex`), and its\n * authoritative transcript comes from `message_end` plus `get_dashboard_snapshot`\n * RPC responses. The dashboard's own EventHub already strips the same fields at\n * the browser SSE boundary (projectDashboardEvent); this module applies the same\n * removal one boundary earlier, before JSONL serialization in runRpcMode.\n *\n * Only the quadratic `message_update` fields are removed here. Broader bounding\n * (agent_end messages, tool_execution_update args, retry discardedPartial,\n * images) remains the EventHub's browser-facing concern.\n */\n\nfunction omit(event: Record<string, unknown>, ...keys: string[]): Record<string, unknown> {\n\tconst copy = { ...event };\n\tfor (const key of keys) delete copy[key];\n\treturn copy;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Project a single agent session event for dashboard-mode RPC transport.\n *\n * Unknown event types are returned exactly as received (same reference) so\n * extensions and future event types remain forward-safe. Projected events are\n * shallow copies — the input event is never mutated, because other session\n * subscribers (and the session's own state) share the same object.\n */\nexport function projectDashboardRpcEvent(event: Record<string, unknown>): Record<string, unknown> {\n\tswitch (event.type) {\n\t\tcase \"message_update\": {\n\t\t\tconst projected = omit(event, \"message\");\n\t\t\tconst streamEvent = event.assistantMessageEvent;\n\t\t\treturn isPlainObject(streamEvent)\n\t\t\t\t? { ...projected, assistantMessageEvent: omit(streamEvent, \"partial\") }\n\t\t\t\t: projected;\n\t\t}\n\t\tcase \"background_agent_event\": {\n\t\t\tconst child = event.event;\n\t\t\treturn isPlainObject(child) ? { ...event, event: projectDashboardRpcEvent(child) } : event;\n\t\t}\n\t\tdefault:\n\t\t\treturn event;\n\t}\n}\n"]}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Dashboard-mode RPC event projection.
3
+ *
4
+ * `message_update` events carry two cumulative copies of the growing assistant
5
+ * message: the top-level `message` field and `assistantMessageEvent.partial`.
6
+ * Serializing both for every token makes the child->parent JSONL pipe quadratic
7
+ * in response length and floods the stdout queue (see issue 448).
8
+ *
9
+ * The dashboard never reads those fields: its browser reducer consumes only
10
+ * the delta fields (`delta`, `content`, `toolCall`, `contentIndex`), and its
11
+ * authoritative transcript comes from `message_end` plus `get_dashboard_snapshot`
12
+ * RPC responses. The dashboard's own EventHub already strips the same fields at
13
+ * the browser SSE boundary (projectDashboardEvent); this module applies the same
14
+ * removal one boundary earlier, before JSONL serialization in runRpcMode.
15
+ *
16
+ * Only the quadratic `message_update` fields are removed here. Broader bounding
17
+ * (agent_end messages, tool_execution_update args, retry discardedPartial,
18
+ * images) remains the EventHub's browser-facing concern.
19
+ */
20
+ function omit(event, ...keys) {
21
+ const copy = { ...event };
22
+ for (const key of keys)
23
+ delete copy[key];
24
+ return copy;
25
+ }
26
+ function isPlainObject(value) {
27
+ return typeof value === "object" && value !== null && !Array.isArray(value);
28
+ }
29
+ /**
30
+ * Project a single agent session event for dashboard-mode RPC transport.
31
+ *
32
+ * Unknown event types are returned exactly as received (same reference) so
33
+ * extensions and future event types remain forward-safe. Projected events are
34
+ * shallow copies — the input event is never mutated, because other session
35
+ * subscribers (and the session's own state) share the same object.
36
+ */
37
+ export function projectDashboardRpcEvent(event) {
38
+ switch (event.type) {
39
+ case "message_update": {
40
+ const projected = omit(event, "message");
41
+ const streamEvent = event.assistantMessageEvent;
42
+ return isPlainObject(streamEvent)
43
+ ? { ...projected, assistantMessageEvent: omit(streamEvent, "partial") }
44
+ : projected;
45
+ }
46
+ case "background_agent_event": {
47
+ const child = event.event;
48
+ return isPlainObject(child) ? { ...event, event: projectDashboardRpcEvent(child) } : event;
49
+ }
50
+ default:
51
+ return event;
52
+ }
53
+ }
54
+ //# sourceMappingURL=rpc-event-projection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc-event-projection.js","sourceRoot":"","sources":["../../../src/modes/rpc/rpc-event-projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,SAAS,IAAI,CAAC,KAA8B,EAAE,GAAG,IAAc,EAA2B;IACzF,MAAM,IAAI,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC;IAC1B,KAAK,MAAM,GAAG,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;IACzC,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,SAAS,aAAa,CAAC,KAAc,EAAoC;IACxE,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAAA,CAC5E;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAA8B,EAA2B;IACjG,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,gBAAgB,EAAE,CAAC;YACvB,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;YACzC,MAAM,WAAW,GAAG,KAAK,CAAC,qBAAqB,CAAC;YAChD,OAAO,aAAa,CAAC,WAAW,CAAC;gBAChC,CAAC,CAAC,EAAE,GAAG,SAAS,EAAE,qBAAqB,EAAE,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,EAAE;gBACvE,CAAC,CAAC,SAAS,CAAC;QACd,CAAC;QACD,KAAK,wBAAwB,EAAE,CAAC;YAC/B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;YAC1B,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,wBAAwB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;QAC5F,CAAC;QACD;YACC,OAAO,KAAK,CAAC;IACf,CAAC;AAAA,CACD","sourcesContent":["/**\n * Dashboard-mode RPC event projection.\n *\n * `message_update` events carry two cumulative copies of the growing assistant\n * message: the top-level `message` field and `assistantMessageEvent.partial`.\n * Serializing both for every token makes the child->parent JSONL pipe quadratic\n * in response length and floods the stdout queue (see issue 448).\n *\n * The dashboard never reads those fields: its browser reducer consumes only\n * the delta fields (`delta`, `content`, `toolCall`, `contentIndex`), and its\n * authoritative transcript comes from `message_end` plus `get_dashboard_snapshot`\n * RPC responses. The dashboard's own EventHub already strips the same fields at\n * the browser SSE boundary (projectDashboardEvent); this module applies the same\n * removal one boundary earlier, before JSONL serialization in runRpcMode.\n *\n * Only the quadratic `message_update` fields are removed here. Broader bounding\n * (agent_end messages, tool_execution_update args, retry discardedPartial,\n * images) remains the EventHub's browser-facing concern.\n */\n\nfunction omit(event: Record<string, unknown>, ...keys: string[]): Record<string, unknown> {\n\tconst copy = { ...event };\n\tfor (const key of keys) delete copy[key];\n\treturn copy;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Project a single agent session event for dashboard-mode RPC transport.\n *\n * Unknown event types are returned exactly as received (same reference) so\n * extensions and future event types remain forward-safe. Projected events are\n * shallow copies — the input event is never mutated, because other session\n * subscribers (and the session's own state) share the same object.\n */\nexport function projectDashboardRpcEvent(event: Record<string, unknown>): Record<string, unknown> {\n\tswitch (event.type) {\n\t\tcase \"message_update\": {\n\t\t\tconst projected = omit(event, \"message\");\n\t\t\tconst streamEvent = event.assistantMessageEvent;\n\t\t\treturn isPlainObject(streamEvent)\n\t\t\t\t? { ...projected, assistantMessageEvent: omit(streamEvent, \"partial\") }\n\t\t\t\t: projected;\n\t\t}\n\t\tcase \"background_agent_event\": {\n\t\t\tconst child = event.event;\n\t\t\treturn isPlainObject(child) ? { ...event, event: projectDashboardRpcEvent(child) } : event;\n\t\t}\n\t\tdefault:\n\t\t\treturn event;\n\t}\n}\n"]}