@adhdev/daemon-core 1.0.28-rc.7 → 1.0.28-rc.8
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/index.d.ts +1 -1
- package/dist/index.js +186 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +185 -41
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-ledger.d.ts +5 -1
- package/dist/mesh/mesh-queue-assignment.d.ts +19 -1
- package/dist/mesh/mesh-runtime-store.d.ts +4 -0
- package/package.json +3 -3
- package/src/index.ts +1 -1
- package/src/mesh/mesh-ledger.ts +72 -11
- package/src/mesh/mesh-queue-assignment.ts +176 -17
- package/src/mesh/mesh-runtime-store.ts +33 -8
- package/src/mesh/mesh-work-queue.ts +4 -0
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { EventEmitter } from 'events';
|
|
16
16
|
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
17
17
|
import { type MeshLedgerOriginatingCoordinatorV2 } from './contracts.js';
|
|
18
|
-
export type MeshLedgerKind = 'task_dispatched' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'task_question_pending' | 'p2p_dispatch_failed' | 'session_launched' | 'session_auto_launch' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_joined' | 'node_removed' | 'coordinator_started' | 'recovery_attempted' | 'ledger_replicated' | 'ledger_reconciled' | 'direct_fast_forward' | 'delivery_unroutable' | 'direct_dispatch_pruned' | 'event_held' | 'event_held_requeued' | 'task_reclaimed' | 'coordinator_operating_note' | 'coordinator_operating_note_tombstone' | 'mission_created' | 'mission_status_changed' | 'mission_goal_updated' | 'magi_dispatched' | 'magi_synthesis' | 'key_injection' | 'worktree_cleanup_candidate';
|
|
18
|
+
export type MeshLedgerKind = 'task_dispatched' | 'task_claimed' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'task_question_pending' | 'p2p_dispatch_failed' | 'session_launched' | 'session_auto_launch' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_joined' | 'node_removed' | 'coordinator_started' | 'recovery_attempted' | 'ledger_replicated' | 'ledger_reconciled' | 'direct_fast_forward' | 'delivery_unroutable' | 'direct_dispatch_pruned' | 'event_held' | 'event_held_requeued' | 'task_reclaimed' | 'coordinator_operating_note' | 'coordinator_operating_note_tombstone' | 'mission_created' | 'mission_status_changed' | 'mission_goal_updated' | 'magi_dispatched' | 'magi_synthesis' | 'key_injection' | 'worktree_cleanup_candidate';
|
|
19
19
|
export interface MeshLedgerEntry {
|
|
20
20
|
id: string;
|
|
21
21
|
meshId: string;
|
|
@@ -24,8 +24,12 @@ export interface MeshLedgerEntry {
|
|
|
24
24
|
nodeId?: string;
|
|
25
25
|
sessionId?: string;
|
|
26
26
|
providerType?: string;
|
|
27
|
+
taskId?: string;
|
|
27
28
|
payload: Record<string, unknown>;
|
|
28
29
|
}
|
|
30
|
+
/** Resolve the taskId for a ledger entry, preferring the base field and falling
|
|
31
|
+
* back to payload.taskId (legacy rows / entries that only carry it in payload). */
|
|
32
|
+
export declare function ledgerEntryTaskId(entry: Pick<MeshLedgerEntry, 'taskId' | 'payload'>): string | undefined;
|
|
29
33
|
export declare function isIntentionalCleanupStopEntry(entry: Pick<MeshLedgerEntry, 'kind' | 'payload'>): boolean;
|
|
30
34
|
export type MeshWorkerResultStatus = 'completed' | 'failed' | 'blocked' | 'partial' | 'unknown';
|
|
31
35
|
export type MeshProcessArtifactKind = 'process' | 'log' | 'port' | 'window' | 'session' | 'file' | 'url' | 'other';
|
|
@@ -5,7 +5,25 @@ import type { RepoMeshSchedulingStrategy } from '../repo-mesh-types.js';
|
|
|
5
5
|
export declare function isWorkspaceAutoFastForwardInFlight(workspace: string | undefined): boolean;
|
|
6
6
|
export declare function __resetIdleAutoFastForwardForTests(): void;
|
|
7
7
|
export declare function getMeshWithCache(components: DaemonComponents, meshId: string): any | undefined;
|
|
8
|
-
export
|
|
8
|
+
export interface MeshTaskRoutingDecision {
|
|
9
|
+
source: 'queue' | 'autoLaunch' | 'direct';
|
|
10
|
+
fitnessScore?: number;
|
|
11
|
+
skippedCandidates?: Array<{
|
|
12
|
+
nodeId: string;
|
|
13
|
+
reason: string;
|
|
14
|
+
}>;
|
|
15
|
+
requiredTagsResult?: {
|
|
16
|
+
required: string[];
|
|
17
|
+
satisfied: boolean;
|
|
18
|
+
missing: string[];
|
|
19
|
+
};
|
|
20
|
+
resolvedProviderType?: string;
|
|
21
|
+
resolvedModel?: string;
|
|
22
|
+
resolvedThinkingLevel?: string;
|
|
23
|
+
resolvedDifficulty?: string;
|
|
24
|
+
reason?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function tryAssignQueueTask(components: DaemonComponents, meshId: string, nodeId: string, sessionId: string, providerType: string, routingDecision?: MeshTaskRoutingDecision): boolean;
|
|
9
27
|
export declare const AUTO_LAUNCH_AWAIT_CLAIM_MS = 90000;
|
|
10
28
|
interface AwaitClaimBackoffState {
|
|
11
29
|
cycles: number;
|
|
@@ -355,6 +355,7 @@ export declare class MeshRuntimeStore {
|
|
|
355
355
|
nodeId?: string | null;
|
|
356
356
|
sessionId?: string | null;
|
|
357
357
|
providerType?: string | null;
|
|
358
|
+
taskId?: string | null;
|
|
358
359
|
payload?: unknown;
|
|
359
360
|
}): void;
|
|
360
361
|
readLedgerEntries(meshId: string, opts?: {
|
|
@@ -370,6 +371,7 @@ export declare class MeshRuntimeStore {
|
|
|
370
371
|
nodeId: string | null;
|
|
371
372
|
sessionId: string | null;
|
|
372
373
|
providerType: string | null;
|
|
374
|
+
taskId: string | null;
|
|
373
375
|
payload: unknown;
|
|
374
376
|
}>;
|
|
375
377
|
/**
|
|
@@ -390,6 +392,7 @@ export declare class MeshRuntimeStore {
|
|
|
390
392
|
nodeId: string | null;
|
|
391
393
|
sessionId: string | null;
|
|
392
394
|
providerType: string | null;
|
|
395
|
+
taskId: string | null;
|
|
393
396
|
payload: unknown;
|
|
394
397
|
}>;
|
|
395
398
|
/** Remove all ledger entries for a mesh (mesh deletion / test cleanup). */
|
|
@@ -406,6 +409,7 @@ export declare class MeshRuntimeStore {
|
|
|
406
409
|
nodeId?: string | null;
|
|
407
410
|
sessionId?: string | null;
|
|
408
411
|
providerType?: string | null;
|
|
412
|
+
taskId?: string | null;
|
|
409
413
|
payload?: unknown;
|
|
410
414
|
}>): number;
|
|
411
415
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "1.0.28-rc.
|
|
3
|
+
"version": "1.0.28-rc.8",
|
|
4
4
|
"description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"author": "vilmire",
|
|
50
50
|
"license": "AGPL-3.0-or-later",
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@adhdev/mesh-shared": "1.0.28-rc.
|
|
53
|
-
"@adhdev/session-host-core": "1.0.28-rc.
|
|
52
|
+
"@adhdev/mesh-shared": "1.0.28-rc.8",
|
|
53
|
+
"@adhdev/session-host-core": "1.0.28-rc.8",
|
|
54
54
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
55
55
|
"ajv": "^8.20.0",
|
|
56
56
|
"ajv-formats": "^3.0.1",
|
package/src/index.ts
CHANGED
|
@@ -288,7 +288,7 @@ export { loadRepoSettings } from './config/repo-settings.js';
|
|
|
288
288
|
export type { RepoSettings, LoadRepoSettingsOptions } from './config/repo-settings.js';
|
|
289
289
|
|
|
290
290
|
// ── Mesh Task Ledger ──
|
|
291
|
-
export { appendLedgerEntry, appendRemoteLedgerEntries, buildTaskCompletionEvidence, normalizeMeshWorkerResult, readLedgerEntries, readLedgerSlice, readLedgerSliceFromStore, getLedgerSummary, getLedgerDir, getSessionRecoveryContext, MAX_LEDGER_SLICE_LIMIT, tombstoneOperatingNote, readOperatingNotes, pruneOperatingNotes, isOperatingNoteTombstoned, OPERATING_NOTE_KIND, OPERATING_NOTE_TOMBSTONE_KIND, OPERATING_NOTE_DEDUPE_WINDOW, OPERATING_NOTE_KEEP_LATEST } from './mesh/mesh-ledger.js';
|
|
291
|
+
export { appendLedgerEntry, appendRemoteLedgerEntries, buildTaskCompletionEvidence, normalizeMeshWorkerResult, readLedgerEntries, readLedgerSlice, readLedgerSliceFromStore, getLedgerSummary, getLedgerDir, getSessionRecoveryContext, ledgerEntryTaskId, MAX_LEDGER_SLICE_LIMIT, tombstoneOperatingNote, readOperatingNotes, pruneOperatingNotes, isOperatingNoteTombstoned, OPERATING_NOTE_KIND, OPERATING_NOTE_TOMBSTONE_KIND, OPERATING_NOTE_DEDUPE_WINDOW, OPERATING_NOTE_KEEP_LATEST } from './mesh/mesh-ledger.js';
|
|
292
292
|
export type { AppendRemoteLedgerResult, MeshLedgerEntry, MeshLedgerKind, MeshLedgerSlice, MeshLedgerSummary, ReadLedgerOptions, ReadLedgerSliceOptions, SessionRecoveryContext, MeshTaskCompletionEvidence, MeshWorkerResultArtifact, MeshProcessArtifact, MeshValidationResultArtifact } from './mesh/mesh-ledger.js';
|
|
293
293
|
export { fastForwardMeshNode } from './mesh/mesh-fast-forward.js';
|
|
294
294
|
export type { MeshFastForwardNodeArgs, MeshFastForwardPlannedStep, MeshFastForwardResult } from './mesh/mesh-fast-forward.js';
|
package/src/mesh/mesh-ledger.ts
CHANGED
|
@@ -29,6 +29,11 @@ import {
|
|
|
29
29
|
|
|
30
30
|
export type MeshLedgerKind =
|
|
31
31
|
| 'task_dispatched'
|
|
32
|
+
// LEDGER-TASK-TRACEABILITY (C): a queue task transitioned pending→assigned (a
|
|
33
|
+
// node/session claimed it). Distinct from task_dispatched (the message was handed
|
|
34
|
+
// to the transport): claim precedes dispatch and marks the lifecycle handoff.
|
|
35
|
+
// payload: { taskId, nodeId, sessionId, providerType?, claimedAt }
|
|
36
|
+
| 'task_claimed'
|
|
32
37
|
| 'task_completed'
|
|
33
38
|
| 'task_failed'
|
|
34
39
|
| 'task_stalled'
|
|
@@ -111,9 +116,40 @@ export interface MeshLedgerEntry {
|
|
|
111
116
|
nodeId?: string;
|
|
112
117
|
sessionId?: string;
|
|
113
118
|
providerType?: string;
|
|
119
|
+
// LEDGER-TASK-TRACEABILITY (B): the task this entry pertains to, promoted from
|
|
120
|
+
// payload.taskId to a top-level base field so a task's lifecycle
|
|
121
|
+
// (task_dispatched → task_claimed → task_completed/failed/stalled/reclaimed) can
|
|
122
|
+
// be joined by kind+taskId without an O(n) per-entry payload scan. Optional and
|
|
123
|
+
// back-compat: legacy rows never carried it — readers fall back to payload.taskId,
|
|
124
|
+
// and appendLedgerEntry auto-derives it from payload.taskId for task-lifecycle
|
|
125
|
+
// kinds so every such entry is uniformly queryable.
|
|
126
|
+
taskId?: string;
|
|
114
127
|
payload: Record<string, unknown>;
|
|
115
128
|
}
|
|
116
129
|
|
|
130
|
+
// LEDGER-TASK-TRACEABILITY (B): the kinds whose taskId base field is auto-derived
|
|
131
|
+
// from payload.taskId at append time (so a join by kind+taskId is uniform). Other
|
|
132
|
+
// kinds may still set taskId explicitly; this only backfills the common lifecycle.
|
|
133
|
+
const TASK_LIFECYCLE_LEDGER_KINDS: ReadonlySet<MeshLedgerKind> = new Set<MeshLedgerKind>([
|
|
134
|
+
'task_dispatched',
|
|
135
|
+
'task_claimed',
|
|
136
|
+
'task_completed',
|
|
137
|
+
'task_failed',
|
|
138
|
+
'task_stalled',
|
|
139
|
+
'task_reclaimed',
|
|
140
|
+
'task_approval_needed',
|
|
141
|
+
'task_question_pending',
|
|
142
|
+
'p2p_dispatch_failed',
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
/** Resolve the taskId for a ledger entry, preferring the base field and falling
|
|
146
|
+
* back to payload.taskId (legacy rows / entries that only carry it in payload). */
|
|
147
|
+
export function ledgerEntryTaskId(entry: Pick<MeshLedgerEntry, 'taskId' | 'payload'>): string | undefined {
|
|
148
|
+
if (typeof entry.taskId === 'string' && entry.taskId.trim()) return entry.taskId.trim();
|
|
149
|
+
const fromPayload = entry.payload && typeof entry.payload === 'object' ? (entry.payload as Record<string, unknown>).taskId : undefined;
|
|
150
|
+
return typeof fromPayload === 'string' && fromPayload.trim() ? fromPayload.trim() : undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
117
153
|
export function isIntentionalCleanupStopEntry(entry: Pick<MeshLedgerEntry, 'kind' | 'payload'>): boolean {
|
|
118
154
|
if (entry.kind !== 'session_stopped' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') return false;
|
|
119
155
|
const payload = entry.payload && typeof entry.payload === 'object' && !Array.isArray(entry.payload)
|
|
@@ -796,6 +832,14 @@ export function appendLedgerEntry(
|
|
|
796
832
|
...partial,
|
|
797
833
|
};
|
|
798
834
|
|
|
835
|
+
// LEDGER-TASK-TRACEABILITY (B): backfill the taskId base field from payload.taskId
|
|
836
|
+
// for task-lifecycle kinds so every dispatch/claim/complete/fail/stall/reclaim
|
|
837
|
+
// entry is uniformly join-able by kind+taskId. An explicit partial.taskId wins.
|
|
838
|
+
if (!entry.taskId && TASK_LIFECYCLE_LEDGER_KINDS.has(entry.kind)) {
|
|
839
|
+
const derived = ledgerEntryTaskId(entry);
|
|
840
|
+
if (derived) entry.taskId = derived;
|
|
841
|
+
}
|
|
842
|
+
|
|
799
843
|
const filePath = getLedgerPath(meshId);
|
|
800
844
|
|
|
801
845
|
// Compact or rotate based on file size
|
|
@@ -822,6 +866,7 @@ export function appendLedgerEntry(
|
|
|
822
866
|
nodeId: entry.nodeId ?? null,
|
|
823
867
|
sessionId: entry.sessionId ?? null,
|
|
824
868
|
providerType: entry.providerType ?? null,
|
|
869
|
+
taskId: entry.taskId ?? null,
|
|
825
870
|
payload: entry.payload,
|
|
826
871
|
});
|
|
827
872
|
} catch {
|
|
@@ -1074,7 +1119,15 @@ function readLedgerFile(meshId: string): MeshLedgerEntry[] {
|
|
|
1074
1119
|
if (!line.trim()) continue;
|
|
1075
1120
|
try {
|
|
1076
1121
|
const entry = JSON.parse(line) as MeshLedgerEntry;
|
|
1077
|
-
if (entry.id && entry.kind)
|
|
1122
|
+
if (entry.id && entry.kind) {
|
|
1123
|
+
// LEDGER-TASK-TRACEABILITY (B): backfill the base taskId from
|
|
1124
|
+
// payload.taskId for legacy JSONL lines that predate the field.
|
|
1125
|
+
if (!entry.taskId) {
|
|
1126
|
+
const derived = ledgerEntryTaskId(entry);
|
|
1127
|
+
if (derived) entry.taskId = derived;
|
|
1128
|
+
}
|
|
1129
|
+
entries.push(entry);
|
|
1130
|
+
}
|
|
1078
1131
|
} catch { /* skip malformed lines */ }
|
|
1079
1132
|
}
|
|
1080
1133
|
return entries;
|
|
@@ -1108,6 +1161,7 @@ function ensureLedgerImported(store: MeshRuntimeStore, meshId: string): void {
|
|
|
1108
1161
|
nodeId: e.nodeId ?? null,
|
|
1109
1162
|
sessionId: e.sessionId ?? null,
|
|
1110
1163
|
providerType: e.providerType ?? null,
|
|
1164
|
+
taskId: ledgerEntryTaskId(e) ?? null,
|
|
1111
1165
|
payload: e.payload ?? {},
|
|
1112
1166
|
})));
|
|
1113
1167
|
} catch { /* import is best-effort; reads fall back to JSONL on store failure */ }
|
|
@@ -1116,16 +1170,23 @@ function ensureLedgerImported(store: MeshRuntimeStore, meshId: string): void {
|
|
|
1116
1170
|
function readLedgerFromStore(meshId: string): MeshLedgerEntry[] {
|
|
1117
1171
|
const store = MeshRuntimeStore.getInstance();
|
|
1118
1172
|
ensureLedgerImported(store, meshId);
|
|
1119
|
-
return store.readLedgerEntriesOrdered(meshId).map(r =>
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1173
|
+
return store.readLedgerEntriesOrdered(meshId).map(r => {
|
|
1174
|
+
const payload = (r.payload && typeof r.payload === 'object' ? r.payload : {}) as Record<string, unknown>;
|
|
1175
|
+
// LEDGER-TASK-TRACEABILITY (B): prefer the column, fall back to payload.taskId
|
|
1176
|
+
// for legacy rows written before the column existed (back-compat join).
|
|
1177
|
+
const taskId = ledgerEntryTaskId({ taskId: r.taskId ?? undefined, payload });
|
|
1178
|
+
return {
|
|
1179
|
+
id: r.id,
|
|
1180
|
+
meshId: r.meshId,
|
|
1181
|
+
timestamp: r.timestamp,
|
|
1182
|
+
kind: r.kind as MeshLedgerKind,
|
|
1183
|
+
...(r.nodeId ? { nodeId: r.nodeId } : {}),
|
|
1184
|
+
...(r.sessionId ? { sessionId: r.sessionId } : {}),
|
|
1185
|
+
...(r.providerType ? { providerType: r.providerType } : {}),
|
|
1186
|
+
...(taskId ? { taskId } : {}),
|
|
1187
|
+
payload,
|
|
1188
|
+
};
|
|
1189
|
+
});
|
|
1129
1190
|
}
|
|
1130
1191
|
|
|
1131
1192
|
function getCachedRawEntries(meshId: string): MeshLedgerEntry[] {
|
|
@@ -293,6 +293,30 @@ function warnDispatchWarmupGetterMissingOnce(daemonId: string): void {
|
|
|
293
293
|
LOG.warn('MeshQueue', `Mesh peer connection getter unavailable for ${String(daemonId).slice(0, 12)}; remote task-dispatch warmup deadline degraded to the combined connect+response window. Avoids a cold-open false-timeout but loses warm/cold precision — wire getMeshPeerConnectionStatus on this daemon.`);
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
// LEDGER-TASK-TRACEABILITY (A/D): the routing rationale captured at claim/dispatch
|
|
297
|
+
// time so mesh_task_history / the dashboard can answer "why THIS node/provider/model".
|
|
298
|
+
// All fields optional — the event-driven / idle drains don't compute a fitness score,
|
|
299
|
+
// so they omit the extras and only `source` is always present.
|
|
300
|
+
export interface MeshTaskRoutingDecision {
|
|
301
|
+
// How the task reached this dispatch: a normal queue claim, the auto-launch drain
|
|
302
|
+
// that spawned a fresh worker, or a coordinator direct dispatch (mesh_send_task).
|
|
303
|
+
source: 'queue' | 'autoLaunch' | 'direct';
|
|
304
|
+
// The task→slot fitness score of the selected node (scoreSlotForTask), when the
|
|
305
|
+
// 'fitness' scheduling strategy ranked candidates. Absent for first_eligible etc.
|
|
306
|
+
fitnessScore?: number;
|
|
307
|
+
// Candidate nodes considered but skipped before this one won, with the reason.
|
|
308
|
+
skippedCandidates?: Array<{ nodeId: string; reason: string }>;
|
|
309
|
+
// Required-tag gating result for the selected node.
|
|
310
|
+
requiredTagsResult?: { required: string[]; satisfied: boolean; missing: string[] };
|
|
311
|
+
// The resolved execution profile (D): provider actually launched, and the
|
|
312
|
+
// model/thinking/difficulty axes that shaped it, plus the human-readable reason.
|
|
313
|
+
resolvedProviderType?: string;
|
|
314
|
+
resolvedModel?: string;
|
|
315
|
+
resolvedThinkingLevel?: string;
|
|
316
|
+
resolvedDifficulty?: string;
|
|
317
|
+
reason?: string;
|
|
318
|
+
}
|
|
319
|
+
|
|
296
320
|
interface DeliverTaskContext {
|
|
297
321
|
meshId: string;
|
|
298
322
|
nodeId: string;
|
|
@@ -302,6 +326,8 @@ interface DeliverTaskContext {
|
|
|
302
326
|
transport: 'remote' | 'local';
|
|
303
327
|
sourceCoordinatorSessionId?: string;
|
|
304
328
|
sourceCoordinatorDaemonId?: string;
|
|
329
|
+
// LEDGER-TASK-TRACEABILITY (A): routing rationale to record on task_dispatched.
|
|
330
|
+
routingDecision?: MeshTaskRoutingDecision;
|
|
305
331
|
}
|
|
306
332
|
|
|
307
333
|
// Readiness barrier for the LOCAL auto-launch path. A just-spawned CLI session is
|
|
@@ -346,6 +372,51 @@ async function waitForLocalSessionReady(components: DaemonComponents, sessionId:
|
|
|
346
372
|
// (response budget governs from t0), so a normal dispatch sees no added latency. The
|
|
347
373
|
// LOCAL transport (in-process cliManager) has no channel to warm up and keeps the
|
|
348
374
|
// flat Bug B hang guard.
|
|
375
|
+
/**
|
|
376
|
+
* LEDGER-TASK-TRACEABILITY (A/D): append a task_dispatched entry from an already-built
|
|
377
|
+
* dispatch context. Reads the execution profile off the claimed task (model/thinking/
|
|
378
|
+
* difficulty/coordinator session were stamped at enqueue/claim) and folds in the caller's
|
|
379
|
+
* routing rationale. Hot-path-safe: no detection, no scoring — everything is precomputed.
|
|
380
|
+
* taskId is promoted to the base field (B) so the row joins the lifecycle by kind+taskId.
|
|
381
|
+
*/
|
|
382
|
+
function recordTaskDispatchedLedger(ctx: DeliverTaskContext, deliveryId: string): void {
|
|
383
|
+
const task = ctx.task;
|
|
384
|
+
const routing = ctx.routingDecision;
|
|
385
|
+
const routingDecision: Record<string, unknown> = {
|
|
386
|
+
source: routing?.source ?? 'queue',
|
|
387
|
+
selectedNodeId: ctx.nodeId,
|
|
388
|
+
...(localCoordinatorDaemonId() ? { daemonId: localCoordinatorDaemonId() } : {}),
|
|
389
|
+
transport: ctx.transport,
|
|
390
|
+
// D: resolved execution profile — prefer the caller's resolved values, fall back
|
|
391
|
+
// to what the claimed task row carries (queue/idle drains carry it on the task).
|
|
392
|
+
resolvedProviderType: routing?.resolvedProviderType ?? ctx.providerType,
|
|
393
|
+
...(routing?.resolvedModel ?? task.model ? { resolvedModel: routing?.resolvedModel ?? task.model } : {}),
|
|
394
|
+
...(routing?.resolvedThinkingLevel ?? task.thinkingLevel ? { resolvedThinkingLevel: routing?.resolvedThinkingLevel ?? task.thinkingLevel } : {}),
|
|
395
|
+
...(routing?.resolvedDifficulty ?? task.difficulty ? { resolvedDifficulty: routing?.resolvedDifficulty ?? task.difficulty } : {}),
|
|
396
|
+
...(typeof routing?.fitnessScore === 'number' ? { fitnessScore: routing.fitnessScore } : {}),
|
|
397
|
+
...(routing?.skippedCandidates?.length ? { skippedCandidates: routing.skippedCandidates } : {}),
|
|
398
|
+
...(routing?.requiredTagsResult ? { requiredTagsResult: routing.requiredTagsResult } : {}),
|
|
399
|
+
...(routing?.reason ? { reason: routing.reason } : {}),
|
|
400
|
+
};
|
|
401
|
+
appendLedgerEntry(ctx.meshId, {
|
|
402
|
+
kind: 'task_dispatched',
|
|
403
|
+
nodeId: ctx.nodeId,
|
|
404
|
+
sessionId: ctx.sessionId,
|
|
405
|
+
providerType: ctx.providerType,
|
|
406
|
+
taskId: task.id,
|
|
407
|
+
payload: {
|
|
408
|
+
taskId: task.id,
|
|
409
|
+
...(task.missionId ? { missionId: task.missionId } : {}),
|
|
410
|
+
deliveryId,
|
|
411
|
+
transport: ctx.transport,
|
|
412
|
+
...(ctx.sourceCoordinatorSessionId ? { coordinatorSessionId: ctx.sourceCoordinatorSessionId } : {}),
|
|
413
|
+
...(ctx.sourceCoordinatorDaemonId ? { coordinatorDaemonId: ctx.sourceCoordinatorDaemonId } : {}),
|
|
414
|
+
...(Array.isArray(task.requiredTags) && task.requiredTags.length ? { requiredTags: task.requiredTags } : {}),
|
|
415
|
+
routingDecision,
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
349
420
|
function deliverTaskToSession(
|
|
350
421
|
dispatchThunk: () => Promise<unknown>,
|
|
351
422
|
ctx: DeliverTaskContext,
|
|
@@ -364,6 +435,15 @@ function deliverTaskToSession(
|
|
|
364
435
|
...(ctx.sourceCoordinatorDaemonId ? { sourceCoordinatorDaemonId: ctx.sourceCoordinatorDaemonId } : {}),
|
|
365
436
|
});
|
|
366
437
|
|
|
438
|
+
// LEDGER-TASK-TRACEABILITY (A): record the dispatch — the single funnel every
|
|
439
|
+
// queue-claim dispatch (local + remote) flows through — so mesh_task_history and the
|
|
440
|
+
// dashboard can show "which device/daemon/provider/model, via what path, and why".
|
|
441
|
+
// All routing values are ALREADY computed by the caller (no re-serialization on the
|
|
442
|
+
// hot path); the delivery id links this to the delivered/failed transitions below.
|
|
443
|
+
try {
|
|
444
|
+
recordTaskDispatchedLedger(ctx, delivery.id);
|
|
445
|
+
} catch { /* ledger write is best-effort — dispatch proceeds regardless */ }
|
|
446
|
+
|
|
367
447
|
// Invoke the transport synchronously (preserves the prior fire-and-forget timing,
|
|
368
448
|
// and lets a synchronous throw fall into the same failure path as a rejection).
|
|
369
449
|
let dispatchPromise: Promise<unknown>;
|
|
@@ -466,7 +546,12 @@ export function tryAssignQueueTask(
|
|
|
466
546
|
meshId: string,
|
|
467
547
|
nodeId: string,
|
|
468
548
|
sessionId: string,
|
|
469
|
-
providerType: string
|
|
549
|
+
providerType: string,
|
|
550
|
+
// LEDGER-TASK-TRACEABILITY (A): routing rationale computed by the auto-launch drain
|
|
551
|
+
// (fitness score, skipped candidates, resolved model/thinking). Threaded to the
|
|
552
|
+
// task_dispatched ledger entry. Other claim paths (event/idle drain) omit it and the
|
|
553
|
+
// entry records source:'queue' with just the resolved provider from the claimed row.
|
|
554
|
+
routingDecision?: MeshTaskRoutingDecision,
|
|
470
555
|
): boolean {
|
|
471
556
|
const mesh = getMeshWithCache(components, meshId);
|
|
472
557
|
// Match with the shared 3-form normalizer (id / nodeId / node_id), not raw
|
|
@@ -652,6 +737,29 @@ export function tryAssignQueueTask(
|
|
|
652
737
|
|
|
653
738
|
LOG.info('MeshQueue', `Node ${nodeId} (${sessionId}) pulled task ${task.id}`);
|
|
654
739
|
|
|
740
|
+
// LEDGER-TASK-TRACEABILITY (C): the task just transitioned pending→assigned. Record
|
|
741
|
+
// the claim (distinct from the later task_dispatched, which fires when the message is
|
|
742
|
+
// handed to the transport in deliverTaskToSession). This is the single funnel every
|
|
743
|
+
// claim path (event/idle drain, auto-launch, remote reclaim) flows through, so one
|
|
744
|
+
// append here covers them all. Best-effort — a ledger write must never fail a claim.
|
|
745
|
+
try {
|
|
746
|
+
appendLedgerEntry(meshId, {
|
|
747
|
+
kind: 'task_claimed',
|
|
748
|
+
nodeId,
|
|
749
|
+
sessionId,
|
|
750
|
+
providerType,
|
|
751
|
+
taskId: task.id,
|
|
752
|
+
payload: {
|
|
753
|
+
taskId: task.id,
|
|
754
|
+
...(task.missionId ? { missionId: task.missionId } : {}),
|
|
755
|
+
nodeId,
|
|
756
|
+
sessionId,
|
|
757
|
+
providerType,
|
|
758
|
+
claimedAt: new Date().toISOString(),
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
} catch { /* best-effort — claim proceeds regardless */ }
|
|
762
|
+
|
|
655
763
|
// FALSE-BLOCKER-CLONE-QUEUE (stale-event clear): the task just claimed and will dispatch,
|
|
656
764
|
// so any actionable blocker previously paged for it (e.g. a 'target_node_id_unmatched'
|
|
657
765
|
// emitted during the clone/bootstrap propagation window before the node became
|
|
@@ -726,6 +834,7 @@ export function tryAssignQueueTask(
|
|
|
726
834
|
transport: 'remote',
|
|
727
835
|
...(sourceCoordinatorSessionId ? { sourceCoordinatorSessionId } : {}),
|
|
728
836
|
...(localDaemonIdForDispatch ? { sourceCoordinatorDaemonId: localDaemonIdForDispatch } : {}),
|
|
837
|
+
...(routingDecision ? { routingDecision } : {}),
|
|
729
838
|
},
|
|
730
839
|
// Warmup-aware deadline: this dispatch can be the FIRST command to a
|
|
731
840
|
// peer whose mesh DataChannel is still opening — charge the cold-open
|
|
@@ -815,6 +924,7 @@ export function tryAssignQueueTask(
|
|
|
815
924
|
transport: 'local',
|
|
816
925
|
...(readNonEmptyString(task.sourceCoordinatorSessionId) ? { sourceCoordinatorSessionId: readNonEmptyString(task.sourceCoordinatorSessionId) } : {}),
|
|
817
926
|
...(localCoordinatorDaemonId() ? { sourceCoordinatorDaemonId: localCoordinatorDaemonId() } : {}),
|
|
927
|
+
...(routingDecision ? { routingDecision } : {}),
|
|
818
928
|
},
|
|
819
929
|
);
|
|
820
930
|
|
|
@@ -1832,6 +1942,11 @@ function recordAutoLaunchEvent(meshId: string, args: {
|
|
|
1832
1942
|
sessionId?: string;
|
|
1833
1943
|
reason?: string;
|
|
1834
1944
|
error?: string;
|
|
1945
|
+
// LEDGER-TASK-TRACEABILITY (D): the resolved execution profile the auto-launch
|
|
1946
|
+
// resolved for this worker, so session_auto_launch records what model/thinking the
|
|
1947
|
+
// spawned worker actually launched with (not just the provider).
|
|
1948
|
+
model?: string;
|
|
1949
|
+
thinkingLevel?: string;
|
|
1835
1950
|
}) {
|
|
1836
1951
|
// Suppress consecutive identical `skipped` entries for the same task (4s reconcile
|
|
1837
1952
|
// re-trigger noise). Non-skip phases and changed reasons always record and reset
|
|
@@ -1853,11 +1968,16 @@ function recordAutoLaunchEvent(meshId: string, args: {
|
|
|
1853
1968
|
nodeId: args.nodeId,
|
|
1854
1969
|
sessionId: args.sessionId,
|
|
1855
1970
|
providerType: args.providerType,
|
|
1971
|
+
// (B) promote taskId so this entry joins the task lifecycle timeline.
|
|
1972
|
+
...(args.taskId ? { taskId: args.taskId } : {}),
|
|
1856
1973
|
payload: {
|
|
1857
1974
|
phase: args.phase,
|
|
1858
1975
|
taskId: args.taskId,
|
|
1859
1976
|
reason: args.reason,
|
|
1860
1977
|
error: args.error,
|
|
1978
|
+
// (D) resolved execution profile for the spawned worker.
|
|
1979
|
+
...(args.model ? { resolvedModel: args.model } : {}),
|
|
1980
|
+
...(args.thinkingLevel ? { resolvedThinkingLevel: args.thinkingLevel } : {}),
|
|
1861
1981
|
},
|
|
1862
1982
|
});
|
|
1863
1983
|
} catch (e: any) {
|
|
@@ -1872,6 +1992,9 @@ function markAutoLaunch(meshId: string, taskId: string, args: {
|
|
|
1872
1992
|
providerType?: string;
|
|
1873
1993
|
sessionId?: string;
|
|
1874
1994
|
error?: string;
|
|
1995
|
+
// LEDGER-TASK-TRACEABILITY (D): resolved execution profile for started/completed.
|
|
1996
|
+
model?: string;
|
|
1997
|
+
thinkingLevel?: string;
|
|
1875
1998
|
}) {
|
|
1876
1999
|
recordTaskAutoLaunch(meshId, taskId, {
|
|
1877
2000
|
status: args.status,
|
|
@@ -1888,6 +2011,8 @@ function markAutoLaunch(meshId: string, taskId: string, args: {
|
|
|
1888
2011
|
sessionId: args.sessionId,
|
|
1889
2012
|
reason: args.reason,
|
|
1890
2013
|
error: args.error,
|
|
2014
|
+
...(args.model ? { model: args.model } : {}),
|
|
2015
|
+
...(args.thinkingLevel ? { thinkingLevel: args.thinkingLevel } : {}),
|
|
1891
2016
|
});
|
|
1892
2017
|
// Fix (1): actively notify the coordinator of a non-self-resolving skip; re-arm the
|
|
1893
2018
|
// notification on any non-skip transition (started/completed) so a later genuine skip
|
|
@@ -2278,6 +2403,20 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
2278
2403
|
{ bumpCursor: true, task: { difficulty: (task as any).difficulty, requiredTags: task.requiredTags } },
|
|
2279
2404
|
).map((c: RankableNode) => c.node);
|
|
2280
2405
|
|
|
2406
|
+
// LEDGER-TASK-TRACEABILITY (A): accumulate the candidate nodes that were
|
|
2407
|
+
// considered but skipped before the winning node, so task_dispatched can record
|
|
2408
|
+
// WHY the other nodes lost (cooldown, dirty, cap, provider mismatch, …). Bounded
|
|
2409
|
+
// so a large fleet can't bloat the entry. markSkip mirrors markAutoLaunch's skip
|
|
2410
|
+
// side effect AND appends to this list in one call.
|
|
2411
|
+
const skippedCandidates: Array<{ nodeId: string; reason: string }> = [];
|
|
2412
|
+
const SKIPPED_CANDIDATES_MAX = 12;
|
|
2413
|
+
const markSkip = (nodeIdForSkip: string, reason: string, extra?: { providerType?: string }) => {
|
|
2414
|
+
markAutoLaunch(meshId, task.id, { status: 'skipped', reason, nodeId: nodeIdForSkip, ...(extra || {}) });
|
|
2415
|
+
if (nodeIdForSkip && skippedCandidates.length < SKIPPED_CANDIDATES_MAX) {
|
|
2416
|
+
skippedCandidates.push({ nodeId: nodeIdForSkip, reason });
|
|
2417
|
+
}
|
|
2418
|
+
};
|
|
2419
|
+
|
|
2281
2420
|
for (const node of orderedCandidateNodes) {
|
|
2282
2421
|
const nodeId = readMeshNodeId(node);
|
|
2283
2422
|
if (!nodeId) continue;
|
|
@@ -2286,19 +2425,19 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
2286
2425
|
const cooldownUntil = autoLaunchCooldownUntil.get(launchKey) || 0;
|
|
2287
2426
|
if (cooldownUntil > 0 && now >= cooldownUntil) autoLaunchCooldownUntil.delete(launchKey);
|
|
2288
2427
|
if (autoLaunchInProgress.has(launchKey)) {
|
|
2289
|
-
|
|
2428
|
+
markSkip(nodeId, 'auto_launch_in_progress');
|
|
2290
2429
|
continue;
|
|
2291
2430
|
}
|
|
2292
2431
|
if (now < cooldownUntil) {
|
|
2293
|
-
|
|
2432
|
+
markSkip(nodeId, 'auto_launch_cooldown');
|
|
2294
2433
|
continue;
|
|
2295
2434
|
}
|
|
2296
2435
|
if (isDirtyNode(node)) {
|
|
2297
|
-
|
|
2436
|
+
markSkip(nodeId, 'dirty_workspace');
|
|
2298
2437
|
continue;
|
|
2299
2438
|
}
|
|
2300
2439
|
if (!isLaunchableNode(node)) {
|
|
2301
|
-
|
|
2440
|
+
markSkip(nodeId, 'node_not_launch_ready');
|
|
2302
2441
|
continue;
|
|
2303
2442
|
}
|
|
2304
2443
|
// FRESHNESS gate (distinct from the health gate above): a clean-tree node that
|
|
@@ -2310,7 +2449,7 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
2310
2449
|
// place. Telemetry-absent nodes pass (never block on missing data). The 4s
|
|
2311
2450
|
// reconcile retries once the node's auto-ff repair path catches it up.
|
|
2312
2451
|
if (!isMeshNodeFreshEnoughToLaunch(node, freshnessGate)) {
|
|
2313
|
-
|
|
2452
|
+
markSkip(nodeId, 'node_stale_behind_upstream');
|
|
2314
2453
|
continue;
|
|
2315
2454
|
}
|
|
2316
2455
|
const launchTarget = resolveAutoLaunchTarget(components, node);
|
|
@@ -2318,7 +2457,7 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
2318
2457
|
// Remote node we can't reach (no transport / no coordinator daemonId).
|
|
2319
2458
|
// Set a cooldown so the 4s reconcile loop doesn't re-attempt this node
|
|
2320
2459
|
// every tick; the de-dup'd skip ledger keeps it diagnosable without flood.
|
|
2321
|
-
|
|
2460
|
+
markSkip(nodeId, launchTarget.reason || 'auto_launch_unavailable');
|
|
2322
2461
|
autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS); sweepExpiredCooldowns();
|
|
2323
2462
|
continue;
|
|
2324
2463
|
}
|
|
@@ -2333,19 +2472,19 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
2333
2472
|
// reason so the coordinator is not paged; the 4s reconcile retries, and once the
|
|
2334
2473
|
// existing session goes terminal this gate clears and a legitimate launch proceeds.
|
|
2335
2474
|
if (nodeHasLiveSessionPendingClaim(components, meshId, nodeId, task, node)) {
|
|
2336
|
-
|
|
2475
|
+
markSkip(nodeId, 'node_has_live_session_pending_claim');
|
|
2337
2476
|
continue;
|
|
2338
2477
|
}
|
|
2339
2478
|
// Write tasks keep the one-active-per-node invariant (worktree isolation);
|
|
2340
2479
|
// read-only diagnoses may auto-launch onto a node that already has an active
|
|
2341
2480
|
// assignment. Classified by the shared isTaskReadonly predicate.
|
|
2342
2481
|
if (!isTaskReadonly(task) && nodeHasActiveAssignment(meshId, nodeId)) {
|
|
2343
|
-
|
|
2482
|
+
markSkip(nodeId, 'node_has_active_assignment');
|
|
2344
2483
|
continue;
|
|
2345
2484
|
}
|
|
2346
2485
|
const maxConcurrentSessions = Number(node?.policy?.maxConcurrentSessions);
|
|
2347
2486
|
if (Number.isFinite(maxConcurrentSessions) && maxConcurrentSessions >= 0 && liveSessionCountForNode(components, meshId, nodeId) >= maxConcurrentSessions) {
|
|
2348
|
-
|
|
2487
|
+
markSkip(nodeId, 'max_concurrent_sessions_reached');
|
|
2349
2488
|
continue;
|
|
2350
2489
|
}
|
|
2351
2490
|
|
|
@@ -2353,7 +2492,7 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
2353
2492
|
try {
|
|
2354
2493
|
const resolved = await resolveUsableProvider(components, nodeId, node, task.requiredTags, { difficulty: (task as any).difficulty, requiredTags: task.requiredTags });
|
|
2355
2494
|
if (!resolved.providerType) {
|
|
2356
|
-
|
|
2495
|
+
markSkip(nodeId, resolved.reason || 'provider_unusable');
|
|
2357
2496
|
continue;
|
|
2358
2497
|
}
|
|
2359
2498
|
// Slot-derived model/thinking: an explicit task.model/thinkingLevel
|
|
@@ -2388,7 +2527,7 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
2388
2527
|
providerCap !== undefined
|
|
2389
2528
|
&& activeProviderAssignedCount(meshId, nodeId, resolved.providerType) >= providerCap
|
|
2390
2529
|
) {
|
|
2391
|
-
|
|
2530
|
+
markSkip(nodeId, 'max_provider_parallel_reached', { providerType: resolved.providerType });
|
|
2392
2531
|
continue;
|
|
2393
2532
|
}
|
|
2394
2533
|
|
|
@@ -2424,7 +2563,7 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
2424
2563
|
meshCoordinatorDaemonId: launchTarget.coordinatorDaemonId,
|
|
2425
2564
|
meshCoordinatorNodeId: nodeId,
|
|
2426
2565
|
};
|
|
2427
|
-
markAutoLaunch(meshId, task.id, { status: 'started', nodeId, providerType: resolved.providerType });
|
|
2566
|
+
markAutoLaunch(meshId, task.id, { status: 'started', nodeId, providerType: resolved.providerType, ...(effectiveModel ? { model: effectiveModel } : {}), ...(effectiveThinkingLevel ? { thinkingLevel: effectiveThinkingLevel } : {}) });
|
|
2428
2567
|
let launchResult: any;
|
|
2429
2568
|
try {
|
|
2430
2569
|
// OFFLINE-NODE-BLOCKING: no peer-connected pre-check before this remote
|
|
@@ -2464,12 +2603,12 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
2464
2603
|
// which (forwarded back here) drives the claim via the normal event path / PHASE 1
|
|
2465
2604
|
// reconcile. Set a cooldown so the 4s loop doesn't re-launch before that lands.
|
|
2466
2605
|
const remoteSessionId = readNonEmptyString(payload.sessionId) || readNonEmptyString(payload.id) || readNonEmptyString(payload.runtimeSessionId);
|
|
2467
|
-
markAutoLaunch(meshId, task.id, { status: 'completed', nodeId, providerType: resolved.providerType, sessionId: remoteSessionId || undefined });
|
|
2606
|
+
markAutoLaunch(meshId, task.id, { status: 'completed', nodeId, providerType: resolved.providerType, sessionId: remoteSessionId || undefined, ...(effectiveModel ? { model: effectiveModel } : {}), ...(effectiveThinkingLevel ? { thinkingLevel: effectiveThinkingLevel } : {}) });
|
|
2468
2607
|
autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS); sweepExpiredCooldowns();
|
|
2469
2608
|
return true;
|
|
2470
2609
|
}
|
|
2471
2610
|
|
|
2472
|
-
markAutoLaunch(meshId, task.id, { status: 'started', nodeId, providerType: resolved.providerType });
|
|
2611
|
+
markAutoLaunch(meshId, task.id, { status: 'started', nodeId, providerType: resolved.providerType, ...(effectiveModel ? { model: effectiveModel } : {}), ...(effectiveThinkingLevel ? { thinkingLevel: effectiveThinkingLevel } : {}) });
|
|
2473
2612
|
const launchResult: any = await components.cliManager.handleCliCommand('launch_cli', {
|
|
2474
2613
|
cliType: resolved.providerType,
|
|
2475
2614
|
dir: node.workspace,
|
|
@@ -2492,7 +2631,7 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
2492
2631
|
autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS); sweepExpiredCooldowns();
|
|
2493
2632
|
return false;
|
|
2494
2633
|
}
|
|
2495
|
-
markAutoLaunch(meshId, task.id, { status: 'completed', nodeId, providerType: resolved.providerType, sessionId });
|
|
2634
|
+
markAutoLaunch(meshId, task.id, { status: 'completed', nodeId, providerType: resolved.providerType, sessionId, ...(effectiveModel ? { model: effectiveModel } : {}), ...(effectiveThinkingLevel ? { thinkingLevel: effectiveThinkingLevel } : {}) });
|
|
2496
2635
|
// Readiness barrier: a freshly-spawned local CLI session is NOT yet
|
|
2497
2636
|
// interactive — its PTY prints the input prompt (and the adapter flips
|
|
2498
2637
|
// isReady()) only ~2-6s after launch. Dispatching the task immediately
|
|
@@ -2503,7 +2642,27 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
2503
2642
|
// first message lands cleanly. The adapter's queue-until-ready path is the
|
|
2504
2643
|
// backstop if readiness is reported late; this just avoids the churn.
|
|
2505
2644
|
await waitForLocalSessionReady(components, sessionId);
|
|
2506
|
-
|
|
2645
|
+
// LEDGER-TASK-TRACEABILITY (A/D): the auto-launch drain has all the routing
|
|
2646
|
+
// rationale in scope (resolved provider/model/thinking, skipped candidates,
|
|
2647
|
+
// fitness score). Fold it into the task_dispatched entry the claim writes.
|
|
2648
|
+
// All values are already computed above — no extra work on the dispatch path.
|
|
2649
|
+
const requiredTags = Array.isArray(task.requiredTags) ? task.requiredTags.filter((t): t is string => !!t) : [];
|
|
2650
|
+
const routingDecision: MeshTaskRoutingDecision = {
|
|
2651
|
+
source: 'autoLaunch',
|
|
2652
|
+
fitnessScore: nodeFitnessForTask(node, { difficulty: (task as any).difficulty, requiredTags: task.requiredTags }),
|
|
2653
|
+
...(skippedCandidates.length ? { skippedCandidates } : {}),
|
|
2654
|
+
requiredTagsResult: {
|
|
2655
|
+
required: requiredTags,
|
|
2656
|
+
satisfied: !requiredTags.length || nodeSatisfiesRequiredTags(requiredTags, buildMeshNodeCapabilityTags(node, resolved.providerType)),
|
|
2657
|
+
missing: requiredTags.filter(t => !buildMeshNodeCapabilityTags(node, resolved.providerType).includes(t)),
|
|
2658
|
+
},
|
|
2659
|
+
resolvedProviderType: resolved.providerType,
|
|
2660
|
+
...(effectiveModel ? { resolvedModel: effectiveModel } : {}),
|
|
2661
|
+
...(effectiveThinkingLevel ? { resolvedThinkingLevel: effectiveThinkingLevel } : {}),
|
|
2662
|
+
...((task as any).difficulty ? { resolvedDifficulty: String((task as any).difficulty) } : {}),
|
|
2663
|
+
...(resolved.reason ? { reason: resolved.reason } : {}),
|
|
2664
|
+
};
|
|
2665
|
+
tryAssignQueueTask(components, meshId, nodeId, sessionId, resolved.providerType, routingDecision);
|
|
2507
2666
|
return true;
|
|
2508
2667
|
} catch (e: any) {
|
|
2509
2668
|
markAutoLaunch(meshId, task.id, { status: 'failed', error: e?.message || String(e), nodeId });
|