@adhdev/daemon-core 0.8.60 → 0.8.61

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.
@@ -0,0 +1,43 @@
1
+ import type { ChatMessage } from '../types.js';
2
+ export declare const BUILTIN_CHAT_MESSAGE_KINDS: readonly ["standard", "thought", "tool", "terminal", "system"];
3
+ export type BuiltinChatMessageKind = typeof BUILTIN_CHAT_MESSAGE_KINDS[number];
4
+ export type ChatMessageKind = BuiltinChatMessageKind | (string & {});
5
+ export declare function isBuiltinChatMessageKind(kind: unknown): kind is BuiltinChatMessageKind;
6
+ export declare function normalizeChatMessageKind(kind: unknown, role: unknown): ChatMessageKind;
7
+ export declare function buildChatMessage<T extends Omit<ChatMessage, 'kind'> & {
8
+ kind?: ChatMessageKind;
9
+ }>(message: T): T & {
10
+ kind: ChatMessageKind;
11
+ };
12
+ export declare function buildSystemChatMessage<T extends Omit<ChatMessage, 'role' | 'kind'> & {
13
+ role?: 'system';
14
+ kind?: ChatMessageKind;
15
+ }>(message: T): (T & {
16
+ role: 'system';
17
+ kind: ChatMessageKind;
18
+ });
19
+ export declare function buildRuntimeSystemChatMessage<T extends Omit<ChatMessage, 'role' | 'kind' | 'senderName'> & {
20
+ role?: 'system';
21
+ kind?: ChatMessageKind;
22
+ senderName?: string;
23
+ }>(message: T): (T & {
24
+ role: 'system';
25
+ kind: ChatMessageKind;
26
+ senderName: string;
27
+ });
28
+ export declare function buildAssistantChatMessage<T extends Omit<ChatMessage, 'role' | 'kind'> & {
29
+ role?: 'assistant';
30
+ kind?: ChatMessageKind;
31
+ }>(message: T): (T & {
32
+ role: 'assistant';
33
+ kind: ChatMessageKind;
34
+ });
35
+ export declare function buildUserChatMessage<T extends Omit<ChatMessage, 'role' | 'kind'> & {
36
+ role?: 'user';
37
+ kind?: ChatMessageKind;
38
+ }>(message: T): (T & {
39
+ role: 'user';
40
+ kind: ChatMessageKind;
41
+ });
42
+ export declare function normalizeChatMessage<T extends ChatMessage>(message: T): T;
43
+ export declare function normalizeChatMessages<T extends ChatMessage>(messages: T[] | null | undefined): T[];
@@ -89,6 +89,7 @@ export declare class CliProviderInstance implements ProviderInstance {
89
89
  private formatMarkerTimestamp;
90
90
  private maybeAppendRuntimeRecoveryMessage;
91
91
  private appendRuntimeSystemMessage;
92
+ private appendRuntimeMessage;
92
93
  private mergeConversationMessages;
93
94
  private formatApprovalRequestMessage;
94
95
  private promoteProviderSessionId;
@@ -7,6 +7,7 @@
7
7
  * - User custom providers use the same contracts
8
8
  */
9
9
  import type { ProviderSummaryMetadata } from '../shared-types.js';
10
+ import type { ChatMessageKind } from './chat-message-normalization.js';
10
11
  export interface ReadChatResult {
11
12
  messages: ChatMessage[];
12
13
  status: AgentStatus;
@@ -43,7 +44,7 @@ export interface ModalInfo {
43
44
  export interface ProviderEffectMessage {
44
45
  role?: 'system' | 'assistant' | 'user';
45
46
  content: string | MessagePart[];
46
- kind?: string;
47
+ kind?: ChatMessageKind;
47
48
  senderName?: string;
48
49
  }
49
50
  export interface ProviderEffectToast {
@@ -59,6 +60,9 @@ export interface ProviderEffectNotification {
59
60
  channels?: ProviderNotificationChannel[];
60
61
  preferenceKey?: ProviderNotificationPreferenceKey;
61
62
  bubbleContent?: string | MessagePart[];
63
+ bubbleKind?: ChatMessageKind;
64
+ bubbleRole?: 'system' | 'assistant' | 'user';
65
+ bubbleSenderName?: string;
62
66
  }
63
67
  export interface ProviderEffect {
64
68
  type: 'message' | 'toast' | 'notification';
@@ -1,7 +1,9 @@
1
1
  import type { ControlInvokeResult, ControlListResult, ControlSetResult, ProviderControlDef, ProviderEffect } from './contracts.js';
2
+ import type { ChatMessage } from '../types.js';
2
3
  export type ProviderControlValue = string | number | boolean;
3
4
  export declare function extractProviderControlValues(controls: ProviderControlDef[] | undefined, data: any): Record<string, ProviderControlValue> | undefined;
4
5
  export declare function normalizeProviderEffects(data: any): ProviderEffect[];
6
+ export declare function buildPersistedProviderEffectMessage(effect: ProviderEffect | null | undefined): ChatMessage | null;
5
7
  export declare function normalizeControlListResult(data: any): ControlListResult;
6
8
  export declare function normalizeControlSetResult(data: any): ControlSetResult;
7
9
  export declare function normalizeControlInvokeResult(data: any): ControlInvokeResult;
@@ -4,7 +4,7 @@
4
4
  * Manages IDE extensions (Cline, Roo Code, etc).
5
5
  * CDP webview discovery + agent stream collection moved here.
6
6
  */
7
- import type { ProviderModule } from './contracts.js';
7
+ import { type ProviderModule } from './contracts.js';
8
8
  import type { ProviderInstance, ProviderState, InstanceContext } from './provider-instance.js';
9
9
  export declare class ExtensionProviderInstance implements ProviderInstance {
10
10
  readonly type: string;
@@ -45,6 +45,7 @@ export declare class ExtensionProviderInstance implements ProviderInstance {
45
45
  private pushEvent;
46
46
  private applyProviderResponse;
47
47
  private appendRuntimeSystemMessage;
48
+ private appendRuntimeMessage;
48
49
  /**
49
50
  * Assign stable receivedAt to extension messages.
50
51
  * Same pattern as IdeProviderInstance.readChat() prevByHash —
@@ -8,7 +8,7 @@
8
8
  * IDE Instance manages child Extension Instances.
9
9
  * Daemon collects all via a single IDE Instance.getState() call.
10
10
  */
11
- import type { ProviderModule } from './contracts.js';
11
+ import { type ProviderModule } from './contracts.js';
12
12
  import type { ProviderInstance, ProviderState, InstanceContext } from './provider-instance.js';
13
13
  import { ExtensionProviderInstance } from './extension-provider-instance.js';
14
14
  export declare class IdeProviderInstance implements ProviderInstance {
@@ -60,6 +60,7 @@ export declare class IdeProviderInstance implements ProviderInstance {
60
60
  private pushEvent;
61
61
  private applyProviderResponse;
62
62
  private appendRuntimeSystemMessage;
63
+ private appendRuntimeMessage;
63
64
  private mergeConversationMessages;
64
65
  private getPersistedEffectContent;
65
66
  private getEffectDedupKey;
package/dist/types.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * When modifying this file, also update interface contracts in AGENT_PROTOCOL.md.
6
6
  */
7
7
  import type { StatusReportPayload, AvailableProviderInfo } from './shared-types.js';
8
+ import type { ChatMessageKind } from './providers/chat-message-normalization.js';
8
9
  /** Full status response from /api/v1/status and WS events */
9
10
  export interface StatusResponse extends StatusReportPayload {
10
11
  /** For standalone API compat */
@@ -23,7 +24,7 @@ export interface ChatMessage {
23
24
  role: string;
24
25
  /** Plain text (legacy) or canonical message parts */
25
26
  content: string | MessagePart[];
26
- kind?: string;
27
+ kind?: ChatMessageKind;
27
28
  id?: string;
28
29
  index?: number;
29
30
  timestamp?: number;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.8.60",
4
- "description": "ADHDev local session host core session registry, protocol, buffers",
3
+ "version": "0.8.61",
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",
7
7
  "exports": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.8.60",
4
- "description": "ADHDev daemon core CDP, IDE detection, providers, command execution",
3
+ "version": "0.8.61",
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
7
  "exports": {
@@ -21,6 +21,7 @@ import { LOG } from '../logging/logger.js';
21
21
  import type { AgentStreamState } from './types.js';
22
22
  import { formatAutoApprovalMessage, pickApprovalButton } from '../providers/approval-utils.js';
23
23
  import type { ProviderModule } from '../providers/contracts.js';
24
+ import { buildRuntimeSystemChatMessage } from '../providers/chat-message-normalization.js';
24
25
 
25
26
  interface ExtensionInstanceLike {
26
27
  type?: string;
@@ -229,12 +230,9 @@ export class AgentStreamPoller {
229
230
  type: 'message',
230
231
  id: effectId,
231
232
  persist: true,
232
- message: {
233
- role: 'system',
234
- senderName: 'System',
235
- kind: 'system',
233
+ message: buildRuntimeSystemChatMessage({
236
234
  content: formatAutoApprovalMessage(stream.activeModal?.message, buttonLabel),
237
- },
235
+ }),
238
236
  },
239
237
  ],
240
238
  };
@@ -44,6 +44,7 @@ import {
44
44
  type CliSessionStatus,
45
45
  type CliTraceEntry,
46
46
  } from './provider-cli-shared.js';
47
+ import { buildChatMessage } from '../providers/chat-message-normalization.js';
47
48
  import {
48
49
  buildCliParseInput,
49
50
  buildCliTraceParseSnapshot,
@@ -1297,11 +1298,10 @@ export class ProviderCliAdapter implements CliAdapter {
1297
1298
  && !this.activeModal;
1298
1299
  if (parsed && Array.isArray(parsed.messages)) {
1299
1300
  const hydratedMessages = shouldPreferCommittedMessages
1300
- ? this.committedMessages.map((message, index) => ({
1301
+ ? this.committedMessages.map((message, index) => buildChatMessage({
1301
1302
  ...message,
1302
1303
  id: message.id || `msg_${index}`,
1303
1304
  index: typeof message.index === 'number' ? message.index : index,
1304
- kind: message.kind || 'standard',
1305
1305
  receivedAt: typeof message.receivedAt === 'number'
1306
1306
  ? message.receivedAt
1307
1307
  : message.timestamp,
@@ -1326,13 +1326,13 @@ export class ProviderCliAdapter implements CliAdapter {
1326
1326
  id: 'cli_session',
1327
1327
  status: this.currentStatus,
1328
1328
  title: this.cliName,
1329
- messages: messages.slice(-50).map((message, index) => ({
1330
- id: `msg_${index}`,
1331
- role: message.role,
1332
- content: message.content,
1333
- timestamp: message.timestamp,
1334
- index,
1335
- kind: 'standard',
1329
+ messages: messages.slice(-50).map((message, index) => buildChatMessage({
1330
+ ...message,
1331
+ id: message.id || `msg_${index}`,
1332
+ index: typeof message.index === 'number' ? message.index : index,
1333
+ receivedAt: typeof message.receivedAt === 'number'
1334
+ ? message.receivedAt
1335
+ : message.timestamp,
1336
1336
  })),
1337
1337
  activeModal: this.activeModal,
1338
1338
  };
@@ -2,6 +2,7 @@ import * as os from 'os';
2
2
  import * as path from 'path';
3
3
  import { execSync } from 'child_process';
4
4
  import type { ProviderResumeCapability } from '../providers/contracts.js';
5
+ import type { ChatMessageKind } from '../providers/chat-message-normalization.js';
5
6
  import { sanitizeSpawnEnv } from './spawn-env.js';
6
7
 
7
8
  export interface CliChatMessage {
@@ -9,10 +10,10 @@ export interface CliChatMessage {
9
10
  content: string;
10
11
  timestamp?: number;
11
12
  receivedAt?: number;
12
- kind?: string;
13
+ kind?: ChatMessageKind;
13
14
  id?: string;
14
15
  index?: number;
15
- meta?: Record<string, any>;
16
+ meta?: Record<string, unknown>;
16
17
  senderName?: string;
17
18
  }
18
19
 
@@ -12,6 +12,7 @@ import { LOG } from '../logging/logger.js';
12
12
  import { recordDebugTrace } from '../logging/debug-trace.js';
13
13
  import type { ChatMessage } from '../types.js';
14
14
  import type { ReadChatCursor, ReadChatSyncMode, SessionTransport } from '../shared-types.js';
15
+ import { normalizeChatMessages } from '../providers/chat-message-normalization.js';
15
16
 
16
17
  const RECENT_SEND_WINDOW_MS = 1200;
17
18
  const recentSendByTarget = new Map<string, number>();
@@ -189,7 +190,7 @@ function normalizeReadChatCursor(args: any): Required<ReadChatCursor> {
189
190
 
190
191
  function normalizeReadChatMessages(payload: Record<string, any>): ChatMessage[] {
191
192
  const messages = Array.isArray(payload.messages) ? payload.messages as ChatMessage[] : [];
192
- return messages;
193
+ return normalizeChatMessages(messages);
193
194
  }
194
195
 
195
196
  function deriveHistoryDedupKey(message: ChatMessage & { _unitKey?: string; _turnKey?: string }): string | undefined {
@@ -12,6 +12,7 @@
12
12
  import * as fs from 'fs';
13
13
  import * as path from 'path';
14
14
  import * as os from 'os';
15
+ import { buildRuntimeSystemChatMessage } from '../providers/chat-message-normalization.js';
15
16
 
16
17
  const HISTORY_DIR = path.join(os.homedir(), '.adhdev', 'history');
17
18
  const RETAIN_DAYS = 30;
@@ -314,11 +315,11 @@ export class ChatHistoryWriter {
314
315
  this.appendNewMessages(
315
316
  agentType,
316
317
  [{
317
- role: 'system',
318
- kind: 'system',
319
- content,
320
- receivedAt: options.receivedAt,
321
- senderName: options.senderName,
318
+ ...buildRuntimeSystemChatMessage({
319
+ content,
320
+ receivedAt: options.receivedAt,
321
+ senderName: options.senderName,
322
+ }),
322
323
  historyDedupKey: options.dedupKey,
323
324
  }],
324
325
  options.sessionTitle,
package/src/index.ts CHANGED
@@ -200,6 +200,19 @@ export type { ProviderModule, CdpTargetFilter, ProviderResumeCapability, InputEn
200
200
  export type { ProviderSourceConfigSnapshot, ProviderSourceConfigUpdate } from './config/provider-source-config.js';
201
201
  export { parseProviderSourceConfigUpdate } from './config/provider-source-config.js';
202
202
  export { normalizeInputEnvelope, normalizeMessageParts, flattenMessageParts } from './providers/io-contracts.js';
203
+ export {
204
+ BUILTIN_CHAT_MESSAGE_KINDS,
205
+ isBuiltinChatMessageKind,
206
+ normalizeChatMessageKind,
207
+ buildChatMessage,
208
+ buildSystemChatMessage,
209
+ buildRuntimeSystemChatMessage,
210
+ buildAssistantChatMessage,
211
+ buildUserChatMessage,
212
+ normalizeChatMessage,
213
+ normalizeChatMessages,
214
+ } from './providers/chat-message-normalization.js';
215
+ export type { BuiltinChatMessageKind, ChatMessageKind } from './providers/chat-message-normalization.js';
203
216
  export { VersionArchive, detectAllVersions } from './providers/version-archive.js';
204
217
  export type { ProviderVersionInfo, VersionHistory } from './providers/version-archive.js';
205
218
 
@@ -51,15 +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
55
  import { LOG } from '../logging/logger.js';
56
+ import type { ChatMessage } from '../types.js';
55
57
 
56
58
  // ─── Internal Display Types (for dashboard) ────────────────────────────
57
59
 
58
- interface AcpMessage {
60
+ type AcpMessage = ChatMessage & {
59
61
  role: 'user' | 'assistant' | 'system';
60
62
  /** Rich content blocks (ACP standard) or plain text (legacy) */
61
63
  content: string | ContentBlock[];
62
- timestamp?: number;
63
64
  /** Tool calls associated with this message */
64
65
  toolCalls?: ToolCallInfo[];
65
66
  }
@@ -293,26 +294,23 @@ export class AcpProviderInstance implements ProviderInstance {
293
294
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
294
295
 
295
296
  // Recent 50 messages
296
- const recentMessages = this.messages.slice(-50).map(m => {
297
+ const recentMessages = normalizeChatMessages(this.messages.slice(-50).map(m => {
297
298
  const content = this.truncateContent(m.content);
298
- return {
299
- role: m.role,
299
+ return buildChatMessage({
300
+ ...m,
300
301
  content,
301
- timestamp: m.timestamp,
302
- toolCalls: m.toolCalls,
303
- };
304
- });
302
+ });
303
+ }));
305
304
 
306
305
  // generating during partial response add
307
306
  if (this.currentStatus === 'generating' && (this.partialContent || this.partialBlocks.length > 0)) {
308
307
  const blocks = this.buildPartialBlocks();
309
308
  if (blocks.length > 0) {
310
- recentMessages.push({
311
- role: 'assistant',
309
+ recentMessages.push(buildAssistantChatMessage({
312
310
  content: blocks,
313
311
  timestamp: Date.now(),
314
312
  toolCalls: this.turnToolCalls.length > 0 ? [...this.turnToolCalls] : undefined,
315
- });
313
+ }));
316
314
  }
317
315
  }
318
316
 
@@ -326,7 +324,7 @@ export class AcpProviderInstance implements ProviderInstance {
326
324
  id: this.sessionId || `${this.type}_${this.workingDir}`,
327
325
  title: `${this.provider.name} · ${dirName}`,
328
326
  status: this.currentStatus,
329
- messages: recentMessages,
327
+ messages: normalizeChatMessages(recentMessages as any),
330
328
  activeModal: this.currentStatus === 'waiting_approval' ? {
331
329
  message: this.activeToolCalls.find(t => t.status === 'running')?.name || 'Permission requested',
332
330
  buttons: ['Approve', 'Reject'],
@@ -975,11 +973,10 @@ export class AcpProviderInstance implements ProviderInstance {
975
973
  : [{ type: 'text', text }];
976
974
 
977
975
  // Add user message locally (store as ContentBlock[])
978
- this.messages.push({
979
- role: 'user',
976
+ this.messages.push(buildUserChatMessage({
980
977
  content: contentBlocks && contentBlocks.length > 0 ? contentBlocks : text,
981
978
  timestamp: Date.now(),
982
- });
979
+ }));
983
980
 
984
981
  this.currentStatus = 'generating';
985
982
  this.partialContent = '';
@@ -1181,11 +1178,11 @@ export class AcpProviderInstance implements ProviderInstance {
1181
1178
  }
1182
1179
 
1183
1180
  if (content.trim()) {
1184
- this.messages.push({
1181
+ this.messages.push(buildChatMessage({
1185
1182
  role: m.role || 'assistant',
1186
1183
  content: content.trim(),
1187
1184
  timestamp: Date.now(),
1188
- });
1185
+ }));
1189
1186
  this.partialContent = '';
1190
1187
  }
1191
1188
  }
@@ -1271,14 +1268,13 @@ export class AcpProviderInstance implements ProviderInstance {
1271
1268
  }).filter(b => b.type !== 'text' || (b.type === 'text' && b.text.trim()));
1272
1269
 
1273
1270
  if (finalBlocks.length > 0) {
1274
- this.messages.push({
1275
- role: 'assistant',
1271
+ this.messages.push(buildAssistantChatMessage({
1276
1272
  content: finalBlocks.length === 1 && finalBlocks[0].type === 'text'
1277
1273
  ? (finalBlocks[0] as {type: 'text', text: string}).text // single text → string (backward compat)
1278
1274
  : finalBlocks,
1279
1275
  timestamp: Date.now(),
1280
1276
  toolCalls: this.turnToolCalls.length > 0 ? [...this.turnToolCalls] : undefined,
1281
- });
1277
+ }));
1282
1278
  }
1283
1279
  this.partialContent = '';
1284
1280
  this.partialBlocks = [];
@@ -1347,11 +1343,10 @@ export class AcpProviderInstance implements ProviderInstance {
1347
1343
  private appendSystemMessage(content: string, timestamp = Date.now()): void {
1348
1344
  const normalizedContent = String(content || '').trim();
1349
1345
  if (!normalizedContent) return;
1350
- this.messages.push({
1351
- role: 'system',
1346
+ this.messages.push(buildRuntimeSystemChatMessage({
1352
1347
  content: normalizedContent,
1353
1348
  timestamp,
1354
- });
1349
+ }));
1355
1350
  if (this.messages.length > 200) {
1356
1351
  this.messages = this.messages.slice(-100);
1357
1352
  }
@@ -0,0 +1,68 @@
1
+ import type { ChatMessage } from '../types.js';
2
+
3
+ export const BUILTIN_CHAT_MESSAGE_KINDS = ['standard', 'thought', 'tool', 'terminal', 'system'] as const;
4
+
5
+ export type BuiltinChatMessageKind = typeof BUILTIN_CHAT_MESSAGE_KINDS[number];
6
+ export type ChatMessageKind = BuiltinChatMessageKind | (string & {});
7
+
8
+ const KNOWN_CHAT_MESSAGE_KINDS = new Set<string>(BUILTIN_CHAT_MESSAGE_KINDS);
9
+
10
+ export function isBuiltinChatMessageKind(kind: unknown): kind is BuiltinChatMessageKind {
11
+ return typeof kind === 'string' && KNOWN_CHAT_MESSAGE_KINDS.has(kind.trim().toLowerCase());
12
+ }
13
+
14
+ 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;
17
+
18
+ const normalizedRole = typeof role === 'string' ? role.trim().toLowerCase() : '';
19
+ return normalizedRole === 'system' ? 'system' : 'standard';
20
+ }
21
+
22
+ export function buildChatMessage<T extends Omit<ChatMessage, 'kind'> & { kind?: ChatMessageKind }>(message: T): T & { kind: ChatMessageKind } {
23
+ return {
24
+ ...message,
25
+ kind: normalizeChatMessageKind(message?.kind, message?.role),
26
+ };
27
+ }
28
+
29
+ export function buildSystemChatMessage<T extends Omit<ChatMessage, 'role' | 'kind'> & { role?: 'system'; kind?: ChatMessageKind }>(message: T): (T & { role: 'system'; kind: ChatMessageKind }) {
30
+ return buildChatMessage({
31
+ ...message,
32
+ role: 'system',
33
+ kind: message?.kind || 'system',
34
+ } as T & { role: 'system'; kind?: ChatMessageKind }) as T & { role: 'system'; kind: ChatMessageKind };
35
+ }
36
+
37
+ export function buildRuntimeSystemChatMessage<T extends Omit<ChatMessage, 'role' | 'kind' | 'senderName'> & { role?: 'system'; kind?: ChatMessageKind; senderName?: string }>(message: T): (T & { role: 'system'; kind: ChatMessageKind; senderName: string }) {
38
+ return buildSystemChatMessage({
39
+ ...message,
40
+ senderName: typeof message?.senderName === 'string' && message.senderName.trim()
41
+ ? message.senderName
42
+ : 'System',
43
+ } as T & { role?: 'system'; kind?: ChatMessageKind; senderName?: string }) as T & { role: 'system'; kind: ChatMessageKind; senderName: string };
44
+ }
45
+
46
+ export function buildAssistantChatMessage<T extends Omit<ChatMessage, 'role' | 'kind'> & { role?: 'assistant'; kind?: ChatMessageKind }>(message: T): (T & { role: 'assistant'; kind: ChatMessageKind }) {
47
+ return buildChatMessage({
48
+ ...message,
49
+ role: 'assistant',
50
+ kind: message?.kind || 'standard',
51
+ } as T & { role: 'assistant'; kind?: ChatMessageKind }) as T & { role: 'assistant'; kind: ChatMessageKind };
52
+ }
53
+
54
+ export function buildUserChatMessage<T extends Omit<ChatMessage, 'role' | 'kind'> & { role?: 'user'; kind?: ChatMessageKind }>(message: T): (T & { role: 'user'; kind: ChatMessageKind }) {
55
+ return buildChatMessage({
56
+ ...message,
57
+ role: 'user',
58
+ kind: message?.kind || 'standard',
59
+ } as T & { role: 'user'; kind?: ChatMessageKind }) as T & { role: 'user'; kind: ChatMessageKind };
60
+ }
61
+
62
+ export function normalizeChatMessage<T extends ChatMessage>(message: T): T {
63
+ return buildChatMessage(message) as T;
64
+ }
65
+
66
+ export function normalizeChatMessages<T extends ChatMessage>(messages: T[] | null | undefined): T[] {
67
+ return (Array.isArray(messages) ? messages : []).map((message) => normalizeChatMessage(message));
68
+ }
@@ -10,7 +10,7 @@ import * as path from 'path';
10
10
  import * as crypto from 'crypto';
11
11
  import * as fs from 'fs';
12
12
  import { createRequire } from 'node:module';
13
- import { normalizeInputEnvelope, type ProviderModule } from './contracts.js';
13
+ import { normalizeInputEnvelope, type ProviderModule, flattenContent } from './contracts.js';
14
14
  import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
15
15
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
16
16
  import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
@@ -19,10 +19,11 @@ import { StatusMonitor } from './status-monitor.js';
19
19
  import { ChatHistoryWriter, readChatHistory } from '../config/chat-history.js';
20
20
  import { LOG } from '../logging/logger.js';
21
21
  import type { ChatMessage } from '../types.js';
22
- import { normalizeProviderEffects } from './control-effects.js';
22
+ import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
23
23
  import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.js';
24
24
  import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
25
25
  import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
26
+ import { buildChatMessage, buildRuntimeSystemChatMessage, normalizeChatMessages } from './chat-message-normalization.js';
26
27
 
27
28
  let CachedDatabaseSync: (new (path: string, options?: { readOnly?: boolean }) => {
28
29
  prepare(sql: string): { get(...params: Array<string | number>): unknown };
@@ -628,8 +629,8 @@ export class CliProviderInstance implements ProviderInstance {
628
629
  this.appliedEffectKeys.add(effectKey);
629
630
 
630
631
  if (effect.persist !== false) {
631
- const persisted = this.getPersistedEffectContent(effect);
632
- if (persisted) this.appendRuntimeSystemMessage(persisted, effectKey);
632
+ const persistedMessage = buildPersistedProviderEffectMessage(effect);
633
+ if (persistedMessage) this.appendRuntimeMessage(persistedMessage, effectKey);
633
634
  }
634
635
 
635
636
  if (effect.type === 'message' && effect.message) {
@@ -772,43 +773,55 @@ export class CliProviderInstance implements ProviderInstance {
772
773
  }
773
774
 
774
775
  private appendRuntimeSystemMessage(content: string, dedupKey: string, receivedAt = Date.now()): void {
775
- const normalizedContent = String(content || '').trim();
776
- if (!normalizedContent) return;
776
+ this.appendRuntimeMessage(buildRuntimeSystemChatMessage({
777
+ content,
778
+ receivedAt,
779
+ timestamp: receivedAt,
780
+ }), dedupKey);
781
+ }
782
+
783
+ private appendRuntimeMessage(message: ChatMessage, dedupKey: string): void {
784
+ const normalizedMessage = buildChatMessage({
785
+ ...message,
786
+ receivedAt: typeof message.receivedAt === 'number' ? message.receivedAt : (message.timestamp || Date.now()),
787
+ timestamp: typeof message.timestamp === 'number' ? message.timestamp : (message.receivedAt || Date.now()),
788
+ } as ChatMessage);
789
+ const normalizedContent = typeof normalizedMessage.content === 'string'
790
+ ? normalizedMessage.content.trim()
791
+ : flattenContent(normalizedMessage.content).trim();
792
+ if (!normalizedContent && (!Array.isArray(normalizedMessage.content) || normalizedMessage.content.length === 0)) return;
777
793
  if (this.runtimeMessages.some((entry) => entry.key === dedupKey)) return;
778
794
 
779
795
  this.runtimeMessages.push({
780
796
  key: dedupKey,
781
- message: {
782
- role: 'system',
783
- senderName: 'System',
784
- content: normalizedContent,
785
- receivedAt,
786
- timestamp: receivedAt,
787
- },
797
+ message: normalizedMessage,
788
798
  });
789
799
  if (this.runtimeMessages.length > 50) {
790
800
  this.runtimeMessages = this.runtimeMessages.slice(-50);
791
801
  }
792
802
 
793
- this.historyWriter.appendNewMessages(
794
- this.type,
795
- [{
796
- role: 'system',
797
- senderName: 'System',
798
- content: normalizedContent,
799
- receivedAt,
800
- historyDedupKey: dedupKey,
801
- }],
802
- this.adapter.getScriptParsedStatus?.()?.title || this.workingDir.split('/').filter(Boolean).pop() || 'session',
803
- this.instanceId,
804
- this.providerSessionId,
805
- );
803
+ if (normalizedContent) {
804
+ this.historyWriter.appendNewMessages(
805
+ this.type,
806
+ [{
807
+ role: normalizedMessage.role,
808
+ senderName: normalizedMessage.senderName,
809
+ kind: normalizedMessage.kind,
810
+ content: normalizedContent,
811
+ receivedAt: normalizedMessage.receivedAt || normalizedMessage.timestamp,
812
+ historyDedupKey: dedupKey,
813
+ }],
814
+ this.adapter.getScriptParsedStatus?.()?.title || this.workingDir.split('/').filter(Boolean).pop() || 'session',
815
+ this.instanceId,
816
+ this.providerSessionId,
817
+ );
818
+ }
806
819
  }
807
820
 
808
821
  private mergeConversationMessages(parsedMessages: any[]): ChatMessage[] {
809
- if (this.runtimeMessages.length === 0) return parsedMessages;
822
+ if (this.runtimeMessages.length === 0) return normalizeChatMessages(parsedMessages);
810
823
 
811
- return [...parsedMessages, ...this.runtimeMessages.map((entry) => entry.message)]
824
+ return normalizeChatMessages([...parsedMessages, ...this.runtimeMessages.map((entry) => entry.message)]
812
825
  .map((message, index) => ({ message, index }))
813
826
  .sort((a, b) => {
814
827
  const aTime = a.message.receivedAt || a.message.timestamp || 0;
@@ -816,7 +829,7 @@ export class CliProviderInstance implements ProviderInstance {
816
829
  if (aTime !== bTime) return aTime - bTime;
817
830
  return a.index - b.index;
818
831
  })
819
- .map((entry) => entry.message);
832
+ .map((entry) => entry.message));
820
833
  }
821
834
 
822
835
  private formatApprovalRequestMessage(modalMessage?: string, buttons?: string[]): string {