@adhdev/daemon-core 0.9.76-rc.8 → 0.9.76

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 (65) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +5 -2
  2. package/dist/cli-adapters/provider-cli-runtime.d.ts +1 -0
  3. package/dist/cli-adapters/provider-cli-shared.d.ts +24 -0
  4. package/dist/commands/chat-commands.d.ts +2 -0
  5. package/dist/commands/cli-manager.d.ts +17 -4
  6. package/dist/commands/mesh-coordinator.d.ts +2 -0
  7. package/dist/commands/router.d.ts +11 -0
  8. package/dist/config/mesh-config.d.ts +3 -0
  9. package/dist/git/git-types.d.ts +1 -1
  10. package/dist/git/git-worktree.d.ts +64 -0
  11. package/dist/git/index.d.ts +2 -0
  12. package/dist/index.d.ts +4 -4
  13. package/dist/index.js +2427 -561
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.mjs +2432 -584
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/mesh/coordinator-prompt.d.ts +1 -0
  18. package/dist/mesh/mesh-events.d.ts +18 -0
  19. package/dist/providers/chat-message-normalization.d.ts +40 -0
  20. package/dist/providers/cli-provider-instance.d.ts +7 -1
  21. package/dist/providers/contracts.d.ts +20 -1
  22. package/dist/providers/io-contracts.d.ts +17 -1
  23. package/dist/providers/provider-input-support.d.ts +18 -2
  24. package/dist/providers/provider-instance-manager.d.ts +1 -0
  25. package/dist/providers/provider-instance.d.ts +4 -0
  26. package/dist/repo-mesh-types.d.ts +34 -0
  27. package/dist/session-host/runtime-support.d.ts +2 -1
  28. package/dist/shared-types.d.ts +8 -0
  29. package/dist/types.d.ts +9 -0
  30. package/package.json +4 -5
  31. package/src/chat/subscription-updates.ts +3 -1
  32. package/src/cli-adapters/provider-cli-adapter.ts +44 -11
  33. package/src/cli-adapters/provider-cli-runtime.ts +3 -2
  34. package/src/cli-adapters/provider-cli-shared.ts +201 -15
  35. package/src/commands/chat-commands.ts +166 -16
  36. package/src/commands/cli-manager.ts +78 -5
  37. package/src/commands/handler.ts +13 -4
  38. package/src/commands/mesh-coordinator.ts +155 -5
  39. package/src/commands/router.d.ts +1 -0
  40. package/src/commands/router.ts +606 -32
  41. package/src/config/mesh-config.ts +27 -2
  42. package/src/git/git-commands.ts +5 -1
  43. package/src/git/git-types.ts +1 -0
  44. package/src/git/git-worktree.ts +214 -0
  45. package/src/git/index.ts +14 -0
  46. package/src/index.ts +20 -1
  47. package/src/mesh/coordinator-prompt.ts +36 -14
  48. package/src/mesh/mesh-events.ts +173 -42
  49. package/src/providers/acp-provider-instance.ts +118 -30
  50. package/src/providers/chat-message-normalization.ts +241 -0
  51. package/src/providers/cli-provider-instance.d.ts +2 -0
  52. package/src/providers/cli-provider-instance.ts +219 -13
  53. package/src/providers/contracts.ts +25 -1
  54. package/src/providers/io-contracts.ts +63 -5
  55. package/src/providers/provider-input-support.ts +125 -1
  56. package/src/providers/provider-instance-manager.ts +20 -1
  57. package/src/providers/provider-instance.ts +4 -0
  58. package/src/providers/provider-schema.ts +38 -8
  59. package/src/providers/read-chat-contract.ts +8 -0
  60. package/src/repo-mesh-types.ts +38 -0
  61. package/src/session-host/runtime-support.ts +55 -7
  62. package/src/shared-types.ts +8 -0
  63. package/src/status/builders.ts +5 -3
  64. package/src/status/reporter.ts +6 -0
  65. package/src/types.ts +9 -0
