@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.
Files changed (55) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +1 -0
  2. package/dist/commands/cli-manager.d.ts +2 -1
  3. package/dist/commands/mesh-coordinator.d.ts +13 -0
  4. package/dist/commands/router.d.ts +5 -1
  5. package/dist/config/chat-history.d.ts +1 -0
  6. package/dist/git/git-commands.d.ts +2 -0
  7. package/dist/git/git-types.d.ts +2 -0
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +15461 -14552
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +14411 -13502
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/mesh/beads-db.d.ts +1 -0
  14. package/dist/mesh/mesh-events.d.ts +46 -1
  15. package/dist/mesh/mesh-work-queue.d.ts +1 -0
  16. package/dist/providers/cli-provider-instance.d.ts +4 -0
  17. package/dist/providers/contracts.d.ts +32 -1
  18. package/dist/providers/native-history/dispatcher.d.ts +2 -0
  19. package/dist/providers/sdk/v1/types/common/index.d.ts +35 -1
  20. package/dist/providers/spec/cli-adapter.d.ts +1 -0
  21. package/dist/providers/spec/driver.d.ts +6 -1
  22. package/dist/providers/spec/native-history-executor.d.ts +2 -0
  23. package/dist/providers/spec/schema.gen.d.ts +22 -0
  24. package/dist/providers/spec/types.d.ts +10 -0
  25. package/dist/repo-mesh-types.d.ts +6 -0
  26. package/package.json +1 -1
  27. package/src/boot/daemon-lifecycle.ts +2 -0
  28. package/src/commands/chat-commands.ts +206 -18
  29. package/src/commands/cli-manager.ts +56 -14
  30. package/src/commands/mesh-coordinator.ts +110 -5
  31. package/src/commands/router.ts +146 -21
  32. package/src/config/chat-history.ts +4 -0
  33. package/src/git/git-commands.ts +20 -2
  34. package/src/git/git-status.ts +35 -6
  35. package/src/git/git-types.ts +2 -0
  36. package/src/index.ts +2 -2
  37. package/src/mesh/beads-db.ts +4 -0
  38. package/src/mesh/mesh-events.ts +264 -4
  39. package/src/mesh/mesh-work-queue.ts +4 -0
  40. package/src/providers/cli-provider-instance.ts +122 -13
  41. package/src/providers/contracts.d.ts +55 -0
  42. package/src/providers/contracts.ts +36 -1
  43. package/src/providers/native-history/dispatcher.ts +126 -17
  44. package/src/providers/provider-loader.ts +4 -7
  45. package/src/providers/provider-schema.ts +56 -1
  46. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +46 -0
  47. package/src/providers/sdk/v1/types/common/index.ts +19 -0
  48. package/src/providers/spec/cli-adapter.ts +32 -5
  49. package/src/providers/spec/driver.ts +68 -1
  50. package/src/providers/spec/evaluator.ts +11 -1
  51. package/src/providers/spec/native-history-executor.ts +93 -27
  52. package/src/providers/spec/schema.gen.ts +12 -1
  53. package/src/providers/spec/schema.json +21 -1
  54. package/src/providers/spec/types.ts +10 -0
  55. package/src/repo-mesh-types.ts +6 -0
@@ -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<void> {
1219
+ export async function triggerMeshQueue(components: DaemonComponents, meshId: string): Promise<MeshQueueTriggerResult> {
1027
1220
  const mesh = getMeshWithCache(components, meshId);
1028
- if (!mesh) return;
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)) continue;
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
- const visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
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
- const activeChatId = this.providerSessionId || runtime?.runtimeId || this.instanceId;
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
- this.settings = { ...newSettings };
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
- if (!approvalResolvedIdle) {
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 latestVisibleStatus = latestAutoApproveActive ? 'generating' : latestStatus.status;
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 newStatus = autoApproveActive ? 'generating' : rawStatus;
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';