@adhdev/daemon-core 0.9.82-rc.484 → 0.9.82-rc.485
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-adapters/provider-cli-adapter.d.ts +3 -0
- package/dist/index.js +66 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +66 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/cli-adapters/provider-cli-adapter.ts +70 -1
- package/src/mesh/mesh-queue-assignment.ts +63 -13
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.485",
|
|
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.485",
|
|
51
|
+
"@adhdev/session-host-core": "0.9.82-rc.485",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -208,6 +208,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
208
208
|
private lastScreenSnapshot = '';
|
|
209
209
|
private lastScreenText = '';
|
|
210
210
|
private lastScreenSnapshotReadAt = Number.NEGATIVE_INFINITY;
|
|
211
|
+
// (FALSEIDLE Path-C) Count of CONSECUTIVE getStatus polls that observed a
|
|
212
|
+
// gate-eligible static-idle screen (detect=idle, no modal, quiet, empty
|
|
213
|
+
// partial buffer). For a mesh/autonomous worker we require several such
|
|
214
|
+
// polls in a row before confirming static-idle (see getStatus), so a
|
|
215
|
+
// single momentarily-silent point-sample of a still-live turn cannot flip
|
|
216
|
+
// it. Reset to 0 the instant any poll is ineligible.
|
|
217
|
+
private staticIdlePollStreak = 0;
|
|
211
218
|
|
|
212
219
|
// Server log forwarding
|
|
213
220
|
private serverConn: any = null;
|
|
@@ -287,6 +294,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
287
294
|
result: any;
|
|
288
295
|
} | null = null;
|
|
289
296
|
private static readonly SCREEN_SNAPSHOT_MIN_INTERVAL_MS = 250;
|
|
297
|
+
// (FALSEIDLE Path-C) Consecutive gate-eligible getStatus polls a mesh/autonomous
|
|
298
|
+
// session must show before the poll-static-idle confirm fires. 2 = one extra
|
|
299
|
+
// status tick of hysteresis: enough to reject a single momentary-silence
|
|
300
|
+
// point-sample of a still-live turn, cheap enough not to materially delay a
|
|
301
|
+
// genuine boot-wedge release (the wedge screen is stably static, so it clears
|
|
302
|
+
// every consecutive poll and confirms on the 2nd).
|
|
303
|
+
private static readonly STATIC_IDLE_POLL_CONFIRM_COUNT = 2;
|
|
290
304
|
|
|
291
305
|
private readonly providerResolutionMeta: ProviderResolutionMeta;
|
|
292
306
|
|
|
@@ -394,6 +408,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
394
408
|
return this.timeouts.statusActivityHold;
|
|
395
409
|
}
|
|
396
410
|
|
|
411
|
+
// (FALSEIDLE Path-C) Whether this session is a mesh worker or coordinator's
|
|
412
|
+
// own autonomous session. Mirrors CliProviderInstance.isAutonomousMeshSession
|
|
413
|
+
// over the runtimeSettings the instance mirrors down via updateRuntimeSettings
|
|
414
|
+
// (meshNodeFor / meshActiveTaskId / meshNodeId / launchedByCoordinator =
|
|
415
|
+
// isMeshWorkerSession, plus meshCoordinatorFor for the coordinator's own turn).
|
|
416
|
+
// Such a session has no human at the keyboard to correct a premature idle, so
|
|
417
|
+
// the poll-static-idle confirm is debounced for it (multiple consecutive idle
|
|
418
|
+
// polls) rather than fired on a single point-sample.
|
|
419
|
+
private isAutonomousMeshSession(): boolean {
|
|
420
|
+
const s = this.runtimeSettings;
|
|
421
|
+
return !!(s?.meshNodeFor || s?.meshActiveTaskId || s?.meshNodeId
|
|
422
|
+
|| s?.launchedByCoordinator || s?.meshCoordinatorFor);
|
|
423
|
+
}
|
|
424
|
+
|
|
397
425
|
// Resolved timeouts
|
|
398
426
|
private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>;
|
|
399
427
|
|
|
@@ -956,15 +984,56 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
956
984
|
const quietForMs = this.lastNonEmptyOutputAt
|
|
957
985
|
? (now - this.lastNonEmptyOutputAt)
|
|
958
986
|
: Number.MAX_SAFE_INTEGER;
|
|
987
|
+
let eligible = false;
|
|
959
988
|
if (quietForMs >= this.getStatusActivityHoldMs()) {
|
|
960
989
|
const screenText = this.terminalScreen.getText();
|
|
961
990
|
const pollDetect = this.runDetectStatus(screenText || this.recentOutputBuffer);
|
|
962
991
|
const pollModal = this.runParseApproval(screenText)
|
|
963
992
|
|| this.runParseApproval(this.recentOutputBuffer);
|
|
964
|
-
|
|
993
|
+
// (FALSEIDLE Path-C) Final-assistant / pending-response discriminator.
|
|
994
|
+
// Paths A and B refuse to finalize a turn whose partial-response buffer
|
|
995
|
+
// is still non-empty (getCompletedFinalizationBlock 'partial_response_pending'
|
|
996
|
+
// / completionFinalAssistantEvidence turnClosed at cli-provider-instance.ts).
|
|
997
|
+
// Path C (this poll) previously OMITTED it, so a genuinely-live but
|
|
998
|
+
// momentarily-silent turn — silent thinking, a backgrounded/long tool child,
|
|
999
|
+
// the gap between two assistant bubbles — whose currentTurnScope anchor was
|
|
1000
|
+
// lost still satisfied the weaker gate and flipped to idle prematurely.
|
|
1001
|
+
// Require an EMPTY partial buffer here too. getPartialResponse() returns the
|
|
1002
|
+
// accumulated assistant stream while isWaitingForResponse (which
|
|
1003
|
+
// applyGenerating leaves set on the boot-banner wedge too), so this does NOT
|
|
1004
|
+
// reintroduce the D4b wedge: the attach/boot-banner seeds only the static
|
|
1005
|
+
// ready screen — no assistant turn ever streamed — so its partial buffer is
|
|
1006
|
+
// empty and the gate still releases it. A mid-turn quiet gap holds a
|
|
1007
|
+
// non-empty buffer and is deferred.
|
|
1008
|
+
const partial = this.getPartialResponse();
|
|
1009
|
+
const partialPending = typeof partial === 'string' && partial.trim().length > 0;
|
|
1010
|
+
eligible = pollDetect === 'idle' && !pollModal && !partialPending;
|
|
1011
|
+
}
|
|
1012
|
+
if (eligible) {
|
|
1013
|
+
// (FALSEIDLE Path-C) Debounce for autonomous mesh sessions. A worker /
|
|
1014
|
+
// coordinator has no human to correct a premature idle, and a single
|
|
1015
|
+
// runDetectStatus point-sample can land in a live turn's momentary silence.
|
|
1016
|
+
// Require STATIC_IDLE_POLL_CONFIRM_COUNT consecutive eligible polls before
|
|
1017
|
+
// confirming, so the FSM must observe a sustained static-idle screen — a
|
|
1018
|
+
// turn that resumes (fresh output, or a re-armed turn scope) resets the
|
|
1019
|
+
// streak. The status poll runs on the 30s-idle / 5s-generating heartbeat and
|
|
1020
|
+
// this getStatus gate is re-hit each dashboard status tick, so 2 confirms is
|
|
1021
|
+
// ~one extra tick of hysteresis — enough to reject a one-sample silence gap
|
|
1022
|
+
// without materially delaying a genuine boot-wedge release. Foreground /
|
|
1023
|
+
// attended sessions keep the single-poll confirm (a human is watching Send).
|
|
1024
|
+
const requiredStreak = this.isAutonomousMeshSession()
|
|
1025
|
+
? ProviderCliAdapter.STATIC_IDLE_POLL_CONFIRM_COUNT
|
|
1026
|
+
: 1;
|
|
1027
|
+
this.staticIdlePollStreak += 1;
|
|
1028
|
+
if (this.staticIdlePollStreak >= requiredStreak) {
|
|
965
1029
|
this.engine.confirmPollStaticIdle('poll_static_idle');
|
|
1030
|
+
this.staticIdlePollStreak = 0;
|
|
966
1031
|
}
|
|
1032
|
+
} else {
|
|
1033
|
+
this.staticIdlePollStreak = 0;
|
|
967
1034
|
}
|
|
1035
|
+
} else {
|
|
1036
|
+
this.staticIdlePollStreak = 0;
|
|
968
1037
|
}
|
|
969
1038
|
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
970
1039
|
let effectiveModal = startupModal || this.engine.activeModal;
|
|
@@ -92,12 +92,62 @@ export function getMeshWithCache(components: DaemonComponents, meshId: string):
|
|
|
92
92
|
* worktree node therefore read a permanently stale 'running' here, so
|
|
93
93
|
* shouldDeferDispatchForBootstrap deferred its claim forever. We now MERGE the inline
|
|
94
94
|
* cache's dynamic runtime bootstrap state onto the config node (config keeps its static
|
|
95
|
-
* fields; worktreeBootstrap is preferred from the inline cache
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
95
|
+
* fields; worktreeBootstrap is preferred from the inline cache) so EVERY consumer of the
|
|
96
|
+
* merged view — not just tryAssignQueueTask's gate — observes the terminal stamp.
|
|
97
|
+
*
|
|
98
|
+
* RESIDUAL-getMeshWithCache-bootstrap-overlay (precedence guard): the overlay is DIRECTIONAL —
|
|
99
|
+
* it prefers the inline entry ONLY when the inline runtime state is actually fresher, never
|
|
100
|
+
* merely because the inline entry carries a status. inlineBootstrapIsFresher() (below) permits
|
|
101
|
+
* the overlay in exactly two cases, mirroring the mission's "terminal OR strictly newer" rule:
|
|
102
|
+
* (1) the inline state is TERMINAL ('complete'/'failed') while the config state is NOT — the
|
|
103
|
+
* markWorktreeBootstrapTerminalState synchronous stamp the async config persist has not
|
|
104
|
+
* yet caught up to; this is the whole point of the overlay (opens the gate).
|
|
105
|
+
* (2) both states are non-terminal but the inline startedAt is STRICTLY newer — a re-driven
|
|
106
|
+
* bootstrap whose fresher 'running' epoch the config has not observed.
|
|
107
|
+
* It REFUSES the overlay when the config state is already terminal and the inline state is a
|
|
108
|
+
* stale/non-terminal 'running' — otherwise a stale inline 'running' would MASK a genuinely
|
|
109
|
+
* complete config node and re-defer its claim forever (the exact anti-case this guard closes).
|
|
110
|
+
* And when both are 'running' with no newer epoch, the config value is kept and the gate still
|
|
111
|
+
* defers — the half-built-worktree → empty-session defense is preserved: only a terminal-confirmed
|
|
112
|
+
* inline state, never an ambiguous read, ever opens the gate.
|
|
100
113
|
*/
|
|
114
|
+
const BOOTSTRAP_TERMINAL_STATUSES = new Set(['complete', 'failed']);
|
|
115
|
+
|
|
116
|
+
function bootstrapEpochMs(bootstrap: any): number {
|
|
117
|
+
const raw = readNonEmptyString(bootstrap?.startedAt) || readNonEmptyString(bootstrap?.completedAt);
|
|
118
|
+
if (!raw) return 0;
|
|
119
|
+
const parsed = Date.parse(raw);
|
|
120
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Directional freshness test for the bootstrap overlay: may the inline runtime state
|
|
125
|
+
* REPLACE the config runtime state? True only when the inline state is terminal and the
|
|
126
|
+
* config state is not (the synchronous terminal stamp the async persist lags), or when
|
|
127
|
+
* both are non-terminal but the inline epoch is strictly newer. A terminal config state is
|
|
128
|
+
* never overwritten by a non-terminal inline read (the stale-'running'-masks-complete
|
|
129
|
+
* anti-case), and equal states never trigger a rewrite.
|
|
130
|
+
*/
|
|
131
|
+
function inlineBootstrapIsFresher(inlineBootstrap: any, configBootstrap: any): boolean {
|
|
132
|
+
const inlineStatus = readNonEmptyString(inlineBootstrap?.status);
|
|
133
|
+
if (!inlineStatus) return false;
|
|
134
|
+
const configStatus = readNonEmptyString(configBootstrap?.status);
|
|
135
|
+
const inlineTerminal = BOOTSTRAP_TERMINAL_STATUSES.has(inlineStatus);
|
|
136
|
+
const configTerminal = !!configStatus && BOOTSTRAP_TERMINAL_STATUSES.has(configStatus);
|
|
137
|
+
// Config already terminal: only a DIFFERENT terminal inline state (e.g. config 'complete'
|
|
138
|
+
// vs a later 'failed' re-drive) may supersede it; a non-terminal inline read must never
|
|
139
|
+
// mask a terminal config state.
|
|
140
|
+
if (configTerminal) {
|
|
141
|
+
return inlineTerminal && inlineStatus !== configStatus
|
|
142
|
+
&& bootstrapEpochMs(inlineBootstrap) > bootstrapEpochMs(configBootstrap);
|
|
143
|
+
}
|
|
144
|
+
// Config not terminal: an inline terminal state is always fresher (opens the gate).
|
|
145
|
+
if (inlineTerminal) return true;
|
|
146
|
+
// Both non-terminal: prefer inline only when its epoch is strictly newer (a re-driven
|
|
147
|
+
// bootstrap the config has not observed). Equal/older ⇒ keep config, gate still defers.
|
|
148
|
+
return bootstrapEpochMs(inlineBootstrap) > bootstrapEpochMs(configBootstrap);
|
|
149
|
+
}
|
|
150
|
+
|
|
101
151
|
function mergeInlineCacheOnlyNodes(localMesh: any, cachedMesh: any): any {
|
|
102
152
|
const localNodes = Array.isArray(localMesh?.nodes) ? localMesh.nodes : [];
|
|
103
153
|
const cachedNodes = Array.isArray(cachedMesh?.nodes) ? cachedMesh.nodes : [];
|
|
@@ -111,10 +161,10 @@ function mergeInlineCacheOnlyNodes(localMesh: any, cachedMesh: any): any {
|
|
|
111
161
|
if (!cachedId) return false;
|
|
112
162
|
return !localNodes.some((localNode: any) => meshNodeIdMatches(localNode, cachedId));
|
|
113
163
|
});
|
|
114
|
-
// Overlay the inline cache's fresher worktreeBootstrap state onto any config node
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
164
|
+
// Overlay the inline cache's fresher worktreeBootstrap state onto any config node that
|
|
165
|
+
// also exists in the inline cache. inlineBootstrapIsFresher() gates the overlay to the
|
|
166
|
+
// "terminal OR strictly newer" cases, so a stale inline 'running' can never mask a
|
|
167
|
+
// terminal config state and the gate's deferral is preserved for a genuine 'running'.
|
|
118
168
|
let overlaidLocalNodes: any[] = localNodes;
|
|
119
169
|
let overlaid = false;
|
|
120
170
|
for (let i = 0; i < localNodes.length; i++) {
|
|
@@ -122,14 +172,14 @@ function mergeInlineCacheOnlyNodes(localMesh: any, cachedMesh: any): any {
|
|
|
122
172
|
const localId = readMeshNodeId(localNode);
|
|
123
173
|
if (!localId) continue;
|
|
124
174
|
const inlineMatch = cachedNodes.find((cachedNode: any) => meshNodeIdMatches(cachedNode, localId));
|
|
125
|
-
|
|
126
|
-
if (!inlineMatch
|
|
175
|
+
if (!inlineMatch) continue;
|
|
176
|
+
if (!inlineBootstrapIsFresher(inlineMatch.worktreeBootstrap, localNode.worktreeBootstrap)) continue;
|
|
127
177
|
if (!overlaid) {
|
|
128
178
|
overlaidLocalNodes = [...localNodes];
|
|
129
179
|
overlaid = true;
|
|
130
180
|
}
|
|
131
|
-
// Keep the config node's static fields;
|
|
132
|
-
//
|
|
181
|
+
// Keep the config node's static fields; overlay only the dynamic worktreeBootstrap
|
|
182
|
+
// runtime substate (fresher terminal stamp / epoch) — config identity is unchanged.
|
|
133
183
|
overlaidLocalNodes[i] = { ...localNode, worktreeBootstrap: inlineMatch.worktreeBootstrap };
|
|
134
184
|
}
|
|
135
185
|
if (!cacheOnly.length && !overlaid) return localMesh;
|