@adhdev/daemon-core 0.9.82-rc.379 → 0.9.82-rc.380
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.js +50 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +50 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/mesh/mesh-event-forwarding.ts +59 -2
- package/src/mesh/mesh-events-stale.ts +42 -0
- package/src/mesh/mesh-reconcile-loop.ts +11 -0
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.380",
|
|
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.380",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -310,6 +310,44 @@ function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metad
|
|
|
310
310
|
return false;
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
// NOTIF-MISS (FIX 2): the set of `source` tags the transcript-reconcile synthesis path stamps
|
|
314
|
+
// onto the terminal ledger entry it writes (mesh-events-stale.reconcileDirectDispatchCompletion
|
|
315
|
+
// FromTranscript). A terminal carrying one of these was NOT produced by an actual provider
|
|
316
|
+
// agent_status_event — it is a coordinator-side reconstruction from the worker's transcript. The
|
|
317
|
+
// authoritative real completion must never be permanently masked by such a synthesized record.
|
|
318
|
+
const RECONCILED_COMPLETION_SOURCES = new Set([
|
|
319
|
+
'direct_task_transcript_reconciliation',
|
|
320
|
+
'daemon_reconcile_transcript_completion',
|
|
321
|
+
'mcp_mesh_status_transcript_reconciliation',
|
|
322
|
+
'no_progress_reconciliation',
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
// True when the recorded terminal ledger entry was SYNTHESIZED by transcript reconciliation
|
|
326
|
+
// rather than emitted by the real provider event. The reconcile path tags its ledger payload
|
|
327
|
+
// with a reconcile `source`; a genuine provider completion carries no such tag (or a normal
|
|
328
|
+
// completionDiagnostic.reason). Both the explicit source AND the diagnostic reason are checked so
|
|
329
|
+
// either carrier identifies the synthesized record.
|
|
330
|
+
function isSynthesizedReconciledTerminal(terminalPayload: Record<string, unknown>): boolean {
|
|
331
|
+
const source = readNonEmptyString(terminalPayload.source);
|
|
332
|
+
if (source && RECONCILED_COMPLETION_SOURCES.has(source)) return true;
|
|
333
|
+
const diagnostic = terminalPayload.completionDiagnostic;
|
|
334
|
+
if (diagnostic && typeof diagnostic === 'object' && !Array.isArray(diagnostic)) {
|
|
335
|
+
const reason = readNonEmptyString((diagnostic as Record<string, unknown>).reason);
|
|
336
|
+
if (reason && RECONCILED_COMPLETION_SOURCES.has(reason)) return true;
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// True when the INCOMING completion event is a REAL provider agent_status_event (an actual
|
|
342
|
+
// agent:generating_completed from the live session) rather than itself a synthesized/reconciled
|
|
343
|
+
// re-injection. We must only let a real event override a synthesized terminal — a second
|
|
344
|
+
// synthesized re-arrival should still dedup normally. A real provider event carries no reconcile
|
|
345
|
+
// `source` tag; a re-injected reconciliation does.
|
|
346
|
+
function isRealProviderCompletionEvent(metadataEvent: Record<string, unknown>): boolean {
|
|
347
|
+
const source = readNonEmptyString(metadataEvent.source);
|
|
348
|
+
return !source || !RECONCILED_COMPLETION_SOURCES.has(source);
|
|
349
|
+
}
|
|
350
|
+
|
|
313
351
|
// The genuine-completion counterpart: a real final summary / worker result is present and
|
|
314
352
|
// the completion is not flagged as a missing-final-assistant false idle. Used to decide
|
|
315
353
|
// whether a new completion may supersede a prior WEAK (false-idle) terminal. NOTE this gates
|
|
@@ -516,7 +554,20 @@ function evaluateMeshEventSuppression(
|
|
|
516
554
|
terminalTaskId,
|
|
517
555
|
eventTaskId,
|
|
518
556
|
});
|
|
519
|
-
|
|
557
|
+
// NOTIF-MISS (FIX 2): the recorded terminal was SYNTHESIZED by transcript reconciliation
|
|
558
|
+
// (no real provider event), and THIS incoming event is the authoritative REAL provider
|
|
559
|
+
// completion. The reconcile path may fire ~1s after a direct dispatch from a reused
|
|
560
|
+
// session's stale transcript tail, writing a synthesized terminal whose providerSessionId
|
|
561
|
+
// (stable across a session's turns) and/or finalSummary then match the real completion —
|
|
562
|
+
// so the providerSessionId/finalSummary dedup below would drop the genuine event and the
|
|
563
|
+
// coordinator would never learn the task finished. A synthesized record must NEVER
|
|
564
|
+
// permanently mask the real completion: let the real event through (it is re-recorded by
|
|
565
|
+
// the normal task_completed path, superseding the synthesized one). A second SYNTHESIZED
|
|
566
|
+
// re-arrival is NOT a real event and still dedups normally.
|
|
567
|
+
const supersedesSynthesizedTerminal = isSynthesizedReconciledTerminal(terminal.payload)
|
|
568
|
+
&& isRealProviderCompletionEvent(args.metadataEvent)
|
|
569
|
+
&& isGenuineCompletionEvidence(args.metadataEvent);
|
|
570
|
+
if (!newDispatchAfterTerminal && !supersedesWeakTerminal && !distinctTaskCompletion && !supersedesTruncatedTerminal && !supersedesSynthesizedTerminal) {
|
|
520
571
|
const terminalProviderSessionId = readNonEmptyString(terminal.payload.providerSessionId);
|
|
521
572
|
const terminalFinalSummary = readNonEmptyString(terminal.payload.finalSummary);
|
|
522
573
|
const eventProviderSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
|
|
@@ -1180,8 +1231,14 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
|
|
|
1180
1231
|
return { success: false, error: 'meshId required' };
|
|
1181
1232
|
}
|
|
1182
1233
|
// EVTTRACE: forwarded event accepted at receive (meshId resolved).
|
|
1234
|
+
// NOTIF-MISS (FIX 3): mirror buildRelayMetadataEvent's taskId fallback. A worker provider
|
|
1235
|
+
// completion stamps its task id as `meshActiveTaskId` (settings → event), not always the
|
|
1236
|
+
// top-level `taskId`, so reading payload.taskId alone rendered `task=-` at the received stage
|
|
1237
|
+
// (the [stage:queued] task=<id> → [stage:received] task=- loss). Falling back to
|
|
1238
|
+
// meshActiveTaskId keeps the trace task-scoped end-to-end so same-task redelivery is
|
|
1239
|
+
// distinguishable from a different task's real completion.
|
|
1183
1240
|
traceMeshEventStage('received', {
|
|
1184
|
-
taskId: payload.taskId,
|
|
1241
|
+
taskId: readNonEmptyString(payload.taskId) || readNonEmptyString(payload.meshActiveTaskId),
|
|
1185
1242
|
sessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId),
|
|
1186
1243
|
nodeId,
|
|
1187
1244
|
meshId,
|
|
@@ -10,6 +10,25 @@ import { meshNodeIdMatches, type MeshNodeIdentified } from '@adhdev/mesh-shared'
|
|
|
10
10
|
// Stale direct-dispatch detection & transcript reconciliation
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
|
|
13
|
+
// NOTIF-MISS grace gate. A direct dispatch (mesh_send_task) to an ALREADY-IDLE,
|
|
14
|
+
// previously-used session reuses a transcript that still holds the PRIOR turn's final
|
|
15
|
+
// assistant message. The session momentarily reads `idle` in the first second after
|
|
16
|
+
// dispatch (the worker hasn't started generating yet), so the transcript reconcile here
|
|
17
|
+
// would otherwise synthesize a "completion" ~1s after dispatch from that stale tail —
|
|
18
|
+
// writing a synthesized task_completed + queuing a completion event. When the REAL
|
|
19
|
+
// completion fires minutes later, checkSuppressMeshEvent drops it as a duplicate of the
|
|
20
|
+
// synthesized one (same providerSessionId/finalSummary), so the coordinator never learns
|
|
21
|
+
// the task actually finished. The grace period refuses to synthesize until enough time has
|
|
22
|
+
// passed since dispatch that a genuine same-task completion is plausible, closing the race
|
|
23
|
+
// window. The stale-summary timestamp guard (transcript predates dispatch) and FIX 2 in
|
|
24
|
+
// mesh-event-forwarding (a synthesized completion never suppresses the authoritative real
|
|
25
|
+
// one) are the correctness backstops; this grace just stops the synthesis from firing in
|
|
26
|
+
// the first place for the common case. An idle-session dispatch (dispatchedToIdleSession)
|
|
27
|
+
// is the exact race above, so it gets a LONGER grace than a dispatch to a session that was
|
|
28
|
+
// already generating (where a transcript reconcile is far less likely to be premature).
|
|
29
|
+
const DIRECT_DISPATCH_RECONCILE_GRACE_MS = 60_000;
|
|
30
|
+
const DIRECT_DISPATCH_IDLE_SESSION_RECONCILE_GRACE_MS = 120_000;
|
|
31
|
+
|
|
13
32
|
export function findRecentTerminalLedgerEvidence(args: {
|
|
14
33
|
meshId: string;
|
|
15
34
|
sessionId?: string;
|
|
@@ -210,6 +229,29 @@ export function reconcileDirectDispatchCompletionFromTranscript(args: {
|
|
|
210
229
|
if (workerResult.source !== 'final_summary_json' && !transcriptAfterDispatch) {
|
|
211
230
|
return { reconciled: false, reason: 'transcript_not_proven_after_dispatch' };
|
|
212
231
|
}
|
|
232
|
+
// NOTIF-MISS grace gate (FIX 1): refuse to synthesize a completion for a direct dispatch
|
|
233
|
+
// until a grace period has elapsed since its dispatch. A direct dispatch to an ALREADY-IDLE,
|
|
234
|
+
// previously-used session reuses a transcript that still holds the PRIOR turn's final
|
|
235
|
+
// assistant message; the session momentarily reads `idle` in the first second after dispatch
|
|
236
|
+
// (the worker hasn't started generating yet), so without this gate the reconcile would
|
|
237
|
+
// synthesize a "completion" ~1s after dispatch from that stale tail. When the REAL completion
|
|
238
|
+
// fires minutes later, checkSuppressMeshEvent drops it as a duplicate of the synthesized one
|
|
239
|
+
// (same providerSessionId/finalSummary) — the coordinator never learns the task finished.
|
|
240
|
+
// The grace refuses synthesis until a genuine same-task completion is plausible. An idle-
|
|
241
|
+
// session dispatch (dispatchedToIdleSession) is the exact race, so it gets a LONGER grace.
|
|
242
|
+
// A structured final_summary_json is self-attributing (the provider emitted it for THIS turn),
|
|
243
|
+
// so it is exempt; the grace only guards the ambiguous plain-text-tail case. When the dispatch
|
|
244
|
+
// entry is missing we cannot date it and do NOT block (the stale-summary timestamp guard +
|
|
245
|
+
// FIX 2 supersession remain the correctness backstops).
|
|
246
|
+
if (Number.isFinite(dispatchTime) && workerResult.source !== 'final_summary_json') {
|
|
247
|
+
const dispatchedToIdleSession = dispatch?.payload?.dispatchedToIdleSession === true;
|
|
248
|
+
const graceMs = dispatchedToIdleSession
|
|
249
|
+
? DIRECT_DISPATCH_IDLE_SESSION_RECONCILE_GRACE_MS
|
|
250
|
+
: DIRECT_DISPATCH_RECONCILE_GRACE_MS;
|
|
251
|
+
if (Date.now() - dispatchTime < graceMs) {
|
|
252
|
+
return { reconciled: false, reason: 'direct_dispatch_grace_period' };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
213
255
|
const workerFailed = workerResult.status === 'failed' || (workerResult.status !== 'completed' && workerResult.errors.length > 0);
|
|
214
256
|
const kind: MeshLedgerKind = workerFailed ? 'task_failed' : 'task_completed';
|
|
215
257
|
|
|
@@ -1291,6 +1291,17 @@ function buildForwardPayloadFromPending(event: any): Record<string, unknown> {
|
|
|
1291
1291
|
? { targetCoordinatorSessionId: readNonEmptyString(event.targetCoordinatorSessionId) }
|
|
1292
1292
|
: {}),
|
|
1293
1293
|
...metadata,
|
|
1294
|
+
// NOTIF-MISS (FIX 3): surface the dispatch task id at the TOP LEVEL so the relay's
|
|
1295
|
+
// received-stage trace (and buildRelayMetadataEvent) recovers it regardless of which
|
|
1296
|
+
// carrier the producing daemon used. The metadata spread above may carry the id only as
|
|
1297
|
+
// `meshActiveTaskId` (a worker provider event), leaving top-level `taskId` unset and the
|
|
1298
|
+
// received stage rendering `task=-`. Resolve both carriers into an explicit `taskId` so
|
|
1299
|
+
// dedup stays task-scoped end-to-end. Only set when a non-empty id exists (no clobber to
|
|
1300
|
+
// undefined when neither is present).
|
|
1301
|
+
...((): Record<string, unknown> => {
|
|
1302
|
+
const tid = readNonEmptyString(metadata.taskId) || readNonEmptyString(metadata.meshActiveTaskId);
|
|
1303
|
+
return tid ? { taskId: tid } : {};
|
|
1304
|
+
})(),
|
|
1294
1305
|
};
|
|
1295
1306
|
}
|
|
1296
1307
|
|