@@ -5,6 +5,43 @@ export const BUILTIN_CHAT_MESSAGE_KINDS = ['standard', 'thought', 'tool', 'termi
5
5
  export type BuiltinChatMessageKind = typeof BUILTIN_CHAT_MESSAGE_KINDS[number];
6
6
  export type ChatMessageKind = BuiltinChatMessageKind | (string & {});
7
7
 
8
+ export const CHAT_MESSAGE_VISIBILITIES = ['user', 'debug', 'internal', 'hidden'] as const;
9
+ export const CHAT_MESSAGE_TRANSCRIPT_VISIBILITIES = ['visible', 'chat', 'user', 'debug', 'internal', 'hidden'] as const;
10
+ export const CHAT_MESSAGE_AUDIENCES = ['chat', 'debug', 'trace', 'internal'] as const;
11
+ export const CHAT_MESSAGE_SOURCES = [
12
+ 'assistant_text',
13
+ 'tool_call',
14
+ 'terminal_command',
15
+ 'runtime_activity',
16
+ 'runtime_status',
17
+ 'provider_chrome',
18
+ 'control',
19
+ ] as const;
20
+ export const CHAT_MESSAGE_ACTIVITY_SOURCES = ['tool_call', 'terminal_command', 'runtime_activity'] as const;
21
+ export const CHAT_MESSAGE_INTERNAL_SOURCES = ['runtime_status', 'provider_chrome', 'control'] as const;
22
+
23
+ export type ChatMessageVisibility = typeof CHAT_MESSAGE_VISIBILITIES[number] | (string & {});
24
+ export type ChatMessageTranscriptVisibility = typeof CHAT_MESSAGE_TRANSCRIPT_VISIBILITIES[number] | (string & {});
25
+ export type ChatMessageAudience = typeof CHAT_MESSAGE_AUDIENCES[number] | (string & {});
26
+ export type ChatMessageSource = typeof CHAT_MESSAGE_SOURCES[number] | (string & {});
27
+ export type ChatMessageTranscriptSurface = 'chat' | 'activity' | 'internal';
28
+
29
+ export interface ChatMessageVisibilityClassification {
30
+ surface: ChatMessageTranscriptSurface;
31
+ isUserFacing: boolean;
32
+ isActivityFacing: boolean;
33
+ isInternal: boolean;
34
+ explicitUserFacing: boolean;
35
+ explicitHidden: boolean;
36
+ role: string;
37
+ kind: ChatMessageKind;
38
+ visibility: string;
39
+ transcriptVisibility: string;
40
+ audience: string;
41
+ source: string;
42
+ }
43
+
44
+
8
45
  const KNOWN_CHAT_MESSAGE_KINDS = new Set<string>(BUILTIN_CHAT_MESSAGE_KINDS);
9
46
  const CHAT_MESSAGE_KIND_ALIASES: Record<string, BuiltinChatMessageKind> = {
10
47
  text: 'standard',
@@ -171,3 +208,207 @@ export function normalizeChatMessage<T extends ChatMessage>(message: T): T {
171
208
  export function normalizeChatMessages<T extends ChatMessage>(messages: T[] | null | undefined): T[] {
172
209
  return (Array.isArray(messages) ? messages : []).map((message) => normalizeChatMessage(message));
173
210
  }
211
+
212
+ function readMessageMeta(message: ChatMessage): Record<string, unknown> | null {
213
+ const meta = message?.meta;
214
+ return meta && typeof meta === 'object' && !Array.isArray(meta)
215
+ ? meta as Record<string, unknown>
216
+ : null;
217
+ }
218
+
219
+ function readStringField(value: unknown): string {
220
+ return typeof value === 'string' ? value.trim().toLowerCase() : '';
221
+ }
222
+
223
+ function readRecordField(message: ChatMessage, meta: Record<string, unknown> | null, key: string): unknown {
224
+ const record = message as ChatMessage & Record<string, unknown>;
225
+ return record[key] ?? meta?.[key];
226
+ }
227
+
228
+ function readVisibilityField(message: ChatMessage, meta: Record<string, unknown> | null): string {
229
+ return readStringField(readRecordField(message, meta, 'visibility'));
230
+ }
231
+
232
+ function readTranscriptVisibilityField(message: ChatMessage, meta: Record<string, unknown> | null): string {
233
+ const record = message as ChatMessage & Record<string, unknown>;
234
+ return readStringField(record.transcriptVisibility ?? meta?.transcriptVisibility ?? record.visibility ?? meta?.visibility);
235
+ }
236
+
237
+ const EXPLICIT_HIDDEN_VISIBILITIES = new Set(['hidden', 'debug', 'internal']);
238
+ const EXPLICIT_VISIBLE_VISIBILITIES = new Set(['visible', 'user', 'chat']);
239
+ const HIDDEN_AUDIENCES = new Set(['debug', 'trace', 'internal']);
240
+ const ACTIVITY_SOURCE_SET = new Set<string>(CHAT_MESSAGE_ACTIVITY_SOURCES);
241
+ const INTERNAL_SOURCE_SET = new Set<string>(CHAT_MESSAGE_INTERNAL_SOURCES);
242
+
243
+ function hasBooleanMarker(message: ChatMessage, meta: Record<string, unknown> | null, keys: string[]): boolean {
244
+ const record = message as ChatMessage & Record<string, unknown>;
245
+ return keys.some((key) => record[key] === true || meta?.[key] === true);
246
+ }
247
+
248
+ function isActivityKind(kind: ChatMessageKind): boolean {
249
+ return kind === 'thought' || kind === 'tool' || kind === 'terminal';
250
+ }
251
+
252
+ function isOrdinaryVisibleTurn(message: ChatMessage, role: string, kind: ChatMessageKind): boolean {
253
+ if (role === 'user' || role === 'human') return kind === 'standard' || kind === '';
254
+ if (role === 'assistant') return kind === 'standard' || kind === '';
255
+ return false;
256
+ }
257
+
258
+ /**
259
+ * Shared transcript visibility protocol for all ADHDev provider chat messages.
260
+ *
261
+ * Producers can stamp visibility/audience/source/userFacing/internal/debug either
262
+ * at the top level or under `meta`. Consumers should use this classifier instead
263
+ * of matching command text, icons, provider names, or terminal UI fragments.
264
+ */
265
+ export function classifyChatMessageVisibility(message: ChatMessage | null | undefined): ChatMessageVisibilityClassification {
266
+ if (!message) {
267
+ return {
268
+ surface: 'internal',
269
+ isUserFacing: false,
270
+ isActivityFacing: false,
271
+ isInternal: true,
272
+ explicitUserFacing: false,
273
+ explicitHidden: true,
274
+ role: '',
275
+ kind: 'standard',
276
+ visibility: '',
277
+ transcriptVisibility: '',
278
+ audience: '',
279
+ source: '',
280
+ };
281
+ }
282
+
283
+ const meta = readMessageMeta(message);
284
+ const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
285
+ const kind = resolveChatMessageKind(message);
286
+ const visibility = readVisibilityField(message, meta);
287
+ const transcriptVisibility = readTranscriptVisibilityField(message, meta);
288
+ const audience = readStringField(readRecordField(message, meta, 'audience'));
289
+ const source = readStringField(readRecordField(message, meta, 'source'));
290
+ const explicitHidden = EXPLICIT_HIDDEN_VISIBILITIES.has(visibility)
291
+ || EXPLICIT_HIDDEN_VISIBILITIES.has(transcriptVisibility)
292
+ || HIDDEN_AUDIENCES.has(audience)
293
+ || hasBooleanMarker(message, meta, ['internal', 'isInternal', 'debug', 'statusOnly', 'controlOnly']);
294
+ const explicitUserFacing = EXPLICIT_VISIBLE_VISIBILITIES.has(visibility)
295
+ || EXPLICIT_VISIBLE_VISIBILITIES.has(transcriptVisibility)
296
+ || audience === 'chat'
297
+ || hasBooleanMarker(message, meta, ['userFacing']);
298
+
299
+ if (explicitHidden) {
300
+ const activityLike = isActivityKind(kind) || ACTIVITY_SOURCE_SET.has(source);
301
+ return {
302
+ surface: activityLike ? 'activity' : 'internal',
303
+ isUserFacing: false,
304
+ isActivityFacing: activityLike,
305
+ isInternal: !activityLike,
306
+ explicitUserFacing,
307
+ explicitHidden,
308
+ role,
309
+ kind,
310
+ visibility,
311
+ transcriptVisibility,
312
+ audience,
313
+ source,
314
+ };
315
+ }
316
+
317
+ if (explicitUserFacing) {
318
+ return {
319
+ surface: 'chat',
320
+ isUserFacing: true,
321
+ isActivityFacing: false,
322
+ isInternal: false,
323
+ explicitUserFacing,
324
+ explicitHidden,
325
+ role,
326
+ kind,
327
+ visibility,
328
+ transcriptVisibility,
329
+ audience,
330
+ source,
331
+ };
332
+ }
333
+
334
+ if (INTERNAL_SOURCE_SET.has(source) || role === 'system' || kind === 'system') {
335
+ return {
336
+ surface: 'internal',
337
+ isUserFacing: false,
338
+ isActivityFacing: false,
339
+ isInternal: true,
340
+ explicitUserFacing,
341
+ explicitHidden,
342
+ role,
343
+ kind,
344
+ visibility,
345
+ transcriptVisibility,
346
+ audience,
347
+ source,
348
+ };
349
+ }
350
+
351
+ if (ACTIVITY_SOURCE_SET.has(source) || isActivityKind(kind)) {
352
+ return {
353
+ surface: 'activity',
354
+ isUserFacing: false,
355
+ isActivityFacing: true,
356
+ isInternal: false,
357
+ explicitUserFacing,
358
+ explicitHidden,
359
+ role,
360
+ kind,
361
+ visibility,
362
+ transcriptVisibility,
363
+ audience,
364
+ source,
365
+ };
366
+ }
367
+
368
+ const isUserFacing = isOrdinaryVisibleTurn(message, role, kind);
369
+ return {
370
+ surface: isUserFacing ? 'chat' : 'internal',
371
+ isUserFacing,
372
+ isActivityFacing: false,
373
+ isInternal: !isUserFacing,
374
+ explicitUserFacing,
375
+ explicitHidden,
376
+ role,
377
+ kind,
378
+ visibility,
379
+ transcriptVisibility,
380
+ audience,
381
+ source,
382
+ };
383
+ }
384
+
385
+ export function isUserFacingChatMessage(message: ChatMessage | null | undefined): boolean {
386
+ return classifyChatMessageVisibility(message).isUserFacing;
387
+ }
388
+
389
+ export function isActivityChatMessage(message: ChatMessage | null | undefined): boolean {
390
+ return classifyChatMessageVisibility(message).isActivityFacing;
391
+ }
392
+
393
+ export function isInternalChatMessage(message: ChatMessage | null | undefined): boolean {
394
+ return classifyChatMessageVisibility(message).isInternal;
395
+ }
396
+
397
+ export function filterUserFacingChatMessages<T extends ChatMessage>(messages: T[] | null | undefined): T[] {
398
+ return (Array.isArray(messages) ? messages : []).filter((message) => isUserFacingChatMessage(message));
399
+ }
400
+
401
+ export function filterActivityChatMessages<T extends ChatMessage>(messages: T[] | null | undefined): T[] {
402
+ return (Array.isArray(messages) ? messages : []).filter((message) => isActivityChatMessage(message));
403
+ }
404
+
405
+ export function filterInternalChatMessages<T extends ChatMessage>(messages: T[] | null | undefined): T[] {
406
+ return (Array.isArray(messages) ? messages : []).filter((message) => isInternalChatMessage(message));
407
+ }
408
+
409
+ export function filterChatMessagesByVisibility<T extends ChatMessage>(
410
+ messages: T[] | null | undefined,
411
+ surface: ChatMessageTranscriptSurface,
412
+ ): T[] {
413
+ return (Array.isArray(messages) ? messages : []).filter((message) => classifyChatMessageVisibility(message).surface === surface);
414
+ }
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { ProviderModule } from './contracts.js';
8
8
  import type { ProviderInstance, ProviderState, InstanceContext } from './provider-instance.js';
9
+ import type { ChatMessage } from '../types.js';
9
10
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
10
11
  import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
11
12
  export declare class CliProviderInstance implements ProviderInstance {
@@ -77,6 +78,7 @@ export declare class CliProviderInstance implements ProviderInstance {
77
78
  private formatMarkerTimestamp;
78
79
  private maybeAppendRuntimeRecoveryMessage;
79
80
  private appendRuntimeSystemMessage;
81
+ mergeRuntimeChatMessages(parsedMessages: ChatMessage[]): ChatMessage[];
80
82
  private mergeConversationMessages;
81
83
  private formatApprovalRequestMessage;
82
84
  private promoteProviderSessionId;
@@ -10,8 +10,8 @@ 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, flattenContent } from './contracts.js';
14
- import { assertTextOnlyInput } from './provider-input-support.js';
13
+ import { normalizeInputEnvelope, type ProviderModule, flattenContent, type InputEnvelope, type InputPart } from './contracts.js';
14
+ import { assertProviderSupportsDeclaredInput, getEffectiveMessageInputSupport } from './provider-input-support.js';
15
15
  import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, ProviderErrorReason, HotChatSessionState, SessionModalState } from './provider-instance.js';
16
16
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
17
17
  import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
@@ -25,7 +25,7 @@ import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.
25
25
  import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
26
26
  import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
27
27
  import { normalizeProviderSessionId } from './provider-session-id.js';
28
- import { buildChatMessage, buildRuntimeSystemChatMessage, normalizeChatMessages } from './chat-message-normalization.js';
28
+ import { buildChatMessage, buildRuntimeSystemChatMessage, isUserFacingChatMessage, normalizeChatMessages, resolveChatMessageKind } from './chat-message-normalization.js';
29
29
 
30
30
  type PersistableCliHistoryMessage = {
31
31
  role: string;
@@ -35,6 +35,125 @@ type PersistableCliHistoryMessage = {
35
35
  receivedAt?: number;
36
36
  };
37
37
 
38
+ const IMAGE_MIME_EXTENSIONS: Record<string, string> = {
39
+ 'image/png': '.png',
40
+ 'image/jpeg': '.jpg',
41
+ 'image/jpg': '.jpg',
42
+ 'image/gif': '.gif',
43
+ 'image/webp': '.webp',
44
+ 'image/bmp': '.bmp',
45
+ 'image/tiff': '.tiff',
46
+ 'image/svg+xml': '.svg',
47
+ };
48
+
49
+ function filePathFromUri(uri: string): string | null {
50
+ if (!uri) return null;
51
+ if (uri.startsWith('file://')) {
52
+ try {
53
+ return decodeURIComponent(new URL(uri).pathname);
54
+ } catch {
55
+ return uri.slice('file://'.length);
56
+ }
57
+ }
58
+ if (path.isAbsolute(uri)) return uri;
59
+ return null;
60
+ }
61
+
62
+ function extensionForImageMime(mimeType: string): string {
63
+ return IMAGE_MIME_EXTENSIONS[mimeType.toLowerCase()] || '.img';
64
+ }
65
+
66
+ function safeInputImageBasename(index: number, mimeType: string): string {
67
+ const extension = extensionForImageMime(mimeType);
68
+ const suffix = crypto.randomBytes(6).toString('hex');
69
+ return `adhdev-input-image-${Date.now()}-${index}-${suffix}${extension}`;
70
+ }
71
+
72
+ function materializeImageDataPart(part: Extract<InputPart, { type: 'image' }>, index: number, dir: string): string | null {
73
+ if (!part.data) return null;
74
+ const rawData = part.data.includes(',') ? part.data.split(',').pop() || '' : part.data;
75
+ if (!rawData) return null;
76
+ fs.mkdirSync(dir, { recursive: true });
77
+ const filePath = path.join(dir, safeInputImageBasename(index, part.mimeType));
78
+ fs.writeFileSync(filePath, Buffer.from(rawData, 'base64'));
79
+ cleanupStaleMaterializedImages(dir);
80
+ return filePath;
81
+ }
82
+
83
+ const MATERIALIZED_IMAGE_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
84
+ const MATERIALIZED_IMAGE_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
85
+ let lastMaterializedImageCleanupAt = 0;
86
+
87
+ function cleanupStaleMaterializedImages(dir: string): void {
88
+ const now = Date.now();
89
+ if (now - lastMaterializedImageCleanupAt < MATERIALIZED_IMAGE_CLEANUP_INTERVAL_MS) return;
90
+ lastMaterializedImageCleanupAt = now;
91
+ try {
92
+ const entries = fs.readdirSync(dir);
93
+ for (const entry of entries) {
94
+ if (!entry.startsWith('adhdev-input-image-')) continue;
95
+ const fullPath = path.join(dir, entry);
96
+ try {
97
+ const stat = fs.statSync(fullPath);
98
+ if (now - stat.mtimeMs > MATERIALIZED_IMAGE_MAX_AGE_MS) {
99
+ fs.unlinkSync(fullPath);
100
+ }
101
+ } catch { /* file may have been removed concurrently */ }
102
+ }
103
+ } catch { /* dir may not exist or be inaccessible */ }
104
+ }
105
+
106
+ export function buildCliStructuredInputPrompt(
107
+ input: InputEnvelope,
108
+ options: { materializeDir?: string } = {},
109
+ ): string {
110
+ const promptParts: string[] = [];
111
+ const imageRefs: string[] = [];
112
+ const resourceRefs: string[] = [];
113
+ const materializeDir = options.materializeDir || path.join(os.tmpdir(), 'adhdev-input-media');
114
+
115
+ input.parts.forEach((part, index) => {
116
+ if (part.type === 'text' && part.text.trim()) {
117
+ promptParts.push(part.text.trim());
118
+ return;
119
+ }
120
+
121
+ if (part.type === 'image') {
122
+ const localPath = typeof part.uri === 'string' ? filePathFromUri(part.uri) : null;
123
+ const materializedPath = !localPath && part.data ? materializeImageDataPart(part, index, materializeDir) : null;
124
+ const ref = localPath || materializedPath || part.uri || '';
125
+ if (ref) imageRefs.push(ref);
126
+ if (part.alt?.trim()) promptParts.push(part.alt.trim());
127
+ return;
128
+ }
129
+
130
+ if (part.type === 'resource_link') {
131
+ resourceRefs.push([part.title, part.name, part.description, part.uri].filter(Boolean).join('\n'));
132
+ return;
133
+ }
134
+
135
+ if (part.type === 'resource') {
136
+ resourceRefs.push([part.name, part.text, part.uri].filter(Boolean).join('\n'));
137
+ }
138
+ });
139
+
140
+ // Only use textFallback when no explicit text parts were collected — it is
141
+ // the flattened version of the same parts, so appending it alongside them
142
+ // would duplicate the content for multipart inputs.
143
+ const hasExplicitTextParts = input.parts.some((part) => part.type === 'text' && part.text.trim());
144
+ if (!hasExplicitTextParts && input.textFallback.trim()) {
145
+ promptParts.push(input.textFallback.trim());
146
+ }
147
+
148
+ const ordered = [
149
+ ...imageRefs,
150
+ ...promptParts,
151
+ ...resourceRefs,
152
+ ].filter((value, index, values) => value.trim().length > 0 && values.indexOf(value) === index);
153
+
154
+ return ordered.join('\n');
155
+ }
156
+
38
157
  function normalizePersistableCliHistoryContent(content: unknown): string {
39
158
  return flattenContent(content as any).replace(/\s+/g, ' ').trim();
40
159
  }
@@ -214,6 +333,7 @@ export class CliProviderInstance implements ProviderInstance {
214
333
  options?: {
215
334
  providerSessionId?: string;
216
335
  launchMode?: 'new' | 'resume' | 'manual';
336
+ extraEnv?: Record<string, string>;
217
337
  onProviderSessionResolved?: (info: {
218
338
  instanceId: string;
219
339
  providerType: string;
@@ -230,7 +350,7 @@ export class CliProviderInstance implements ProviderInstance {
230
350
  this.providerSessionId = options?.providerSessionId;
231
351
  this.launchMode = options?.launchMode || 'new';
232
352
  this.onProviderSessionResolved = options?.onProviderSessionResolved;
233
- this.adapter = new ProviderCliAdapter(provider as CliProviderModule, workingDir, cliArgs, transportFactory);
353
+ this.adapter = new ProviderCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory);
234
354
  this.monitor = new StatusMonitor();
235
355
  this.historyWriter = new ChatHistoryWriter();
236
356
  }
@@ -475,6 +595,7 @@ export class CliProviderInstance implements ProviderInstance {
475
595
  resume: this.provider.resume,
476
596
  controlValues: surface.controlValues,
477
597
  providerControls: this.provider.controls,
598
+ messageInput: getEffectiveMessageInputSupport(this.provider),
478
599
  summaryMetadata: surface.summaryMetadata as any,
479
600
  errorMessage: this.errorMessage,
480
601
  errorReason: this.errorReason,
@@ -531,9 +652,10 @@ export class CliProviderInstance implements ProviderInstance {
531
652
  onEvent(event: string, data?: any): void {
532
653
  if (event === 'send_message') {
533
654
  const input = normalizeInputEnvelope(data);
534
- assertTextOnlyInput(this.provider, input);
535
- if (input.textFallback) {
536
- void this.adapter.sendMessage(input.textFallback).catch((e: any) => {
655
+ assertProviderSupportsDeclaredInput(this.provider, input);
656
+ const promptText = buildCliStructuredInputPrompt(input);
657
+ if (promptText) {
658
+ void this.adapter.sendMessage(promptText).catch((e: any) => {
537
659
  LOG.warn('CLI', `[${this.type}] send_message failed: ${e?.message || e}`);
538
660
  });
539
661
  }
@@ -737,7 +859,29 @@ export class CliProviderInstance implements ProviderInstance {
737
859
  }
738
860
 
739
861
  private pushEvent(event: ProviderEvent): void {
740
- this.events.push(event);
862
+ const enrichedEvent: ProviderEvent = {
863
+ ...event,
864
+ instanceId: typeof event.instanceId === 'string' && event.instanceId.trim()
865
+ ? event.instanceId
866
+ : this.instanceId,
867
+ targetSessionId: typeof event.targetSessionId === 'string' && event.targetSessionId.trim()
868
+ ? event.targetSessionId
869
+ : this.instanceId,
870
+ providerType: typeof event.providerType === 'string' && event.providerType.trim()
871
+ ? event.providerType
872
+ : this.type,
873
+ workspaceName: typeof event.workspaceName === 'string' && event.workspaceName.trim()
874
+ ? event.workspaceName
875
+ : this.workingDir,
876
+ providerSessionId: typeof event.providerSessionId === 'string' && event.providerSessionId.trim()
877
+ ? event.providerSessionId
878
+ : this.providerSessionId,
879
+ };
880
+ if (this.context?.emitProviderEvent) {
881
+ this.context.emitProviderEvent(enrichedEvent);
882
+ return;
883
+ }
884
+ this.events.push(enrichedEvent);
741
885
  }
742
886
 
743
887
  private flushEvents(): ProviderEvent[] {
@@ -977,15 +1121,77 @@ export class CliProviderInstance implements ProviderInstance {
977
1121
  }
978
1122
  }
979
1123
 
1124
+ mergeRuntimeChatMessages(parsedMessages: ChatMessage[]): ChatMessage[] {
1125
+ return this.mergeConversationMessages(parsedMessages);
1126
+ }
1127
+
980
1128
  private mergeConversationMessages(parsedMessages: any[]): ChatMessage[] {
981
1129
  if (this.runtimeMessages.length === 0) return normalizeChatMessages(parsedMessages);
982
1130
 
983
- return normalizeChatMessages([...parsedMessages, ...this.runtimeMessages.map((entry) => entry.message)]
984
- .map((message, index) => ({ message, index }))
1131
+ type MergeEntry = { message: ChatMessage; index: number; source: 'parsed' | 'runtime'; runtimeKey?: string };
1132
+ const parsedEntries: MergeEntry[] = parsedMessages.map((message, index) => ({
1133
+ message,
1134
+ index,
1135
+ source: 'parsed',
1136
+ }));
1137
+ const runtimeEntries: MergeEntry[] = this.runtimeMessages.map((entry, index) => ({
1138
+ message: entry.message,
1139
+ index: parsedMessages.length + index,
1140
+ source: 'runtime',
1141
+ runtimeKey: entry.key,
1142
+ }));
1143
+ const getTime = (message: ChatMessage): number => {
1144
+ const value = typeof message.receivedAt === 'number'
1145
+ ? message.receivedAt
1146
+ : typeof message.timestamp === 'number'
1147
+ ? message.timestamp
1148
+ : 0;
1149
+ return Number.isFinite(value) && value > 0 ? value : 0;
1150
+ };
1151
+
1152
+ const getRole = (message: ChatMessage): string => typeof message.role === 'string'
1153
+ ? message.role.trim().toLowerCase()
1154
+ : '';
1155
+ const isRuntimeOverlay = (entry: MergeEntry): boolean => {
1156
+ if (entry.source !== 'runtime') return false;
1157
+ const key = typeof entry.runtimeKey === 'string' ? entry.runtimeKey.trim().toLowerCase() : '';
1158
+ if (key.startsWith('auto_approval:')) return true;
1159
+ return !isUserFacingChatMessage(entry.message);
1160
+ };
1161
+ const shouldKeepParsedBeforeUntimedRuntime = (message: ChatMessage): boolean => {
1162
+ const role = getRole(message);
1163
+ return role === 'user' || role === 'human';
1164
+ };
1165
+ const shouldKeepParsedAfterUntimedRuntime = (message: ChatMessage): boolean => {
1166
+ const role = getRole(message);
1167
+ if (role !== 'assistant') return false;
1168
+ const kind = resolveChatMessageKind(message);
1169
+ return kind === 'standard' || kind === 'terminal';
1170
+ };
1171
+
1172
+ return normalizeChatMessages([...parsedEntries, ...runtimeEntries]
985
1173
  .sort((a, b) => {
986
- const aTime = a.message.receivedAt || a.message.timestamp || 0;
987
- const bTime = b.message.receivedAt || b.message.timestamp || 0;
988
- if (aTime !== bTime) return aTime - bTime;
1174
+ const aTime = getTime(a.message);
1175
+ const bTime = getTime(b.message);
1176
+ if (aTime && bTime && aTime !== bTime) return aTime - bTime;
1177
+ if (a.source !== b.source && aTime !== bTime) {
1178
+ const parsedEntry = a.source === 'parsed' ? a : b.source === 'parsed' ? b : null;
1179
+ const runtimeEntry = a.source === 'runtime' ? a : b.source === 'runtime' ? b : null;
1180
+ if (parsedEntry && runtimeEntry && isRuntimeOverlay(runtimeEntry) && getTime(parsedEntry.message) === 0 && getTime(runtimeEntry.message) > 0) {
1181
+ if (shouldKeepParsedBeforeUntimedRuntime(parsedEntry.message)) {
1182
+ return a.source === 'parsed' ? -1 : 1;
1183
+ }
1184
+ if (shouldKeepParsedAfterUntimedRuntime(parsedEntry.message)) {
1185
+ return a.source === 'parsed' ? 1 : -1;
1186
+ }
1187
+ }
1188
+ }
1189
+ // Many provider-owned CLI transcripts (including Hermes CLI in debug bundles)
1190
+ // do not carry timestamps on parsed messages. In that case there is no safe
1191
+ // clock basis for interleaving timestamped runtime/system messages into the
1192
+ // provider transcript. Keep user prompts before runtime overlays, but do not
1193
+ // let timed runtime/system/tool/internal overlays become the final chat turns
1194
+ // after an untimed parsed assistant transcript.
989
1195
  return a.index - b.index;
990
1196
  })
991
1197
  .map((entry) => entry.message));
@@ -128,6 +128,7 @@ export type ContentBlock =
128
128
  | TextBlock
129
129
  | ImageBlock
130
130
  | AudioBlock
131
+ | VideoBlock
131
132
  | ResourceLinkBlock
132
133
  | ResourceBlock;
133
134
 
@@ -144,6 +145,7 @@ export interface ImageBlock {
144
145
  data: string; // base64-encoded
145
146
  mimeType: string; // 'image/png', 'image/jpeg', etc.
146
147
  uri?: string; // optional URL reference
148
+ alt?: string;
147
149
  annotations?: ContentAnnotations;
148
150
  }
149
151
 
@@ -152,6 +154,19 @@ export interface AudioBlock {
152
154
  type: 'audio';
153
155
  data: string; // base64-encoded
154
156
  mimeType: string;
157
+ uri?: string;
158
+ transcript?: string;
159
+ annotations?: ContentAnnotations;
160
+ }
161
+
162
+ /** Video content — ADHDev canonical display block. ACP prompt input degrades video to resource_link/text. */
163
+ export interface VideoBlock {
164
+ type: 'video';
165
+ data?: string; // base64-encoded
166
+ mimeType: string;
167
+ uri?: string;
168
+ transcript?: string;
169
+ posterUri?: string;
155
170
  annotations?: ContentAnnotations;
156
171
  }
157
172
 
@@ -595,7 +610,16 @@ export interface ProviderModule {
595
610
  // ─── Contract version / capability declaration ───
596
611
  contractVersion?: number;
597
612
  capabilities?: {
598
- input?: { multipart?: boolean; mediaTypes?: Array<'text' | 'image' | 'audio' | 'video' | 'resource'> };
613
+ input?: {
614
+ multipart?: boolean;
615
+ mediaTypes?: Array<'text' | 'image' | 'audio' | 'video' | 'resource'>;
616
+ strategies?: Array<{
617
+ mediaType: 'text' | 'image' | 'audio' | 'video' | 'resource';
618
+ strategies?: Array<'native' | 'native_acp' | 'resource_link' | 'text_fallback' | 'paste' | 'upload'>;
619
+ native?: boolean;
620
+ degradation?: Array<'native' | 'native_acp' | 'resource_link' | 'text_fallback' | 'paste' | 'upload'>;
621
+ }>;
622
+ };
599
623
  output?: { richContent?: boolean; mediaTypes?: Array<'text' | 'image' | 'audio' | 'video' | 'resource'> };
600
624
  controls?: { typedResults?: boolean };
601
625
  };