@adhdev/daemon-core 0.9.82-rc.501 → 0.9.82-rc.503
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 +63 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +63 -10
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +26 -0
- package/package.json +3 -3
- package/src/mesh/mesh-events-pending.ts +34 -0
- package/src/mesh/mesh-remote-event-pull.ts +36 -20
- package/src/providers/cli-provider-instance.ts +72 -1
|
@@ -145,6 +145,7 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
145
145
|
private busyEpoch;
|
|
146
146
|
private fastCollapseSynthesizedTaskId;
|
|
147
147
|
private startupGraceCollapseAt;
|
|
148
|
+
private meshTaskInjectedAt;
|
|
148
149
|
private settings;
|
|
149
150
|
private monitor;
|
|
150
151
|
private generatingDebounceTimer;
|
|
@@ -440,6 +441,31 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
440
441
|
* turn completes overwrites it. Returns undefined for a non-task ad-hoc turn.
|
|
441
442
|
*/
|
|
442
443
|
private completingTurnTaskId;
|
|
444
|
+
/**
|
|
445
|
+
* ANTIGRAVITY-PREMATURE-COMPLETION gate: has the CURRENTLY-injected task actually
|
|
446
|
+
* entered generating (a real onTurnStarted for it)? Used to reject stale external-
|
|
447
|
+
* native completion evidence that predates the injected task's turn.
|
|
448
|
+
*
|
|
449
|
+
* The injected task's id is the session scalar `meshActiveTaskId`, stamped by
|
|
450
|
+
* attachMeshAssignment BEFORE the PTY turn starts. That stamp also records
|
|
451
|
+
* `meshTaskInjectedAt`. The turn that has genuinely started is marked by
|
|
452
|
+
* `adapter.currentTurnStartedAt` (set ONLY by onTurnStarted). Two naive signals both
|
|
453
|
+
* FAIL for a reused-idle session:
|
|
454
|
+
* - `currentTurnStartedAt > 0` alone: it persists from the PRIOR turn, so it is
|
|
455
|
+
* already > 0 the instant a new task is injected (pre-onTurnStarted).
|
|
456
|
+
* - `currentTurnTaskId === meshActiveTaskId` alone: forceSendMessage (the mesh
|
|
457
|
+
* inject path) pre-binds currentTurnTaskId to the new taskId at inject time,
|
|
458
|
+
* BEFORE the turn starts, so this matches prematurely too.
|
|
459
|
+
* The robust discriminator is TEMPORAL: the producing turn must have STARTED AFTER
|
|
460
|
+
* the injection — `currentTurnStartedAt > meshTaskInjectedAt`. Only then has the
|
|
461
|
+
* injected task's own onTurnStarted fired.
|
|
462
|
+
* - No injected task since boot (meshTaskInjectedAt === 0, e.g. an ad-hoc/dashboard
|
|
463
|
+
* turn or a non-mesh session): fall back to the plain "a turn has started" check
|
|
464
|
+
* so non-mesh completion is unaffected.
|
|
465
|
+
* Fails CLOSED for the injected-but-not-started window; open once the injected turn is
|
|
466
|
+
* genuinely underway (preserving the rc.480/481 completion-fires win).
|
|
467
|
+
*/
|
|
468
|
+
private injectedTaskHasStartedGenerating;
|
|
443
469
|
private meshTraceCtx;
|
|
444
470
|
private completionTraceOn;
|
|
445
471
|
private fsmTraceOn;
|
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.503",
|
|
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",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"author": "vilmire",
|
|
48
48
|
"license": "AGPL-3.0-or-later",
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
51
|
-
"@adhdev/session-host-core": "0.9.82-rc.
|
|
50
|
+
"@adhdev/mesh-shared": "0.9.82-rc.503",
|
|
51
|
+
"@adhdev/session-host-core": "0.9.82-rc.503",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -891,6 +891,39 @@ export function stampPendingEventV2(
|
|
|
891
891
|
// has no owner to leak from and must reach whatever coordinator drains here.
|
|
892
892
|
const dispatchedBySelfFallback = selfFallback && stamp.scope === 'broadcast';
|
|
893
893
|
|
|
894
|
+
// CODA-TERMINAL-EVENT-HELD-WHILE-GENERATING (project-mesh-self-fallback-terminal-
|
|
895
|
+
// broadcast-drop): an ownerless self-fallback broadcast terminal event
|
|
896
|
+
// (worktree_bootstrap_complete / refine:completed / a summary-less
|
|
897
|
+
// agent:generating_completed emitted while THIS machine's coordinator CLI session is
|
|
898
|
+
// generating) previously carried NO targetCoordinatorDaemonId. The reconcile loop
|
|
899
|
+
// holds it under `generating_no_idle_coordinator`, and its `event_held` ledger mirror
|
|
900
|
+
// records targetCoordinatorDaemonId:null — so the held event is not addressable to
|
|
901
|
+
// the local coordinator's per-daemon scoped file, and (live 2026-07-12) the
|
|
902
|
+
// coordinator only ever learns of it by polling the ledger, violating the no-polling
|
|
903
|
+
// rule.
|
|
904
|
+
//
|
|
905
|
+
// The event's INTENDED coordinator IS this machine's own coordinator: the
|
|
906
|
+
// self-fallback minted `dispatchedBy` under this daemon's own machineId precisely
|
|
907
|
+
// because the originating task was dispatched by this machine's coordinator. Stamp
|
|
908
|
+
// `targetCoordinatorDaemonId` = that same self daemon id so the held event is
|
|
909
|
+
// addressable to the local coordinator's scoped file / drain filter, WITHOUT
|
|
910
|
+
// changing its BROADCAST scope or the `dispatchedBySelfFallback` machine-level
|
|
911
|
+
// `deliverSelfFallback` guard (which already keeps a replica completion on machine A
|
|
912
|
+
// from fanning out to a coordinator on machine B). The target is a MACHINE id,
|
|
913
|
+
// matched by machine core, so it only ever reaches a coordinator on THIS machine.
|
|
914
|
+
//
|
|
915
|
+
// Gate: apply ONLY when the event carries NO targetCoordinatorSessionId. A
|
|
916
|
+
// session-strict event (targetCoordinatorSessionId set, coordinator daemon id
|
|
917
|
+
// unresolved) is deliberately held/expired by the reconcile loop's strict-route path
|
|
918
|
+
// keyed on the SESSION, not the daemon — stamping a daemon target there would let it
|
|
919
|
+
// drain to a sibling before its session returns and break the strict-route hold. An
|
|
920
|
+
// event that already carries an explicit daemon target is likewise left untouched.
|
|
921
|
+
const selfFallbackTarget = dispatchedBySelfFallback
|
|
922
|
+
&& !readNonEmptyString(event.targetCoordinatorDaemonId)
|
|
923
|
+
&& !readNonEmptyString(event.targetCoordinatorSessionId)
|
|
924
|
+
? readNonEmptyString(stamp.dispatchedBy.daemonId)
|
|
925
|
+
: undefined;
|
|
926
|
+
|
|
894
927
|
return {
|
|
895
928
|
...event,
|
|
896
929
|
protocolVersion: stamp.protocolVersion,
|
|
@@ -899,6 +932,7 @@ export function stampPendingEventV2(
|
|
|
899
932
|
dispatchedBy: stamp.dispatchedBy,
|
|
900
933
|
...(stamp.intendedFor ? { intendedFor: stamp.intendedFor } : {}),
|
|
901
934
|
...(dispatchedBySelfFallback ? { dispatchedBySelfFallback: true } : {}),
|
|
935
|
+
...(selfFallbackTarget ? { targetCoordinatorDaemonId: selfFallbackTarget } : {}),
|
|
902
936
|
};
|
|
903
937
|
}
|
|
904
938
|
|
|
@@ -72,18 +72,26 @@ export async function pullRemoteNodeQueues(
|
|
|
72
72
|
if (daemonIdsEquivalent(nodeDaemonId, localDaemonId)) return;
|
|
73
73
|
if (daemonIdListIncludes(candidateDaemonIds, nodeDaemonId)) return;
|
|
74
74
|
|
|
75
|
-
// Peer-connected pre-check (EVENT-DELIVERY-DELAY fix(a)):
|
|
76
|
-
// DataChannel is not open would sink this pull into
|
|
77
|
-
// until CONNECT_TIMEOUT_MS (90s), formerly freezing
|
|
78
|
-
// delaying completion-event recovery from healthy
|
|
79
|
-
// tick and retry next tick — LOSSLESS: an
|
|
80
|
-
// anything (drained=0 preserved), so its events
|
|
81
|
-
// successful tick. Skip = delay, never loss.
|
|
82
|
-
// •
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
75
|
+
// Peer-connected pre-check (EVENT-DELIVERY-DELAY fix(a) + OFFLINE-NODE-FANOUT):
|
|
76
|
+
// a degraded peer whose DataChannel is not open would sink this pull into
|
|
77
|
+
// peer.connectQueue and stall until CONNECT_TIMEOUT_MS (90s), formerly freezing
|
|
78
|
+
// the whole serial loop and delaying completion-event recovery from healthy
|
|
79
|
+
// nodes. Skip such a node THIS tick and retry next tick — LOSSLESS: an
|
|
80
|
+
// unconnected peer has not drained anything (drained=0 preserved), so its events
|
|
81
|
+
// are recovered whole on the next successful tick. Skip = delay, never loss.
|
|
82
|
+
// • getter WIRED (cloud) → a null/undefined snapshot means "no peer object
|
|
83
|
+
// right now" = NOT connected (a powered-off node whose failPeer just deleted
|
|
84
|
+
// the peer each cycle). Treat it EXACTLY like state !== 'connected' and skip;
|
|
85
|
+
// dialing here would re-queue for another 90s (the null-race the guard is
|
|
86
|
+
// meant to prevent). Only a snapshot with state === 'connected' proceeds.
|
|
87
|
+
// • getter UNWIRED (standalone) → DO NOT skip; fall through to the legacy path
|
|
88
|
+
// so this stays regression-free (the standalone case the guard's history
|
|
89
|
+
// references).
|
|
90
|
+
const getPeerStatus = components.getMeshPeerConnectionStatus;
|
|
91
|
+
if (getPeerStatus) {
|
|
92
|
+
const peerSnapshot = getPeerStatus(nodeDaemonId);
|
|
93
|
+
if (!peerSnapshot || String(peerSnapshot.state) !== 'connected') return;
|
|
94
|
+
}
|
|
87
95
|
|
|
88
96
|
for (const pendingEventArgs of pulls) {
|
|
89
97
|
let events: unknown;
|
|
@@ -185,15 +193,23 @@ export async function collectLiveNodesWithSessions(
|
|
|
185
193
|
const isLocalNode = !nodeDaemonId
|
|
186
194
|
|| daemonIdListIncludes(selfIds, nodeDaemonId)
|
|
187
195
|
|| daemonIdsEquivalent(nodeDaemonId, localDaemonId);
|
|
188
|
-
// Peer-connected pre-check (EVENT-DELIVERY-DELAY fix(a)):
|
|
189
|
-
// Without this the 90s connect-deadline block
|
|
190
|
-
// a degraded remote's get_status_metadata sinks
|
|
191
|
-
// the whole prune probe. Only call the remote
|
|
192
|
-
// unconnected peer is left undecorated (empty
|
|
193
|
-
//
|
|
196
|
+
// Peer-connected pre-check (EVENT-DELIVERY-DELAY fix(a) + OFFLINE-NODE-FANOUT):
|
|
197
|
+
// mirror pullRemoteNodeQueues. Without this the 90s connect-deadline block
|
|
198
|
+
// re-enters via this Promise.all — a degraded remote's get_status_metadata sinks
|
|
199
|
+
// into peer.connectQueue and stalls the whole prune probe. Only call the remote
|
|
200
|
+
// when the peer is 'connected'; an unconnected peer is left undecorated (empty
|
|
201
|
+
// session list), same as unreachable.
|
|
202
|
+
// • getter WIRED (cloud) → a null snapshot means "no peer object right now" =
|
|
203
|
+
// NOT connected (offline node whose failPeer deleted the peer). Skip (leave
|
|
204
|
+
// undecorated) rather than dialing into another 90s connect wait — the same
|
|
205
|
+
// null-race harden as pullRemoteNodeQueues.
|
|
206
|
+
// • getter UNWIRED (standalone) → do NOT skip, fall through (regression-free).
|
|
194
207
|
if (!isLocalNode) {
|
|
195
|
-
const
|
|
196
|
-
if (
|
|
208
|
+
const getPeerStatus = components.getMeshPeerConnectionStatus;
|
|
209
|
+
if (getPeerStatus) {
|
|
210
|
+
const peerSnapshot = getPeerStatus(nodeDaemonId);
|
|
211
|
+
if (!peerSnapshot || String(peerSnapshot.state) !== 'connected') return node;
|
|
212
|
+
}
|
|
197
213
|
}
|
|
198
214
|
let statusResult: unknown;
|
|
199
215
|
try {
|
|
@@ -256,6 +256,15 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
256
256
|
// R4b miss: collapse at boot+8s, dispatch at boot+12.4s > the 12s boot window).
|
|
257
257
|
// Anchoring on the collapse moment makes the window cover dispatch-delay+turn.
|
|
258
258
|
private startupGraceCollapseAt: number | null = null;
|
|
259
|
+
// ANTIGRAVITY-PREMATURE-COMPLETION gate: wall-clock when the CURRENT mesh task
|
|
260
|
+
// was injected/attached (attachMeshAssignment). The injected task counts as having
|
|
261
|
+
// genuinely entered generating only once a turn STARTS after this moment
|
|
262
|
+
// (currentTurnStartedAt > meshTaskInjectedAt) — because currentTurnStartedAt
|
|
263
|
+
// persists from the PRIOR turn and forceSendMessage pre-binds currentTurnTaskId at
|
|
264
|
+
// inject time, so neither alone distinguishes "injected but not yet generating"
|
|
265
|
+
// from "genuinely generating". This timestamp is that discriminator. 0 = no task
|
|
266
|
+
// injected since boot (ad-hoc/non-mesh turns fall back to the plain turn-started check).
|
|
267
|
+
private meshTaskInjectedAt = 0;
|
|
259
268
|
private settings: Record<string, any> = {};
|
|
260
269
|
private monitor: StatusMonitor;
|
|
261
270
|
private generatingDebounceTimer: NodeJS.Timeout | null = null;
|
|
@@ -862,6 +871,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
862
871
|
*/
|
|
863
872
|
attachMeshAssignment(assignment: { meshId: string; nodeId?: string; taskId?: string; dispatchNonce?: number; coordinatorDaemonId?: string; coordinatorSessionId?: string }): void {
|
|
864
873
|
if (!assignment?.meshId) return;
|
|
874
|
+
// ANTIGRAVITY-PREMATURE-COMPLETION gate: stamp the injection moment for a task
|
|
875
|
+
// attach so injectedTaskHasStartedGenerating() can require the producing turn to
|
|
876
|
+
// START after this point (rejecting the prior turn's stale native-history tail
|
|
877
|
+
// that would otherwise fire generating_completed before generating_started).
|
|
878
|
+
if (assignment.taskId && assignment.taskId.trim()) {
|
|
879
|
+
this.meshTaskInjectedAt = Date.now();
|
|
880
|
+
}
|
|
865
881
|
this.settings = {
|
|
866
882
|
...this.settings,
|
|
867
883
|
meshNodeFor: assignment.meshId,
|
|
@@ -1468,7 +1484,25 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1468
1484
|
|
|
1469
1485
|
const externalMessages = this.readExternalCompletionMessages();
|
|
1470
1486
|
if (externalMessages) {
|
|
1471
|
-
|
|
1487
|
+
// ANTIGRAVITY-PREMATURE-COMPLETION (recur): the external-native transcript is
|
|
1488
|
+
// the WHOLE session's native-history, not turn-scoped by the provider. On a
|
|
1489
|
+
// reused-idle antigravity session, a completion-gate poll can run AFTER a new
|
|
1490
|
+
// task is injected but BEFORE that task's onTurnStarted fires. The transcript
|
|
1491
|
+
// then still tails the PRIOR turn's final-assistant bubble; completionHasFinal‑
|
|
1492
|
+
// AssistantMessage accepts it (turnStartedAt is 0/undefined pre-onTurnStarted →
|
|
1493
|
+
// fails open, or the prior bubble post-dates the prior turn → passes) and a
|
|
1494
|
+
// generating_completed fires for the NEW task BEFORE generating_started — the
|
|
1495
|
+
// exact live 06:34→06:35 inversion. Gate external-native evidence on the current
|
|
1496
|
+
// injected task having genuinely entered generating: if a task is attached but its
|
|
1497
|
+
// turn has not started (turnStartedInjectedTask() === false), the tail is stale by
|
|
1498
|
+
// construction, so this evidence must NOT satisfy the completion gate. Fail CLOSED
|
|
1499
|
+
// (present=false) rather than open. This does NOT regress the rc.480/481 win: once
|
|
1500
|
+
// the injected task's onTurnStarted fires, currentTurnStartedAt/currentTurnTaskId
|
|
1501
|
+
// bind to it and a real final bubble still fires completion normally.
|
|
1502
|
+
const injectedTaskGenerating = this.injectedTaskHasStartedGenerating();
|
|
1503
|
+
const present = injectedTaskGenerating
|
|
1504
|
+
&& turnClosed
|
|
1505
|
+
&& this.completionHasFinalAssistantMessage(externalMessages, turnStartedAt);
|
|
1472
1506
|
// Dashboard tail-repair cache: this runs on EVERY completion check
|
|
1473
1507
|
// (mesh AND non-mesh — the non-mesh path suppresses the
|
|
1474
1508
|
// generating_completed emit, so completionFinalSummary never runs there
|
|
@@ -1924,6 +1958,43 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1924
1958
|
return typeof scalar === 'string' && scalar.trim() ? scalar : undefined;
|
|
1925
1959
|
}
|
|
1926
1960
|
|
|
1961
|
+
/**
|
|
1962
|
+
* ANTIGRAVITY-PREMATURE-COMPLETION gate: has the CURRENTLY-injected task actually
|
|
1963
|
+
* entered generating (a real onTurnStarted for it)? Used to reject stale external-
|
|
1964
|
+
* native completion evidence that predates the injected task's turn.
|
|
1965
|
+
*
|
|
1966
|
+
* The injected task's id is the session scalar `meshActiveTaskId`, stamped by
|
|
1967
|
+
* attachMeshAssignment BEFORE the PTY turn starts. That stamp also records
|
|
1968
|
+
* `meshTaskInjectedAt`. The turn that has genuinely started is marked by
|
|
1969
|
+
* `adapter.currentTurnStartedAt` (set ONLY by onTurnStarted). Two naive signals both
|
|
1970
|
+
* FAIL for a reused-idle session:
|
|
1971
|
+
* - `currentTurnStartedAt > 0` alone: it persists from the PRIOR turn, so it is
|
|
1972
|
+
* already > 0 the instant a new task is injected (pre-onTurnStarted).
|
|
1973
|
+
* - `currentTurnTaskId === meshActiveTaskId` alone: forceSendMessage (the mesh
|
|
1974
|
+
* inject path) pre-binds currentTurnTaskId to the new taskId at inject time,
|
|
1975
|
+
* BEFORE the turn starts, so this matches prematurely too.
|
|
1976
|
+
* The robust discriminator is TEMPORAL: the producing turn must have STARTED AFTER
|
|
1977
|
+
* the injection — `currentTurnStartedAt > meshTaskInjectedAt`. Only then has the
|
|
1978
|
+
* injected task's own onTurnStarted fired.
|
|
1979
|
+
* - No injected task since boot (meshTaskInjectedAt === 0, e.g. an ad-hoc/dashboard
|
|
1980
|
+
* turn or a non-mesh session): fall back to the plain "a turn has started" check
|
|
1981
|
+
* so non-mesh completion is unaffected.
|
|
1982
|
+
* Fails CLOSED for the injected-but-not-started window; open once the injected turn is
|
|
1983
|
+
* genuinely underway (preserving the rc.480/481 completion-fires win).
|
|
1984
|
+
*/
|
|
1985
|
+
private injectedTaskHasStartedGenerating(): boolean {
|
|
1986
|
+
const turnStartedAt = typeof (this.adapter as any)?.currentTurnStartedAt === 'number'
|
|
1987
|
+
? (this.adapter as any).currentTurnStartedAt as number
|
|
1988
|
+
: 0;
|
|
1989
|
+
const turnStarted = Number.isFinite(turnStartedAt) && turnStartedAt > 0;
|
|
1990
|
+
if (this.meshTaskInjectedAt <= 0) {
|
|
1991
|
+
// No mesh task injected since boot — plain "a turn has started" suffices.
|
|
1992
|
+
return turnStarted;
|
|
1993
|
+
}
|
|
1994
|
+
// A task was injected: the producing turn must have STARTED after that injection.
|
|
1995
|
+
return turnStarted && turnStartedAt > this.meshTaskInjectedAt;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1927
1998
|
// EVTTRACE correlation context for this session's completion lifecycle. taskId is
|
|
1928
1999
|
// the primary grep anchor; instanceId is the session fallback.
|
|
1929
2000
|
private meshTraceCtx(event = 'agent:generating_completed'): Record<string, unknown> {
|