@adhdev/daemon-core 0.7.41 → 0.7.43

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 (55) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +3 -4
  2. package/dist/cli-adapters/pty-transport.d.ts +1 -0
  3. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +4 -0
  4. package/dist/cli-adapters/terminal-backends/types.d.ts +4 -0
  5. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +4 -0
  6. package/dist/cli-adapters/terminal-screen.d.ts +4 -0
  7. package/dist/commands/cli-manager.d.ts +4 -2
  8. package/dist/config/chat-history.d.ts +0 -3
  9. package/dist/config/config.d.ts +2 -22
  10. package/dist/index.js +377 -195
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +377 -195
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/providers/cli-provider-instance.d.ts +4 -10
  15. package/dist/providers/contracts.d.ts +0 -79
  16. package/dist/providers/extension-provider-instance.d.ts +1 -0
  17. package/dist/providers/provider-instance.d.ts +0 -3
  18. package/dist/shared-types.d.ts +1 -3
  19. package/dist/status/normalize.js +60 -1
  20. package/dist/status/normalize.js.map +1 -1
  21. package/dist/status/normalize.mjs +60 -1
  22. package/dist/status/normalize.mjs.map +1 -1
  23. package/dist/status/reporter.d.ts +1 -0
  24. package/node_modules/@adhdev/session-host-core/dist/index.js +2 -2
  25. package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
  26. package/node_modules/@adhdev/session-host-core/dist/index.mjs +2 -2
  27. package/node_modules/@adhdev/session-host-core/dist/index.mjs.map +1 -1
  28. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  29. package/package.json +1 -1
  30. package/src/agent-stream/forward.ts +21 -1
  31. package/src/agent-stream/poller.ts +6 -1
  32. package/src/cli-adapters/provider-cli-adapter.ts +115 -71
  33. package/src/cli-adapters/pty-transport.ts +2 -0
  34. package/src/cli-adapters/session-host-transport.ts +1 -0
  35. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +5 -0
  36. package/src/cli-adapters/terminal-backends/types.ts +1 -0
  37. package/src/cli-adapters/terminal-backends/xterm-backend.ts +10 -0
  38. package/src/cli-adapters/terminal-screen.ts +4 -0
  39. package/src/commands/cli-manager.ts +44 -39
  40. package/src/commands/router.ts +1 -0
  41. package/src/commands/stream-commands.ts +14 -0
  42. package/src/config/chat-history.ts +3 -55
  43. package/src/config/config.d.ts +5 -50
  44. package/src/config/config.ts +71 -49
  45. package/src/config/workspaces.d.ts +1 -4
  46. package/src/providers/cli-provider-instance.ts +18 -42
  47. package/src/providers/contracts.ts +0 -81
  48. package/src/providers/extension-provider-instance.ts +27 -0
  49. package/src/providers/ide-provider-instance.ts +12 -0
  50. package/src/providers/provider-instance.d.ts +0 -1
  51. package/src/providers/provider-instance.ts +0 -3
  52. package/src/shared-types.ts +1 -3
  53. package/src/status/builders.ts +7 -2
  54. package/src/status/normalize.ts +81 -0
  55. package/src/status/reporter.ts +31 -2
@@ -7,7 +7,7 @@
7
7
 
8
8
  import * as path from 'path';
9
9
  import * as crypto from 'crypto';
10
- import type { ProviderModule, ProviderLaunchMode, ProviderLaunchOption } from './contracts.js';
10
+ import type { ProviderModule } from './contracts.js';
11
11
  import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
12
12
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
13
13
  import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
