@adhdev/daemon-core 0.9.82-rc.400 → 0.9.82-rc.402
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/cli-adapter-types.d.ts +2 -1
- package/dist/cli-adapters/cli-state-engine.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +3 -1
- package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/dist/git/git-worktree.d.ts +49 -1
- package/dist/git/index.d.ts +1 -1
- package/dist/index.js +382 -207
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +379 -204
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +10 -0
- package/package.json +2 -2
- package/src/cli-adapter-types.d.ts +2 -1
- package/src/cli-adapter-types.ts +2 -2
- package/src/cli-adapters/cli-state-engine.ts +15 -0
- package/src/cli-adapters/provider-cli-adapter.ts +32 -11
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +6 -0
- package/src/commands/cli-manager.ts +16 -2
- package/src/commands/med-family/mesh-crud.ts +9 -0
- package/src/git/git-worktree.ts +185 -3
- package/src/git/index.ts +1 -0
- package/src/mesh/mesh-event-forwarding.ts +13 -2
- package/src/mesh/mesh-queue-assignment.ts +12 -0
- package/src/mesh/mesh-reconcile-loop.ts +4 -0
- package/src/mesh/mesh-runtime-store.ts +20 -2
- package/src/mesh/mesh-work-queue.ts +23 -0
- package/src/providers/cli-provider-instance.ts +52 -8
|
@@ -566,11 +566,23 @@ export class MeshRuntimeStore {
|
|
|
566
566
|
|
|
567
567
|
/** A node may only execute one write task at a time (worktree isolation). */
|
|
568
568
|
private hasActiveNodeAssignment(meshId: string, nodeId: string): boolean {
|
|
569
|
+
// The serialization gate (claimNextQueueTask's `!nodeBusy`) must see a node as
|
|
570
|
+
// busy when ANY active row's assigned_node_id matches in ANY equivalent
|
|
571
|
+
// daemon-id form (config-form `daemon_mach_X` vs stamp-form `mach_X`, or the
|
|
572
|
+
// standalone form). A raw `assigned_node_id = ?` on a single form silently
|
|
573
|
+
// misses a form-variant assigned row, making an already-assigned node look idle
|
|
574
|
+
// and letting a second write task claim it — duplicate claim / base leak. Mirror
|
|
575
|
+
// the node-pinned SELECT below (the `target_node_id IN (...)` query): expand to
|
|
576
|
+
// every equivalent form and bind an IN (...) set so the busy gate and the
|
|
577
|
+
// candidate SELECT use the SAME matching rule.
|
|
578
|
+
const nodeIdForms = expandDaemonIdForms(nodeId);
|
|
579
|
+
if (nodeIdForms.length === 0) return false;
|
|
580
|
+
const placeholders = nodeIdForms.map(() => '?').join(', ');
|
|
569
581
|
const row = this.db.prepare(`
|
|
570
582
|
SELECT 1 FROM mesh_queue
|
|
571
|
-
WHERE mesh_id = ? AND status = 'assigned' AND assigned_node_id
|
|
583
|
+
WHERE mesh_id = ? AND status = 'assigned' AND assigned_node_id IN (${placeholders})
|
|
572
584
|
LIMIT 1
|
|
573
|
-
`).get(meshId,
|
|
585
|
+
`).get(meshId, ...nodeIdForms);
|
|
574
586
|
return row !== undefined;
|
|
575
587
|
}
|
|
576
588
|
|
|
@@ -772,6 +784,12 @@ export class MeshRuntimeStore {
|
|
|
772
784
|
// an empty session. Accept the candidate when the target resolves to the
|
|
773
785
|
// same node under ANY equivalent form; keep targetSessionId an exact match.
|
|
774
786
|
const targetMatches = (candidate: MeshWorkQueueEntry): boolean => {
|
|
787
|
+
// SESSION-ID IS SINGLE-FORM: unlike node/daemon ids (3 serialization
|
|
788
|
+
// forms requiring expandDaemonIdForms), a session id is a single
|
|
789
|
+
// canonical UUID minted once via crypto.randomUUID() in the provider
|
|
790
|
+
// instance (cli/acp/extension/ide) and carried verbatim across daemons
|
|
791
|
+
// (resolveEventSessionId applies no transformation). So an exact `!==`
|
|
792
|
+
// is correct here and needs no normalization helper.
|
|
775
793
|
if (candidate.targetSessionId && candidate.targetSessionId !== sessionId) return false;
|
|
776
794
|
if (
|
|
777
795
|
candidate.targetNodeId
|
|
@@ -7,6 +7,7 @@ import { getMesh } from '../config/mesh-config.js';
|
|
|
7
7
|
import { LOG } from '../logging/logger.js';
|
|
8
8
|
import { appendLedgerEntry } from './mesh-ledger.js';
|
|
9
9
|
import type { MeshLedgerKind } from './mesh-ledger.js';
|
|
10
|
+
import { createSessionDelivery } from './mesh-delivery-policy.js';
|
|
10
11
|
|
|
11
12
|
export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
|
|
12
13
|
export type MeshActiveTaskStatus = Extract<MeshTaskStatus, 'pending' | 'assigned'>;
|
|
@@ -836,6 +837,28 @@ export function recordDirectDispatchTask(
|
|
|
836
837
|
updatedAt: now,
|
|
837
838
|
};
|
|
838
839
|
MeshRuntimeStore.getInstance().insertQueueEntry(entry);
|
|
840
|
+
// R2 / NOTIF-DROP: a mission-attributed DIRECT dispatch (mesh_send_task) has
|
|
841
|
+
// already been handed to the transport by the time we materialise this assigned
|
|
842
|
+
// row — unlike a queue claim, there is no later delivery-confirmation write for
|
|
843
|
+
// it. Without a confirmed delivery record keyed by this taskId, the assigned-
|
|
844
|
+
// stranded watchdog (recoverStrandedAssignedDispatches → taskHasConfirmedDelivery)
|
|
845
|
+
// sees the row as never-confirmed after ASSIGNED_STRANDED_DEADLINE_MS and reclaims
|
|
846
|
+
// a task the worker already COMPLETED, dropping its agent:generating_completed
|
|
847
|
+
// (live PROBE-B repro: "never confirmed delivered → pending"). Record a confirmed
|
|
848
|
+
// delivery here so taskHasConfirmedDelivery() is true and the watchdog leaves the
|
|
849
|
+
// row to PHASE 4 completion reconcile. This point is only reached after the direct
|
|
850
|
+
// dispatch's result.success, so 'delivered' is the accurate state.
|
|
851
|
+
try {
|
|
852
|
+
createSessionDelivery({
|
|
853
|
+
meshId,
|
|
854
|
+
...(opts.assignedNodeId ? { nodeId: opts.assignedNodeId } : {}),
|
|
855
|
+
...(opts.assignedSessionId ? { sessionId: opts.assignedSessionId } : {}),
|
|
856
|
+
taskId,
|
|
857
|
+
kind: 'task',
|
|
858
|
+
message,
|
|
859
|
+
status: 'delivered',
|
|
860
|
+
});
|
|
861
|
+
} catch { /* best-effort — the assigned row is already recorded */ }
|
|
839
862
|
return entry;
|
|
840
863
|
});
|
|
841
864
|
}
|
|
@@ -62,6 +62,12 @@ type CompletedDebouncePending = {
|
|
|
62
62
|
loggedBlockReason?: string;
|
|
63
63
|
loggedTranscriptProbe?: boolean;
|
|
64
64
|
transcriptProbeHistory?: ExternalTranscriptProbe[];
|
|
65
|
+
// ARCH-REFACTOR R1: the taskId of the turn that produced this (debounced) completion,
|
|
66
|
+
// captured SYNCHRONOUSLY at the generating→idle transition. The actual completion
|
|
67
|
+
// event is emitted later by the debounce flush, by which point a follow-up task may
|
|
68
|
+
// already have started its own turn and overwritten engine.currentTurnTaskId — so the
|
|
69
|
+
// id must be snapshotted here, not re-read at flush time.
|
|
70
|
+
taskId?: string;
|
|
65
71
|
};
|
|
66
72
|
|
|
67
73
|
function isIdleStatus(value: unknown): boolean {
|
|
@@ -1641,11 +1647,29 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1641
1647
|
|| this.settings.meshNodeId || this.settings.launchedByCoordinator);
|
|
1642
1648
|
}
|
|
1643
1649
|
|
|
1650
|
+
/**
|
|
1651
|
+
* ARCH-REFACTOR R1: the taskId to attribute the CURRENTLY-completing turn to.
|
|
1652
|
+
* Prefers the per-turn binding (engine.currentTurnTaskId, set when the turn was
|
|
1653
|
+
* submitted and surviving until the next turn starts) over the last-write-wins
|
|
1654
|
+
* session scalar (settings.meshActiveTaskId). The scalar is retained only as a
|
|
1655
|
+
* backward-compat alias for the "current/last assignment" and is the source of the
|
|
1656
|
+
* NOTIF-MISDELIVER / TASK-MSG-MISROUTE race: a second task attaching before this
|
|
1657
|
+
* turn completes overwrites it. Returns undefined for a non-task ad-hoc turn.
|
|
1658
|
+
*/
|
|
1659
|
+
private completingTurnTaskId(): string | undefined {
|
|
1660
|
+
const turnTaskId = this.adapter?.currentTurnTaskId;
|
|
1661
|
+
if (typeof turnTaskId === 'string' && turnTaskId.trim()) return turnTaskId;
|
|
1662
|
+
const scalar = this.settings.meshActiveTaskId;
|
|
1663
|
+
return typeof scalar === 'string' && scalar.trim() ? scalar : undefined;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1644
1666
|
// EVTTRACE correlation context for this session's completion lifecycle. taskId is
|
|
1645
1667
|
// the primary grep anchor; instanceId is the session fallback.
|
|
1646
1668
|
private meshTraceCtx(event = 'agent:generating_completed'): Record<string, unknown> {
|
|
1647
1669
|
return {
|
|
1648
|
-
|
|
1670
|
+
// ARCH-REFACTOR R1: trace the per-turn taskId (falling back to the scalar) so
|
|
1671
|
+
// EvtTrace anchors on the same id the completion event actually carries.
|
|
1672
|
+
taskId: this.completingTurnTaskId(),
|
|
1649
1673
|
sessionId: this.instanceId,
|
|
1650
1674
|
nodeId: this.settings.meshNodeId,
|
|
1651
1675
|
meshId: this.settings.meshNodeFor,
|
|
@@ -1738,6 +1762,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1738
1762
|
chatTitle: pending.chatTitle,
|
|
1739
1763
|
duration: pending.duration,
|
|
1740
1764
|
timestamp: pending.timestamp,
|
|
1765
|
+
// ARCH-REFACTOR R1: attribute to the turn captured at idle-transition.
|
|
1766
|
+
...(pending.taskId ? { taskId: pending.taskId } : {}),
|
|
1741
1767
|
// When finalization is forced past the timeout on a `parsed_status:` block
|
|
1742
1768
|
// (the parser never confirmed a final assistant turn) we previously rode an
|
|
1743
1769
|
// empty `finalSummary` unconditionally. That empty value propagates to the
|
|
@@ -1767,6 +1793,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1767
1793
|
chatTitle: pending.chatTitle,
|
|
1768
1794
|
duration: pending.duration,
|
|
1769
1795
|
timestamp: pending.timestamp,
|
|
1796
|
+
// ARCH-REFACTOR R1: attribute to the turn captured at idle-transition.
|
|
1797
|
+
...(pending.taskId ? { taskId: pending.taskId } : {}),
|
|
1770
1798
|
finalSummary: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
1771
1799
|
});
|
|
1772
1800
|
this.completedDebouncePending = null;
|
|
@@ -2227,6 +2255,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2227
2255
|
timestamp: now,
|
|
2228
2256
|
firstObservedAt: now,
|
|
2229
2257
|
previousStatus: this.lastStatus,
|
|
2258
|
+
// ARCH-REFACTOR R1: snapshot the completing turn's taskId NOW (sync),
|
|
2259
|
+
// before any follow-up task's flush can start a new turn and move
|
|
2260
|
+
// engine.currentTurnTaskId.
|
|
2261
|
+
...(this.completingTurnTaskId() ? { taskId: this.completingTurnTaskId() } : {}),
|
|
2230
2262
|
};
|
|
2231
2263
|
const ownsExternalHistory = !!(this.adapter as any)?.chatMessagesOwnedExternally;
|
|
2232
2264
|
// (FALSEIDLE-BGCHILD-a) Native-history providers flush immediately (the
|
|
@@ -2374,17 +2406,29 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2374
2406
|
// a mesh worker session. The consumer (updateDirectDispatchStatus) was switched
|
|
2375
2407
|
// to key on task_id (CANON-B), but the producer never carried it — so every
|
|
2376
2408
|
// forwarded metadataEvent.taskId arrived undefined and the coordinator fell back
|
|
2377
|
-
// to a session_id match, which can flip a sibling dispatch row.
|
|
2378
|
-
//
|
|
2379
|
-
//
|
|
2380
|
-
//
|
|
2381
|
-
//
|
|
2382
|
-
|
|
2409
|
+
// to a session_id match, which can flip a sibling dispatch row. Surface it here so
|
|
2410
|
+
// updateDirectDispatchStatus hits the exact PK row and the session_id fallback is
|
|
2411
|
+
// never exercised. Non-mesh sessions get no taskId (regression guard) —
|
|
2412
|
+
// isMeshWorkerSession() gates the injection.
|
|
2413
|
+
//
|
|
2414
|
+
// ARCH-REFACTOR R1 (per-turn identity): resolution order is
|
|
2415
|
+
// (1) an explicit taskId already on the event — the debounce-flush completion
|
|
2416
|
+
// path stamps the taskId captured at the generating→idle transition (the
|
|
2417
|
+
// turn that actually produced this completion);
|
|
2418
|
+
// (2) the per-turn binding (engine.currentTurnTaskId) for synchronously-emitted
|
|
2419
|
+
// events whose turn is still the current one;
|
|
2420
|
+
// (3) the legacy session scalar (settings.meshActiveTaskId) as a last-resort
|
|
2421
|
+
// backward-compat alias.
|
|
2422
|
+
// The scalar is last because it is last-write-wins: a second task attaching while
|
|
2423
|
+
// this turn was still running overwrites it, which is the exact NOTIF-MISDELIVER /
|
|
2424
|
+
// TASK-MSG-MISROUTE race this refactor removes.
|
|
2425
|
+
if (this.isMeshWorkerSession()) {
|
|
2383
2426
|
const existingTaskId = typeof enrichedEvent.taskId === 'string' && enrichedEvent.taskId.trim()
|
|
2384
2427
|
? enrichedEvent.taskId
|
|
2385
2428
|
: undefined;
|
|
2386
2429
|
if (!existingTaskId) {
|
|
2387
|
-
|
|
2430
|
+
const resolved = this.completingTurnTaskId();
|
|
2431
|
+
if (resolved) enrichedEvent.taskId = resolved;
|
|
2388
2432
|
}
|
|
2389
2433
|
}
|
|
2390
2434
|
if (this.context?.emitProviderEvent) {
|