@adhdev/daemon-core 0.8.81 → 0.8.82

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 (32) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +1 -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 +232 -41
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +232 -41
  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 +1 -1
  13. package/dist/status/snapshot.d.ts +1 -0
  14. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  15. package/package.json +1 -1
  16. package/src/cli-adapter-types.d.ts +2 -0
  17. package/src/cli-adapters/provider-cli-adapter.ts +14 -5
  18. package/src/cli-adapters/provider-cli-shared.d.ts +3 -4
  19. package/src/cli-adapters/provider-cli-shared.ts +2 -0
  20. package/src/commands/chat-commands.ts +8 -3
  21. package/src/commands/router.ts +59 -1
  22. package/src/config/recent-activity.ts +122 -0
  23. package/src/config/state-store.ts +16 -0
  24. package/src/logging/command-log.ts +2 -0
  25. package/src/providers/acp-provider-instance.ts +2 -17
  26. package/src/providers/cli-provider-instance.ts +32 -10
  27. package/src/providers/extension-provider-instance.ts +0 -2
  28. package/src/providers/ide-provider-instance.ts +0 -2
  29. package/src/providers/provider-instance.d.ts +1 -1
  30. package/src/providers/provider-instance.ts +1 -0
  31. package/src/shared-types.ts +3 -1
  32. package/src/status/snapshot.ts +18 -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 {
@@ -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.82",
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.82",
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[];
@@ -113,6 +113,7 @@ export class ProviderCliAdapter implements CliAdapter {
113
113
  private recentOutputBuffer = '';
114
114
  private isWaitingForResponse = false;
115
115
  private activeModal: { message: string; buttons: string[] } | null = null;
116
+ private parseErrorMessage: string | null = null;
116
117
  private responseTimeout: NodeJS.Timeout | null = null;
117
118
  private idleTimeout: NodeJS.Timeout | null = null;
118
119
  private ready = false;
@@ -1267,10 +1268,12 @@ export class ProviderCliAdapter implements CliAdapter {
1267
1268
 
1268
1269
  getStatus(): CliSessionStatus {
1269
1270
  return {
1270
- status: this.currentStatus,
1271
+ status: this.parseErrorMessage ? 'error' : this.currentStatus,
1271
1272
  messages: [...this.committedMessages],
1272
1273
  workingDir: this.workingDir,
1273
1274
  activeModal: this.activeModal,
1275
+ errorMessage: this.parseErrorMessage || undefined,
1276
+ errorReason: this.parseErrorMessage ? 'parse_error' : undefined,
1274
1277
  };
1275
1278
  }
1276
1279
 
@@ -1340,7 +1343,7 @@ export class ProviderCliAdapter implements CliAdapter {
1340
1343
  id: 'cli_session',
1341
1344
  status: this.currentStatus,
1342
1345
  title: this.cliName,
1343
- messages: messages.slice(-50).map((message, index) => buildChatMessage({
1346
+ messages: messages.map((message, index) => buildChatMessage({
1344
1347
  ...message,
1345
1348
  id: message.id || `msg_${index}`,
1346
1349
  index: typeof message.index === 'number' ? message.index : index,
@@ -1375,7 +1378,10 @@ export class ProviderCliAdapter implements CliAdapter {
1375
1378
  }
1376
1379
 
1377
1380
  private parseCurrentTranscript(baseMessages: CliChatMessage[], partialResponse: string, scope?: TurnParseScope | null): any {
1378
- if (!this.cliScripts?.parseOutput) return null;
1381
+ if (!this.cliScripts?.parseOutput) {
1382
+ this.parseErrorMessage = null;
1383
+ return null;
1384
+ }
1379
1385
  try {
1380
1386
  const input = buildCliParseInput({
1381
1387
  accumulatedBuffer: this.accumulatedBuffer,
@@ -1403,10 +1409,13 @@ export class ProviderCliAdapter implements CliAdapter {
1403
1409
  lastAssistant.content = trimPromptEchoPrefix(lastAssistant.content, promptForTrim);
1404
1410
  }
1405
1411
  }
1412
+ this.parseErrorMessage = null;
1406
1413
  return parsed;
1407
1414
  } catch (e: any) {
1408
- LOG.warn('CLI', `[${this.cliType}] parseOutput error: ${e.message}`);
1409
- return null;
1415
+ const message = e?.message || String(e);
1416
+ this.parseErrorMessage = message;
1417
+ LOG.warn('CLI', `[${this.cliType}] parseOutput error: ${message}`);
1418
+ throw e;
1410
1419
  }
1411
1420
  }
1412
1421
 
@@ -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 {
@@ -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
  }
@@ -23,6 +23,10 @@ export interface DaemonState {
23
23
  sessionReads: Record<string, number>;
24
24
  /** Last seen completion marker for live sessions, keyed by sessionId */
25
25
  sessionReadMarkers: Record<string, string>;
26
+ /** Current notification dismissal ids keyed by stable session target */
27
+ sessionNotificationDismissals: Record<string, string>;
28
+ /** Current notification unread override ids keyed by stable session target */
29
+ sessionNotificationUnreadOverrides: Record<string, string>;
26
30
  }
27
31
 
28
32
  const DEFAULT_STATE: DaemonState = {
@@ -30,6 +34,8 @@ const DEFAULT_STATE: DaemonState = {
30
34
  savedProviderSessions: [],
31
35
  sessionReads: {},
32
36
  sessionReadMarkers: {},
37
+ sessionNotificationDismissals: {},
38
+ sessionNotificationUnreadOverrides: {},
33
39
  };
34
40
 
35
41
  function isPlainObject(value: unknown): value is Record<string, any> {
@@ -71,12 +77,22 @@ function normalizeState(raw: unknown): DaemonState {
71
77
  Object.entries(isPlainObject(parsed.sessionReadMarkers) ? parsed.sessionReadMarkers : {})
72
78
  .filter(([key, value]) => !isLegacyVolatileSessionReadKey(key) && typeof value === 'string')
73
79
  );
80
+ const sessionNotificationDismissals = Object.fromEntries(
81
+ Object.entries(isPlainObject(parsed.sessionNotificationDismissals) ? parsed.sessionNotificationDismissals : {})
82
+ .filter(([key, value]) => !isLegacyVolatileSessionReadKey(key) && typeof value === 'string' && value.length > 0)
83
+ );
84
+ const sessionNotificationUnreadOverrides = Object.fromEntries(
85
+ Object.entries(isPlainObject(parsed.sessionNotificationUnreadOverrides) ? parsed.sessionNotificationUnreadOverrides : {})
86
+ .filter(([key, value]) => !isLegacyVolatileSessionReadKey(key) && typeof value === 'string' && value.length > 0)
87
+ );
74
88
 
75
89
  return {
76
90
  recentActivity,
77
91
  savedProviderSessions,
78
92
  sessionReads,
79
93
  sessionReadMarkers,
94
+ sessionNotificationDismissals,
95
+ sessionNotificationUnreadOverrides,
80
96
  };
81
97
  }
82
98
 
@@ -118,6 +118,8 @@ const SKIP_COMMANDS = new Set([
118
118
  'status_report',
119
119
  'read_chat',
120
120
  'mark_session_seen',
121
+ 'delete_notification',
122
+ 'mark_notification_unread',
121
123
  ]);
122
124
 
123
125
  export function shouldLogCommand(cmd: string): boolean {
@@ -286,9 +286,8 @@ export class AcpProviderInstance implements ProviderInstance {
286
286
  getState(): AcpProviderState {
287
287
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
288
288
 
289
- // Recent 50 messages
290
- const recentMessages = normalizeChatMessages(this.messages.slice(-50).map(m => {
291
- const content = this.truncateContent(m.content);
289
+ const recentMessages = normalizeChatMessages(this.messages.map(m => {
290
+ const content = m.content;
292
291
  return buildChatMessage({
293
292
  ...m,
294
293
  content,
@@ -1239,19 +1238,6 @@ export class AcpProviderInstance implements ProviderInstance {
1239
1238
 
1240
1239
  // ─── Rich Content Helpers ────────────────────────────
1241
1240
 
1242
- /** Truncate content for transport (text: 2000 chars, images preserved) */
1243
- private truncateContent(content: string | ContentBlock[]): string | ContentBlock[] {
1244
- if (typeof content === 'string') {
1245
- return content.length > 2000 ? content.slice(0, 2000) + '\n... (truncated)' : content;
1246
- }
1247
- return content.map(b => {
1248
- if (b.type === 'text' && b.text.length > 2000) {
1249
- return { ...b, text: b.text.slice(0, 2000) + '\n... (truncated)' };
1250
- }
1251
- return b;
1252
- });
1253
- }
1254
-
1255
1241
  /** Build ContentBlock[] from current partial state */
1256
1242
  private buildPartialBlocks(): ContentBlock[] {
1257
1243
  const blocks: ContentBlock[] = [];
@@ -1437,7 +1423,6 @@ export class AcpProviderInstance implements ProviderInstance {
1437
1423
 
1438
1424
  private pushEvent(event: ProviderEvent): void {
1439
1425
  this.events.push(event);
1440
- if (this.events.length > 50) this.events = this.events.slice(-50);
1441
1426
  }
1442
1427
 
1443
1428
  private appendSystemMessage(content: string, timestamp = Date.now()): void {
@@ -12,7 +12,7 @@ import * as fs from 'fs';
12
12
  import { createRequire } from 'node:module';
13
13
  import { normalizeInputEnvelope, type ProviderModule, flattenContent } from './contracts.js';
14
14
  import { assertTextOnlyInput } from './provider-input-support.js';
15
- import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
15
+ import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, ProviderErrorReason } from './provider-instance.js';
16
16
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
17
17
  import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
18
18
  import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pty-transport.js';
@@ -114,6 +114,8 @@ export class CliProviderInstance implements ProviderInstance {
114
114
  private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
115
115
  readonly instanceId: string;
116
116
  private suppressIdleHistoryReplay = false;
117
+ private errorMessage: string | undefined = undefined;
118
+ private errorReason: ProviderErrorReason | undefined = undefined;
117
119
 
118
120
  private presentationMode: 'terminal' | 'chat';
119
121
  private providerSessionId?: string;
@@ -302,9 +304,26 @@ export class CliProviderInstance implements ProviderInstance {
302
304
 
303
305
  getState(): ProviderState {
304
306
  const adapterStatus = this.adapter.getStatus();
305
- const parsedStatus = this.adapter.getScriptParsedStatus?.() || null;
307
+ let parsedStatus: any = null;
308
+ let parseErrorMessage: string | undefined;
309
+ if (typeof this.adapter.getScriptParsedStatus === 'function') {
310
+ try {
311
+ parsedStatus = this.adapter.getScriptParsedStatus() || null;
312
+ this.errorMessage = undefined;
313
+ this.errorReason = undefined;
314
+ } catch (error: any) {
315
+ parseErrorMessage = error?.message || String(error);
316
+ this.errorMessage = parseErrorMessage;
317
+ this.errorReason = 'parse_error';
318
+ }
319
+ } else {
320
+ this.errorMessage = undefined;
321
+ this.errorReason = undefined;
322
+ }
306
323
  const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
307
- const visibleStatus = autoApproveActive ? 'generating' : adapterStatus.status;
324
+ const visibleStatus = parseErrorMessage
325
+ ? 'error'
326
+ : (autoApproveActive ? 'generating' : adapterStatus.status);
308
327
  const parsedProviderSessionId = normalizeProviderSessionId(
309
328
  this.type,
310
329
  typeof parsedStatus?.providerSessionId === 'string' ? parsedStatus.providerSessionId : '',
@@ -314,7 +333,11 @@ export class CliProviderInstance implements ProviderInstance {
314
333
  }
315
334
  const runtime = this.adapter.getRuntimeMetadata();
316
335
  this.maybeAppendRuntimeRecoveryMessage(runtime);
317
- let parsedMessages = Array.isArray(parsedStatus?.messages) ? parsedStatus.messages : [];
336
+ let parsedMessages = Array.isArray(parsedStatus?.messages)
337
+ ? parsedStatus.messages
338
+ : (parseErrorMessage
339
+ ? normalizeChatMessages(Array.isArray(adapterStatus.messages) ? adapterStatus.messages as any : [])
340
+ : []);
318
341
  const historyMessageCount = Number.isFinite(parsedStatus?.historyMessageCount)
319
342
  ? Math.max(0, Number(parsedStatus.historyMessageCount))
320
343
  : null;
@@ -365,7 +388,9 @@ export class CliProviderInstance implements ProviderInstance {
365
388
  activeChat: {
366
389
  id: `${this.type}_${this.workingDir}`,
367
390
  title: parsedStatus?.title || dirName,
368
- status: autoApproveActive && parsedStatus?.status === 'waiting_approval'
391
+ status: parseErrorMessage
392
+ ? 'error'
393
+ : autoApproveActive && parsedStatus?.status === 'waiting_approval'
369
394
  ? 'generating'
370
395
  : (parsedStatus?.status || visibleStatus),
371
396
  messages: mergedMessages,
@@ -394,6 +419,8 @@ export class CliProviderInstance implements ProviderInstance {
394
419
  controlValues: surface.controlValues,
395
420
  providerControls: this.provider.controls,
396
421
  summaryMetadata: surface.summaryMetadata as any,
422
+ errorMessage: this.errorMessage,
423
+ errorReason: this.errorReason,
397
424
  };
398
425
  }
399
426
 
@@ -591,8 +618,6 @@ export class CliProviderInstance implements ProviderInstance {
591
618
 
592
619
  private pushEvent(event: ProviderEvent): void {
593
620
  this.events.push(event);
594
- // Max 50
595
- if (this.events.length > 50) this.events = this.events.slice(-50);
596
621
  }
597
622
 
598
623
  private flushEvents(): ProviderEvent[] {
@@ -805,9 +830,6 @@ export class CliProviderInstance implements ProviderInstance {
805
830
  key: dedupKey,
806
831
  message: normalizedMessage,
807
832
  });
808
- if (this.runtimeMessages.length > 50) {
809
- this.runtimeMessages = this.runtimeMessages.slice(-50);
810
- }
811
833
 
812
834
  if (normalizedContent) {
813
835
  this.historyWriter.appendNewMessages(
@@ -246,7 +246,6 @@ export class ExtensionProviderInstance implements ProviderInstance {
246
246
 
247
247
  private pushEvent(event: ProviderEvent): void {
248
248
  this.events.push(event);
249
- if (this.events.length > 50) this.events = this.events.slice(-50);
250
249
  }
251
250
 
252
251
  private applyProviderResponse(data: any, options: { phase: 'immediate' | 'turn_completed' }): void {
@@ -335,7 +334,6 @@ export class ExtensionProviderInstance implements ProviderInstance {
335
334
  key: dedupKey,
336
335
  message: normalizedMessage,
337
336
  });
338
- if (this.runtimeMessages.length > 50) this.runtimeMessages = this.runtimeMessages.slice(-50);
339
337
 
340
338
  if (normalizedContent) {
341
339
  this.historyWriter.appendNewMessages(
@@ -473,7 +473,6 @@ export class IdeProviderInstance implements ProviderInstance {
473
473
 
474
474
  private pushEvent(event: ProviderEvent): void {
475
475
  this.events.push(event);
476
- if (this.events.length > 50) this.events = this.events.slice(-50);
477
476
  }
478
477
 
479
478
  private applyProviderResponse(data: any, options: { phase: 'immediate' | 'turn_completed' }): void {
@@ -576,7 +575,6 @@ export class IdeProviderInstance implements ProviderInstance {
576
575
  key: dedupKey,
577
576
  message: normalizedMessage,
578
577
  });
579
- if (this.runtimeMessages.length > 50) this.runtimeMessages = this.runtimeMessages.slice(-50);
580
578
 
581
579
  if (normalizedContent) {
582
580
  this.historyWriter.appendNewMessages(
@@ -40,7 +40,7 @@ export interface ActiveChatData {
40
40
  inputContent?: string;
41
41
  }
42
42
  /** Standardized error reasons across all provider categories */
43
- export type ProviderErrorReason = 'not_installed' | 'auth_failed' | 'spawn_error' | 'init_failed' | 'crash' | 'timeout' | 'cdp_error' | 'disconnected';
43
+ export type ProviderErrorReason = 'not_installed' | 'auth_failed' | 'spawn_error' | 'init_failed' | 'parse_error' | 'crash' | 'timeout' | 'cdp_error' | 'disconnected';
44
44
  /** Common fields shared by all provider categories */
45
45
  interface ProviderStateBase {
46
46
  /** Provider type (e.g. 'gemini-cli', 'cursor', 'cline') */
@@ -55,6 +55,7 @@ export type ProviderErrorReason =
55
55
  | 'auth_failed' // Authentication/API key error
56
56
  | 'spawn_error' // Process spawn failure
57
57
  | 'init_failed' // Initialization/handshake failure
58
+ | 'parse_error' // Provider parser/adapter script failure
58
59
  | 'crash' // Unexpected process crash
59
60
  | 'timeout' // Operation timeout
60
61
  | 'cdp_error' // CDP connection failure (IDE)