@adhdev/daemon-core 0.8.81 → 0.8.83

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 (36) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -0
  2. package/dist/cli-adapters/provider-cli-shared.d.ts +2 -0
  3. package/dist/config/recent-activity.d.ts +14 -0
  4. package/dist/config/state-store.d.ts +4 -0
  5. package/dist/index.js +363 -117
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +363 -117
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/providers/acp-provider-instance.d.ts +0 -2
  10. package/dist/providers/cli-provider-instance.d.ts +2 -0
  11. package/dist/providers/provider-instance.d.ts +1 -1
  12. package/dist/shared-types.d.ts +3 -1
  13. package/dist/status/chat-tail-hot-sessions.d.ts +2 -0
  14. package/dist/status/snapshot.d.ts +1 -0
  15. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  16. package/package.json +1 -1
  17. package/src/cli-adapter-types.d.ts +2 -0
  18. package/src/cli-adapters/provider-cli-adapter.ts +107 -30
  19. package/src/cli-adapters/provider-cli-shared.d.ts +3 -4
  20. package/src/cli-adapters/provider-cli-shared.ts +2 -0
  21. package/src/cli-adapters/terminal-screen.ts +6 -4
  22. package/src/commands/chat-commands.ts +8 -3
  23. package/src/commands/router.ts +59 -1
  24. package/src/config/recent-activity.ts +122 -0
  25. package/src/config/state-store.ts +16 -0
  26. package/src/logging/command-log.ts +2 -0
  27. package/src/providers/acp-provider-instance.ts +2 -17
  28. package/src/providers/cli-provider-instance.ts +33 -10
  29. package/src/providers/extension-provider-instance.ts +0 -2
  30. package/src/providers/ide-provider-instance.ts +0 -2
  31. package/src/providers/provider-instance.d.ts +1 -1
  32. package/src/providers/provider-instance.ts +1 -0
  33. package/src/shared-types.d.ts +3 -0
  34. package/src/shared-types.ts +5 -1
  35. package/src/status/chat-tail-hot-sessions.ts +15 -1
  36. package/src/status/snapshot.ts +20 -3
