@adhdev/daemon-core 0.7.40 → 0.7.42

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,7 +1,7 @@
1
1
  /**
2
2
  * ADHDev Launcher — Configuration
3
- *
4
- * Manages launcher config, server connection tokens, and user preferences.
3
+ *
4
+ * Manages launcher config, machine auth, and user preferences.
5
5
  */
6
6
 
7
7
  import { homedir } from 'os';
@@ -16,8 +16,6 @@ export type { RecentActivityEntry } from './recent-activity.js';
16
16
  export interface ADHDevConfig {
17
17
  // Server connection
18
18
  serverUrl: string;
19
- apiToken: string | null;
20
- connectionToken: string | null;
21
19
 
22
20
  // Selected IDE (primary)
23
21
  selectedIde: string | null;
@@ -28,16 +26,6 @@ export interface ADHDevConfig {
28
26
  // Installed extensions
29
27
  installedExtensions: string[];
30
28
 
31
- // User preferences
32
- autoConnect: boolean;
33
- /**
34
- * @deprecated Not read at runtime. Notification preferences are now managed by:
35
- * - Web UI layer: useNotificationPrefs (localStorage)
36
- * - Daemon layer: per-provider settings (approvalAlert, longGeneratingAlert)
37
- * Kept for backward config compat — will be removed in v0.7+.
38
- */
39
- notifications: boolean;
40
-
41
29
  // Auth
42
30
  userEmail: string | null;
43
31
  userName: string | null;
@@ -46,9 +34,6 @@ export interface ADHDevConfig {
46
34
  setupCompleted: boolean;
47
35
  setupDate: string | null;
48
36
 
49
- // Configured CLI agents
50
- configuredCLIs: string[];
51
-
52
37
  // Daemon: which IDEs to connect (empty = all)
53
38
  enabledIdes: string[];
54
39
 
@@ -70,20 +55,15 @@ export interface ADHDevConfig {
70
55
  /**
71
56
  * Stable local machine ID (prefix: `mach_`) — generated locally on first run.
72
57
  * Used as daemon instance key (`daemon_<machineId>`) and in status reports.
73
- * NOT the same as the server-side D1 `machines.id` — see `registeredMachineId`.
74
58
  */
75
59
  machineId?: string;
76
60
 
77
- // Machine secret for server auth (replaces connectionToken)
61
+ // Machine secret for server auth
78
62
  machineSecret?: string | null;
79
63
 
80
64
  /**
81
65
  * Server-side D1 `machines.id` — the row ID assigned when daemon registers via
82
- * `POST /cli/complete`. Corresponds to `machineId` in server DO context
83
- * (`DaemonConnection.machineId`, `StatusContext.machineId`).
84
- *
85
- * Naming differs from server-side `machineId` to avoid confusion with the local
86
- * `config.machineId` (mach_ prefix) which is a different value.
66
+ * `POST /cli/complete`. Used as fallback for machine lookup on re-auth.
87
67
  *
88
68
  * @deprecated Legacy bridge field — will be removed after 2026-04-06.
89
69
  * Modern auth flow uses `machineSecret` (adm_) to identify machines.
@@ -108,18 +88,13 @@ export interface ADHDevConfig {
108
88
 
109
89
  const DEFAULT_CONFIG: ADHDevConfig = {
110
90
  serverUrl: 'https://api.adhf.dev',
111
- apiToken: null,
112
- connectionToken: null,
113
91
  selectedIde: null,
114
92
  configuredIdes: [],
115
93
  installedExtensions: [],
116
- autoConnect: true,
117
- notifications: true,
118
94
  userEmail: null,
119
95
  userName: null,
120
96
  setupCompleted: false,
121
97
  setupDate: null,
122
- configuredCLIs: [],
123
98
  enabledIdes: [],
124
99
  workspaces: [],
125
100
  defaultWorkspaceId: null,
@@ -137,6 +112,68 @@ const DEFAULT_CONFIG: ADHDevConfig = {
137
112
 
138
113
  const MACHINE_ID_PREFIX = 'mach_';
139
114
 
115
+ function isPlainObject(value: unknown): value is Record<string, any> {
116
+ return !!value && typeof value === 'object' && !Array.isArray(value);
117
+ }
118
+
119
+ function asStringArray(value: unknown): string[] {
120
+ if (!Array.isArray(value)) return [];
121
+ return value.filter((item): item is string => typeof item === 'string');
122
+ }
123
+
124
+ function asNullableString(value: unknown): string | null {
125
+ return typeof value === 'string' ? value : null;
126
+ }
127
+
128
+ function asOptionalString(value: unknown): string | undefined {
129
+ return typeof value === 'string' && value.trim() ? value : undefined;
130
+ }
131
+
132
+ function asBoolean(value: unknown, fallback: boolean): boolean {
133
+ return typeof value === 'boolean' ? value : fallback;
134
+ }
135
+
136
+ function normalizeConfig(raw: unknown): ADHDevConfig & { activeWorkspaceId?: string | null } {
137
+ const parsed = isPlainObject(raw) ? raw : {};
138
+ const legacySessionReads = isPlainObject(parsed.recentSessionReads) ? parsed.recentSessionReads : {};
139
+ const sessionReads = isPlainObject(parsed.sessionReads) ? parsed.sessionReads : {};
140
+ const mergedSessionReads = Object.fromEntries(
141
+ Object.entries({ ...legacySessionReads, ...sessionReads })
142
+ .filter(([, value]) => typeof value === 'number' && Number.isFinite(value))
143
+ );
144
+ const sessionReadMarkers = Object.fromEntries(
145
+ Object.entries(isPlainObject(parsed.sessionReadMarkers) ? parsed.sessionReadMarkers : {})
146
+ .filter(([, value]) => typeof value === 'string')
147
+ );
148
+
149
+ return {
150
+ serverUrl: typeof parsed.serverUrl === 'string' && parsed.serverUrl.trim()
151
+ ? parsed.serverUrl
152
+ : DEFAULT_CONFIG.serverUrl,
153
+ selectedIde: asNullableString(parsed.selectedIde),
154
+ configuredIdes: asStringArray(parsed.configuredIdes),
155
+ installedExtensions: asStringArray(parsed.installedExtensions),
156
+ userEmail: asNullableString(parsed.userEmail),
157
+ userName: asNullableString(parsed.userName),
158
+ setupCompleted: asBoolean(parsed.setupCompleted, DEFAULT_CONFIG.setupCompleted),
159
+ setupDate: asNullableString(parsed.setupDate),
160
+ enabledIdes: asStringArray(parsed.enabledIdes),
161
+ workspaces: Array.isArray(parsed.workspaces) ? parsed.workspaces as WorkspaceEntry[] : [],
162
+ defaultWorkspaceId: asNullableString(parsed.defaultWorkspaceId) ?? asNullableString(parsed.activeWorkspaceId),
163
+ recentActivity: Array.isArray(parsed.recentActivity) ? parsed.recentActivity as RecentActivityEntry[] : [],
164
+ sessionReads: mergedSessionReads,
165
+ sessionReadMarkers,
166
+ machineNickname: asNullableString(parsed.machineNickname),
167
+ machineId: asOptionalString(parsed.machineId),
168
+ machineSecret: parsed.machineSecret === null ? null : asOptionalString(parsed.machineSecret),
169
+ registeredMachineId: asOptionalString(parsed.registeredMachineId),
170
+ providerSettings: isPlainObject(parsed.providerSettings) ? parsed.providerSettings : {},
171
+ ideSettings: isPlainObject(parsed.ideSettings) ? parsed.ideSettings : {},
172
+ disableUpstream: asBoolean(parsed.disableUpstream, DEFAULT_CONFIG.disableUpstream ?? false),
173
+ providerDir: asOptionalString(parsed.providerDir),
174
+ };
175
+ }
176
+
140
177
  export function generateMachineId(): string {
141
178
  return `${MACHINE_ID_PREFIX}${randomUUID().replace(/-/g, '')}`;
142
179
  }
@@ -201,14 +238,10 @@ export function loadConfig(): ADHDevConfig {
201
238
  try {
202
239
  const raw = readFileSync(configPath, 'utf-8');
203
240
  const parsed = JSON.parse(raw);
204
- const merged = { ...DEFAULT_CONFIG, ...parsed } as ADHDevConfig & { activeWorkspaceId?: string | null };
205
- if (merged.defaultWorkspaceId == null && merged.activeWorkspaceId != null) {
206
- (merged as ADHDevConfig).defaultWorkspaceId = merged.activeWorkspaceId;
207
- }
208
- delete (merged as any).activeWorkspaceId;
209
- const ensured = ensureMachineId(merged);
241
+ const normalizedInput = normalizeConfig(parsed);
242
+ const ensured = ensureMachineId(normalizedInput);
210
243
  const normalized = ensured.config as ADHDevConfig & { activeWorkspaceId?: string | null };
211
- if (ensured.changed) {
244
+ if (ensured.changed || JSON.stringify(parsed) !== JSON.stringify(normalized)) {
212
245
  try {
213
246
  saveConfig(normalized);
214
247
  } catch { /* ignore */ }
@@ -226,12 +259,13 @@ export function loadConfig(): ADHDevConfig {
226
259
  export function saveConfig(config: ADHDevConfig): void {
227
260
  const configPath = getConfigPath();
228
261
  const dir = getConfigDir();
262
+ const normalized = normalizeConfig(config);
229
263
 
230
264
  if (!existsSync(dir)) {
231
265
  mkdirSync(dir, { recursive: true, mode: 0o700 });
232
266
  }
233
267
 
234
- writeFileSync(configPath, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });
268
+ writeFileSync(configPath, JSON.stringify(normalized, null, 2), { encoding: 'utf-8', mode: 0o600 });
235
269
  try { chmodSync(configPath, 0o600); } catch { /* Windows etc. not supported */ }
236
270
  }
237
271
 
@@ -276,15 +310,3 @@ export function isSetupComplete(): boolean {
276
310
  export function resetConfig(): void {
277
311
  saveConfig({ ...DEFAULT_CONFIG });
278
312
  }
279
-
280
- /**
281
- * Generate a connection token for server authentication
282
- */
283
- export function generateConnectionToken(): string {
284
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
285
- let token = 'db_';
286
- for (let i = 0; i < 32; i++) {
287
- token += chars.charAt(Math.floor(Math.random() * chars.length));
288
- }
289
- return token;
290
- }
@@ -17,10 +17,7 @@ export declare function validateWorkspacePath(absPath: string): {
17
17
  };
18
18
  /** Default workspace label from path */
19
19
  export declare function defaultWorkspaceLabel(absPath: string): string;
20
- /**
21
- * Ensure config.workspaces exists; seed from recentCliWorkspaces once (same paths).
22
- */
23
- export declare function migrateWorkspacesFromRecent(config: ADHDevConfig): ADHDevConfig;
20
+
24
21
  export declare function getDefaultWorkspacePath(config: ADHDevConfig): string | null;
25
22
  export declare function getWorkspaceState(config: ADHDevConfig): {
26
23
  workspaces: WorkspaceEntry[];
@@ -33,6 +33,8 @@ export class CliProviderInstance implements ProviderInstance {
33
33
  private historyWriter: ChatHistoryWriter;
34
34
  readonly instanceId: string;
35
35
 
36
+ private presentationMode: 'terminal' | 'chat';
37
+
36
38
  constructor(
37
39
  private provider: ProviderModule,
38
40
  private workingDir: string,
@@ -42,6 +44,7 @@ export class CliProviderInstance implements ProviderInstance {
42
44
  ) {
43
45
  this.type = provider.type;
44
46
  this.instanceId = instanceId || crypto.randomUUID();
47
+ this.presentationMode = 'terminal';
45
48
  this.adapter = new ProviderCliAdapter(provider as any as CliProviderModule, workingDir, cliArgs, transportFactory);
46
49
  this.monitor = new StatusMonitor();
47
50
  this.historyWriter = new ChatHistoryWriter();
@@ -84,6 +87,7 @@ export class CliProviderInstance implements ProviderInstance {
84
87
 
85
88
  getState(): ProviderState {
86
89
  const adapterStatus = this.adapter.getStatus();
90
+ const parsedStatus = this.adapter.getScriptParsedStatus?.() || null;
87
91
  const runtime = this.adapter.getRuntimeMetadata();
88
92
 
89
93
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
@@ -102,13 +106,13 @@ export class CliProviderInstance implements ProviderInstance {
102
106
  name: this.provider.name,
103
107
  category: 'cli',
104
108
  status: adapterStatus.status,
105
- mode: 'terminal',
109
+ mode: this.presentationMode,
106
110
  activeChat: {
107
111
  id: `${this.type}_${this.workingDir}`,
108
- title: `${this.provider.name} · ${dirName}`,
109
- status: adapterStatus.status,
110
- messages: [],
111
- activeModal: adapterStatus.activeModal,
112
+ title: parsedStatus?.title || `${this.provider.name} · ${dirName}`,
113
+ status: parsedStatus?.status || adapterStatus.status,
114
+ messages: Array.isArray(parsedStatus?.messages) ? parsedStatus.messages : [],
115
+ activeModal: parsedStatus?.activeModal ?? adapterStatus.activeModal,
112
116
  terminalHistory: adapterStatus.terminalHistory,
113
117
  inputContent: '',
114
118
  },
@@ -131,6 +135,15 @@ export class CliProviderInstance implements ProviderInstance {
131
135
  };
132
136
  }
133
137
 
138
+ setPresentationMode(mode: 'terminal' | 'chat'): void {
139
+ if (this.presentationMode === mode) return;
140
+ this.presentationMode = mode;
141
+ }
142
+
143
+ getPresentationMode(): 'terminal' | 'chat' {
144
+ return this.presentationMode;
145
+ }
146
+
134
147
  onEvent(event: string, data?: any): void {
135
148
  if (event === 'send_message' && data?.text) {
136
149
  void this.adapter.sendMessage(data.text).catch((e: any) => {
@@ -608,4 +608,3 @@ export interface ProviderControlDef {
608
608
  /** Hide this control when condition not met */
609
609
  hidden?: boolean;
610
610
  }
611
-
@@ -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
 
@@ -102,6 +102,8 @@ export interface SessionEntry {
102
102
  runtimeKey?: string;
103
103
  runtimeDisplayName?: string;
104
104
  runtimeWorkspaceLabel?: string;
105
+ /** CLI only: active presentation mode */
106
+ mode?: 'terminal' | 'chat';
105
107
  runtimeWriteOwner?: RuntimeWriteOwner | null;
106
108
  runtimeAttachedClients?: RuntimeAttachedClient[];
107
109
  resume?: ProviderResumeCapability;
@@ -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,9 +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 || [],
274
+ mode: state.mode,
268
275
  resume: state.resume,
269
276
  activeChat,
270
- capabilities: PTY_SESSION_CAPABILITIES,
277
+ capabilities: state.mode === 'terminal' ? PTY_SESSION_CAPABILITIES : CLI_CHAT_SESSION_CAPABILITIES,
271
278
  controlValues: state.controlValues,
272
279
  providerControls: buildFallbackControls(
273
280
  state.providerControls
@@ -20,6 +20,78 @@ 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_TERMINAL_HISTORY_LIMIT = 8 * 1024;
30
+ const STATUS_INPUT_CONTENT_LIMIT = 2 * 1024;
31
+ const STATUS_MODAL_MESSAGE_LIMIT = 2 * 1024;
32
+ const STATUS_MODAL_BUTTON_LIMIT = 120;
33
+
34
+ function truncateString(value: string, maxChars: number): string {
35
+ if (value.length <= maxChars) return value;
36
+ if (maxChars <= 12) return value.slice(0, Math.max(0, maxChars));
37
+ return `${value.slice(0, maxChars - 12)}...[truncated]`;
38
+ }
39
+
40
+ function truncateStringTail(value: string, maxChars: number): string {
41
+ if (value.length <= maxChars) return value;
42
+ if (maxChars <= 12) return value.slice(value.length - Math.max(0, maxChars));
43
+ return `...[truncated]${value.slice(value.length - (maxChars - 12))}`;
44
+ }
45
+
46
+ function trimStructuredStrings(value: unknown, maxChars: number): unknown {
47
+ if (typeof value === 'string') return truncateString(value, maxChars);
48
+ if (Array.isArray(value)) return value.map((item) => trimStructuredStrings(item, maxChars));
49
+ if (!value || typeof value !== 'object') return value;
50
+ return Object.fromEntries(
51
+ Object.entries(value).map(([key, nested]) => [key, trimStructuredStrings(nested, maxChars)]),
52
+ );
53
+ }
54
+
55
+ function estimateBytes(value: unknown): number {
56
+ try {
57
+ return JSON.stringify(value).length;
58
+ } catch {
59
+ return String(value ?? '').length;
60
+ }
61
+ }
62
+
63
+ function trimMessageForStatus(message: unknown, stringLimit: number): unknown {
64
+ if (!message || typeof message !== 'object') return message;
65
+ return trimStructuredStrings(message, stringLimit);
66
+ }
67
+
68
+ function trimMessagesForStatus(messages: unknown[] | null | undefined): unknown[] {
69
+ if (!Array.isArray(messages) || messages.length === 0) return [];
70
+
71
+ const recent = messages.slice(-STATUS_ACTIVE_CHAT_MESSAGE_LIMIT);
72
+ const kept: unknown[] = [];
73
+ let totalBytes = 0;
74
+
75
+ for (let i = recent.length - 1; i >= 0; i -= 1) {
76
+ let normalized = trimMessageForStatus(recent[i], STATUS_ACTIVE_CHAT_STRING_LIMIT);
77
+ let size = estimateBytes(normalized);
78
+
79
+ if (size > STATUS_ACTIVE_CHAT_TOTAL_BYTES_LIMIT) {
80
+ normalized = trimMessageForStatus(recent[i], STATUS_ACTIVE_CHAT_FALLBACK_STRING_LIMIT);
81
+ size = estimateBytes(normalized);
82
+ }
83
+
84
+ if (kept.length > 0 && (totalBytes + size) > STATUS_ACTIVE_CHAT_TOTAL_BYTES_LIMIT) {
85
+ continue;
86
+ }
87
+
88
+ kept.push(normalized);
89
+ totalBytes += size;
90
+ }
91
+
92
+ return kept.reverse();
93
+ }
94
+
23
95
  function hasApprovalButtons(activeModal?: { buttons?: unknown[] | null } | null): boolean {
24
96
  return (activeModal?.buttons?.length ?? 0) > 0;
25
97
  }
@@ -60,5 +132,18 @@ export function normalizeActiveChatData<T extends ActiveChatData | null | undefi
60
132
  return {
61
133
  ...activeChat,
62
134
  status: normalizeManagedStatus(activeChat.status, { activeModal: activeChat.activeModal }),
135
+ messages: trimMessagesForStatus(activeChat.messages) as T extends { messages: infer M } ? M : never,
136
+ activeModal: activeChat.activeModal ? {
137
+ message: truncateString(activeChat.activeModal.message || '', STATUS_MODAL_MESSAGE_LIMIT),
138
+ buttons: (activeChat.activeModal.buttons || []).map((button) =>
139
+ truncateString(String(button || ''), STATUS_MODAL_BUTTON_LIMIT)
140
+ ),
141
+ } : activeChat.activeModal,
142
+ terminalHistory: activeChat.terminalHistory
143
+ ? truncateStringTail(activeChat.terminalHistory, STATUS_TERMINAL_HISTORY_LIMIT)
144
+ : activeChat.terminalHistory,
145
+ inputContent: activeChat.inputContent
146
+ ? truncateString(activeChat.inputContent, STATUS_INPUT_CONTENT_LIMIT)
147
+ : activeChat.inputContent,
63
148
  } as T;
64
149
  }
@@ -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)`);