@adhdev/daemon-core 0.9.76-rc.9 → 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,7 @@ export type InputPart =
5
5
  | ImageInputPart
6
6
  | AudioInputPart
7
7
  | VideoInputPart
8
+ | ResourceLinkInputPart
8
9
  | ResourceInputPart
9
10
 
10
11
  export interface TextInputPart {
@@ -33,6 +34,7 @@ export interface VideoInputPart {
33
34
  mimeType: string
34
35
  uri?: string
35
36
  data?: string
37
+ transcript?: string
36
38
  posterUri?: string
37
39
  }
38
40
 
@@ -45,6 +47,17 @@ export interface ResourceInputPart {
45
47
  data?: string
46
48
  }
47
49
 
50
+ export interface ResourceLinkInputPart {
51
+ type: 'resource_link'
52
+ uri: string
53
+ name: string
54
+ title?: string
55
+ description?: string
56
+ mimeType?: string
57
+ size?: number
58
+ annotations?: ContentAnnotations
59
+ }
60
+
48
61
  export interface InputEnvelope {
49
62
  parts: InputPart[]
50
63
  textFallback: string
@@ -73,6 +86,7 @@ export interface ImageMessagePart {
73
86
  mimeType: string
74
87
  uri?: string
75
88
  data?: string
89
+ alt?: string
76
90
  annotations?: ContentAnnotations
77
91
  }
78
92
 
@@ -90,6 +104,7 @@ export interface VideoMessagePart {
90
104
  mimeType: string
91
105
  uri?: string
92
106
  data?: string
107
+ transcript?: string
93
108
  posterUri?: string
94
109
  annotations?: ContentAnnotations
95
110
  }
@@ -98,8 +113,11 @@ export interface ResourceLinkMessagePart {
98
113
  type: 'resource_link'
99
114
  uri: string
100
115
  name: string
116
+ title?: string
117
+ description?: string
101
118
  mimeType?: string
102
119
  size?: number
120
+ annotations?: ContentAnnotations
103
121
  }
104
122
 
105
123
  export interface ResourceMessagePart {
@@ -149,6 +167,10 @@ export function flattenMessageParts(parts: MessagePart[]): string {
149
167
  .map((part) => {
150
168
  if (part.type === 'text') return part.text
151
169
  if (part.type === 'resource') return part.resource.text || ''
170
+ if (part.type === 'image') return part.alt || (part.data ? `[image: ${part.mimeType}]` : '')
171
+ if (part.type === 'audio') return part.transcript || (part.data ? `[audio: ${part.mimeType}]` : '')
172
+ if (part.type === 'video') return part.transcript || (part.data ? `[video: ${part.mimeType}]` : '')
173
+ if (part.type === 'resource_link') return [part.name, part.description].filter(Boolean).join('\n')
152
174
  return ''
153
175
  })
154
176
  .filter((value) => value.length > 0)
@@ -247,6 +269,7 @@ function normalizeInputPartObject(raw: Record<string, unknown>): InputPart | nul
247
269
  mimeType: raw.mimeType,
248
270
  ...(typeof raw.uri === 'string' ? { uri: raw.uri } : {}),
249
271
  ...(typeof raw.data === 'string' ? { data: raw.data } : {}),
272
+ ...(typeof raw.transcript === 'string' ? { transcript: raw.transcript } : {}),
250
273
  ...(typeof raw.posterUri === 'string' ? { posterUri: raw.posterUri } : {}),
251
274
  }
252
275
  }
@@ -262,10 +285,14 @@ function normalizeInputPartObject(raw: Record<string, unknown>): InputPart | nul
262
285
  }
263
286
  if (type === 'resource_link' && typeof raw.uri === 'string') {
264
287
  return {
265
- type: 'resource',
288
+ type,
266
289
  uri: raw.uri,
290
+ name: typeof raw.name === 'string' ? raw.name : getUriDisplayName(raw.uri, 'resource'),
291
+ ...(typeof raw.title === 'string' ? { title: raw.title } : {}),
292
+ ...(typeof raw.description === 'string' ? { description: raw.description } : {}),
267
293
  ...(typeof raw.mimeType === 'string' ? { mimeType: raw.mimeType } : {}),
268
- ...(typeof raw.name === 'string' ? { name: raw.name } : {}),
294
+ ...(typeof raw.size === 'number' && Number.isFinite(raw.size) ? { size: raw.size } : {}),
295
+ ...normalizeAnnotationsProperty(raw.annotations),
269
296
  }
270
297
  }
271
298
  return null
@@ -282,6 +309,7 @@ function normalizeMessagePartObject(raw: Record<string, unknown>): MessagePart |
282
309
  mimeType: raw.mimeType,
283
310
  ...(typeof raw.uri === 'string' ? { uri: raw.uri } : {}),
284
311
  ...(typeof raw.data === 'string' ? { data: raw.data } : {}),
312
+ ...(typeof raw.alt === 'string' ? { alt: raw.alt } : {}),
285
313
  }
