@adhdev/daemon-core 0.9.82-rc.186 → 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/mesh-coordinator.d.ts +13 -0
- package/dist/commands/router.d.ts +5 -1
- package/dist/config/chat-history.d.ts +1 -0
- package/dist/git/git-commands.d.ts +2 -0
- package/dist/git/git-types.d.ts +2 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +15461 -14552
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +14411 -13502
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/beads-db.d.ts +1 -0
- package/dist/mesh/mesh-events.d.ts +46 -1
- package/dist/mesh/mesh-work-queue.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +4 -0
- package/dist/providers/contracts.d.ts +32 -1
- package/dist/providers/native-history/dispatcher.d.ts +2 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +35 -1
- package/dist/providers/spec/cli-adapter.d.ts +1 -0
- package/dist/providers/spec/driver.d.ts +6 -1
- package/dist/providers/spec/native-history-executor.d.ts +2 -0
- 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 +206 -18
- package/src/commands/cli-manager.ts +56 -14
- package/src/commands/mesh-coordinator.ts +110 -5
- package/src/commands/router.ts +146 -21
- package/src/config/chat-history.ts +4 -0
- 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 +2 -2
- package/src/mesh/beads-db.ts +4 -0
- package/src/mesh/mesh-events.ts +264 -4
- package/src/mesh/mesh-work-queue.ts +4 -0
- package/src/providers/cli-provider-instance.ts +122 -13
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +36 -1
- package/src/providers/native-history/dispatcher.ts +126 -17
- package/src/providers/provider-loader.ts +4 -7
- 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/cli-adapter.ts +32 -5
- package/src/providers/spec/driver.ts +68 -1
- package/src/providers/spec/evaluator.ts +11 -1
- package/src/providers/spec/native-history-executor.ts +93 -27
- 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
|
@@ -390,7 +390,7 @@ export interface ProviderMeshCoordinatorConfig {
|
|
|
390
390
|
requiresRestart?: boolean;
|
|
391
391
|
/** User-facing setup explanation for manual modes. */
|
|
392
392
|
instructions?: string;
|
|
393
|
-
/** Copyable setup template. Supports {{meshId}}, {{adhdevMcpCommand}}, {{workspace}}, {{serverName}}. */
|
|
393
|
+
/** Copyable setup template. Supports {{meshId}}, {{adhdevMcpCommand}}, {{adhdevMcpArgs}}, {{workspace}}, {{serverName}}. */
|
|
394
394
|
template?: string;
|
|
395
395
|
};
|
|
396
396
|
/**
|
|
@@ -403,8 +403,43 @@ export interface ProviderMeshCoordinatorConfig {
|
|
|
403
403
|
* the CLI doesn't recognize).
|
|
404
404
|
*/
|
|
405
405
|
systemPromptInjection?: MeshCoordinatorSystemPromptInjection;
|
|
406
|
+
/**
|
|
407
|
+
* How coordinator-launched worker sessions are isolated from coordinator-only
|
|
408
|
+
* MCP/tools/config. Provider-specific CLI quirks belong here, not in daemon
|
|
409
|
+
* launch code.
|
|
410
|
+
*/
|
|
411
|
+
delegatedWorkerIsolation?: MeshCoordinatorDelegatedWorkerIsolation;
|
|
406
412
|
}
|
|
407
413
|
|
|
414
|
+
export interface MeshCoordinatorDelegatedWorkerIsolation {
|
|
415
|
+
/** Environment variables to unset for delegated worker sessions. */
|
|
416
|
+
env?: {
|
|
417
|
+
unset?: string[];
|
|
418
|
+
};
|
|
419
|
+
/** Spawn-argument rules applied before launching a delegated worker. */
|
|
420
|
+
args?: MeshCoordinatorDelegatedWorkerArgRule[];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export type MeshCoordinatorDelegatedWorkerArgRule =
|
|
424
|
+
| {
|
|
425
|
+
mode: 'empty_mcp_config';
|
|
426
|
+
/** CLI flag that points at an MCP config file, e.g. '--mcp-config'. */
|
|
427
|
+
flag: string;
|
|
428
|
+
/** Optional CLI flag that forces only the provided MCP config to be used. */
|
|
429
|
+
strictFlag?: string;
|
|
430
|
+
}
|
|
431
|
+
| {
|
|
432
|
+
mode: 'config_override';
|
|
433
|
+
/** CLI config flag, e.g. '-c' or '--config'. */
|
|
434
|
+
flag: string;
|
|
435
|
+
/** Config key to set for worker isolation. */
|
|
436
|
+
key: string;
|
|
437
|
+
/** Config value to set. */
|
|
438
|
+
value: string;
|
|
439
|
+
/** Optional broader key prefix used for duplicate detection. */
|
|
440
|
+
dedupeKey?: string;
|
|
441
|
+
};
|
|
442
|
+
|
|
408
443
|
/**
|
|
409
444
|
* Declarative description of how a CLI accepts a session-scoped system prompt.
|
|
410
445
|
*
|
|
@@ -27,6 +27,7 @@ export interface NativeHistoryInput {
|
|
|
27
27
|
providerSessionId?: string;
|
|
28
28
|
historySessionId?: string;
|
|
29
29
|
workspace?: string;
|
|
30
|
+
sessionStartedAtMs?: number;
|
|
30
31
|
format?: string;
|
|
31
32
|
watchPath?: string;
|
|
32
33
|
forceRefresh?: boolean;
|
|
@@ -34,7 +35,7 @@ export interface NativeHistoryInput {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export interface NativeHistoryResult {
|
|
37
|
-
messages: Array<{ role: string; content: string; receivedAt?: number; kind?: string }>;
|
|
38
|
+
messages: Array<{ role: string; content: string; receivedAt?: number; kind?: string; workspace?: string }>;
|
|
38
39
|
providerSessionId?: string;
|
|
39
40
|
sourcePath: string;
|
|
40
41
|
sourceMtimeMs: number;
|
|
@@ -53,7 +54,12 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
|
|
|
53
54
|
// shows up before I type anything" on every provider).
|
|
54
55
|
const requestedProviderSid = input.providerSessionId || '';
|
|
55
56
|
|
|
56
|
-
const
|
|
57
|
+
const sessionStartedAtMs = typeof input.sessionStartedAtMs === 'number'
|
|
58
|
+
? input.sessionStartedAtMs
|
|
59
|
+
: typeof input.args?.sessionStartedAtMs === 'number'
|
|
60
|
+
? input.args.sessionStartedAtMs
|
|
61
|
+
: 0;
|
|
62
|
+
const sourcePath = resolveSourcePath(reader, workspace, sessionId, sessionStartedAtMs);
|
|
57
63
|
if (!sourcePath) return null;
|
|
58
64
|
if (input.forceRefresh === true || input.args?.forceRefresh === true) {
|
|
59
65
|
try { fs.statSync(sourcePath); } catch { /* best-effort metadata refresh */ }
|
|
@@ -72,6 +78,7 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
|
|
|
72
78
|
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
|
73
79
|
receivedAt: typeof m.receivedAt === 'number' ? m.receivedAt : Date.parse(m.timestamp || '') || Date.now(),
|
|
74
80
|
kind: typeof m.kind === 'string' ? m.kind : 'standard',
|
|
81
|
+
workspace: typeof m.workspace === 'string' ? m.workspace : workspace || undefined,
|
|
75
82
|
})),
|
|
76
83
|
providerSessionId: session.providerSessionId,
|
|
77
84
|
sourcePath: session.sourcePath,
|
|
@@ -85,10 +92,10 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
|
|
|
85
92
|
// Per-provider path resolution
|
|
86
93
|
// ────────────────────────────────────────────────────────────────────────────
|
|
87
94
|
|
|
88
|
-
function resolveSourcePath(reader: ReaderId, workspace: string, sessionId: string): string | null {
|
|
95
|
+
function resolveSourcePath(reader: ReaderId, workspace: string, sessionId: string, sessionStartedAtMs: number): string | null {
|
|
89
96
|
switch (reader) {
|
|
90
97
|
case 'claude-cli': return resolveClaudePath(workspace, sessionId);
|
|
91
|
-
case 'codex-cli': return resolveCodexPath(workspace);
|
|
98
|
+
case 'codex-cli': return resolveCodexPath(workspace, sessionId, sessionStartedAtMs);
|
|
92
99
|
case 'antigravity-cli': return resolveAntigravityPath(workspace);
|
|
93
100
|
case 'hermes-cli': return resolveHermesPath(workspace, sessionId);
|
|
94
101
|
}
|
|
@@ -112,21 +119,111 @@ function resolveClaudePath(workspace: string, sessionId: string): string | null
|
|
|
112
119
|
return null;
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
function resolveCodexPath(workspace: string): string | null {
|
|
116
|
-
void workspace;
|
|
122
|
+
function resolveCodexPath(workspace: string, sessionId: string, sessionStartedAtMs: number): string | null {
|
|
117
123
|
// codex stores by UTC date: ~/.codex/sessions/<year>/<month>/<day>/<file>.jsonl
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
String(now.getUTCFullYear()),
|
|
122
|
-
String(now.getUTCMonth() + 1).padStart(2, '0'),
|
|
123
|
-
String(now.getUTCDate()).padStart(2, '0'),
|
|
124
|
-
);
|
|
125
|
-
if (fs.existsSync(dir)) {
|
|
126
|
-
const f = newestRecentFile(dir, /\.jsonl$/);
|
|
127
|
-
if (f) return f;
|
|
124
|
+
const root = codexSessionsRoot();
|
|
125
|
+
if (sessionId && isUuidLikeSessionId(sessionId)) {
|
|
126
|
+
return findCodexPathBySessionId(root, sessionId);
|
|
128
127
|
}
|
|
129
|
-
return
|
|
128
|
+
return findCodexPathByRuntime(root, workspace, sessionStartedAtMs);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function findCodexPathBySessionId(root: string, sessionId: string): string | null {
|
|
132
|
+
if (!fs.existsSync(root)) return null;
|
|
133
|
+
const needle = sessionId.toLowerCase();
|
|
134
|
+
const matches: Array<{ p: string; mtime: number }> = [];
|
|
135
|
+
const stack: string[] = [root];
|
|
136
|
+
while (stack.length > 0) {
|
|
137
|
+
const current = stack.pop()!;
|
|
138
|
+
let entries: fs.Dirent[] = [];
|
|
139
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; }
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const entryPath = path.join(current, entry.name);
|
|
142
|
+
if (entry.isDirectory()) {
|
|
143
|
+
stack.push(entryPath);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
|
|
147
|
+
if (!entry.name.toLowerCase().includes(needle)) continue;
|
|
148
|
+
if (!isSafeFilename(entry.name.replace('.jsonl', ''))) continue;
|
|
149
|
+
matches.push({ p: entryPath, mtime: safeMtime(entryPath) });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
matches.sort((a, b) => b.mtime - a.mtime);
|
|
153
|
+
return matches[0]?.p ?? null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const CODEX_SPAWN_BIND_GRACE_MS = 10_000;
|
|
157
|
+
|
|
158
|
+
function findCodexPathByRuntime(root: string, workspace: string, sessionStartedAtMs: number): string | null {
|
|
159
|
+
if (!fs.existsSync(root) || !workspace) return null;
|
|
160
|
+
const workspaceResolved = resolveRealPath(workspace);
|
|
161
|
+
const cutoff = Date.now() - RECENT_WINDOW_MS;
|
|
162
|
+
const matches: Array<{ p: string; mtime: number; diff: number }> = [];
|
|
163
|
+
const stack: string[] = [root];
|
|
164
|
+
|
|
165
|
+
while (stack.length > 0) {
|
|
166
|
+
const current = stack.pop()!;
|
|
167
|
+
let entries: fs.Dirent[] = [];
|
|
168
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; }
|
|
169
|
+
for (const entry of entries) {
|
|
170
|
+
const entryPath = path.join(current, entry.name);
|
|
171
|
+
if (entry.isDirectory()) {
|
|
172
|
+
stack.push(entryPath);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
|
|
176
|
+
const mtime = safeMtime(entryPath);
|
|
177
|
+
if (mtime < cutoff) continue;
|
|
178
|
+
const meta = readCodexSessionMeta(entryPath);
|
|
179
|
+
if (!meta?.cwd || resolveRealPath(meta.cwd) !== workspaceResolved) continue;
|
|
180
|
+
const diff = sessionStartedAtMs > 0 && meta.timestampMs != null
|
|
181
|
+
? Math.abs(meta.timestampMs - sessionStartedAtMs)
|
|
182
|
+
: 0;
|
|
183
|
+
if (sessionStartedAtMs > 0 && (meta.timestampMs == null || diff > CODEX_SPAWN_BIND_GRACE_MS)) continue;
|
|
184
|
+
matches.push({ p: entryPath, mtime, diff });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
matches.sort((a, b) => sessionStartedAtMs > 0
|
|
189
|
+
? a.diff - b.diff || b.mtime - a.mtime
|
|
190
|
+
: b.mtime - a.mtime);
|
|
191
|
+
return matches[0]?.p ?? null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function readCodexSessionMeta(filePath: string): { cwd?: string; timestampMs?: number } | null {
|
|
195
|
+
try {
|
|
196
|
+
const fd = fs.openSync(filePath, 'r');
|
|
197
|
+
try {
|
|
198
|
+
const buffer = Buffer.alloc(8192);
|
|
199
|
+
const bytes = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
200
|
+
if (bytes <= 0) return null;
|
|
201
|
+
const text = buffer.subarray(0, bytes).toString('utf8');
|
|
202
|
+
const firstLine = text.slice(0, text.indexOf('\n') >= 0 ? text.indexOf('\n') : text.length).trim();
|
|
203
|
+
if (!firstLine) return null;
|
|
204
|
+
const record = JSON.parse(firstLine) as Record<string, unknown>;
|
|
205
|
+
if (record.type !== 'session_meta' || !record.payload || typeof record.payload !== 'object') return null;
|
|
206
|
+
const payload = record.payload as Record<string, unknown>;
|
|
207
|
+
const timestampRaw = payload.timestamp;
|
|
208
|
+
const timestampMs = typeof timestampRaw === 'string'
|
|
209
|
+
? Date.parse(timestampRaw)
|
|
210
|
+
: typeof timestampRaw === 'number'
|
|
211
|
+
? (timestampRaw < 1e12 ? timestampRaw * 1000 : timestampRaw)
|
|
212
|
+
: NaN;
|
|
213
|
+
return {
|
|
214
|
+
cwd: typeof payload.cwd === 'string' ? payload.cwd : undefined,
|
|
215
|
+
timestampMs: Number.isFinite(timestampMs) ? timestampMs : undefined,
|
|
216
|
+
};
|
|
217
|
+
} finally {
|
|
218
|
+
fs.closeSync(fd);
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function resolveRealPath(value: string): string {
|
|
226
|
+
try { return fs.realpathSync(value); } catch { return value; }
|
|
130
227
|
}
|
|
131
228
|
|
|
132
229
|
function resolveAntigravityPath(workspace: string): string | null {
|
|
@@ -187,6 +284,18 @@ function cwdAsDashes(cwd: string): string {
|
|
|
187
284
|
return cwd.replace(/\//g, '-');
|
|
188
285
|
}
|
|
189
286
|
|
|
287
|
+
function codexSessionsRoot(): string {
|
|
288
|
+
return path.join(os.homedir(), '.codex', 'sessions');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function isUuidLikeSessionId(sessionId: string): boolean {
|
|
292
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isSafeFilename(name: string): boolean {
|
|
296
|
+
return /^[A-Za-z0-9._:-]+$/.test(name) && !name.includes('..');
|
|
297
|
+
}
|
|
298
|
+
|
|
190
299
|
function newestFile(dir: string, pattern: RegExp): string | null {
|
|
191
300
|
try {
|
|
192
301
|
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
@@ -36,6 +36,9 @@ import {
|
|
|
36
36
|
} from './external-sources.js';
|
|
37
37
|
import type { ProviderSourceMode } from '../config/config.js';
|
|
38
38
|
import type { ProviderSourceConfigSnapshot, ProviderUserDirSource } from '../config/provider-source-config.js';
|
|
39
|
+
import { loadSpec } from './spec/loader.js';
|
|
40
|
+
import { executeNativeHistory } from './spec/native-history-executor.js';
|
|
41
|
+
import { createNativeHistoryDispatcher, type ReaderId } from './native-history/dispatcher.js';
|
|
39
42
|
|
|
40
43
|
/**
|
|
41
44
|
* Adds a provider-script root to the require whitelist. Wrapped in a
|
|
@@ -1244,8 +1247,6 @@ export class ProviderLoader {
|
|
|
1244
1247
|
// Hand the resolved spec path off to route.ts via a hidden field
|
|
1245
1248
|
// so the routing layer doesn't have to repeat the candidate walk.
|
|
1246
1249
|
(resolved as any)._resolvedSpecPath = specPath;
|
|
1247
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1248
|
-
const { loadSpec } = require('./spec/loader.js');
|
|
1249
1250
|
const r = loadSpec(specPath);
|
|
1250
1251
|
// Stub each control_bar entry as a provider.scripts.<id>. The
|
|
1251
1252
|
// upstream invoke_provider_script gate checks that the script
|
|
@@ -1274,8 +1275,6 @@ export class ProviderLoader {
|
|
|
1274
1275
|
let format = 'spec';
|
|
1275
1276
|
|
|
1276
1277
|
if (nh.source) {
|
|
1277
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1278
|
-
const { executeNativeHistory } = require('./spec/native-history-executor.js');
|
|
1279
1278
|
format = `spec-${nh.source.kind}`;
|
|
1280
1279
|
reader = (input: any) => executeNativeHistory(nh, input);
|
|
1281
1280
|
} else if (nh.override_path) {
|
|
@@ -1294,9 +1293,7 @@ export class ProviderLoader {
|
|
|
1294
1293
|
} catch { /* fall through — leave native unavailable */ }
|
|
1295
1294
|
}
|
|
1296
1295
|
} else if (nh.reader) {
|
|
1297
|
-
|
|
1298
|
-
const { createNativeHistoryDispatcher } = require('./native-history/dispatcher.js');
|
|
1299
|
-
const dispatch = createNativeHistoryDispatcher(nh.reader);
|
|
1296
|
+
const dispatch = createNativeHistoryDispatcher(nh.reader as ReaderId);
|
|
1300
1297
|
format = nh.reader;
|
|
1301
1298
|
reader = (input: any) => dispatch(input);
|
|
1302
1299
|
}
|
|
@@ -300,7 +300,11 @@ function validateMeshCoordinator(raw: unknown, errors: string[]): void {
|
|
|
300
300
|
errors.push('meshCoordinator.reason must be a non-empty string when provided')
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
|
|
303
|
+
validateMeshCoordinatorMcpConfig(meshCoordinator.mcpConfig, errors)
|
|
304
|
+
validateMeshCoordinatorDelegatedWorkerIsolation(meshCoordinator.delegatedWorkerIsolation, errors)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function validateMeshCoordinatorMcpConfig(mcpConfig: unknown, errors: string[]): void {
|
|
304
308
|
if (mcpConfig === undefined) return
|
|
305
309
|
if (!mcpConfig || typeof mcpConfig !== 'object' || Array.isArray(mcpConfig)) {
|
|
306
310
|
errors.push('meshCoordinator.mcpConfig must be an object')
|
|
@@ -348,6 +352,57 @@ function validateMeshCoordinator(raw: unknown, errors: string[]): void {
|
|
|
348
352
|
}
|
|
349
353
|
}
|
|
350
354
|
|
|
355
|
+
function validateMeshCoordinatorDelegatedWorkerIsolation(raw: unknown, errors: string[]): void {
|
|
356
|
+
if (raw === undefined) return
|
|
357
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
358
|
+
errors.push('meshCoordinator.delegatedWorkerIsolation must be an object')
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
const isolation = raw as Record<string, unknown>
|
|
362
|
+
const env = isolation.env
|
|
363
|
+
if (env !== undefined) {
|
|
364
|
+
if (!env || typeof env !== 'object' || Array.isArray(env)) {
|
|
365
|
+
errors.push('meshCoordinator.delegatedWorkerIsolation.env must be an object')
|
|
366
|
+
} else {
|
|
367
|
+
const unset = (env as Record<string, unknown>).unset
|
|
368
|
+
if (unset !== undefined && (!Array.isArray(unset) || unset.some((key) => typeof key !== 'string' || !key.trim()))) {
|
|
369
|
+
errors.push('meshCoordinator.delegatedWorkerIsolation.env.unset must be an array of non-empty strings')
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const args = isolation.args
|
|
374
|
+
if (args === undefined) return
|
|
375
|
+
if (!Array.isArray(args)) {
|
|
376
|
+
errors.push('meshCoordinator.delegatedWorkerIsolation.args must be an array')
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
for (const [index, rule] of args.entries()) {
|
|
380
|
+
const prefix = `meshCoordinator.delegatedWorkerIsolation.args[${index}]`
|
|
381
|
+
if (!rule || typeof rule !== 'object' || Array.isArray(rule)) {
|
|
382
|
+
errors.push(`${prefix} must be an object`)
|
|
383
|
+
continue
|
|
384
|
+
}
|
|
385
|
+
const item = rule as Record<string, unknown>
|
|
386
|
+
const mode = item.mode
|
|
387
|
+
if (mode !== 'empty_mcp_config' && mode !== 'config_override') {
|
|
388
|
+
errors.push(`${prefix}.mode must be one of: empty_mcp_config, config_override`)
|
|
389
|
+
continue
|
|
390
|
+
}
|
|
391
|
+
for (const key of mode === 'empty_mcp_config' ? ['flag'] : ['flag', 'key', 'value']) {
|
|
392
|
+
const value = item[key]
|
|
393
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
394
|
+
errors.push(`${prefix}.${key} must be a non-empty string`)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
for (const key of ['strictFlag', 'dedupeKey']) {
|
|
398
|
+
const value = item[key]
|
|
399
|
+
if (value !== undefined && (typeof value !== 'string' || !value.trim())) {
|
|
400
|
+
errors.push(`${prefix}.${key} must be a non-empty string when provided`)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
351
406
|
function validateControl(control: ProviderControlDef, errors: string[]): void {
|
|
352
407
|
if (!control || typeof control !== 'object') {
|
|
353
408
|
errors.push('controls: each control must be an object')
|
|
@@ -320,6 +320,52 @@
|
|
|
320
320
|
{ "type": "object", "additionalProperties": false, "required": ["mode", "name"],
|
|
321
321
|
"properties": { "mode": { "const": "env_var" }, "name": { "type": "string" } } }
|
|
322
322
|
]
|
|
323
|
+
},
|
|
324
|
+
"delegatedWorkerIsolation": {
|
|
325
|
+
"description": "Provider-declared launch isolation for coordinator-spawned worker sessions. Keeps worker-only sessions from inheriting coordinator MCP/tools/config.",
|
|
326
|
+
"type": "object",
|
|
327
|
+
"additionalProperties": false,
|
|
328
|
+
"properties": {
|
|
329
|
+
"env": {
|
|
330
|
+
"type": "object",
|
|
331
|
+
"additionalProperties": false,
|
|
332
|
+
"properties": {
|
|
333
|
+
"unset": {
|
|
334
|
+
"type": "array",
|
|
335
|
+
"items": { "type": "string", "minLength": 1 }
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
"args": {
|
|
340
|
+
"type": "array",
|
|
341
|
+
"items": {
|
|
342
|
+
"oneOf": [
|
|
343
|
+
{
|
|
344
|
+
"type": "object",
|
|
345
|
+
"additionalProperties": false,
|
|
346
|
+
"required": ["mode", "flag"],
|
|
347
|
+
"properties": {
|
|
348
|
+
"mode": { "const": "empty_mcp_config" },
|
|
349
|
+
"flag": { "type": "string", "minLength": 1 },
|
|
350
|
+
"strictFlag": { "type": "string", "minLength": 1 }
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"type": "object",
|
|
355
|
+
"additionalProperties": false,
|
|
356
|
+
"required": ["mode", "flag", "key", "value"],
|
|
357
|
+
"properties": {
|
|
358
|
+
"mode": { "const": "config_override" },
|
|
359
|
+
"flag": { "type": "string", "minLength": 1 },
|
|
360
|
+
"key": { "type": "string", "minLength": 1 },
|
|
361
|
+
"value": { "type": "string", "minLength": 1 },
|
|
362
|
+
"dedupeKey": { "type": "string", "minLength": 1 }
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
]
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
323
369
|
}
|
|
324
370
|
}
|
|
325
371
|
},
|
|
@@ -169,8 +169,27 @@ export interface McpConfigDef {
|
|
|
169
169
|
export interface MeshCoordinatorDef {
|
|
170
170
|
supported: boolean;
|
|
171
171
|
mcpConfig?: McpConfigDef;
|
|
172
|
+
systemPromptInjection?: MeshCoordinatorSystemPromptInjectionDef;
|
|
173
|
+
delegatedWorkerIsolation?: MeshCoordinatorDelegatedWorkerIsolationDef;
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
export type MeshCoordinatorSystemPromptInjectionDef =
|
|
177
|
+
| { mode: 'cli_arg'; flag: string }
|
|
178
|
+
| { mode: 'config_override'; flag: string; template: string }
|
|
179
|
+
| { mode: 'context_file'; path: string; wrapper?: string }
|
|
180
|
+
| { mode: 'env_var'; name: string };
|
|
181
|
+
|
|
182
|
+
export interface MeshCoordinatorDelegatedWorkerIsolationDef {
|
|
183
|
+
env?: {
|
|
184
|
+
unset?: ReadonlyArray<string>;
|
|
185
|
+
};
|
|
186
|
+
args?: ReadonlyArray<MeshCoordinatorDelegatedWorkerArgRuleDef>;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export type MeshCoordinatorDelegatedWorkerArgRuleDef =
|
|
190
|
+
| { mode: 'empty_mcp_config'; flag: string; strictFlag?: string }
|
|
191
|
+
| { mode: 'config_override'; flag: string; key: string; value: string; dedupeKey?: string };
|
|
192
|
+
|
|
174
193
|
// ─── Compatibility ──────────────────────────────────────────────────────
|
|
175
194
|
|
|
176
195
|
export interface CompatibilityEntryDef {
|
|
@@ -36,6 +36,14 @@ import {
|
|
|
36
36
|
type InteractivePromptResponse,
|
|
37
37
|
} from '../types/interactive-prompt.js';
|
|
38
38
|
|
|
39
|
+
function stripAnsi(text: string): string {
|
|
40
|
+
// eslint-disable-next-line no-control-regex
|
|
41
|
+
return String(text || '')
|
|
42
|
+
.replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '')
|
|
43
|
+
.replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, '')
|
|
44
|
+
.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
export class SpecCliAdapter implements CliAdapter {
|
|
40
48
|
readonly cliType: string;
|
|
41
49
|
readonly cliName: string;
|
|
@@ -129,15 +137,16 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
129
137
|
}
|
|
130
138
|
|
|
131
139
|
getStatus(): CliAdapterStatus {
|
|
132
|
-
|
|
133
|
-
if (
|
|
140
|
+
const sessionFields = this.providerSessionId ? { providerSessionId: this.providerSessionId } : {};
|
|
141
|
+
if (this.exited) return { status: 'stopped', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
|
|
142
|
+
if (!this.spawned) return { status: 'starting', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
|
|
134
143
|
|
|
135
144
|
// Refresh native history lazily — the watch_path is cheap to stat,
|
|
136
145
|
// but parsing a full session.jsonl every call would be wasteful.
|
|
137
146
|
this.maybeRefreshNativeHistory();
|
|
138
147
|
|
|
139
148
|
const state = this.latestState;
|
|
140
|
-
if (!state) return { status: 'starting', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt };
|
|
149
|
+
if (!state) return { status: 'starting', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
|
|
141
150
|
|
|
142
151
|
const modal = this.latestModal;
|
|
143
152
|
const lc = state.id.toLowerCase();
|
|
@@ -150,12 +159,13 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
150
159
|
buttons: modal.buttons.map(b => b.label),
|
|
151
160
|
},
|
|
152
161
|
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
162
|
+
...sessionFields,
|
|
153
163
|
};
|
|
154
164
|
}
|
|
155
165
|
if (lc === 'busy' || lc === 'generating') {
|
|
156
|
-
return { status: 'generating', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt };
|
|
166
|
+
return { status: 'generating', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
|
|
157
167
|
}
|
|
158
|
-
return { status: 'idle', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt };
|
|
168
|
+
return { status: 'idle', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
|
|
159
169
|
}
|
|
160
170
|
|
|
161
171
|
private maybeRefreshNativeHistory(): void {
|
|
@@ -165,10 +175,13 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
165
175
|
}
|
|
166
176
|
|
|
167
177
|
getScriptParsedStatus(): unknown {
|
|
178
|
+
const providerSessionId = this.extractProviderSessionIdFromScreen();
|
|
179
|
+
if (providerSessionId) this.providerSessionId = providerSessionId;
|
|
168
180
|
const status = this.getStatus();
|
|
169
181
|
return {
|
|
170
182
|
...status,
|
|
171
183
|
messages: this.readClaudeScreenAssistantMessages(),
|
|
184
|
+
...(this.providerSessionId ? { providerSessionId: this.providerSessionId } : {}),
|
|
172
185
|
};
|
|
173
186
|
}
|
|
174
187
|
|
|
@@ -345,6 +358,7 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
345
358
|
displayName: this.spec.name,
|
|
346
359
|
spawnedAtMs: this.spawnedAtMs,
|
|
347
360
|
spawnedEnv: this.spawnedEnv,
|
|
361
|
+
...(this.providerSessionId ? { providerSessionId: this.providerSessionId } : {}),
|
|
348
362
|
};
|
|
349
363
|
}
|
|
350
364
|
updateRuntimeMeta(meta?: Record<string, unknown>): void {
|
|
@@ -419,6 +433,19 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
419
433
|
}
|
|
420
434
|
}
|
|
421
435
|
|
|
436
|
+
private extractProviderSessionIdFromScreen(): string | undefined {
|
|
437
|
+
if (this.cliType !== 'codex-cli') return this.providerSessionId;
|
|
438
|
+
let screenText = '';
|
|
439
|
+
try {
|
|
440
|
+
screenText = this.driver.snapshot();
|
|
441
|
+
} catch {
|
|
442
|
+
return this.providerSessionId;
|
|
443
|
+
}
|
|
444
|
+
const clean = stripAnsi(screenText);
|
|
445
|
+
const match = clean.match(/(?:gpt-|o\d|codex-)[^\n·]*·[^\n·]*·\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
|
|
446
|
+
return match?.[1] || this.providerSessionId;
|
|
447
|
+
}
|
|
448
|
+
|
|
422
449
|
private readClaudeScreenAssistantMessages(): ChatMessage[] {
|
|
423
450
|
if (this.cliType !== 'claude-cli') return [];
|
|
424
451
|
let screenText = '';
|
|
@@ -132,6 +132,37 @@ export function resolveSubmitDelayMs(specBeforeSubmit: number | undefined, text:
|
|
|
132
132
|
return Math.max(spec, SUBMIT_DELAY_FLOOR_MS + linesBonus);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
export function matchesCompletionIdleRule(spec: CliSpec, ev: SpecEvaluation, screen: string): string | null {
|
|
136
|
+
const rule = spec.debounce?.completion_idle_after;
|
|
137
|
+
if (!rule?.regex) return null;
|
|
138
|
+
const haystack = rule.section
|
|
139
|
+
? ev.sections.find(section => section.id === rule.section)?.text ?? ''
|
|
140
|
+
: screen;
|
|
141
|
+
if (!haystack) return null;
|
|
142
|
+
try {
|
|
143
|
+
const regex = new RegExp(rule.regex, rule.flags || '');
|
|
144
|
+
const match = haystack.match(regex);
|
|
145
|
+
return match?.[0] || null;
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function matchesCompletionIdleTargetState(spec: CliSpec, ev: SpecEvaluation, screen: string): boolean {
|
|
152
|
+
const target = spec.states.find(state => state.id === spec.default_state)
|
|
153
|
+
?? spec.states.find(state => state.id === 'idle');
|
|
154
|
+
if (!target?.when?.regex) return false;
|
|
155
|
+
const haystack = target.when.section
|
|
156
|
+
? ev.sections.find(section => section.id === target.when.section)?.text ?? ''
|
|
157
|
+
: screen;
|
|
158
|
+
if (!haystack) return false;
|
|
159
|
+
try {
|
|
160
|
+
return new RegExp(target.when.regex, target.when.flags || 'i').test(haystack);
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
135
166
|
export class SpecDriver {
|
|
136
167
|
private spec!: CliSpec;
|
|
137
168
|
private adapter!: TerminalAdapter;
|
|
@@ -157,6 +188,8 @@ export class SpecDriver {
|
|
|
157
188
|
* because the evaluator already moved past busy by the time the hold
|
|
158
189
|
* kicks in. */
|
|
159
190
|
private lastBusyState: SpecEvaluation['state'] | null = null;
|
|
191
|
+
private completionIdleFirstSeenAt = 0;
|
|
192
|
+
private completionIdleKey = '';
|
|
160
193
|
/** Timer that re-runs evaluate() once the hold window expires. Needed
|
|
161
194
|
* because the PTY stops emitting once the agent finishes; without an
|
|
162
195
|
* explicit wake-up there's nothing to trigger the busy → idle
|
|
@@ -305,6 +338,40 @@ export class SpecDriver {
|
|
|
305
338
|
evState = this.lastBusyState ?? evState;
|
|
306
339
|
}
|
|
307
340
|
}
|
|
341
|
+
const completionIdleRule = this.spec.debounce?.completion_idle_after;
|
|
342
|
+
let busyWakeMs = busyHoldMs;
|
|
343
|
+
if (evState.id === 'busy' && completionIdleRule) {
|
|
344
|
+
const completionKey = matchesCompletionIdleRule(this.spec, ev, screen);
|
|
345
|
+
if (completionKey) {
|
|
346
|
+
const now = Date.now();
|
|
347
|
+
if (completionKey !== this.completionIdleKey) {
|
|
348
|
+
this.completionIdleKey = completionKey;
|
|
349
|
+
this.completionIdleFirstSeenAt = now;
|
|
350
|
+
}
|
|
351
|
+
const holdMs = Math.max(0, completionIdleRule.hold_ms || 0);
|
|
352
|
+
const ageMs = now - this.completionIdleFirstSeenAt;
|
|
353
|
+
if (ageMs >= holdMs) {
|
|
354
|
+
if (matchesCompletionIdleTargetState(this.spec, ev, screen)) {
|
|
355
|
+
const idle = this.spec.states.find(state => state.id === this.spec.default_state)
|
|
356
|
+
?? this.spec.states.find(state => state.id === 'idle');
|
|
357
|
+
evState = idle
|
|
358
|
+
? { id: idle.id, label: idle.label, title: null }
|
|
359
|
+
: { id: 'idle', label: 'Ready', title: null };
|
|
360
|
+
} else {
|
|
361
|
+
busyWakeMs = Math.min(busyWakeMs, 1000);
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
busyWakeMs = Math.min(busyWakeMs, Math.max(holdMs - ageMs, 0));
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
this.completionIdleKey = '';
|
|
368
|
+
this.completionIdleFirstSeenAt = 0;
|
|
369
|
+
}
|
|
370
|
+
} else if (evState.id !== 'busy') {
|
|
371
|
+
this.completionIdleKey = '';
|
|
372
|
+
this.completionIdleFirstSeenAt = 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
308
375
|
if (evState.id === 'busy') {
|
|
309
376
|
this.lastBusyAt = Date.now();
|
|
310
377
|
this.lastBusyState = evState;
|
|
@@ -313,7 +380,7 @@ export class SpecDriver {
|
|
|
313
380
|
// footer settles), so without an explicit timer the driver
|
|
314
381
|
// never wakes up to downshift to idle and the dashboard sees
|
|
315
382
|
// status stuck at generating long after the turn ended.
|
|
316
|
-
this.scheduleBusyExpiry(
|
|
383
|
+
this.scheduleBusyExpiry(busyWakeMs);
|
|
317
384
|
}
|
|
318
385
|
|
|
319
386
|
const changed = forceEmit
|
|
@@ -231,8 +231,18 @@ export function evaluate(spec: CliSpec, screenText: string): SpecEvaluation {
|
|
|
231
231
|
for (const st of spec.states) {
|
|
232
232
|
const { matched, title } = matchState(st, sections, screenText, trace);
|
|
233
233
|
if (!matched) continue;
|
|
234
|
+
const extractedModal = extractModal(st, sections, screenText, title, trace);
|
|
235
|
+
// If the state declares modal_buttons but extraction failed (button
|
|
236
|
+
// count below min_count, or text-was-mistaken-for-modal), do not
|
|
237
|
+
// promote the state. Otherwise we would surface a phantom approval
|
|
238
|
+
// built from arbitrary screen text — see claude-cli numbered-list
|
|
239
|
+
// false-positive on 2026-06-07.
|
|
240
|
+
if (st.modal_buttons && !extractedModal) {
|
|
241
|
+
trace.push({ kind: 'state_skip', text: `state[${st.id}] matched but modal_buttons extraction failed — not promoting` });
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
234
244
|
activeState = { id: st.id, label: st.label, title };
|
|
235
|
-
modal =
|
|
245
|
+
modal = extractedModal;
|
|
236
246
|
break;
|
|
237
247
|
}
|
|
238
248
|
|