@@ -33,8 +33,7 @@ export class CliProviderInstance implements ProviderInstance {
33
33
  private historyWriter: ChatHistoryWriter;
34
34
  readonly instanceId: string;
35
35
 
36
- private launchMode: ProviderLaunchMode | null;
37
- private resolvedOutputFormat: 'terminal' | 'stream-json';
36
+ private presentationMode: 'terminal' | 'chat';
38
37
 
39
38
  constructor(
40
39
  private provider: ProviderModule,
@@ -42,37 +41,15 @@ export class CliProviderInstance implements ProviderInstance {
42
41
  private cliArgs: string[] = [],
43
42
  instanceId?: string,
44
43
  transportFactory?: PtyTransportFactory,
45
- launchModeId?: string,
46
44
  ) {
47
45
  this.type = provider.type;
48
46
  this.instanceId = instanceId || crypto.randomUUID();
49
- this.launchMode = (launchModeId && provider.launchModes?.find(m => m.id === launchModeId)) || null;
50
- this.resolvedOutputFormat = this.resolveOutputFormat();
47
+ this.presentationMode = 'chat';
51
48
  this.adapter = new ProviderCliAdapter(provider as any as CliProviderModule, workingDir, cliArgs, transportFactory);
52
49
  this.monitor = new StatusMonitor();
53
50
  this.historyWriter = new ChatHistoryWriter();
54
51
  }
55
52
 
56
- /**
57
- * Determine output rendering format from:
58
- * 1. launchMode.outputFormat (explicit override)
59
- * 2. launchOptions[].outputFormatMap — check actual args for matching values
60
- * 3. Default: 'terminal'
61
- */
62
- private resolveOutputFormat(): 'terminal' | 'stream-json' {
63
- if (this.launchMode?.outputFormat) return this.launchMode.outputFormat;
64
- if (this.provider.launchOptions?.length) {
65
- for (const opt of this.provider.launchOptions) {
66
- if (!opt.outputFormatMap) continue;
67
- // Check if any cliArg matches a value with an outputFormatMap entry
68
- for (const [val, fmt] of Object.entries(opt.outputFormatMap)) {
69
- if (this.cliArgs.includes(val)) return fmt;
70
- }
71
- }
72
- }
73
- return 'terminal';
74
- }
75
-
76
53
  // ─── Lifecycle ─────────────────────────────────
77
54
 
78
55
  async init(context: InstanceContext): Promise<void> {
@@ -110,33 +87,23 @@ export class CliProviderInstance implements ProviderInstance {
110
87
 
111
88
  getState(): ProviderState {
112
89
  const adapterStatus = this.adapter.getStatus();
90
+ const parsedStatus = this.adapter.getScriptParsedStatus?.() || null;
113
91
  const runtime = this.adapter.getRuntimeMetadata();
114
92
 
115
93
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
116
94
 
117
- if (adapterStatus.terminalHistory?.trim()) {
118
- this.historyWriter.appendTerminalHistory(
119
- this.type,
120
- adapterStatus.terminalHistory,
121
- `${this.provider.name} · ${dirName}`,
122
- this.instanceId,
123
- );
124
- }
125
-
126
95
  return {
127
96
  type: this.type,
128
97
  name: this.provider.name,
129
98
  category: 'cli',
130
99
  status: adapterStatus.status,
131
- mode: this.resolvedOutputFormat === 'stream-json' ? 'chat' : 'terminal',
132
- launchMode: this.launchMode?.id,
100
+ mode: this.presentationMode,
133
101
  activeChat: {
134
102
  id: `${this.type}_${this.workingDir}`,
135
- title: `${this.provider.name} · ${dirName}`,
136
- status: adapterStatus.status,
137
- messages: [],
138
- activeModal: adapterStatus.activeModal,
139
- terminalHistory: adapterStatus.terminalHistory,
103
+ title: parsedStatus?.title || `${this.provider.name} · ${dirName}`,
104
+ status: parsedStatus?.status || adapterStatus.status,
105
+ messages: Array.isArray(parsedStatus?.messages) ? parsedStatus.messages : [],
106
+ activeModal: parsedStatus?.activeModal ?? adapterStatus.activeModal,
140
107
  inputContent: '',
141
108
  },
142
109
  workspace: this.workingDir,
@@ -158,6 +125,15 @@ export class CliProviderInstance implements ProviderInstance {
158
125
  };
159
126
  }
160
127
 
128
+ setPresentationMode(mode: 'terminal' | 'chat'): void {
129
+ if (this.presentationMode === mode) return;
130
+ this.presentationMode = mode;
131
+ }
132
+
133
+ getPresentationMode(): 'terminal' | 'chat' {
134
+ return this.presentationMode;
135
+ }
136
+
161
137
  onEvent(event: string, data?: any): void {
162
138
  if (event === 'send_message' && data?.text) {
163
139
  void this.adapter.sendMessage(data.text).catch((e: any) => {
@@ -317,20 +317,6 @@ export interface ProviderModule {
317
317
  shell?: boolean;
318
318
  env?: Record<string, string>;
319
319
  };
320
- /**
321
- * Configurable options shown at session launch time (schema declaration).
322
- * The frontend renders these as a launch config UI.
323
- * Values are passed to launchArgBuilder to produce the final args.
324
- */
325
- launchOptions?: ProviderLaunchOption[];
326
- /**
327
- * Builds extra spawn args from user-selected launch option values.
328
- * Called with the merged defaults + mode preset + user overrides.
329
- * When defined, takes precedence over launchMode.extraArgs.
330
- */
331
- launchArgBuilder?: (options: Record<string, string | boolean | number>) => string[];
332
- /** Named presets — shortcuts that set launchOption values in bulk */
333
- launchModes?: ProviderLaunchMode[];
334
320
  patterns?: {
335
321
  prompt?: RegExp[];
336
322
  generating?: RegExp[];
@@ -407,72 +393,6 @@ export interface ProviderModule {
407
393
  auth?: AcpAuthMethod[];
408
394
  }
409
395
 
410
- // ─── CLI Launch Options (individual configurable flags) ────────────────
411
-
412
- export type ProviderLaunchOptionType = 'select' | 'boolean' | 'string' | 'number';
413
-
414
- /**
415
- * A single configurable option exposed at session launch time.
416
- * Providers declare these so the frontend can render a launch config UI.
417
- * The final args are built by launchArgBuilder(selectedValues).
418
- *
419
- * Example — Claude Code:
420
- * { id: 'outputFormat', type: 'select', options: [
421
- * { value: 'terminal', label: 'Terminal' },
422
- * { value: 'stream-json', label: 'Chat' },
423
- * ], default: 'terminal' }
424
- */
425
- export interface ProviderLaunchOption {
426
- /** Unique identifier — key used in launchArgBuilder options map */
427
- id: string;
428
- /** Display label */
429
- name: string;
430
- description?: string;
431
- type: ProviderLaunchOptionType;
432
- /** Options for 'select' type */
433
- options?: { value: string; label: string; description?: string }[];
434
- /** Default value — applied when not explicitly set */
435
- default?: string | boolean | number;
436
- /**
437
- * Maps specific values of this option to a frontend rendering hint.
438
- * e.g. { 'stream-json': 'stream-json' } tells the UI to switch to Chat mode.
439
- */
440
- outputFormatMap?: Record<string, 'terminal' | 'stream-json'>;
441
- }
442
-
443
- /**
444
- * A named launch preset — shorthand for a specific set of launchOption values.
445
- * When selected, its `options` are merged with user-configured values before
446
- * calling launchArgBuilder. Falls back to `extraArgs` if no launchArgBuilder defined.
447
- */
448
- export interface ProviderLaunchMode {
449
- /** Unique mode identifier (e.g. 'terminal', 'chat') */
450
- id: string;
451
- /** Display name */
452
- name: string;
453
- description?: string;
454
- /**
455
- * Preset option values — merged over defaults before calling launchArgBuilder.
456
- * Keys correspond to ProviderLaunchOption.id.
457
- */
458
- options?: Record<string, string | boolean | number>;
459
- /**
460
- * Fallback: raw args appended when no launchArgBuilder is defined.
461
- * Use launchArgBuilder + options for anything more than trivial cases.
462
- */
463
- extraArgs?: string[];
464
- /** Env var overrides applied on top of spawn.env */
465
- env?: Record<string, string>;
466
- /**
467
- * Output rendering hint (shorthand when not using launchOptions/outputFormatMap).
468
- * - 'terminal' — raw PTY stream (default)
469
- * - 'stream-json' — structured JSON events → chat messages
470
- */
471
- outputFormat?: 'terminal' | 'stream-json';
472
- /** Whether this is the default mode when none is specified */
473
- default?: boolean;
474
- }
475
-
476
396
  export interface ProviderResumeCapability {
477
397
  supported: boolean;
478
398
  stopStrategy?: 'command' | 'ctrl_c';
@@ -688,4 +608,3 @@ export interface ProviderControlDef {
688
608
  /** Hide this control when condition not met */
689
609
  hidden?: boolean;
690
610
  }
691
-
@@ -111,6 +111,8 @@ export class ExtensionProviderInstance implements ProviderInstance {
111
111
  this.detectTransition(newStatus, data);
112
112
  this.currentStatus = newStatus;
113
113
  }
114
+ } else if (event === 'stream_reset') {
115
+ this.resetStreamState();
114
116
  } else if (event === 'extension_connected') {
115
117
  this.ideType = data?.ideType || '';
116
118
  // Maintain instanceId UUID — do not overwrite
@@ -208,4 +210,29 @@ export class ExtensionProviderInstance implements ProviderInstance {
208
210
  : this.chatTitle;
209
211
  return title || this.agentName || this.provider.name;
210
212
  }
213
+
214
+ private resetStreamState(): void {
215
+ if (this.currentStatus !== 'idle') {
216
+ this.detectTransition('idle', {
217
+ title: this.chatTitle,
218
+ agentName: this.agentName,
219
+ extensionId: this.extensionId,
220
+ messages: this.messages,
221
+ });
222
+ }
223
+ this.agentStreams = [];
224
+ this.messages = [];
225
+ this.activeModal = null;
226
+ this.currentModel = '';
227
+ this.currentMode = '';
228
+ this.controlValues = {};
229
+ this.currentStatus = 'idle';
230
+ this.chatId = null;
231
+ this.chatTitle = null;
232
+ this.agentName = '';
233
+ this.extensionId = '';
234
+ this.lastAgentStatus = 'idle';
235
+ this.generatingStartedAt = 0;
236
+ this.monitor.reset();
237
+ }
211
238
  }
@@ -140,12 +140,24 @@ export class IdeProviderInstance implements ProviderInstance {
140
140
  } else if (event === 'cdp_disconnected') {
141
141
  this.cachedChat = null;
142
142
  this.currentStatus = 'idle';
143
+ for (const ext of this.extensions.values()) {
144
+ ext.onEvent('stream_reset');
145
+ }
143
146
  } else if (event === 'stream_update') {
144
147
  // Forward to Extension
145
148
  const extType = data?.extensionType;
146
149
  if (extType && this.extensions.has(extType)) {
147
150
  this.extensions.get(extType)!.onEvent('stream_update', data);
148
151
  }
152
+ } else if (event === 'stream_reset') {
153
+ const extType = data?.extensionType;
154
+ if (extType && this.extensions.has(extType)) {
155
+ this.extensions.get(extType)!.onEvent('stream_reset');
156
+ }
157
+ } else if (event === 'stream_reset_all') {
158
+ for (const ext of this.extensions.values()) {
159
+ ext.onEvent('stream_reset');
160
+ }
149
161
  }
150
162
  }
151
163
 
@@ -37,7 +37,6 @@ export interface ActiveChatData {
37
37
  message: string;
38
38
  buttons: string[];
39
39
  } | null;
40
- terminalHistory?: string;
41
40
  inputContent?: string;
42
41
  }
43
42
  /** Standardized error reasons across all provider categories */
@@ -42,7 +42,6 @@ export interface ActiveChatData {
42
42
  status: string;
43
43
  messages: ChatMessage[];
44
44
  activeModal: { message: string; buttons: string[] } | null;
45
- terminalHistory?: string;
46
45
  inputContent?: string;
47
46
  }
48
47
 
@@ -103,8 +102,6 @@ export interface CliProviderState extends ProviderStateBase {
103
102
  category: 'cli';
104
103
  /** terminal = PTY stream, chat = parsed conversation */
105
104
  mode: 'terminal' | 'chat';
106
- /** Active launch mode id (e.g. 'chat') — undefined means default terminal */
107
- launchMode?: string;
108
105
  }
109
106
 
110
107
  /** ACP provider state */
@@ -102,9 +102,7 @@ export interface SessionEntry {
102
102
  runtimeKey?: string;
103
103
  runtimeDisplayName?: string;
104
104
  runtimeWorkspaceLabel?: string;
105
- /** CLI only: active launch mode id (e.g. 'terminal', 'chat') */
106
- launchMode?: string;
107
- /** CLI only: output rendering mode derived from launchMode.outputFormat */
105
+ /** CLI only: active presentation mode */
108
106
  mode?: 'terminal' | 'chat';
109
107
  runtimeWriteOwner?: RuntimeWriteOwner | null;
110
108
  runtimeAttachedClients?: RuntimeAttachedClient[];
@@ -167,6 +167,12 @@ const PTY_SESSION_CAPABILITIES: SessionCapability[] = [
167
167
  'resize_terminal',
168
168
  ];
169
169
 
170
+ const CLI_CHAT_SESSION_CAPABILITIES: SessionCapability[] = [
171
+ 'read_chat',
172
+ 'send_message',
173
+ 'resolve_action',
174
+ ];
175
+
170
176
  const ACP_SESSION_CAPABILITIES: SessionCapability[] = [
171
177
  'read_chat',
172
178
  'send_message',
@@ -265,11 +271,10 @@ function buildCliSession(state: CliProviderState): SessionEntry {
265
271
  runtimeWorkspaceLabel: state.runtime?.workspaceLabel,
266
272
  runtimeWriteOwner: state.runtime?.writeOwner || null,
267
273
  runtimeAttachedClients: state.runtime?.attachedClients || [],
268
- launchMode: state.launchMode,
269
274
  mode: state.mode,
270
275
  resume: state.resume,
271
276
  activeChat,
272
- capabilities: PTY_SESSION_CAPABILITIES,
277
+ capabilities: state.mode === 'terminal' ? PTY_SESSION_CAPABILITIES : CLI_CHAT_SESSION_CAPABILITIES,
273
278
  controlValues: state.controlValues,
274
279
  providerControls: buildFallbackControls(
275
280
  state.providerControls
@@ -20,6 +20,77 @@ const WORKING_STATUSES = new Set([
20
20
  'active',
21
21
  ]);
22
22
 
23
+ // Status snapshots are sent over P2P every 5s, so keep only a recent live window here.
24
+ // Older history is fetched on demand via `chat_history`, and CLI terminals stream via runtime events.
25
+ const STATUS_ACTIVE_CHAT_MESSAGE_LIMIT = 60;
26
+ const STATUS_ACTIVE_CHAT_TOTAL_BYTES_LIMIT = 96 * 1024;
27
+ const STATUS_ACTIVE_CHAT_STRING_LIMIT = 4 * 1024;
28
+ const STATUS_ACTIVE_CHAT_FALLBACK_STRING_LIMIT = 1024;
29
+ const STATUS_INPUT_CONTENT_LIMIT = 2 * 1024;
30
+ const STATUS_MODAL_MESSAGE_LIMIT = 2 * 1024;
31
+ const STATUS_MODAL_BUTTON_LIMIT = 120;
32
+
33
+ function truncateString(value: string, maxChars: number): string {
34
+ if (value.length <= maxChars) return value;
35
+ if (maxChars <= 12) return value.slice(0, Math.max(0, maxChars));
36
+ return `${value.slice(0, maxChars - 12)}...[truncated]`;
37
+ }
38
+
39
+ function truncateStringTail(value: string, maxChars: number): string {
40
+ if (value.length <= maxChars) return value;
41
+ if (maxChars <= 12) return value.slice(value.length - Math.max(0, maxChars));
42
+ return `...[truncated]${value.slice(value.length - (maxChars - 12))}`;
43
+ }
44
+
45
+ function trimStructuredStrings(value: unknown, maxChars: number): unknown {
46
+ if (typeof value === 'string') return truncateString(value, maxChars);
47
+ if (Array.isArray(value)) return value.map((item) => trimStructuredStrings(item, maxChars));
48
+ if (!value || typeof value !== 'object') return value;
49
+ return Object.fromEntries(
50
+ Object.entries(value).map(([key, nested]) => [key, trimStructuredStrings(nested, maxChars)]),
51
+ );
52
+ }
53
+
54
+ function estimateBytes(value: unknown): number {
55
+ try {
56
+ return JSON.stringify(value).length;
57
+ } catch {
58
+ return String(value ?? '').length;
59
+ }
60
+ }
61
+
62
+ function trimMessageForStatus(message: unknown, stringLimit: number): unknown {
63
+ if (!message || typeof message !== 'object') return message;
64
+ return trimStructuredStrings(message, stringLimit);
65
+ }
66
+
67
+ function trimMessagesForStatus(messages: unknown[] | null | undefined): unknown[] {
68
+ if (!Array.isArray(messages) || messages.length === 0) return [];
69
+
70
+ const recent = messages.slice(-STATUS_ACTIVE_CHAT_MESSAGE_LIMIT);
71
+ const kept: unknown[] = [];
72
+ let totalBytes = 0;
73
+
74
+ for (let i = recent.length - 1; i >= 0; i -= 1) {
75
+ let normalized = trimMessageForStatus(recent[i], STATUS_ACTIVE_CHAT_STRING_LIMIT);
76
+ let size = estimateBytes(normalized);
77
+
78
+ if (size > STATUS_ACTIVE_CHAT_TOTAL_BYTES_LIMIT) {
79
+ normalized = trimMessageForStatus(recent[i], STATUS_ACTIVE_CHAT_FALLBACK_STRING_LIMIT);
80
+ size = estimateBytes(normalized);
81
+ }
82
+
83
+ if (kept.length > 0 && (totalBytes + size) > STATUS_ACTIVE_CHAT_TOTAL_BYTES_LIMIT) {
84
+ continue;
85
+ }
86
+
87
+ kept.push(normalized);
88
+ totalBytes += size;
89
+ }
90
+
91
+ return kept.reverse();
92
+ }
93
+
23
94
  function hasApprovalButtons(activeModal?: { buttons?: unknown[] | null } | null): boolean {
24
95
  return (activeModal?.buttons?.length ?? 0) > 0;
25
96
  }
@@ -60,5 +131,15 @@ export function normalizeActiveChatData<T extends ActiveChatData | null | undefi
60
131
  return {
61
132
  ...activeChat,
62
133
  status: normalizeManagedStatus(activeChat.status, { activeModal: activeChat.activeModal }),
134
+ messages: trimMessagesForStatus(activeChat.messages) as T extends { messages: infer M } ? M : never,
135
+ activeModal: activeChat.activeModal ? {
136
+ message: truncateString(activeChat.activeModal.message || '', STATUS_MODAL_MESSAGE_LIMIT),
137
+ buttons: (activeChat.activeModal.buttons || []).map((button) =>
138
+ truncateString(String(button || ''), STATUS_MODAL_BUTTON_LIMIT)
139
+ ),
140
+ } : activeChat.activeModal,
141
+ inputContent: activeChat.inputContent
142
+ ? truncateString(activeChat.inputContent, STATUS_INPUT_CONTENT_LIMIT)
143
+ : activeChat.inputContent,
63
144
  } as T;
64
145
  }
@@ -113,6 +113,26 @@ export class DaemonStatusReporter {
113
113
  return new Date().toISOString().slice(11, 23); // HH:mm:ss.SSS
114
114
  }
115
115
 
116
+ private summarizeLargePayloadSessions(payload: Record<string, any>): string {
117
+ const sessions = Array.isArray(payload.sessions) ? payload.sessions : [];
118
+ return sessions
119
+ .map((session: any) => ({
120
+ id: String(session?.id || ''),
121
+ providerType: String(session?.providerType || ''),
122
+ bytes: (() => {
123
+ try {
124
+ return JSON.stringify(session).length;
125
+ } catch {
126
+ return 0;
127
+ }
128
+ })(),
129
+ }))
130
+ .sort((a, b) => b.bytes - a.bytes)
131
+ .slice(0, 3)
132
+ .map((session) => `${session.providerType || 'unknown'}:${session.id}=${session.bytes}b`)
133
+ .join(', ');
134
+ }
135
+
116
136
  async sendUnifiedStatusReport(opts?: { p2pOnly?: boolean }): Promise<void> {
117
137
  const { serverConn, p2p } = this.deps;
118
138
  if (!serverConn?.isConnected()) return;
@@ -179,10 +199,17 @@ export class DaemonStatusReporter {
179
199
  connectedExtensions: [],
180
200
  };
181
201
 
182
- // ═══ P2P transmit ═══
202
+ // ═══ P2P transmit ═══
203
+ const payloadBytes = JSON.stringify(payload).length;
183
204
  const p2pSent = this.sendP2PPayload(payload);
184
205
  if (p2pSent) {
185
- LOG.debug('P2P', `sent (${JSON.stringify(payload).length} bytes)`);
206
+ LOG.debug('P2P', `sent (${payloadBytes} bytes)`);
207
+ if (payloadBytes > 256 * 1024) {
208
+ LOG.warn(
209
+ 'P2P',
210
+ `large status payload (${payloadBytes} bytes) top sessions: ${this.summarizeLargePayloadSessions(payload) || 'n/a'}`,
211
+ );
212
+ }
186
213
  }
187
214
 
188
215
  // ═══ Server transmit (minimal routing meta only) ═══
@@ -215,6 +242,8 @@ export class DaemonStatusReporter {
215
242
  })),
216
243
  p2p: payload.p2p,
217
244
  timestamp: now,
245
+ detectedIdes: payload.detectedIdes,
246
+ availableProviders: payload.availableProviders,
218
247
  };
219
248
  serverConn.sendMessage('status_report', wsPayload);
220
249
  LOG.debug('Server', `sent status_report (${JSON.stringify(wsPayload).length} bytes)`);