@@ -91,8 +91,6 @@ export declare class AcpProviderInstance implements ProviderInstance {
91
91
  private handleLegacyUpdate;
92
92
  /** Map SDK ToolCallStatus to internal status */
93
93
  private mapToolCallStatus;
94
- /** Truncate content for transport (text: 2000 chars, images preserved) */
95
- private truncateContent;
96
94
  /** Build ContentBlock[] from current partial state */
97
95
  private buildPartialBlocks;
98
96
  private buildPartialThoughtMessage;
@@ -41,6 +41,8 @@ export declare class CliProviderInstance implements ProviderInstance {
41
41
  private runtimeMessages;
42
42
  readonly instanceId: string;
43
43
  private suppressIdleHistoryReplay;
44
+ private errorMessage;
45
+ private errorReason;
44
46
  private presentationMode;
45
47
  private providerSessionId?;
46
48
  private launchMode;
@@ -44,7 +44,7 @@ export interface ActiveChatData {
44
44
  inputContent?: string;
45
45
  }
46
46
  /** Standardized error reasons across all provider categories */
47
- export type ProviderErrorReason = 'not_installed' | 'auth_failed' | 'spawn_error' | 'init_failed' | 'crash' | 'timeout' | 'cdp_error' | 'disconnected';
47
+ export type ProviderErrorReason = 'not_installed' | 'auth_failed' | 'spawn_error' | 'init_failed' | 'parse_error' | 'crash' | 'timeout' | 'cdp_error' | 'disconnected';
48
48
  /** Common fields shared by all provider categories */
49
49
  interface ProviderStateBase {
50
50
  /** Provider type (e.g. 'gemini-cli', 'cursor', 'cline') */
@@ -225,7 +225,7 @@ export type UnsubscribeRequest = {
225
225
  export type StandaloneWsStatusPayload = StatusReportPayload;
226
226
  export type SessionTransport = 'cdp-page' | 'cdp-webview' | 'pty' | 'acp';
227
227
  export type SessionKind = 'workspace' | 'agent';
228
- export type SessionCapability = 'read_chat' | 'send_message' | 'new_session' | 'list_sessions' | 'switch_session' | 'resolve_action' | 'open_panel' | 'terminal_io' | 'resize_terminal' | 'change_model' | 'set_mode' | 'set_thought_level';
228
+ export type SessionCapability = 'read_chat' | 'send_message' | 'new_session' | 'list_sessions' | 'switch_session' | 'resolve_action' | 'open_panel' | 'terminal_io' | 'resize_terminal' | 'change_model' | 'set_mode' | 'set_thought_level' | 'delete_notification' | 'mark_notification_unread';
229
229
  import type { RuntimeWriteOwner, RuntimeAttachedClient, SessionStatus } from './shared-types-extra.js';
230
230
  export type { RuntimeWriteOwner, RuntimeAttachedClient, SessionStatus } from './shared-types-extra.js';
231
231
  export interface SessionEntry {
@@ -270,6 +270,8 @@ export interface SessionEntry {
270
270
  unread?: boolean;
271
271
  lastSeenAt?: number;
272
272
  inboxBucket?: RecentSessionBucket;
273
+ completionMarker?: string;
274
+ seenCompletionMarker?: string;
273
275
  surfaceHidden?: boolean;
274
276
  }
275
277
  /**
@@ -3,6 +3,8 @@ export declare const DEFAULT_CHAT_TAIL_RECENT_MESSAGE_GRACE_MS = 8000;
3
3
  export interface HotChatSessionLike {
4
4
  id?: string | null;
5
5
  status?: unknown;
6
+ unread?: unknown;
7
+ inboxBucket?: unknown;
6
8
  lastMessageAt?: unknown;
7
9
  runtimeLifecycle?: unknown;
8
10
  runtimeSurfaceKind?: unknown;
@@ -59,6 +59,7 @@ export interface RecentReadDebugSnapshot {
59
59
  }
60
60
  export declare function shouldEmitRecentReadDebugLog(cache: Map<string, string>, snapshot: RecentReadDebugSnapshot): boolean;
61
61
  export declare function buildMachineInfo(profile?: 'full' | 'live' | 'metadata'): MachineInfo;
62
+ export { getSessionCurrentNotificationId, applySessionNotificationOverlay } from '../config/recent-activity.js';
62
63
  export declare function getSessionCompletionMarker(session: {
63
64
  activeChat?: {
64
65
  messages?: Array<{
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.8.81",
3
+ "version": "0.8.83",
4
4
  "description": "ADHDev local session host core \u2014 session registry, protocol, buffers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.8.81",
3
+ "version": "0.8.83",
4
4
  "description": "ADHDev daemon core \u2014 CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -7,6 +7,8 @@ import type { ChatMessage } from './types.js';
7
7
  export interface CliAdapterStatus {
8
8
  status?: string;
9
9
  messages?: ChatMessage[];
10
+ errorMessage?: string;
11
+ errorReason?: string;
10
12
  activeModal?: {
11
13
  message: string;
12
14
  buttons: string[];
@@ -17,6 +17,7 @@
17
17
  import * as os from 'os';
18
18
  import type { CliAdapter } from '../cli-adapter-types.js';
19
19
  import { LOG } from '../logging/logger.js';
20
+ import { getDebugRuntimeConfig } from '../logging/debug-config.js';
20
21
  import { TerminalScreen } from './terminal-screen.js';
21
22
  import {
22
23
  NodePtyTransportFactory,
@@ -113,6 +114,7 @@ export class ProviderCliAdapter implements CliAdapter {
113
114
  private recentOutputBuffer = '';
114
115
  private isWaitingForResponse = false;
115
116
  private activeModal: { message: string; buttons: string[] } | null = null;
117
+ private parseErrorMessage: string | null = null;
116
118
  private responseTimeout: NodeJS.Timeout | null = null;
117
119
  private idleTimeout: NodeJS.Timeout | null = null;
118
120
  private ready = false;
@@ -181,8 +183,23 @@ export class ProviderCliAdapter implements CliAdapter {
181
183
  private currentTurnScope: TurnParseScope | null = null;
182
184
  private traceEntries: CliTraceEntry[] = [];
183
185
  private traceSeq = 0;
184
- private traceSessionId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
186
+ private traceSessionId = '';
187
+ private parsedStatusCache: {
188
+ committedMessagesRef: CliChatMessage[];
189
+ responseBuffer: string;
190
+ currentTurnScope: TurnParseScope | null;
191
+ recentOutputBuffer: string;
192
+ accumulatedBuffer: string;
193
+ accumulatedRawBuffer: string;
194
+ screenText: string;
195
+ currentStatus: CliSessionStatus['status'];
196
+ activeModal: { message: string; buttons: string[] } | null;
197
+ cliName: string;
198
+ lastOutputAt: number;
199
+ result: any;
200
+ } | null = null;
185
201
  private static readonly MAX_TRACE_ENTRIES = 250;
202
+
186
203
  private readonly providerResolutionMeta: ProviderResolutionMeta;
187
204
  private static readonly FINISH_RETRY_DELAY_MS = 300;
188
205
  private static readonly MAX_FINISH_RETRIES = 2;
@@ -322,7 +339,19 @@ export class ProviderCliAdapter implements CliAdapter {
322
339
  `[${this.cliType}] Provider resolution: providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'} source=${this.providerResolutionMeta.scriptsSource || '-'} version=${this.providerResolutionMeta.resolvedVersion || '-'}`
323
340
  );
324
341
  } else {
325
- LOG.warn('CLI', `[${this.cliType}] No CLI scripts loaded! Provider needs scripts/{version}/scripts.js`);
342
+ const resolutionSummary = `providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'} source=${this.providerResolutionMeta.scriptsSource || '-'} version=${this.providerResolutionMeta.resolvedVersion || '-'}`;
343
+ const hasResolvedProviderScripts = Boolean(
344
+ this.providerResolutionMeta.providerDir
345
+ || this.providerResolutionMeta.scriptDir
346
+ || this.providerResolutionMeta.scriptsPath
347
+ || this.providerResolutionMeta.scriptsSource
348
+ || this.providerResolutionMeta.resolvedVersion,
349
+ );
350
+ if (hasResolvedProviderScripts) {
351
+ LOG.warn('CLI', `[${this.cliType}] ⚠ No CLI scripts loaded! Provider needs scripts/{version}/scripts.js (${resolutionSummary})`);
352
+ } else {
353
+ LOG.info('CLI', `[${this.cliType}] CLI scripts not yet resolved (${resolutionSummary})`);
354
+ }
326
355
  }
327
356
  }
328
357
 
@@ -484,7 +513,8 @@ export class ProviderCliAdapter implements CliAdapter {
484
513
  this.terminalScreen.write(rawData);
485
514
  const cleanData = sanitizeTerminalText(rawData);
486
515
  const now = Date.now();
487
- const normalizedScreenSnapshot = normalizeScreenSnapshot(this.terminalScreen.getText());
516
+ const screenText = this.terminalScreen.getText();
517
+ const normalizedScreenSnapshot = normalizeScreenSnapshot(screenText);
488
518
  this.lastOutputAt = now;
489
519
  if (cleanData.trim()) this.lastNonEmptyOutputAt = now;
490
520
  if (normalizedScreenSnapshot !== this.lastScreenSnapshot) {
@@ -497,13 +527,14 @@ export class ProviderCliAdapter implements CliAdapter {
497
527
  if (this.idleFinishCandidate && (rawData.length > 0 || cleanData.length > 0)) {
498
528
  this.clearIdleFinishCandidate('new_output');
499
529
  }
500
- this.recordTrace('output', {
501
- rawLength: rawData.length,
502
- cleanLength: cleanData.length,
503
- rawPreview: summarizeCliTraceText(rawData, 300),
504
- cleanPreview: summarizeCliTraceText(cleanData, 300),
505
- screenText: summarizeCliTraceText(this.terminalScreen.getText(), 1200),
506
- });
530
+ if (getDebugRuntimeConfig().collectDebugTrace) {
531
+ this.recordTrace('output', {
532
+ rawLength: rawData.length,
533
+ cleanLength: cleanData.length,
534
+ rawPreview: summarizeCliTraceText(rawData, 300),
535
+ cleanPreview: summarizeCliTraceText(cleanData, 300),
536
+ });
537
+ }
507
538
 
508
539
  if (this.startupParseGate) {
509
540
  this.scheduleStartupSettleCheck();
@@ -1267,10 +1298,12 @@ export class ProviderCliAdapter implements CliAdapter {
1267
1298
 
1268
1299
  getStatus(): CliSessionStatus {
1269
1300
  return {
1270
- status: this.currentStatus,
1301
+ status: this.parseErrorMessage ? 'error' : this.currentStatus,
1271
1302
  messages: [...this.committedMessages],
1272
1303
  workingDir: this.workingDir,
1273
1304
  activeModal: this.activeModal,
1305
+ errorMessage: this.parseErrorMessage || undefined,
1306
+ errorReason: this.parseErrorMessage ? 'parse_error' : undefined,
1274
1307
  };
1275
1308
  }
1276
1309
 
@@ -1301,15 +1334,36 @@ export class ProviderCliAdapter implements CliAdapter {
1301
1334
  * Called by command handler / dashboard for rich content rendering.
1302
1335
  */
1303
1336
  getScriptParsedStatus(): any {
1337
+ const screenText = this.terminalScreen.getText();
1338
+ const cached = this.parsedStatusCache;
1339
+ if (
1340
+ cached
1341
+ && cached.committedMessagesRef === this.committedMessages
1342
+ && cached.responseBuffer === this.responseBuffer
1343
+ && cached.currentTurnScope === this.currentTurnScope
1344
+ && cached.recentOutputBuffer === this.recentOutputBuffer
1345
+ && cached.accumulatedBuffer === this.accumulatedBuffer
1346
+ && cached.accumulatedRawBuffer === this.accumulatedRawBuffer
1347
+ && cached.screenText === screenText
1348
+ && cached.currentStatus === this.currentStatus
1349
+ && cached.activeModal === this.activeModal
1350
+ && cached.cliName === this.cliName
1351
+ && cached.lastOutputAt === this.lastOutputAt
1352
+ ) {
1353
+ return cached.result;
1354
+ }
1355
+
1304
1356
  const parsed = this.parseCurrentTranscript(
1305
1357
  this.committedMessages,
1306
1358
  this.responseBuffer,
1307
1359
  this.currentTurnScope,
1360
+ screenText,
1308
1361
  );
1309
1362
  const shouldPreferCommittedMessages =
1310
1363
  !this.currentTurnScope
1311
1364
  && this.currentStatus === 'idle'
1312
1365
  && !this.activeModal;
1366
+ let result: any;
1313
1367
  if (parsed && Array.isArray(parsed.messages)) {
1314
1368
  const hydratedMessages = shouldPreferCommittedMessages
1315
1369
  ? this.committedMessages.map((message, index) => buildChatMessage({
@@ -1325,7 +1379,7 @@ export class ProviderCliAdapter implements CliAdapter {
1325
1379
  scope: this.currentTurnScope,
1326
1380
  lastOutputAt: this.lastOutputAt,
1327
1381
  });
1328
- return {
1382
+ result = {
1329
1383
  id: parsed.id || 'cli_session',
1330
1384
  status: parsed.status || this.currentStatus,
1331
1385
  title: parsed.title || this.cliName,
@@ -1333,23 +1387,39 @@ export class ProviderCliAdapter implements CliAdapter {
1333
1387
  activeModal: parsed.activeModal ?? this.activeModal,
1334
1388
  providerSessionId: typeof parsed.providerSessionId === 'string' ? parsed.providerSessionId : undefined,
1335
1389
  };
1390
+ } else {
1391
+ const messages = [...this.committedMessages];
1392
+ result = {
1393
+ id: 'cli_session',
1394
+ status: this.currentStatus,
1395
+ title: this.cliName,
1396
+ messages: messages.map((message, index) => buildChatMessage({
1397
+ ...message,
1398
+ id: message.id || `msg_${index}`,
1399
+ index: typeof message.index === 'number' ? message.index : index,
1400
+ receivedAt: typeof message.receivedAt === 'number'
1401
+ ? message.receivedAt
1402
+ : message.timestamp,
1403
+ })),
1404
+ activeModal: this.activeModal,
1405
+ };
1336
1406
  }
1337
1407
 
1338
- const messages = [...this.committedMessages];
1339
- return {
1340
- id: 'cli_session',
1341
- status: this.currentStatus,
1342
- title: this.cliName,
1343
- messages: messages.slice(-50).map((message, index) => buildChatMessage({
1344
- ...message,
1345
- id: message.id || `msg_${index}`,
1346
- index: typeof message.index === 'number' ? message.index : index,
1347
- receivedAt: typeof message.receivedAt === 'number'
1348
- ? message.receivedAt
1349
- : message.timestamp,
1350
- })),
1408
+ this.parsedStatusCache = {
1409
+ committedMessagesRef: this.committedMessages,
1410
+ responseBuffer: this.responseBuffer,
1411
+ currentTurnScope: this.currentTurnScope,
1412
+ recentOutputBuffer: this.recentOutputBuffer,
1413
+ accumulatedBuffer: this.accumulatedBuffer,
1414
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
1415
+ screenText,
1416
+ currentStatus: this.currentStatus,
1351
1417
  activeModal: this.activeModal,
1418
+ cliName: this.cliName,
1419
+ lastOutputAt: this.lastOutputAt,
1420
+ result,
1352
1421
  };
1422
+ return result;
1353
1423
  }
1354
1424
 
1355
1425
  async invokeScript(scriptName: string, args?: Record<string, any>): Promise<any> {
@@ -1374,14 +1444,18 @@ export class ProviderCliAdapter implements CliAdapter {
1374
1444
  }));
1375
1445
  }
1376
1446
 
1377
- private parseCurrentTranscript(baseMessages: CliChatMessage[], partialResponse: string, scope?: TurnParseScope | null): any {
1378
- if (!this.cliScripts?.parseOutput) return null;
1447
+ private parseCurrentTranscript(baseMessages: CliChatMessage[], partialResponse: string, scope?: TurnParseScope | null, screenTextOverride?: string): any {
1448
+ if (!this.cliScripts?.parseOutput) {
1449
+ this.parseErrorMessage = null;
1450
+ return null;
1451
+ }
1379
1452
  try {
1453
+ const screenText = typeof screenTextOverride === 'string' ? screenTextOverride : this.terminalScreen.getText();
1380
1454
  const input = buildCliParseInput({
1381
1455
  accumulatedBuffer: this.accumulatedBuffer,
1382
1456
  accumulatedRawBuffer: this.accumulatedRawBuffer,
1383
1457
  recentOutputBuffer: this.recentOutputBuffer,
1384
- terminalScreenText: this.terminalScreen.getText(),
1458
+ terminalScreenText: screenText,
1385
1459
  baseMessages,
1386
1460
  partialResponse,
1387
1461
  isWaitingForResponse: this.isWaitingForResponse,
@@ -1403,10 +1477,13 @@ export class ProviderCliAdapter implements CliAdapter {
1403
1477
  lastAssistant.content = trimPromptEchoPrefix(lastAssistant.content, promptForTrim);
1404
1478
  }
1405
1479
  }
1480
+ this.parseErrorMessage = null;
1406
1481
  return parsed;
1407
1482
  } catch (e: any) {
1408
- LOG.warn('CLI', `[${this.cliType}] parseOutput error: ${e.message}`);
1409
- return null;
1483
+ const message = e?.message || String(e);
1484
+ this.parseErrorMessage = message;
1485
+ LOG.warn('CLI', `[${this.cliType}] parseOutput error: ${message}`);
1486
+ throw e;
1410
1487
  }
1411
1488
  }
1412
1489
 
@@ -15,10 +15,9 @@ export interface CliSessionStatus {
15
15
  status: 'idle' | 'generating' | 'waiting_approval' | 'error' | 'stopped' | 'starting';
16
16
  messages: CliChatMessage[];
17
17
  workingDir: string;
18
- activeModal: {
19
- message: string;
20
- buttons: string[];
21
- } | null;
18
+ activeModal: { message: string; buttons: string[] } | null;
19
+ errorMessage?: string;
20
+ errorReason?: string;
22
21
  }
23
22
  export interface CliScripts {
24
23
  parseOutput?: (input: CliScriptInput) => any;
@@ -22,6 +22,8 @@ export interface CliSessionStatus {
22
22
  messages: CliChatMessage[];
23
23
  workingDir: string;
24
24
  activeModal: { message: string; buttons: string[] } | null;
25
+ errorMessage?: string;
26
+ errorReason?: string;
25
27
  }
26
28
 
27
29
  export interface CliScripts {
@@ -67,10 +67,12 @@ function logTerminalBackendSelection(
67
67
  if (loggedTerminalBackends.has(key)) return;
68
68
  loggedTerminalBackends.add(key);
69
69
  if (backendKind === 'xterm' && preference !== 'xterm' && !ghosttyAvailable) {
70
- LOG.warn(
71
- 'Terminal',
72
- `[terminal-screen] ghostty-vt unavailable; using xterm fallback (preference=${preference})`,
73
- );
70
+ const message = `[terminal-screen] ghostty-vt unavailable; using xterm fallback (preference=${preference})`;
71
+ if (preference === 'auto') {
72
+ LOG.info('Terminal', message);
73
+ } else {
74
+ LOG.warn('Terminal', message);
75
+ }
74
76
  return;
75
77
  }
76
78
  LOG.info(
@@ -469,9 +469,14 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
469
469
  const adapter = getTargetedCliAdapter(h, args, provider?.type);
470
470
  if (adapter) {
471
471
  _log(`${transport} adapter: ${adapter.cliType}`);
472
- const parsedStatus = typeof adapter.getScriptParsedStatus === 'function'
473
- ? parseMaybeJson(adapter.getScriptParsedStatus())
474
- : null;
472
+ let parsedStatus: any = null;
473
+ if (typeof adapter.getScriptParsedStatus === 'function') {
474
+ try {
475
+ parsedStatus = parseMaybeJson(adapter.getScriptParsedStatus());
476
+ } catch (error: any) {
477
+ return { success: false, error: error?.message || String(error) };
478
+ }
479
+ }
475
480
  const parsedRecord = parsedStatus && typeof parsedStatus === 'object'
476
481
  ? parsedStatus as Record<string, any>
477
482
  : null;
@@ -21,7 +21,7 @@ import { launchWithCdp, killIdeProcess, isIdeRunning } from '../launch.js';
21
21
  import { loadConfig, saveConfig, updateConfig } from '../config/config.js';
22
22
  import { loadState, saveState } from '../config/state-store.js';
23
23
  import { resolveIdeLaunchWorkspace } from '../config/workspaces.js';
24
- import { appendRecentActivity, getRecentActivity, markSessionSeen } from '../config/recent-activity.js';
24
+ import { appendRecentActivity, getRecentActivity, markSessionSeen, dismissSessionNotification, markSessionNotificationUnread } from '../config/recent-activity.js';
25
25
  import { getSavedProviderSessions } from '../config/saved-sessions.js';
26
26
  import { listSavedHistorySessions } from '../config/chat-history.js';
27
27
  import { detectIDEs } from '../detection/ide-detector.js';
@@ -733,6 +733,64 @@ export class DaemonCommandRouter {
733
733
  };
734
734
  }
735
735
 
736
+ case 'delete_notification': {
737
+ const sessionId = args?.sessionId;
738
+ const notificationId = typeof args?.notificationId === 'string' ? args.notificationId.trim() : '';
739
+ if (!sessionId || typeof sessionId !== 'string') {
740
+ return { success: false, error: 'sessionId is required' };
741
+ }
742
+ if (!notificationId) {
743
+ return { success: false, error: 'notificationId is required' };
744
+ }
745
+ const sessionEntries = buildSessionEntries(
746
+ this.deps.instanceManager.collectAllStates(),
747
+ this.deps.cdpManagers,
748
+ );
749
+ const targetSession = sessionEntries.find((entry) => entry.id === sessionId);
750
+ const next = dismissSessionNotification(
751
+ loadState(),
752
+ sessionId,
753
+ notificationId,
754
+ targetSession?.providerSessionId,
755
+ );
756
+ saveState(next);
757
+ this.deps.onStatusChange?.();
758
+ return {
759
+ success: true,
760
+ sessionId,
761
+ notificationId,
762
+ };
763
+ }
764
+
765
+ case 'mark_notification_unread': {
766
+ const sessionId = args?.sessionId;
767
+ const notificationId = typeof args?.notificationId === 'string' ? args.notificationId.trim() : '';
768
+ if (!sessionId || typeof sessionId !== 'string') {
769
+ return { success: false, error: 'sessionId is required' };
770
+ }
771
+ if (!notificationId) {
772
+ return { success: false, error: 'notificationId is required' };
773
+ }
774
+ const sessionEntries = buildSessionEntries(
775
+ this.deps.instanceManager.collectAllStates(),
776
+ this.deps.cdpManagers,
777
+ );
778
+ const targetSession = sessionEntries.find((entry) => entry.id === sessionId);
779
+ const next = markSessionNotificationUnread(
780
+ loadState(),
781
+ sessionId,
782
+ notificationId,
783
+ targetSession?.providerSessionId,
784
+ );
785
+ saveState(next);
786
+ this.deps.onStatusChange?.();
787
+ return {
788
+ success: true,
789
+ sessionId,
790
+ notificationId,
791
+ };
792
+ }
793
+
736
794
  // ─── Daemon Self-Upgrade ───
737
795
  case 'daemon_upgrade': {
738
796
  LOG.info('Upgrade', 'Remote upgrade requested from dashboard');
@@ -9,6 +9,7 @@
9
9
 
10
10
  import * as path from 'path';
11
11
  import type { ProviderSummaryMetadata } from '../shared-types.js';
12
+ import type { RecentSessionBucket, SessionEntry } from '../shared-types.js';
12
13
  import type { DaemonState } from './state-store.js';
13
14
  import { expandPath } from './workspaces.js';
14
15
  import { normalizePersistedSummaryMetadata } from '../providers/summary-metadata.js';
@@ -99,6 +100,121 @@ export function getSessionSeenMarker(state: DaemonState, sessionId: string, prov
99
100
  return state.sessionReadMarkers?.[providerKey] || state.sessionReadMarkers?.[sessionId] || '';
100
101
  }
101
102
 
103
+ export function getSessionNotificationDismissal(state: DaemonState, sessionId: string, providerSessionId?: string | null): string {
104
+ const providerKey = buildSessionReadStateKey(sessionId, providerSessionId);
105
+ return state.sessionNotificationDismissals?.[providerKey] || state.sessionNotificationDismissals?.[sessionId] || '';
106
+ }
107
+
108
+ export function getSessionNotificationUnreadOverride(state: DaemonState, sessionId: string, providerSessionId?: string | null): string {
109
+ const providerKey = buildSessionReadStateKey(sessionId, providerSessionId);
110
+ return state.sessionNotificationUnreadOverrides?.[providerKey] || state.sessionNotificationUnreadOverrides?.[sessionId] || '';
111
+ }
112
+
113
+ export function dismissSessionNotification(
114
+ state: DaemonState,
115
+ sessionId: string,
116
+ notificationId: string,
117
+ providerSessionId?: string | null,
118
+ ): DaemonState {
119
+ const dismissalId = String(notificationId || '').trim();
120
+ if (!dismissalId) return state;
121
+ const dismissalKeys = Array.from(new Set([
122
+ sessionId,
123
+ buildSessionReadStateKey(sessionId, providerSessionId),
124
+ ].filter(Boolean)));
125
+ const nextSessionNotificationDismissals = { ...(state.sessionNotificationDismissals || {}) };
126
+ const nextSessionNotificationUnreadOverrides = { ...(state.sessionNotificationUnreadOverrides || {}) };
127
+ for (const key of dismissalKeys) {
128
+ nextSessionNotificationDismissals[key] = dismissalId;
129
+ delete nextSessionNotificationUnreadOverrides[key];
130
+ }
131
+ return {
132
+ ...state,
133
+ sessionNotificationDismissals: nextSessionNotificationDismissals,
134
+ sessionNotificationUnreadOverrides: nextSessionNotificationUnreadOverrides,
135
+ };
136
+ }
137
+
138
+ export function markSessionNotificationUnread(
139
+ state: DaemonState,
140
+ sessionId: string,
141
+ notificationId: string,
142
+ providerSessionId?: string | null,
143
+ ): DaemonState {
144
+ const unreadId = String(notificationId || '').trim();
145
+ if (!unreadId) return state;
146
+ const unreadKeys = Array.from(new Set([
147
+ sessionId,
148
+ buildSessionReadStateKey(sessionId, providerSessionId),
149
+ ].filter(Boolean)));
150
+ const nextSessionNotificationDismissals = { ...(state.sessionNotificationDismissals || {}) };
151
+ const nextSessionNotificationUnreadOverrides = { ...(state.sessionNotificationUnreadOverrides || {}) };
152
+ for (const key of unreadKeys) {
153
+ nextSessionNotificationUnreadOverrides[key] = unreadId;
154
+ delete nextSessionNotificationDismissals[key];
155
+ }
156
+ return {
157
+ ...state,
158
+ sessionNotificationDismissals: nextSessionNotificationDismissals,
159
+ sessionNotificationUnreadOverrides: nextSessionNotificationUnreadOverrides,
160
+ };
161
+ }
162
+
163
+ export function getSessionNotificationTargetValue(session: Pick<SessionEntry, 'id' | 'providerSessionId'>): string {
164
+ const providerSessionId = typeof session.providerSessionId === 'string' ? session.providerSessionId.trim() : '';
165
+ return providerSessionId || session.id;
166
+ }
167
+
168
+ export function getSessionCurrentNotificationId(session: Pick<SessionEntry, 'id' | 'providerSessionId' | 'inboxBucket' | 'unread' | 'lastMessageHash' | 'lastMessageAt' | 'lastUpdated' | 'status'>): string {
169
+ const inboxBucket = session.inboxBucket || 'idle';
170
+ const isNeedsAttention = inboxBucket === 'needs_attention' || session.status === 'waiting_approval';
171
+ const isTaskComplete = inboxBucket === 'task_complete' && !!session.unread;
172
+ const type = isNeedsAttention ? 'needs_attention' : isTaskComplete ? 'task_complete' : '';
173
+ if (!type) return '';
174
+ const target = getSessionNotificationTargetValue(session);
175
+ const lastMessageHash = typeof session.lastMessageHash === 'string' ? session.lastMessageHash : '';
176
+ const timestamp = Number(session.lastMessageAt || session.lastUpdated || 0);
177
+ return [type, target, lastMessageHash, String(timestamp)].join('|');
178
+ }
179
+
180
+ export function applySessionNotificationOverlay(
181
+ session: Pick<SessionEntry, 'id' | 'providerSessionId' | 'inboxBucket' | 'unread' | 'lastMessageHash' | 'lastMessageAt' | 'lastUpdated' | 'status'>,
182
+ overlay: { dismissedNotificationId?: string | null; unreadNotificationId?: string | null },
183
+ ): { unread: boolean; inboxBucket: RecentSessionBucket } {
184
+ const currentNotificationId = getSessionCurrentNotificationId(session);
185
+ const taskCompleteNotificationId = (() => {
186
+ const target = getSessionNotificationTargetValue(session);
187
+ const lastMessageHash = typeof session.lastMessageHash === 'string' ? session.lastMessageHash : '';
188
+ const timestamp = Number(session.lastMessageAt || session.lastUpdated || 0);
189
+ if (!target || !lastMessageHash || !timestamp) return '';
190
+ return ['task_complete', target, lastMessageHash, String(timestamp)].join('|');
191
+ })();
192
+ const dismissedNotificationId = typeof overlay.dismissedNotificationId === 'string' ? overlay.dismissedNotificationId.trim() : '';
193
+ const unreadNotificationId = typeof overlay.unreadNotificationId === 'string' ? overlay.unreadNotificationId.trim() : '';
194
+ if (
195
+ unreadNotificationId
196
+ && (currentNotificationId === unreadNotificationId || taskCompleteNotificationId === unreadNotificationId)
197
+ ) {
198
+ const forcedInboxBucket = session.inboxBucket === 'needs_attention' || session.status === 'waiting_approval'
199
+ ? 'needs_attention'
200
+ : 'task_complete';
201
+ return {
202
+ unread: true,
203
+ inboxBucket: forcedInboxBucket,
204
+ };
205
+ }
206
+ if (!currentNotificationId || !dismissedNotificationId || currentNotificationId !== dismissedNotificationId) {
207
+ return {
208
+ unread: !!session.unread,
209
+ inboxBucket: session.inboxBucket || 'idle',
210
+ };
211
+ }
212
+ return {
213
+ unread: false,
214
+ inboxBucket: 'idle',
215
+ };
216
+ }
217
+
102
218
  export function markSessionSeen(
103
219
  state: DaemonState,
104
220
  sessionId: string,
@@ -115,13 +231,19 @@ export function markSessionSeen(
115
231
  ].filter(Boolean)));
116
232
  const nextSessionReads = { ...prev };
117
233
  const nextSessionReadMarkers = { ...prevMarkers };
234
+ const nextSessionNotificationDismissals = { ...(state.sessionNotificationDismissals || {}) };
235
+ const nextSessionNotificationUnreadOverrides = { ...(state.sessionNotificationUnreadOverrides || {}) };
118
236
  for (const key of readKeys) {
119
237
  nextSessionReads[key] = Math.max(prev[key] || 0, seenAt);
120
238
  if (nextMarker) nextSessionReadMarkers[key] = nextMarker;
239
+ delete nextSessionNotificationDismissals[key];
240
+ delete nextSessionNotificationUnreadOverrides[key];
121
241
  }
122
242
  return {
123
243
  ...state,
124
244
  sessionReads: nextSessionReads,
125
245
  sessionReadMarkers: nextMarker ? nextSessionReadMarkers : prevMarkers,
246
+ sessionNotificationDismissals: nextSessionNotificationDismissals,
247
+ sessionNotificationUnreadOverrides: nextSessionNotificationUnreadOverrides,
126
248
  };
127
249
  }