@adhdev/daemon-core 0.9.82-rc.394 → 0.9.82-rc.396
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 +45 -23
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +45 -23
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-stale.d.ts +9 -0
- package/package.json +2 -2
- package/src/mesh/mesh-events-stale.ts +43 -17
- package/src/mesh/mesh-queue-assignment.ts +31 -0
- package/src/mesh/mesh-reconcile-loop.ts +26 -3
|
@@ -34,6 +34,15 @@ export declare function reconcileDirectDispatchCompletionFromTranscript(args: {
|
|
|
34
34
|
transcriptMessageAt?: string;
|
|
35
35
|
completedAt?: string;
|
|
36
36
|
targetCoordinatorDaemonId?: string;
|
|
37
|
+
/**
|
|
38
|
+
* NOTIF-DROP-SYNTH-NO-MESSAGE: the originating coordinator SESSION that dispatched this
|
|
39
|
+
* task (the `coordinatorSessionId` stamped onto the task_dispatched ledger payload at
|
|
40
|
+
* dispatch). Stamped onto the synthesized completion's targetCoordinatorSessionId so PHASE 2
|
|
41
|
+
* STRICT routing matches the exact coordinator session — instead of relying on the
|
|
42
|
+
* daemon-keyed fallback. When the caller does not pass it, it is recovered from the dispatch
|
|
43
|
+
* ledger entry below; absent on legacy rows → daemon-level routing (unchanged).
|
|
44
|
+
*/
|
|
45
|
+
targetCoordinatorSessionId?: string;
|
|
37
46
|
source?: string;
|
|
38
47
|
}): {
|
|
39
48
|
reconciled: boolean;
|
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.396",
|
|
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.396",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -3,7 +3,7 @@ import type { MeshLedgerKind } from './mesh-ledger.js';
|
|
|
3
3
|
import { updateDirectDispatchStatus, cleanupTerminalDirectDispatches } from './mesh-work-queue.js';
|
|
4
4
|
import { markSessionDeliveriesTerminal } from './mesh-delivery-policy.js';
|
|
5
5
|
import { queuePendingMeshCoordinatorEvent } from './mesh-events-pending.js';
|
|
6
|
-
import { readNonEmptyString, readRecord, resolveEventSessionId, readWorkerResultMetadata, isWeakCompletionEvidence } from './mesh-events-utils.js';
|
|
6
|
+
import { readNonEmptyString, readRecord, resolveEventSessionId, readWorkerResultMetadata, isWeakCompletionEvidence, buildMeshSystemMessage } from './mesh-events-utils.js';
|
|
7
7
|
import { meshNodeIdMatches, type MeshNodeIdentified } from '@adhdev/mesh-shared';
|
|
8
8
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
@@ -188,6 +188,15 @@ export function reconcileDirectDispatchCompletionFromTranscript(args: {
|
|
|
188
188
|
transcriptMessageAt?: string;
|
|
189
189
|
completedAt?: string;
|
|
190
190
|
targetCoordinatorDaemonId?: string;
|
|
191
|
+
/**
|
|
192
|
+
* NOTIF-DROP-SYNTH-NO-MESSAGE: the originating coordinator SESSION that dispatched this
|
|
193
|
+
* task (the `coordinatorSessionId` stamped onto the task_dispatched ledger payload at
|
|
194
|
+
* dispatch). Stamped onto the synthesized completion's targetCoordinatorSessionId so PHASE 2
|
|
195
|
+
* STRICT routing matches the exact coordinator session — instead of relying on the
|
|
196
|
+
* daemon-keyed fallback. When the caller does not pass it, it is recovered from the dispatch
|
|
197
|
+
* ledger entry below; absent on legacy rows → daemon-level routing (unchanged).
|
|
198
|
+
*/
|
|
199
|
+
targetCoordinatorSessionId?: string;
|
|
191
200
|
source?: string;
|
|
192
201
|
}): { reconciled: boolean; kind?: MeshLedgerKind; alreadyTerminal?: boolean; workerResult?: unknown; ledgerEntryId?: string; reason?: string } {
|
|
193
202
|
const finalSummary = readNonEmptyString(args.finalSummary);
|
|
@@ -282,27 +291,44 @@ export function reconcileDirectDispatchCompletionFromTranscript(args: {
|
|
|
282
291
|
updateDirectDispatchStatus(args.meshId, args.sessionId, kind === 'task_completed' ? 'completed' : 'failed', args.taskId);
|
|
283
292
|
markSessionDeliveriesTerminal(args.meshId, args.sessionId, kind === 'task_completed' ? 'completed' : 'failed');
|
|
284
293
|
setImmediate(() => cleanupTerminalDirectDispatches());
|
|
294
|
+
// NOTIF-DROP-SYNTH-NO-MESSAGE: the queued synth completion MUST carry a coordinatorMessage,
|
|
295
|
+
// or injectPendingIntoCoordinator early-returns (`!pending.coordinatorMessage`) and the row is
|
|
296
|
+
// drained-without-inject → no [System] surface. The ~8s-later native completion then collides
|
|
297
|
+
// on the taskId-anchored fingerprint and is blocked at INSERT, so the notification is lost
|
|
298
|
+
// forever. Build the SAME [System] message the native path builds (buildMeshSystemMessage) so
|
|
299
|
+
// the synth is itself a complete, deliverable completion. Routing is made STRICT too: stamp the
|
|
300
|
+
// originating coordinator session so PHASE 2 strict-match delivers to the exact coordinator
|
|
301
|
+
// session that dispatched the task instead of falling back to a daemon-level broadcast.
|
|
302
|
+
const eventName = kind === 'task_completed' ? 'agent:generating_completed' : 'agent:stopped';
|
|
303
|
+
const nodeLabel = nodeId ? `Node '${nodeId}'` : 'Remote agent';
|
|
304
|
+
const metadataEvent = {
|
|
305
|
+
targetSessionId: args.sessionId,
|
|
306
|
+
providerType: providerType || undefined,
|
|
307
|
+
providerSessionId: readNonEmptyString(args.providerSessionId),
|
|
308
|
+
finalSummary,
|
|
309
|
+
taskId: args.taskId,
|
|
310
|
+
workerResult,
|
|
311
|
+
completionDiagnostic: {
|
|
312
|
+
reason: 'direct_task_transcript_reconciliation',
|
|
313
|
+
terminalLedgerKind: kind,
|
|
314
|
+
terminalLedgerId: entry.id,
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
// Originating coordinator session: prefer the explicit arg; otherwise recover it from the
|
|
318
|
+
// task_dispatched ledger payload (the MCP dispatch path stamps `coordinatorSessionId` there).
|
|
319
|
+
// Absent on legacy rows → undefined → daemon-level routing (unchanged, no regression).
|
|
320
|
+
const targetCoordinatorSessionId = readNonEmptyString(args.targetCoordinatorSessionId)
|
|
321
|
+
|| readNonEmptyString(dispatch?.payload?.coordinatorSessionId);
|
|
285
322
|
queuePendingMeshCoordinatorEvent({
|
|
286
|
-
event:
|
|
323
|
+
event: eventName,
|
|
287
324
|
meshId: args.meshId,
|
|
288
|
-
nodeLabel
|
|
325
|
+
nodeLabel,
|
|
289
326
|
nodeId: nodeId || undefined,
|
|
290
|
-
metadataEvent
|
|
291
|
-
|
|
292
|
-
providerType: providerType || undefined,
|
|
293
|
-
providerSessionId: readNonEmptyString(args.providerSessionId),
|
|
294
|
-
finalSummary,
|
|
295
|
-
taskId: args.taskId,
|
|
296
|
-
workerResult,
|
|
297
|
-
completionDiagnostic: {
|
|
298
|
-
reason: 'direct_task_transcript_reconciliation',
|
|
299
|
-
terminalLedgerKind: kind,
|
|
300
|
-
terminalLedgerId: entry.id,
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
coordinatorMessage: undefined,
|
|
327
|
+
metadataEvent,
|
|
328
|
+
coordinatorMessage: buildMeshSystemMessage({ event: eventName, nodeLabel, metadataEvent }),
|
|
304
329
|
queuedAt: Date.now(),
|
|
305
330
|
...(readNonEmptyString(args.targetCoordinatorDaemonId) ? { targetCoordinatorDaemonId: readNonEmptyString(args.targetCoordinatorDaemonId) } : {}),
|
|
331
|
+
...(targetCoordinatorSessionId ? { targetCoordinatorSessionId } : {}),
|
|
306
332
|
});
|
|
307
333
|
|
|
308
334
|
return { reconciled: true, kind, workerResult, ledgerEntryId: entry.id };
|
|
@@ -269,6 +269,37 @@ export function tryAssignQueueTask(
|
|
|
269
269
|
const mesh = getMeshWithCache(components, meshId);
|
|
270
270
|
const node = mesh?.nodes.find((n: any) => readMeshNodeId(n) === nodeId);
|
|
271
271
|
|
|
272
|
+
// WORKTREE-CLAIM-GATE-BYPASS: the SINGLE claim-time gate for the worktree-bootstrap defer.
|
|
273
|
+
// tryAssignQueueTask is the one funnel every claim path flows through — the event-driven
|
|
274
|
+
// agent:ready drain, the triggerMeshQueue idle-session drain (local + remote), the
|
|
275
|
+
// auto-launch claim, and the PHASE 3 reconcile re-drain all call it. The agent:ready handler
|
|
276
|
+
// (mesh-event-forwarding) deferred its OWN claim while a worktree node's bootstrap was still
|
|
277
|
+
// 'running', but it ALSO called setRemoteIdleSession first — registering the session as a
|
|
278
|
+
// claim candidate. A concurrent triggerMeshQueue drain then pulled that candidate and claimed
|
|
279
|
+
// through tryAssignQueueTask within ~0.16s, BYPASSING the event-handler-local defer: the task
|
|
280
|
+
// dispatched into a half-built worktree (native addons not yet installed → child daemon dies
|
|
281
|
+
// → empty session, totalMessages=0). The transport ack returns ok:true, so neither the
|
|
282
|
+
// assigned-stranded watchdog nor the pending-only PHASE 3 reconcile ever re-fires it → the
|
|
283
|
+
// session is stranded empty forever.
|
|
284
|
+
//
|
|
285
|
+
// Lowering the gate HERE makes the defer a property of the claim itself, not of one caller:
|
|
286
|
+
// a worktree node whose bootstrap is still 'running' can never be claimed from any path. The
|
|
287
|
+
// task stays pending (we return false WITHOUT touching its status — no fail/cancel), so the
|
|
288
|
+
// bootstrap_complete refire (triggerMeshQueue re-fired on worktree_bootstrap_complete) re-runs
|
|
289
|
+
// this claim and passes once status is no longer 'running'. The registered remote-idle session
|
|
290
|
+
// persists for REMOTE_IDLE_SESSION_TTL_MS (5min > observed ~2m8s bootstrap), so the refire
|
|
291
|
+
// still finds a live candidate to re-claim. Identity uses meshNodeIdMatches (the same shared
|
|
292
|
+
// 3-form normalizer the defer guard uses), never a raw === — canon-identity regression guard.
|
|
293
|
+
// Conservative: any non-'running' status (idle/complete/failed/absent/unknown) does NOT gate,
|
|
294
|
+
// so a base node and a fully-bootstrapped worktree keep prior behavior exactly.
|
|
295
|
+
const gateNode = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId)) as
|
|
296
|
+
| { worktreeBootstrap?: { status?: string } }
|
|
297
|
+
| undefined;
|
|
298
|
+
if (gateNode?.worktreeBootstrap?.status === 'running') {
|
|
299
|
+
LOG.info('MeshQueue', `Gating queue claim for worktree node ${nodeId} (${sessionId}): worktree bootstrap still running — task left pending; claim re-fires once bootstrap reaches a terminal state (guards against dispatching into a half-built worktree → empty session)`);
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
|
|
272
303
|
// WTCLAIM (fix-B extended to the enqueue→claim path): a base-targeted task must never be
|
|
273
304
|
// claimed by — and dispatched into — a co-located worktree-clone session, nor vice versa.
|
|
274
305
|
// The drain candidate's nodeId is derived from settings.meshNodeId || settings.nodeId
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
ackUnresolvedDelegateForward,
|
|
56
56
|
expireStaleUnresolvedDelegateForwards,
|
|
57
57
|
} from './mesh-unresolved-forward-outbox.js';
|
|
58
|
-
import { readNonEmptyString, readMeshCompletionSummary } from './mesh-events-utils.js';
|
|
58
|
+
import { readNonEmptyString, readMeshCompletionSummary, buildMeshSystemMessage } from './mesh-events-utils.js';
|
|
59
59
|
import { traceMeshEventStage, traceMeshEventDrop } from './mesh-event-trace.js';
|
|
60
60
|
import { expandDaemonIdForms, daemonIdsEquivalent } from '@adhdev/mesh-shared';
|
|
61
61
|
import { getActiveDirectDispatches, getQueue, reclaimStrandedAssignedTask, updateTaskStatus } from './mesh-work-queue.js';
|
|
@@ -368,7 +368,30 @@ function injectPendingIntoCoordinator(
|
|
|
368
368
|
coordinator: LiveCoordinator['instance'],
|
|
369
369
|
pending: PendingMeshCoordinatorEvent,
|
|
370
370
|
): void {
|
|
371
|
-
if (!coordinator
|
|
371
|
+
if (!coordinator) return;
|
|
372
|
+
// NOTIF-DROP-SYNTH-NO-MESSAGE (defence-in-depth): a queued event with no coordinatorMessage
|
|
373
|
+
// used to be dropped here (drain-without-inject) — the row had already been consumed
|
|
374
|
+
// (drained=1) by the caller's drain, so silently returning lost it forever. The primary fix
|
|
375
|
+
// makes the transcript-reconcile synth always carry a coordinatorMessage, but as a backstop,
|
|
376
|
+
// lazily synthesize the [System] text for any force-inject (terminal: completion / approval /
|
|
377
|
+
// stop / refine·bootstrap) event that still arrives message-less, so it surfaces instead of
|
|
378
|
+
// vanishing. A NON-force lifecycle event (agent:ready / generating_started) legitimately
|
|
379
|
+
// carries no message and must NOT be injected (it is queued only to re-drive the claim state
|
|
380
|
+
// machine on pull) — for it we still return without injecting.
|
|
381
|
+
let coordinatorMessage = pending.coordinatorMessage;
|
|
382
|
+
if (!coordinatorMessage) {
|
|
383
|
+
if (!shouldForceInjectMeshEvent(pending.event)) return;
|
|
384
|
+
const metadataEvent = pending.metadataEvent && typeof pending.metadataEvent === 'object'
|
|
385
|
+
? pending.metadataEvent
|
|
386
|
+
: {};
|
|
387
|
+
coordinatorMessage = buildMeshSystemMessage({
|
|
388
|
+
event: pending.event,
|
|
389
|
+
nodeLabel: pending.nodeLabel,
|
|
390
|
+
metadataEvent,
|
|
391
|
+
});
|
|
392
|
+
if (!coordinatorMessage) return; // builder produced nothing — nothing to surface
|
|
393
|
+
LOG.warn('MeshReconcile', `Lazily synthesized missing coordinatorMessage for ${pending.event} (mesh ${pending.meshId}) at inject time — a queued terminal event arrived message-less`);
|
|
394
|
+
}
|
|
372
395
|
const force = shouldForceInjectMeshEvent(pending.event);
|
|
373
396
|
// EVTTRACE: event surfaced to the coordinator (injected into its live CLI session).
|
|
374
397
|
// This is the terminal happy-path stage. Observation only.
|
|
@@ -380,7 +403,7 @@ function injectPendingIntoCoordinator(
|
|
|
380
403
|
event: pending.event,
|
|
381
404
|
}, force ? 'force-inject' : 'inject');
|
|
382
405
|
coordinator.onEvent('send_message', {
|
|
383
|
-
input: { text:
|
|
406
|
+
input: { text: coordinatorMessage, textFallback: coordinatorMessage },
|
|
384
407
|
...(force ? { force: true } : {}),
|
|
385
408
|
});
|
|
386
409
|
}
|