agent-relay 2.1.1 → 2.1.3

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.
Files changed (70) hide show
  1. package/dist/index.cjs +179 -8
  2. package/dist/src/cli/index.d.ts +11 -1
  3. package/dist/src/cli/index.d.ts.map +1 -1
  4. package/dist/src/cli/index.js +112 -110
  5. package/dist/src/cli/index.js.map +1 -1
  6. package/package.json +18 -18
  7. package/packages/api-types/package.json +1 -1
  8. package/packages/benchmark/package.json +4 -4
  9. package/packages/bridge/package.json +8 -8
  10. package/packages/cli-tester/package.json +1 -1
  11. package/packages/config/package.json +2 -2
  12. package/packages/continuity/package.json +2 -2
  13. package/packages/daemon/dist/connection.d.ts +5 -0
  14. package/packages/daemon/dist/connection.d.ts.map +1 -1
  15. package/packages/daemon/dist/connection.js +19 -1
  16. package/packages/daemon/dist/connection.js.map +1 -1
  17. package/packages/daemon/dist/server.js +2 -2
  18. package/packages/daemon/dist/server.js.map +1 -1
  19. package/packages/daemon/package.json +12 -12
  20. package/packages/daemon/src/connection.ts +22 -1
  21. package/packages/daemon/src/router.test.ts +32 -0
  22. package/packages/daemon/src/server.ts +2 -2
  23. package/packages/hooks/package.json +4 -4
  24. package/packages/mcp/package.json +3 -3
  25. package/packages/memory/package.json +2 -2
  26. package/packages/policy/package.json +2 -2
  27. package/packages/protocol/dist/types.d.ts +5 -0
  28. package/packages/protocol/dist/types.d.ts.map +1 -1
  29. package/packages/protocol/package.json +1 -1
  30. package/packages/protocol/src/types.ts +5 -0
  31. package/packages/resiliency/package.json +1 -1
  32. package/packages/sdk/dist/client.d.ts +6 -0
  33. package/packages/sdk/dist/client.d.ts.map +1 -1
  34. package/packages/sdk/dist/client.js +1 -0
  35. package/packages/sdk/dist/client.js.map +1 -1
  36. package/packages/sdk/package.json +2 -2
  37. package/packages/sdk/src/client.ts +7 -0
  38. package/packages/spawner/package.json +1 -1
  39. package/packages/state/package.json +1 -1
  40. package/packages/storage/dist/adapter.d.ts +2 -0
  41. package/packages/storage/dist/adapter.d.ts.map +1 -1
  42. package/packages/storage/dist/adapter.js +7 -1
  43. package/packages/storage/dist/adapter.js.map +1 -1
  44. package/packages/storage/dist/jsonl-adapter.d.ts +14 -0
  45. package/packages/storage/dist/jsonl-adapter.d.ts.map +1 -1
  46. package/packages/storage/dist/jsonl-adapter.js +75 -0
  47. package/packages/storage/dist/jsonl-adapter.js.map +1 -1
  48. package/packages/storage/package.json +2 -2
  49. package/packages/storage/src/adapter.ts +9 -1
  50. package/packages/storage/src/jsonl-adapter.test.ts +31 -0
  51. package/packages/storage/src/jsonl-adapter.ts +86 -0
  52. package/packages/telemetry/package.json +1 -1
  53. package/packages/trajectory/package.json +2 -2
  54. package/packages/user-directory/package.json +2 -2
  55. package/packages/utils/package.json +2 -2
  56. package/packages/wrapper/dist/base-wrapper.d.ts +5 -0
  57. package/packages/wrapper/dist/base-wrapper.d.ts.map +1 -1
  58. package/packages/wrapper/dist/base-wrapper.js +14 -1
  59. package/packages/wrapper/dist/base-wrapper.js.map +1 -1
  60. package/packages/wrapper/dist/shared.d.ts +36 -0
  61. package/packages/wrapper/dist/shared.d.ts.map +1 -1
  62. package/packages/wrapper/dist/shared.js +123 -2
  63. package/packages/wrapper/dist/shared.js.map +1 -1
  64. package/packages/wrapper/dist/tmux-wrapper.js +1 -1
  65. package/packages/wrapper/dist/tmux-wrapper.js.map +1 -1
  66. package/packages/wrapper/package.json +6 -6
  67. package/packages/wrapper/src/base-wrapper.ts +15 -0
  68. package/packages/wrapper/src/shared.test.ts +156 -11
  69. package/packages/wrapper/src/shared.ts +154 -2
  70. package/packages/wrapper/src/tmux-wrapper.ts +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/wrapper",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "CLI agent wrappers for Agent Relay - tmux, pty integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,11 +30,11 @@
