@adhdev/daemon-core 0.8.63 → 0.8.65

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.
@@ -39,6 +39,7 @@ export declare class AcpProviderInstance implements ProviderInstance {
39
39
  private activeToolCalls;
40
40
  private stopReason;
41
41
  private partialContent;
42
+ private partialThoughtContent;
42
43
  /** Rich content blocks accumulated during streaming */
43
44
  private partialBlocks;
44
45
  /** Tool calls collected during current turn */
@@ -94,6 +95,10 @@ export declare class AcpProviderInstance implements ProviderInstance {
94
95
  private truncateContent;
95
96
  /** Build ContentBlock[] from current partial state */
96
97
  private buildPartialBlocks;
98
+ private buildPartialThoughtMessage;
99
+ private buildToolCallBubbleKind;
100
+ private summarizeToolCallBubbleContent;
101
+ private buildTurnToolCallMessages;
97
102
  /** Finalize streaming content into an assistant message */
98
103
  private finalizeAssistantMessage;
99
104
  /** Convert ACP ToolCallContent[] to our ToolCallContent[] */
@@ -4,6 +4,7 @@ export type BuiltinChatMessageKind = typeof BUILTIN_CHAT_MESSAGE_KINDS[number];
4
4
  export type ChatMessageKind = BuiltinChatMessageKind | (string & {});
5
5
  export declare function isBuiltinChatMessageKind(kind: unknown): kind is BuiltinChatMessageKind;
6
6
  export declare function normalizeChatMessageKind(kind: unknown, role: unknown): ChatMessageKind;
7
+ export declare function resolveChatMessageKind<T extends ChatMessage>(message: T): ChatMessageKind;
7
8
  export declare function buildChatMessage<T extends Omit<ChatMessage, 'kind'> & {
8
9
  kind?: ChatMessageKind;
9
10
  }>(message: T): T & {
@@ -32,6 +33,27 @@ export declare function buildAssistantChatMessage<T extends Omit<ChatMessage, 'r
32
33
  role: 'assistant';
33
34
  kind: ChatMessageKind;
34
35
  });
36
+ export declare function buildThoughtChatMessage<T extends Omit<ChatMessage, 'role' | 'kind'> & {
37
+ role?: 'assistant';
38
+ kind?: ChatMessageKind;
39
+ }>(message: T): (T & {
40
+ role: 'assistant';
41
+ kind: ChatMessageKind;
42
+ });
43
+ export declare function buildToolChatMessage<T extends Omit<ChatMessage, 'role' | 'kind'> & {
44
+ role?: 'assistant';
45
+ kind?: ChatMessageKind;
46
+ }>(message: T): (T & {
47
+ role: 'assistant';
48
+ kind: ChatMessageKind;
49
+ });
50
+ export declare function buildTerminalChatMessage<T extends Omit<ChatMessage, 'role' | 'kind'> & {
51
+ role?: 'assistant';
52
+ kind?: ChatMessageKind;
53
+ }>(message: T): (T & {
54
+ role: 'assistant';
55
+ kind: ChatMessageKind;
56
+ });
35
57
  export declare function buildUserChatMessage<T extends Omit<ChatMessage, 'role' | 'kind'> & {
36
58
  role?: 'user';
37
59
  kind?: ChatMessageKind;
@@ -0,0 +1,15 @@
1
+ export declare const DEFAULT_ACTIVE_CHAT_POLL_STATUSES: Set<string>;
2
+ export declare const DEFAULT_CHAT_TAIL_RECENT_MESSAGE_GRACE_MS = 8000;
3
+ export interface HotChatSessionLike {
4
+ id?: string | null;
5
+ status?: unknown;
6
+ lastMessageAt?: unknown;
7
+ }
8
+ export declare function classifyHotChatSessionsForSubscriptionFlush(sessions: HotChatSessionLike[], previousHotSessionIds: ReadonlySet<string>, options?: {
9
+ now?: number;
10
+ recentMessageGraceMs?: number;
11
+ activeStatuses?: ReadonlySet<string>;
12
+ }): {
13
+ active: Set<string>;
14
+ finalizing: Set<string>;
15
+ };
@@ -66,6 +66,7 @@ export declare function getSessionCompletionMarker(session: {
66
66
  id?: string;
67
67
  index?: number;
68
68
  receivedAt?: number | string;
69
+ timestamp?: number | string;
69
70
  _turnKey?: string;
70
71
  }> | null;
71
72
  } | null;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.8.63",
4
- "description": "ADHDev local session host core \u2014 session registry, protocol, buffers",
3
+ "version": "0.8.65",
4
+ "description": "ADHDev local session host core session registry, protocol, buffers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.8.63",
4
- "description": "ADHDev daemon core \u2014 CDP, IDE detection, providers, command execution",
3
+ "version": "0.8.65",
4
+ "description": "ADHDev daemon core CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
@@ -14,6 +14,7 @@ import type {
14
14
  import type { ProviderModule, ProviderScripts } from '../providers/contracts.js';
15
15
  import { extractProviderControlValues, normalizeProviderEffects } from '../providers/control-effects.js';
16
16
  import { resolveProviderStateSurface } from '../providers/provider-patch-state.js';
17
+ import { normalizeChatMessages } from '../providers/chat-message-normalization.js';
17
18
 
18
19
  export class ProviderStreamAdapter implements IAgentStreamAdapter {
19
20
  readonly agentType: string;
@@ -153,7 +154,7 @@ export class ProviderStreamAdapter implements IAgentStreamAdapter {
153
154
  agentName: this.agentName,
154
155
  extensionId: this.extensionId,
155
156
  status: data.status || 'idle',
156
- messages: data.messages || [],
157
+ messages: normalizeChatMessages(Array.isArray(data.messages) ? data.messages : []) as any,
157
158
  inputContent: data.inputContent || '',
158
159
  activeModal: data.activeModal,
159
160
  };
@@ -7,12 +7,21 @@
7
7
 
8
8
  import type { ProviderEffect } from '../providers/contracts.js';
9
9
  import type { ProviderSummaryMetadata } from '../shared-types.js';
10
+ import type { ChatMessageKind } from '../providers/chat-message-normalization.js';
10
11
 
11
12
  /** Agent chat message */
12
13
  export interface AgentChatMessage {
13
14
  role: 'user' | 'assistant' | 'system';
14
15
  content: string;
16
+ kind?: ChatMessageKind;
15
17
  timestamp?: number;
18
+ receivedAt?: number;
19
+ id?: string;
20
+ index?: number;
21
+ meta?: Record<string, unknown>;
22
+ senderName?: string;
23
+ _type?: string;
24
+ _sub?: string;
16
25
  }
17
26
 
18
27
  /** Agent chat history item */
package/src/index.ts CHANGED
@@ -115,6 +115,11 @@ export type { IDEInfo } from './detection/ide-detector.js';
115
115
  export { detectCLIs } from './detection/cli-detector.js';
116
116
  export { getHostMemorySnapshot } from './system/host-memory.js';
117
117
  export type { HostMemorySnapshot } from './system/host-memory.js';
118
+ export {
119
+ classifyHotChatSessionsForSubscriptionFlush,
120
+ DEFAULT_ACTIVE_CHAT_POLL_STATUSES,
121
+ DEFAULT_CHAT_TAIL_RECENT_MESSAGE_GRACE_MS,
122
+ } from './status/chat-tail-hot-sessions.js';
118
123
 
119
124
  // ── CDP ──
120
125
  export { DaemonCdpManager } from './cdp/manager.js';
@@ -204,10 +209,14 @@ export {
204
209
  BUILTIN_CHAT_MESSAGE_KINDS,
205
210
  isBuiltinChatMessageKind,
206
211
  normalizeChatMessageKind,
212
+ resolveChatMessageKind,
207
213
  buildChatMessage,
208
214
  buildSystemChatMessage,
209
215
  buildRuntimeSystemChatMessage,
210
216
  buildAssistantChatMessage,
217
+ buildThoughtChatMessage,
218
+ buildToolChatMessage,
219
+ buildTerminalChatMessage,
211
220
  buildUserChatMessage,
212
221
  normalizeChatMessage,
213
222
  normalizeChatMessages,
@@ -51,7 +51,16 @@ import { normalizeContent, flattenContent, normalizeInputEnvelope } from './cont
51
51
  import type { ProviderInstance, ProviderState, AcpProviderState, ProviderErrorReason, ProviderEvent, InstanceContext } from './provider-instance.js';
52
52
  import { StatusMonitor } from './status-monitor.js';
53
53
  import { buildLegacyModelModeSummaryMetadata } from './summary-metadata.js';
54
- import { buildAssistantChatMessage, buildChatMessage, buildRuntimeSystemChatMessage, buildUserChatMessage, normalizeChatMessages } from './chat-message-normalization.js';
54
+ import {
55
+ buildAssistantChatMessage,
56
+ buildChatMessage,
57
+ buildRuntimeSystemChatMessage,
58
+ buildTerminalChatMessage,
59
+ buildThoughtChatMessage,
60
+ buildToolChatMessage,
61
+ buildUserChatMessage,
62
+ normalizeChatMessages,
63
+ } from './chat-message-normalization.js';
55
64
  import { LOG } from '../logging/logger.js';
56
65
  import type { ChatMessage } from '../types.js';
57
66
 
@@ -231,6 +240,7 @@ export class AcpProviderInstance implements ProviderInstance {
231
240
  private activeToolCalls: AcpToolCall[] = [];
232
241
  private stopReason: string | null = null;
233
242
  private partialContent = '';
243
+ private partialThoughtContent = '';
234
244
  /** Rich content blocks accumulated during streaming */
235
245
  private partialBlocks: ContentBlock[] = [];
236
246
  /** Tool calls collected during current turn */
@@ -300,7 +310,12 @@ export class AcpProviderInstance implements ProviderInstance {
300
310
  ...m,
301
311
  content,
302
312
  });
303
- }));
313
+ })) as ChatMessage[];
314
+
315
+ if (this.currentStatus === 'generating') {
316
+ const partialThoughtMessage = this.buildPartialThoughtMessage(Date.now());
317
+ if (partialThoughtMessage) recentMessages.push(partialThoughtMessage as ChatMessage);
318
+ }
304
319
 
