@adhdev/daemon-core 0.8.60 → 0.8.62

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 (34) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +1 -0
  2. package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
  3. package/dist/cli-adapters/provider-cli-shared.d.ts +5 -2
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +521 -343
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +511 -343
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/providers/chat-message-normalization.d.ts +43 -0
  10. package/dist/providers/cli-provider-instance.d.ts +1 -0
  11. package/dist/providers/contracts.d.ts +5 -1
  12. package/dist/providers/control-effects.d.ts +2 -0
  13. package/dist/providers/extension-provider-instance.d.ts +2 -1
  14. package/dist/providers/ide-provider-instance.d.ts +2 -1
  15. package/dist/types.d.ts +2 -1
  16. package/node_modules/@adhdev/session-host-core/package.json +2 -2
  17. package/package.json +2 -2
  18. package/src/agent-stream/poller.ts +3 -5
  19. package/src/cli-adapters/provider-cli-adapter.ts +32 -10
  20. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  21. package/src/cli-adapters/provider-cli-parse.ts +3 -0
  22. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  23. package/src/cli-adapters/provider-cli-shared.ts +5 -2
  24. package/src/commands/chat-commands.ts +2 -1
  25. package/src/config/chat-history.ts +6 -5
  26. package/src/index.ts +13 -0
  27. package/src/providers/acp-provider-instance.ts +19 -24
  28. package/src/providers/chat-message-normalization.ts +68 -0
  29. package/src/providers/cli-provider-instance.ts +42 -29
  30. package/src/providers/contracts.ts +5 -1
  31. package/src/providers/control-effects.ts +72 -0
  32. package/src/providers/extension-provider-instance.ts +43 -31
  33. package/src/providers/ide-provider-instance.ts +42 -30
  34. package/src/types.ts +2 -1
@@ -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.62",
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.62",
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,
@@ -738,6 +739,19 @@ export class ProviderCliAdapter implements CliAdapter {
738
739
  || screenStableMs < holdMs;
739
740
  }
740
741
 
