@adhdev/daemon-core 0.9.76-rc.66 → 0.9.76-rc.68

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.76-rc.66",
3
+ "version": "0.9.76-rc.68",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -35,6 +35,7 @@ import {
35
35
  normalizeScreenSnapshot,
36
36
  promptLikelyVisible,
37
37
  sanitizeTerminalText,
38
+ TerminalTranscriptAccumulator,
38
39
  type CliChatMessage,
39
40
  type CliProviderModule,
40
41
  type CliScriptInput,
@@ -195,8 +196,10 @@ export class ProviderCliAdapter implements CliAdapter {
195
196
  // ─── CLI Scripts (script-based parsing) ───
196
197
  private cliScripts: CliScripts;
197
198
  private runtimeSettings: Record<string, any> = {};
198
- /** Full accumulated ANSI-stripped PTY output */
199
+ /** Full accumulated rendered PTY transcript for parser/readback use */
199
200
  private accumulatedBuffer: string = '';
201
+ /** Stateful rendered transcript accumulator; raw debug remains in accumulatedRawBuffer. */
202
+ private transcriptAccumulator = new TerminalTranscriptAccumulator();
200
203
  /** Full accumulated raw PTY output (with ANSI) */
201
204
  private accumulatedRawBuffer: string = '';
202
205
  /** Current visible terminal screen snapshot */
@@ -287,6 +290,7 @@ export class ProviderCliAdapter implements CliAdapter {
287
290
 
288
291
  private resetTerminalScreen(rows?: number, cols?: number): void {
289
292
  this.terminalScreen.reset(rows, cols);
293
+ this.transcriptAccumulator.reset();
290
294
  this.lastScreenText = '';
291
295
  this.lastScreenSnapshot = '';
292
296
  this.lastScreenChangeAt = 0;
@@ -634,6 +638,7 @@ export class ProviderCliAdapter implements CliAdapter {
634
638
  private handleOutput(rawData: string): void {
635
639
  this.terminalScreen.write(rawData);
636
640
  const cleanData = sanitizeTerminalText(rawData);
641
+ const renderedTranscript = this.transcriptAccumulator.append(rawData);
637
642
  const now = Date.now();
638
643
  const shouldReadScreen = this.shouldReadTerminalScreenSnapshot(now);
639
644
  const screenText = shouldReadScreen ? this.readTerminalScreenText(now) : this.lastScreenText;
@@ -680,15 +685,21 @@ export class ProviderCliAdapter implements CliAdapter {
680
685
  }
681
686
  }
682
687
 
683
- // Rolling buffers
688
+ // Rolling parser/readback buffers. `accumulatedBuffer` and
689
+ // `recentOutputBuffer` intentionally use the rendered transcript state,
690
+ // not raw PTY append text, so overwritten CLI status/tool lines do not
691
+ // leak stale cells into read_chat / mesh_read_chat compact summaries.
684
692
  const prevRecentLen = this.recentOutputBuffer.length;
685
- const prevAccumulatedLen = this.accumulatedBuffer.length;
686
693
  const prevAccumulatedRawLen = this.accumulatedRawBuffer.length;
687
- this.recentOutputBuffer = appendBoundedText(this.recentOutputBuffer, cleanData, ProviderCliAdapter.MAX_RECENT_OUTPUT_BUFFER);
688
- this.accumulatedBuffer = appendBoundedText(this.accumulatedBuffer, cleanData, ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
694
+ const nextAccumulatedBuffer = renderedTranscript.length <= ProviderCliAdapter.MAX_ACCUMULATED_BUFFER
695
+ ? renderedTranscript
696
+ : renderedTranscript.slice(-ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
697
+ const nextRecentOutputBuffer = nextAccumulatedBuffer.slice(-ProviderCliAdapter.MAX_RECENT_OUTPUT_BUFFER);
698
+ this.recentOutputBuffer = nextRecentOutputBuffer;
699
+ this.accumulatedBuffer = nextAccumulatedBuffer;
689
700
  this.accumulatedRawBuffer = appendBoundedText(this.accumulatedRawBuffer, rawData, ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
690
- const droppedRecent = this.recordBoundedAppendDrop(prevRecentLen, cleanData.length, this.recentOutputBuffer.length);
691
- const droppedClean = this.recordBoundedAppendDrop(prevAccumulatedLen, cleanData.length, this.accumulatedBuffer.length);
701
+ const droppedRecent = Math.max(0, prevRecentLen - this.recentOutputBuffer.length);
702
+ const droppedClean = Math.max(0, renderedTranscript.length - this.accumulatedBuffer.length);
692
703
  const droppedRaw = this.recordBoundedAppendDrop(prevAccumulatedRawLen, rawData.length, this.accumulatedRawBuffer.length);
693
704
  this.recentOutputDroppedChars += droppedRecent;
694
705
  this.accumulatedBufferDroppedChars += droppedClean;
@@ -173,32 +173,216 @@ export interface CliProviderModule {
173
173
  function stripAnsi(str: string): string {
174
174
  // eslint-disable-next-line no-control-regex
175
175
  return str
176
- .replace(/\x1B\][^\x07]*\x07/g, '')
177
- .replace(/\x1B\][\s\S]*?\x1B\\/g, '')
176
+ .replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '')
178
177
  .replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, '')
179
- .replace(/\x1B\[\d*[A-HJKSTfG]/g, ' ')
180
- .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '')
181
- .replace(/ +/g, ' ');
178
+ .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
179
+ }
180
+
181
+ type SavedCursor = { row: number; col: number };
182
+
183
+ function parseCount(params: string, fallback = 1): number {
184
+ const first = Number(String(params || '').split(';')[0] || fallback);
185
+ return Math.max(1, Number.isFinite(first) ? first : fallback);
186
+ }
187
+
188
+ function isCombiningMark(ch: string): boolean {
189
+ return /[\u0300-\u036F\u1AB0-\u1AFF\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/.test(ch);
190
+ }
191
+
192
+ function isWideCodePoint(ch: string): boolean {
193
+ const cp = ch.codePointAt(0) || 0;
194
+ return cp >= 0x1100 && (
195
+ cp <= 0x115F || cp === 0x2329 || cp === 0x232A ||
196
+ (cp >= 0x2E80 && cp <= 0xA4CF && cp !== 0x303F) ||
197
+ (cp >= 0xAC00 && cp <= 0xD7A3) ||
198
+ (cp >= 0xF900 && cp <= 0xFAFF) ||
199
+ (cp >= 0xFE10 && cp <= 0xFE19) ||
200
+ (cp >= 0xFE30 && cp <= 0xFE6F) ||
201
+ (cp >= 0xFF00 && cp <= 0xFF60) ||
202
+ (cp >= 0xFFE0 && cp <= 0xFFE6) ||
203
+ (cp >= 0x1F300 && cp <= 0x1FAFF)
204
+ );
205
+ }
206
+
207
+ /**
208
+ * Stateful, transcript-oriented terminal cell accumulator.
209
+ *
210
+ * CLI transcript parsing must not consume raw PTY append text for user-visible
211
+ * readback: CLIs rewrite prompts/status/tool lines with CR, BS, CSI cursor
212
+ * motion and erase-line. This accumulator preserves parser state across chunks
213
+ * and mutates rendered cells before exposing plain transcript text. It is a
214
+ * deliberately small terminal model for readback buffers; live UI rendering still
215
+ * uses TerminalScreen's ghostty/xterm backend.
216
+ */
217
+ export class TerminalTranscriptAccumulator {
218
+ private lines: string[][] = [[]];
219
+ private row = 0;
220
+ private col = 0;
221
+ private savedCursor: SavedCursor | null = null;
222
+ private pendingEscape = '';
223
+
224
+ append(data: string): string {
225
+ const input = this.pendingEscape + String(data || '');
226
+ this.pendingEscape = '';
227
+ for (let i = 0; i < input.length; i += 1) {
228
+ let ch = input[i];
229
+ if (ch === '\x1B') {
230
+ const consumed = this.consumeEscape(input.slice(i));
231
+ if (consumed === 0) {
232
+ this.pendingEscape = input.slice(i);
233
+ break;
234
+ }
235
+ i += consumed - 1;
236
+ continue;
237
+ }
238
+ const cp = input.codePointAt(i);
239
+ if (cp && cp > 0xFFFF) {
240
+ ch = String.fromCodePoint(cp);
241
+ i += 1;
242
+ }
243
+ this.writeControlOrChar(ch);
244
+ }
245
+ return this.getText();
246
+ }
247
+
248
+ reset(): void {
249
+ this.lines = [[]];
250
+ this.row = 0;
251
+ this.col = 0;
252
+ this.savedCursor = null;
253
+ this.pendingEscape = '';
254
+ }
255
+
256
+ getText(): string {
257
+ return this.lines.map(line => line.join('').replace(/[ \t]+$/g, '')).join('\n');
258
+ }
259
+
260
+ private ensureRow(row = this.row): void {
261
+ while (this.lines.length <= row) this.lines.push([]);
262
+ }
263
+
264
+ private writeControlOrChar(ch: string): void {
265
+ if (ch === '\r') {
266
+ this.col = 0;
267
+ return;
268
+ }
269
+ if (ch === '\n') {
270
+ this.row += 1;
271
+ this.col = 0;
272
+ this.ensureRow();
273
+ return;
274
+ }
275
+ if (ch === '\b') {
276
+ this.col = Math.max(0, this.col - 1);
277
+ return;
278
+ }
279
+ if (ch < ' ' || ch === '\x7F') return;
280
+
281
+ this.ensureRow();
282
+ const line = this.lines[this.row];
283
+ if (isCombiningMark(ch) && this.col > 0) {
284
+ line[this.col - 1] = `${line[this.col - 1] || ''}${ch}`;
285
+ return;
286
+ }
287
+ while (line.length < this.col) line.push(' ');
288
+ line[this.col] = ch;
289
+ this.col += isWideCodePoint(ch) ? 2 : 1;
290
+ }
291
+
292
+ private consumeEscape(seq: string): number {
293
+ if (seq.length < 2) return 0;
294
+ const next = seq[1];
295
+ if (next === '7') {
296
+ this.savedCursor = { row: this.row, col: this.col };
297
+ return 2;
298
+ }
299
+ if (next === '8') {
300
+ if (this.savedCursor) {
301
+ this.row = this.savedCursor.row;
302
+ this.col = this.savedCursor.col;
303
+ this.ensureRow();
304
+ }
305
+ return 2;
306
+ }
307
+ if (next === ']') {
308
+ const bel = seq.indexOf('\x07', 2);
309
+ const st = seq.indexOf('\x1B\\', 2);
310
+ const end = bel >= 0 && (st < 0 || bel < st) ? bel + 1 : st >= 0 ? st + 2 : 0;
311
+ return end;
312
+ }
313
+ if (next === '[') {
314
+ const match = seq.match(/^\x1B\[([0-?]*)([ -/]*)([@-~])/);
315
+ if (!match) return seq.length < 32 ? 0 : 1;
316
+ this.applyCsi(match[1] || '', match[3]);
317
+ return match[0].length;
318
+ }
319
+ if (/[P^_X]/.test(next)) {
320
+ const bel = seq.indexOf('\x07', 2);
321
+ const st = seq.indexOf('\x1B\\', 2);
322
+ const end = bel >= 0 && (st < 0 || bel < st) ? bel + 1 : st >= 0 ? st + 2 : 0;
323
+ return end;
324
+ }
325
+ return 2;
326
+ }
327
+
328
+ private applyCsi(params: string, final: string): void {
329
+ const count = parseCount(params);
330
+ this.ensureRow();
331
+ if (final === 'A') this.row = Math.max(0, this.row - count);
332
+ else if (final === 'B') this.row += count;
333
+ else if (final === 'C') this.col += count;
334
+ else if (final === 'D') this.col = Math.max(0, this.col - count);
335
+ else if (final === 'G') this.col = Math.max(0, count - 1);
336
+ else if (final === 'H' || final === 'f') {
337
+ const parts = String(params || '').split(';');
338
+ this.row = Math.max(0, (Number(parts[0] || 1) || 1) - 1);
339
+ this.col = Math.max(0, (Number(parts[1] || 1) || 1) - 1);
340
+ } else if (final === 'J') {
341
+ const mode = Number(params || 0) || 0;
342
+ if (mode === 2 || mode === 3) {
343
+ this.lines = [[]];
344
+ this.row = 0;
345
+ this.col = 0;
346
+ } else if (mode === 0) {
347
+ this.lines[this.row] = this.lines[this.row].slice(0, this.col);
348
+ this.lines.splice(this.row + 1);
349
+ } else if (mode === 1) {
350
+ for (let r = 0; r < this.row; r += 1) this.lines[r] = [];
351
+ const line = this.lines[this.row];
352
+ for (let c = 0; c <= Math.min(this.col, line.length - 1); c += 1) line[c] = ' ';
353
+ }
354
+ } else if (final === 'K') {
355
+ const mode = Number(params || 0) || 0;
356
+ const line = this.lines[this.row];
357
+ if (mode === 2) this.lines[this.row] = [];
358
+ else if (mode === 1) {
359
+ for (let c = 0; c <= Math.min(this.col, line.length - 1); c += 1) line[c] = ' ';
360
+ } else {
361
+ this.lines[this.row] = line.slice(0, this.col);
362
+ }
363
+ } else if (final === 's') {
364
+ this.savedCursor = { row: this.row, col: this.col };
365
+ } else if (final === 'u') {
366
+ if (this.savedCursor) {
367
+ this.row = this.savedCursor.row;
368
+ this.col = this.savedCursor.col;
369
+ }
370
+ }
371
+ this.ensureRow();
372
+ }
182
373
  }
183
374
 
184
375
  function stripTerminalNoise(str: string): string {
185
376
  return String(str || '')
186
377
  .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '')
187
- .replace(/(^|[\s([])(?:\??\d{1,4}(?:;\d{1,4})*[A-Za-z])(?=$|[\s)\]])/g, '$1')
188
- .replace(/(^|[\s([])(?:\[\??\d{1,4}(?:;\d{1,4})*[A-Za-z])(?=$|[\s)\]])/g, '$1')
189
- .replace(/(^|[\s([])(?:\d{1,4};\?)(?=$|[\s)\]])/g, '$1')
190
- .replace(/(^|[\s([])(?:\d+\$r[0-9;\" ]*[A-Za-z]?)(?=$|[\s)\]])/g, '$1')
191
- .replace(/(^|[\s([])(?:>\|[A-Za-z0-9_.:-]+(?:\([^)]*\))?)(?=$|[\s)\]])/g, '$1')
192
- .replace(/(^|[\s([])(?:[A-Z]\d(?:\s+[A-Z]\d)+)(?=$|[\s)\]])/g, '$1')
193
- .replace(/(^|[\s([])(?:\d+;[^\s)\]]+)(?=$|[\s)\]])/g, '$1')
194
378
  .replace(/\r+/g, '\n')
195
379
  .replace(/[ \t]+\n/g, '\n')
196
- .replace(/\n{3,}/g, '\n\n')
197
- .replace(/ {2,}/g, ' ');
380
+ .replace(/\n{4,}/g, '\n\n\n');
198
381
  }
199
382
 
200
383
  export function sanitizeTerminalText(str: string): string {
201
- return stripTerminalNoise(stripAnsi(str));
384
+ const accumulator = new TerminalTranscriptAccumulator();
385
+ return stripTerminalNoise(stripAnsi(accumulator.append(str)));
202
386
  }
203
387
 
204
388
  export function listCliScriptNames(scripts: CliScripts | undefined): string[] {
@@ -6,6 +6,17 @@ function readNonEmptyString(value: unknown): string {
6
6
  return typeof value === 'string' && value.trim() ? value.trim() : '';
7
7
  }
8
8
 
9
+ const MESH_COORDINATOR_EVENTS = new Set([
10
+ 'agent:generating_completed',
11
+ 'agent:waiting_approval',
12
+ 'agent:stopped',
13
+ 'monitor:long_generating',
14
+ ]);
15
+
16
+ function isMeshCoordinatorEvent(eventName: unknown): eventName is string {
17
+ return typeof eventName === 'string' && MESH_COORDINATOR_EVENTS.has(eventName);
18
+ }
19
+
9
20
  function formatCompletionMetadata(event: Record<string, unknown>): string {
10
21
  const parts = [
11
22
  readNonEmptyString(event.targetSessionId) ? `session_id=${readNonEmptyString(event.targetSessionId)}` : '',
@@ -27,6 +38,12 @@ function buildMeshSystemMessage(args: {
27
38
  if (args.event === 'agent:waiting_approval') {
28
39
  return `[System] ${args.nodeLabel} is waiting for approval to proceed${metadata}. You may use mesh_read_chat and mesh_approve to handle it.`;
29
40
  }
41
+ if (args.event === 'agent:stopped') {
42
+ return `[System] ${args.nodeLabel} has stopped${metadata}. Use mesh_read_chat once if you need to inspect its last output.`;
43
+ }
44
+ if (args.event === 'monitor:long_generating') {
45
+ return `[System] ${args.nodeLabel} has been generating for a long time${metadata}. Use mesh_read_chat once for a status check, but do not poll repeatedly.`;
46
+ }
30
47
  return '';
31
48
  }
32
49
 
@@ -63,7 +80,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
63
80
 
64
81
  export function handleMeshForwardEvent(components: DaemonComponents, payload: Record<string, unknown>) {
65
82
  const eventName = readNonEmptyString(payload.event);
66
- if (eventName !== 'agent:generating_completed' && eventName !== 'agent:waiting_approval') {
83
+ if (!isMeshCoordinatorEvent(eventName)) {
67
84
  return { success: false, error: 'unsupported mesh event' };
68
85
  }
69
86
  const meshId = readNonEmptyString(payload.meshId);
@@ -86,8 +103,8 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
86
103
 
87
104
  export function setupMeshEventForwarding(components: DaemonComponents) {
88
105
  components.instanceManager.onEvent((event) => {
89
- // We only care about agent sub-session completion or waiting approval
90
- if (event.event !== 'agent:generating_completed' && event.event !== 'agent:waiting_approval') return;
106
+ // We only care about lightweight Repo Mesh coordinator control/status hints.
107
+ if (!isMeshCoordinatorEvent(event.event)) return;
91
108
 
92
109
  const instanceId = readNonEmptyString(event.instanceId);
93
110
  if (!instanceId) return;