@adhdev/daemon-core 0.8.59 → 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.59",
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.59",
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,
@@ -183,6 +184,7 @@ export class ProviderCliAdapter implements CliAdapter {
183
184
  private static readonly MAX_TRACE_ENTRIES = 250;
184
185
  private readonly providerResolutionMeta: ProviderResolutionMeta;
185
186
  private static readonly IDLE_FINISH_CONFIRM_MS = 2000;
187
+ private static readonly HERMES_IDLE_FINISH_CONFIRM_MS = 5000;
186
188
  private static readonly STATUS_ACTIVITY_HOLD_MS = 2000;
187
189
  private static readonly FINISH_RETRY_DELAY_MS = 300;
188
190
  private static readonly MAX_FINISH_RETRIES = 2;
@@ -192,6 +194,18 @@ export class ProviderCliAdapter implements CliAdapter {
192
194
  this.structuredMessages = [...this.committedMessages];
193
195
  }
194
196
 
197
+ private getIdleFinishConfirmMs(): number {
198
+ return this.cliType === 'hermes-cli'
199
+ ? ProviderCliAdapter.HERMES_IDLE_FINISH_CONFIRM_MS
200
+ : ProviderCliAdapter.IDLE_FINISH_CONFIRM_MS;
201
+ }
202
+
203
+ private getStatusActivityHoldMs(): number {
204
+ return this.cliType === 'hermes-cli'
205
+ ? ProviderCliAdapter.HERMES_IDLE_FINISH_CONFIRM_MS
206
+ : ProviderCliAdapter.STATUS_ACTIVITY_HOLD_MS;
207
+ }
208
+
195
209
  private setStatus(status: CliSessionStatus['status'], trigger?: string): void {
196
210
  const prev = this.currentStatus;
197
211
  if (prev === status) return;
@@ -216,6 +230,7 @@ export class ProviderCliAdapter implements CliAdapter {
216
230
 
217
231
  private armIdleFinishCandidate(assistantLength: number): void {
218
232
  const now = Date.now();
233
+ const idleFinishConfirmMs = this.getIdleFinishConfirmMs();
219
234
  this.idleFinishCandidate = {
220
235
  armedAt: now,
221
236
  lastOutputAt: this.lastOutputAt,
@@ -224,7 +239,7 @@ export class ProviderCliAdapter implements CliAdapter {
224
239
  assistantLength,
225
240
  };
226
241
  this.recordTrace('idle_candidate_armed', {
227
- confirmMs: ProviderCliAdapter.IDLE_FINISH_CONFIRM_MS,
242
+ confirmMs: idleFinishConfirmMs,
228
243
  candidate: this.idleFinishCandidate,
229
244
  ...buildCliTraceParseSnapshot({
230
245
  accumulatedBuffer: this.accumulatedBuffer,
@@ -239,7 +254,7 @@ export class ProviderCliAdapter implements CliAdapter {
239
254
  this.settleTimer = null;
240
255
  this.settledBuffer = this.recentOutputBuffer;
241
256
  this.evaluateSettled();
242
- }, ProviderCliAdapter.IDLE_FINISH_CONFIRM_MS);
257
+ }, idleFinishConfirmMs);
243
258
  }
244
259
 
245
260
 
@@ -719,8 +734,9 @@ export class ProviderCliAdapter implements CliAdapter {
719
734
  private hasRecentInteractiveActivity(now: number): boolean {
720
735
  const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
721
736
  const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : Number.MAX_SAFE_INTEGER;
722
- return quietForMs < ProviderCliAdapter.STATUS_ACTIVITY_HOLD_MS
723
- || screenStableMs < ProviderCliAdapter.STATUS_ACTIVITY_HOLD_MS;
737
+ const holdMs = this.getStatusActivityHoldMs();
738
+ return quietForMs < holdMs
739
+ || screenStableMs < holdMs;
724
740
  }
725
741
 
726
742
  private getStartupConfirmationModal(screenText: string): { message: string; buttons: string[] } | null {
@@ -901,6 +917,7 @@ export class ProviderCliAdapter implements CliAdapter {
901
917
  }
902
918
 
903
919
  const recentInteractiveActivity = this.hasRecentInteractiveActivity(now);
920
+ const statusActivityHoldMs = this.getStatusActivityHoldMs();
904
921
  const shouldHoldGenerating =
905
922
  scriptStatus === 'idle'
906
923
  && this.isWaitingForResponse
@@ -921,7 +938,7 @@ export class ProviderCliAdapter implements CliAdapter {
921
938
  recentInteractiveActivity,
922
939
  lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
923
940
  lastScreenChangeAt: this.lastScreenChangeAt,
924
- holdMs: ProviderCliAdapter.STATUS_ACTIVITY_HOLD_MS,
941
+ holdMs: statusActivityHoldMs,
925
942
  ...buildCliTraceParseSnapshot({
926
943
  accumulatedBuffer: this.accumulatedBuffer,
927
944
  accumulatedRawBuffer: this.accumulatedRawBuffer,
@@ -1013,8 +1030,9 @@ export class ProviderCliAdapter implements CliAdapter {
1013
1030
  const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
1014
1031
  const hasAssistantTurn = !!lastParsedAssistant;
1015
1032
  const assistantLength = lastParsedAssistant?.content?.length || 0;
1016
- const idleQuietThresholdMs = Math.max(2000, this.timeouts.outputSettle);
1017
- const idleStableThresholdMs = 2000;
1033
+ const idleFinishConfirmMs = this.getIdleFinishConfirmMs();
1034
+ const idleQuietThresholdMs = Math.max(idleFinishConfirmMs, this.timeouts.outputSettle);
1035
+ const idleStableThresholdMs = idleFinishConfirmMs;
1018
1036
  const idleReady = visibleIdlePrompt
1019
1037
  && !modal
1020
1038
  && hasAssistantTurn
@@ -1026,7 +1044,7 @@ export class ProviderCliAdapter implements CliAdapter {
1026
1044
  && candidate.lastOutputAt === this.lastOutputAt
1027
1045
  && candidate.lastScreenChangeAt === this.lastScreenChangeAt
1028
1046
  && assistantLength >= candidate.assistantLength
1029
- && (now - candidate.armedAt) >= ProviderCliAdapter.IDLE_FINISH_CONFIRM_MS;
1047
+ && (now - candidate.armedAt) >= idleFinishConfirmMs;
1030
1048
  const canFinishImmediately = idleReady && candidateQuiet;
1031
1049
 
1032
1050
  this.recordTrace('idle_decision', {
@@ -1039,7 +1057,7 @@ export class ProviderCliAdapter implements CliAdapter {
1039
1057
  idleQuietThresholdMs,
1040
1058
  idleStableThresholdMs,
1041
1059
  idleReady,
1042
- idleFinishConfirmMs: ProviderCliAdapter.IDLE_FINISH_CONFIRM_MS,
1060
+ idleFinishConfirmMs,
1043
1061
  idleFinishCandidate: candidate,
1044
1062
  candidateQuiet,
1045
1063
  canFinishImmediately,
@@ -1280,11 +1298,10 @@ export class ProviderCliAdapter implements CliAdapter {
1280
1298
  && !this.activeModal;
1281
1299
  if (parsed && Array.isArray(parsed.messages)) {
1282
1300
  const hydratedMessages = shouldPreferCommittedMessages
1283
- ? this.committedMessages.map((message, index) => ({
1301
+ ? this.committedMessages.map((message, index) => buildChatMessage({
1284
1302
  ...message,
1285
1303
  id: message.id || `msg_${index}`,
1286
1304
  index: typeof message.index === 'number' ? message.index : index,
1287
- kind: message.kind || 'standard',
1288
1305
  receivedAt: typeof message.receivedAt === 'number'
1289
1306
  ? message.receivedAt
1290
1307
  : message.timestamp,
@@ -1309,13 +1326,13 @@ export class ProviderCliAdapter implements CliAdapter {
1309
1326
  id: 'cli_session',
1310
1327
  status: this.currentStatus,
1311
1328
  title: this.cliName,
1312
- messages: messages.slice(-50).map((message, index) => ({
1313
- id: `msg_${index}`,
1314
- role: message.role,
1315
- content: message.content,
1316
- timestamp: message.timestamp,
1317
- index,
1318
- 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,
1319
1336
  })),
1320
1337
  activeModal: this.activeModal,
1321
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
+ }