742
+ private shouldDeferIdleTimeoutFinish(): boolean {
743
+ if (!this.isWaitingForResponse || this.currentStatus === 'waiting_approval') {
744
+ return false;
745
+ }
746
+ const latestStatus = this.runDetectStatus(this.recentOutputBuffer) || this.currentStatus;
747
+ if (latestStatus === 'generating') {
748
+ this.settledBuffer = this.recentOutputBuffer;
749
+ this.evaluateSettled();
750
+ return true;
751
+ }
752
+ return false;
753
+ }
754
+
741
755
  private getStartupConfirmationModal(screenText: string): { message: string; buttons: string[] } | null {
742
756
  const text = sanitizeTerminalText(String(screenText || ''));
743
757
  if (!text.trim()) return null;
@@ -929,6 +943,7 @@ export class ProviderCliAdapter implements CliAdapter {
929
943
  if (this.idleTimeout) clearTimeout(this.idleTimeout);
930
944
  this.idleTimeout = setTimeout(() => {
931
945
  if (this.isWaitingForResponse && this.currentStatus !== 'waiting_approval') {
946
+ if (this.shouldDeferIdleTimeoutFinish()) return;
932
947
  this.finishResponse();
933
948
  }
934
949
  }, this.timeouts.generatingIdle);
@@ -962,6 +977,7 @@ export class ProviderCliAdapter implements CliAdapter {
962
977
  if (this.idleTimeout) clearTimeout(this.idleTimeout);
963
978
  this.idleTimeout = setTimeout(() => {
964
979
  if (this.isWaitingForResponse && this.currentStatus !== 'waiting_approval') {
980
+ if (this.shouldDeferIdleTimeoutFinish()) return;
965
981
  this.finishResponse();
966
982
  }
967
983
  }, this.timeouts.generatingIdle);
@@ -1011,7 +1027,10 @@ export class ProviderCliAdapter implements CliAdapter {
1011
1027
  // Reset idle timeout
1012
1028
  if (this.idleTimeout) clearTimeout(this.idleTimeout);
1013
1029
  this.idleTimeout = setTimeout(() => {
1014
- if (this.isWaitingForResponse) this.finishResponse();
1030
+ if (this.isWaitingForResponse) {
1031
+ if (this.shouldDeferIdleTimeoutFinish()) return;
1032
+ this.finishResponse();
1033
+ }
1015
1034
  }, this.timeouts.generatingIdle);
1016
1035
  this.onStatusChange?.();
1017
1036
  return;
@@ -1090,6 +1109,7 @@ export class ProviderCliAdapter implements CliAdapter {
1090
1109
  if (this.idleTimeout) clearTimeout(this.idleTimeout);
1091
1110
  this.idleTimeout = setTimeout(() => {
1092
1111
  if (this.isWaitingForResponse && this.currentStatus !== 'waiting_approval') {
1112
+ if (this.shouldDeferIdleTimeoutFinish()) return;
1093
1113
  this.clearIdleFinishCandidate('idle_timeout_finish');
1094
1114
  this.finishResponse();
1095
1115
  }
@@ -1218,6 +1238,7 @@ export class ProviderCliAdapter implements CliAdapter {
1218
1238
  tail: text.slice(-500),
1219
1239
  screenText,
1220
1240
  rawBuffer: this.accumulatedRawBuffer,
1241
+ isWaitingForResponse: this.isWaitingForResponse,
1221
1242
  screen: buildCliScreenSnapshot(screenText),
1222
1243
  tailScreen: buildCliScreenSnapshot(text.slice(-500)),
1223
1244
  });
@@ -1297,11 +1318,10 @@ export class ProviderCliAdapter implements CliAdapter {
1297
1318
  && !this.activeModal;
1298
1319
  if (parsed && Array.isArray(parsed.messages)) {
1299
1320
  const hydratedMessages = shouldPreferCommittedMessages
1300
- ? this.committedMessages.map((message, index) => ({
1321
+ ? this.committedMessages.map((message, index) => buildChatMessage({
1301
1322
  ...message,
1302
1323
  id: message.id || `msg_${index}`,
1303
1324
  index: typeof message.index === 'number' ? message.index : index,
1304
- kind: message.kind || 'standard',
1305
1325
  receivedAt: typeof message.receivedAt === 'number'
1306
1326
  ? message.receivedAt
1307
1327
  : message.timestamp,
@@ -1326,13 +1346,13 @@ export class ProviderCliAdapter implements CliAdapter {
1326
1346
  id: 'cli_session',
1327
1347
  status: this.currentStatus,
1328
1348
  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',
1349
+ messages: messages.slice(-50).map((message, index) => buildChatMessage({
1350
+ ...message,
1351
+ id: message.id || `msg_${index}`,
1352
+ index: typeof message.index === 'number' ? message.index : index,
1353
+ receivedAt: typeof message.receivedAt === 'number'
1354
+ ? message.receivedAt
1355
+ : message.timestamp,
1336
1356
  })),
1337
1357
  activeModal: this.activeModal,
1338
1358
  };
@@ -1350,6 +1370,7 @@ export class ProviderCliAdapter implements CliAdapter {
1350
1370
  terminalScreenText: this.terminalScreen.getText(),
1351
1371
  baseMessages: this.committedMessages,
1352
1372
  partialResponse: this.responseBuffer,
1373
+ isWaitingForResponse: this.isWaitingForResponse,
1353
1374
  scope: this.currentTurnScope,
1354
1375
  runtimeSettings: this.runtimeSettings,
1355
1376
  });
@@ -1369,6 +1390,7 @@ export class ProviderCliAdapter implements CliAdapter {
1369
1390
  terminalScreenText: this.terminalScreen.getText(),
1370
1391
  baseMessages,
1371
1392
  partialResponse,
1393
+ isWaitingForResponse: this.isWaitingForResponse,
1372
1394
  scope,
1373
1395
  runtimeSettings: this.runtimeSettings,
1374
1396
  });
@@ -24,6 +24,7 @@ export declare function buildCliParseInput(options: {
24
24
  terminalScreenText: string;
25
25
  baseMessages: CliChatMessage[];
26
26
  partialResponse: string;
27
+ isWaitingForResponse?: boolean;
27
28
  scope?: TurnParseScope | null;
28
29
  runtimeSettings: Record<string, any>;
29
30
  }): CliScriptInput;
@@ -123,6 +123,7 @@ export function buildCliParseInput(options: {
123
123
  terminalScreenText: string;
124
124
  baseMessages: CliChatMessage[];
125
125
  partialResponse: string;
126
+ isWaitingForResponse?: boolean;
126
127
  scope?: TurnParseScope | null;
127
128
  runtimeSettings: Record<string, any>;
128
129
  }): CliScriptInput {
@@ -133,6 +134,7 @@ export function buildCliParseInput(options: {
133
134
  terminalScreenText,
134
135
  baseMessages,
135
136
  partialResponse,
137
+ isWaitingForResponse,
136
138
  scope,
137
139
  runtimeSettings,
138
140
  } = options;
@@ -155,6 +157,7 @@ export function buildCliParseInput(options: {
155
157
  recentScreen: buildCliScreenSnapshot(recentBuffer),
156
158
  messages: [...baseMessages],
157
159
  partialResponse,
160
+ isWaitingForResponse,
158
161
  promptText: scope?.prompt || '',
159
162
  settings: { ...runtimeSettings },
160
163
  };
@@ -62,6 +62,7 @@ export interface CliScriptInput {
62
62
  recentScreen: CliScreenSnapshot;
63
63
  messages: CliChatMessage[];
64
64
  partialResponse: string;
65
+ isWaitingForResponse?: boolean;
65
66
  promptText?: string;
66
67
  settings?: Record<string, any>;
67
68
  args?: Record<string, any>;
@@ -70,6 +71,7 @@ export interface CliStatusInput {
70
71
  tail: string;
71
72
  screenText?: string;
72
73
  rawBuffer?: string;
74
+ isWaitingForResponse?: boolean;
73
75
  screen: CliScreenSnapshot;
74
76
  tailScreen: CliScreenSnapshot;
75
77
  }
@@ -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
 
@@ -65,6 +66,7 @@ export interface CliScriptInput {
65
66
  recentScreen: CliScreenSnapshot;
66
67
  messages: CliChatMessage[];
67
68
  partialResponse: string;
69
+ isWaitingForResponse?: boolean;
68
70
  promptText?: string;
69
71
  settings?: Record<string, any>;
70
72
  args?: Record<string, any>;
@@ -74,6 +76,7 @@ export interface CliStatusInput {
74
76
  tail: string;
75
77
  screenText?: string;
76
78
  rawBuffer?: string;
79
+ isWaitingForResponse?: boolean;
77
80
  screen: CliScreenSnapshot;
78
81
  tailScreen: CliScreenSnapshot;
79
82
  }
@@ -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
+ }