@adhdev/daemon-core 0.9.82-rc.556 → 0.9.82-rc.558
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 +92 -28
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +92 -28
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +31 -12
- package/package.json +3 -3
- package/src/providers/cli-provider-instance.ts +117 -27
|
@@ -170,7 +170,9 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
170
170
|
* with no approval never carries recency and is never held (no regression).
|
|
171
171
|
*/
|
|
172
172
|
private static readonly APPROVAL_RESUME_GRACE_MS;
|
|
173
|
-
private static readonly
|
|
173
|
+
private static readonly MESH_WORKER_STALL_IDLE_THRESHOLD_MS;
|
|
174
|
+
private static readonly MESH_WORKER_STALL_TURN_THRESHOLD_MS;
|
|
175
|
+
private static readonly MESH_WORKER_STALL_REFIRE_COOLDOWN_MS;
|
|
174
176
|
private adapter;
|
|
175
177
|
private context;
|
|
176
178
|
private events;
|
|
@@ -179,6 +181,8 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
179
181
|
private generatingStartedAt;
|
|
180
182
|
private meshStallAnchorAt;
|
|
181
183
|
private meshStallEmittedForAnchor;
|
|
184
|
+
private meshStallTurnActiveLast;
|
|
185
|
+
private meshStallLastFiredAt;
|
|
182
186
|
private busyEpoch;
|
|
183
187
|
private fastCollapseSynthesizedTaskId;
|
|
184
188
|
private startupGraceCollapseAt;
|
|
@@ -507,25 +511,40 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
507
511
|
* ProviderInstanceManager's existing 5s onTick loop (NO new timer) — see
|
|
508
512
|
* ProviderInstanceManager.startTicking. Reuses the adapter's raw-PTY-output
|
|
509
513
|
* clock (lastOutputAt, bumped on every output chunk) as the sole signal: if a
|
|
510
|
-
* live worker's screen has been byte-for-byte unchanged
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
* pendingCoordinatorEvent path.
|
|
514
|
+
* live worker's screen has been byte-for-byte unchanged past the turn-scoped
|
|
515
|
+
* threshold (below), fire ONE informational monitor:no_progress event down the
|
|
516
|
+
* existing task_stalled ledger + pendingCoordinatorEvent path.
|
|
514
517
|
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
518
|
+
* The reported status is read for TWO bounded purposes only — it does NOT
|
|
519
|
+
* suppress the fire (sticky-status blindness would hide a real wedge):
|
|
520
|
+
* • Fix B (anchor re-arm on turn end): the FSM's completion/idle transition
|
|
521
|
+
* never touches the stall anchor, so a completed worker that goes idle would
|
|
522
|
+
* otherwise keep counting from its LAST pre-completion output and false-fire
|
|
523
|
+
* in the quiet valley right after finishing. We detect the turn-active edge
|
|
524
|
+
* (hasAdapterPendingResponse()) and, on active → inactive, re-arm the anchor
|
|
525
|
+
* to `now` so the post-completion idle valley starts a fresh clock.
|
|
526
|
+
* • Fix C (turn-scoped threshold): while a turn is genuinely in flight the bar
|
|
527
|
+
* is raised (MESH_WORKER_STALL_TURN_THRESHOLD_MS) to absorb long normal
|
|
528
|
+
* thinking gaps; outside a turn the tighter idle bound applies. This is a
|
|
529
|
+
* RAISE, not a skip — a real mid-turn wedge still fires late at the turn bound.
|
|
521
530
|
*
|
|
522
531
|
* Anchoring: the episode arms against the current lastOutputAt; a worker that
|
|
523
532
|
* has emitted nothing yet (lastOutputAt === 0) anchors on this.startedAt (spawn
|
|
524
533
|
* time) so a silent spawn is still caught. Any new output re-arms the anchor
|
|
525
534
|
* and clears the emitted flag, so one continuous stall emits at most once and a
|
|
526
|
-
* later stall re-arms cleanly.
|
|
535
|
+
* later stall re-arms cleanly. Fix E adds a per-session refire cooldown so a
|
|
536
|
+
* dribble of one-byte-per-few-minutes output cannot page the coordinator on
|
|
537
|
+
* every re-arm.
|
|
527
538
|
*/
|
|
528
539
|
checkMeshWorkerStall(now?: number): void;
|
|
540
|
+
/**
|
|
541
|
+
* FALSE-STALL-WATCHDOG-OVERFIRE: drop the entire stall episode for this session.
|
|
542
|
+
* Called when the session is no longer a mesh worker or the PTY is dead — the
|
|
543
|
+
* next live tick re-arms a fresh episode. Clears the fix-B turn-edge state and
|
|
544
|
+
* the fix-E refire cooldown alongside the base anchor so a re-armed session
|
|
545
|
+
* starts with a clean slate (a restart is not throttled by a prior stall).
|
|
546
|
+
*/
|
|
547
|
+
private resetMeshStallEpisode;
|
|
529
548
|
/**
|
|
530
549
|
* AUTOAPPROVE-FLAP-RECUR (Fix A+B): how long a busy blip / modal scroll-out may
|
|
531
550
|
* persist before the in-progress settle gate is torn down. For a delegated
|
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.558",
|
|
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.558",
|
|
51
|
+
"@adhdev/session-host-core": "0.9.82-rc.558",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -259,9 +259,30 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
259
259
|
// event. Unlike the StatusMonitor no-progress watchdog (which only runs while a
|
|
260
260
|
// turn is generating), this observes pure screen stasis regardless of the
|
|
261
261
|
// reported status — a worker parked idle, wedged mid-turn, or spawned with no
|
|
262
|
-
// output at all.
|
|
263
|
-
//
|
|
264
|
-
|
|
262
|
+
// output at all.
|
|
263
|
+
//
|
|
264
|
+
// FALSE-STALL-WATCHDOG-OVERFIRE (fix C): the threshold is now turn-scoped. When
|
|
265
|
+
// an explicit turn is in flight (hasAdapterPendingResponse() — the adapter's
|
|
266
|
+
// currentTurnScope / isWaitingForResponse / isProcessing / partial buffer), a
|
|
267
|
+
// long silent thinking gap (claude-cli opus/high can go minutes between visible
|
|
268
|
+
// tokens) is normal, so the bar is raised to MESH_WORKER_STALL_TURN_THRESHOLD_MS.
|
|
269
|
+
// Outside a turn (idle) the tighter MESH_WORKER_STALL_IDLE_THRESHOLD_MS applies.
|
|
270
|
+
// This is a THRESHOLD RAISE, not a suppression: a genuine mid-turn wedge still
|
|
271
|
+
// fires (late, at the turn bound) rather than being hidden behind a sticky
|
|
272
|
+
// generating status. 180s matches DEFAULT_MONITOR_CONFIG.noProgressThresholdSec
|
|
273
|
+
// so the idle bound agrees with the StatusMonitor watchdog's "long interval".
|
|
274
|
+
private static readonly MESH_WORKER_STALL_IDLE_THRESHOLD_MS = 180_000;
|
|
275
|
+
private static readonly MESH_WORKER_STALL_TURN_THRESHOLD_MS = 360_000;
|
|
276
|
+
// FALSE-STALL-WATCHDOG-OVERFIRE (fix E): minimum spacing between two stall
|
|
277
|
+
// notifications for the SAME session, even across anchor re-arms. The
|
|
278
|
+
// per-anchor meshStallEmittedForAnchor guard already stops a single continuous
|
|
279
|
+
// stall from re-firing; this cooldown additionally throttles the churn where a
|
|
280
|
+
// worker dribbles one byte every few minutes (each re-arming the anchor and then
|
|
281
|
+
// re-crossing the bar), which would otherwise page the coordinator repeatedly.
|
|
282
|
+
// The stall is still fired for observability — just not more than once per
|
|
283
|
+
// window per session. Set larger than the stall thresholds so consecutive
|
|
284
|
+
// re-armed stalls a few minutes apart collapse into a single notification.
|
|
285
|
+
private static readonly MESH_WORKER_STALL_REFIRE_COOLDOWN_MS = 600_000;
|
|
265
286
|
|
|
266
287
|
private adapter: ProviderCliAdapter;
|
|
267
288
|
private context: InstanceContext | null = null;
|
|
@@ -286,6 +307,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
286
307
|
// -1 = not yet initialised for this session.
|
|
287
308
|
private meshStallAnchorAt = -1;
|
|
288
309
|
private meshStallEmittedForAnchor = false;
|
|
310
|
+
// FALSE-STALL-WATCHDOG-OVERFIRE (fix B): the turn-active state observed on the
|
|
311
|
+
// PREVIOUS stall tick. When a turn ends (active → inactive: the completion/idle
|
|
312
|
+
// transition), the anchor is force re-armed to `now` so the completed→idle valley
|
|
313
|
+
// does not fire against the pre-completion output clock. The turn-end edge is the
|
|
314
|
+
// signal; updateStatus's own idle transition does not touch the stall anchor, so
|
|
315
|
+
// this watchdog-local edge detector owns the re-arm. undefined = no prior tick.
|
|
316
|
+
private meshStallTurnActiveLast: boolean | undefined = undefined;
|
|
317
|
+
// FALSE-STALL-WATCHDOG-OVERFIRE (fix E): wall-clock of the most recent stall
|
|
318
|
+
// emission for this session, or -1 if none. Enforces
|
|
319
|
+
// MESH_WORKER_STALL_REFIRE_COOLDOWN_MS between successive emissions across
|
|
320
|
+
// anchor re-arms (the per-anchor guard only covers a single continuous stall).
|
|
321
|
+
private meshStallLastFiredAt = -1;
|
|
289
322
|
// FALSE-IDLE continuity epoch: monotonically bumped on EVERY entry into a busy
|
|
290
323
|
// phase (→generating or →waiting_approval). The completedDebouncePending snapshots
|
|
291
324
|
// this value at arm time (busyEpochAtArm); the flush guard requires it UNCHANGED
|
|
@@ -2108,30 +2141,36 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2108
2141
|
* ProviderInstanceManager's existing 5s onTick loop (NO new timer) — see
|
|
2109
2142
|
* ProviderInstanceManager.startTicking. Reuses the adapter's raw-PTY-output
|
|
2110
2143
|
* clock (lastOutputAt, bumped on every output chunk) as the sole signal: if a
|
|
2111
|
-
* live worker's screen has been byte-for-byte unchanged
|
|
2112
|
-
*
|
|
2113
|
-
*
|
|
2114
|
-
* pendingCoordinatorEvent path.
|
|
2144
|
+
* live worker's screen has been byte-for-byte unchanged past the turn-scoped
|
|
2145
|
+
* threshold (below), fire ONE informational monitor:no_progress event down the
|
|
2146
|
+
* existing task_stalled ledger + pendingCoordinatorEvent path.
|
|
2115
2147
|
*
|
|
2116
|
-
*
|
|
2117
|
-
*
|
|
2118
|
-
*
|
|
2119
|
-
*
|
|
2120
|
-
*
|
|
2121
|
-
*
|
|
2148
|
+
* The reported status is read for TWO bounded purposes only — it does NOT
|
|
2149
|
+
* suppress the fire (sticky-status blindness would hide a real wedge):
|
|
2150
|
+
* • Fix B (anchor re-arm on turn end): the FSM's completion/idle transition
|
|
2151
|
+
* never touches the stall anchor, so a completed worker that goes idle would
|
|
2152
|
+
* otherwise keep counting from its LAST pre-completion output and false-fire
|
|
2153
|
+
* in the quiet valley right after finishing. We detect the turn-active edge
|
|
2154
|
+
* (hasAdapterPendingResponse()) and, on active → inactive, re-arm the anchor
|
|
2155
|
+
* to `now` so the post-completion idle valley starts a fresh clock.
|
|
2156
|
+
* • Fix C (turn-scoped threshold): while a turn is genuinely in flight the bar
|
|
2157
|
+
* is raised (MESH_WORKER_STALL_TURN_THRESHOLD_MS) to absorb long normal
|
|
2158
|
+
* thinking gaps; outside a turn the tighter idle bound applies. This is a
|
|
2159
|
+
* RAISE, not a skip — a real mid-turn wedge still fires late at the turn bound.
|
|
2122
2160
|
*
|
|
2123
2161
|
* Anchoring: the episode arms against the current lastOutputAt; a worker that
|
|
2124
2162
|
* has emitted nothing yet (lastOutputAt === 0) anchors on this.startedAt (spawn
|
|
2125
2163
|
* time) so a silent spawn is still caught. Any new output re-arms the anchor
|
|
2126
2164
|
* and clears the emitted flag, so one continuous stall emits at most once and a
|
|
2127
|
-
* later stall re-arms cleanly.
|
|
2165
|
+
* later stall re-arms cleanly. Fix E adds a per-session refire cooldown so a
|
|
2166
|
+
* dribble of one-byte-per-few-minutes output cannot page the coordinator on
|
|
2167
|
+
* every re-arm.
|
|
2128
2168
|
*/
|
|
2129
2169
|
checkMeshWorkerStall(now: number = Date.now()): void {
|
|
2130
2170
|
if (!this.isMeshWorkerSession()) {
|
|
2131
2171
|
// Not (or no longer) a mesh worker — drop any armed episode so a session
|
|
2132
2172
|
// whose mesh markers were detached does not carry a stale anchor.
|
|
2133
|
-
this.
|
|
2134
|
-
this.meshStallEmittedForAnchor = false;
|
|
2173
|
+
this.resetMeshStallEpisode();
|
|
2135
2174
|
return;
|
|
2136
2175
|
}
|
|
2137
2176
|
// Defensive: not every CliAdapter implementation exposes isAlive() — the
|
|
@@ -2144,27 +2183,53 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2144
2183
|
if (typeof this.adapter.isAlive === 'function' && !this.adapter.isAlive()) {
|
|
2145
2184
|
// Dead PTY: nothing to watch. agent:stopped covers the exit; re-arm on
|
|
2146
2185
|
// the next live session so a restart starts a fresh episode.
|
|
2147
|
-
this.
|
|
2148
|
-
this.meshStallEmittedForAnchor = false;
|
|
2186
|
+
this.resetMeshStallEpisode();
|
|
2149
2187
|
return;
|
|
2150
2188
|
}
|
|
2151
2189
|
|
|
2152
2190
|
let lastOutputAt: number;
|
|
2191
|
+
let observedStatus = 'unknown';
|
|
2153
2192
|
try {
|
|
2154
2193
|
// allowParse:false — a cheap status read that must NOT trigger parsing
|
|
2155
2194
|
// and must NOT mutate lastOutputAt (getState/getStatus never bump it).
|
|
2156
|
-
|
|
2195
|
+
// Read once for both the anchor clock and the observedStatus context.
|
|
2196
|
+
const status = this.adapter.getStatus({ allowParse: false }) as { lastOutputAt?: unknown; status?: unknown };
|
|
2157
2197
|
lastOutputAt = typeof status?.lastOutputAt === 'number' && Number.isFinite(status.lastOutputAt)
|
|
2158
2198
|
? status.lastOutputAt
|
|
2159
2199
|
: 0;
|
|
2200
|
+
if (typeof status?.status === 'string' && status.status) observedStatus = status.status;
|
|
2160
2201
|
} catch {
|
|
2161
2202
|
return; // defensive: a failed status read just skips this tick
|
|
2162
2203
|
}
|
|
2163
2204
|
|
|
2205
|
+
// FALSE-STALL-WATCHDOG-OVERFIRE (fix B + C): observe whether a turn is
|
|
2206
|
+
// genuinely in flight. hasAdapterPendingResponse() reads the adapter's
|
|
2207
|
+
// currentTurnScope / isWaitingForResponse / isProcessing / partial buffer —
|
|
2208
|
+
// real liveness, NOT the sticky FSM status. This drives both the turn-end
|
|
2209
|
+
// anchor re-arm (B) and the turn-scoped threshold (C).
|
|
2210
|
+
let turnActive = false;
|
|
2211
|
+
try {
|
|
2212
|
+
turnActive = this.hasAdapterPendingResponse();
|
|
2213
|
+
} catch { /* defensive: missing adapter diagnostics → treat as no active turn */ }
|
|
2214
|
+
const turnEnded = this.meshStallTurnActiveLast === true && !turnActive;
|
|
2215
|
+
this.meshStallTurnActiveLast = turnActive;
|
|
2216
|
+
|
|
2164
2217
|
// The anchor is the last raw output; before any output, the spawn time so a
|
|
2165
2218
|
// silent worker is still caught.
|
|
2166
2219
|
const anchor = lastOutputAt > 0 ? lastOutputAt : this.startedAt;
|
|
2167
2220
|
|
|
2221
|
+
// FALSE-STALL-WATCHDOG-OVERFIRE (fix B): a turn just ended (active → idle).
|
|
2222
|
+
// The completion/idle FSM transition does not touch the stall anchor, so the
|
|
2223
|
+
// post-completion quiet valley would otherwise be measured from the last
|
|
2224
|
+
// pre-completion output and false-fire almost immediately. Force the anchor
|
|
2225
|
+
// forward to `now` so the idle valley starts a fresh clock. This overrides a
|
|
2226
|
+
// stale (pre-completion) anchor even though `anchor` itself did not advance.
|
|
2227
|
+
if (turnEnded && this.meshStallAnchorAt !== -1) {
|
|
2228
|
+
this.meshStallAnchorAt = Math.max(anchor, now);
|
|
2229
|
+
this.meshStallEmittedForAnchor = false;
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2168
2233
|
if (this.meshStallAnchorAt === -1) {
|
|
2169
2234
|
// First observation this session — arm against the current anchor.
|
|
2170
2235
|
this.meshStallAnchorAt = anchor;
|
|
@@ -2181,20 +2246,31 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2181
2246
|
|
|
2182
2247
|
if (this.meshStallEmittedForAnchor) return; // already fired for this stall
|
|
2183
2248
|
|
|
2249
|
+
// FALSE-STALL-WATCHDOG-OVERFIRE (fix C): raise the bar while a turn is in
|
|
2250
|
+
// flight so a long normal thinking gap is absorbed; a genuine mid-turn wedge
|
|
2251
|
+
// still fires — late, at the turn bound — rather than being hidden.
|
|
2252
|
+
const threshold = turnActive
|
|
2253
|
+
? CliProviderInstance.MESH_WORKER_STALL_TURN_THRESHOLD_MS
|
|
2254
|
+
: CliProviderInstance.MESH_WORKER_STALL_IDLE_THRESHOLD_MS;
|
|
2184
2255
|
const stalledMs = now - this.meshStallAnchorAt;
|
|
2185
|
-
if (stalledMs <
|
|
2256
|
+
if (stalledMs < threshold) return;
|
|
2186
2257
|
|
|
2258
|
+
// FALSE-STALL-WATCHDOG-OVERFIRE (fix E): per-session refire cooldown. Mark
|
|
2259
|
+
// this anchor emitted regardless so a still-static anchor does not re-check
|
|
2260
|
+
// every tick, but suppress the actual coordinator notification (and the
|
|
2261
|
+
// trace/event) when the previous emission for this session was too recent.
|
|
2187
2262
|
this.meshStallEmittedForAnchor = true;
|
|
2263
|
+
if (this.meshStallLastFiredAt >= 0
|
|
2264
|
+
&& now - this.meshStallLastFiredAt < CliProviderInstance.MESH_WORKER_STALL_REFIRE_COOLDOWN_MS) {
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
this.meshStallLastFiredAt = now;
|
|
2188
2268
|
|
|
2189
|
-
// observedStatus
|
|
2190
|
-
//
|
|
2269
|
+
// observedStatus (read above from the single allowParse:false status probe)
|
|
2270
|
+
// is surfaced as context only — deliberately NOT stamped as the
|
|
2271
|
+
// reconciliation-triggering `status` field (which would let
|
|
2191
2272
|
// buildNoProgressCompletionReconciliation mistake an idle stall for a
|
|
2192
2273
|
// completion). See mesh-events-stale.buildNoProgressCompletionReconciliation.
|
|
2193
|
-
let observedStatus = 'unknown';
|
|
2194
|
-
try {
|
|
2195
|
-
const s = (this.adapter.getStatus({ allowParse: false }) as { status?: unknown })?.status;
|
|
2196
|
-
if (typeof s === 'string' && s) observedStatus = s;
|
|
2197
|
-
} catch { /* best-effort context only */ }
|
|
2198
2274
|
|
|
2199
2275
|
if (this.isMeshWorkerSession()) {
|
|
2200
2276
|
traceMeshEventStage('fired', this.meshTraceCtx('monitor:no_progress'), 'mesh_worker_stall_watchdog');
|
|
@@ -2217,6 +2293,20 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2217
2293
|
});
|
|
2218
2294
|
}
|
|
2219
2295
|
|
|
2296
|
+
/**
|
|
2297
|
+
* FALSE-STALL-WATCHDOG-OVERFIRE: drop the entire stall episode for this session.
|
|
2298
|
+
* Called when the session is no longer a mesh worker or the PTY is dead — the
|
|
2299
|
+
* next live tick re-arms a fresh episode. Clears the fix-B turn-edge state and
|
|
2300
|
+
* the fix-E refire cooldown alongside the base anchor so a re-armed session
|
|
2301
|
+
* starts with a clean slate (a restart is not throttled by a prior stall).
|
|
2302
|
+
*/
|
|
2303
|
+
private resetMeshStallEpisode(): void {
|
|
2304
|
+
this.meshStallAnchorAt = -1;
|
|
2305
|
+
this.meshStallEmittedForAnchor = false;
|
|
2306
|
+
this.meshStallTurnActiveLast = undefined;
|
|
2307
|
+
this.meshStallLastFiredAt = -1;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2220
2310
|
/**
|
|
2221
2311
|
* AUTOAPPROVE-FLAP-RECUR (Fix A+B): how long a busy blip / modal scroll-out may
|
|
2222
2312
|
* persist before the in-progress settle gate is torn down. For a delegated
|