30
30
  "clean": "rm -rf dist"
31
31
  },
32
32
  "dependencies": {
33
- "@agent-relay/api-types": "2.1.1",
34
- "@agent-relay/protocol": "2.1.1",
35
- "@agent-relay/config": "2.1.1",
36
- "@agent-relay/continuity": "2.1.1",
37
- "@agent-relay/resiliency": "2.1.1",
33
+ "@agent-relay/api-types": "2.1.3",
34
+ "@agent-relay/protocol": "2.1.3",
35
+ "@agent-relay/config": "2.1.3",
36
+ "@agent-relay/continuity": "2.1.3",
37
+ "@agent-relay/resiliency": "2.1.3",
38
38
  "zod": "^3.23.8"
39
39
  },
40
40
  "devDependencies": {
@@ -32,6 +32,7 @@ import {
32
32
  sortByPriority,
33
33
  getPriorityFromImportance,
34
34
  MESSAGE_PRIORITY,
35
+ shouldIgnoreForIdleDetection,
35
36
  } from './shared.js';
36
37
  import {
37
38
  DEFAULT_IDLE_BEFORE_INJECT_MS,
@@ -267,8 +268,22 @@ export abstract class BaseWrapper extends EventEmitter {
267
268
  /**
268
269
  * Feed output to the idle and stuck detectors.
269
270
  * Call this whenever new output is received from the agent.
271
+ *
272
+ * Note: Auto-suggestions (ghost text) are filtered out to prevent
273
+ * false idle resets. Claude Code and other CLIs show suggestions
274
+ * in gray/dim text with cursor save/restore, which should not
275
+ * be treated as "real" output for idle detection.
270
276
  */
271
277
  protected feedIdleDetectorOutput(output: string): void {
278
+ // Check if this output is likely an auto-suggestion (ghost text)
279
+ // Auto-suggestions should not reset the idle timer
280
+ if (shouldIgnoreForIdleDetection(output)) {
281
+ // Still feed to stuck detector - it strips ANSI and checks for patterns
282
+ // But don't feed to idle detector to avoid resetting the silence timer
283
+ this.stuckDetector.onOutput(output);
284
+ return;
285
+ }
286
+
272
287
  this.idleDetector.onOutput(output);
273
288
  this.stuckDetector.onOutput(output);
274
289
  }
@@ -13,7 +13,7 @@ describe('buildInjectionString', () => {
13
13
  };
14
14
 
15
15
  describe('sender name display', () => {
16
- it('uses msg.from when from is not _DashboardUI', () => {
16
+ it('uses msg.from when from is not Dashboard', () => {
17
17
  const msg: QueuedMessage = {
18
18
  ...baseMessage,
19
19
  from: 'RegularAgent',
@@ -22,40 +22,40 @@ describe('buildInjectionString', () => {
22
22
  expect(result).toContain('Relay message from RegularAgent');
23
23
  });
24
24
 
25
- it('uses msg.from when from is _DashboardUI but no senderName in data', () => {
25
+ it('uses msg.from when from is Dashboard but no senderName in data', () => {
26
26
  const msg: QueuedMessage = {
27
27
  ...baseMessage,
28
- from: '_DashboardUI',
28
+ from: 'Dashboard',
29
29
  };
30
30
  const result = buildInjectionString(msg);
31
- expect(result).toContain('Relay message from _DashboardUI');
31
+ expect(result).toContain('Relay message from Dashboard');
32
32
  });
33
33
 
34
- it('uses senderName when from is _DashboardUI and senderName exists', () => {
34
+ it('uses senderName when from is Dashboard and senderName exists', () => {
35
35
  const msg: QueuedMessage = {
36
36
  ...baseMessage,
37
- from: '_DashboardUI',
37
+ from: 'Dashboard',
38
38
  data: { senderName: 'GitHubUser123' },
39
39
  };
40
40
  const result = buildInjectionString(msg);
41
41
  expect(result).toContain('Relay message from GitHubUser123');
42
- expect(result).not.toContain('_DashboardUI');
42
+ expect(result).not.toContain('Dashboard');
43
43
  });
44
44
 
45
45
  it('uses msg.from when senderName is not a string', () => {
46
46
  const msg: QueuedMessage = {
47
47
  ...baseMessage,
48
- from: '_DashboardUI',
48
+ from: 'Dashboard',
49
49
  data: { senderName: 12345 }, // not a string
50
50
  };
51
51
  const result = buildInjectionString(msg);
52
- expect(result).toContain('Relay message from _DashboardUI');
52
+ expect(result).toContain('Relay message from Dashboard');
53
53
  });
54
54
 
55
55
  it('uses msg.from when senderName is empty string', () => {
56
56
  const msg: QueuedMessage = {
57
57
  ...baseMessage,
58
- from: '_DashboardUI',
58
+ from: 'Dashboard',
59
59
  data: { senderName: '' },
60
60
  };
61
61
  // Empty string is falsy but still a string - our check uses typeof === 'string'
@@ -65,7 +65,7 @@ describe('buildInjectionString', () => {
65
65
  expect(result).toContain('Relay message from ['); // empty between 'from' and '['
66
66
  });
67
67
 
68
- it('does not use senderName when from is not _DashboardUI even if senderName exists', () => {
68
+ it('does not use senderName when from is not Dashboard even if senderName exists', () => {
69
69
  const msg: QueuedMessage = {
70
70
  ...baseMessage,
71
71
  from: 'OtherAgent',
@@ -320,3 +320,148 @@ describe('Message Priority System', () => {
320
320
  });
321
321
  });
322
322
  });
323
+
324
+ // Import auto-suggestion detection functions for testing
325
+ import { detectAutoSuggest, shouldIgnoreForIdleDetection } from './shared.js';
326
+
327
+ describe('Auto-suggestion Detection', () => {
328
+ describe('detectAutoSuggest', () => {
329
+ it('detects dim text styling (common for ghost text)', () => {
330
+ // \x1B[2m is dim text
331
+ const output = '\x1B[2msuggested completion\x1B[0m';
332
+ const result = detectAutoSuggest(output);
333
+
334
+ expect(result.isAutoSuggest).toBe(true);
335
+ expect(result.patterns).toContain('dim');
336
+ expect(result.confidence).toBeGreaterThanOrEqual(0.4);
337
+ });
338
+
339
+ it('detects bright black (dark gray) styling', () => {
340
+ // \x1B[90m is bright black (dark gray)
341
+ const output = '\x1B[90mauto-suggested text\x1B[0m';
342
+ const result = detectAutoSuggest(output);
343
+
344
+ expect(result.isAutoSuggest).toBe(true);
345
+ expect(result.patterns).toContain('brightBlack');
346
+ expect(result.confidence).toBeGreaterThanOrEqual(0.4);
347
+ });
348
+
349
+ it('detects 256-color gray styling (pattern detected but alone not enough)', () => {
350
+ // \x1B[38;5;8m is 256-color dark gray
351
+ // By itself it only adds 0.3 confidence, below the 0.4 threshold
352
+ const output = '\x1B[38;5;8msuggestion\x1B[0m';
353
+ const result = detectAutoSuggest(output);
354
+
355
+ expect(result.patterns).toContain('gray256');
356
+ expect(result.confidence).toBeGreaterThan(0);
357
+ // 256-color gray alone isn't enough - need additional signals
358
+ expect(result.isAutoSuggest).toBe(false);
359
+ });
360
+
361
+ it('detects 256-color gray combined with other signals', () => {
362
+ // 256-color gray + cursor save/restore = strong signal
363
+ const output = '\x1B[s\x1B[38;5;8msuggestion\x1B[0m\x1B[u';
364
+ const result = detectAutoSuggest(output);
365
+
366
+ expect(result.isAutoSuggest).toBe(true);
367
+ expect(result.patterns).toContain('gray256');
368
+ expect(result.patterns).toContain('cursorSaveRestore');
369
+ });
370
+
371
+ it('detects cursor save/restore pair (strong indicator)', () => {
372
+ // \x1B[s saves cursor, \x1B[u restores cursor
373
+ const output = '\x1B[s\x1B[90msuggestion\x1B[0m\x1B[u';
374
+ const result = detectAutoSuggest(output);
375
+
376
+ expect(result.isAutoSuggest).toBe(true);
377
+ expect(result.patterns).toContain('cursorSaveRestore');
378
+ expect(result.confidence).toBeGreaterThanOrEqual(0.5);
379
+ });
380
+
381
+ it('detects alternative cursor save/restore (ESC 7/8)', () => {
382
+ // \x1B7 saves cursor, \x1B8 restores cursor (alternative format)
383
+ const output = '\x1B7\x1B[2mcompletion\x1B0m\x1B8';
384
+ const result = detectAutoSuggest(output);
385
+
386
+ expect(result.patterns).toContain('cursorSaveRestore');
387
+ });
388
+
389
+ it('returns false for normal text output', () => {
390
+ const output = 'Hello, this is normal output text\n';
391
+ const result = detectAutoSuggest(output);
392
+
393
+ expect(result.isAutoSuggest).toBe(false);
394
+ expect(result.confidence).toBe(0);
395
+ expect(result.patterns).toHaveLength(0);
396
+ });
397
+
398
+ it('returns false for colored output without gray/dim', () => {
399
+ // Regular green text - not an auto-suggestion
400
+ const output = '\x1B[32mSuccess: Tests passed\x1B[0m';
401
+ const result = detectAutoSuggest(output);
402
+
403
+ expect(result.isAutoSuggest).toBe(false);
404
+ });
405
+
406
+ it('reduces confidence for multi-line output', () => {
407
+ // Auto-suggestions are typically single-line
408
+ const multiLine = '\x1B[90mLine 1\nLine 2\nLine 3\nLine 4\x1B[0m';
409
+ const result = detectAutoSuggest(multiLine);
410
+
411
+ // Still detects the pattern but confidence is reduced
412
+ expect(result.patterns).toContain('brightBlack');
413
+ // Multi-line reduces confidence by 50%
414
+ expect(result.confidence).toBeLessThan(0.4);
415
+ });
416
+
417
+ it('combines multiple patterns for higher confidence', () => {
418
+ // Both dim and cursor save/restore
419
+ const output = '\x1B[s\x1B[2m\x1B[90msuggestion\x1B[0m\x1B[u';
420
+ const result = detectAutoSuggest(output);
421
+
422
+ expect(result.patterns).toContain('dim');
423
+ expect(result.patterns).toContain('brightBlack');
424
+ expect(result.patterns).toContain('cursorSaveRestore');
425
+ expect(result.confidence).toBeGreaterThanOrEqual(0.8);
426
+ });
427
+
428
+ it('includes stripped content for debugging', () => {
429
+ const output = '\x1B[90msuggested text\x1B[0m';
430
+ const result = detectAutoSuggest(output);
431
+
432
+ expect(result.strippedContent).toBe('suggested text');
433
+ });
434
+ });
435
+
436
+ describe('shouldIgnoreForIdleDetection', () => {
437
+ it('ignores empty output', () => {
438
+ expect(shouldIgnoreForIdleDetection('')).toBe(true);
439
+ });
440
+
441
+ it('ignores output that is only ANSI control sequences', () => {
442
+ // Just cursor movement, no actual content
443
+ const output = '\x1B[2J\x1B[H'; // Clear screen and home cursor
444
+ expect(shouldIgnoreForIdleDetection(output)).toBe(true);
445
+ });
446
+
447
+ it('ignores auto-suggestion output', () => {
448
+ const autoSuggest = '\x1B[90mtype "exit" to quit\x1B[0m';
449
+ expect(shouldIgnoreForIdleDetection(autoSuggest)).toBe(true);
450
+ });
451
+
452
+ it('does not ignore normal text output', () => {
453
+ const normalOutput = 'Hello world\n';
454
+ expect(shouldIgnoreForIdleDetection(normalOutput)).toBe(false);
455
+ });
456
+
457
+ it('does not ignore colored output (non-gray)', () => {
458
+ const greenOutput = '\x1B[32mTest passed!\x1B[0m\n';
459
+ expect(shouldIgnoreForIdleDetection(greenOutput)).toBe(false);
460
+ });
461
+
462
+ it('does not ignore relay messages', () => {
463
+ const relayMessage = '->relay:Lead Task completed';
464
+ expect(shouldIgnoreForIdleDetection(relayMessage)).toBe(false);
465
+ });
466
+ });
467
+ });
@@ -200,6 +200,158 @@ export function sleep(ms: number): Promise<void> {
200
200
  return new Promise((resolve) => setTimeout(resolve, ms));
201
201
  }
202
202
 
203
+ /**
204
+ * ANSI escape patterns for auto-suggestion (ghost text) detection.
205
+ *
206
+ * Claude Code and other CLIs show auto-suggestions using:
207
+ * - Dim text: \x1B[2m
208
+ * - Bright black (gray): \x1B[90m
209
+ * - 256-color gray: \x1B[38;5;8m or \x1B[38;5;240m through \x1B[38;5;250m
210
+ * - Cursor save/restore: \x1B[s / \x1B[u or \x1B7 / \x1B8
211
+ *
212
+ * Auto-suggestions are typically:
213
+ * 1. Styled with dim/gray text
214
+ * 2. Cursor position is saved before, restored after (so cursor doesn't advance)
215
+ * 3. The actual text content is the "ghost" suggestion
216
+ */
217
+ // eslint-disable-next-line no-control-regex
218
+ const AUTO_SUGGEST_PATTERNS = {
219
+ // Dim text styling - commonly used for ghost text
220
+ dim: /\x1B\[2m/,
221
+ // Bright black (dark gray) - common for suggestions
222
+ brightBlack: /\x1B\[90m/,
223
+ // 256-color grays (8 is dark gray, 240-250 are grays)
224
+ gray256: /\x1B\[38;5;(?:8|24[0-9]|250)m/,
225
+ // Cursor save (CSI s or ESC 7)
226
+ cursorSave: /\x1B\[s|\x1B7/,
227
+ // Cursor restore (CSI u or ESC 8)
228
+ cursorRestore: /\x1B\[u|\x1B8/,
229
+ // Italic text - sometimes used for suggestions
230
+ italic: /\x1B\[3m/,
231
+ };
232
+
233
+ /**
234
+ * Result of auto-suggestion detection.
235
+ */
236
+ export interface AutoSuggestResult {
237
+ /** True if the output looks like an auto-suggestion */
238
+ isAutoSuggest: boolean;
239
+ /** Confidence level (0-1) */
240
+ confidence: number;
241
+ /** Which patterns were detected */
242
+ patterns: string[];
243
+ /** The actual content after stripping ANSI (for debugging) */
244
+ strippedContent?: string;
245
+ }
246
+
247
+ /**
248
+ * Detect if terminal output is likely an auto-suggestion (ghost text).
249
+ *
250
+ * Auto-suggestions should NOT reset the idle timer because they represent
251
+ * the CLI showing suggestions to the user, not actual output from the agent.
252
+ *
253
+ * Detection heuristics:
254
+ * 1. Contains dim/gray styling without other foreground colors
255
+ * 2. Has cursor save/restore patterns (suggestion doesn't advance cursor)
256
+ * 3. Stripped content is non-empty but doesn't contain relay commands
257
+ *
258
+ * @param output Raw terminal output including ANSI codes
259
+ * @returns Detection result with confidence and matched patterns
260
+ */
261
+ export function detectAutoSuggest(output: string): AutoSuggestResult {
262
+ const patterns: string[] = [];
263
+ let confidence = 0;
264
+
265
+ // Check for dim styling (very common for ghost text)
266
+ if (AUTO_SUGGEST_PATTERNS.dim.test(output)) {
267
+ patterns.push('dim');
268
+ confidence += 0.4;
269
+ }
270
+
271
+ // Check for bright black (dark gray)
272
+ if (AUTO_SUGGEST_PATTERNS.brightBlack.test(output)) {
273
+ patterns.push('brightBlack');
274
+ confidence += 0.4;
275
+ }
276
+
277
+ // Check for 256-color gray
278
+ if (AUTO_SUGGEST_PATTERNS.gray256.test(output)) {
279
+ patterns.push('gray256');
280
+ confidence += 0.3;
281
+ }
282
+
283
+ // Check for italic (sometimes used for suggestions)
284
+ if (AUTO_SUGGEST_PATTERNS.italic.test(output)) {
285
+ patterns.push('italic');
286
+ confidence += 0.2;
287
+ }
288
+
289
+ // Check for cursor save/restore pair (strong indicator)
290
+ const hasCursorSave = AUTO_SUGGEST_PATTERNS.cursorSave.test(output);
291
+ const hasCursorRestore = AUTO_SUGGEST_PATTERNS.cursorRestore.test(output);
292
+
293
+ if (hasCursorSave && hasCursorRestore) {
294
+ patterns.push('cursorSaveRestore');
295
+ confidence += 0.5;
296
+ } else if (hasCursorSave || hasCursorRestore) {
297
+ patterns.push(hasCursorSave ? 'cursorSave' : 'cursorRestore');
298
+ confidence += 0.2;
299
+ }
300
+
301
+ // Cap confidence at 1.0
302
+ confidence = Math.min(confidence, 1.0);
303
+
304
+ // Strip ANSI to check actual content
305
+ const stripped = stripAnsi(output);
306
+
307
+ // If no patterns detected, it's not an auto-suggest
308
+ if (patterns.length === 0) {
309
+ return { isAutoSuggest: false, confidence: 0, patterns, strippedContent: stripped };
310
+ }
311
+
312
+ // Additional checks to reduce false positives:
313
+ // - Actual content should be relatively short (suggestions are typically one line)
314
+ // - Should not contain newlines (multi-line output is probably real output)
315
+ const lines = stripped.split('\n').filter(l => l.trim().length > 0);
316
+ if (lines.length > 2) {
317
+ // Multi-line content - less likely to be just a suggestion
318
+ confidence *= 0.5;
319
+ }
320
+
321
+ // Consider it an auto-suggest if confidence is above threshold
322
+ const isAutoSuggest = confidence >= 0.4;
323
+
324
+ return { isAutoSuggest, confidence, patterns, strippedContent: stripped };
325
+ }
326
+
327
+ /**
328
+ * Check if output should be ignored for idle detection purposes.
329
+ * Returns true if the output is likely an auto-suggestion or control sequence only.
330
+ *
331
+ * @param output Raw terminal output
332
+ * @returns true if output should be ignored for idle detection
333
+ */
334
+ export function shouldIgnoreForIdleDetection(output: string): boolean {
335
+ // Empty output should be ignored
336
+ if (!output || output.length === 0) {
337
+ return true;
338
+ }
339
+
340
+ // Check if it's an auto-suggestion
341
+ const result = detectAutoSuggest(output);
342
+ if (result.isAutoSuggest) {
343
+ return true;
344
+ }
345
+
346
+ // Check if stripped content is empty (only control sequences)
347
+ const stripped = stripAnsi(output).trim();
348
+ if (stripped.length === 0) {
349
+ return true;
350
+ }
351
+
352
+ return false;
353
+ }
354
+
203
355
  /**
204
356
  * Build the injection string for a relay message.
205
357
  * Format: Relay message from {from} [{shortId}]{hints}: {body}
@@ -221,9 +373,9 @@ export function buildInjectionString(msg: QueuedMessage): string {
221
373
 
222
374
  const shortId = msg.messageId.substring(0, 8);
223
375
 
224
- // Use senderName from data if available (for dashboard messages sent via _DashboardUI)
376
+ // Use senderName from data if available (for dashboard messages sent via Dashboard)
225
377
  // This allows showing the actual GitHub username instead of the system client name
226
- const displayFrom = (msg.from === '_DashboardUI' && typeof msg.data?.senderName === 'string')
378
+ const displayFrom = (msg.from === 'Dashboard' && typeof msg.data?.senderName === 'string')
227
379
  ? msg.data.senderName
228
380
  : msg.from;
229
381
 
@@ -1291,7 +1291,7 @@ export class TmuxWrapper extends BaseWrapper {
1291
1291
  this.logStderr(`${agentName} is online, sending task...`);
1292
1292
 
1293
1293
  // Send task directly via our relay client (not dashboard API)
1294
- // This ensures the message comes "from" this agent, not from _DashboardUI
1294
+ // This ensures the message comes "from" this agent, not from Dashboard
1295
1295
  if (this.client.state === 'READY') {
1296
1296
  const sent = this.client.sendMessage(agentName, task, 'message');
1297
1297
  if (sent) {