305
320
  // generating during partial response add
306
321
  if (this.currentStatus === 'generating' && (this.partialContent || this.partialBlocks.length > 0)) {
@@ -980,6 +995,7 @@ export class AcpProviderInstance implements ProviderInstance {
980
995
 
981
996
  this.currentStatus = 'generating';
982
997
  this.partialContent = '';
998
+ this.partialThoughtContent = '';
983
999
  this.partialBlocks = [];
984
1000
  this.turnToolCalls = [];
985
1001
  this.detectStatusTransition();
@@ -1076,9 +1092,15 @@ export class AcpProviderInstance implements ProviderInstance {
1076
1092
  this.currentStatus = 'generating';
1077
1093
  break;
1078
1094
  }
1079
- case 'agent_thought_chunk':
1095
+ case 'agent_thought_chunk': {
1096
+ const content = update.content;
1097
+ if (content?.type === 'text' && typeof content.text === 'string') {
1098
+ this.partialThoughtContent += content.text;
1099
+ }
1100
+ this.currentStatus = 'generating';
1101
+ break;
1102
+ }
1080
1103
  case 'user_message_chunk': {
1081
- // Track but don't display thought chunks as main content
1082
1104
  break;
1083
1105
  }
1084
1106
  case 'tool_call': {
@@ -1256,8 +1278,101 @@ export class AcpProviderInstance implements ProviderInstance {
1256
1278
  return blocks;
1257
1279
  }
1258
1280
 
1281
+ private buildPartialThoughtMessage(timestamp = Date.now()): AcpMessage | null {
1282
+ const content = this.partialThoughtContent.trim();
1283
+ if (!content) return null;
1284
+ return buildThoughtChatMessage({
1285
+ content,
1286
+ timestamp,
1287
+ meta: {
1288
+ label: 'Thought',
1289
+ isRunning: this.currentStatus === 'generating',
1290
+ },
1291
+ });
1292
+ }
1293
+
1294
+ private buildToolCallBubbleKind(toolCall: ToolCallInfo): 'thought' | 'tool' | 'terminal' {
1295
+ if (toolCall.kind === 'think') return 'thought';
1296
+ if (toolCall.kind === 'execute') return 'terminal';
1297
+ if (Array.isArray(toolCall.content) && toolCall.content.some((entry) => entry?.type === 'terminal')) return 'terminal';
1298
+ return 'tool';
1299
+ }
1300
+
1301
+ private summarizeToolCallBubbleContent(toolCall: ToolCallInfo): string {
1302
+ const rawOutput = typeof toolCall.rawOutput === 'string'
1303
+ ? toolCall.rawOutput.trim()
1304
+ : (toolCall.rawOutput != null ? JSON.stringify(toolCall.rawOutput) : '');
1305
+ if (rawOutput) return rawOutput;
1306
+
1307
+ const contentText = Array.isArray(toolCall.content)
1308
+ ? toolCall.content
1309
+ .map((entry) => {
1310
+ if (!entry || typeof entry !== 'object') return '';
1311
+ if (entry.type === 'content') return flattenContent([entry.content]).trim();
1312
+ if (entry.type === 'diff') return `${entry.path}\n${entry.newText || ''}`.trim();
1313
+ if (entry.type === 'terminal') return `Terminal: ${entry.terminalId || ''}`.trim();
1314
+ return '';
1315
+ })
1316
+ .filter(Boolean)
1317
+ .join('\n\n')
1318
+ .trim()
1319
+ : '';
1320
+ if (contentText) return contentText;
1321
+
1322
+ const rawInput = typeof toolCall.rawInput === 'string'
1323
+ ? toolCall.rawInput.trim()
1324
+ : (toolCall.rawInput != null ? JSON.stringify(toolCall.rawInput) : '');
1325
+ if (rawInput) {
1326
+ return toolCall.title ? `${toolCall.title}\n${rawInput}` : rawInput;
1327
+ }
1328
+
1329
+ return toolCall.title || '';
1330
+ }
1331
+
1332
+ private buildTurnToolCallMessages(timestamp = Date.now()): AcpMessage[] {
1333
+ return this.turnToolCalls
1334
+ .map((toolCall) => {
1335
+ const content = this.summarizeToolCallBubbleContent(toolCall);
1336
+ if (!content) return null;
1337
+ const isRunning = toolCall.status === 'pending' || toolCall.status === 'in_progress';
1338
+ const label = toolCall.title || undefined;
1339
+ const kind = this.buildToolCallBubbleKind(toolCall);
1340
+ if (kind === 'thought') {
1341
+ return buildThoughtChatMessage({
1342
+ content,
1343
+ timestamp,
1344
+ meta: { label: label || 'Thought', isRunning },
1345
+ });
1346
+ }
1347
+ if (kind === 'terminal') {
1348
+ return buildTerminalChatMessage({
1349
+ content,
1350
+ timestamp,
1351
+ meta: { label: label || 'Ran command', isRunning },
1352
+ });
1353
+ }
1354
+ return buildToolChatMessage({
1355
+ content,
1356
+ timestamp,
1357
+ meta: { label: label || 'Tool call', isRunning },
1358
+ });
1359
+ })
1360
+ .filter(Boolean) as AcpMessage[];
1361
+ }
1362
+
1259
1363
  /** Finalize streaming content into an assistant message */
1260
1364
  private finalizeAssistantMessage(): void {
1365
+ const timestamp = Date.now();
1366
+ const thoughtMessage = this.buildPartialThoughtMessage(timestamp);
1367
+ if (thoughtMessage) {
1368
+ this.messages.push(thoughtMessage);
1369
+ }
1370
+
1371
+ const toolCallMessages = this.buildTurnToolCallMessages(timestamp);
1372
+ if (toolCallMessages.length > 0) {
1373
+ this.messages.push(...toolCallMessages);
1374
+ }
1375
+
1261
1376
  const blocks = this.buildPartialBlocks();
1262
1377
  // Remove trailing '...' from text blocks for final message
1263
1378
  const finalBlocks = blocks.map(b => {
@@ -1277,6 +1392,7 @@ export class AcpProviderInstance implements ProviderInstance {
1277
1392
  }));
1278
1393
  }
1279
1394
  this.partialContent = '';
1395
+ this.partialThoughtContent = '';
1280
1396
  this.partialBlocks = [];
1281
1397
  this.turnToolCalls = [];
1282
1398
  }
@@ -6,23 +6,107 @@ export type BuiltinChatMessageKind = typeof BUILTIN_CHAT_MESSAGE_KINDS[number];
6
6
  export type ChatMessageKind = BuiltinChatMessageKind | (string & {});
7
7
 
8
8
  const KNOWN_CHAT_MESSAGE_KINDS = new Set<string>(BUILTIN_CHAT_MESSAGE_KINDS);
9
+ const CHAT_MESSAGE_KIND_ALIASES: Record<string, BuiltinChatMessageKind> = {
10
+ text: 'standard',
11
+ message: 'standard',
12
+ assistant: 'standard',
13
+ thinking: 'thought',
14
+ think: 'thought',
15
+ reasoning: 'thought',
16
+ reason: 'thought',
17
+ toolcall: 'tool',
18
+ tool_call: 'tool',
19
+ tooluse: 'tool',
20
+ tool_use: 'tool',
21
+ action: 'tool',
22
+ command: 'terminal',
23
+ cmd: 'terminal',
24
+ shell: 'terminal',
25
+ console: 'terminal',
26
+ };
27
+
28
+ function canonicalizeKindHint(value: string): string {
29
+ return value.trim().toLowerCase().replace(/[\s-]+/g, '_');
30
+ }
31
+
32
+ function resolveBuiltinOrAliasKind(kind: unknown): BuiltinChatMessageKind | null {
33
+ if (typeof kind !== 'string') return null;
34
+ const normalizedKind = canonicalizeKindHint(kind);
35
+ if (!normalizedKind) return null;
36
+ if (KNOWN_CHAT_MESSAGE_KINDS.has(normalizedKind)) return normalizedKind as BuiltinChatMessageKind;
37
+ return CHAT_MESSAGE_KIND_ALIASES[normalizedKind] || null;
38
+ }
39
+
40
+ function inferHintKind(value: unknown): BuiltinChatMessageKind | null {
41
+ const direct = resolveBuiltinOrAliasKind(value);
42
+ if (direct) return direct;
43
+ if (typeof value !== 'string') return null;
44
+ const normalized = canonicalizeKindHint(value);
45
+ if (!normalized) return null;
46
+ if (/thought|thinking|reasoning/.test(normalized)) return 'thought';
47
+ if (/tool/.test(normalized)) return 'tool';
48
+ if (/terminal|command|shell|console/.test(normalized)) return 'terminal';
49
+ return null;
50
+ }
51
+
52
+ function inferKindFromToolCalls(message: ChatMessage): BuiltinChatMessageKind | null {
53
+ const toolCalls = Array.isArray(message?.toolCalls) ? message.toolCalls : [];
54
+ if (toolCalls.length === 0) return null;
55
+ if (toolCalls.some((toolCall) => toolCall?.kind === 'think')) return 'thought';
56
+ if (toolCalls.some((toolCall) => toolCall?.kind === 'execute')) return 'terminal';
57
+ if (toolCalls.some((toolCall) => Array.isArray(toolCall?.content) && toolCall.content.some((entry) => entry?.type === 'terminal'))) {
58
+ return 'terminal';
59
+ }
60
+ return 'tool';
61
+ }
62
+
63
+ function inferMissingChatMessageKind(message: ChatMessage): BuiltinChatMessageKind | null {
64
+ const role = typeof message?.role === 'string' ? message.role.trim().toLowerCase() : '';
65
+ if (role === 'system') return 'system';
66
+
67
+ const meta = message?.meta && typeof message.meta === 'object' ? message.meta as Record<string, unknown> : undefined;
68
+ const hintCandidates: unknown[] = [
69
+ message?._sub,
70
+ message?._type,
71
+ meta?.label,
72
+ typeof message?.senderName === 'string' ? message.senderName : undefined,
73
+ ];
74
+
75
+ for (const candidate of hintCandidates) {
76
+ const inferred = inferHintKind(candidate);
77
+ if (inferred) return inferred;
78
+ }
79
+
80
+ const inferredFromToolCalls = inferKindFromToolCalls(message);
81
+ if (inferredFromToolCalls) return inferredFromToolCalls;
82
+ return null;
83
+ }
9
84
 
10
85
  export function isBuiltinChatMessageKind(kind: unknown): kind is BuiltinChatMessageKind {
11
- return typeof kind === 'string' && KNOWN_CHAT_MESSAGE_KINDS.has(kind.trim().toLowerCase());
86
+ return resolveBuiltinOrAliasKind(kind) !== null;
12
87
  }
13
88
 
14
89
  export function normalizeChatMessageKind(kind: unknown, role: unknown): ChatMessageKind {
15
- const normalizedKind = typeof kind === 'string' ? kind.trim().toLowerCase() : '';
16
- if (normalizedKind && KNOWN_CHAT_MESSAGE_KINDS.has(normalizedKind)) return normalizedKind as BuiltinChatMessageKind;
90
+ const resolvedKind = resolveBuiltinOrAliasKind(kind);
91
+ if (resolvedKind) return resolvedKind;
17
92
 
18
93
  const normalizedRole = typeof role === 'string' ? role.trim().toLowerCase() : '';
19
94
  return normalizedRole === 'system' ? 'system' : 'standard';
20
95
  }
21
96
 
97
+ export function resolveChatMessageKind<T extends ChatMessage>(message: T): ChatMessageKind {
98
+ const explicitKind = resolveBuiltinOrAliasKind(message?.kind);
99
+ if (explicitKind) return explicitKind;
100
+
101
+ const inferredKind = inferMissingChatMessageKind(message);
102
+ if (inferredKind) return inferredKind;
103
+ return normalizeChatMessageKind(message?.kind, message?.role);
104
+ }
105
+
22
106
  export function buildChatMessage<T extends Omit<ChatMessage, 'kind'> & { kind?: ChatMessageKind }>(message: T): T & { kind: ChatMessageKind } {
23
107
  return {
24
108
  ...message,
25
- kind: normalizeChatMessageKind(message?.kind, message?.role),
109
+ kind: resolveChatMessageKind(message as unknown as ChatMessage),
26
110
  };
27
111
  }
28
112
 
@@ -51,6 +135,27 @@ export function buildAssistantChatMessage<T extends Omit<ChatMessage, 'role' | '
51
135
  } as T & { role: 'assistant'; kind?: ChatMessageKind }) as T & { role: 'assistant'; kind: ChatMessageKind };
