@adhdev/daemon-core 0.9.82-rc.185 → 0.9.82-rc.187
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/commands/mesh-coordinator.d.ts +13 -0
- package/dist/config/chat-history.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +21088 -20535
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6601 -6047
- 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/contracts.d.ts +1 -1
- package/dist/providers/native-history/dispatcher.d.ts +2 -0
- package/dist/providers/spec/cli-adapter.d.ts +1 -0
- package/dist/providers/spec/native-history-executor.d.ts +2 -0
- package/package.json +1 -1
- package/src/chat/subscription-updates.ts +6 -0
- package/src/commands/chat-commands.ts +182 -18
- package/src/commands/cli-manager.ts +4 -0
- package/src/commands/mesh-coordinator.ts +110 -5
- package/src/commands/router.ts +111 -17
- package/src/config/chat-history.ts +4 -0
- package/src/index.ts +1 -1
- 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 +12 -4
- package/src/providers/contracts.ts +1 -1
- package/src/providers/native-history/dispatcher.ts +126 -17
- package/src/providers/provider-loader.ts +4 -7
- package/src/providers/spec/cli-adapter.ts +32 -5
- package/src/providers/spec/evaluator.ts +11 -1
- package/src/providers/spec/native-history-executor.ts +93 -27
- package/src/providers/types/interactive-prompt.ts +13 -1
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 = {
|
|
@@ -613,7 +614,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
613
614
|
if (suppressFreshLaunchStartupReplay) {
|
|
614
615
|
parsedMessages = [];
|
|
615
616
|
}
|
|
616
|
-
|
|
617
|
+
// Adapter runtime metadata is transport-owned and is not guaranteed to
|
|
618
|
+
// identify this conversation. Spec adapters historically exposed the
|
|
619
|
+
// provider spec id (for example "codex-cli") as runtimeId, which made
|
|
620
|
+
// concurrent sessions share one activeChat identity until their native
|
|
621
|
+
// provider session ids were discovered.
|
|
622
|
+
const activeChatId = this.providerSessionId || this.instanceId;
|
|
617
623
|
const historyMessageCount = Number.isFinite(parsedStatus?.historyMessageCount)
|
|
618
624
|
? Math.max(0, Number(parsedStatus.historyMessageCount))
|
|
619
625
|
: null;
|
|
@@ -1164,6 +1170,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1164
1170
|
if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
|
|
1165
1171
|
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1166
1172
|
const finalAssistantEvidence = this.completionFinalAssistantEvidence(parsed?.messages);
|
|
1173
|
+
const allowMissingAssistantTimeout = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
|
|
1167
1174
|
if (!finalAssistantEvidence.present) {
|
|
1168
1175
|
if (adapterOwnsMessagesElsewhere) {
|
|
1169
1176
|
if (finalAssistantEvidence.source === 'external-native') {
|
|
@@ -1172,19 +1179,20 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1172
1179
|
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
1180
|
pending.loggedTranscriptProbe = true;
|
|
1174
1181
|
}
|
|
1175
|
-
return { reason: 'missing_final_assistant', terminal: true };
|
|
1182
|
+
return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
|
|
1176
1183
|
}
|
|
1177
1184
|
// SpecCliAdapter never populates parsed.messages — chat history flows
|
|
1178
1185
|
// through the daemon's native-history pipeline, not the status hook.
|
|
1179
1186
|
// If that pipeline is unavailable, keep the old skip behavior for
|
|
1180
1187
|
// providers that have not opted into strict final-assistant evidence.
|
|
1181
1188
|
if ((this.provider as any).requiresFinalAssistantBeforeIdle === true) {
|
|
1182
|
-
return { reason: 'missing_final_assistant', terminal: true };
|
|
1189
|
+
return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
|
|
1183
1190
|
}
|
|
1184
1191
|
} else {
|
|
1185
1192
|
return {
|
|
1186
1193
|
reason: 'missing_final_assistant',
|
|
1187
1194
|
terminal: (this.provider as any).requiresFinalAssistantBeforeIdle === true,
|
|
1195
|
+
allowTimeout: allowMissingAssistantTimeout,
|
|
1188
1196
|
};
|
|
1189
1197
|
}
|
|
1190
1198
|
}
|
|
@@ -1234,7 +1242,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1234
1242
|
if (block) {
|
|
1235
1243
|
const blockReason = block.reason;
|
|
1236
1244
|
const waitedMs = Date.now() - pending.firstObservedAt;
|
|
1237
|
-
if (block.terminal || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
|
|
1245
|
+
if ((block.terminal && !block.allowTimeout) || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
|
|
1238
1246
|
if (pending.loggedBlockReason !== blockReason) {
|
|
1239
1247
|
LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
|
|
1240
1248
|
pending.loggedBlockReason = blockReason;
|
|
@@ -390,7 +390,7 @@ export interface ProviderMeshCoordinatorConfig {
|
|
|
390
390
|
requiresRestart?: boolean;
|
|
391
391
|
/** User-facing setup explanation for manual modes. */
|
|
392
392
|
instructions?: string;
|
|
393
|
-
/** Copyable setup template. Supports {{meshId}}, {{adhdevMcpCommand}}, {{workspace}}, {{serverName}}. */
|
|
393
|
+
/** Copyable setup template. Supports {{meshId}}, {{adhdevMcpCommand}}, {{adhdevMcpArgs}}, {{workspace}}, {{serverName}}. */
|
|
394
394
|
template?: string;
|
|
395
395
|
};
|
|
396
396
|
/**
|
|
@@ -27,6 +27,7 @@ export interface NativeHistoryInput {
|
|
|
27
27
|
providerSessionId?: string;
|
|
28
28
|
historySessionId?: string;
|
|
29
29
|
workspace?: string;
|
|
30
|
+
sessionStartedAtMs?: number;
|
|
30
31
|
format?: string;
|
|
31
32
|
watchPath?: string;
|
|
32
33
|
forceRefresh?: boolean;
|
|
@@ -34,7 +35,7 @@ export interface NativeHistoryInput {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export interface NativeHistoryResult {
|
|
37
|
-
messages: Array<{ role: string; content: string; receivedAt?: number; kind?: string }>;
|
|
38
|
+
messages: Array<{ role: string; content: string; receivedAt?: number; kind?: string; workspace?: string }>;
|
|
38
39
|
providerSessionId?: string;
|
|
39
40
|
sourcePath: string;
|
|
40
41
|
sourceMtimeMs: number;
|
|
@@ -53,7 +54,12 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
|
|
|
53
54
|
// shows up before I type anything" on every provider).
|
|
54
55
|
const requestedProviderSid = input.providerSessionId || '';
|
|
55
56
|
|
|
56
|
-
const
|
|
57
|
+
const sessionStartedAtMs = typeof input.sessionStartedAtMs === 'number'
|
|
58
|
+
? input.sessionStartedAtMs
|
|
59
|
+
: typeof input.args?.sessionStartedAtMs === 'number'
|
|
60
|
+
? input.args.sessionStartedAtMs
|
|
61
|
+
: 0;
|
|
62
|
+
const sourcePath = resolveSourcePath(reader, workspace, sessionId, sessionStartedAtMs);
|
|
57
63
|
if (!sourcePath) return null;
|
|
58
64
|
if (input.forceRefresh === true || input.args?.forceRefresh === true) {
|
|
59
65
|
try { fs.statSync(sourcePath); } catch { /* best-effort metadata refresh */ }
|
|
@@ -72,6 +78,7 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
|
|
|
72
78
|
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
|
73
79
|
receivedAt: typeof m.receivedAt === 'number' ? m.receivedAt : Date.parse(m.timestamp || '') || Date.now(),
|
|
74
80
|
kind: typeof m.kind === 'string' ? m.kind : 'standard',
|
|
81
|
+
workspace: typeof m.workspace === 'string' ? m.workspace : workspace || undefined,
|
|
75
82
|
})),
|
|
76
83
|
providerSessionId: session.providerSessionId,
|
|
77
84
|
sourcePath: session.sourcePath,
|
|
@@ -85,10 +92,10 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
|
|
|
85
92
|
// Per-provider path resolution
|
|
86
93
|
// ────────────────────────────────────────────────────────────────────────────
|
|
87
94
|
|
|
88
|
-
function resolveSourcePath(reader: ReaderId, workspace: string, sessionId: string): string | null {
|
|
95
|
+
function resolveSourcePath(reader: ReaderId, workspace: string, sessionId: string, sessionStartedAtMs: number): string | null {
|
|
89
96
|
switch (reader) {
|
|
90
97
|
case 'claude-cli': return resolveClaudePath(workspace, sessionId);
|
|
91
|
-
case 'codex-cli': return resolveCodexPath(workspace);
|
|
98
|
+
case 'codex-cli': return resolveCodexPath(workspace, sessionId, sessionStartedAtMs);
|
|
92
99
|
case 'antigravity-cli': return resolveAntigravityPath(workspace);
|
|
93
100
|
case 'hermes-cli': return resolveHermesPath(workspace, sessionId);
|
|
94
101
|
}
|
|
@@ -112,21 +119,111 @@ function resolveClaudePath(workspace: string, sessionId: string): string | null
|
|
|
112
119
|
return null;
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
function resolveCodexPath(workspace: string): string | null {
|
|
116
|
-
void workspace;
|
|
122
|
+
function resolveCodexPath(workspace: string, sessionId: string, sessionStartedAtMs: number): string | null {
|
|
117
123
|
// codex stores by UTC date: ~/.codex/sessions/<year>/<month>/<day>/<file>.jsonl
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
String(now.getUTCFullYear()),
|
|
122
|
-
String(now.getUTCMonth() + 1).padStart(2, '0'),
|
|
123
|
-
String(now.getUTCDate()).padStart(2, '0'),
|
|
124
|
-
);
|
|
125
|
-
if (fs.existsSync(dir)) {
|
|
126
|
-
const f = newestRecentFile(dir, /\.jsonl$/);
|
|
127
|
-
if (f) return f;
|
|
124
|
+
const root = codexSessionsRoot();
|
|
125
|
+
if (sessionId && isUuidLikeSessionId(sessionId)) {
|
|
126
|
+
return findCodexPathBySessionId(root, sessionId);
|
|
128
127
|
}
|
|
129
|
-
return
|
|
128
|
+
return findCodexPathByRuntime(root, workspace, sessionStartedAtMs);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function findCodexPathBySessionId(root: string, sessionId: string): string | null {
|
|
132
|
+
if (!fs.existsSync(root)) return null;
|
|
133
|
+
const needle = sessionId.toLowerCase();
|
|
134
|
+
const matches: Array<{ p: string; mtime: number }> = [];
|
|
135
|
+
const stack: string[] = [root];
|
|
136
|
+
while (stack.length > 0) {
|
|
137
|
+
const current = stack.pop()!;
|
|
138
|
+
let entries: fs.Dirent[] = [];
|
|
139
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; }
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const entryPath = path.join(current, entry.name);
|
|
142
|
+
if (entry.isDirectory()) {
|
|
143
|
+
stack.push(entryPath);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
|
|
147
|
+
if (!entry.name.toLowerCase().includes(needle)) continue;
|
|
148
|
+
if (!isSafeFilename(entry.name.replace('.jsonl', ''))) continue;
|
|
149
|
+
matches.push({ p: entryPath, mtime: safeMtime(entryPath) });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
matches.sort((a, b) => b.mtime - a.mtime);
|
|
153
|
+
return matches[0]?.p ?? null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const CODEX_SPAWN_BIND_GRACE_MS = 10_000;
|
|
157
|
+
|
|
158
|
+
function findCodexPathByRuntime(root: string, workspace: string, sessionStartedAtMs: number): string | null {
|
|
159
|
+
if (!fs.existsSync(root) || !workspace) return null;
|
|
160
|
+
const workspaceResolved = resolveRealPath(workspace);
|
|
161
|
+
const cutoff = Date.now() - RECENT_WINDOW_MS;
|
|
162
|
+
const matches: Array<{ p: string; mtime: number; diff: number }> = [];
|
|
163
|
+
const stack: string[] = [root];
|
|
164
|
+
|
|
165
|
+
while (stack.length > 0) {
|
|
166
|
+
const current = stack.pop()!;
|
|
167
|
+
let entries: fs.Dirent[] = [];
|
|
168
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; }
|
|
169
|
+
for (const entry of entries) {
|
|
170
|
+
const entryPath = path.join(current, entry.name);
|
|
171
|
+
if (entry.isDirectory()) {
|
|
172
|
+
stack.push(entryPath);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
|
|
176
|
+
const mtime = safeMtime(entryPath);
|
|
177
|
+
if (mtime < cutoff) continue;
|
|
178
|
+
const meta = readCodexSessionMeta(entryPath);
|
|
179
|
+
if (!meta?.cwd || resolveRealPath(meta.cwd) !== workspaceResolved) continue;
|
|
180
|
+
const diff = sessionStartedAtMs > 0 && meta.timestampMs != null
|
|
181
|
+
? Math.abs(meta.timestampMs - sessionStartedAtMs)
|
|
182
|
+
: 0;
|
|
183
|
+
if (sessionStartedAtMs > 0 && (meta.timestampMs == null || diff > CODEX_SPAWN_BIND_GRACE_MS)) continue;
|
|
184
|
+
matches.push({ p: entryPath, mtime, diff });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
matches.sort((a, b) => sessionStartedAtMs > 0
|
|
189
|
+
? a.diff - b.diff || b.mtime - a.mtime
|
|
190
|
+
: b.mtime - a.mtime);
|
|
191
|
+
return matches[0]?.p ?? null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function readCodexSessionMeta(filePath: string): { cwd?: string; timestampMs?: number } | null {
|
|
195
|
+
try {
|
|
196
|
+
const fd = fs.openSync(filePath, 'r');
|
|
197
|
+
try {
|
|
198
|
+
const buffer = Buffer.alloc(8192);
|
|
199
|
+
const bytes = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
200
|
+
if (bytes <= 0) return null;
|
|
201
|
+
const text = buffer.subarray(0, bytes).toString('utf8');
|
|
202
|
+
const firstLine = text.slice(0, text.indexOf('\n') >= 0 ? text.indexOf('\n') : text.length).trim();
|
|
203
|
+
if (!firstLine) return null;
|
|
204
|
+
const record = JSON.parse(firstLine) as Record<string, unknown>;
|
|
205
|
+
if (record.type !== 'session_meta' || !record.payload || typeof record.payload !== 'object') return null;
|
|
206
|
+
const payload = record.payload as Record<string, unknown>;
|
|
207
|
+
const timestampRaw = payload.timestamp;
|
|
208
|
+
const timestampMs = typeof timestampRaw === 'string'
|
|
209
|
+
? Date.parse(timestampRaw)
|
|
210
|
+
: typeof timestampRaw === 'number'
|
|
211
|
+
? (timestampRaw < 1e12 ? timestampRaw * 1000 : timestampRaw)
|
|
212
|
+
: NaN;
|
|
213
|
+
return {
|
|
214
|
+
cwd: typeof payload.cwd === 'string' ? payload.cwd : undefined,
|
|
215
|
+
timestampMs: Number.isFinite(timestampMs) ? timestampMs : undefined,
|
|
216
|
+
};
|
|
217
|
+
} finally {
|
|
218
|
+
fs.closeSync(fd);
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function resolveRealPath(value: string): string {
|
|
226
|
+
try { return fs.realpathSync(value); } catch { return value; }
|
|
130
227
|
}
|
|
131
228
|
|
|
132
229
|
function resolveAntigravityPath(workspace: string): string | null {
|
|
@@ -187,6 +284,18 @@ function cwdAsDashes(cwd: string): string {
|
|
|
187
284
|
return cwd.replace(/\//g, '-');
|
|
188
285
|
}
|
|
189
286
|
|
|
287
|
+
function codexSessionsRoot(): string {
|
|
288
|
+
return path.join(os.homedir(), '.codex', 'sessions');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function isUuidLikeSessionId(sessionId: string): boolean {
|
|
292
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isSafeFilename(name: string): boolean {
|
|
296
|
+
return /^[A-Za-z0-9._:-]+$/.test(name) && !name.includes('..');
|
|
297
|
+
}
|
|
298
|
+
|
|
190
299
|
function newestFile(dir: string, pattern: RegExp): string | null {
|
|
191
300
|
try {
|
|
192
301
|
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
@@ -36,6 +36,9 @@ import {
|
|
|
36
36
|
} from './external-sources.js';
|
|
37
37
|
import type { ProviderSourceMode } from '../config/config.js';
|
|
38
38
|
import type { ProviderSourceConfigSnapshot, ProviderUserDirSource } from '../config/provider-source-config.js';
|
|
39
|
+
import { loadSpec } from './spec/loader.js';
|
|
40
|
+
import { executeNativeHistory } from './spec/native-history-executor.js';
|
|
41
|
+
import { createNativeHistoryDispatcher, type ReaderId } from './native-history/dispatcher.js';
|
|
39
42
|
|
|
40
43
|
/**
|
|
41
44
|
* Adds a provider-script root to the require whitelist. Wrapped in a
|
|
@@ -1244,8 +1247,6 @@ export class ProviderLoader {
|
|
|
1244
1247
|
// Hand the resolved spec path off to route.ts via a hidden field
|
|
1245
1248
|
// so the routing layer doesn't have to repeat the candidate walk.
|
|
1246
1249
|
(resolved as any)._resolvedSpecPath = specPath;
|
|
1247
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1248
|
-
const { loadSpec } = require('./spec/loader.js');
|
|
1249
1250
|
const r = loadSpec(specPath);
|
|
1250
1251
|
// Stub each control_bar entry as a provider.scripts.<id>. The
|
|
1251
1252
|
// upstream invoke_provider_script gate checks that the script
|
|
@@ -1274,8 +1275,6 @@ export class ProviderLoader {
|
|
|
1274
1275
|
let format = 'spec';
|
|
1275
1276
|
|
|
1276
1277
|
if (nh.source) {
|
|
1277
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1278
|
-
const { executeNativeHistory } = require('./spec/native-history-executor.js');
|
|
1279
1278
|
format = `spec-${nh.source.kind}`;
|
|
1280
1279
|
reader = (input: any) => executeNativeHistory(nh, input);
|
|
1281
1280
|
} else if (nh.override_path) {
|
|
@@ -1294,9 +1293,7 @@ export class ProviderLoader {
|
|
|
1294
1293
|
} catch { /* fall through — leave native unavailable */ }
|
|
1295
1294
|
}
|
|
1296
1295
|
} else if (nh.reader) {
|
|
1297
|
-
|
|
1298
|
-
const { createNativeHistoryDispatcher } = require('./native-history/dispatcher.js');
|
|
1299
|
-
const dispatch = createNativeHistoryDispatcher(nh.reader);
|
|
1296
|
+
const dispatch = createNativeHistoryDispatcher(nh.reader as ReaderId);
|
|
1300
1297
|
format = nh.reader;
|
|
1301
1298
|
reader = (input: any) => dispatch(input);
|
|
1302
1299
|
}
|