@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
|
@@ -73,6 +73,12 @@ type CompletionFinalAssistantEvidence = {
|
|
|
73
73
|
source: 'parsed' | 'external-native' | 'unavailable';
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
+
type ExternalNativeFinalReconciliation = {
|
|
77
|
+
fingerprint: string;
|
|
78
|
+
finalSummary: string;
|
|
79
|
+
evidence: CompletionFinalAssistantEvidence;
|
|
80
|
+
};
|
|
81
|
+
|
|
76
82
|
type ExternalTranscriptProbe = {
|
|
77
83
|
readAt: number;
|
|
78
84
|
msgCount: number;
|
|
@@ -369,6 +375,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
369
375
|
private historyWriter: ChatHistoryWriter;
|
|
370
376
|
private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
|
|
371
377
|
private lastPersistedHistoryMessages: PersistableCliHistoryMessage[] = [];
|
|
378
|
+
private lastAcknowledgedUserInputAt = 0;
|
|
379
|
+
private externalBusyIdleFingerprint = '';
|
|
372
380
|
private lastNativeSourceCanonicalCheckAt = 0;
|
|
373
381
|
private lastNativeSourceCanonicalCacheKey: string | undefined = undefined;
|
|
374
382
|
private cachedSqliteDb: {
|
|
@@ -587,9 +595,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
587
595
|
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
|
588
596
|
);
|
|
589
597
|
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, Date.now());
|
|
590
|
-
|
|
598
|
+
let visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
|
|
591
599
|
? 'error'
|
|
592
600
|
: (autoApproveActive ? 'generating' : adapterStatus.status);
|
|
601
|
+
const externalNativeFinal = this.getExternalNativeFinalReconciliation(parsedStatus?.messages, adapterStatus);
|
|
602
|
+
if (externalNativeFinal && isCliGeneratingLikeStatus(visibleStatus)) {
|
|
603
|
+
visibleStatus = 'idle';
|
|
604
|
+
}
|
|
593
605
|
const runtime = this.adapter.getRuntimeMetadata();
|
|
594
606
|
this.maybeAppendRuntimeRecoveryMessage(runtime);
|
|
595
607
|
let parsedMessages = Array.isArray(parsedStatus?.messages)
|
|
@@ -788,7 +800,22 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
788
800
|
}
|
|
789
801
|
|
|
790
802
|
updateSettings(newSettings: Record<string, any>): void {
|
|
791
|
-
|
|
803
|
+
const runtimeMeshSettings: Record<string, any> = {};
|
|
804
|
+
for (const key of [
|
|
805
|
+
'meshNodeFor',
|
|
806
|
+
'meshNodeId',
|
|
807
|
+
'meshActiveTaskId',
|
|
808
|
+
'meshCoordinatorFor',
|
|
809
|
+
'meshCoordinatorDaemonId',
|
|
810
|
+
'meshCoordinatorNodeId',
|
|
811
|
+
'spawnedSessionVisibility',
|
|
812
|
+
'launchedByCoordinator',
|
|
813
|
+
]) {
|
|
814
|
+
if (this.settings[key] !== undefined && newSettings[key] === undefined) {
|
|
815
|
+
runtimeMeshSettings[key] = this.settings[key];
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
this.settings = { ...newSettings, ...runtimeMeshSettings };
|
|
792
819
|
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
793
820
|
this.monitor.updateConfig({
|
|
794
821
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
@@ -884,6 +911,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
884
911
|
if (!content) return;
|
|
885
912
|
|
|
886
913
|
const receivedAt = Date.now();
|
|
914
|
+
this.lastAcknowledgedUserInputAt = receivedAt;
|
|
915
|
+
this.externalBusyIdleFingerprint = '';
|
|
887
916
|
const dedupKey = `user_input_ack:${crypto
|
|
888
917
|
.createHash('sha256')
|
|
889
918
|
.update(`${this.instanceId}:${content}:${receivedAt}`)
|
|
@@ -1049,6 +1078,59 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1049
1078
|
return extractFinalSummaryFromMessages(evidence.messages as any);
|
|
1050
1079
|
}
|
|
1051
1080
|
|
|
1081
|
+
private externalNativeFinalFingerprint(evidence: CompletionFinalAssistantEvidence): string {
|
|
1082
|
+
const messages = Array.isArray(evidence.messages) ? evidence.messages : [];
|
|
1083
|
+
const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1084
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1085
|
+
const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
|
|
1086
|
+
const receivedAt = lastVisible ? getMessageTime(lastVisible) : 0;
|
|
1087
|
+
const probe = this.lastExternalCompletionProbe;
|
|
1088
|
+
return crypto
|
|
1089
|
+
.createHash('sha256')
|
|
1090
|
+
.update([
|
|
1091
|
+
this.type,
|
|
1092
|
+
this.providerSessionId || '',
|
|
1093
|
+
probe?.sourcePath || '',
|
|
1094
|
+
String(probe?.sourceMtimeMs || 0),
|
|
1095
|
+
String(receivedAt || 0),
|
|
1096
|
+
content.slice(-500),
|
|
1097
|
+
].join('\0'))
|
|
1098
|
+
.digest('hex')
|
|
1099
|
+
.slice(0, 24);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
private getExternalNativeFinalReconciliation(parsedMessages: unknown, adapterStatus: any): ExternalNativeFinalReconciliation | null {
|
|
1103
|
+
const rawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
|
|
1104
|
+
if (!isCliGeneratingLikeStatus(rawStatus)) return null;
|
|
1105
|
+
if (hasNonEmptyCliModalButtons(adapterStatus?.activeModal ?? adapterStatus?.modal)) return null;
|
|
1106
|
+
|
|
1107
|
+
const evidence = this.completionFinalAssistantEvidence(parsedMessages);
|
|
1108
|
+
if (evidence.source !== 'external-native' || !evidence.present) return null;
|
|
1109
|
+
|
|
1110
|
+
const messages = Array.isArray(evidence.messages) ? evidence.messages : [];
|
|
1111
|
+
const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1112
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1113
|
+
const lastMessageAt = lastVisible ? getMessageTime(lastVisible) : 0;
|
|
1114
|
+
const sourceMtimeMs = Number(this.lastExternalCompletionProbe?.sourceMtimeMs || 0);
|
|
1115
|
+
const minEvidenceAt = Math.max(
|
|
1116
|
+
this.startedAt > 0 ? this.startedAt - 5_000 : 0,
|
|
1117
|
+
this.generatingStartedAt > 0 ? this.generatingStartedAt - 5_000 : 0,
|
|
1118
|
+
this.lastAcknowledgedUserInputAt > 0 ? this.lastAcknowledgedUserInputAt - 1_000 : 0,
|
|
1119
|
+
);
|
|
1120
|
+
if (minEvidenceAt > 0 && lastMessageAt > 0 && lastMessageAt < minEvidenceAt && sourceMtimeMs < minEvidenceAt) {
|
|
1121
|
+
return null;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const finalSummary = extractFinalSummaryFromMessages(evidence.messages as any);
|
|
1125
|
+
if (!finalSummary) return null;
|
|
1126
|
+
const fingerprint = this.externalNativeFinalFingerprint(evidence);
|
|
1127
|
+
if (fingerprint === this.externalBusyIdleFingerprint) {
|
|
1128
|
+
return { fingerprint, finalSummary, evidence };
|
|
1129
|
+
}
|
|
1130
|
+
this.externalBusyIdleFingerprint = fingerprint;
|
|
1131
|
+
return { fingerprint, finalSummary, evidence };
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1052
1134
|
private buildCompletedFinalizationDiagnostic(args: {
|
|
1053
1135
|
blockReason: string;
|
|
1054
1136
|
latestStatus?: any;
|
|
@@ -1138,12 +1220,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1138
1220
|
return true;
|
|
1139
1221
|
}
|
|
1140
1222
|
|
|
1141
|
-
private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending): CompletedFinalizationBlock | null {
|
|
1223
|
+
private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending, opts?: { externalNativeFinal?: ExternalNativeFinalReconciliation | null }): CompletedFinalizationBlock | null {
|
|
1142
1224
|
if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
|
|
1143
1225
|
|
|
1144
1226
|
const adapterAny = this.adapter as any;
|
|
1145
1227
|
const approvalResolvedIdle = pending.previousStatus === 'waiting_approval';
|
|
1146
|
-
|
|
1228
|
+
const externalNativeFinal = opts?.externalNativeFinal || null;
|
|
1229
|
+
if (!approvalResolvedIdle && !externalNativeFinal) {
|
|
1147
1230
|
if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
|
|
1148
1231
|
if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
|
|
1149
1232
|
if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
|
|
@@ -1152,7 +1235,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1152
1235
|
const partial = typeof this.adapter.getPartialResponse === 'function'
|
|
1153
1236
|
? this.adapter.getPartialResponse()
|
|
1154
1237
|
: '';
|
|
1155
|
-
if (typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
|
|
1238
|
+
if (!externalNativeFinal && typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
|
|
1156
1239
|
|
|
1157
1240
|
let parsed: any;
|
|
1158
1241
|
try {
|
|
@@ -1164,6 +1247,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1164
1247
|
const parsedStatus = typeof parsed?.status === 'string' ? parsed.status : 'unknown';
|
|
1165
1248
|
if (parsedStatus !== 'idle') {
|
|
1166
1249
|
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1250
|
+
if (externalNativeFinal && isCliGeneratingLikeStatus(parsedStatus)) return null;
|
|
1167
1251
|
if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
|
|
1168
1252
|
return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
|
|
1169
1253
|
}
|
|
@@ -1230,7 +1314,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1230
1314
|
|
|
1231
1315
|
const latestStatus = this.adapter.getStatus({ allowParse: false });
|
|
1232
1316
|
const latestAutoApproveActive = latestStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
1233
|
-
const
|
|
1317
|
+
const externalNativeFinal = this.getExternalNativeFinalReconciliation(undefined, latestStatus);
|
|
1318
|
+
const latestVisibleStatus = externalNativeFinal && isCliGeneratingLikeStatus(latestStatus.status)
|
|
1319
|
+
? 'idle'
|
|
1320
|
+
: (latestAutoApproveActive ? 'generating' : latestStatus.status);
|
|
1234
1321
|
if (latestVisibleStatus !== 'idle') {
|
|
1235
1322
|
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
|
|
1236
1323
|
this.completedDebouncePending = null;
|
|
@@ -1238,7 +1325,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1238
1325
|
return;
|
|
1239
1326
|
}
|
|
1240
1327
|
|
|
1241
|
-
const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending);
|
|
1328
|
+
const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending, { externalNativeFinal });
|
|
1242
1329
|
if (block) {
|
|
1243
1330
|
const blockReason = block.reason;
|
|
1244
1331
|
const waitedMs = Date.now() - pending.firstObservedAt;
|
|
@@ -1282,7 +1369,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1282
1369
|
chatTitle: pending.chatTitle,
|
|
1283
1370
|
duration: pending.duration,
|
|
1284
1371
|
timestamp: pending.timestamp,
|
|
1285
|
-
finalSummary: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
1372
|
+
finalSummary: externalNativeFinal?.finalSummary || this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
1373
|
+
...(externalNativeFinal ? {
|
|
1374
|
+
completionDiagnostic: {
|
|
1375
|
+
providerType: this.type,
|
|
1376
|
+
sessionId: this.instanceId,
|
|
1377
|
+
providerSessionId: this.providerSessionId || null,
|
|
1378
|
+
reconciliationReason: 'external_native_final_assistant_while_adapter_busy',
|
|
1379
|
+
finalAssistantPresent: true,
|
|
1380
|
+
finalAssistantEvidenceSource: externalNativeFinal.evidence.source,
|
|
1381
|
+
externalFinalFingerprint: externalNativeFinal.fingerprint,
|
|
1382
|
+
},
|
|
1383
|
+
} : {}),
|
|
1286
1384
|
});
|
|
1287
1385
|
this.completedDebouncePending = null;
|
|
1288
1386
|
this.completedDebounceTimer = null;
|
|
@@ -1359,7 +1457,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1359
1457
|
const parsedStatus = null;
|
|
1360
1458
|
const rawStatus = adapterStatus.status;
|
|
1361
1459
|
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
|
|
1362
|
-
const
|
|
1460
|
+
const externalNativeFinal = this.getExternalNativeFinalReconciliation(undefined, adapterStatus);
|
|
1461
|
+
const newStatus = externalNativeFinal && isCliGeneratingLikeStatus(rawStatus)
|
|
1462
|
+
? 'idle'
|
|
1463
|
+
: (autoApproveActive ? 'generating' : rawStatus);
|
|
1363
1464
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
1364
1465
|
const chatTitle = `${this.provider.name} · ${dirName}`;
|
|
1365
1466
|
const partial = this.adapter.getPartialResponse();
|
|
@@ -401,7 +401,62 @@ export interface ProviderModule {
|
|
|
401
401
|
spawnArgBuilder?: (config: Record<string, string>) => string[];
|
|
402
402
|
/** ACP agent auth methods (multiple supported — in priority order) */
|
|
403
403
|
auth?: AcpAuthMethod[];
|
|
404
|
+
/**
|
|
405
|
+
* Repo Mesh coordinator capability and MCP ingestion behavior.
|
|
406
|
+
* Providers must declare this rather than relying on daemon hardcoded CLI quirks.
|
|
407
|
+
*/
|
|
408
|
+
meshCoordinator?: ProviderMeshCoordinatorConfig;
|
|
409
|
+
}
|
|
410
|
+
export type MeshCoordinatorMcpConfigMode = 'auto_import' | 'manual' | 'none';
|
|
411
|
+
export type MeshCoordinatorMcpConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
412
|
+
export interface ProviderMeshCoordinatorConfig {
|
|
413
|
+
supported: boolean;
|
|
414
|
+
reason?: string;
|
|
415
|
+
mcpConfig?: {
|
|
416
|
+
mode: MeshCoordinatorMcpConfigMode;
|
|
417
|
+
format?: MeshCoordinatorMcpConfigFormat;
|
|
418
|
+
path?: string;
|
|
419
|
+
serverName?: string;
|
|
420
|
+
configPathCommand?: string;
|
|
421
|
+
requiresRestart?: boolean;
|
|
422
|
+
instructions?: string;
|
|
423
|
+
template?: string;
|
|
424
|
+
};
|
|
425
|
+
systemPromptInjection?: MeshCoordinatorSystemPromptInjection;
|
|
426
|
+
delegatedWorkerIsolation?: MeshCoordinatorDelegatedWorkerIsolation;
|
|
404
427
|
}
|
|
428
|
+
export type MeshCoordinatorSystemPromptInjection = {
|
|
429
|
+
mode: 'cli_arg';
|
|
430
|
+
flag: string;
|
|
431
|
+
} | {
|
|
432
|
+
mode: 'config_override';
|
|
433
|
+
flag: string;
|
|
434
|
+
template: string;
|
|
435
|
+
} | {
|
|
436
|
+
mode: 'context_file';
|
|
437
|
+
path: string;
|
|
438
|
+
wrapper?: string;
|
|
439
|
+
} | {
|
|
440
|
+
mode: 'env_var';
|
|
441
|
+
name: string;
|
|
442
|
+
};
|
|
443
|
+
export interface MeshCoordinatorDelegatedWorkerIsolation {
|
|
444
|
+
env?: {
|
|
445
|
+
unset?: string[];
|
|
446
|
+
};
|
|
447
|
+
args?: MeshCoordinatorDelegatedWorkerArgRule[];
|
|
448
|
+
}
|
|
449
|
+
export type MeshCoordinatorDelegatedWorkerArgRule = {
|
|
450
|
+
mode: 'empty_mcp_config';
|
|
451
|
+
flag: string;
|
|
452
|
+
strictFlag?: string;
|
|
453
|
+
} | {
|
|
454
|
+
mode: 'config_override';
|
|
455
|
+
flag: string;
|
|
456
|
+
key: string;
|
|
457
|
+
value: string;
|
|
458
|
+
dedupeKey?: string;
|
|
459
|
+
};
|
|
405
460
|
export interface ProviderResumeCapability {
|
|
406
461
|
supported: boolean;
|
|
407
462
|
stopStrategy?: 'command' | 'ctrl_c';
|
|
@@ -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
|
*
|
|
@@ -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 {
|
|
@@ -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
|
|
@@ -130,7 +130,18 @@ export const SCHEMA = {
|
|
|
130
130
|
"additionalProperties": false,
|
|
131
131
|
"properties": {
|
|
132
132
|
"busy_hold_ms": { "type": "integer", "minimum": 0 },
|
|
133
|
-
"startup_grace_ms": { "type": "integer", "minimum": 0 }
|
|
133
|
+
"startup_grace_ms": { "type": "integer", "minimum": 0 },
|
|
134
|
+
"completion_idle_after": {
|
|
135
|
+
"type": "object",
|
|
136
|
+
"additionalProperties": false,
|
|
137
|
+
"required": ["regex", "hold_ms"],
|
|
138
|
+
"properties": {
|
|
139
|
+
"section": { "type": "string", "minLength": 1 },
|
|
140
|
+
"regex": { "type": "string", "minLength": 1 },
|
|
141
|
+
"flags": { "type": "string" },
|
|
142
|
+
"hold_ms": { "type": "integer", "minimum": 0 }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
134
145
|
}
|
|
135
146
|
}
|
|
136
147
|
},
|
|
@@ -59,7 +59,27 @@
|
|
|
59
59
|
"default": [],
|
|
60
60
|
"items": { "$ref": "#/definitions/delegateTrigger" }
|
|
61
61
|
},
|
|
62
|
-
"native_history": { "$ref": "#/definitions/nativeHistory" }
|
|
62
|
+
"native_history": { "$ref": "#/definitions/nativeHistory" },
|
|
63
|
+
"cli_version_range": { "type": "string", "minLength": 1 },
|
|
64
|
+
"debounce": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"properties": {
|
|
68
|
+
"busy_hold_ms": { "type": "integer", "minimum": 0 },
|
|
69
|
+
"startup_grace_ms": { "type": "integer", "minimum": 0 },
|
|
70
|
+
"completion_idle_after": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"additionalProperties": false,
|
|
73
|
+
"required": ["regex", "hold_ms"],
|
|
74
|
+
"properties": {
|
|
75
|
+
"section": { "type": "string", "minLength": 1 },
|
|
76
|
+
"regex": { "type": "string", "minLength": 1 },
|
|
77
|
+
"flags": { "type": "string" },
|
|
78
|
+
"hold_ms": { "type": "integer", "minimum": 0 }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
63
83
|
},
|
|
64
84
|
"definitions": {
|
|
65
85
|
"size": {
|
|
@@ -227,5 +227,15 @@ export interface CliSpec {
|
|
|
227
227
|
* once the window passes and an idle state has actually been
|
|
228
228
|
* observed. */
|
|
229
229
|
startup_grace_ms?: number;
|
|
230
|
+
/** Treat a provider-specific completion marker as idle after it has
|
|
231
|
+
* remained visible for hold_ms. This handles TUIs that leave their
|
|
232
|
+
* last spinner glyph next to a completed timer, causing the normal
|
|
233
|
+
* busy regex to keep matching after the turn is done. */
|
|
234
|
+
completion_idle_after?: {
|
|
235
|
+
section?: string;
|
|
236
|
+
regex: string;
|
|
237
|
+
flags?: string;
|
|
238
|
+
hold_ms: number;
|
|
239
|
+
};
|
|
230
240
|
};
|
|
231
241
|
}
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -359,11 +359,17 @@ export interface RepoMeshSessionStatus {
|
|
|
359
359
|
sessionId: string;
|
|
360
360
|
providerType?: string;
|
|
361
361
|
state?: string;
|
|
362
|
+
chatStatus?: string;
|
|
362
363
|
lifecycle?: 'starting' | 'running' | 'stopping' | 'stopped' | 'failed' | 'interrupted';
|
|
363
364
|
surfaceKind?: 'live_runtime' | 'recovery_snapshot' | 'inactive_record';
|
|
364
365
|
recoveryState?: string | null;
|
|
365
366
|
workspace?: string | null;
|
|
366
367
|
title?: string | null;
|
|
368
|
+
role?: string | null;
|
|
369
|
+
isSelfCoordinator?: boolean;
|
|
370
|
+
statusNote?: string | null;
|
|
371
|
+
createdAt?: string | null;
|
|
372
|
+
startedAt?: string | null;
|
|
367
373
|
lastActivityAt?: string | null;
|
|
368
374
|
isCached?: boolean;
|
|
369
375
|
}
|