@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
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -608,6 +608,166 @@ function hasUnterminalDirectDispatchLedgerEntry(meshId: string, sessionId: strin
|
|
|
608
608
|
return false;
|
|
609
609
|
}
|
|
610
610
|
|
|
611
|
+
function findDirectDispatchLedgerEntry(args: {
|
|
612
|
+
meshId: string;
|
|
613
|
+
taskId: string;
|
|
614
|
+
sessionId?: string;
|
|
615
|
+
}): { id: string; timestamp: string; nodeId?: string; sessionId?: string; providerType?: string; payload: Record<string, unknown> } | null {
|
|
616
|
+
const entries = readLedgerEntries(args.meshId, { tail: 500 });
|
|
617
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
618
|
+
const entry = entries[i];
|
|
619
|
+
if (entry.kind !== 'task_dispatched') continue;
|
|
620
|
+
const payloadTaskId = readNonEmptyString(entry.payload?.taskId);
|
|
621
|
+
if (payloadTaskId !== args.taskId) continue;
|
|
622
|
+
if (args.sessionId && entry.sessionId && entry.sessionId !== args.sessionId) continue;
|
|
623
|
+
return {
|
|
624
|
+
id: entry.id,
|
|
625
|
+
timestamp: entry.timestamp,
|
|
626
|
+
nodeId: entry.nodeId,
|
|
627
|
+
sessionId: entry.sessionId,
|
|
628
|
+
providerType: entry.providerType,
|
|
629
|
+
payload: entry.payload || {},
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function hasTerminalLedgerAfterDispatch(args: {
|
|
636
|
+
meshId: string;
|
|
637
|
+
taskId: string;
|
|
638
|
+
sessionId?: string;
|
|
639
|
+
dispatchEntryId?: string;
|
|
640
|
+
dispatchTimestamp?: string;
|
|
641
|
+
}): boolean {
|
|
642
|
+
const entries = readLedgerEntries(args.meshId, { tail: 500 });
|
|
643
|
+
let afterDispatch = !args.dispatchEntryId && !args.dispatchTimestamp;
|
|
644
|
+
const dispatchTime = args.dispatchTimestamp ? new Date(args.dispatchTimestamp).getTime() : Number.NaN;
|
|
645
|
+
for (const entry of entries) {
|
|
646
|
+
if (!afterDispatch) {
|
|
647
|
+
if (args.dispatchEntryId && entry.id === args.dispatchEntryId) {
|
|
648
|
+
afterDispatch = true;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (!args.dispatchEntryId && Number.isFinite(dispatchTime)) {
|
|
652
|
+
const entryTime = new Date(entry.timestamp).getTime();
|
|
653
|
+
if (Number.isFinite(entryTime) && entryTime >= dispatchTime) afterDispatch = true;
|
|
654
|
+
}
|
|
655
|
+
if (!afterDispatch) continue;
|
|
656
|
+
}
|
|
657
|
+
if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
|
|
658
|
+
const terminalTaskId = readNonEmptyString(entry.payload?.taskId);
|
|
659
|
+
if (terminalTaskId && terminalTaskId === args.taskId) return true;
|
|
660
|
+
if (terminalTaskId && terminalTaskId !== args.taskId) continue;
|
|
661
|
+
if (args.sessionId && entry.sessionId === args.sessionId) return true;
|
|
662
|
+
}
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export function reconcileDirectDispatchCompletionFromTranscript(args: {
|
|
667
|
+
meshId: string;
|
|
668
|
+
nodeId?: string;
|
|
669
|
+
sessionId: string;
|
|
670
|
+
providerType?: string;
|
|
671
|
+
providerSessionId?: string;
|
|
672
|
+
taskId: string;
|
|
673
|
+
finalSummary: string;
|
|
674
|
+
transcriptMessageAt?: string;
|
|
675
|
+
completedAt?: string;
|
|
676
|
+
targetCoordinatorDaemonId?: string;
|
|
677
|
+
source?: string;
|
|
678
|
+
}): { reconciled: boolean; kind?: MeshLedgerKind; alreadyTerminal?: boolean; workerResult?: unknown; ledgerEntryId?: string; reason?: string } {
|
|
679
|
+
const finalSummary = readNonEmptyString(args.finalSummary);
|
|
680
|
+
if (!args.meshId || !args.taskId || !args.sessionId || !finalSummary) {
|
|
681
|
+
return { reconciled: false, reason: 'missing_required_completion_evidence' };
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const dispatch = findDirectDispatchLedgerEntry({
|
|
685
|
+
meshId: args.meshId,
|
|
686
|
+
taskId: args.taskId,
|
|
687
|
+
sessionId: args.sessionId,
|
|
688
|
+
});
|
|
689
|
+
if (hasTerminalLedgerAfterDispatch({
|
|
690
|
+
meshId: args.meshId,
|
|
691
|
+
taskId: args.taskId,
|
|
692
|
+
sessionId: args.sessionId,
|
|
693
|
+
dispatchEntryId: dispatch?.id,
|
|
694
|
+
dispatchTimestamp: dispatch?.timestamp,
|
|
695
|
+
})) {
|
|
696
|
+
return { reconciled: false, alreadyTerminal: true, reason: 'terminal_ledger_entry_exists' };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const nodeId = readNonEmptyString(args.nodeId) || dispatch?.nodeId;
|
|
700
|
+
const providerType = readNonEmptyString(args.providerType) || dispatch?.providerType || readNonEmptyString(dispatch?.payload.providerType);
|
|
701
|
+
const completedAt = args.completedAt || new Date().toISOString();
|
|
702
|
+
const evidence = buildTaskCompletionEvidence({
|
|
703
|
+
event: 'agent:generating_completed',
|
|
704
|
+
nodeId: nodeId || 'unknown',
|
|
705
|
+
sessionId: args.sessionId,
|
|
706
|
+
providerType,
|
|
707
|
+
providerSessionId: readNonEmptyString(args.providerSessionId),
|
|
708
|
+
finalSummary,
|
|
709
|
+
completedAt,
|
|
710
|
+
});
|
|
711
|
+
const workerResult = evidence.workerResult;
|
|
712
|
+
const dispatchTime = dispatch?.timestamp ? new Date(dispatch.timestamp).getTime() : Number.NaN;
|
|
713
|
+
const transcriptTime = args.transcriptMessageAt ? new Date(args.transcriptMessageAt).getTime() : Number.NaN;
|
|
714
|
+
const transcriptAfterDispatch = Number.isFinite(dispatchTime) && Number.isFinite(transcriptTime) && transcriptTime >= dispatchTime;
|
|
715
|
+
if (workerResult.source !== 'final_summary_json' && !transcriptAfterDispatch) {
|
|
716
|
+
return { reconciled: false, reason: 'transcript_not_proven_after_dispatch' };
|
|
717
|
+
}
|
|
718
|
+
const workerFailed = workerResult.status === 'failed' || (workerResult.status !== 'completed' && workerResult.errors.length > 0);
|
|
719
|
+
const kind: MeshLedgerKind = workerFailed ? 'task_failed' : 'task_completed';
|
|
720
|
+
|
|
721
|
+
const entry = appendLedgerEntry(args.meshId, {
|
|
722
|
+
kind,
|
|
723
|
+
nodeId: nodeId || undefined,
|
|
724
|
+
sessionId: args.sessionId,
|
|
725
|
+
providerType: providerType || undefined,
|
|
726
|
+
payload: {
|
|
727
|
+
event: 'agent:generating_completed',
|
|
728
|
+
source: args.source || 'direct_task_transcript_reconciliation',
|
|
729
|
+
taskId: args.taskId,
|
|
730
|
+
providerSessionId: readNonEmptyString(args.providerSessionId),
|
|
731
|
+
finalSummary,
|
|
732
|
+
workerResult,
|
|
733
|
+
completionDiagnostic: {
|
|
734
|
+
reason: 'direct_task_transcript_reconciliation',
|
|
735
|
+
dispatchEntryId: dispatch?.id,
|
|
736
|
+
dispatchTimestamp: dispatch?.timestamp,
|
|
737
|
+
transcriptMessageAt: readNonEmptyString(args.transcriptMessageAt),
|
|
738
|
+
transcriptFinalAssistantPresent: true,
|
|
739
|
+
},
|
|
740
|
+
evidence,
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
updateDirectDispatchStatus(args.meshId, args.sessionId, kind === 'task_completed' ? 'completed' : 'failed');
|
|
744
|
+
setImmediate(() => cleanupTerminalDirectDispatches());
|
|
745
|
+
queuePendingMeshCoordinatorEvent({
|
|
746
|
+
event: kind === 'task_completed' ? 'agent:generating_completed' : 'agent:stopped',
|
|
747
|
+
meshId: args.meshId,
|
|
748
|
+
nodeLabel: nodeId ? `Node '${nodeId}'` : 'Remote agent',
|
|
749
|
+
nodeId: nodeId || undefined,
|
|
750
|
+
metadataEvent: {
|
|
751
|
+
targetSessionId: args.sessionId,
|
|
752
|
+
providerType: providerType || undefined,
|
|
753
|
+
providerSessionId: readNonEmptyString(args.providerSessionId),
|
|
754
|
+
finalSummary,
|
|
755
|
+
taskId: args.taskId,
|
|
756
|
+
workerResult,
|
|
757
|
+
completionDiagnostic: {
|
|
758
|
+
reason: 'direct_task_transcript_reconciliation',
|
|
759
|
+
terminalLedgerKind: kind,
|
|
760
|
+
terminalLedgerId: entry.id,
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
coordinatorMessage: undefined,
|
|
764
|
+
queuedAt: Date.now(),
|
|
765
|
+
...(readNonEmptyString(args.targetCoordinatorDaemonId) ? { targetCoordinatorDaemonId: readNonEmptyString(args.targetCoordinatorDaemonId) } : {}),
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
return { reconciled: true, kind, workerResult, ledgerEntryId: entry.id };
|
|
769
|
+
}
|
|
770
|
+
|
|
611
771
|
function buildLongGeneratingCompletionReconciliation(args: {
|
|
612
772
|
meshId: string;
|
|
613
773
|
nodeId?: string;
|
|
@@ -1019,13 +1179,69 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
1019
1179
|
return false;
|
|
1020
1180
|
}
|
|
1021
1181
|
|
|
1182
|
+
export interface MeshQueueTriggerResult {
|
|
1183
|
+
success: true;
|
|
1184
|
+
meshId: string;
|
|
1185
|
+
pendingBefore: number;
|
|
1186
|
+
assignedBefore: number;
|
|
1187
|
+
pendingAfter: number;
|
|
1188
|
+
assignedAfter: number;
|
|
1189
|
+
claimed: boolean;
|
|
1190
|
+
newlyAssignedTasks: Array<{
|
|
1191
|
+
id: string;
|
|
1192
|
+
nodeId?: string;
|
|
1193
|
+
sessionId?: string;
|
|
1194
|
+
}>;
|
|
1195
|
+
localIdleSessionsChecked: number;
|
|
1196
|
+
remoteIdleSessionsChecked: number;
|
|
1197
|
+
skippedSessions: Array<{
|
|
1198
|
+
nodeId?: string;
|
|
1199
|
+
sessionId?: string;
|
|
1200
|
+
reason: string;
|
|
1201
|
+
status?: string;
|
|
1202
|
+
}>;
|
|
1203
|
+
autoLaunchStarted: boolean;
|
|
1204
|
+
noIdleMeshSessionAvailable?: boolean;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function countQueueStatus(meshId: string, status: 'pending' | 'assigned'): number {
|
|
1208
|
+
return getQueue(meshId, { status: [status] as any }).length;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function getQueueStatusById(meshId: string): Map<string, string> {
|
|
1212
|
+
return new Map(getQueue(meshId).map(task => [task.id, task.status]));
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1022
1215
|
/**
|
|
1023
1216
|
* Triggers a queue check for all nodes in the mesh.
|
|
1024
1217
|
* Called when a new task is enqueued, in case nodes are already idle.
|
|
1025
1218
|
*/
|
|
1026
|
-
export async function triggerMeshQueue(components: DaemonComponents, meshId: string): Promise<
|
|
1219
|
+
export async function triggerMeshQueue(components: DaemonComponents, meshId: string): Promise<MeshQueueTriggerResult> {
|
|
1027
1220
|
const mesh = getMeshWithCache(components, meshId);
|
|
1028
|
-
|
|
1221
|
+
const pendingBefore = countQueueStatus(meshId, 'pending');
|
|
1222
|
+
const assignedBefore = countQueueStatus(meshId, 'assigned');
|
|
1223
|
+
const beforeStatus = getQueueStatusById(meshId);
|
|
1224
|
+
const skippedSessions: MeshQueueTriggerResult['skippedSessions'] = [];
|
|
1225
|
+
let localIdleSessionsChecked = 0;
|
|
1226
|
+
let remoteIdleSessionsChecked = 0;
|
|
1227
|
+
let autoLaunchStarted = false;
|
|
1228
|
+
if (!mesh) {
|
|
1229
|
+
return {
|
|
1230
|
+
success: true,
|
|
1231
|
+
meshId,
|
|
1232
|
+
pendingBefore,
|
|
1233
|
+
assignedBefore,
|
|
1234
|
+
pendingAfter: pendingBefore,
|
|
1235
|
+
assignedAfter: assignedBefore,
|
|
1236
|
+
claimed: false,
|
|
1237
|
+
newlyAssignedTasks: [],
|
|
1238
|
+
localIdleSessionsChecked,
|
|
1239
|
+
remoteIdleSessionsChecked,
|
|
1240
|
+
skippedSessions: [{ reason: 'mesh_not_found' }],
|
|
1241
|
+
autoLaunchStarted,
|
|
1242
|
+
noIdleMeshSessionAvailable: true,
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1029
1245
|
|
|
1030
1246
|
// Find all CLI instances that belong to this mesh and are idle
|
|
1031
1247
|
const cliInstances = components.instanceManager.getByCategory('cli');
|
|
@@ -1042,14 +1258,30 @@ export async function triggerMeshQueue(components: DaemonComponents, meshId: str
|
|
|
1042
1258
|
// Only genuinely idle live sessions can pull work. Restored/stopped
|
|
1043
1259
|
// records are kept for transcript/recovery visibility, but assigning
|
|
1044
1260
|
// queue items to them strands tasks in assigned/pending without chat.
|
|
1045
|
-
if (!isIdleSessionState(state))
|
|
1261
|
+
if (!isIdleSessionState(state)) {
|
|
1262
|
+
const status = readNonEmptyString(state.status).toLowerCase();
|
|
1263
|
+
skippedSessions.push({
|
|
1264
|
+
nodeId,
|
|
1265
|
+
sessionId: readNonEmptyString(state.instanceId),
|
|
1266
|
+
reason: isTerminalSessionStatus(status) ? 'terminal_session' : 'session_not_idle',
|
|
1267
|
+
status: status || undefined,
|
|
1268
|
+
});
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1046
1271
|
|
|
1047
1272
|
const sessionId = state.instanceId;
|
|
1048
1273
|
const providerType = state.type || readNonEmptyString(settings.providerType);
|
|
1049
1274
|
|
|
1050
1275
|
if (providerType) {
|
|
1051
1276
|
// Try to assign a task to this idle node
|
|
1277
|
+
localIdleSessionsChecked += 1;
|
|
1052
1278
|
tryAssignQueueTask(components, meshId, nodeId, sessionId, providerType);
|
|
1279
|
+
} else {
|
|
1280
|
+
skippedSessions.push({
|
|
1281
|
+
nodeId,
|
|
1282
|
+
sessionId,
|
|
1283
|
+
reason: 'provider_type_missing',
|
|
1284
|
+
});
|
|
1053
1285
|
}
|
|
1054
1286
|
}
|
|
1055
1287
|
|
|
@@ -1058,6 +1290,7 @@ export async function triggerMeshQueue(components: DaemonComponents, meshId: str
|
|
|
1058
1290
|
// Find if this node is in the same mesh
|
|
1059
1291
|
const node = mesh.nodes.find((n: any) => n.id === idle.nodeId);
|
|
1060
1292
|
if (node) {
|
|
1293
|
+
remoteIdleSessionsChecked += 1;
|
|
1061
1294
|
const assigned = tryAssignQueueTask(components, meshId, idle.nodeId, idle.sessionId, idle.providerType);
|
|
1062
1295
|
if (assigned) {
|
|
1063
1296
|
remoteIdleSessions.delete(key);
|
|
@@ -1065,7 +1298,34 @@ export async function triggerMeshQueue(components: DaemonComponents, meshId: str
|
|
|
1065
1298
|
}
|
|
1066
1299
|
}
|
|
1067
1300
|
|
|
1068
|
-
await maybeAutoLaunchOneQueueSession(components, meshId, mesh);
|
|
1301
|
+
autoLaunchStarted = await maybeAutoLaunchOneQueueSession(components, meshId, mesh);
|
|
1302
|
+
const afterQueue = getQueue(meshId);
|
|
1303
|
+
const pendingAfter = afterQueue.filter(task => task.status === 'pending').length;
|
|
1304
|
+
const assignedAfter = afterQueue.filter(task => task.status === 'assigned').length;
|
|
1305
|
+
const newlyAssignedTasks = afterQueue
|
|
1306
|
+
.filter(task => task.status === 'assigned' && beforeStatus.get(task.id) !== 'assigned')
|
|
1307
|
+
.map(task => ({
|
|
1308
|
+
id: task.id,
|
|
1309
|
+
nodeId: task.assignedNodeId,
|
|
1310
|
+
sessionId: task.assignedSessionId,
|
|
1311
|
+
}));
|
|
1312
|
+
return {
|
|
1313
|
+
success: true,
|
|
1314
|
+
meshId,
|
|
1315
|
+
pendingBefore,
|
|
1316
|
+
assignedBefore,
|
|
1317
|
+
pendingAfter,
|
|
1318
|
+
assignedAfter,
|
|
1319
|
+
claimed: newlyAssignedTasks.length > 0,
|
|
1320
|
+
newlyAssignedTasks,
|
|
1321
|
+
localIdleSessionsChecked,
|
|
1322
|
+
remoteIdleSessionsChecked,
|
|
1323
|
+
skippedSessions,
|
|
1324
|
+
autoLaunchStarted,
|
|
1325
|
+
...(pendingAfter > 0 && newlyAssignedTasks.length === 0 && localIdleSessionsChecked === 0 && remoteIdleSessionsChecked === 0 && !autoLaunchStarted
|
|
1326
|
+
? { noIdleMeshSessionAvailable: true }
|
|
1327
|
+
: {}),
|
|
1328
|
+
};
|
|
1069
1329
|
}
|
|
1070
1330
|
|
|
1071
1331
|
async function maybeAutoFastForwardIdleNode(components: DaemonComponents, args: {
|
|
@@ -372,6 +372,10 @@ export function __clearMeshQueueForTests(meshId: string): void {
|
|
|
372
372
|
BeadsDB.getInstance().deleteQueue(meshId);
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
export function __clearDirectDispatchesForTests(meshId: string): void {
|
|
376
|
+
BeadsDB.getInstance().deleteDirectDispatches(meshId);
|
|
377
|
+
}
|
|
378
|
+
|
|
375
379
|
export function __resetBeadsDBForTests(): void {
|
|
376
380
|
BeadsDB.resetForTests();
|
|
377
381
|
}
|
|
@@ -64,6 +64,7 @@ function getMessageTime(message: unknown): number {
|
|
|
64
64
|
type CompletedFinalizationBlock = {
|
|
65
65
|
reason: string;
|
|
66
66
|
terminal?: boolean;
|
|
67
|
+
allowTimeout?: boolean;
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
type CompletionFinalAssistantEvidence = {
|
|
@@ -72,6 +73,12 @@ type CompletionFinalAssistantEvidence = {
|
|
|
72
73
|
source: 'parsed' | 'external-native' | 'unavailable';
|
|
73
74
|
};
|
|
74
75
|
|
|
76
|
+
type ExternalNativeFinalReconciliation = {
|
|
77
|
+
fingerprint: string;
|
|
78
|
+
finalSummary: string;
|
|
79
|
+
evidence: CompletionFinalAssistantEvidence;
|
|
80
|
+
};
|
|
81
|
+
|
|
75
82
|
type ExternalTranscriptProbe = {
|
|
76
83
|
readAt: number;
|
|
77
84
|
msgCount: number;
|
|
@@ -368,6 +375,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
368
375
|
private historyWriter: ChatHistoryWriter;
|
|
369
376
|
private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
|
|
370
377
|
private lastPersistedHistoryMessages: PersistableCliHistoryMessage[] = [];
|
|
378
|
+
private lastAcknowledgedUserInputAt = 0;
|
|
379
|
+
private externalBusyIdleFingerprint = '';
|
|
371
380
|
private lastNativeSourceCanonicalCheckAt = 0;
|
|
372
381
|
private lastNativeSourceCanonicalCacheKey: string | undefined = undefined;
|
|
373
382
|
private cachedSqliteDb: {
|
|
@@ -586,9 +595,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
586
595
|
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
|
587
596
|
);
|
|
588
597
|
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, Date.now());
|
|
589
|
-
|
|
598
|
+
let visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
|
|
590
599
|
? 'error'
|
|
591
600
|
: (autoApproveActive ? 'generating' : adapterStatus.status);
|
|
601
|
+
const externalNativeFinal = this.getExternalNativeFinalReconciliation(parsedStatus?.messages, adapterStatus);
|
|
602
|
+
if (externalNativeFinal && isCliGeneratingLikeStatus(visibleStatus)) {
|
|
603
|
+
visibleStatus = 'idle';
|
|
604
|
+
}
|
|
592
605
|
const runtime = this.adapter.getRuntimeMetadata();
|
|
593
606
|
this.maybeAppendRuntimeRecoveryMessage(runtime);
|
|
594
607
|
let parsedMessages = Array.isArray(parsedStatus?.messages)
|
|
@@ -613,7 +626,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
613
626
|
if (suppressFreshLaunchStartupReplay) {
|
|
614
627
|
parsedMessages = [];
|
|
615
628
|
}
|
|
616
|
-
|
|
629
|
+
// Adapter runtime metadata is transport-owned and is not guaranteed to
|
|
630
|
+
// identify this conversation. Spec adapters historically exposed the
|
|
631
|
+
// provider spec id (for example "codex-cli") as runtimeId, which made
|
|
632
|
+
// concurrent sessions share one activeChat identity until their native
|
|
633
|
+
// provider session ids were discovered.
|
|
634
|
+
const activeChatId = this.providerSessionId || this.instanceId;
|
|
617
635
|
const historyMessageCount = Number.isFinite(parsedStatus?.historyMessageCount)
|
|
618
636
|
? Math.max(0, Number(parsedStatus.historyMessageCount))
|
|
619
637
|
: null;
|
|
@@ -782,7 +800,22 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
782
800
|
}
|
|
783
801
|
|
|
784
802
|
updateSettings(newSettings: Record<string, any>): void {
|
|
785
|
-
|
|
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 };
|
|
786
819
|
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
787
820
|
this.monitor.updateConfig({
|
|
788
821
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
@@ -878,6 +911,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
878
911
|
if (!content) return;
|
|
879
912
|
|
|
880
913
|
const receivedAt = Date.now();
|
|
914
|
+
this.lastAcknowledgedUserInputAt = receivedAt;
|
|
915
|
+
this.externalBusyIdleFingerprint = '';
|
|
881
916
|
const dedupKey = `user_input_ack:${crypto
|
|
882
917
|
.createHash('sha256')
|
|
883
918
|
.update(`${this.instanceId}:${content}:${receivedAt}`)
|
|
@@ -1043,6 +1078,59 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1043
1078
|
return extractFinalSummaryFromMessages(evidence.messages as any);
|
|
1044
1079
|
}
|
|
1045
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
|
+
|
|
1046
1134
|
private buildCompletedFinalizationDiagnostic(args: {
|
|
1047
1135
|
blockReason: string;
|
|
1048
1136
|
latestStatus?: any;
|
|
@@ -1132,12 +1220,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1132
1220
|
return true;
|
|
1133
1221
|
}
|
|
1134
1222
|
|
|
1135
|
-
private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending): CompletedFinalizationBlock | null {
|
|
1223
|
+
private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending, opts?: { externalNativeFinal?: ExternalNativeFinalReconciliation | null }): CompletedFinalizationBlock | null {
|
|
1136
1224
|
if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
|
|
1137
1225
|
|
|
1138
1226
|
const adapterAny = this.adapter as any;
|
|
1139
1227
|
const approvalResolvedIdle = pending.previousStatus === 'waiting_approval';
|
|
1140
|
-
|
|
1228
|
+
const externalNativeFinal = opts?.externalNativeFinal || null;
|
|
1229
|
+
if (!approvalResolvedIdle && !externalNativeFinal) {
|
|
1141
1230
|
if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
|
|
1142
1231
|
if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
|
|
1143
1232
|
if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
|
|
@@ -1146,7 +1235,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1146
1235
|
const partial = typeof this.adapter.getPartialResponse === 'function'
|
|
1147
1236
|
? this.adapter.getPartialResponse()
|
|
1148
1237
|
: '';
|
|
1149
|
-
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 };
|
|
1150
1239
|
|
|
1151
1240
|
let parsed: any;
|
|
1152
1241
|
try {
|
|
@@ -1158,12 +1247,14 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1158
1247
|
const parsedStatus = typeof parsed?.status === 'string' ? parsed.status : 'unknown';
|
|
1159
1248
|
if (parsedStatus !== 'idle') {
|
|
1160
1249
|
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1250
|
+
if (externalNativeFinal && isCliGeneratingLikeStatus(parsedStatus)) return null;
|
|
1161
1251
|
if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
|
|
1162
1252
|
return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
|
|
1163
1253
|
}
|
|
1164
1254
|
if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
|
|
1165
1255
|
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1166
1256
|
const finalAssistantEvidence = this.completionFinalAssistantEvidence(parsed?.messages);
|
|
1257
|
+
const allowMissingAssistantTimeout = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
|
|
1167
1258
|
if (!finalAssistantEvidence.present) {
|
|
1168
1259
|
if (adapterOwnsMessagesElsewhere) {
|
|
1169
1260
|
if (finalAssistantEvidence.source === 'external-native') {
|
|
@@ -1172,19 +1263,20 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1172
1263
|
LOG.info('CLI', `[${this.type}] external transcript probe: msgCount=${probe.msgCount} lastRole=${probe.lastRole || 'none'} lastKind=${probe.lastKind || 'none'} contentLen=${probe.contentLen} sourceMtime=${probe.sourceMtimeMs ?? 'unknown'} mtimeAge=${probe.mtimeAgeMs ?? 'unknown'}ms`);
|
|
1173
1264
|
pending.loggedTranscriptProbe = true;
|
|
1174
1265
|
}
|
|
1175
|
-
return { reason: 'missing_final_assistant', terminal: true };
|
|
1266
|
+
return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
|
|
1176
1267
|
}
|
|
1177
1268
|
// SpecCliAdapter never populates parsed.messages — chat history flows
|
|
1178
1269
|
// through the daemon's native-history pipeline, not the status hook.
|
|
1179
1270
|
// If that pipeline is unavailable, keep the old skip behavior for
|
|
1180
1271
|
// providers that have not opted into strict final-assistant evidence.
|
|
1181
1272
|
if ((this.provider as any).requiresFinalAssistantBeforeIdle === true) {
|
|
1182
|
-
return { reason: 'missing_final_assistant', terminal: true };
|
|
1273
|
+
return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
|
|
1183
1274
|
}
|
|
1184
1275
|
} else {
|
|
1185
1276
|
return {
|
|
1186
1277
|
reason: 'missing_final_assistant',
|
|
1187
1278
|
terminal: (this.provider as any).requiresFinalAssistantBeforeIdle === true,
|
|
1279
|
+
allowTimeout: allowMissingAssistantTimeout,
|
|
1188
1280
|
};
|
|
1189
1281
|
}
|
|
1190
1282
|
}
|
|
@@ -1222,7 +1314,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1222
1314
|
|
|
1223
1315
|
const latestStatus = this.adapter.getStatus({ allowParse: false });
|
|
1224
1316
|
const latestAutoApproveActive = latestStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
1225
|
-
const
|
|
1317
|
+
const externalNativeFinal = this.getExternalNativeFinalReconciliation(undefined, latestStatus);
|
|
1318
|
+
const latestVisibleStatus = externalNativeFinal && isCliGeneratingLikeStatus(latestStatus.status)
|
|
1319
|
+
? 'idle'
|
|
1320
|
+
: (latestAutoApproveActive ? 'generating' : latestStatus.status);
|
|
1226
1321
|
if (latestVisibleStatus !== 'idle') {
|
|
1227
1322
|
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
|
|
1228
1323
|
this.completedDebouncePending = null;
|
|
@@ -1230,11 +1325,11 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1230
1325
|
return;
|
|
1231
1326
|
}
|
|
1232
1327
|
|
|
1233
|
-
const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending);
|
|
1328
|
+
const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending, { externalNativeFinal });
|
|
1234
1329
|
if (block) {
|
|
1235
1330
|
const blockReason = block.reason;
|
|
1236
1331
|
const waitedMs = Date.now() - pending.firstObservedAt;
|
|
1237
|
-
if (block.terminal || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
|
|
1332
|
+
if ((block.terminal && !block.allowTimeout) || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
|
|
1238
1333
|
if (pending.loggedBlockReason !== blockReason) {
|
|
1239
1334
|
LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
|
|
1240
1335
|
pending.loggedBlockReason = blockReason;
|
|
@@ -1274,7 +1369,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1274
1369
|
chatTitle: pending.chatTitle,
|
|
1275
1370
|
duration: pending.duration,
|
|
1276
1371
|
timestamp: pending.timestamp,
|
|
1277
|
-
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
|
+
} : {}),
|
|
1278
1384
|
});
|
|
1279
1385
|
this.completedDebouncePending = null;
|
|
1280
1386
|
this.completedDebounceTimer = null;
|
|
@@ -1351,7 +1457,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1351
1457
|
const parsedStatus = null;
|
|
1352
1458
|
const rawStatus = adapterStatus.status;
|
|
1353
1459
|
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
|
|
1354
|
-
const
|
|
1460
|
+
const externalNativeFinal = this.getExternalNativeFinalReconciliation(undefined, adapterStatus);
|
|
1461
|
+
const newStatus = externalNativeFinal && isCliGeneratingLikeStatus(rawStatus)
|
|
1462
|
+
? 'idle'
|
|
1463
|
+
: (autoApproveActive ? 'generating' : rawStatus);
|
|
1355
1464
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
1356
1465
|
const chatTitle = `${this.provider.name} · ${dirName}`;
|
|
1357
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';
|