@adhdev/daemon-core 0.9.76-rc.6 → 0.9.76-rc.60

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 (53) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -1
  2. package/dist/cli-adapters/provider-cli-runtime.d.ts +1 -0
  3. package/dist/commands/cli-manager.d.ts +17 -4
  4. package/dist/commands/mesh-coordinator.d.ts +2 -0
  5. package/dist/commands/router.d.ts +11 -0
  6. package/dist/config/mesh-config.d.ts +3 -0
  7. package/dist/git/git-types.d.ts +1 -1
  8. package/dist/git/git-worktree.d.ts +64 -0
  9. package/dist/git/index.d.ts +2 -0
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +1525 -446
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +1550 -477
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/mesh/coordinator-prompt.d.ts +1 -0
  16. package/dist/mesh/mesh-events.d.ts +9 -0
  17. package/dist/providers/chat-message-normalization.d.ts +11 -0
  18. package/dist/providers/cli-provider-instance.d.ts +3 -0
  19. package/dist/providers/provider-instance-manager.d.ts +1 -0
  20. package/dist/providers/provider-instance.d.ts +2 -0
  21. package/dist/repo-mesh-types.d.ts +27 -0
  22. package/dist/session-host/runtime-support.d.ts +2 -1
  23. package/dist/shared-types.d.ts +4 -0
  24. package/dist/types.d.ts +9 -0
  25. package/package.json +4 -5
  26. package/src/cli-adapters/provider-cli-adapter.ts +28 -7
  27. package/src/cli-adapters/provider-cli-runtime.ts +3 -2
  28. package/src/commands/chat-commands.ts +126 -11
  29. package/src/commands/cli-manager.ts +78 -5
  30. package/src/commands/handler.ts +13 -4
  31. package/src/commands/mesh-coordinator.ts +148 -5
  32. package/src/commands/router.d.ts +1 -0
  33. package/src/commands/router.ts +553 -34
  34. package/src/config/mesh-config.ts +23 -2
  35. package/src/git/git-commands.ts +5 -1
  36. package/src/git/git-types.ts +1 -0
  37. package/src/git/git-worktree.ts +214 -0
  38. package/src/git/index.ts +14 -0
  39. package/src/index.ts +3 -0
  40. package/src/mesh/coordinator-prompt.ts +29 -14
  41. package/src/mesh/mesh-events.ts +109 -43
  42. package/src/providers/chat-message-normalization.ts +80 -0
  43. package/src/providers/cli-provider-instance.d.ts +2 -0
  44. package/src/providers/cli-provider-instance.ts +93 -8
  45. package/src/providers/provider-instance-manager.ts +20 -1
  46. package/src/providers/provider-instance.ts +2 -0
  47. package/src/providers/read-chat-contract.ts +8 -0
  48. package/src/repo-mesh-types.ts +30 -0
  49. package/src/session-host/runtime-support.ts +55 -7
  50. package/src/shared-types.ts +4 -0
  51. package/src/status/builders.ts +17 -12
  52. package/src/status/reporter.ts +6 -0
  53. package/src/types.ts +9 -0
