@adhdev/daemon-core 0.9.82-rc.187 → 0.9.82-rc.188
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.
- package/dist/boot/daemon-lifecycle.d.ts +1 -0
- package/dist/commands/cli-manager.d.ts +2 -1
- package/dist/commands/router.d.ts +5 -1
- package/dist/git/git-commands.d.ts +2 -0
- package/dist/git/git-types.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +401 -33
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +400 -33
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +4 -0
- package/dist/providers/contracts.d.ts +31 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +35 -1
- package/dist/providers/spec/driver.d.ts +6 -1
- package/dist/providers/spec/schema.gen.d.ts +22 -0
- package/dist/providers/spec/types.d.ts +10 -0
- package/dist/repo-mesh-types.d.ts +6 -0
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +2 -0
- package/src/commands/chat-commands.ts +26 -0
- package/src/commands/cli-manager.ts +52 -14
- package/src/commands/router.ts +35 -4
- package/src/git/git-commands.ts +20 -2
- package/src/git/git-status.ts +35 -6
- package/src/git/git-types.ts +2 -0
- package/src/index.ts +1 -1
- package/src/providers/cli-provider-instance.ts +110 -9
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +35 -0
- package/src/providers/provider-schema.ts +56 -1
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +46 -0
- package/src/providers/sdk/v1/types/common/index.ts +19 -0
- package/src/providers/spec/driver.ts +68 -1
- package/src/providers/spec/schema.gen.ts +12 -1
- package/src/providers/spec/schema.json +21 -1
- package/src/providers/spec/types.ts +10 -0
- 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;
|
|
@@ -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
|
|
@@ -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
|
};
|
|
@@ -208,5 +208,15 @@ export interface CliSpec {
|
|
|
208
208
|
* once the window passes and an idle state has actually been
|
|
209
209
|
* observed. */
|
|
210
210
|
startup_grace_ms?: number;
|
|
211
|
+
/** Treat a provider-specific completion marker as idle after it has
|
|
212
|
+
* remained visible for hold_ms. This handles TUIs that leave their
|
|
213
|
+
* last spinner glyph next to a completed timer, causing the normal
|
|
214
|
+
* busy regex to keep matching after the turn is done. */
|
|
215
|
+
completion_idle_after?: {
|
|
216
|
+
section?: string;
|
|
217
|
+
regex: string;
|
|
218
|
+
flags?: string;
|
|
219
|
+
hold_ms: number;
|
|
220
|
+
};
|
|
211
221
|
};
|
|
212
222
|
}
|
|
@@ -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
|
@@ -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
|
|
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 || {})
|
|
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
|
-
|
|
290
|
-
|
|
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
|
|
1108
|
-
const provTrust =
|
|
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:
|
|
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.',
|
package/src/commands/router.ts
CHANGED
|
@@ -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
|
|
1085
|
+
state,
|
|
1086
|
+
chatStatus,
|
|
1074
1087
|
lifecycle: readStringValue(record?.lifecycle),
|
|
1075
1088
|
surfaceKind: getSessionHostSurfaceKind(record as any),
|
|
1076
|
-
recoveryState: readStringValue(
|
|
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
|
|
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
|
|
package/src/git/git-commands.ts
CHANGED
|
@@ -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
|
|
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 });
|
package/src/git/git-status.ts
CHANGED
|
@@ -48,6 +48,11 @@ export async function getGitRepoStatus(
|
|
|
48
48
|
if (includeSubmodules) {
|
|
49
49
|
submodules = await getSubmoduleStatuses(repo, options);
|
|
50
50
|
}
|
|
51
|
+
const submoduleDirty = (submodules || []).some(submodule => submodule.dirty || submodule.outOfSync || !!submodule.error);
|
|
52
|
+
const dirty = parsed.staged + parsed.modified + parsed.untracked + parsed.deleted + parsed.renamed > 0
|
|
53
|
+
|| parsed.conflictFiles.length > 0
|
|
54
|
+
|| stashCount > 0
|
|
55
|
+
|| submoduleDirty;
|
|
51
56
|
|
|
52
57
|
return {
|
|
53
58
|
workspace: repo.workspace,
|
|
@@ -67,6 +72,7 @@ export async function getGitRepoStatus(
|
|
|
67
72
|
untracked: parsed.untracked,
|
|
68
73
|
deleted: parsed.deleted,
|
|
69
74
|
renamed: parsed.renamed,
|
|
75
|
+
dirty,
|
|
70
76
|
hasConflicts: parsed.conflictFiles.length > 0,
|
|
71
77
|
conflictFiles: parsed.conflictFiles,
|
|
72
78
|
stashCount,
|
|
@@ -285,6 +291,7 @@ function emptyStatus(workspace: string, lastCheckedAt: number, error: GitCommand
|
|
|
285
291
|
untracked: 0,
|
|
286
292
|
deleted: 0,
|
|
287
293
|
renamed: 0,
|
|
294
|
+
dirty: false,
|
|
288
295
|
hasConflicts: false,
|
|
289
296
|
conflictFiles: [],
|
|
290
297
|
stashCount: 0,
|
|
@@ -304,12 +311,34 @@ async function getSubmoduleStatuses(
|
|
|
304
311
|
|
|
305
312
|
try {
|
|
306
313
|
const result = await runGit(repo, ['submodule', 'status', '--recursive'], options);
|
|
307
|
-
|
|
314
|
+
const submodules = parseSubmoduleStatusOutput(result.stdout, repo.repoRoot, options.submoduleIgnorePaths);
|
|
315
|
+
await Promise.all(submodules.map(submodule => enrichSubmoduleWorktreeStatus(repo, submodule, options)));
|
|
316
|
+
return submodules;
|
|
308
317
|
} catch {
|
|
309
318
|
return [];
|
|
310
319
|
}
|
|
311
320
|
}
|
|
312
321
|
|
|
322
|
+
async function enrichSubmoduleWorktreeStatus(
|
|
323
|
+
repo: ResolvedGitRepo,
|
|
324
|
+
submodule: GitSubmoduleStatus,
|
|
325
|
+
options: GitStatusOptions,
|
|
326
|
+
): Promise<void> {
|
|
327
|
+
try {
|
|
328
|
+
const result = await runGit(repo, ['status', '--porcelain=v2', '--branch'], {
|
|
329
|
+
...options,
|
|
330
|
+
cwd: submodule.repoPath,
|
|
331
|
+
});
|
|
332
|
+
const parsed = parsePorcelainV2Status(result.stdout);
|
|
333
|
+
const dirty = parsed.staged + parsed.modified + parsed.untracked + parsed.deleted + parsed.renamed > 0
|
|
334
|
+
|| parsed.conflictFiles.length > 0;
|
|
335
|
+
submodule.dirty = submodule.dirty || dirty;
|
|
336
|
+
} catch (error) {
|
|
337
|
+
submodule.dirty = true;
|
|
338
|
+
submodule.error = formatGitError(error);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
313
342
|
function parseSubmoduleStatusOutput(
|
|
314
343
|
output: string,
|
|
315
344
|
repoRoot: string,
|
|
@@ -321,9 +350,9 @@ function parseSubmoduleStatusOutput(
|
|
|
321
350
|
for (const line of output.split('\n')) {
|
|
322
351
|
if (!line.trim()) continue;
|
|
323
352
|
|
|
324
|
-
// Format: [+- ]<commit> <path> (<branch>)
|
|
325
|
-
// - = out of sync,
|
|
326
|
-
const match = line.match(/^([
|
|
353
|
+
// Format: [+-U ]<commit> <path> (<branch>)
|
|
354
|
+
// - = not initialized, + = gitlink out of sync, U = conflict, ' ' = aligned.
|
|
355
|
+
const match = line.match(/^([\-+U\s])([0-9a-f]{40})\s+(\S+)(?:\s+\(([^)]+)\))?/);
|
|
327
356
|
if (!match) continue;
|
|
328
357
|
|
|
329
358
|
const prefix = match[1];
|
|
@@ -336,8 +365,8 @@ function parseSubmoduleStatusOutput(
|
|
|
336
365
|
path,
|
|
337
366
|
commit,
|
|
338
367
|
repoPath: repoRoot + '/' + path,
|
|
339
|
-
dirty: prefix === '
|
|
340
|
-
outOfSync: prefix === '-',
|
|
368
|
+
dirty: prefix === 'U',
|
|
369
|
+
outOfSync: prefix === '-' || prefix === '+',
|
|
341
370
|
lastCheckedAt: Date.now(),
|
|
342
371
|
});
|
|
343
372
|
}
|
package/src/git/git-types.ts
CHANGED
|
@@ -60,6 +60,8 @@ export interface GitRepoStatus extends GitRepoIdentity {
|
|
|
60
60
|
untracked: number;
|
|
61
61
|
deleted: number;
|
|
62
62
|
renamed: number;
|
|
63
|
+
/** Aggregate dirty flag including root worktree changes, conflicts, stash, and submodule drift. */
|
|
64
|
+
dirty: boolean;
|
|
63
65
|
hasConflicts: boolean;
|
|
64
66
|
conflictFiles: string[];
|
|
65
67
|
stashCount: number;
|
package/src/index.ts
CHANGED
|
@@ -275,7 +275,7 @@ export type { CdpInitializerConfig } from './cdp/initializer.js';
|
|
|
275
275
|
// ── Commands ──
|
|
276
276
|
export { DaemonCommandHandler } from './commands/handler.js';
|
|
277
277
|
export type { CommandResult, CommandContext } from './commands/handler.js';
|
|
278
|
-
export { DaemonCommandRouter } from './commands/router.js';
|
|
278
|
+
export { DaemonCommandRouter, readCachedInlineMeshActiveSessionDetails } from './commands/router.js';
|
|
279
279
|
export type { CommandRouterDeps, CommandRouterResult } from './commands/router.js';
|
|
280
280
|
export {
|
|
281
281
|
maybeRunDaemonUpgradeHelperFromEnv,
|