@adhdev/daemon-core 0.9.82-rc.187 → 0.9.82-rc.189

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 (42) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +1 -0
  2. package/dist/commands/cli-manager.d.ts +2 -1
  3. package/dist/commands/router.d.ts +5 -1
  4. package/dist/git/git-commands.d.ts +2 -0
  5. package/dist/git/git-types.d.ts +2 -0
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +459 -38
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +458 -38
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/providers/cli-provider-instance.d.ts +4 -0
  12. package/dist/providers/contracts.d.ts +31 -0
  13. package/dist/providers/sdk/v1/types/common/index.d.ts +35 -1
  14. package/dist/providers/spec/adapter.d.ts +4 -0
  15. package/dist/providers/spec/driver.d.ts +10 -1
  16. package/dist/providers/spec/evaluator.d.ts +9 -1
  17. package/dist/providers/spec/schema.gen.d.ts +38 -0
  18. package/dist/providers/spec/types.d.ts +25 -0
  19. package/dist/repo-mesh-types.d.ts +6 -0
  20. package/package.json +1 -1
  21. package/src/boot/daemon-lifecycle.ts +2 -0
  22. package/src/commands/chat-commands.ts +26 -0
  23. package/src/commands/cli-manager.ts +52 -14
  24. package/src/commands/router.ts +35 -4
  25. package/src/git/git-commands.ts +20 -2
  26. package/src/git/git-status.ts +35 -6
  27. package/src/git/git-types.ts +2 -0
  28. package/src/index.ts +1 -1
  29. package/src/mesh/mesh-events.ts +7 -0
  30. package/src/providers/cli-provider-instance.ts +110 -9
  31. package/src/providers/contracts.d.ts +55 -0
  32. package/src/providers/contracts.ts +35 -0
  33. package/src/providers/provider-schema.ts +56 -1
  34. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +46 -0
  35. package/src/providers/sdk/v1/types/common/index.ts +19 -0
  36. package/src/providers/spec/adapter.ts +8 -0
  37. package/src/providers/spec/driver.ts +74 -2
  38. package/src/providers/spec/evaluator.ts +39 -3
  39. package/src/providers/spec/schema.gen.ts +28 -1
  40. package/src/providers/spec/schema.json +26 -2
  41. package/src/providers/spec/types.ts +25 -0
  42. package/src/repo-mesh-types.ts +6 -0