52
136
  }
53
137
 
138
+ export function buildThoughtChatMessage<T extends Omit<ChatMessage, 'role' | 'kind'> & { role?: 'assistant'; kind?: ChatMessageKind }>(message: T): (T & { role: 'assistant'; kind: ChatMessageKind }) {
139
+ return buildAssistantChatMessage({
140
+ ...message,
141
+ kind: message?.kind || 'thought',
142
+ } as T & { role?: 'assistant'; kind?: ChatMessageKind }) as T & { role: 'assistant'; kind: ChatMessageKind };
143
+ }
144
+
145
+ export function buildToolChatMessage<T extends Omit<ChatMessage, 'role' | 'kind'> & { role?: 'assistant'; kind?: ChatMessageKind }>(message: T): (T & { role: 'assistant'; kind: ChatMessageKind }) {
146
+ return buildAssistantChatMessage({
147
+ ...message,
148
+ kind: message?.kind || 'tool',
149
+ } as T & { role?: 'assistant'; kind?: ChatMessageKind }) as T & { role: 'assistant'; kind: ChatMessageKind };
150
+ }
151
+
152
+ export function buildTerminalChatMessage<T extends Omit<ChatMessage, 'role' | 'kind'> & { role?: 'assistant'; kind?: ChatMessageKind }>(message: T): (T & { role: 'assistant'; kind: ChatMessageKind }) {
153
+ return buildAssistantChatMessage({
154
+ ...message,
155
+ kind: message?.kind || 'terminal',
156
+ } as T & { role?: 'assistant'; kind?: ChatMessageKind }) as T & { role: 'assistant'; kind: ChatMessageKind };
157
+ }
158
+
54
159
  export function buildUserChatMessage<T extends Omit<ChatMessage, 'role' | 'kind'> & { role?: 'user'; kind?: ChatMessageKind }>(message: T): (T & { role: 'user'; kind: ChatMessageKind }) {
55
160
  return buildChatMessage({
56
161
  ...message,
@@ -336,11 +336,13 @@ export class IdeProviderInstance implements ProviderInstance {
336
336
  if (pm.receivedAt) prevByHash.set(h, pm.receivedAt);
337
337
  }
338
338
  const now = Date.now();
339
- const messages = chat.messages || [];
340
- for (const msg of messages) {
339
+ const rawMessages = chat.messages || [];
340
+ for (const msg of rawMessages) {
341
341
  const h = `${msg.role}:${(msg.content || '').slice(0, 100)}`;
342
342
  msg.receivedAt = prevByHash.get(h) || now;
343
343
  }
344
+ chat.messages = normalizeChatMessages(rawMessages as ChatMessage[]) as any;
345
+ const messages = chat.messages || [];
344
346
 
345
347
  // Filter messages by provider settings (showThinking, showToolCalls, showTerminal)
346
348
  if (messages.length > 0) {
@@ -0,0 +1,61 @@
1
+ export const DEFAULT_ACTIVE_CHAT_POLL_STATUSES = new Set([
2
+ 'generating',
3
+ 'waiting_approval',
4
+ 'starting',
5
+ ]);
6
+
7
+ export const DEFAULT_CHAT_TAIL_RECENT_MESSAGE_GRACE_MS = 8_000;
8
+
9
+ export interface HotChatSessionLike {
10
+ id?: string | null;
11
+ status?: unknown;
12
+ lastMessageAt?: unknown;
13
+ }
14
+
15
+ function parseMessageTimestamp(value: unknown): number {
16
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
17
+ if (typeof value === 'string') {
18
+ const parsed = Date.parse(value);
19
+ if (Number.isFinite(parsed)) return parsed;
20
+ }
21
+ return 0;
22
+ }
23
+
24
+ export function classifyHotChatSessionsForSubscriptionFlush(
25
+ sessions: HotChatSessionLike[],
26
+ previousHotSessionIds: ReadonlySet<string>,
27
+ options: {
28
+ now?: number;
29
+ recentMessageGraceMs?: number;
30
+ activeStatuses?: ReadonlySet<string>;
31
+ } = {},
32
+ ): { active: Set<string>; finalizing: Set<string> } {
33
+ const now = options.now ?? Date.now();
34
+ const recentMessageGraceMs = Math.max(
35
+ 0,
36
+ Number.isFinite(options.recentMessageGraceMs)
37
+ ? Number(options.recentMessageGraceMs)
38
+ : DEFAULT_CHAT_TAIL_RECENT_MESSAGE_GRACE_MS,
39
+ );
40
+ const activeStatuses = options.activeStatuses ?? DEFAULT_ACTIVE_CHAT_POLL_STATUSES;
41
+ const active = new Set<string>();
42
+
43
+ for (const session of sessions) {
44
+ const sessionId = typeof session?.id === 'string' ? session.id : '';
45
+ if (!sessionId) continue;
46
+
47
+ const status = String(session?.status || '').toLowerCase();
48
+ const lastMessageAt = parseMessageTimestamp(session?.lastMessageAt);
49
+ const recentlyUpdated = lastMessageAt > 0 && (now - lastMessageAt) <= recentMessageGraceMs;
50
+
51
+ if (activeStatuses.has(status) || recentlyUpdated) {
52
+ active.add(sessionId);
53
+ }
54
+ }
55
+
56
+ const finalizing = new Set(
57
+ Array.from(previousHotSessionIds).filter((sessionId) => !active.has(sessionId)),
58
+ );
59
+
60
+ return { active, finalizing };
61
+ }
@@ -189,6 +189,10 @@ function parseMessageTime(value: unknown): number {
189
189
  return 0;
190
190
  }
191
191
 
192
+ function getMessageEventTime(message: { receivedAt?: unknown; timestamp?: unknown } | null | undefined): number {
193
+ return parseMessageTime(message?.receivedAt) || parseMessageTime(message?.timestamp) || 0;
194
+ }
195
+
192
196
  function stringifyPreviewContent(content: unknown): string {
193
197
  if (typeof content === 'string') return content;
194
198
  if (Array.isArray(content)) {
@@ -233,6 +237,7 @@ function getLastDisplayMessage(session: {
233
237
  role?: string;
234
238
  content?: unknown;
235
239
  receivedAt?: number | string;
240
+ timestamp?: number | string;
236
241
  }> | null
237
242
  } | null
238
243
  }) {
@@ -247,7 +252,7 @@ function getLastDisplayMessage(session: {
247
252
  return {
248
253
  role,
249
254
  preview,
250
- receivedAt: parseMessageTime(candidate?.receivedAt),
255
+ receivedAt: getMessageEventTime(candidate),
251
256
  hash: simplePreviewHash(`${role}:${preview}`),
252
257
  };
253
258
  }
@@ -256,12 +261,12 @@ function getLastDisplayMessage(session: {
256
261
 
257
262
  function getSessionMessageUpdatedAt(session: {
258
263
  activeChat?: {
259
- messages?: Array<{ receivedAt?: number | string }> | null
264
+ messages?: Array<{ receivedAt?: number | string; timestamp?: number | string }> | null
260
265
  } | null
261
266
  }) {
262
267
  const lastMessage = session.activeChat?.messages?.at?.(-1);
263
268
  if (!lastMessage) return 0;
264
- return parseMessageTime(lastMessage.receivedAt) || 0;
269
+ return getMessageEventTime(lastMessage);
265
270
  }
266
271
 
267
272
  export function getSessionCompletionMarker(session: {
@@ -271,6 +276,7 @@ export function getSessionCompletionMarker(session: {
271
276
  id?: string;
272
277
  index?: number;
273
278
  receivedAt?: number | string;
279
+ timestamp?: number | string;
274
280
  _turnKey?: string;
275
281
  }> | null
276
282
  } | null
@@ -282,13 +288,13 @@ export function getSessionCompletionMarker(session: {
282
288
  if (typeof lastMessage._turnKey === 'string' && lastMessage._turnKey) return `turn:${lastMessage._turnKey}`;
283
289
  if (typeof lastMessage.id === 'string' && lastMessage.id) return `id:${lastMessage.id}`;
284
290
  if (typeof lastMessage.index === 'number' && Number.isFinite(lastMessage.index)) return `idx:${lastMessage.index}`;
285
- const timestamp = parseMessageTime(lastMessage.receivedAt);
291
+ const timestamp = getMessageEventTime(lastMessage);
286
292
  return timestamp > 0 ? `ts:${timestamp}` : '';
287
293
  }
288
294
 
289
295
  function getSessionLastUsedAt(session: {
290
296
  activeChat?: {
291
- messages?: Array<{ receivedAt?: number | string }> | null
297
+ messages?: Array<{ receivedAt?: number | string; timestamp?: number | string }> | null
292
298
  } | null
293
299
  lastUpdated?: number
294
300
  }) {