@adhdev/daemon-core 0.9.82-rc.354 → 0.9.82-rc.355
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 +571 -202
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +571 -202
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-event-trace.d.ts +21 -0
- package/dist/mesh/mesh-runtime-store.d.ts +1 -1
- package/dist/mesh/mesh-work-queue.d.ts +1 -1
- package/dist/providers/cli-provider-instance.d.ts +2 -0
- package/dist/providers/spec/adapter.d.ts +22 -0
- package/dist/providers/spec/fsm-driver.d.ts +49 -7
- package/dist/providers/spec/fsm-evaluator.d.ts +4 -0
- package/dist/providers/spec/types.d.ts +9 -5
- package/package.json +2 -2
- package/src/commands/router.ts +19 -6
- package/src/mesh/mesh-event-trace.ts +67 -0
- package/src/mesh/mesh-events-coordinator.ts +117 -12
- package/src/mesh/mesh-events-pending.ts +33 -0
- package/src/mesh/mesh-events-stale.ts +3 -1
- package/src/mesh/mesh-reconcile-loop.ts +47 -0
- package/src/mesh/mesh-runtime-store.ts +18 -2
- package/src/mesh/mesh-work-queue.ts +8 -1
- package/src/providers/cli-provider-instance.ts +69 -4
- package/src/providers/spec/adapter.ts +67 -0
- package/src/providers/spec/cli-adapter.ts +6 -0
- package/src/providers/spec/evaluator.ts +24 -9
- package/src/providers/spec/fsm-driver.ts +135 -13
- package/src/providers/spec/fsm-evaluator.ts +19 -2
- package/src/providers/spec/types.ts +9 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
2
|
import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
|
|
3
3
|
import { loadConfig } from '../config/config.js';
|
|
4
|
-
import { getMesh, getMeshByRepo } from '../config/mesh-config.js';
|
|
4
|
+
import { getMesh, getMeshByRepo, listMeshes } from '../config/mesh-config.js';
|
|
5
5
|
import { detectCLI } from '../detection/cli-detector.js';
|
|
6
6
|
import { LOG } from '../logging/logger.js';
|
|
7
7
|
import { appendLedgerEntry, buildTaskCompletionEvidence, getSessionRecoveryContext, isIntentionalCleanupStopEntry, readLedgerEntries } from './mesh-ledger.js';
|
|
@@ -15,6 +15,7 @@ import { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents } f
|
|
|
15
15
|
import type { PendingMeshCoordinatorEvent } from './mesh-events-pending.js';
|
|
16
16
|
import { resolveWorkerDelegateRouting, recordUnroutableDelegateEvent, isUnroutableDelegateRejection } from './mesh-routing.js';
|
|
17
17
|
import { enqueueUnresolvedDelegateForward, peekUnresolvedDelegateForwards, ackUnresolvedDelegateForward } from './mesh-unresolved-forward-outbox.js';
|
|
18
|
+
import { traceMeshEventStage, traceMeshEventDrop } from './mesh-event-trace.js';
|
|
18
19
|
import { getLastDisplayMessage } from '../status/snapshot.js';
|
|
19
20
|
import { resolveDelegatedWorkerAutoApprove, resolveProviderMaxParallel, resolveNodeSchedulingPriority, normalizeMeshSchedulingStrategy } from '../repo-mesh-types.js';
|
|
20
21
|
import type { RepoMeshSchedulingStrategy } from '../repo-mesh-types.js';
|
|
@@ -70,6 +71,25 @@ function getCachedMeshByWorkspace(workspace: string): any {
|
|
|
70
71
|
return mesh;
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
// Deterministic meshId recovery for a forwarded worker event that carries no meshId.
|
|
75
|
+
// An unresolved-mesh worker (forwardUnresolvedDelegateEvent) cannot resolve its own
|
|
76
|
+
// meshId locally, so it pushes the event with nodeId + workspace only and relies on
|
|
77
|
+
// the coordinator — which hosts the mesh — to recover the id. Workspace recovery
|
|
78
|
+
// (getCachedMeshByWorkspace → getMeshByRepo) is the fast path but can miss (a worktree
|
|
79
|
+
// clone whose repoIdentity differs, or a transient cache state), which left the retry
|
|
80
|
+
// permanently rejected with "meshId required". The node-id IS a stable, coordinator-side
|
|
81
|
+
// fact: scan the hosted meshes for the one whose node matches the forwarded nodeId
|
|
82
|
+
// (3-form normalizer). This is timing-independent and never depends on repo lookup.
|
|
83
|
+
function recoverMeshIdByNodeId(nodeId: string): string {
|
|
84
|
+
if (!nodeId) return '';
|
|
85
|
+
for (const mesh of listMeshes()) {
|
|
86
|
+
if (Array.isArray(mesh.nodes) && mesh.nodes.some((n: any) => meshNodeIdMatches(n, nodeId))) {
|
|
87
|
+
return readNonEmptyString(mesh.id);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return '';
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
export function __resetIdleAutoFastForwardForTests(): void {
|
|
74
94
|
idleAutoFastForwardLastAttempt.clear();
|
|
75
95
|
}
|
|
@@ -1540,6 +1560,16 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1540
1560
|
const eventSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
1541
1561
|
const eventNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
1542
1562
|
|
|
1563
|
+
// EVTTRACE correlation context for this event's coordinator-side lifecycle (queue /
|
|
1564
|
+
// dedup / suppress). Observation only — never read by any decision below.
|
|
1565
|
+
const traceCtx = {
|
|
1566
|
+
taskId: args.metadataEvent.taskId,
|
|
1567
|
+
sessionId: eventSessionId,
|
|
1568
|
+
nodeId: eventNodeId,
|
|
1569
|
+
meshId: args.meshId,
|
|
1570
|
+
event: args.event,
|
|
1571
|
+
};
|
|
1572
|
+
|
|
1543
1573
|
const sourceSession = args.sourceInstanceId
|
|
1544
1574
|
? components.instanceManager.getInstance(args.sourceInstanceId)
|
|
1545
1575
|
: undefined;
|
|
@@ -1626,6 +1656,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1626
1656
|
} catch { /* best-effort */ }
|
|
1627
1657
|
}
|
|
1628
1658
|
LOG.info('MeshEvents', `Suppressed ${args.event} for intentionally cleanup-stopped session ${eventSessionId || '(unknown session)'}`);
|
|
1659
|
+
traceMeshEventDrop('intentional_cleanup_stop', traceCtx);
|
|
1629
1660
|
return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
|
|
1630
1661
|
}
|
|
1631
1662
|
|
|
@@ -1647,6 +1678,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1647
1678
|
}
|
|
1648
1679
|
if (reconciledCompletion?.source === 'no_progress_terminal_ledger_suppression') {
|
|
1649
1680
|
LOG.info('MeshEvents', `Suppressed no-progress monitor because terminal ledger evidence already exists for session ${eventSessionId || '(unknown session)'}`);
|
|
1681
|
+
traceMeshEventDrop('no_progress_terminal_ledger_suppression', traceCtx, `terminalKind=${reconciledCompletion.terminalLedgerKind}`);
|
|
1650
1682
|
return {
|
|
1651
1683
|
success: true,
|
|
1652
1684
|
forwarded: 0,
|
|
@@ -1659,6 +1691,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1659
1691
|
|
|
1660
1692
|
if (isDuplicateRefineTerminalEvent(args.meshId, args.event, args.metadataEvent)) {
|
|
1661
1693
|
LOG.info('MeshEvents', `Suppressed duplicate ${args.event} for refine job ${readRefineJobId({ metadataEvent: args.metadataEvent })}`);
|
|
1694
|
+
traceMeshEventDrop('duplicate_refine_terminal', traceCtx);
|
|
1662
1695
|
return { success: true, forwarded: 0, suppressed: true, duplicateRefineTerminalEvent: true };
|
|
1663
1696
|
}
|
|
1664
1697
|
|
|
@@ -1674,6 +1707,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1674
1707
|
});
|
|
1675
1708
|
if (duplicateApproval) {
|
|
1676
1709
|
LOG.info('MeshEvents', `Suppressed duplicate approval event for mesh ${args.meshId} session ${eventSessionId}`);
|
|
1710
|
+
traceMeshEventDrop('duplicate_approval', traceCtx);
|
|
1677
1711
|
return { success: true, forwarded: 0, suppressed: true, duplicateApproval: true };
|
|
1678
1712
|
}
|
|
1679
1713
|
}
|
|
@@ -1706,6 +1740,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1706
1740
|
|| args.metadataEvent.source === 'no_progress_reconciliation'
|
|
1707
1741
|
) {
|
|
1708
1742
|
LOG.info('MeshEvents', `Suppressed duplicate completion with existing terminal ledger evidence for mesh ${args.meshId} session ${eventSessionId}`);
|
|
1743
|
+
traceMeshEventDrop('duplicate_completion_terminal_ledger', traceCtx);
|
|
1709
1744
|
return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true, terminalLedgerEvidence: true };
|
|
1710
1745
|
}
|
|
1711
1746
|
}
|
|
@@ -1724,6 +1759,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1724
1759
|
});
|
|
1725
1760
|
if (duplicateCompletion) {
|
|
1726
1761
|
LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
|
|
1762
|
+
traceMeshEventDrop('duplicate_completion', traceCtx);
|
|
1727
1763
|
return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true };
|
|
1728
1764
|
}
|
|
1729
1765
|
}
|
|
@@ -1742,6 +1778,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1742
1778
|
});
|
|
1743
1779
|
if (duplicateStopped) {
|
|
1744
1780
|
LOG.info('MeshEvents', `Suppressed duplicate stopped event for mesh ${args.meshId} session ${eventSessionId}`);
|
|
1781
|
+
traceMeshEventDrop('duplicate_stopped', traceCtx);
|
|
1745
1782
|
return { success: true, forwarded: 0, suppressed: true, duplicateStopped: true };
|
|
1746
1783
|
}
|
|
1747
1784
|
}
|
|
@@ -1762,7 +1799,9 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1762
1799
|
// genuine evidence, is marked terminal as before.
|
|
1763
1800
|
const leaveDirectDispatchActive = !task && opts?.tentativeIfDirect === true;
|
|
1764
1801
|
if (!leaveDirectDispatchActive) {
|
|
1765
|
-
|
|
1802
|
+
// CANON-B: flip the exact dispatch row the completion echoed its taskId for; the
|
|
1803
|
+
// session_id fallback (no echoed taskId) still covers legacy/relayed workers.
|
|
1804
|
+
updateDirectDispatchStatus(args.meshId, sessionId, outcome, eventTaskId);
|
|
1766
1805
|
}
|
|
1767
1806
|
markSessionDeliveriesTerminal(args.meshId, sessionId, outcome);
|
|
1768
1807
|
setImmediate(() => cleanupTerminalDirectDispatches());
|
|
@@ -1779,7 +1818,10 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1779
1818
|
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
1780
1819
|
|
|
1781
1820
|
if (sessionId) {
|
|
1782
|
-
|
|
1821
|
+
// CANON-B: trust the taskId the completion echoed; only fall back to the
|
|
1822
|
+
// most-recent-by-session heuristic when the worker carried none.
|
|
1823
|
+
directDispatchTaskIdForLedger = readNonEmptyString(args.metadataEvent.taskId)
|
|
1824
|
+
|| resolveActiveDirectDispatchTaskId(args.meshId, sessionId);
|
|
1783
1825
|
// A false-idle completion of a direct dispatch is recorded but kept tentative (the
|
|
1784
1826
|
// dispatch row stays active for the reconcile fallback); a genuine completion is terminal.
|
|
1785
1827
|
const isFalseIdle = isFalseIdleCompletion(args.metadataEvent);
|
|
@@ -1867,12 +1909,21 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1867
1909
|
} catch { /* best-effort */ }
|
|
1868
1910
|
}
|
|
1869
1911
|
if (sessionId) {
|
|
1870
|
-
|
|
1871
|
-
|
|
1912
|
+
// CANON-B: a generating_started that echoes its taskId acks exactly the dispatch
|
|
1913
|
+
// and the delivery for THAT task — not every in-flight dispatch/delivery on the
|
|
1914
|
+
// session. A session that already holds a freshly-dispatched (still 'dispatched')
|
|
1915
|
+
// sibling must keep that row 'dispatched' so its own confirm can match it; acking
|
|
1916
|
+
// by session would mark it 'acked' prematurely and hide a genuine non-delivery.
|
|
1917
|
+
const startedTaskId = readNonEmptyString(args.metadataEvent.taskId) || undefined;
|
|
1918
|
+
updateDirectDispatchStatus(args.meshId, sessionId, 'acked', startedTaskId);
|
|
1919
|
+
const activeDeliveries = ((): { id: string; taskId: string | null }[] => {
|
|
1872
1920
|
try { return MeshRuntimeStore.getInstance().getActiveSessionDeliveries(args.meshId, sessionId); }
|
|
1873
1921
|
catch { return []; }
|
|
1874
1922
|
})();
|
|
1875
|
-
|
|
1923
|
+
const deliveriesToAck = startedTaskId
|
|
1924
|
+
? activeDeliveries.filter(d => d.taskId === startedTaskId)
|
|
1925
|
+
: activeDeliveries;
|
|
1926
|
+
for (const d of deliveriesToAck) {
|
|
1876
1927
|
updateSessionDeliveryStatus(d.id, 'acked');
|
|
1877
1928
|
}
|
|
1878
1929
|
}
|
|
@@ -1885,7 +1936,9 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1885
1936
|
} catch { /* best-effort */ }
|
|
1886
1937
|
}
|
|
1887
1938
|
if (sessionId) {
|
|
1888
|
-
|
|
1939
|
+
// CANON-B: prefer the echoed taskId; session heuristic is the fallback.
|
|
1940
|
+
directDispatchTaskIdForLedger = readNonEmptyString(args.metadataEvent.taskId)
|
|
1941
|
+
|| resolveActiveDirectDispatchTaskId(args.meshId, sessionId);
|
|
1889
1942
|
completedTaskForLedger = markSessionTerminal(sessionId, 'failed');
|
|
1890
1943
|
}
|
|
1891
1944
|
}
|
|
@@ -2081,6 +2134,11 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
2081
2134
|
};
|
|
2082
2135
|
if (queuePendingMeshCoordinatorEvent(pendingEvent)) {
|
|
2083
2136
|
LOG.info('MeshEvents', `Queued ${args.event} for coordinator (mesh ${args.meshId}${workerCoordinatorDaemonId ? `, coordinator daemon ${workerCoordinatorDaemonId}` : ''}${workerCoordinatorSessionId ? `, coordinator session ${workerCoordinatorSessionId}` : ''})`);
|
|
2137
|
+
// EVTTRACE: event persisted to the coordinator pending queue (awaiting reconcile drain).
|
|
2138
|
+
traceMeshEventStage('queued', traceCtx, workerCoordinatorDaemonId ? `coordinatorDaemon=${workerCoordinatorDaemonId}` : 'broadcast');
|
|
2139
|
+
} else {
|
|
2140
|
+
// EVTTRACE: queue rejected the event (dedup at queue time / persistence guard).
|
|
2141
|
+
traceMeshEventDrop('queue_dedup', traceCtx);
|
|
2084
2142
|
}
|
|
2085
2143
|
return { success: true, forwarded: 0 };
|
|
2086
2144
|
}
|
|
@@ -2094,12 +2152,37 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
|
|
|
2094
2152
|
const workspace = readNonEmptyString(payload.workspace);
|
|
2095
2153
|
|
|
2096
2154
|
// The fallback worker-forward path (forwardUnresolvedDelegateEvent) cannot resolve a
|
|
2097
|
-
// mesh id locally on the remote worker, so it forwards the event with
|
|
2098
|
-
// The coordinator hosting the mesh CAN resolve it
|
|
2099
|
-
//
|
|
2155
|
+
// mesh id locally on the remote worker, so it forwards the event with nodeId +
|
|
2156
|
+
// workspace only. The coordinator hosting the mesh CAN resolve it. Two recovery
|
|
2157
|
+
// paths, in order:
|
|
2158
|
+
// 1) workspace → mesh (fast path; cached repoIdentity lookup), then
|
|
2159
|
+
// 2) nodeId → mesh (deterministic backstop; scans hosted meshes for the node).
|
|
2160
|
+
// Workspace recovery alone was unreliable — a worktree clone whose repoIdentity
|
|
2161
|
+
// differs, or a transient cache miss, left the reconcile retry permanently rejected
|
|
2162
|
+
// ("meshId required") so the worker's completion never surfaced to the coordinator.
|
|
2163
|
+
// The nodeId is a stable coordinator-side fact and resolves timing-independently.
|
|
2100
2164
|
const meshId = readNonEmptyString(payload.meshId)
|
|
2101
|
-
|| (workspace ? readNonEmptyString(getCachedMeshByWorkspace(workspace)?.id) : '')
|
|
2102
|
-
|
|
2165
|
+
|| (workspace ? readNonEmptyString(getCachedMeshByWorkspace(workspace)?.id) : '')
|
|
2166
|
+
|| recoverMeshIdByNodeId(nodeId);
|
|
2167
|
+
if (!meshId) {
|
|
2168
|
+
// EVTTRACE: forwarded event rejected at receive — no meshId could be resolved
|
|
2169
|
+
// (no payload.meshId, no workspace→mesh, no nodeId→mesh). Observation only.
|
|
2170
|
+
traceMeshEventDrop('meshId_required', {
|
|
2171
|
+
taskId: payload.taskId,
|
|
2172
|
+
sessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId),
|
|
2173
|
+
nodeId,
|
|
2174
|
+
event: eventName,
|
|
2175
|
+
}, workspace ? `workspace=${workspace} unresolved` : 'no workspace/nodeId');
|
|
2176
|
+
return { success: false, error: 'meshId required' };
|
|
2177
|
+
}
|
|
2178
|
+
// EVTTRACE: forwarded event accepted at receive (meshId resolved).
|
|
2179
|
+
traceMeshEventStage('received', {
|
|
2180
|
+
taskId: payload.taskId,
|
|
2181
|
+
sessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId),
|
|
2182
|
+
nodeId,
|
|
2183
|
+
meshId,
|
|
2184
|
+
event: eventName,
|
|
2185
|
+
});
|
|
2103
2186
|
const nodeLabel = nodeId ? `Node '${nodeId}'` : workspace ? `Agent at ${workspace}` : 'Remote agent';
|
|
2104
2187
|
const relayModalMessage = readNonEmptyString(payload.modalMessage);
|
|
2105
2188
|
const relayModalButtons = Array.isArray(payload.modalButtons)
|
|
@@ -2235,13 +2318,24 @@ function forwardUnresolvedDelegateEvent(
|
|
|
2235
2318
|
// push below (degrades to the old at-most-once behaviour rather than dropping
|
|
2236
2319
|
// the chance entirely).
|
|
2237
2320
|
const persisted = enqueueUnresolvedDelegateForward(coordinatorDaemonId, eventName, payload);
|
|
2321
|
+
// EVTTRACE: unresolved-mesh worker persisted its completion to the outbox (no meshId
|
|
2322
|
+
// available locally; coordinator will recover it on receive).
|
|
2323
|
+
const fwdTraceCtx = {
|
|
2324
|
+
taskId: (payload as Record<string, unknown>).taskId,
|
|
2325
|
+
sessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId),
|
|
2326
|
+
nodeId: readNonEmptyString(routing.nodeId) || readNonEmptyString(event.meshNodeId),
|
|
2327
|
+
event: eventName,
|
|
2328
|
+
};
|
|
2329
|
+
traceMeshEventStage('outbox_enqueue', fwdTraceCtx, `coordinatorDaemon=${coordinatorDaemonId} meshId=absent`);
|
|
2238
2330
|
|
|
2239
2331
|
// 2) Best-effort immediate push for low latency. On success, ack the outbox row so
|
|
2240
2332
|
// the retry loop won't re-send it. On failure, leave it queued — PHASE 0 retries.
|
|
2333
|
+
traceMeshEventStage('forward_send', fwdTraceCtx, 'immediate push');
|
|
2241
2334
|
Promise.resolve(components.dispatchMeshCommand(coordinatorDaemonId, 'mesh_forward_event', payload))
|
|
2242
2335
|
.then((result: any) => {
|
|
2243
2336
|
if (result && result.success === false) {
|
|
2244
2337
|
LOG.warn('MeshEvents', `Immediate forward of ${eventName} to coordinator ${coordinatorDaemonId} rejected (${readNonEmptyString(result.error) || 'no reason'}) — left queued for retry`);
|
|
2338
|
+
traceMeshEventDrop('immediate_forward_rejected', fwdTraceCtx, readNonEmptyString(result.error) || 'no reason');
|
|
2245
2339
|
return;
|
|
2246
2340
|
}
|
|
2247
2341
|
// Acked. Mark the durable copy delivered so the retry loop skips it.
|
|
@@ -2368,6 +2462,17 @@ export function setupMeshEventForwarding(components: DaemonComponents) {
|
|
|
2368
2462
|
// silently. Leave a fail-loud diagnostic so the missing completion is traceable.
|
|
2369
2463
|
// Benign non-delegate rejections (not_cli / no_workspace / etc.) are no-ops inside
|
|
2370
2464
|
// recordUnroutableDelegateEvent.
|
|
2465
|
+
// EVTTRACE: a delegate event that could not be routed AND could not be
|
|
2466
|
+
// fallback-forwarded (no coordinator anchor). Only mesh_unresolved is a real
|
|
2467
|
+
// drop; the benign non-delegate rejections are ordinary non-mesh traffic.
|
|
2468
|
+
if (isUnroutableDelegateRejection(routing)) {
|
|
2469
|
+
traceMeshEventDrop('unroutable', {
|
|
2470
|
+
taskId: (event as Record<string, unknown>).meshActiveTaskId ?? (event as Record<string, unknown>).taskId,
|
|
2471
|
+
sessionId: routing.sessionId,
|
|
2472
|
+
nodeId: routing.nodeId,
|
|
2473
|
+
event: event.event,
|
|
2474
|
+
}, 'no coordinator anchor / mesh_unresolved');
|
|
2475
|
+
}
|
|
2371
2476
|
recordUnroutableDelegateEvent(routing, event.event);
|
|
2372
2477
|
return;
|
|
2373
2478
|
}
|
|
@@ -88,6 +88,25 @@ function hasPendingRefineTerminalEventDuplicate(event: PendingMeshCoordinatorEve
|
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// CANON-B / DUPNOTIF: terminal completion events that the coordinator surfaces as a
|
|
92
|
+
// notification. The native completion path (handleMeshCoordinatorEvent) and the transcript
|
|
93
|
+
// reconciliation fallback (reconcileDirectDispatchCompletionFromTranscript) BOTH queue one of
|
|
94
|
+
// these for the same finished task — with DIFFERENT timestamps — so a timestamp-bearing
|
|
95
|
+
// fingerprint lets both surface and the coordinator notifies twice. When the event carries a
|
|
96
|
+
// taskId we anchor the fingerprint on the taskId (dropping the timestamp), collapsing the two
|
|
97
|
+
// paths into a single surface. A weakness marker keeps a tentative false-idle completion
|
|
98
|
+
// distinct from the genuine completion that supersedes it, so the genuine one is never
|
|
99
|
+
// swallowed by the earlier weak one.
|
|
100
|
+
const TERMINAL_COMPLETION_EVENTS = new Set(['agent:generating_completed', 'agent:stopped']);
|
|
101
|
+
|
|
102
|
+
function isWeakCompletionMetadata(metadata: Record<string, unknown>): boolean {
|
|
103
|
+
const evidenceLevel = readNonEmptyString(metadata.evidenceLevel);
|
|
104
|
+
if (evidenceLevel === 'insufficient' || evidenceLevel === 'weak') return true;
|
|
105
|
+
if (metadata.reviewRecommended === true) return true;
|
|
106
|
+
const diag = readRecord(metadata.completionDiagnostic);
|
|
107
|
+
return diag?.finalAssistantPresent === false || diag?.blockReason === 'missing_final_assistant';
|
|
108
|
+
}
|
|
109
|
+
|
|
91
110
|
export function buildPendingEventFingerprint(event: PendingMeshCoordinatorEvent): string {
|
|
92
111
|
const metadata = readRecord(event.metadataEvent) || {};
|
|
93
112
|
// Bootstrap events are node-scoped: dedup by meshId+event+nodeId only.
|
|
@@ -96,6 +115,20 @@ export function buildPendingEventFingerprint(event: PendingMeshCoordinatorEvent)
|
|
|
96
115
|
if (event.event === 'worktree_bootstrap_complete' || event.event === 'worktree_bootstrap_failed') {
|
|
97
116
|
return [event.meshId, event.event, event.nodeId || ''].join('::');
|
|
98
117
|
}
|
|
118
|
+
// DUPNOTIF: a terminal completion carrying a taskId is deduped by taskId (+ weakness),
|
|
119
|
+
// NOT by timestamp — the native and transcript-reconciliation paths timestamp the same
|
|
120
|
+
// completion differently, and only taskId is stable across both.
|
|
121
|
+
if (TERMINAL_COMPLETION_EVENTS.has(event.event)) {
|
|
122
|
+
const terminalTaskId = readNonEmptyString(metadata.taskId) || readNonEmptyString(readRecord(metadata.payload)?.taskId);
|
|
123
|
+
if (terminalTaskId) {
|
|
124
|
+
return [
|
|
125
|
+
event.meshId,
|
|
126
|
+
event.event,
|
|
127
|
+
terminalTaskId,
|
|
128
|
+
isWeakCompletionMetadata(metadata) ? 'weak' : 'genuine',
|
|
129
|
+
].join('::');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
99
132
|
const sessionId = resolveEventSessionId(metadata);
|
|
100
133
|
const providerSessionId = readNonEmptyString(metadata.providerSessionId);
|
|
101
134
|
const taskId = readNonEmptyString(metadata.taskId) || readNonEmptyString(readRecord(metadata.payload)?.taskId);
|
|
@@ -223,7 +223,9 @@ export function reconcileDirectDispatchCompletionFromTranscript(args: {
|
|
|
223
223
|
evidence,
|
|
224
224
|
},
|
|
225
225
|
});
|
|
226
|
-
|
|
226
|
+
// CANON-B: this reconcile path always knows the exact taskId — flip that dispatch row, not
|
|
227
|
+
// whichever sibling the session_id happens to match.
|
|
228
|
+
updateDirectDispatchStatus(args.meshId, args.sessionId, kind === 'task_completed' ? 'completed' : 'failed', args.taskId);
|
|
227
229
|
markSessionDeliveriesTerminal(args.meshId, args.sessionId, kind === 'task_completed' ? 'completed' : 'failed');
|
|
228
230
|
setImmediate(() => cleanupTerminalDirectDispatches());
|
|
229
231
|
queuePendingMeshCoordinatorEvent({
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
expireStaleUnresolvedDelegateForwards,
|
|
57
57
|
} from './mesh-unresolved-forward-outbox.js';
|
|
58
58
|
import { readNonEmptyString, readMeshCompletionSummary } from './mesh-events-utils.js';
|
|
59
|
+
import { traceMeshEventStage, traceMeshEventDrop } from './mesh-event-trace.js';
|
|
59
60
|
import { expandDaemonIdForms } from '@adhdev/mesh-shared';
|
|
60
61
|
import { getActiveDirectDispatches, getQueue, reclaimStrandedAssignedTask } from './mesh-work-queue.js';
|
|
61
62
|
import { readLedgerEntries } from './mesh-ledger.js';
|
|
@@ -230,6 +231,15 @@ function injectPendingIntoCoordinator(
|
|
|
230
231
|
): void {
|
|
231
232
|
if (!coordinator || !pending.coordinatorMessage) return;
|
|
232
233
|
const force = shouldForceInjectMeshEvent(pending.event);
|
|
234
|
+
// EVTTRACE: event surfaced to the coordinator (injected into its live CLI session).
|
|
235
|
+
// This is the terminal happy-path stage. Observation only.
|
|
236
|
+
traceMeshEventStage('surfaced', {
|
|
237
|
+
taskId: pending.metadataEvent?.taskId,
|
|
238
|
+
sessionId: pending.metadataEvent?.targetSessionId ?? pending.targetCoordinatorSessionId,
|
|
239
|
+
nodeId: pending.nodeId,
|
|
240
|
+
meshId: pending.meshId,
|
|
241
|
+
event: pending.event,
|
|
242
|
+
}, force ? 'force-inject' : 'inject');
|
|
233
243
|
coordinator.onEvent('send_message', {
|
|
234
244
|
input: { text: pending.coordinatorMessage, textFallback: pending.coordinatorMessage },
|
|
235
245
|
...(force ? { force: true } : {}),
|
|
@@ -357,6 +367,16 @@ function recoverStrandedAssignedDispatches(meshId: string, store: MeshRuntimeSto
|
|
|
357
367
|
LOG.warn('MeshReconcile', `Reclaimed stranded assigned task ${row.id} on mesh ${meshId} `
|
|
358
368
|
+ `(node=${row.assignedNodeId ?? '?'} session=${row.assignedSessionId ?? '?'}, dispatched `
|
|
359
369
|
+ `${Math.round((nowMs - dispatchedAtMs) / 1000)}s ago, never confirmed delivered → ${reclaimed.status})`);
|
|
370
|
+
// EVTTRACE: the dispatch for this task was stranded (assigned, never confirmed
|
|
371
|
+
// delivered) and reclaimed (CANON-B) — its expected completion event never
|
|
372
|
+
// arrived. Observation only; the reclaim decision above is unchanged.
|
|
373
|
+
traceMeshEventDrop('assigned_stranded_reclaim', {
|
|
374
|
+
taskId: row.id,
|
|
375
|
+
sessionId: row.assignedSessionId,
|
|
376
|
+
nodeId: row.assignedNodeId,
|
|
377
|
+
meshId,
|
|
378
|
+
event: 'agent:generating_completed',
|
|
379
|
+
}, `unconfirmed ${Math.round((nowMs - dispatchedAtMs) / 1000)}s → ${reclaimed.status}`);
|
|
360
380
|
}
|
|
361
381
|
}
|
|
362
382
|
}
|
|
@@ -652,6 +672,15 @@ function holdOrExpireStrictUnmatchedEvent(
|
|
|
652
672
|
try {
|
|
653
673
|
queuePendingMeshCoordinatorEvent(pending); // preserves queuedAt → true age retained
|
|
654
674
|
LOG.info('MeshReconcile', `Strict route hold: coordinator session ${wantSession} not live on mesh ${meshId} — re-queued (${pending.event})`);
|
|
675
|
+
// EVTTRACE: event held (re-queued) — its originating coordinator session is not
|
|
676
|
+
// currently deliverable. Held, not dropped; surfaces later or expires past TTL.
|
|
677
|
+
traceMeshEventDrop('strict_route_hold', {
|
|
678
|
+
taskId: pending.metadataEvent?.taskId,
|
|
679
|
+
sessionId: pending.metadataEvent?.targetSessionId ?? wantSession,
|
|
680
|
+
nodeId: pending.nodeId,
|
|
681
|
+
meshId,
|
|
682
|
+
event: pending.event,
|
|
683
|
+
}, `coordinatorSession=${wantSession} not live`);
|
|
655
684
|
} catch (e: any) {
|
|
656
685
|
LOG.warn('MeshReconcile', `Strict route re-queue failed for ${pending.event} on mesh ${meshId}: ${e?.message || e}`);
|
|
657
686
|
}
|
|
@@ -675,6 +704,14 @@ function holdOrExpireStrictUnmatchedEvent(
|
|
|
675
704
|
},
|
|
676
705
|
});
|
|
677
706
|
LOG.warn('MeshReconcile', `Strict route expire: coordinator session ${wantSession} never returned for mesh ${meshId} — recorded to ledger (recoverable), dropped (${pending.event})`);
|
|
707
|
+
// EVTTRACE: event expired past the strict-route TTL — dropped (recoverable, ledgered).
|
|
708
|
+
traceMeshEventDrop('strict_route_expired', {
|
|
709
|
+
taskId: pending.metadataEvent?.taskId,
|
|
710
|
+
sessionId: pending.metadataEvent?.targetSessionId ?? wantSession,
|
|
711
|
+
nodeId: pending.nodeId,
|
|
712
|
+
meshId,
|
|
713
|
+
event: pending.event,
|
|
714
|
+
}, `coordinatorSession=${wantSession} never returned`);
|
|
678
715
|
} catch (e: any) {
|
|
679
716
|
LOG.warn('MeshReconcile', `Failed to ledger-expire strict-unmatched ${pending.event} for mesh ${meshId}: ${e?.message || e}`);
|
|
680
717
|
}
|
|
@@ -698,16 +735,26 @@ async function retryUnresolvedDelegateForwards(components: DaemonComponents): Pr
|
|
|
698
735
|
if (entries.length === 0) return;
|
|
699
736
|
|
|
700
737
|
for (const entry of entries) {
|
|
738
|
+
// EVTTRACE correlation context for this outbox entry's retry.
|
|
739
|
+
const entryTraceCtx = {
|
|
740
|
+
taskId: (entry.payload as Record<string, unknown>).taskId,
|
|
741
|
+
sessionId: readNonEmptyString(entry.payload.targetSessionId) || readNonEmptyString(entry.payload.sessionId),
|
|
742
|
+
nodeId: readNonEmptyString(entry.payload.nodeId),
|
|
743
|
+
event: readNonEmptyString(entry.payload.event),
|
|
744
|
+
};
|
|
701
745
|
let result: any;
|
|
702
746
|
try {
|
|
747
|
+
traceMeshEventStage('forward_send', entryTraceCtx, `retry → ${entry.coordinatorDaemonId}`);
|
|
703
748
|
result = await dispatchMeshCommand(entry.coordinatorDaemonId, 'mesh_forward_event', entry.payload);
|
|
704
749
|
} catch (e: any) {
|
|
705
750
|
// Coordinator unreachable — keep the entry queued and try again next tick.
|
|
706
751
|
LOG.warn('MeshReconcile', `Retry forward to coordinator ${entry.coordinatorDaemonId} failed: ${e?.message || e} — left queued`);
|
|
752
|
+
traceMeshEventDrop('retry_forward_failed', entryTraceCtx, e?.message || String(e));
|
|
707
753
|
continue;
|
|
708
754
|
}
|
|
709
755
|
if (result && result.success === false) {
|
|
710
756
|
LOG.warn('MeshReconcile', `Retry forward to coordinator ${entry.coordinatorDaemonId} rejected (${readNonEmptyString(result.error) || 'no reason'}) — left queued`);
|
|
757
|
+
traceMeshEventDrop('retry_forward_rejected', entryTraceCtx, readNonEmptyString(result.error) || 'no reason');
|
|
711
758
|
continue;
|
|
712
759
|
}
|
|
713
760
|
// Acked — mark the durable copy delivered.
|
|
@@ -887,9 +887,25 @@ export class MeshRuntimeStore {
|
|
|
887
887
|
}));
|
|
888
888
|
}
|
|
889
889
|
|
|
890
|
-
|
|
891
|
-
|
|
890
|
+
// CANON-B (dispatch identity): mesh_direct_dispatches is keyed by task_id (PK), but a
|
|
891
|
+
// single session can host several sequential direct dispatches (re-dispatch / nudge), so
|
|
892
|
+
// matching a status flip by session_id alone hits EVERY non-terminal row for that session
|
|
893
|
+
// — flipping a sibling task's row and stranding the one whose event actually fired (the
|
|
894
|
+
// assigned-stranded watchdog then requeues a task that is really still generating). When
|
|
895
|
+
// the firing event carries a taskId, target the single PK row; the session_id match is the
|
|
896
|
+
// legacy fallback only for events that arrive without a taskId.
|
|
897
|
+
updateDirectDispatchStatus(meshId: string, sessionId: string, status: 'acked' | 'completed' | 'failed' | 'stale', taskId?: string): void {
|
|
892
898
|
const now = new Date().toISOString();
|
|
899
|
+
if (taskId) {
|
|
900
|
+
this.db.prepare(`
|
|
901
|
+
UPDATE mesh_direct_dispatches
|
|
902
|
+
SET status = @status, updated_at = @updatedAt
|
|
903
|
+
WHERE mesh_id = @meshId AND task_id = @taskId
|
|
904
|
+
AND status NOT IN ('completed', 'failed')
|
|
905
|
+
`).run({ status, meshId, taskId, updatedAt: now });
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (!sessionId) return; // never update rows without a session binding
|
|
893
909
|
this.db.prepare(`
|
|
894
910
|
UPDATE mesh_direct_dispatches
|
|
895
911
|
SET status = @status, updated_at = @updatedAt
|
|
@@ -1140,9 +1140,16 @@ export function updateDirectDispatchStatus(
|
|
|
1140
1140
|
meshId: string,
|
|
1141
1141
|
sessionId: string,
|
|
1142
1142
|
status: 'acked' | 'completed' | 'failed' | 'stale',
|
|
1143
|
+
taskId?: string,
|
|
1143
1144
|
): void {
|
|
1144
1145
|
try {
|
|
1145
|
-
|
|
1146
|
+
// CANON-B: prefer the exact task_id row; fall back to the session_id match only when
|
|
1147
|
+
// the firing event carried no taskId (a legacy/relayed event). Warn on the fallback so
|
|
1148
|
+
// the residual PK-substitute path is observable when it strands a sibling dispatch.
|
|
1149
|
+
if (!taskId) {
|
|
1150
|
+
LOG.warn('MeshQueue', `updateDirectDispatchStatus(${status}) for mesh ${meshId} session ${sessionId} has no taskId — falling back to session_id match (may flip a sibling dispatch row)`);
|
|
1151
|
+
}
|
|
1152
|
+
MeshRuntimeStore.getInstance().updateDirectDispatchStatus(meshId, sessionId, status, taskId);
|
|
1146
1153
|
} catch { /* best-effort */ }
|
|
1147
1154
|
}
|
|
1148
1155
|
|
|
@@ -21,6 +21,7 @@ import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pt
|
|
|
21
21
|
import { StatusMonitor } from './status-monitor.js';
|
|
22
22
|
import { ChatHistoryWriter, isNativeSourceCanonicalHistory, materializeProviderNativeHistory, readChatHistory, readProviderChatHistory } from '../config/chat-history.js';
|
|
23
23
|
import { LOG } from '../logging/logger.js';
|
|
24
|
+
import { traceMeshEventStage, traceMeshEventDrop } from '../mesh/mesh-event-trace.js';
|
|
24
25
|
import type { ChatMessage } from '../types.js';
|
|
25
26
|
import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
|
|
26
27
|
import { formatAutoApprovalMessage, pickApprovalButton, pickAutoApprovalButton, looksLikeActiveApprovalPromptText } from './approval-utils.js';
|
|
@@ -1411,6 +1412,26 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1411
1412
|
this.completedDebounceTimer = setTimeout(() => this.flushCompletedDebounceIfFinalized(), delayMs);
|
|
1412
1413
|
}
|
|
1413
1414
|
|
|
1415
|
+
// EVTTRACE (observation-only): is this a mesh worker session whose completion
|
|
1416
|
+
// events must route to a coordinator? Used purely to gate trace logging so a
|
|
1417
|
+
// non-mesh CLI session's completions don't add EvtTrace noise. No decision logic.
|
|
1418
|
+
private isMeshWorkerSession(): boolean {
|
|
1419
|
+
return !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId
|
|
1420
|
+
|| this.settings.meshNodeId || this.settings.launchedByCoordinator);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// EVTTRACE correlation context for this session's completion lifecycle. taskId is
|
|
1424
|
+
// the primary grep anchor; instanceId is the session fallback.
|
|
1425
|
+
private meshTraceCtx(event = 'agent:generating_completed'): Record<string, unknown> {
|
|
1426
|
+
return {
|
|
1427
|
+
taskId: this.settings.meshActiveTaskId,
|
|
1428
|
+
sessionId: this.instanceId,
|
|
1429
|
+
nodeId: this.settings.meshNodeId,
|
|
1430
|
+
meshId: this.settings.meshNodeFor,
|
|
1431
|
+
event,
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1414
1435
|
private flushCompletedDebounceIfFinalized(): void {
|
|
1415
1436
|
const pending = this.completedDebouncePending;
|
|
1416
1437
|
if (!pending) {
|
|
@@ -1433,24 +1454,55 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1433
1454
|
if (block) {
|
|
1434
1455
|
const blockReason = block.reason;
|
|
1435
1456
|
const waitedMs = Date.now() - pending.firstObservedAt;
|
|
1436
|
-
|
|
1437
|
-
|
|
1457
|
+
// CANON-C (completion-gate decouple): a block carrying `allowTimeout` is the
|
|
1458
|
+
// transcript-evidence gate — the worker FSM has ALREADY reached idle and the only
|
|
1459
|
+
// thing missing is the append-only transcript's final assistant turn (a native-source
|
|
1460
|
+
// race: claude-cli owns its history externally and the file write trails the idle
|
|
1461
|
+
// transition). `allowTimeout` is set ONLY on the missing_final_assistant block, and
|
|
1462
|
+
// ONLY for mesh worker sessions (meshNodeFor / meshActiveTaskId / launchedByCoordinator).
|
|
1463
|
+
// The coordinator's sole path to learn this session is idle is agent:generating_completed,
|
|
1464
|
+
// so holding it up to COMPLETED_FINALIZATION_MAX_WAIT_MS (30s) leaves the coordinator
|
|
1465
|
+
// false-generating while the worker is done. Decouple the idle NOTIFICATION from the
|
|
1466
|
+
// transcript evidence: emit the completion immediately, marked weak
|
|
1467
|
+
// (completionDiagnostic.blockReason=missing_final_assistant, finalAssistantPresent=false).
|
|
1468
|
+
// The finalSummary is enriched on a SEPARATE path — the mesh reconcile loop reads the
|
|
1469
|
+
// transcript once written and re-emits a GENUINE completion (CANON-B weak→genuine
|
|
1470
|
+
// upgrade; buildPendingEventFingerprint keeps weak and genuine distinct so the enriched
|
|
1471
|
+
// one still surfaces, and isFalseIdleCompletion keeps the direct dispatch active until
|
|
1472
|
+
// then). All OTHER blocks (genuinely-busy adapter/partial/parsed states, transient
|
|
1473
|
+
// parse_error) keep the existing terminal-hold / 30s-retry behavior unchanged.
|
|
1474
|
+
const isTranscriptEvidenceGate = block.allowTimeout === true;
|
|
1475
|
+
LOG.debug('CLI', `[${this.type}] finalization block: reason=${blockReason} terminal=${block.terminal} allowTimeout=${isTranscriptEvidenceGate} waitedMs=${waitedMs} maxWait=${COMPLETED_FINALIZATION_MAX_WAIT_MS}`);
|
|
1476
|
+
if (!isTranscriptEvidenceGate && (block.terminal || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS)) {
|
|
1438
1477
|
if (pending.loggedBlockReason !== blockReason) {
|
|
1439
1478
|
LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
|
|
1479
|
+
// EVTTRACE: completion held by the finalization gate (CANON-C). Observation
|
|
1480
|
+
// only — does not change the hold decision above.
|
|
1481
|
+
if (this.isMeshWorkerSession()) {
|
|
1482
|
+
traceMeshEventDrop('completion_gate_hold', this.meshTraceCtx(), `${blockReason} waited=${waitedMs}ms`);
|
|
1483
|
+
}
|
|
1440
1484
|
pending.loggedBlockReason = blockReason;
|
|
1441
1485
|
}
|
|
1442
1486
|
this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
|
|
1443
1487
|
return;
|
|
1444
1488
|
}
|
|
1489
|
+
const emittedAfterFinalizationTimeout = waitedMs >= COMPLETED_FINALIZATION_MAX_WAIT_MS;
|
|
1445
1490
|
const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
|
|
1446
1491
|
blockReason,
|
|
1447
1492
|
latestStatus,
|
|
1448
1493
|
latestVisibleStatus,
|
|
1449
1494
|
waitedMs,
|
|
1450
1495
|
pending,
|
|
1451
|
-
emittedAfterFinalizationTimeout
|
|
1496
|
+
emittedAfterFinalizationTimeout,
|
|
1452
1497
|
});
|
|
1453
|
-
|
|
1498
|
+
// Surface the CANON-C immediate-emit path distinctly so a delegated worker's idle
|
|
1499
|
+
// notification (transcript still pending) is not mistaken for a 30s-timeout fallback.
|
|
1500
|
+
(completionDiagnostic as Record<string, unknown>).decoupledImmediateEmit = isTranscriptEvidenceGate && !emittedAfterFinalizationTimeout;
|
|
1501
|
+
LOG.warn('CLI', `[${this.type}] emitting completed event (${isTranscriptEvidenceGate && !emittedAfterFinalizationTimeout ? 'CANON-C decoupled-immediate, transcript pending' : `after ${waitedMs}ms`}) without finalized assistant turn (${blockReason})`);
|
|
1502
|
+
// EVTTRACE: completion fired (forced past the finalization timeout / CANON-C decoupled-immediate).
|
|
1503
|
+
if (this.isMeshWorkerSession()) {
|
|
1504
|
+
traceMeshEventStage('fired', this.meshTraceCtx(), `forced after ${waitedMs}ms (${blockReason})`);
|
|
1505
|
+
}
|
|
1454
1506
|
this.pushEvent({
|
|
1455
1507
|
event: 'agent:generating_completed',
|
|
1456
1508
|
chatTitle: pending.chatTitle,
|
|
@@ -1476,6 +1528,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1476
1528
|
}
|
|
1477
1529
|
|
|
1478
1530
|
LOG.info('CLI', `[${this.type}] completed in ${pending.duration}s`);
|
|
1531
|
+
// EVTTRACE: completion fired (transcript finalized cleanly).
|
|
1532
|
+
if (this.isMeshWorkerSession()) {
|
|
1533
|
+
traceMeshEventStage('fired', this.meshTraceCtx(), `duration=${pending.duration}s`);
|
|
1534
|
+
}
|
|
1479
1535
|
this.pushEvent({
|
|
1480
1536
|
event: 'agent:generating_completed',
|
|
1481
1537
|
chatTitle: pending.chatTitle,
|
|
@@ -1837,7 +1893,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1837
1893
|
LOG.info('CLI', `[${this.type}] short completion suppressed: missing final assistant evidence, no mesh context (source=${shortEvidenceSource})`);
|
|
1838
1894
|
// completedDebouncePending intentionally left null — the session is now idle
|
|
1839
1895
|
// with no confirmed turn, matching the startup-blip suppression semantics.
|
|
1896
|
+
// (No EvtTrace: not a mesh session, so nothing routes to a coordinator.)
|
|
1840
1897
|
} else {
|
|
1898
|
+
// EVTTRACE: completion fired (short-generating idle path).
|
|
1899
|
+
if (this.isMeshWorkerSession()) {
|
|
1900
|
+
traceMeshEventStage('fired', this.meshTraceCtx(), `short-generating idle (source=${shortEvidenceSource})`);
|
|
1901
|
+
}
|
|
1841
1902
|
this.pushEvent({
|
|
1842
1903
|
event: 'agent:generating_completed',
|
|
1843
1904
|
chatTitle,
|
|
@@ -1918,6 +1979,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1918
1979
|
&& !this.hasAdapterPendingResponse()
|
|
1919
1980
|
&& !hasNonEmptyCliModalButtons(monitorParsedStatus?.activeModal ?? monitorParsedStatus?.modal)
|
|
1920
1981
|
) {
|
|
1982
|
+
// EVTTRACE: completion fired (no-progress monitor reconciled to completion).
|
|
1983
|
+
if (this.isMeshWorkerSession()) {
|
|
1984
|
+
traceMeshEventStage('fired', this.meshTraceCtx(), 'no_progress_monitor_final_summary');
|
|
1985
|
+
}
|
|
1921
1986
|
this.pushEvent({
|
|
1922
1987
|
event: 'agent:generating_completed',
|
|
1923
1988
|
chatTitle,
|