@@ -55,6 +55,8 @@ export declare class CliProviderInstance implements ProviderInstance {
55
55
  private historyWriter;
56
56
  private runtimeMessages;
57
57
  private lastPersistedHistoryMessages;
58
+ private lastAcknowledgedUserInputAt;
59
+ private externalBusyIdleFingerprint;
58
60
  private lastNativeSourceCanonicalCheckAt;
59
61
  private lastNativeSourceCanonicalCacheKey;
60
62
  private cachedSqliteDb;
@@ -130,6 +132,8 @@ export declare class CliProviderInstance implements ProviderInstance {
130
132
  private readExternalCompletionMessages;
131
133
  private completionFinalAssistantEvidence;
132
134
  private completionFinalSummary;
135
+ private externalNativeFinalFingerprint;
136
+ private getExternalNativeFinalReconciliation;
133
137
  private buildCompletedFinalizationDiagnostic;
134
138
  private hasAdapterPendingResponse;
135
139
  private shouldSuppressStaleParsedBusyStatus;
@@ -315,7 +315,38 @@ export interface ProviderMeshCoordinatorConfig {
315
315
  * the CLI doesn't recognize).
316
316
  */
317
317
  systemPromptInjection?: MeshCoordinatorSystemPromptInjection;
318
+ /**
319
+ * How coordinator-launched worker sessions are isolated from coordinator-only
320
+ * MCP/tools/config. Provider-specific CLI quirks belong here, not in daemon
321
+ * launch code.
322
+ */
323
+ delegatedWorkerIsolation?: MeshCoordinatorDelegatedWorkerIsolation;
324
+ }
325
+ export interface MeshCoordinatorDelegatedWorkerIsolation {
326
+ /** Environment variables to unset for delegated worker sessions. */
327
+ env?: {
328
+ unset?: string[];
329
+ };
330
+ /** Spawn-argument rules applied before launching a delegated worker. */
331
+ args?: MeshCoordinatorDelegatedWorkerArgRule[];
318
332
  }
333
+ export type MeshCoordinatorDelegatedWorkerArgRule = {
334
+ mode: 'empty_mcp_config';
335
+ /** CLI flag that points at an MCP config file, e.g. '--mcp-config'. */
336
+ flag: string;
337
+ /** Optional CLI flag that forces only the provided MCP config to be used. */
338
+ strictFlag?: string;
339
+ } | {
340
+ mode: 'config_override';
341
+ /** CLI config flag, e.g. '-c' or '--config'. */
342
+ flag: string;
343
+ /** Config key to set for worker isolation. */
344
+ key: string;
345
+ /** Config value to set. */
346
+ value: string;
347
+ /** Optional broader key prefix used for duplicate detection. */
348
+ dedupeKey?: string;
349
+ };
319
350
  /**
320
351
  * Declarative description of how a CLI accepts a session-scoped system prompt.
321
352
  *
@@ -138,7 +138,41 @@ export interface McpConfigDef {
138
138
  export interface MeshCoordinatorDef {
139
139
  supported: boolean;
140
140
  mcpConfig?: McpConfigDef;
141
- }
141
+ systemPromptInjection?: MeshCoordinatorSystemPromptInjectionDef;
142
+ delegatedWorkerIsolation?: MeshCoordinatorDelegatedWorkerIsolationDef;
143
+ }
144
+ export type MeshCoordinatorSystemPromptInjectionDef = {
145
+ mode: 'cli_arg';
146
+ flag: string;
147
+ } | {
148
+ mode: 'config_override';
149
+ flag: string;
150
+ template: string;
151
+ } | {
152
+ mode: 'context_file';
153
+ path: string;
154
+ wrapper?: string;
155
+ } | {
156
+ mode: 'env_var';
157
+ name: string;
158
+ };
159
+ export interface MeshCoordinatorDelegatedWorkerIsolationDef {
160
+ env?: {
161
+ unset?: ReadonlyArray<string>;
162
+ };
163
+ args?: ReadonlyArray<MeshCoordinatorDelegatedWorkerArgRuleDef>;
164
+ }
165
+ export type MeshCoordinatorDelegatedWorkerArgRuleDef = {
166
+ mode: 'empty_mcp_config';
167
+ flag: string;
168
+ strictFlag?: string;
169
+ } | {
170
+ mode: 'config_override';
171
+ flag: string;
172
+ key: string;
173
+ value: string;
174
+ dedupeKey?: string;
175
+ };
142
176
  export interface CompatibilityEntryDef {
143
177
  /** SemVer range against the agent's own version. Optional. */
144
178
  ideVersion?: string;
@@ -49,6 +49,10 @@ export declare class TerminalAdapter {
49
49
  send_keys(s: string): void;
50
50
  resize(cols: number, rows: number): void;
51
51
  snapshot(): string;
52
+ getCursorPosition(): {
53
+ row: number;
54
+ col: number;
55
+ };
52
56
  kill(): void;
53
57
  private onChunk;
54
58
  private computeScreen;
@@ -1,5 +1,6 @@
1
1
  import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
2
- import { type TraceEntry } from './evaluator.js';
2
+ import { type SpecEvaluation, type TraceEntry } from './evaluator.js';
3
+ import type { CliSpec } from './types.js';
3
4
  export type DashboardEvent = {
4
5
  kind: 'pty_data';
5
6
  chunk: string;
@@ -96,6 +97,8 @@ export interface SpecDriverOpts {
96
97
  * embedded `\n`.
97
98
  */
98
99
  export declare function resolveSubmitDelayMs(specBeforeSubmit: number | undefined, text: string): number;
100
+ export declare function matchesCompletionIdleRule(spec: CliSpec, ev: SpecEvaluation, screen: string): string | null;
101
+ export declare function matchesCompletionIdleTargetState(spec: CliSpec, ev: SpecEvaluation, screen: string): boolean;
99
102
  export declare class SpecDriver {
100
103
  private readonly opts;
101
104
  private spec;
@@ -122,6 +125,8 @@ export declare class SpecDriver {
122
125
  * because the evaluator already moved past busy by the time the hold
123
126
  * kicks in. */
124
127
  private lastBusyState;
128
+ private completionIdleFirstSeenAt;
129
+ private completionIdleKey;
125
130
  /** Timer that re-runs evaluate() once the hold window expires. Needed
126
131
  * because the PTY stops emitting once the agent finishes; without an
127
132
  * explicit wake-up there's nothing to trigger the busy → idle
@@ -134,6 +139,10 @@ export declare class SpecDriver {
134
139
  start(): void;
135
140
  dispatch(cmd: DashboardCommand): void;
136
141
  snapshot(): string;
142
+ getCursorPosition(): {
143
+ row: number;
144
+ col: number;
145
+ };
137
146
  shutdown(): void;
138
147
  private loadSpecOrThrow;
139
148
  private buildAdapterOpts;
@@ -44,4 +44,12 @@ export interface SpecEvaluation {
44
44
  sections: ResolvedSection[];
45
45
  trace: TraceEntry[];
46
46
  }
47
- export declare function evaluate(spec: CliSpec, screenText: string): SpecEvaluation;
47
+ export declare function evaluate(spec: CliSpec, screenText: string,
48
+ /** Optional cursor position (0-based row and col). When supplied, states
49
+ * with cursor_row_min/max or cursor_col_min/max predicates are filtered.
50
+ * When omitted, cursor predicates are ignored and evaluation is text-only
51
+ * (backward-compatible with all existing specs and call sites). */
52
+ cursor?: {
53
+ row: number;
54
+ col: number;
55
+ }): SpecEvaluation;
@@ -124,6 +124,28 @@ export declare const SCHEMA: {
124
124
  readonly type: "integer";
125
125
  readonly minimum: 0;
126
126
  };
127
+ readonly completion_idle_after: {
128
+ readonly type: "object";
129
+ readonly additionalProperties: false;
130
+ readonly required: readonly ["regex", "hold_ms"];
131
+ readonly properties: {
132
+ readonly section: {
133
+ readonly type: "string";
134
+ readonly minLength: 1;
135
+ };
136
+ readonly regex: {
137
+ readonly type: "string";
138
+ readonly minLength: 1;
139
+ };
140
+ readonly flags: {
141
+ readonly type: "string";
142
+ };
143
+ readonly hold_ms: {
144
+ readonly type: "integer";
145
+ readonly minimum: 0;
146
+ };
147
+ };
148
+ };
127
149
  };
128
150
  };
129
151
  };
@@ -181,6 +203,22 @@ export declare const SCHEMA: {
181
203
  readonly type: "string";
182
204
  readonly default: "i";
183
205
  };
206
+ readonly cursor_row_min: {
207
+ readonly type: "integer";
208
+ readonly minimum: 0;
209
+ };
210
+ readonly cursor_row_max: {
211
+ readonly type: "integer";
212
+ readonly minimum: 0;
213
+ };
214
+ readonly cursor_col_min: {
215
+ readonly type: "integer";
216
+ readonly minimum: 0;
217
+ };
218
+ readonly cursor_col_max: {
219
+ readonly type: "integer";
220
+ readonly minimum: 0;
221
+ };
184
222
  };
185
223
  };
186
224
  readonly sectionPattern: {
@@ -11,6 +11,21 @@ export interface SectionRegex {
11
11
  section?: string;
12
12
  regex: string;
13
13
  flags?: string;
14
+ /**
15
+ * Optional cursor-position guards. When present, the state is only
16
+ * considered matched if the terminal cursor row/column satisfies the
17
+ * bounds (0-based, inclusive). Missing or undefined means "no constraint".
18
+ *
19
+ * Use case: distinguish modal zone from body zone for TUIs that use
20
+ * cursor position rather than distinct text to locate the active prompt
21
+ * (e.g. Antigravity cursor lands in modal_zone rows 8-31 when approval
22
+ * is visible, never in body rows 0-7). Without this guard, body text
23
+ * containing "Do you want to proceed?" could false-positive a modal match.
24
+ */
25
+ cursor_row_min?: number;
26
+ cursor_row_max?: number;
27
+ cursor_col_min?: number;
28
+ cursor_col_max?: number;
14
29
  }
15
30
  export interface SectionPattern {
16
31
  section?: string;
@@ -208,5 +223,15 @@ export interface CliSpec {
208
223
  * once the window passes and an idle state has actually been
209
224
  * observed. */
210
225
  startup_grace_ms?: number;
226
+ /** Treat a provider-specific completion marker as idle after it has
227
+ * remained visible for hold_ms. This handles TUIs that leave their
228
+ * last spinner glyph next to a completed timer, causing the normal
229
+ * busy regex to keep matching after the turn is done. */
230
+ completion_idle_after?: {
231
+ section?: string;
232
+ regex: string;
233
+ flags?: string;
234
+ hold_ms: number;
235
+ };
211
236
  };
212
237
  }
@@ -305,11 +305,17 @@ export interface RepoMeshSessionStatus {
305
305
  sessionId: string;
306
306
  providerType?: string;
307
307
  state?: string;
308
+ chatStatus?: string;
308
309
  lifecycle?: 'starting' | 'running' | 'stopping' | 'stopped' | 'failed' | 'interrupted';
309
310
  surfaceKind?: 'live_runtime' | 'recovery_snapshot' | 'inactive_record';
310
311
  recoveryState?: string | null;
311
312
  workspace?: string | null;
312
313
  title?: string | null;
314
+ role?: string | null;
315
+ isSelfCoordinator?: boolean;
316
+ statusNote?: string | null;
317
+ createdAt?: string | null;
318
+ startedAt?: string | null;
313
319
  lastActivityAt?: string | null;
314
320
  isCached?: boolean;
315
321
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.187",
3
+ "version": "0.9.82-rc.189",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -63,6 +63,7 @@ export interface DaemonInitConfig {
63
63
 
64
64
  /** Router transport-specific callbacks */
65
65
  onStatusChange?: () => void;
66
+ onMeshStateChange?: (meshId: string) => void;
66
67
  onPostChatCommand?: () => void;
67
68
  sessionHostControl?: SessionHostControlPlane | null;
68
69
  getCdpLogFn?: (ideType: string) => (msg: string) => void;
@@ -310,6 +311,7 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
310
311
  },
311
312
  onIdeConnected: () => poller?.start(),
312
313
  onStatusChange: config.onStatusChange,
314
+ onMeshStateChange: config.onMeshStateChange,
313
315
  onPostChatCommand: config.onPostChatCommand,
314
316
  sessionHostControl: config.sessionHostControl,
315
317
  statusInstanceId: config.statusInstanceId,
@@ -1378,6 +1378,15 @@ function hasVisibleAssistantMessage(messages: unknown[] | undefined): boolean {
1378
1378
  });
1379
1379
  }
1380
1380
 
1381
+ function hasFinalVisibleAssistantMessage(messages: unknown[] | undefined): boolean {
1382
+ if (!Array.isArray(messages)) return false;
1383
+ const visible = filterUserFacingChatMessages(messages as ChatMessage[]);
1384
+ const last = visible[visible.length - 1] as ChatMessage | undefined;
1385
+ const role = typeof last?.role === 'string' ? last.role.trim().toLowerCase() : '';
1386
+ const content = last ? flattenContent(last.content).trim() : '';
1387
+ return (role === 'assistant' || role === 'model') && content.length > 0;
1388
+ }
1389
+
1381
1390
  function shouldTrustCliAdapterTerminalStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): boolean {
1382
1391
  if (!isGeneratingLikeStatus(parsedStatus)) return false;
1383
1392
  if (hasNonEmptyModalButtons(activeModal)) return false;
@@ -2479,6 +2488,23 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2479
2488
  });