286
314
  }
287
315
  if (type === 'audio' && typeof raw.mimeType === 'string') {
@@ -299,6 +327,7 @@ function normalizeMessagePartObject(raw: Record<string, unknown>): MessagePart |
299
327
  mimeType: raw.mimeType,
300
328
  ...(typeof raw.uri === 'string' ? { uri: raw.uri } : {}),
301
329
  ...(typeof raw.data === 'string' ? { data: raw.data } : {}),
330
+ ...(typeof raw.transcript === 'string' ? { transcript: raw.transcript } : {}),
302
331
  ...(typeof raw.posterUri === 'string' ? { posterUri: raw.posterUri } : {}),
303
332
  }
304
333
  }
@@ -307,8 +336,11 @@ function normalizeMessagePartObject(raw: Record<string, unknown>): MessagePart |
307
336
  type,
308
337
  uri: raw.uri,
309
338
  name: raw.name,
339
+ ...(typeof raw.title === 'string' ? { title: raw.title } : {}),
340
+ ...(typeof raw.description === 'string' ? { description: raw.description } : {}),
310
341
  ...(typeof raw.mimeType === 'string' ? { mimeType: raw.mimeType } : {}),
311
- ...(typeof raw.size === 'number' ? { size: raw.size } : {}),
342
+ ...(typeof raw.size === 'number' && Number.isFinite(raw.size) ? { size: raw.size } : {}),
343
+ ...normalizeAnnotationsProperty(raw.annotations),
312
344
  }
313
345
  }
