@adhdev/daemon-core 0.9.82-rc.329 → 0.9.82-rc.330
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 +110 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +110 -11
- package/dist/index.mjs.map +1 -1
- package/dist/providers/spec/fsm-driver.d.ts +28 -0
- package/dist/providers/spec/fsm-types.d.ts +16 -0
- package/package.json +2 -2
- package/src/mesh/mesh-events-coordinator.ts +8 -1
- package/src/providers/cli-provider-instance.ts +8 -0
- package/src/providers/spec/fsm-driver.ts +89 -5
- package/src/providers/spec/fsm-loader.ts +8 -0
- package/src/providers/spec/fsm-types.ts +16 -0
|
@@ -169,6 +169,14 @@ export declare class FsmDriver implements ISpecDriver {
|
|
|
169
169
|
/** Timer that re-runs evaluate() when a time-condition would flip true
|
|
170
170
|
* with no PTY frame to trigger it. */
|
|
171
171
|
private wakeTimer;
|
|
172
|
+
/** Timer driving the focus-gated stall watchdog (refocus_when_stalled_ms).
|
|
173
|
+
* Re-arms itself while the machine is generating so a re-prime can fire
|
|
174
|
+
* even when the PTY has gone completely quiet. */
|
|
175
|
+
private stallTimer;
|
|
176
|
+
/** Wall-clock time the last stall re-prime was injected. The cooldown gate:
|
|
177
|
+
* after a re-prime we don't re-inject until the screen changes (which
|
|
178
|
+
* resets the stall reference) or another full stall window lapses. */
|
|
179
|
+
private lastRefocusAt;
|
|
172
180
|
private currentEval;
|
|
173
181
|
private stateHistory;
|
|
174
182
|
private prevStateAt;
|
|
@@ -187,6 +195,11 @@ export declare class FsmDriver implements ISpecDriver {
|
|
|
187
195
|
subscribe(listener: (ev: DashboardEvent) => void): () => void;
|
|
188
196
|
start(): void;
|
|
189
197
|
private scheduleSpawnPrime;
|
|
198
|
+
/** Write the declared `send_on_spawn` sequences to the PTY once. Used both
|
|
199
|
+
* at spawn (scheduleSpawnPrime) and, for focus-gated TUIs, by the stall
|
|
200
|
+
* watchdog to re-inject the focus-in wake mid-turn. No-op when the spec
|
|
201
|
+
* declares no prime, so non-focus-gated CLIs are never poked. */
|
|
202
|
+
private sendSpawnPrime;
|
|
190
203
|
dispatch(cmd: DashboardCommand): void;
|
|
191
204
|
snapshot(): string;
|
|
192
205
|
getCursorPosition(): {
|
|
@@ -260,6 +273,21 @@ export declare class FsmDriver implements ISpecDriver {
|
|
|
260
273
|
* this, a state whose only exit is time-based would never leave once the
|
|
261
274
|
* PTY goes quiet. */
|
|
262
275
|
private scheduleWakeForState;
|
|
276
|
+
/** True when this spec opts into stall recovery (declares a positive
|
|
277
|
+
* refocus window AND a wake sequence to re-inject). */
|
|
278
|
+
private stallRecoveryEnabled;
|
|
279
|
+
/** Wall-clock time the screen last changed in the current state, falling
|
|
280
|
+
* back to state entry when it has not changed since (i.e. fully stalled
|
|
281
|
+
* from the start of the state). */
|
|
282
|
+
private lastScreenChangeAt;
|
|
283
|
+
/** Arm a timer to re-inject the focus-in prime if the screen stays frozen
|
|
284
|
+
* through a `generating` state. Only active for opted-in focus-gated specs;
|
|
285
|
+
* a no-op (and cleared) for every other CLI and every non-generating state.
|
|
286
|
+
* Re-arms itself so it keeps watching while the PTY is quiet. */
|
|
287
|
+
private scheduleStallWatchdog;
|
|
288
|
+
/** Fire when the stall window elapses: if the screen is still frozen and we
|
|
289
|
+
* are still generating, re-inject the focus-in wake once, then re-arm. */
|
|
290
|
+
private onStallTick;
|
|
263
291
|
private maybeMarkReady;
|
|
264
292
|
private fireNotifications;
|
|
265
293
|
private armOrCancelDelegateTimers;
|
|
@@ -95,6 +95,22 @@ export interface CliSpecV4 {
|
|
|
95
95
|
send_on_spawn?: string[];
|
|
96
96
|
/** Delay (ms) after spawn before writing `send_on_spawn`. Default 250. */
|
|
97
97
|
send_on_spawn_delay_ms?: number;
|
|
98
|
+
/**
|
|
99
|
+
* Opt-in stall recovery for focus-gated TUIs. The same CLIs that need
|
|
100
|
+
* `send_on_spawn` (focus-event TUIs like antigravity's `agy`) also stop
|
|
101
|
+
* rendering / flushing output the moment they believe they have lost focus
|
|
102
|
+
* MID-TURN — the screen freezes and only repaints once the user presses a
|
|
103
|
+
* key, which the daemon never does on its own. When this field is set and
|
|
104
|
+
* the machine is in a `generating`-status state whose screen has not changed
|
|
105
|
+
* for `refocus_when_stalled_ms`, the engine re-injects the `send_on_spawn`
|
|
106
|
+
* prime (the focus-in event) once to wake the render loop, then waits for the
|
|
107
|
+
* screen to change or for the stall window to lapse again before re-priming
|
|
108
|
+
* (a cooldown that prevents a tight re-prime loop). Requires `send_on_spawn`
|
|
109
|
+
* to carry the wake sequence; with no `send_on_spawn` there is nothing to
|
|
110
|
+
* re-inject and the engine does nothing. Omitted by every non-focus-gated
|
|
111
|
+
* CLI spec, so the engine stays CLI-agnostic.
|
|
112
|
+
*/
|
|
113
|
+
refocus_when_stalled_ms?: number;
|
|
98
114
|
/**
|
|
99
115
|
* Optional pre-spawn folder-trust step. When present, the engine adds the
|
|
100
116
|
* launch workspace path to the declared trusted-folders array before
|
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.330",
|
|
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",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"author": "vilmire",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.330",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -1399,6 +1399,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1399
1399
|
meshId: args.meshId,
|
|
1400
1400
|
nodeId: eventNodeId || undefined,
|
|
1401
1401
|
...args.metadataEvent,
|
|
1402
|
+
// Ensure a `workspace` field reaches updateMeshOwnedSession even when the
|
|
1403
|
+
// worker provider event only carried `workspaceName`. The merge spread of
|
|
1404
|
+
// metadataEvent above wins when it already has a non-empty `workspace`.
|
|
1405
|
+
workspace: readNonEmptyString(args.metadataEvent.workspace)
|
|
1406
|
+
|| readNonEmptyString(args.metadataEvent.workspaceName)
|
|
1407
|
+
|| undefined,
|
|
1402
1408
|
});
|
|
1403
1409
|
} catch { /* dashboard metadata sync is best-effort */ }
|
|
1404
1410
|
}
|
|
@@ -1814,7 +1820,8 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1814
1820
|
meshId: args.meshId,
|
|
1815
1821
|
nodeLabel: args.nodeLabel,
|
|
1816
1822
|
nodeId: args.nodeId || undefined,
|
|
1817
|
-
workspace: readNonEmptyString(args.metadataEvent.workspace)
|
|
1823
|
+
workspace: readNonEmptyString(args.metadataEvent.workspace)
|
|
1824
|
+
|| readNonEmptyString(args.metadataEvent.workspaceName),
|
|
1818
1825
|
metadataEvent: {
|
|
1819
1826
|
...args.metadataEvent,
|
|
1820
1827
|
...(recoveryContext ? { recoveryContext } : {}),
|
|
@@ -1816,6 +1816,14 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1816
1816
|
workspaceName: typeof event.workspaceName === 'string' && event.workspaceName.trim()
|
|
1817
1817
|
? event.workspaceName
|
|
1818
1818
|
: this.workingDir,
|
|
1819
|
+
// Carry the workspace under BOTH `workspace` and `workspaceName` so the
|
|
1820
|
+
// downstream mesh forward/merge path — which reads `workspace` — can
|
|
1821
|
+
// propagate it to the coordinator snapshot. Without `workspace` the live
|
|
1822
|
+
// event path delivers an empty workspace and the dashboard falls back to
|
|
1823
|
+
// the generic "Terminal (Mesh Node)" title.
|
|
1824
|
+
workspace: typeof event.workspace === 'string' && event.workspace.trim()
|
|
1825
|
+
? event.workspace
|
|
1826
|
+
: this.workingDir,
|
|
1819
1827
|
providerSessionId: typeof event.providerSessionId === 'string' && event.providerSessionId.trim()
|
|
1820
1828
|
? event.providerSessionId
|
|
1821
1829
|
: this.providerSessionId,
|
|
@@ -201,6 +201,14 @@ export class FsmDriver implements ISpecDriver {
|
|
|
201
201
|
/** Timer that re-runs evaluate() when a time-condition would flip true
|
|
202
202
|
* with no PTY frame to trigger it. */
|
|
203
203
|
private wakeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
204
|
+
/** Timer driving the focus-gated stall watchdog (refocus_when_stalled_ms).
|
|
205
|
+
* Re-arms itself while the machine is generating so a re-prime can fire
|
|
206
|
+
* even when the PTY has gone completely quiet. */
|
|
207
|
+
private stallTimer: ReturnType<typeof setTimeout> | null = null;
|
|
208
|
+
/** Wall-clock time the last stall re-prime was injected. The cooldown gate:
|
|
209
|
+
* after a re-prime we don't re-inject until the screen changes (which
|
|
210
|
+
* resets the stall reference) or another full stall window lapses. */
|
|
211
|
+
private lastRefocusAt = 0;
|
|
204
212
|
|
|
205
213
|
private currentEval: CurrentEval | null = null;
|
|
206
214
|
private stateHistory: HistoryEntry[] = [];
|
|
@@ -268,11 +276,19 @@ export class FsmDriver implements ISpecDriver {
|
|
|
268
276
|
const seqs = this.spec.send_on_spawn;
|
|
269
277
|
if (!Array.isArray(seqs) || seqs.length === 0) return;
|
|
270
278
|
const delay = Math.max(0, this.spec.send_on_spawn_delay_ms ?? 250);
|
|
271
|
-
setTimeout(() =>
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
279
|
+
setTimeout(() => this.sendSpawnPrime(), delay);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Write the declared `send_on_spawn` sequences to the PTY once. Used both
|
|
283
|
+
* at spawn (scheduleSpawnPrime) and, for focus-gated TUIs, by the stall
|
|
284
|
+
* watchdog to re-inject the focus-in wake mid-turn. No-op when the spec
|
|
285
|
+
* declares no prime, so non-focus-gated CLIs are never poked. */
|
|
286
|
+
private sendSpawnPrime(): void {
|
|
287
|
+
const seqs = this.spec.send_on_spawn;
|
|
288
|
+
if (!Array.isArray(seqs) || seqs.length === 0) return;
|
|
289
|
+
for (const seq of seqs) {
|
|
290
|
+
if (typeof seq === 'string' && seq.length > 0) this.adapter.send_keys(seq);
|
|
291
|
+
}
|
|
276
292
|
}
|
|
277
293
|
|
|
278
294
|
dispatch(cmd: DashboardCommand): void {
|
|
@@ -309,6 +325,7 @@ export class FsmDriver implements ISpecDriver {
|
|
|
309
325
|
for (const t of this.delegateTimers.values()) clearTimeout(t);
|
|
310
326
|
this.delegateTimers.clear();
|
|
311
327
|
if (this.wakeTimer) { clearTimeout(this.wakeTimer); this.wakeTimer = null; }
|
|
328
|
+
if (this.stallTimer) { clearTimeout(this.stallTimer); this.stallTimer = null; }
|
|
312
329
|
this.specWatcher?.close();
|
|
313
330
|
this.adapter.kill();
|
|
314
331
|
}
|
|
@@ -484,6 +501,7 @@ export class FsmDriver implements ISpecDriver {
|
|
|
484
501
|
// that's already satisfied doesn't wait for the next PTY frame.
|
|
485
502
|
this.emitStateChanged(forceEmit);
|
|
486
503
|
this.scheduleWakeForState();
|
|
504
|
+
this.scheduleStallWatchdog();
|
|
487
505
|
// Drain queued sends on the SAME frame the machine reaches "ready".
|
|
488
506
|
// The first delegated message is queued in pendingSends until the
|
|
489
507
|
// FSM first enters a non-initial idle state (the prompt is drawn).
|
|
@@ -504,6 +522,7 @@ export class FsmDriver implements ISpecDriver {
|
|
|
504
522
|
this.emitStateChanged(forceEmit);
|
|
505
523
|
// Schedule a wake for the soonest pending time-condition.
|
|
506
524
|
this.scheduleWakeForState();
|
|
525
|
+
this.scheduleStallWatchdog();
|
|
507
526
|
// Drain queued sends once we first reach a "ready" state.
|
|
508
527
|
this.maybeMarkReady();
|
|
509
528
|
}
|
|
@@ -685,6 +704,71 @@ export class FsmDriver implements ISpecDriver {
|
|
|
685
704
|
this.wakeTimer = setTimeout(() => { this.wakeTimer = null; this.reevaluate(); }, Math.max(soonest + 30, 50));
|
|
686
705
|
}
|
|
687
706
|
|
|
707
|
+
// ── Focus-gated stall watchdog (refocus_when_stalled_ms) ──────────────────
|
|
708
|
+
//
|
|
709
|
+
// A focus-event TUI (antigravity's `agy`) freezes its render loop the moment
|
|
710
|
+
// it thinks it has lost focus mid-turn: the screen stops updating and only
|
|
711
|
+
// repaints on the next keypress, which the daemon never sends. The output
|
|
712
|
+
// pump is wired entirely to PTY onData (no PTY data → no reevaluate, no
|
|
713
|
+
// on_screen_changed), so a normal time-wake that only re-reads the screen
|
|
714
|
+
// can't help — there is nothing new to read. The fix is to re-inject the
|
|
715
|
+
// focus-in wake (`send_on_spawn`) so the CLI flushes the held output itself.
|
|
716
|
+
|
|
717
|
+
/** True when this spec opts into stall recovery (declares a positive
|
|
718
|
+
* refocus window AND a wake sequence to re-inject). */
|
|
719
|
+
private stallRecoveryEnabled(): boolean {
|
|
720
|
+
const ms = this.spec.refocus_when_stalled_ms;
|
|
721
|
+
return typeof ms === 'number' && ms > 0
|
|
722
|
+
&& Array.isArray(this.spec.send_on_spawn) && this.spec.send_on_spawn.length > 0;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** Wall-clock time the screen last changed in the current state, falling
|
|
726
|
+
* back to state entry when it has not changed since (i.e. fully stalled
|
|
727
|
+
* from the start of the state). */
|
|
728
|
+
private lastScreenChangeAt(): number {
|
|
729
|
+
return this.regionLastChangedAt.get(-1) ?? this.stateEnteredAt;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/** Arm a timer to re-inject the focus-in prime if the screen stays frozen
|
|
733
|
+
* through a `generating` state. Only active for opted-in focus-gated specs;
|
|
734
|
+
* a no-op (and cleared) for every other CLI and every non-generating state.
|
|
735
|
+
* Re-arms itself so it keeps watching while the PTY is quiet. */
|
|
736
|
+
private scheduleStallWatchdog(): void {
|
|
737
|
+
if (this.stallTimer) { clearTimeout(this.stallTimer); this.stallTimer = null; }
|
|
738
|
+
if (!this.stallRecoveryEnabled()) return;
|
|
739
|
+
const st = stateById(this.spec, this.currentStateId);
|
|
740
|
+
if (!st || statusForState(st) !== 'generating') return;
|
|
741
|
+
const windowMs = this.spec.refocus_when_stalled_ms as number;
|
|
742
|
+
// Cooldown reference: a re-prime defers the next one by a full window,
|
|
743
|
+
// even if the screen has not yet repainted, so we don't tight-loop.
|
|
744
|
+
const since = Math.max(this.lastScreenChangeAt(), this.lastRefocusAt);
|
|
745
|
+
const remaining = windowMs - (Date.now() - since);
|
|
746
|
+
this.stallTimer = setTimeout(
|
|
747
|
+
() => { this.stallTimer = null; this.onStallTick(); },
|
|
748
|
+
Math.max(remaining + 30, 50),
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** Fire when the stall window elapses: if the screen is still frozen and we
|
|
753
|
+
* are still generating, re-inject the focus-in wake once, then re-arm. */
|
|
754
|
+
private onStallTick(): void {
|
|
755
|
+
if (!this.stallRecoveryEnabled()) return;
|
|
756
|
+
const st = stateById(this.spec, this.currentStateId);
|
|
757
|
+
if (!st || statusForState(st) !== 'generating') return;
|
|
758
|
+
const windowMs = this.spec.refocus_when_stalled_ms as number;
|
|
759
|
+
const now = Date.now();
|
|
760
|
+
const stalledFor = now - this.lastScreenChangeAt();
|
|
761
|
+
const sinceLastRefocus = now - this.lastRefocusAt;
|
|
762
|
+
if (stalledFor >= windowMs && sinceLastRefocus >= windowMs) {
|
|
763
|
+
LOG.info('FsmDriver', `[${this.specTag()}] stall detected (${stalledFor}ms quiet, generating) — re-injecting focus-in`);
|
|
764
|
+
this.sendSpawnPrime();
|
|
765
|
+
this.lastRefocusAt = now;
|
|
766
|
+
}
|
|
767
|
+
// Keep watching: the re-prime may not flush instantly, and a still-quiet
|
|
768
|
+
// screen needs the next window to come around.
|
|
769
|
+
this.scheduleStallWatchdog();
|
|
770
|
+
}
|
|
771
|
+
|
|
688
772
|
private maybeMarkReady(): void {
|
|
689
773
|
if (this.readySeenOnce) return;
|
|
690
774
|
const st = stateById(this.spec, this.currentStateId);
|
|
@@ -46,6 +46,14 @@ export function validateFsmSpec(raw: unknown): string[] {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
if (spec.refocus_when_stalled_ms !== undefined) {
|
|
50
|
+
if (typeof spec.refocus_when_stalled_ms !== 'number' || !(spec.refocus_when_stalled_ms > 0)) {
|
|
51
|
+
errs.push('refocus_when_stalled_ms must be a positive number');
|
|
52
|
+
} else if (!Array.isArray(spec.send_on_spawn) || spec.send_on_spawn.length === 0) {
|
|
53
|
+
errs.push('refocus_when_stalled_ms requires send_on_spawn (the wake sequence to re-inject)');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
if (!Array.isArray(spec.states) || spec.states.length === 0) {
|
|
50
58
|
errs.push('states[] must be a non-empty array');
|
|
51
59
|
return errs;
|
|
@@ -160,6 +160,22 @@ export interface CliSpecV4 {
|
|
|
160
160
|
send_on_spawn?: string[];
|
|
161
161
|
/** Delay (ms) after spawn before writing `send_on_spawn`. Default 250. */
|
|
162
162
|
send_on_spawn_delay_ms?: number;
|
|
163
|
+
/**
|
|
164
|
+
* Opt-in stall recovery for focus-gated TUIs. The same CLIs that need
|
|
165
|
+
* `send_on_spawn` (focus-event TUIs like antigravity's `agy`) also stop
|
|
166
|
+
* rendering / flushing output the moment they believe they have lost focus
|
|
167
|
+
* MID-TURN — the screen freezes and only repaints once the user presses a
|
|
168
|
+
* key, which the daemon never does on its own. When this field is set and
|
|
169
|
+
* the machine is in a `generating`-status state whose screen has not changed
|
|
170
|
+
* for `refocus_when_stalled_ms`, the engine re-injects the `send_on_spawn`
|
|
171
|
+
* prime (the focus-in event) once to wake the render loop, then waits for the
|
|
172
|
+
* screen to change or for the stall window to lapse again before re-priming
|
|
173
|
+
* (a cooldown that prevents a tight re-prime loop). Requires `send_on_spawn`
|
|
174
|
+
* to carry the wake sequence; with no `send_on_spawn` there is nothing to
|
|
175
|
+
* re-inject and the engine does nothing. Omitted by every non-focus-gated
|
|
176
|
+
* CLI spec, so the engine stays CLI-agnostic.
|
|
177
|
+
*/
|
|
178
|
+
refocus_when_stalled_ms?: number;
|
|
163
179
|
/**
|
|
164
180
|
* Optional pre-spawn folder-trust step. When present, the engine adds the
|
|
165
181
|
* launch workspace path to the declared trusted-folders array before
|