2480
2489
  }
2481
2490
  }
2491
+ if (
2492
+ isGeneratingLikeStatus(selectedStatus)
2493
+ && selectedTranscriptAuthority === 'provider'
2494
+ && !hasNonEmptyModalButtons(activeModal)
2495
+ && hasFinalVisibleAssistantMessage(selectedMessages)
2496
+ ) {
2497
+ selectedStatus = 'idle';
2498
+ selectedMessages = finalizeStreamingMessagesWhenIdle(selectedMessages, selectedStatus);
2499
+ messageSource = {
2500
+ ...messageSource,
2501
+ statusReconciled: {
2502
+ from: returnedStatus,
2503
+ to: 'idle',
2504
+ reason: 'provider_native_final_assistant',
2505
+ },
2506
+ };
2507
+ }
2482
2508
  LOG.debug('Command', `[read_chat] cli-like parsed provider=${adapter.cliType} target=${String(args?.targetSessionId || '')} adapterStatus=${String(adapterStatus.status || '')} parsedStatus=${String(parsedRecord.status || '')} parsedMsgCount=${parsedRecord.messages.length} returnedMsgCount=${returnedMessages.length}`);
2483
2509
  return buildReadChatCommandResult({
2484
2510
  messages: selectedMessages,
@@ -25,7 +25,7 @@ import { CliProviderInstance } from '../providers/cli-provider-instance.js';
25
25
  import { AcpProviderInstance } from '../providers/acp-provider-instance.js';
26
26
  import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
27
27
  import { ProviderLoader } from '../providers/provider-loader.js';
28
- import { normalizeInputEnvelope, type ProviderModule, type ProviderResumeCapability } from '../providers/contracts.js';
28
+ import { normalizeInputEnvelope, type MeshCoordinatorDelegatedWorkerIsolation, type ProviderModule, type ProviderResumeCapability } from '../providers/contracts.js';
29
29
  import { assertProviderSupportsDeclaredInput, assertTextOnlyInput } from '../providers/provider-input-support.js';
30
30
  import type { CliAdapter } from '../cli-adapter-types.js';
31
31
  import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
@@ -247,18 +247,19 @@ type CliStartOptions = {
247
247
  extraEnv?: Record<string, string>;
248
248
  };
249
249
 
250
- const COORDINATOR_DELEGATED_ENV_UNSETS: Record<string, string> = {
251
- ADHDEV_INLINE_MESH: '',
252
- ADHDEV_MCP_TRANSPORT: '',
253
- ADHDEV_MESH_ID: '',
254
- HERMES_EPHEMERAL_SYSTEM_PROMPT: '',
255
- };
250
+ const DEFAULT_COORDINATOR_DELEGATED_ENV_UNSETS = [
251
+ 'ADHDEV_INLINE_MESH',
252
+ 'ADHDEV_MCP_TRANSPORT',
253
+ 'ADHDEV_MESH_ID',
254
+ 'HERMES_EPHEMERAL_SYSTEM_PROMPT',
255
+ ] as const;
256
256
 
257
257
  export interface CoordinatorDelegatedCliLaunchOptionsInput {
258
258
  cliType: string;
259
259
  workspace: string;
260
260
  cliArgs?: string[];
261
261
  env?: Record<string, string>;
262
+ isolation?: MeshCoordinatorDelegatedWorkerIsolation;
262
263
  }
263
264
 
264
265
  export interface CoordinatorDelegatedCliLaunchOptions {
@@ -270,6 +271,21 @@ function hasCliArg(args: string[], flag: string): boolean {
270
271
  return args.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
271
272
  }
272
273
 
274
+ function hasConfigOverride(args: string[], key: string): boolean {
275
+ for (let index = 0; index < args.length; index += 1) {
276
+ const arg = args[index];
277
+ const next = args[index + 1];
278
+ if ((arg === '-c' || arg === '--config') && typeof next === 'string') {
279
+ if (next === key || next.startsWith(`${key}=`) || next.startsWith(`${key}.`)) return true;
280
+ }
281
+ if (arg.startsWith('--config=')) {
282
+ const value = arg.slice('--config='.length);
283
+ if (value === key || value.startsWith(`${key}=`) || value.startsWith(`${key}.`)) return true;
284
+ }
285
+ }
286
+ return false;
287
+ }
288
+
273
289
  function ensureEmptyDelegatedMcpConfig(workspace: string): string {
274
290
  const baseDir = path.join(os.tmpdir(), 'adhdev-delegated-agent-empty-mcp');
275
291
  mkdirSync(baseDir, { recursive: true });
@@ -282,12 +298,31 @@ function ensureEmptyDelegatedMcpConfig(workspace: string): string {
282
298
  export function buildCoordinatorDelegatedCliLaunchOptions(
283
299
  input: CoordinatorDelegatedCliLaunchOptionsInput,
284
300
  ): CoordinatorDelegatedCliLaunchOptions {
285
- const cliType = String(input.cliType || '').trim();
286
301
  const cliArgs = Array.isArray(input.cliArgs) ? [...input.cliArgs] : [];
287
- const env: Record<string, string> = { ...(input.env || {}), ...COORDINATOR_DELEGATED_ENV_UNSETS };
302
+ const env: Record<string, string> = { ...(input.env || {}) };
303
+ const envUnsets = new Set<string>(DEFAULT_COORDINATOR_DELEGATED_ENV_UNSETS);
304
+ for (const key of input.isolation?.env?.unset || []) {
305
+ if (typeof key === 'string' && key.trim()) envUnsets.add(key.trim());
306
+ }
307
+ for (const key of envUnsets) env[key] = '';
288
308
 
289
- if (cliType === 'claude-cli' && !hasCliArg(cliArgs, '--mcp-config')) {
290
- cliArgs.unshift('--mcp-config', ensureEmptyDelegatedMcpConfig(input.workspace));
309
+ for (const rule of input.isolation?.args || []) {
310
+ if (!rule || typeof rule !== 'object') continue;
311
+ if (rule.mode === 'empty_mcp_config') {
312
+ if (rule.flag && !hasCliArg(cliArgs, rule.flag)) {
313
+ cliArgs.unshift(rule.flag, ensureEmptyDelegatedMcpConfig(input.workspace));
314
+ }
315
+ if (rule.strictFlag && !hasCliArg(cliArgs, rule.strictFlag)) {
316
+ cliArgs.unshift(rule.strictFlag);
317
+ }
318
+ continue;
319
+ }
320
+ if (rule.mode === 'config_override') {
321
+ const key = String(rule.dedupeKey || rule.key || '').trim();
322
+ const flag = String(rule.flag || '').trim();
323
+ if (!key || !flag || hasConfigOverride(cliArgs, key)) continue;
324
+ cliArgs.unshift(flag, `${rule.key}=${rule.value}`);
325
+ }
291
326
  }
292
327
 
293
328
  return { cliArgs, env };
@@ -1090,6 +1125,8 @@ export class DaemonCliManager {
1090
1125
  const launchSource = resolved.source;
1091
1126
  if (!cliType) throw new Error('cliType required');
1092
1127
 
1128
+ const providerType = this.providerLoader.resolveAlias(cliType);
1129
+ const provLookup = this.providerLoader.getMeta(providerType) as ProviderModule | undefined;
1093
1130
  const settingsOverride = args?.settings && typeof args.settings === 'object' ? args.settings : undefined;
1094
1131
  const delegatedLaunch = settingsOverride?.launchedByCoordinator === true
1095
1132
  ? buildCoordinatorDelegatedCliLaunchOptions({
@@ -1097,6 +1134,7 @@ export class DaemonCliManager {
1097
1134
  workspace: dir,
1098
1135
  cliArgs: args?.cliArgs,
1099
1136
  env: args?.env,
1137
+ isolation: provLookup?.meshCoordinator?.delegatedWorkerIsolation,
1100
1138
  })
1101
1139
  : null;
1102
1140
  // Untrusted-provider gate: an external source that ships JS
@@ -1104,15 +1142,15 @@ export class DaemonCliManager {
1104
1142
  // launch. Dashboards add `confirmExternalUntrusted: true` to
1105
1143
  // the launch args after showing the trust modal. Without
1106
1144
  // that ack we refuse to spawn and tell the caller why.
1107
- const provLookup = this.providerLoader.getMeta(this.providerLoader.resolveAlias(cliType)) as any;
1108
- const provTrust = provLookup?._sourceTrust;
1145
+ const provMeta = provLookup as any;
1146
+ const provTrust = provMeta?._sourceTrust;
1109
1147
  if (provTrust === 'external-untrusted' && args?.confirmExternalUntrusted !== true) {
1110
1148
  return {
1111
1149
  success: false,
1112
1150
  error: 'untrusted_external_provider',
1113
1151
  provider: {
1114
1152
  type: provLookup?.type ?? cliType,
1115
- sourceName: provLookup?._sourceName ?? null,
1153
+ sourceName: provMeta?._sourceName ?? null,
1116
1154
  trust: provTrust,
1117
1155
  },
1118
1156
  hint: 'Resend launch_cli with confirmExternalUntrusted=true after the user explicitly approves running JavaScript from this 3rd-party source.',
@@ -849,7 +849,7 @@ function readCachedInlineMeshActiveSessions(node: any): string[] {
849
849
  return sessionId ? [sessionId] : [];
850
850
  }
851
851
 
852
- function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<string, unknown>> {
852
+ export function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<string, unknown>> {
853
853
  const cachedStatus = readObjectRecord(node?.cachedStatus);
854
854
  const activeSession = readObjectRecord(cachedStatus.activeSession);
855
855
  const fallbackSession = Object.keys(activeSession).length
@@ -877,9 +877,14 @@ function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<strin
877
877
  node?.provider_type,
878
878
  ),
879
879
  state: readStringValue(fallbackSession.status, fallbackSession.state, fallbackSession.lifecycle),
880
+ chatStatus: readStringValue(fallbackSession.chatStatus, fallbackSession.chat_status),
880
881
  lifecycle: readStringValue(fallbackSession.lifecycle),
881
882
  title: readStringValue(fallbackSession.title, fallbackSession.displayName, fallbackSession.display_name) ?? null,
882
883
  workspace: readStringValue(fallbackSession.workspace, node?.workspace) ?? null,
884
+ role: readStringValue(fallbackSession.role) ?? null,
885
+ isSelfCoordinator: fallbackSession.isSelfCoordinator === true || fallbackSession.is_self_coordinator === true,
886
+ createdAt: readStringValue(fallbackSession.createdAt, fallbackSession.created_at) ?? null,
887
+ startedAt: readStringValue(fallbackSession.startedAt, fallbackSession.started_at) ?? null,
883
888
  lastActivityAt: readStringValue(fallbackSession.lastActivityAt, fallbackSession.last_activity_at) ?? null,
884
889
  recoveryState: readStringValue(fallbackSession.recoveryState, fallbackSession.recovery_state) ?? null,
885
890
  isCached: true,
@@ -1067,15 +1072,28 @@ async function hydrateInlineMeshDirectTruth(args: {
1067
1072
  }
1068
1073
 
1069
1074
  function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
1075
+ const meta = readObjectRecord(record?.meta);
1076
+ const isSelfCoordinator = Boolean(readStringValue(meta.meshCoordinatorFor));
1077
+ const chatStatus = readStringValue(record?.chatStatus, record?.activeChat?.status, meta.chatStatus, meta.sessionStatus);
1078
+ const state = readLiveMeshSessionState(record);
1079
+ const statusNote = isSelfCoordinator && (!chatStatus || chatStatus === 'idle' || state === 'idle')
1080
+ ? 'Coordinator self status is sampled from the session host and may read idle while the coordinator is generating this response.'
1081
+ : null;
1070
1082
  return {
1071
1083
  sessionId: readStringValue(record?.sessionId) || 'unknown',
1072
1084
  providerType: readStringValue(record?.providerType),
1073
- state: readLiveMeshSessionState(record),
1085
+ state,
1086
+ chatStatus,
1074
1087
  lifecycle: readStringValue(record?.lifecycle),
1075
1088
  surfaceKind: getSessionHostSurfaceKind(record as any),
1076
- recoveryState: readStringValue(record?.meta?.runtimeRecoveryState) ?? null,
1089
+ recoveryState: readStringValue(meta.runtimeRecoveryState) ?? null,
1077
1090
  workspace: readStringValue(record?.workspace) ?? null,
1078
1091
  title: readStringValue(record?.displayName, record?.workspaceLabel) ?? null,
1092
+ role: isSelfCoordinator ? 'coordinator' : readStringValue(meta.meshRole, meta.role) ?? null,
1093
+ isSelfCoordinator,
1094
+ statusNote,
1095
+ createdAt: toIsoTimestamp(record?.createdAt ?? record?.created_at),
1096
+ startedAt: toIsoTimestamp(record?.startedAt ?? record?.started_at ?? record?.spawnedAtMs ?? record?.spawned_at_ms),
1079
1097
  lastActivityAt: toIsoTimestamp(record?.updatedAt ?? record?.lastActivityAt ?? record?.last_activity_at),
1080
1098
  isCached: false,
1081
1099
  };
@@ -2130,12 +2148,14 @@ export interface CommandRouterDeps {
2130
2148
  /** Reference to detected IDEs array (mutable — router updates it) */
2131
2149
  detectedIdes: { value: any[] };
2132
2150
  sessionRegistry: SessionRegistry;
2133
- /** Callback for CDP manager creation after launch_ide */
2151
+ /** Callback after CDP manager created (transport-specific extras) */
2134
2152
  onCdpManagerCreated?: (ideType: string, manager: DaemonCdpManager) => void;
2135
2153
  /** Callback after IDE connected (e.g., startAgentStreamPolling) */
2136
2154
  onIdeConnected?: () => void;
2137
2155
  /** Callback after status change (stop_ide, restart) */
2138
2156
  onStatusChange?: () => void;
2157
+ /** Callback when a mesh state is invalidated */
2158
+ onMeshStateChange?: (meshId: string) => void;
2139
2159
  /** Callback after chat-related commands */
2140
2160
  onPostChatCommand?: () => void;
2141
2161
  /** Get a connected CDP manager (for agent stream reset check) */
@@ -2456,6 +2476,16 @@ export class DaemonCommandRouter {
2456
2476
  return next;
2457
2477
  }
2458
2478
 
2479
+ public getCachedInlineMeshNodes(): any[] {
2480
+ const nodes: any[] = [];
2481
+ for (const mesh of this.inlineMeshCache.values()) {
2482
+ if (Array.isArray(mesh?.nodes)) {
2483
+ nodes.push(...mesh.nodes);
2484
+ }
2485
+ }
2486
+ return nodes;
2487
+ }
2488
+
2459
2489
  public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
2460
2490
  if (inlineMesh && typeof inlineMesh === 'object') {
2461
2491
  return this.warmInlineMeshCache(meshId, inlineMesh);
@@ -2510,6 +2540,7 @@ export class DaemonCommandRouter {
2510
2540
 
2511
2541
  private invalidateAggregateMeshStatus(meshId: string): void {
2512
2542
  this.aggregateMeshStatusCache.delete(meshId);
2543
+ this.deps.onMeshStateChange?.(meshId);
2513
2544
  }
2514
2545
 
2515
2546
 
@@ -66,7 +66,7 @@ export interface GitPushResult extends GitRepoIdentity {
66
66
  }
67
67
 
68
68
  export interface GitCommandServices {
69
- getStatus?: (params: { workspace: string; refreshUpstream?: boolean }) => Promise<GitRepoStatus> | GitRepoStatus;
69
+ getStatus?: (params: { workspace: string; refreshUpstream?: boolean; includeSubmodules?: boolean; submoduleIgnorePaths?: string[] }) => Promise<GitRepoStatus> | GitRepoStatus;
70
70
  getDiffSummary?: (params: { workspace: string; staged?: boolean }) => Promise<GitDiffSummary> | GitDiffSummary;
71
71
  getDiffFile?: (params: { workspace: string; path: string; staged?: boolean }) => Promise<GitFileDiff> | GitFileDiff;
72
72
  createSnapshot?: (params: {
@@ -294,7 +294,16 @@ export async function handleGitCommand(
294
294
  switch (command) {
295
295
  case 'git_status': {
296
296
  if (!services.getStatus) return serviceNotImplemented(command);
297
- const status = await runService(() => services.getStatus!({ workspace, refreshUpstream: optionalBoolean(args?.refreshUpstream) }));
297
+ const submoduleIgnorePaths = Array.isArray(args?.submoduleIgnorePaths)
298
+ ? args.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string' && value.trim().length > 0)
299
+ : undefined;
300
+ const statusParams: { workspace: string; refreshUpstream?: boolean; includeSubmodules?: boolean; submoduleIgnorePaths?: string[] } = { workspace };
301
+ const refreshUpstream = optionalBoolean(args?.refreshUpstream);
302
+ const includeSubmodules = optionalBoolean(args?.includeSubmodules);
303
+ if (refreshUpstream !== undefined) statusParams.refreshUpstream = refreshUpstream;
304
+ if (includeSubmodules !== undefined) statusParams.includeSubmodules = includeSubmodules;
305
+ if (submoduleIgnorePaths && submoduleIgnorePaths.length > 0) statusParams.submoduleIgnorePaths = submoduleIgnorePaths;
306
+ const status = await runService(() => services.getStatus!(statusParams));
298
307
  return 'success' in status ? status : { success: true, status };
299
308
  }
300
309
 
@@ -446,6 +455,15 @@ async function gitCheckpoint(
446
455
  if (statusResult.hasConflicts) {
447
456
  throw new GitCommandError('conflict', 'Repository has conflicts — resolve before checkpointing');
448
457
  }
458
+ const dirtySubmodules = (statusResult.submodules || []).filter(submodule => submodule.dirty);
459
+ if (dirtySubmodules.length > 0) {
460
+ const paths = dirtySubmodules.map(submodule => submodule.path).join(', ');
461
+ throw new GitCommandError(
462
+ 'dirty_index_required',
463
+ `Repository has dirty submodules that must be checkpointed first: ${paths}. ` +
464
+ 'Checkpoint or commit each dirty submodule, then checkpoint this repository to record gitlink changes.',
465
+ );
466
+ }
449
467
 
450
468
  const addArgs = includeUntracked ? ['-A'] : ['-u'];
451
469
  await runGit(repo, ['add', ...addArgs], { cwd: repoRoot });