@@ -171,3 +171,83 @@ export function normalizeChatMessage<T extends ChatMessage>(message: T): T {
171
171
  export function normalizeChatMessages<T extends ChatMessage>(messages: T[] | null | undefined): T[] {
172
172
  return (Array.isArray(messages) ? messages : []).map((message) => normalizeChatMessage(message));
173
173
  }
174
+
175
+ function readMessageMeta(message: ChatMessage): Record<string, unknown> | null {
176
+ const meta = message?.meta;
177
+ return meta && typeof meta === 'object' && !Array.isArray(meta)
178
+ ? meta as Record<string, unknown>
179
+ : null;
180
+ }
181
+
182
+ function readStringField(value: unknown): string {
183
+ return typeof value === 'string' ? value.trim().toLowerCase() : '';
184
+ }
185
+
186
+ function readVisibilityField(message: ChatMessage, meta: Record<string, unknown> | null): string {
187
+ const record = message as ChatMessage & Record<string, unknown>;
188
+ return readStringField(record.visibility ?? record.transcriptVisibility ?? meta?.visibility ?? meta?.transcriptVisibility);
189
+ }
190
+
191
+ function isExplicitlyHiddenFromTranscript(message: ChatMessage, meta: Record<string, unknown> | null): boolean {
192
+ const record = message as ChatMessage & Record<string, unknown>;
193
+ const visibility = readVisibilityField(message, meta);
194
+ const audience = readStringField(record.audience ?? meta?.audience);
195
+ const source = readStringField(record.source ?? meta?.source);
196
+
197
+ return visibility === 'hidden'
198
+ || visibility === 'debug'
199
+ || visibility === 'internal'
200
+ || audience === 'debug'
201
+ || audience === 'trace'
202
+ || audience === 'internal'
203
+ || source === 'runtime_status'
204
+ || source === 'runtime_activity'
205
+ || source === 'provider_chrome'
206
+ || source === 'control'
207
+ || record.internal === true
208
+ || record.isInternal === true
209
+ || record.debug === true
210
+ || meta?.internal === true
211
+ || meta?.isInternal === true
212
+ || meta?.debug === true
213
+ || meta?.statusOnly === true
214
+ || meta?.controlOnly === true;
215
+ }
216
+
217
+ function isExplicitlyVisibleInTranscript(message: ChatMessage, meta: Record<string, unknown> | null): boolean {
218
+ const record = message as ChatMessage & Record<string, unknown>;
219
+ const visibility = readVisibilityField(message, meta);
220
+ const audience = readStringField(record.audience ?? meta?.audience);
221
+ return visibility === 'visible'
222
+ || visibility === 'user'
223
+ || visibility === 'chat'
224
+ || audience === 'chat'
225
+ || record.userFacing === true
226
+ || meta?.userFacing === true;
227
+ }
228
+
229
+ /**
230
+ * Product chat transcript visibility contract.
231
+ *
232
+ * read_chat/debug paths may preserve every normalized message, including tool,
233
+ * terminal, thought, status, and control rows. The default user-facing chat UX
234
+ * should only render meaningful conversation turns unless a producer explicitly
235
+ * marks a non-standard row as user-facing. This keeps internal tool/status/control
236
+ * plumbing out of the ordinary transcript without matching provider-specific text.
237
+ */
238
+ export function isUserFacingChatMessage(message: ChatMessage | null | undefined): boolean {
239
+ if (!message) return false;
240
+ const meta = readMessageMeta(message);
241
+ if (isExplicitlyHiddenFromTranscript(message, meta)) return false;
242
+ if (isExplicitlyVisibleInTranscript(message, meta)) return true;
243
+
244
+ const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
245
+ const kind = resolveChatMessageKind(message);
246
+ if (role === 'user' || role === 'human') return kind === 'standard' || kind === '';
247
+ if (role === 'assistant') return kind === 'standard' || kind === '';
248
+ return false;
249
+ }
250
+
251
+ export function filterUserFacingChatMessages<T extends ChatMessage>(messages: T[] | null | undefined): T[] {
252
+ return (Array.isArray(messages) ? messages : []).filter((message) => isUserFacingChatMessage(message));
253
+ }
@@ -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;
@@ -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;
@@ -214,6 +214,7 @@ export class CliProviderInstance implements ProviderInstance {
214
214
  options?: {
215
215
  providerSessionId?: string;
216
216
  launchMode?: 'new' | 'resume' | 'manual';
217
+ extraEnv?: Record<string, string>;
217
218
  onProviderSessionResolved?: (info: {
218
219
  instanceId: string;
219
220
  providerType: string;
@@ -230,7 +231,7 @@ export class CliProviderInstance implements ProviderInstance {
230
231
  this.providerSessionId = options?.providerSessionId;
231
232
  this.launchMode = options?.launchMode || 'new';
232
233
  this.onProviderSessionResolved = options?.onProviderSessionResolved;
233
- this.adapter = new ProviderCliAdapter(provider as CliProviderModule, workingDir, cliArgs, transportFactory);
234
+ this.adapter = new ProviderCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory);
234
235
  this.monitor = new StatusMonitor();
235
236
  this.historyWriter = new ChatHistoryWriter();
236
237
  }
@@ -737,7 +738,29 @@ export class CliProviderInstance implements ProviderInstance {
737
738
  }
738
739
 
739
740
  private pushEvent(event: ProviderEvent): void {
740
- this.events.push(event);
741
+ const enrichedEvent: ProviderEvent = {
742
+ ...event,
743
+ instanceId: typeof event.instanceId === 'string' && event.instanceId.trim()
744
+ ? event.instanceId
745
+ : this.instanceId,
746
+ targetSessionId: typeof event.targetSessionId === 'string' && event.targetSessionId.trim()
747
+ ? event.targetSessionId
748
+ : this.instanceId,
749
+ providerType: typeof event.providerType === 'string' && event.providerType.trim()
750
+ ? event.providerType
751
+ : this.type,
752
+ workspaceName: typeof event.workspaceName === 'string' && event.workspaceName.trim()
753
+ ? event.workspaceName
754
+ : this.workingDir,
755
+ providerSessionId: typeof event.providerSessionId === 'string' && event.providerSessionId.trim()
756
+ ? event.providerSessionId
757
+ : this.providerSessionId,
758
+ };
759
+ if (this.context?.emitProviderEvent) {
760
+ this.context.emitProviderEvent(enrichedEvent);
761
+ return;
762
+ }
763
+ this.events.push(enrichedEvent);
741
764
  }
742
765
 
743
766
  private flushEvents(): ProviderEvent[] {
@@ -977,15 +1000,77 @@ export class CliProviderInstance implements ProviderInstance {
977
1000
  }
978
1001
  }
979
1002
 
1003
+ mergeRuntimeChatMessages(parsedMessages: ChatMessage[]): ChatMessage[] {
1004
+ return this.mergeConversationMessages(parsedMessages);
1005
+ }
1006
+
980
1007
  private mergeConversationMessages(parsedMessages: any[]): ChatMessage[] {
981
1008
  if (this.runtimeMessages.length === 0) return normalizeChatMessages(parsedMessages);
982
1009
 
983
- return normalizeChatMessages([...parsedMessages, ...this.runtimeMessages.map((entry) => entry.message)]
984
- .map((message, index) => ({ message, index }))
1010
+ type MergeEntry = { message: ChatMessage; index: number; source: 'parsed' | 'runtime'; runtimeKey?: string };
1011
+ const parsedEntries: MergeEntry[] = parsedMessages.map((message, index) => ({
1012
+ message,
1013
+ index,
1014
+ source: 'parsed',
1015
+ }));
1016
+ const runtimeEntries: MergeEntry[] = this.runtimeMessages.map((entry, index) => ({
1017
+ message: entry.message,
1018
+ index: parsedMessages.length + index,
1019
+ source: 'runtime',
1020
+ runtimeKey: entry.key,
1021
+ }));
1022
+ const getTime = (message: ChatMessage): number => {
1023
+ const value = typeof message.receivedAt === 'number'
1024
+ ? message.receivedAt
1025
+ : typeof message.timestamp === 'number'
1026
+ ? message.timestamp
1027
+ : 0;
1028
+ return Number.isFinite(value) && value > 0 ? value : 0;
1029
+ };
1030
+
1031
+ const getRole = (message: ChatMessage): string => typeof message.role === 'string'
1032
+ ? message.role.trim().toLowerCase()
1033
+ : '';
1034
+ const isRuntimeOverlay = (entry: MergeEntry): boolean => {
1035
+ if (entry.source !== 'runtime') return false;
1036
+ const key = typeof entry.runtimeKey === 'string' ? entry.runtimeKey.trim().toLowerCase() : '';
1037
+ if (key.startsWith('auto_approval:')) return true;
1038
+ return !isUserFacingChatMessage(entry.message);
1039
+ };
1040
+ const shouldKeepParsedBeforeUntimedRuntime = (message: ChatMessage): boolean => {
1041
+ const role = getRole(message);
1042
+ return role === 'user' || role === 'human';
1043
+ };
1044
+ const shouldKeepParsedAfterUntimedRuntime = (message: ChatMessage): boolean => {
1045
+ const role = getRole(message);
1046
+ if (role !== 'assistant') return false;
1047
+ const kind = resolveChatMessageKind(message);
1048
+ return kind === 'standard' || kind === 'terminal';
1049
+ };
1050
+
1051
+ return normalizeChatMessages([...parsedEntries, ...runtimeEntries]
985
1052
  .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;
1053
+ const aTime = getTime(a.message);
1054
+ const bTime = getTime(b.message);
1055
+ if (aTime && bTime && aTime !== bTime) return aTime - bTime;
1056
+ if (a.source !== b.source && aTime !== bTime) {
1057
+ const parsedEntry = a.source === 'parsed' ? a : b.source === 'parsed' ? b : null;
1058
+ const runtimeEntry = a.source === 'runtime' ? a : b.source === 'runtime' ? b : null;
1059
+ if (parsedEntry && runtimeEntry && isRuntimeOverlay(runtimeEntry) && getTime(parsedEntry.message) === 0 && getTime(runtimeEntry.message) > 0) {
1060
+ if (shouldKeepParsedBeforeUntimedRuntime(parsedEntry.message)) {
1061
+ return a.source === 'parsed' ? -1 : 1;
1062
+ }
1063
+ if (shouldKeepParsedAfterUntimedRuntime(parsedEntry.message)) {
1064
+ return a.source === 'parsed' ? 1 : -1;
1065
+ }
1066
+ }
1067
+ }
1068
+ // Many provider-owned CLI transcripts (including Hermes CLI in debug bundles)
1069
+ // do not carry timestamps on parsed messages. In that case there is no safe
1070
+ // clock basis for interleaving timestamped runtime/system messages into the
1071
+ // provider transcript. Keep user prompts before runtime overlays, but do not
1072
+ // let timed runtime/system/tool/internal overlays become the final chat turns
1073
+ // after an untimed parsed assistant transcript.
989
1074
  return a.index - b.index;
990
1075
  })
991
1076
  .map((entry) => entry.message));
@@ -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,
@@ -174,6 +174,8 @@ export interface InstanceContext {
174
174
  onPtyData?: (data: string) => void;
175
175
  /** Provider configvalue (resolved) */
176
176
  settings: Record<string, any>;
177
+ /** Immediate provider-originated status/event emission. Used to avoid waiting for status polling. */
178
+ emitProviderEvent?: (event: ProviderEvent) => void;
177
179
  }
178
180
 
179
181
  export interface ProviderInstance {
@@ -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,8 @@ export type RepoMeshNodeHealth =
55
55
 
56
56
  // ─── Policy Types ───────────────────────────────
57
57
 
58
+ export type RepoMeshSessionCleanupMode = 'preserve' | 'stop' | 'delete_stopped' | 'stop_and_delete';
59
+
58
60
  export interface RepoMeshPolicy {
59
61
  requirePreTaskCheckpoint: boolean;
60
62
  requirePostTaskCheckpoint: boolean;
@@ -63,12 +65,33 @@ export interface RepoMeshPolicy {
63
65
  dirtyWorkspaceBehavior: 'block' | 'warn' | 'checkpoint_then_continue';
64
66
  maxParallelTasks: number;
65
67
  allowedProviders?: string[];
68
+ /**
69
+ * What to do with delegated session-host records for a node when it is removed.
70
+ * Defaults to 'preserve' so completed work can be reviewed later and live
71
+ * runtimes are never stopped/deleted unless the mesh owner opts in.
72
+ */
73
+ sessionCleanupOnNodeRemove?: RepoMeshSessionCleanupMode;
74
+ }
75
+
76
+ export interface RepoMeshRelatedRepo {
77
+ /** Stable display label for an explicitly configured associated checkout. */
78
+ label: string;
79
+ /** Absolute checkout/workspace path for git freshness probes. */
80
+ workspace: string;
66
81
  }
67
82
 
68
83
  export interface RepoMeshNodePolicy {
69
84
  readOnly?: boolean;
70
85
  canPush?: boolean;
71
86
  maxConcurrentSessions?: number;
87
+ /** Ordered provider preference used when mesh_launch_session omits an explicit type. */
88
+ providerPriority?: string[];
89
+ /**
90
+ * Optional associated/external repos that must be checked alongside this node.
91
+ * These are explicit policy/config entries only; Repo Mesh does not auto-discover
92
+ * sibling paths so freshness checks stay fail-closed and non-surprising.
93
+ */
94
+ relatedRepos?: RepoMeshRelatedRepo[];
72
95
  }
73
96
 
74
97
  export const DEFAULT_MESH_POLICY: RepoMeshPolicy = {
@@ -78,6 +101,7 @@ export const DEFAULT_MESH_POLICY: RepoMeshPolicy = {
78
101
  requireApprovalForDestructiveGit: true,
79
102
  dirtyWorkspaceBehavior: 'warn',
80
103
  maxParallelTasks: 2,
104
+ sessionCleanupOnNodeRemove: 'preserve',
81
105
  };
82
106
 
83
107
  // ─── Capabilities ───────────────────────────────
@@ -189,6 +213,12 @@ export interface LocalMeshNodeEntry {
189
213
  policy: RepoMeshNodePolicy;
190
214
  /** For single-machine mesh: same daemon, different worktree */
191
215
  isLocalWorktree?: boolean;
216
+ /** Branch this worktree tracks (set when created via clone_mesh_node) */
217
+ worktreeBranch?: string;
218
+ /** Node ID this worktree was cloned from */
219
+ clonedFromNodeId?: string;
220
+ /** Optional associated/external repos configured as node metadata. */
221
+ relatedRepos?: RepoMeshRelatedRepo[];
192
222
  }
193
223
 
194
224
  // ─── 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
 
@@ -641,6 +641,8 @@ export interface DaemonStatusEventPayload {
641
641
  timestamp: number;
642
642
  targetSessionId?: string;
643
643
  providerType?: string;
644
+ providerSessionId?: string;
645
+ workspaceName?: string;
644
646
  duration?: number;
645
647
  elapsedSec?: number;
646
648
  modalMessage?: string;
@@ -662,6 +664,8 @@ export interface DashboardStatusEventPayload {
662
664
  daemonId?: string;
663
665
  providerType?: string;
664
666
  targetSessionId?: string;
667
+ providerSessionId?: string;
668
+ workspaceName?: string;
665
669
  duration?: number;
666
670
  elapsedSec?: number;
667
671
  modalMessage?: string;
@@ -43,6 +43,19 @@ function getActiveChatOptions(profile: SessionEntryProfile): NormalizeActiveChat
43
43
  return LIVE_STATUS_ACTIVE_CHAT_OPTIONS;
44
44
  }
45
45
 
46
+ function resolveSessionStatus(
47
+ activeChat: { status?: string | null; activeModal?: { buttons?: unknown[] | null } | null } | null | undefined,
48
+ providerStatus?: string | null,
49
+ ) {
50
+ const chatStatus = normalizeManagedStatus(activeChat?.status, { activeModal: activeChat?.activeModal || null });
51
+ const topLevelStatus = normalizeManagedStatus(providerStatus, { activeModal: activeChat?.activeModal || null });
52
+
53
+ if (chatStatus === 'waiting_approval' || topLevelStatus === 'waiting_approval') return 'waiting_approval';
54
+ if (chatStatus === 'generating' || topLevelStatus === 'generating') return 'generating';
55
+ if (topLevelStatus !== 'idle') return topLevelStatus;
56
+ return chatStatus;
57
+ }
58
+
46
59
  function shouldIncludeSessionControls(profile: SessionEntryProfile): boolean {
47
60
  return profile !== 'live';
48
61
  }
@@ -170,9 +183,7 @@ function buildIdeWorkspaceSession(
170
183
  providerName: state.name,
171
184
  kind: 'workspace',
172
185
  transport: 'cdp-page',
173
- status: normalizeManagedStatus(activeChat?.status || state.status, {
174
- activeModal: activeChat?.activeModal || null,
175
- }),
186
+ status: resolveSessionStatus(activeChat, state.status),
176
187
  title,
177
188
  workspace,
178
189
  ...(git && { git }),
@@ -212,9 +223,7 @@ function buildExtensionAgentSession(
212
223
  providerSessionId: ext.providerSessionId,
213
224
  kind: 'agent',
214
225
  transport: 'cdp-webview',
215
- status: normalizeManagedStatus(activeChat?.status || ext.status, {
216
- activeModal: activeChat?.activeModal || null,
217
- }),
226
+ status: resolveSessionStatus(activeChat, ext.status),
218
227
  title: activeChat?.title || ext.name,
219
228
  workspace,
220
229
  ...(git && { git }),
@@ -277,9 +286,7 @@ function buildCliSession(state: CliProviderState, options: SessionEntryBuildOpti
277
286
  providerSessionId: state.providerSessionId,
278
287
  kind: 'agent',
279
288
  transport: 'pty',
280
- status: normalizeManagedStatus(activeChat?.status || state.status, {
281
- activeModal: activeChat?.activeModal || null,
282
- }),
289
+ status: resolveSessionStatus(activeChat, state.status),
283
290
  title: activeChat?.title || state.name,
284
291
  workspace,
285
292
  ...(git && { git }),
@@ -328,9 +335,7 @@ function buildAcpSession(state: AcpProviderState, options: SessionEntryBuildOpti
328
335
  providerName: state.name,
329
336
  kind: 'agent',
330
337
  transport: 'acp',
331
- status: normalizeManagedStatus(activeChat?.status || state.status, {
332
- activeModal: activeChat?.activeModal || null,
333
- }),
338
+ status: resolveSessionStatus(activeChat, state.status),
334
339
  title: activeChat?.title || state.name,
335
340
  workspace,
336
341
  ...(git && { git }),
@@ -151,6 +151,12 @@ export class DaemonStatusReporter {
151
151
  if (providerType) {
152
152
  payload.providerType = providerType;
153
153
  }
154
+ if (typeof event.providerSessionId === 'string' && event.providerSessionId.trim()) {
155
+ payload.providerSessionId = event.providerSessionId.trim();
156
+ }
157
+ if (typeof event.workspaceName === 'string' && event.workspaceName.trim()) {
158
+ payload.workspaceName = event.workspaceName.trim();
159
+ }
154
160
  if (typeof event.duration === 'number' && Number.isFinite(event.duration)) {
155
161
  payload.duration = event.duration;
156
162
  }
package/src/types.ts CHANGED
@@ -49,6 +49,15 @@ export interface ChatMessage {
49
49
  /** Optional: fiber metadata */
50
50
  _type?: string;
51
51
  _sub?: string;
52
+ /** Transcript visibility/audience contract for separating chat-visible content from internal/debug runtime rows. */
53
+ visibility?: 'visible' | 'user' | 'chat' | 'hidden' | 'debug' | 'internal' | (string & {});
54
+ transcriptVisibility?: 'visible' | 'user' | 'chat' | 'hidden' | 'debug' | 'internal' | (string & {});
55
+ audience?: 'chat' | 'debug' | 'trace' | 'internal' | (string & {});
56
+ source?: 'assistant_text' | 'tool_call' | 'terminal_command' | 'runtime_activity' | 'runtime_status' | 'provider_chrome' | 'control' | (string & {});
57
+ userFacing?: boolean;
58
+ internal?: boolean;
59
+ isInternal?: boolean;
60
+ debug?: boolean;
52
61
  /** Meta information for thought/terminal logs etc */
53
62
  meta?: { label?: string; isRunning?: boolean } | Record<string, any>;
54
63
  /** Sender name for shared sessions */