314
346
  if (type === 'resource' && raw.resource && typeof raw.resource === 'object') {
@@ -331,10 +363,36 @@ function flattenInputParts(parts: InputPart[]): string {
331
363
  return parts
332
364
  .map((part) => {
333
365
  if (part.type === 'text') return part.text
334
- if (part.type === 'audio') return part.transcript || ''
335
- if (part.type === 'resource') return part.text || ''
366
+ if (part.type === 'image') return part.alt || (part.data ? `[image: ${part.mimeType}]` : '')
367
+ if (part.type === 'audio') return part.transcript || (part.data ? `[audio: ${part.mimeType}]` : '')
368
+ if (part.type === 'video') return part.transcript || (part.data ? `[video: ${part.mimeType}]` : '')
369
+ if (part.type === 'resource_link') return [part.title, part.name, part.description, part.uri].filter(Boolean).join('\n')
370
+ if (part.type === 'resource') return part.text || part.name || part.uri
336
371
  return ''
337
372
  })
338
373
  .filter((value) => value.length > 0)
339
374
  .join('\n')
340
375
  }
376
+
377
+ function getUriDisplayName(uri: string, fallback: string): string {
378
+ try {
379
+ const pathname = uri.startsWith('file://') ? new URL(uri).pathname : uri
380
+ return pathname.split(/[\\/]/).filter(Boolean).pop() || fallback
381
+ } catch {
382
+ return uri.split(/[\\/]/).filter(Boolean).pop() || fallback
383
+ }
384
+ }
385
+
386
+ function normalizeAnnotationsProperty(value: unknown): { annotations?: ContentAnnotations } {
387
+ if (!value || typeof value !== 'object') return {}
388
+ const record = value as Record<string, unknown>
389
+ const annotations: ContentAnnotations = {}
390
+ if (Array.isArray(record.audience)) {
391
+ const audience = record.audience.filter((item): item is 'user' | 'assistant' => item === 'user' || item === 'assistant')
392
+ if (audience.length > 0) annotations.audience = audience
393
+ }
394
+ if (typeof record.priority === 'number' && Number.isFinite(record.priority)) {
395
+ annotations.priority = record.priority
396
+ }
397
+ return Object.keys(annotations).length > 0 ? { annotations } : {}
398
+ }
@@ -1,8 +1,38 @@
1
1
  import type { InputEnvelope, ProviderModule } from './contracts.js'
2
2
 
3
- type InputMediaType = 'text' | 'image' | 'audio' | 'video' | 'resource'
3
+ export type InputMediaType = 'text' | 'image' | 'audio' | 'video' | 'resource'
4
+
5
+ export type InputAttachmentStrategy =
6
+ | 'native'
7
+ | 'native_acp'
8
+ | 'resource_link'
9
+ | 'text_fallback'
10
+ | 'paste'
11
+ | 'upload'
12
+
13
+ export interface InputMediaStrategyDescriptor {
14
+ mediaType: InputMediaType
15
+ strategies: InputAttachmentStrategy[]
16
+ native?: boolean
17
+ degradation?: InputAttachmentStrategy[]
18
+ }
19
+
20
+ export interface MessageInputSupport {
21
+ text: boolean
22
+ multipart: boolean
23
+ mediaTypes: InputMediaType[]
24
+ strategies: InputMediaStrategyDescriptor[]
25
+ }
4
26
 
5
27
  const VALID_INPUT_MEDIA_TYPES = new Set<InputMediaType>(['text', 'image', 'audio', 'video', 'resource'])
28
+ const VALID_INPUT_STRATEGIES = new Set<InputAttachmentStrategy>(['native', 'native_acp', 'resource_link', 'text_fallback', 'paste', 'upload'])
29
+
30
+ export const TEXT_ONLY_MESSAGE_INPUT_SUPPORT: MessageInputSupport = Object.freeze<MessageInputSupport>({
31
+ text: true,
32
+ multipart: false,
33
+ mediaTypes: ['text'],
34
+ strategies: [],
35
+ })
6
36
 
7
37
  function getProviderLabel(provider?: Pick<ProviderModule, 'name' | 'type'> | null): string {
8
38
  return provider?.name || provider?.type || 'This provider'
@@ -44,14 +74,108 @@ export function assertTextOnlyInput(provider: Pick<ProviderModule, 'name' | 'typ
44
74
  export function getDeclaredProviderInputSupport(provider?: Pick<ProviderModule, 'capabilities'> | null): {
45
75
  multipart: boolean
46
76
  mediaTypes: Set<InputMediaType>
77
+ strategies: InputMediaStrategyDescriptor[]
47
78
  } {
48
79
  const rawMediaTypes = Array.isArray(provider?.capabilities?.input?.mediaTypes)
49
80
  ? provider?.capabilities?.input?.mediaTypes.filter((type): type is InputMediaType => VALID_INPUT_MEDIA_TYPES.has(type as InputMediaType))
50
81
  : []
82
+ const strategies = normalizeInputStrategyDescriptors(provider?.capabilities?.input?.strategies)
51
83
 
52
84
  return {
53
85
  multipart: provider?.capabilities?.input?.multipart === true,
54
86
  mediaTypes: new Set<InputMediaType>(rawMediaTypes.length > 0 ? rawMediaTypes : ['text']),
87
+ strategies,
88
+ }
89
+ }
90
+
91
+ export function normalizeInputStrategyDescriptors(raw: unknown): InputMediaStrategyDescriptor[] {
92
+ if (!Array.isArray(raw)) return []
93
+ const result: InputMediaStrategyDescriptor[] = []
94
+ for (const entry of raw) {
95
+ if (!entry || typeof entry !== 'object') continue
96
+ const record = entry as Record<string, unknown>
97
+ const mediaType = record.mediaType
98
+ if (typeof mediaType !== 'string' || !VALID_INPUT_MEDIA_TYPES.has(mediaType as InputMediaType)) continue
99
+ const strategies = Array.isArray(record.strategies)
100
+ ? record.strategies.filter((value): value is InputAttachmentStrategy => typeof value === 'string' && VALID_INPUT_STRATEGIES.has(value as InputAttachmentStrategy))
101
+ : []
102
+ const degradation = Array.isArray(record.degradation)
103
+ ? record.degradation.filter((value): value is InputAttachmentStrategy => typeof value === 'string' && VALID_INPUT_STRATEGIES.has(value as InputAttachmentStrategy))
104
+ : []
105
+ if (strategies.length === 0 && degradation.length === 0) continue
106
+ result.push({
107
+ mediaType: mediaType as InputMediaType,
108
+ strategies,
109
+ ...(typeof record.native === 'boolean' ? { native: record.native } : {}),
110
+ ...(degradation.length > 0 ? { degradation } : {}),
111
+ })
112
+ }
113
+ return result
114
+ }
115
+
116
+ function promptCapabilityFlags(runtimeCapabilities?: Record<string, any> | null): { image: boolean; audio: boolean; embeddedContext: boolean } {
117
+ const prompt = runtimeCapabilities?.promptCapabilities || {}
118
+ return {
119
+ image: prompt.image === true,
120
+ audio: prompt.audio === true,
121
+ embeddedContext: prompt.embeddedContext === true,
122
+ }
123
+ }
124
+
125
+ function supportFromDeclared(provider?: Pick<ProviderModule, 'capabilities'> | null): MessageInputSupport {
126
+ const declared = getDeclaredProviderInputSupport(provider)
127
+ return {
128
+ text: true,
129
+ multipart: declared.multipart,
130
+ mediaTypes: Array.from(declared.mediaTypes),
131
+ strategies: declared.strategies,
132
+ }
133
+ }
134
+
135
+ export function getEffectiveMessageInputSupport(
136
+ provider?: Pick<ProviderModule, 'category' | 'capabilities'> | null,
137
+ runtimeCapabilities?: Record<string, any> | null,
138
+ ): MessageInputSupport {
139
+ if (provider?.category !== 'acp') {
140
+ const declared = supportFromDeclared(provider)
141
+ return {
142
+ ...declared,
143
+ mediaTypes: [...declared.mediaTypes],
144
+ strategies: declared.strategies.map((strategy) => ({
145
+ ...strategy,
146
+ strategies: [...strategy.strategies],
147
+ ...(strategy.degradation ? { degradation: [...strategy.degradation] } : {}),
148
+ })),
149
+ }
150
+ }
151
+
152
+ const declared = supportFromDeclared(provider)
153
+ const caps = promptCapabilityFlags(runtimeCapabilities)
154
+ const mediaTypes = new Set<InputMediaType>(['text'])
155
+ const strategies: InputMediaStrategyDescriptor[] = []
156
+
157
+ if (declared.mediaTypes.includes('resource')) {
158
+ mediaTypes.add('resource')
159
+ strategies.push({ mediaType: 'resource', strategies: caps.embeddedContext ? ['native_acp', 'resource_link', 'text_fallback'] : ['resource_link', 'text_fallback'], native: caps.embeddedContext, degradation: ['resource_link', 'text_fallback'] })
160
+ }
161
+ if (declared.mediaTypes.includes('video')) {
162
+ mediaTypes.add('video')
163
+ strategies.push({ mediaType: 'video', strategies: ['resource_link', 'text_fallback'], native: false, degradation: ['resource_link', 'text_fallback'] })
164
+ }
165
+ if (declared.mediaTypes.includes('image')) {
166
+ mediaTypes.add('image')
167
+ strategies.push({ mediaType: 'image', strategies: caps.image ? ['native_acp', 'resource_link', 'text_fallback'] : ['resource_link', 'text_fallback'], native: caps.image, degradation: ['resource_link', 'text_fallback'] })
168
+ }
169
+ if (declared.mediaTypes.includes('audio')) {
170
+ mediaTypes.add('audio')
171
+ strategies.push({ mediaType: 'audio', strategies: caps.audio ? ['native_acp', 'resource_link', 'text_fallback'] : ['resource_link', 'text_fallback'], native: caps.audio, degradation: ['resource_link', 'text_fallback'] })
172
+ }
173
+
174
+ return {
175
+ text: true,
176
+ multipart: declared.multipart && mediaTypes.size > 1,
177
+ mediaTypes: Array.from(mediaTypes),
178
+ strategies,
55
179
  }
56
180
  }
57
181
 
@@ -47,7 +47,10 @@ export class ProviderInstanceManager {
47
47
  this.instances.get(id)!.dispose();
48
48
  }
49
49
  this.instances.set(id, instance);
50
- await instance.init(context);
50
+ await instance.init({
51
+ ...context,
52
+ emitProviderEvent: (event) => this.emitProviderEvent(instance.type, id, event),
53
+ });
51
54
  }
52
55
 
53
56
  /**
@@ -237,6 +240,22 @@ export class ProviderInstanceManager {
237
240
  this.eventListeners.push(listener);
238
241
  }
239
242
 
243
+ emitProviderEvent(providerType: string, instanceId: string, event: ProviderEvent): void {
244
+ const payload = {
245
+ ...event,
246
+ providerType,
247
+ instanceId: typeof event.instanceId === 'string' && event.instanceId.trim()
248
+ ? event.instanceId
249
+ : instanceId,
250
+ targetSessionId: typeof event.targetSessionId === 'string' && event.targetSessionId.trim()
251
+ ? event.targetSessionId
252
+ : instanceId,
253
+ } as ProviderEvent & { providerType: string };
254
+ for (const listener of this.eventListeners) {
255
+ listener(payload);
256
+ }
257
+ }
258
+
240
259
  private emitPendingEvents(
241
260
  providerType: string,
242
261
  state: ProviderState,
@@ -10,6 +10,7 @@
10
10
 
11
11
  import type { ProviderModule, ProviderSettingDef, ProviderResumeCapability } from './contracts.js';
12
12
  import type { AcpConfigOption, AcpMode, ProviderControlSchema, ProviderSummaryMetadata, SessionCapability } from '../shared-types.js';
13
+ import type { MessageInputSupport } from './provider-input-support.js';
13
14
  import type { ChatMessage } from '../types.js';
14
15
 
15
16
  // ─── ProviderState — Discriminated union by category ─────────────
@@ -87,6 +88,7 @@ interface ProviderStateBase {
87
88
  runtime?: ProviderRuntimeInfo;
88
89
  resume?: ProviderResumeCapability;
89
90
  sessionCapabilities?: SessionCapability[];
91
+ messageInput?: MessageInputSupport;
90
92
  /** Dynamic control current values */
91
93
  controlValues?: Record<string, string | number | boolean>;
92
94
  /** Provider-declared controls schema (from provider.controls) */
@@ -174,6 +176,8 @@ export interface InstanceContext {
174
176
  onPtyData?: (data: string) => void;
175
177
  /** Provider configvalue (resolved) */
176
178
  settings: Record<string, any>;
179
+ /** Immediate provider-originated status/event emission. Used to avoid waiting for status polling. */
180
+ emitProviderEvent?: (event: ProviderEvent) => void;
177
181
  }
178
182
 
179
183
  export interface ProviderInstance {
@@ -2,6 +2,7 @@ import type { ProviderControlDef, ProviderControlType, ProviderModule } from './
2
2
  import { providerHasOpenPanelSupport } from './open-panel-support.js'
3
3
 
4
4
  const VALID_CAPABILITY_MEDIA_TYPES = new Set(['text', 'image', 'audio', 'video', 'resource'])
5
+ const VALID_INPUT_STRATEGIES = new Set(['native', 'native_acp', 'resource_link', 'text_fallback', 'paste', 'upload'])
5
6
 
6
7
  const KNOWN_PROVIDER_FIELDS = new Set<string>([
7
8
  'type',
@@ -158,16 +159,45 @@ function validateCapabilities(provider: ProviderModule, controls: ProviderContro
158
159
  }
159
160
 
160
161
  const input = capabilities.input
161
- if (!input || typeof input !== 'object') {
162
- errors.push('capabilities.input is required')
163
- } else {
164
- if (typeof input.multipart !== 'boolean') {
162
+ if (input !== undefined) {
163
+ if (!input || typeof input !== 'object') {
164
+ errors.push('capabilities.input must be an object when provided')
165
+ } else if (typeof input.multipart !== 'boolean') {
165
166
  errors.push('capabilities.input.multipart must be boolean')
166
167
  }
167
- if (!Array.isArray(input.mediaTypes) || input.mediaTypes.length === 0) {
168
- errors.push('capabilities.input.mediaTypes must be a non-empty array')
169
- } else if (input.mediaTypes.some((type) => typeof type !== 'string' || !VALID_CAPABILITY_MEDIA_TYPES.has(type))) {
170
- errors.push(`capabilities.input.mediaTypes must only include: ${Array.from(VALID_CAPABILITY_MEDIA_TYPES).join(', ')}`)
168
+ if (input && typeof input === 'object') {
169
+ const mediaTypes = Array.isArray(input.mediaTypes) ? input.mediaTypes : undefined
170
+ if (!mediaTypes || mediaTypes.length === 0) {
171
+ errors.push('capabilities.input.mediaTypes must be a non-empty array')
172
+ } else if (mediaTypes.some((type) => typeof type !== 'string' || !VALID_CAPABILITY_MEDIA_TYPES.has(type))) {
173
+ errors.push(`capabilities.input.mediaTypes must only include: ${Array.from(VALID_CAPABILITY_MEDIA_TYPES).join(', ')}`)
174
+ }
175
+ }
176
+ if (input && typeof input === 'object' && input.strategies !== undefined) {
177
+ if (!Array.isArray(input.strategies)) {
178
+ errors.push('capabilities.input.strategies must be an array when provided')
179
+ } else {
180
+ for (const strategy of input.strategies) {
181
+ if (!strategy || typeof strategy !== 'object' || Array.isArray(strategy)) {
182
+ errors.push('capabilities.input.strategies entries must be objects')
183
+ continue
184
+ }
185
+ const entry = strategy as Record<string, unknown>
186
+ if (typeof entry.mediaType !== 'string' || !VALID_CAPABILITY_MEDIA_TYPES.has(entry.mediaType)) {
187
+ errors.push(`capabilities.input.strategies.mediaType must only include: ${Array.from(VALID_CAPABILITY_MEDIA_TYPES).join(', ')}`)
188
+ }
189
+ for (const field of ['strategies', 'degradation'] as const) {
190
+ const values = entry[field]
191
+ if (values === undefined) continue
192
+ if (!Array.isArray(values) || values.some((value) => typeof value !== 'string' || !VALID_INPUT_STRATEGIES.has(value))) {
193
+ errors.push(`capabilities.input.strategies.${field} must only include: ${Array.from(VALID_INPUT_STRATEGIES).join(', ')}`)
194
+ }
195
+ }
196
+ if (entry.native !== undefined && typeof entry.native !== 'boolean') {
197
+ errors.push('capabilities.input.strategies.native must be boolean when provided')
198
+ }
199
+ }
200
+ }
171
201
  }
172
202
  }
173
203
 
@@ -77,6 +77,14 @@ function validateMessage(message: unknown, source: string, index: number): ChatM
77
77
  if (typeof message.senderName === 'string') normalized.senderName = message.senderName
78
78
  if (typeof (message as any)._type === 'string') normalized._type = (message as any)._type
79
79
  if (typeof (message as any)._sub === 'string') normalized._sub = (message as any)._sub
80
+ if (typeof (message as any).visibility === 'string') normalized.visibility = (message as any).visibility
81
+ if (typeof (message as any).transcriptVisibility === 'string') normalized.transcriptVisibility = (message as any).transcriptVisibility
82
+ if (typeof (message as any).audience === 'string') normalized.audience = (message as any).audience
83
+ if (typeof (message as any).source === 'string') normalized.source = (message as any).source
84
+ if (typeof (message as any).userFacing === 'boolean') normalized.userFacing = (message as any).userFacing
85
+ if (typeof (message as any).internal === 'boolean') normalized.internal = (message as any).internal
86
+ if (typeof (message as any).isInternal === 'boolean') normalized.isInternal = (message as any).isInternal
87
+ if (typeof (message as any).debug === 'boolean') normalized.debug = (message as any).debug
80
88
 
81
89
  return normalized
82
90
  }
@@ -55,6 +55,9 @@ export type RepoMeshNodeHealth =
55
55
 
56
56
  // ─── Policy Types ───────────────────────────────
57
57
 
58
+ export type RepoMeshSessionCleanupMode = 'preserve' | 'stop' | 'delete_stopped' | 'stop_and_delete';
59
+ export type RepoMeshSpawnedSessionVisibility = 'visible' | 'hidden';
60
+
58
61
  export interface RepoMeshPolicy {
59
62
  requirePreTaskCheckpoint: boolean;
60
63
  requirePostTaskCheckpoint: boolean;
@@ -63,12 +66,39 @@ export interface RepoMeshPolicy {
63
66
  dirtyWorkspaceBehavior: 'block' | 'warn' | 'checkpoint_then_continue';
64
67
  maxParallelTasks: number;
65
68
  allowedProviders?: string[];
69
+ /**
70
+ * Whether sessions spawned by mesh/coordinator policy should auto-open as visible
71
+ * dashboard tabs or start hidden. Defaults to 'visible' to preserve existing
72
+ * watch-the-agents behavior; hidden sessions remain discoverable and manually openable.
73
+ */
74
+ spawnedSessionVisibility?: RepoMeshSpawnedSessionVisibility;
75
+ /**
76
+ * What to do with delegated session-host records for a node when it is removed.
77
+ * Defaults to 'preserve' so completed work can be reviewed later and live
78
+ * runtimes are never stopped/deleted unless the mesh owner opts in.
79
+ */
80
+ sessionCleanupOnNodeRemove?: RepoMeshSessionCleanupMode;
81
+ }
82
+
83
+ export interface RepoMeshRelatedRepo {
84
+ /** Stable display label for an explicitly configured associated checkout. */
85
+ label: string;
86
+ /** Absolute checkout/workspace path for git freshness probes. */
87
+ workspace: string;
66
88
  }
67
89
 
68
90
  export interface RepoMeshNodePolicy {
69
91
  readOnly?: boolean;
70
92
  canPush?: boolean;
71
93
  maxConcurrentSessions?: number;
94
+ /** Ordered provider preference used when mesh_launch_session omits an explicit type. */
95
+ providerPriority?: string[];
96
+ /**
97
+ * Optional associated/external repos that must be checked alongside this node.
98
+ * These are explicit policy/config entries only; Repo Mesh does not auto-discover
99
+ * sibling paths so freshness checks stay fail-closed and non-surprising.
100
+ */
101
+ relatedRepos?: RepoMeshRelatedRepo[];
72
102
  }
73
103
 
74
104
  export const DEFAULT_MESH_POLICY: RepoMeshPolicy = {
@@ -78,6 +108,8 @@ export const DEFAULT_MESH_POLICY: RepoMeshPolicy = {
78
108
  requireApprovalForDestructiveGit: true,
79
109
  dirtyWorkspaceBehavior: 'warn',
80
110
  maxParallelTasks: 2,
111
+ spawnedSessionVisibility: 'visible',
112
+ sessionCleanupOnNodeRemove: 'preserve',
81
113
  };
82
114
 
83
115
  // ─── Capabilities ───────────────────────────────
@@ -189,6 +221,12 @@ export interface LocalMeshNodeEntry {
189
221
  policy: RepoMeshNodePolicy;
190
222
  /** For single-machine mesh: same daemon, different worktree */
191
223
  isLocalWorktree?: boolean;
224
+ /** Branch this worktree tracks (set when created via clone_mesh_node) */
225
+ worktreeBranch?: string;
226
+ /** Node ID this worktree was cloned from */
227
+ clonedFromNodeId?: string;
228
+ /** Optional associated/external repos configured as node metadata. */
229
+ relatedRepos?: RepoMeshRelatedRepo[];
192
230
  }
193
231
 
194
232
  // ─── Mesh Status (runtime, not persisted) ───────
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  SessionHostClient,
3
3
  getDefaultSessionHostEndpoint,
4
+ type SessionHostDiagnostics,
4
5
  type SessionHostEndpoint,
5
6
  type SessionHostRecord,
7
+ type SessionHostRequestType,
6
8
  } from '@adhdev/session-host-core';
7
9
  import type { HostedCliRuntimeDescriptor } from '../commands/cli-manager.js';
8
10
  import { DEFAULT_SESSION_HOST_READY_TIMEOUT_MS } from '../runtime-defaults.js';
@@ -10,21 +12,65 @@ import { DEFAULT_SESSION_HOST_READY_TIMEOUT_MS } from '../runtime-defaults.js';
10
12
  const STARTUP_TIMEOUT_MS = DEFAULT_SESSION_HOST_READY_TIMEOUT_MS;
11
13
  const STARTUP_POLL_MS = 200;
12
14
 
13
- async function canConnect(endpoint: SessionHostEndpoint): Promise<boolean> {
15
+ class SessionHostCompatibilityError extends Error {
16
+ constructor(message: string) {
17
+ super(message);
18
+ this.name = 'SessionHostCompatibilityError';
19
+ }
20
+ }
21
+
22
+ function getMissingRequestTypes(
23
+ diagnostics: SessionHostDiagnostics | undefined,
24
+ requiredRequestTypes: readonly SessionHostRequestType[],
25
+ ): SessionHostRequestType[] {
26
+ const supported = new Set(diagnostics?.supportedRequestTypes || []);
27
+ return requiredRequestTypes.filter((requestType) => !supported.has(requestType));
28
+ }
29
+
30
+ async function assertRequiredRequestTypes(
31
+ client: SessionHostClient,
32
+ requiredRequestTypes: readonly SessionHostRequestType[],
33
+ ): Promise<void> {
34
+ if (requiredRequestTypes.length === 0) return;
35
+
36
+ const response = await client.request<SessionHostDiagnostics>({
37
+ type: 'get_host_diagnostics',
38
+ payload: { includeSessions: false },
39
+ });
40
+ const missing = getMissingRequestTypes(response.success ? response.result : undefined, requiredRequestTypes);
41
+ if (missing.length > 0) {
42
+ const detail = response.success ? '' : ` (${response.error || 'capability probe failed'})`;
43
+ throw new SessionHostCompatibilityError(
44
+ `Session host does not support required request types: ${missing.join(', ')}${detail}`,
45
+ );
46
+ }
47
+ }
48
+
49
+ async function canConnect(
50
+ endpoint: SessionHostEndpoint,
51
+ requiredRequestTypes: readonly SessionHostRequestType[] = [],
52
+ ): Promise<boolean> {
14
53
  const client = new SessionHostClient({ endpoint });
15
54
  try {
16
55
  await client.connect();
17
- await client.close();
56
+ await assertRequiredRequestTypes(client, requiredRequestTypes);
18
57
  return true;
19
- } catch {
58
+ } catch (error) {
59
+ if (error instanceof SessionHostCompatibilityError) throw error;
20
60
  return false;
61
+ } finally {
62
+ await client.close().catch(() => {});
21
63
  }
22
64
  }
23
65
 
24
- async function waitForReady(endpoint: SessionHostEndpoint, timeoutMs = STARTUP_TIMEOUT_MS): Promise<void> {
66
+ async function waitForReady(
67
+ endpoint: SessionHostEndpoint,
68
+ timeoutMs = STARTUP_TIMEOUT_MS,
69
+ requiredRequestTypes: readonly SessionHostRequestType[] = [],
70
+ ): Promise<void> {
25
71
  const deadline = Date.now() + timeoutMs;
26
72
  while (Date.now() < deadline) {
27
- if (await canConnect(endpoint)) return;
73
+ if (await canConnect(endpoint, requiredRequestTypes)) return;
28
74
  await new Promise((resolve) => setTimeout(resolve, STARTUP_POLL_MS));
29
75
  }
30
76
  throw new Error(`Session host did not become ready within ${timeoutMs}ms`);
@@ -34,11 +80,13 @@ export async function ensureSessionHostReady(options: {
34
80
  appName?: string;
35
81
  spawnHost: () => void;
36
82
  timeoutMs?: number;
83
+ requiredRequestTypes?: readonly SessionHostRequestType[];
37
84
  }): Promise<SessionHostEndpoint> {
38
85
  const endpoint = getDefaultSessionHostEndpoint(options.appName || 'adhdev');
39
- if (await canConnect(endpoint)) return endpoint;
86
+ const requiredRequestTypes = options.requiredRequestTypes || [];
87
+ if (await canConnect(endpoint, requiredRequestTypes)) return endpoint;
40
88
  options.spawnHost();
41
- await waitForReady(endpoint, options.timeoutMs);
89
+ await waitForReady(endpoint, options.timeoutMs, requiredRequestTypes);
42
90
  return endpoint;
43
91
  }
44
92
 
@@ -336,7 +336,9 @@ export type SessionCapability =
336
336
  | 'mark_notification_unread';
337
337
 
338
338
  import type { RuntimeWriteOwner, RuntimeAttachedClient, SessionStatus } from './shared-types-extra.js';
339
+ import type { MessageInputSupport } from './providers/provider-input-support.js';
339
340
  export type { RuntimeWriteOwner, RuntimeAttachedClient, SessionStatus } from './shared-types-extra.js';
341
+ export type { MessageInputSupport, InputMediaStrategyDescriptor, InputAttachmentStrategy, InputMediaType } from './providers/provider-input-support.js';
340
342
 
341
343
  export interface SessionEntry {
342
344
  id: string;
@@ -364,6 +366,8 @@ export interface SessionEntry {
364
366
  resume?: ProviderResumeCapability;
365
367
  activeChat: SessionActiveChatData | null;
366
368
  capabilities?: SessionCapability[];
369
+ /** Effective message input/media support for this session. Defaults fail-closed to text-only. */
370
+ messageInput?: MessageInputSupport;
367
371
  cdpConnected?: boolean;
368
372
  /** Dynamic control current values (generic key-value) */
369
373
  controlValues?: Record<string, string | number | boolean>;
@@ -641,6 +645,8 @@ export interface DaemonStatusEventPayload {
641
645
  timestamp: number;
642
646
  targetSessionId?: string;
643
647
  providerType?: string;
648
+ providerSessionId?: string;
649
+ workspaceName?: string;
644
650
  duration?: number;
645
651
  elapsedSec?: number;
646
652
  modalMessage?: string;
@@ -662,6 +668,8 @@ export interface DashboardStatusEventPayload {
662
668
  daemonId?: string;
663
669
  providerType?: string;
664
670
  targetSessionId?: string;
671
+ providerSessionId?: string;
672
+ workspaceName?: string;
665
673
  duration?: number;
666
674
  elapsedSec?: number;
667
675
  modalMessage?: string;