@adhdev/daemon-core 0.9.82-rc.350 → 0.9.82-rc.351
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 -0
- package/dist/index.js +108 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +106 -31
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-runtime-store.d.ts +1 -0
- package/package.json +2 -2
- package/src/index.ts +5 -0
- package/src/mesh/mesh-active-work.ts +64 -20
- package/src/mesh/mesh-events-coordinator.ts +84 -5
- package/src/mesh/mesh-events-pending.ts +13 -4
- package/src/mesh/mesh-events-stale.ts +17 -0
- package/src/mesh/mesh-runtime-store.ts +5 -1
- package/src/mesh/mesh-task-stats.ts +8 -1
|
@@ -6,6 +6,7 @@ export declare class MeshRuntimeStore {
|
|
|
6
6
|
private readonly migratedMeshIds;
|
|
7
7
|
private fingerprintSweepCounter;
|
|
8
8
|
private walWriteCounter;
|
|
9
|
+
private toolCallLogCounter;
|
|
9
10
|
private static readonly WAL_CHECK_INTERVAL;
|
|
10
11
|
private static readonly WAL_MAX_BYTES;
|
|
11
12
|
private constructor();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.351",
|
|
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",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"author": "vilmire",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.351",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
package/src/index.ts
CHANGED
|
@@ -252,6 +252,11 @@ export { buildMeshHostRequiredFailure, createDefaultMeshHostMetadata, isMeshHost
|
|
|
252
252
|
// ── Mesh Events ──
|
|
253
253
|
export { triggerMeshQueue, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, clearPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent, reconcileDirectDispatchCompletionFromTranscript } from './mesh/mesh-events.js';
|
|
254
254
|
export type { PendingMeshCoordinatorEvent } from './mesh/mesh-events.js';
|
|
255
|
+
// The coordinator-side preview surfaced from a worker's completion/status event
|
|
256
|
+
// (finalSummary / workerResult.summary / lastMessagePreview). Same data the mobile
|
|
257
|
+
// inbox is fed; reused by mesh_read_chat's cache fallback when the live P2P read path
|
|
258
|
+
// is unavailable (saturated/unreachable peer).
|
|
259
|
+
export { resolveMeshSurfacedSessionPreview, readMeshCompletionSummary } from './mesh/mesh-events-utils.js';
|
|
255
260
|
|
|
256
261
|
// ── Mesh Delivery Policy ──
|
|
257
262
|
export { resolveDeliveryDecision, createSessionDelivery, updateSessionDeliveryStatus, getActiveSessionDeliveries, markSessionDeliveriesTerminal, recordCompletionConflict, getRecentCompletionConflicts } from './mesh/mesh-delivery-policy.js';
|
|
@@ -168,6 +168,46 @@ function statusFromTerminal(entry: MeshLedgerEntry): MeshActiveWorkStatus {
|
|
|
168
168
|
return 'failed';
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
const LEDGER_ONLY_STALE_REASON =
|
|
172
|
+
'direct task dispatch has no provider acknowledgement, transcript append, or active runtime transition';
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Single source of truth for classifying whether a direct dispatch is ledger-only
|
|
176
|
+
* stale (dispatched but never acknowledged: no provider ack, no transcript append,
|
|
177
|
+
* no runtime transition) and whether that staleness is "fresh unacknowledged"
|
|
178
|
+
* (target session still live) vs. an orphaned historical record.
|
|
179
|
+
*
|
|
180
|
+
* Previously this predicate was inlined in all three dispatch-classification blocks
|
|
181
|
+
* (MeshRuntimeStore path, remote-ledger path, full-ledger path) and had drifted —
|
|
182
|
+
* e.g. one block gated `isIdleUnacknowledged` on `!isTerminal` and another did not.
|
|
183
|
+
* Extracting it keeps the three callers byte-for-byte consistent. Status derivation
|
|
184
|
+
* stays per-block: it genuinely differs by data source and is passed in via `status`.
|
|
185
|
+
*/
|
|
186
|
+
function classifyDirectDispatch(params: {
|
|
187
|
+
/** Final classified status of the dispatch. */
|
|
188
|
+
status: MeshActiveWorkStatus;
|
|
189
|
+
/** True when a real terminal row applies (completed/failed/stale); excludes approval_needed. Gates the stale verdict. */
|
|
190
|
+
isTerminalRow: boolean;
|
|
191
|
+
/** True when any terminal-derived status exists (incl. approval_needed). Used to detect "no transition". */
|
|
192
|
+
hasTerminalStatus: boolean;
|
|
193
|
+
/** Live session status from the mesh nodes, if any. */
|
|
194
|
+
liveStatus?: string;
|
|
195
|
+
/** Live stale reason from the mesh nodes, if any. */
|
|
196
|
+
liveStaleReason?: string;
|
|
197
|
+
/** Whether the dispatch targeted an already-idle session. */
|
|
198
|
+
dispatchedToIdleSession: boolean;
|
|
199
|
+
}): { ledgerOnlyStaleReason: string | undefined; isFreshUnacknowledged: boolean } {
|
|
200
|
+
const { status, isTerminalRow, hasTerminalStatus, liveStatus, liveStaleReason, dispatchedToIdleSession } = params;
|
|
201
|
+
const isNoTransition = !hasTerminalStatus && !liveStatus;
|
|
202
|
+
const isIdleUnacknowledged = status === 'idle';
|
|
203
|
+
const ledgerOnlyStaleReason =
|
|
204
|
+
!isTerminalRow && (isIdleUnacknowledged || isNoTransition || (dispatchedToIdleSession && isIdleUnacknowledged))
|
|
205
|
+
? LEDGER_ONLY_STALE_REASON
|
|
206
|
+
: undefined;
|
|
207
|
+
const isFreshUnacknowledged = Boolean(ledgerOnlyStaleReason && !liveStaleReason);
|
|
208
|
+
return { ledgerOnlyStaleReason, isFreshUnacknowledged };
|
|
209
|
+
}
|
|
210
|
+
|
|
171
211
|
export function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRecord[]): MeshActiveWorkSummary {
|
|
172
212
|
const statusCounts: Record<MeshActiveWorkStatus, number> = {
|
|
173
213
|
pending: 0,
|
|
@@ -239,12 +279,14 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
239
279
|
const status: MeshActiveWorkStatus = isTerminal
|
|
240
280
|
? (dbStatus === 'completed' ? 'idle' : 'failed')
|
|
241
281
|
: live.status || (dbStatus === 'acked' ? 'generating' : 'assigned');
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
:
|
|
247
|
-
|
|
282
|
+
const { ledgerOnlyStaleReason, isFreshUnacknowledged } = classifyDirectDispatch({
|
|
283
|
+
status,
|
|
284
|
+
isTerminalRow: isTerminal,
|
|
285
|
+
hasTerminalStatus: isTerminal,
|
|
286
|
+
liveStatus: live.status,
|
|
287
|
+
liveStaleReason: live.staleReason,
|
|
288
|
+
dispatchedToIdleSession: dispatch.dispatchedToIdleSession === true,
|
|
289
|
+
});
|
|
248
290
|
const { title, summary } = summarizeMessage(dispatch.message || '');
|
|
249
291
|
const record: MeshActiveWorkRecord = {
|
|
250
292
|
taskId: dispatch.taskId,
|
|
@@ -291,15 +333,16 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
291
333
|
const live = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
|
|
292
334
|
const status = terminalStatus || live.status || 'assigned';
|
|
293
335
|
const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
:
|
|
336
|
+
const { ledgerOnlyStaleReason, isFreshUnacknowledged } = classifyDirectDispatch({
|
|
337
|
+
status,
|
|
338
|
+
isTerminalRow: terminalRow,
|
|
339
|
+
hasTerminalStatus: Boolean(terminalStatus),
|
|
340
|
+
liveStatus: live.status,
|
|
341
|
+
liveStaleReason: live.staleReason,
|
|
342
|
+
dispatchedToIdleSession: dispatch.payload?.dispatchedToIdleSession === true,
|
|
343
|
+
});
|
|
300
344
|
const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
|
|
301
345
|
const { title, summary } = summarizeMessage(message);
|
|
302
|
-
const isFreshUnacknowledged = Boolean(ledgerOnlyStaleReason && !live.staleReason);
|
|
303
346
|
const record: MeshActiveWorkRecord = {
|
|
304
347
|
taskId,
|
|
305
348
|
source: 'direct',
|
|
@@ -344,15 +387,16 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
344
387
|
const live = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
|
|
345
388
|
const status = terminalStatus || live.status || 'assigned';
|
|
346
389
|
const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
:
|
|
390
|
+
const { ledgerOnlyStaleReason, isFreshUnacknowledged } = classifyDirectDispatch({
|
|
391
|
+
status,
|
|
392
|
+
isTerminalRow: terminalRow,
|
|
393
|
+
hasTerminalStatus: Boolean(terminalStatus),
|
|
394
|
+
liveStatus: live.status,
|
|
395
|
+
liveStaleReason: live.staleReason,
|
|
396
|
+
dispatchedToIdleSession: dispatch.payload?.dispatchedToIdleSession === true,
|
|
397
|
+
});
|
|
353
398
|
const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
|
|
354
399
|
const { title, summary } = summarizeMessage(message);
|
|
355
|
-
const isFreshUnacknowledged = Boolean(ledgerOnlyStaleReason && !live.staleReason);
|
|
356
400
|
const record: MeshActiveWorkRecord = {
|
|
357
401
|
taskId,
|
|
358
402
|
source: 'direct',
|
|
@@ -251,6 +251,55 @@ function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metad
|
|
|
251
251
|
return false;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
// A worker/coordinator "false idle": the provider dropped to idle WITHOUT a confirmed
|
|
255
|
+
// final assistant message for the turn (a finalization timeout, or a "scheduled fallback"
|
|
256
|
+
// idle). This is the signal cli-provider-instance emits as
|
|
257
|
+
// completionDiagnostic.blockReason='missing_final_assistant' / finalAssistantPresent=false.
|
|
258
|
+
// Such a completion is NOT trustworthy terminal evidence: it must neither permanently
|
|
259
|
+
// terminate a direct-dispatch task nor suppress the genuine completion a later turn
|
|
260
|
+
// (commonly driven by a coordinator nudge / re-dispatch) produces.
|
|
261
|
+
function isFalseIdleCompletion(metadataEvent: Record<string, unknown>): boolean {
|
|
262
|
+
const diag = readRecord(metadataEvent.completionDiagnostic);
|
|
263
|
+
if (!diag) return false;
|
|
264
|
+
return diag.finalAssistantPresent === false || diag.blockReason === 'missing_final_assistant';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// The genuine-completion counterpart: a real final summary / worker result is present and
|
|
268
|
+
// the completion is not flagged as a missing-final-assistant false idle. Used to decide
|
|
269
|
+
// whether a new completion may supersede a prior WEAK (false-idle) terminal.
|
|
270
|
+
function isGenuineCompletionEvidence(metadataEvent: Record<string, unknown>): boolean {
|
|
271
|
+
if (isFalseIdleCompletion(metadataEvent)) return false;
|
|
272
|
+
return !!readWorkerResultMetadata(metadataEvent) || !!readNonEmptyString(metadataEvent.finalSummary);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// True when a terminal ledger payload was recorded from WEAK completion evidence (a false
|
|
276
|
+
// idle): insufficient evidence level, review-recommended, or a missing-final-assistant
|
|
277
|
+
// completion diagnostic. A weak terminal is non-authoritative — a later genuine completion
|
|
278
|
+
// (live path) or a transcript reconcile (fallback path) may supersede it.
|
|
279
|
+
function isWeakTerminalLedgerPayload(payload: Record<string, unknown> | undefined): boolean {
|
|
280
|
+
if (!payload) return false;
|
|
281
|
+
if (payload.evidenceLevel === 'insufficient' || payload.reviewRecommended === true) return true;
|
|
282
|
+
const diag = readRecord(payload.completionDiagnostic);
|
|
283
|
+
return diag?.finalAssistantPresent === false || diag?.blockReason === 'missing_final_assistant';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// The latest still-active direct-dispatch taskId for a session, resolved BEFORE the
|
|
287
|
+
// completion flips the dispatch row terminal. Direct dispatches (mesh_send_task) have no
|
|
288
|
+
// work-queue row, so this is the only taskId available to attribute the terminal ledger
|
|
289
|
+
// entry (and thus mesh task-stats) to — without it the terminal carries no taskId and the
|
|
290
|
+
// task surfaces as status='unknown' / terminalKind=null in computeMeshTaskStats.
|
|
291
|
+
function resolveActiveDirectDispatchTaskId(meshId: string, sessionId: string): string | undefined {
|
|
292
|
+
try {
|
|
293
|
+
const matches = getActiveDirectDispatches(meshId).filter(d => d.sessionId === sessionId);
|
|
294
|
+
if (!matches.length) return undefined;
|
|
295
|
+
// getActiveDirectDispatches returns rows ordered by dispatched_at ASC; the last is
|
|
296
|
+
// the most recent dispatch (the re-dispatch / nudge whose completion this is).
|
|
297
|
+
return readNonEmptyString(matches[matches.length - 1].taskId) || undefined;
|
|
298
|
+
} catch {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
254
303
|
// ---------------------------------------------------------------------------
|
|
255
304
|
// Queue assignment
|
|
256
305
|
// ---------------------------------------------------------------------------
|
|
@@ -1555,7 +1604,17 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1555
1604
|
});
|
|
1556
1605
|
if (terminal?.kind === 'task_completed' && !sessionHasActiveAssignment(args.meshId, eventSessionId)) {
|
|
1557
1606
|
const newDispatchAfterTerminal = hasDispatchAfterTerminal(args.meshId, eventSessionId, terminal.id);
|
|
1558
|
-
|
|
1607
|
+
// Fix B (re-dispatch 2nd-completion routing): a prior terminal recorded from a FALSE
|
|
1608
|
+
// idle (weak evidence / no confirmed final assistant) must NOT permanently suppress a
|
|
1609
|
+
// later GENUINE completion of the same session. providerSessionId is stable across a
|
|
1610
|
+
// session's turns, so the providerSessionId/finalSummary dedup below would otherwise
|
|
1611
|
+
// swallow the real 2nd-turn completion that a coordinator nudge (direct re-dispatch)
|
|
1612
|
+
// drove — exactly the missed-event bug. When the prior terminal was weak and the new
|
|
1613
|
+
// event carries genuine completion evidence, let it through so it is recorded and
|
|
1614
|
+
// re-attributed to the latest task (the normal task_completed path below).
|
|
1615
|
+
const supersedesWeakTerminal = isWeakTerminalLedgerPayload(terminal.payload)
|
|
1616
|
+
&& isGenuineCompletionEvidence(args.metadataEvent);
|
|
1617
|
+
if (!newDispatchAfterTerminal && !supersedesWeakTerminal) {
|
|
1559
1618
|
const terminalProviderSessionId = readNonEmptyString(terminal.payload.providerSessionId);
|
|
1560
1619
|
const terminalFinalSummary = readNonEmptyString(terminal.payload.finalSummary);
|
|
1561
1620
|
const eventProviderSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
|
|
@@ -1606,7 +1665,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1606
1665
|
}
|
|
1607
1666
|
}
|
|
1608
1667
|
|
|
1609
|
-
function markSessionTerminal(sessionId: string, outcome: 'completed' | 'failed', occurredAtMs?: number | null): { id?: string } | null {
|
|
1668
|
+
function markSessionTerminal(sessionId: string, outcome: 'completed' | 'failed', occurredAtMs?: number | null, opts?: { tentativeIfDirect?: boolean }): { id?: string } | null {
|
|
1610
1669
|
// C2: prefer an exact taskId match when the completion event carries one —
|
|
1611
1670
|
// it's immune to coordinator↔worker clock skew that can hide the assigned row.
|
|
1612
1671
|
const eventTaskId = readNonEmptyString(args.metadataEvent.taskId) || undefined;
|
|
@@ -1614,20 +1673,36 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1614
1673
|
occurredAt: occurredAtMs != null ? new Date(occurredAtMs).toISOString() : undefined,
|
|
1615
1674
|
taskId: eventTaskId,
|
|
1616
1675
|
});
|
|
1617
|
-
|
|
1676
|
+
// Fix A (early-terminal prevention): a false-idle completion (no confirmed final
|
|
1677
|
+
// assistant) for a DIRECT dispatch — i.e. no work-queue row matched — must not flip the
|
|
1678
|
+
// dispatch row terminal. Leaving it active lets the reconcile loop (PHASE 4) re-read the
|
|
1679
|
+
// transcript and record the genuine completion once the worker truly finishes (commonly
|
|
1680
|
+
// after a coordinator nudge / re-dispatch). A matched queue task, or a completion with
|
|
1681
|
+
// genuine evidence, is marked terminal as before.
|
|
1682
|
+
const leaveDirectDispatchActive = !task && opts?.tentativeIfDirect === true;
|
|
1683
|
+
if (!leaveDirectDispatchActive) {
|
|
1684
|
+
updateDirectDispatchStatus(args.meshId, sessionId, outcome);
|
|
1685
|
+
}
|
|
1618
1686
|
markSessionDeliveriesTerminal(args.meshId, sessionId, outcome);
|
|
1619
1687
|
setImmediate(() => cleanupTerminalDirectDispatches());
|
|
1620
1688
|
return task ? { id: task.id } : null;
|
|
1621
1689
|
}
|
|
1622
1690
|
|
|
1623
1691
|
let completedTaskForLedger: { id?: string } | null = null;
|
|
1692
|
+
// Fix B: direct-dispatch taskId used to attribute the terminal ledger entry when no
|
|
1693
|
+
// work-queue row matches (resolved BEFORE markSessionTerminal flips the dispatch terminal).
|
|
1694
|
+
let directDispatchTaskIdForLedger: string | undefined;
|
|
1624
1695
|
if (args.event === 'agent:generating_completed') {
|
|
1625
1696
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
1626
1697
|
const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
1627
1698
|
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
1628
1699
|
|
|
1629
1700
|
if (sessionId) {
|
|
1630
|
-
|
|
1701
|
+
directDispatchTaskIdForLedger = resolveActiveDirectDispatchTaskId(args.meshId, sessionId);
|
|
1702
|
+
// A false-idle completion of a direct dispatch is recorded but kept tentative (the
|
|
1703
|
+
// dispatch row stays active for the reconcile fallback); a genuine completion is terminal.
|
|
1704
|
+
const isFalseIdle = isFalseIdleCompletion(args.metadataEvent);
|
|
1705
|
+
completedTaskForLedger = markSessionTerminal(sessionId, 'completed', eventTimestamp, { tentativeIfDirect: isFalseIdle });
|
|
1631
1706
|
if (nodeId && providerType) {
|
|
1632
1707
|
runIdleMaintenanceThenAssignQueue(components, { meshId: args.meshId, nodeId, sessionId, providerType });
|
|
1633
1708
|
}
|
|
@@ -1729,6 +1804,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1729
1804
|
} catch { /* best-effort */ }
|
|
1730
1805
|
}
|
|
1731
1806
|
if (sessionId) {
|
|
1807
|
+
directDispatchTaskIdForLedger = resolveActiveDirectDispatchTaskId(args.meshId, sessionId);
|
|
1732
1808
|
completedTaskForLedger = markSessionTerminal(sessionId, 'failed');
|
|
1733
1809
|
}
|
|
1734
1810
|
}
|
|
@@ -1761,7 +1837,10 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1761
1837
|
payload: {
|
|
1762
1838
|
event: args.event,
|
|
1763
1839
|
nodeLabel: args.nodeLabel,
|
|
1764
|
-
|
|
1840
|
+
// Fix B: fall back to the direct-dispatch taskId when no work-queue row
|
|
1841
|
+
// matched, so the terminal entry is attributable in mesh task-stats
|
|
1842
|
+
// (otherwise the direct task shows status='unknown' / terminalKind=null).
|
|
1843
|
+
taskId: completedTaskForLedger?.id || directDispatchTaskIdForLedger || undefined,
|
|
1765
1844
|
providerSessionId,
|
|
1766
1845
|
finalSummary,
|
|
1767
1846
|
workerResult,
|
|
@@ -303,6 +303,7 @@ export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEv
|
|
|
303
303
|
const fingerprint = buildPendingEventFingerprint(event);
|
|
304
304
|
|
|
305
305
|
// G3: Write to SQLite inbox (primary path going forward)
|
|
306
|
+
let sqliteOk = false;
|
|
306
307
|
try {
|
|
307
308
|
MeshRuntimeStore.getInstance().insertPendingEvent({
|
|
308
309
|
id: randomUUID(),
|
|
@@ -313,14 +314,22 @@ export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEv
|
|
|
313
314
|
fingerprint: fingerprint || null,
|
|
314
315
|
queuedAt: event.queuedAt,
|
|
315
316
|
});
|
|
317
|
+
sqliteOk = true;
|
|
316
318
|
} catch {
|
|
317
319
|
// SQLite write failure is non-fatal; JSONL fallback below still works.
|
|
318
320
|
}
|
|
319
321
|
|
|
320
|
-
// Also write to JSONL (retained as legacy/export artifact)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
322
|
+
// Also write to JSONL (retained as legacy/export artifact). Best-effort once
|
|
323
|
+
// SQLite (the primary store) has the event: a JSONL append failure (disk full,
|
|
324
|
+
// permissions) must NOT report the whole persist as failed when SQLite holds it.
|
|
325
|
+
try {
|
|
326
|
+
const path = getPendingEventsPath(event.meshId, event.targetCoordinatorDaemonId);
|
|
327
|
+
trimPendingEventsIfNeeded(path);
|
|
328
|
+
appendFileSync(path, JSON.stringify(event) + '\n', 'utf-8');
|
|
329
|
+
} catch (e: any) {
|
|
330
|
+
if (!sqliteOk) throw e; // neither store has it — surface as a real failure
|
|
331
|
+
LOG.warn('MeshEvents', `JSONL append failed for mesh ${event.meshId}; SQLite holds the event: ${e?.message || e}`);
|
|
332
|
+
}
|
|
324
333
|
return true;
|
|
325
334
|
} catch (e: any) {
|
|
326
335
|
LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
|
|
@@ -74,6 +74,17 @@ export function hasUnterminalDirectDispatchLedgerEntry(meshId: string, sessionId
|
|
|
74
74
|
return false;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// True when a terminal ledger payload was recorded from WEAK completion evidence (a false
|
|
78
|
+
// idle): insufficient evidence level, review-recommended, or a missing-final-assistant
|
|
79
|
+
// completion diagnostic. Mirrors isWeakTerminalLedgerPayload in mesh-events-coordinator —
|
|
80
|
+
// a weak terminal is non-authoritative and may be superseded by a genuine completion.
|
|
81
|
+
function isWeakCompletionLedgerPayload(payload: Record<string, unknown> | undefined): boolean {
|
|
82
|
+
if (!payload) return false;
|
|
83
|
+
if (payload.evidenceLevel === 'insufficient' || payload.reviewRecommended === true) return true;
|
|
84
|
+
const diag = readRecord(payload.completionDiagnostic);
|
|
85
|
+
return diag?.finalAssistantPresent === false || diag?.blockReason === 'missing_final_assistant';
|
|
86
|
+
}
|
|
87
|
+
|
|
77
88
|
function findDirectDispatchLedgerEntry(args: {
|
|
78
89
|
meshId: string;
|
|
79
90
|
taskId: string;
|
|
@@ -121,6 +132,12 @@ function hasTerminalLedgerAfterDispatch(args: {
|
|
|
121
132
|
if (!afterDispatch) continue;
|
|
122
133
|
}
|
|
123
134
|
if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
|
|
135
|
+
// Fix C (reconcile fallback expansion): a task_completed recorded from a FALSE idle
|
|
136
|
+
// (weak evidence / no confirmed final assistant) is NOT authoritative terminal
|
|
137
|
+
// evidence. Skip it so the transcript reconcile can still synthesize the GENUINE
|
|
138
|
+
// completion for a re-dispatched / prematurely-terminated direct task instead of
|
|
139
|
+
// bailing with alreadyTerminal.
|
|
140
|
+
if (entry.kind === 'task_completed' && isWeakCompletionLedgerPayload(entry.payload)) continue;
|
|
124
141
|
const terminalTaskId = readNonEmptyString(entry.payload?.taskId);
|
|
125
142
|
if (terminalTaskId && terminalTaskId === args.taskId) return true;
|
|
126
143
|
if (terminalTaskId && terminalTaskId !== args.taskId) continue;
|
|
@@ -70,6 +70,10 @@ export class MeshRuntimeStore {
|
|
|
70
70
|
private readonly migratedMeshIds = new Set<string>();
|
|
71
71
|
private fingerprintSweepCounter = 0;
|
|
72
72
|
private walWriteCounter = 0;
|
|
73
|
+
// Independent cadence for the tool-call-log sweep. Must NOT share walWriteCounter:
|
|
74
|
+
// sharing makes each store's threshold drift by the other's write volume (WAL
|
|
75
|
+
// checkpoint at 500 vs tool-log sweep at 200 would interfere arbitrarily).
|
|
76
|
+
private toolCallLogCounter = 0;
|
|
73
77
|
private static readonly WAL_CHECK_INTERVAL = 500;
|
|
74
78
|
private static readonly WAL_MAX_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
75
79
|
|
|
@@ -1171,7 +1175,7 @@ export class MeshRuntimeStore {
|
|
|
1171
1175
|
const callsInWindow = row?.cnt ?? 0;
|
|
1172
1176
|
|
|
1173
1177
|
// Sweep old entries periodically to keep the table lean (every 200 calls across all tools).
|
|
1174
|
-
if (++this.
|
|
1178
|
+
if (++this.toolCallLogCounter % 200 === 0) {
|
|
1175
1179
|
this.db.prepare(
|
|
1176
1180
|
'DELETE FROM mesh_tool_call_log WHERE called_at < ?'
|
|
1177
1181
|
).run(now - Math.max(windowMs * 10, 60_000));
|
|
@@ -91,9 +91,16 @@ export function computeMeshTaskStats(meshId: string, opts?: { taskIds?: string[]
|
|
|
91
91
|
|
|
92
92
|
return targetIds.map(taskId => {
|
|
93
93
|
const queueEntry = queueById.get(taskId);
|
|
94
|
-
const status = queueEntry?.status ?? 'unknown';
|
|
95
94
|
const dispatch = dispatches.get(taskId);
|
|
96
95
|
const terminal = terminals.get(taskId);
|
|
96
|
+
// Direct dispatches (mesh_send_task) have no work-queue row, so queueEntry is undefined.
|
|
97
|
+
// Derive a terminal status from the attributed terminal ledger entry instead of reporting
|
|
98
|
+
// status='unknown' — without this a completed direct task showed unknown + terminalKind=null
|
|
99
|
+
// even though its task_completed event fired (see mesh-events-coordinator Fix B attribution).
|
|
100
|
+
const status = queueEntry?.status
|
|
101
|
+
?? (terminal
|
|
102
|
+
? (terminal.kind === 'task_completed' ? 'completed' : 'failed')
|
|
103
|
+
: 'unknown');
|
|
97
104
|
const isTerminalStatus = status === 'completed' || status === 'failed' || status === 'cancelled';
|
|
98
105
|
const dispatchTime = parseTime(dispatch?.first ?? queueEntry?.dispatchTimestamp);
|
|
99
106
|
const terminalTime = parseTime(terminal?.at);
|