@adhdev/daemon-core 0.9.82-rc.197 → 0.9.82-rc.198
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 +115 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +117 -16
- package/dist/index.mjs.map +1 -1
- package/dist/providers/spec/driver.d.ts +19 -0
- package/dist/providers/spec/types.d.ts +6 -0
- package/package.json +1 -1
- package/src/commands/router.ts +19 -0
- package/src/providers/spec/cli-adapter.ts +4 -0
- package/src/providers/spec/driver.ts +85 -3
- package/src/providers/spec/types.ts +6 -0
|
@@ -135,7 +135,15 @@ export declare class SpecDriver {
|
|
|
135
135
|
* explicit wake-up there's nothing to trigger the busy → idle
|
|
136
136
|
* downshift. */
|
|
137
137
|
private busyExpiryTimer;
|
|
138
|
+
/** Pending idle-commit timer. Armed when the evaluator first returns idle;
|
|
139
|
+
* fires after idle_hold_ms if no non-idle reading has cancelled it. */
|
|
140
|
+
private idleHoldTimer;
|
|
141
|
+
/** State snapshot captured when the idle hold was armed — emitted on commit. */
|
|
142
|
+
private pendingIdleState;
|
|
138
143
|
private specWatcher;
|
|
144
|
+
/** Ring buffer of committed state transitions (max 50). */
|
|
145
|
+
private stateHistory;
|
|
146
|
+
private prevStateAt;
|
|
139
147
|
constructor(opts: SpecDriverOpts);
|
|
140
148
|
/** Subscribe to outbound events. Returns an unsubscribe fn. */
|
|
141
149
|
subscribe(listener: (ev: DashboardEvent) => void): () => void;
|
|
@@ -147,6 +155,17 @@ export declare class SpecDriver {
|
|
|
147
155
|
col: number;
|
|
148
156
|
};
|
|
149
157
|
shutdown(): void;
|
|
158
|
+
private cancelIdleHold;
|
|
159
|
+
private pushHistory;
|
|
160
|
+
getStateHistory(): ReadonlyArray<{
|
|
161
|
+
stateId: string;
|
|
162
|
+
label: string;
|
|
163
|
+
at: number;
|
|
164
|
+
durationMs: number;
|
|
165
|
+
}>;
|
|
166
|
+
getLastBusyAt(): number;
|
|
167
|
+
hasIdleHoldPending(): boolean;
|
|
168
|
+
getSpecPath(): string;
|
|
150
169
|
private loadSpecOrThrow;
|
|
151
170
|
private buildAdapterOpts;
|
|
152
171
|
private armSpecWatcher;
|
|
@@ -249,6 +249,12 @@ export interface CliSpec {
|
|
|
249
249
|
* Absorbs per-frame flicker in TUIs that stream output through
|
|
250
250
|
* the same region as the spinner. */
|
|
251
251
|
busy_hold_ms?: number;
|
|
252
|
+
/** Min time the idle state must remain matched before it is
|
|
253
|
+
* committed. Filters transient idle flickers that appear during
|
|
254
|
+
* approval dismissals, layout reflows, or brief spinner gaps.
|
|
255
|
+
* Any non-idle reading within the window cancels the transition.
|
|
256
|
+
* When omitted the idle transition is immediate (legacy behaviour). */
|
|
257
|
+
idle_hold_ms?: number;
|
|
252
258
|
/** Min time after start() before a send_message is allowed to
|
|
253
259
|
* reach the PTY. Banner paints + auth flows + skill listings
|
|
254
260
|
* can keep the agent unable to accept input for several seconds
|
package/package.json
CHANGED
package/src/commands/router.ts
CHANGED
|
@@ -4178,6 +4178,25 @@ export class DaemonCommandRouter {
|
|
|
4178
4178
|
};
|
|
4179
4179
|
}
|
|
4180
4180
|
|
|
4181
|
+
case 'get_spec_debug': {
|
|
4182
|
+
const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim()
|
|
4183
|
+
: typeof args?.sessionId === 'string' ? args.sessionId.trim() : '';
|
|
4184
|
+
if (!sessionId) return { success: false, error: 'targetSessionId required' };
|
|
4185
|
+
const target = this.deps.sessionRegistry.get(sessionId);
|
|
4186
|
+
if (!target) return { success: false, error: 'Session not found', sessionId };
|
|
4187
|
+
const adapter = this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter;
|
|
4188
|
+
const snapshot = (adapter && typeof (adapter as any).getDebugSnapshot === 'function')
|
|
4189
|
+
? (adapter as any).getDebugSnapshot()
|
|
4190
|
+
: null;
|
|
4191
|
+
return {
|
|
4192
|
+
success: true,
|
|
4193
|
+
sessionId,
|
|
4194
|
+
providerType: target.providerType,
|
|
4195
|
+
isSpecProvider: snapshot !== null,
|
|
4196
|
+
snapshot,
|
|
4197
|
+
};
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4181
4200
|
// ── User-level coordinator-prompt files (~/.adhdev/coordinator-prompts/).
|
|
4182
4201
|
// These live on this daemon's filesystem and never sync to the
|
|
4183
4202
|
// cloud / other daemons — they're per-machine config. The
|
|
@@ -349,6 +349,10 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
349
349
|
exited: this.exited,
|
|
350
350
|
screen,
|
|
351
351
|
sections,
|
|
352
|
+
stateHistory: this.driver.getStateHistory(),
|
|
353
|
+
idleHoldPending: this.driver.hasIdleHoldPending(),
|
|
354
|
+
lastBusyAt: this.driver.getLastBusyAt(),
|
|
355
|
+
specPath: this.driver.getSpecPath(),
|
|
352
356
|
};
|
|
353
357
|
}
|
|
354
358
|
getRuntimeMetadata(): unknown {
|
|
@@ -218,7 +218,15 @@ export class SpecDriver {
|
|
|
218
218
|
* explicit wake-up there's nothing to trigger the busy → idle
|
|
219
219
|
* downshift. */
|
|
220
220
|
private busyExpiryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
221
|
+
/** Pending idle-commit timer. Armed when the evaluator first returns idle;
|
|
222
|
+
* fires after idle_hold_ms if no non-idle reading has cancelled it. */
|
|
223
|
+
private idleHoldTimer: ReturnType<typeof setTimeout> | null = null;
|
|
224
|
+
/** State snapshot captured when the idle hold was armed — emitted on commit. */
|
|
225
|
+
private pendingIdleState: SpecEvaluation['state'] | null = null;
|
|
221
226
|
private specWatcher: fs.FSWatcher | null = null;
|
|
227
|
+
/** Ring buffer of committed state transitions (max 50). */
|
|
228
|
+
private stateHistory: Array<{ stateId: string; label: string; at: number; durationMs: number }> = [];
|
|
229
|
+
private prevStateAt = 0;
|
|
222
230
|
|
|
223
231
|
constructor(private readonly opts: SpecDriverOpts) {
|
|
224
232
|
this.loadSpecOrThrow();
|
|
@@ -277,10 +285,33 @@ export class SpecDriver {
|
|
|
277
285
|
shutdown(): void {
|
|
278
286
|
for (const t of this.delegateTimers.values()) clearTimeout(t);
|
|
279
287
|
this.delegateTimers.clear();
|
|
288
|
+
this.cancelIdleHold();
|
|
289
|
+
if (this.busyExpiryTimer) { clearTimeout(this.busyExpiryTimer); this.busyExpiryTimer = null; }
|
|
280
290
|
this.specWatcher?.close();
|
|
281
291
|
this.adapter.kill();
|
|
282
292
|
}
|
|
283
293
|
|
|
294
|
+
private cancelIdleHold(): void {
|
|
295
|
+
if (this.idleHoldTimer) { clearTimeout(this.idleHoldTimer); this.idleHoldTimer = null; }
|
|
296
|
+
this.pendingIdleState = null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private pushHistory(stateId: string, label: string): void {
|
|
300
|
+
const now = Date.now();
|
|
301
|
+
const durationMs = this.prevStateAt > 0 ? now - this.prevStateAt : 0;
|
|
302
|
+
this.prevStateAt = now;
|
|
303
|
+
this.stateHistory.push({ stateId, label, at: now, durationMs });
|
|
304
|
+
if (this.stateHistory.length > 50) this.stateHistory.shift();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
getStateHistory(): ReadonlyArray<{ stateId: string; label: string; at: number; durationMs: number }> {
|
|
308
|
+
return this.stateHistory;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
getLastBusyAt(): number { return this.lastBusyAt; }
|
|
312
|
+
hasIdleHoldPending(): boolean { return this.idleHoldTimer !== null; }
|
|
313
|
+
getSpecPath(): string { return this.opts.specPath; }
|
|
314
|
+
|
|
284
315
|
// ────────────────────────────────────────────────────────────────────
|
|
285
316
|
// Loading & adapter wiring
|
|
286
317
|
// ────────────────────────────────────────────────────────────────────
|
|
@@ -307,9 +338,13 @@ export class SpecDriver {
|
|
|
307
338
|
|
|
308
339
|
private armSpecWatcher(): void {
|
|
309
340
|
try {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
341
|
+
// Watch the parent directory so we catch atomic replacements
|
|
342
|
+
// (cp, install scripts) that create a new inode — a file-level
|
|
343
|
+
// watch misses those on macOS because the original inode is gone.
|
|
344
|
+
const dir = path.dirname(this.opts.specPath);
|
|
345
|
+
const base = path.basename(this.opts.specPath);
|
|
346
|
+
this.specWatcher = fs.watch(dir, { persistent: false }, (_event, filename) => {
|
|
347
|
+
if (filename && filename !== base) return;
|
|
313
348
|
const res = loadSpec(this.opts.specPath);
|
|
314
349
|
if (!res.ok) { this.emit({ kind: 'spec_error', errors: res.errors }); return; }
|
|
315
350
|
this.spec = res.spec;
|
|
@@ -411,12 +446,58 @@ export class SpecDriver {
|
|
|
411
446
|
if (evState.id === 'busy') {
|
|
412
447
|
this.lastBusyAt = Date.now();
|
|
413
448
|
this.lastBusyState = evState;
|
|
449
|
+
// Cancel any pending idle commit — non-idle reading invalidates it.
|
|
450
|
+
this.cancelIdleHold();
|
|
414
451
|
// Schedule a re-evaluation when the hold window expires. PTYs
|
|
415
452
|
// typically stop emitting once the agent stops printing (the
|
|
416
453
|
// footer settles), so without an explicit timer the driver
|
|
417
454
|
// never wakes up to downshift to idle and the dashboard sees
|
|
418
455
|
// status stuck at generating long after the turn ended.
|
|
419
456
|
this.scheduleBusyExpiry(busyWakeMs);
|
|
457
|
+
} else if (evState.id !== this.currentStateId && evState.id !== 'busy') {
|
|
458
|
+
// Non-busy modal states (approval, picker, signing_in) also cancel
|
|
459
|
+
// any in-flight idle hold — they are higher-priority than idle.
|
|
460
|
+
if (evState.id !== (this.spec.default_state ?? 'idle')) {
|
|
461
|
+
this.cancelIdleHold();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Idle hold: if idle_hold_ms is set, don't commit idle immediately.
|
|
466
|
+
// Arm a timer; if a non-idle reading arrives before it fires, cancel.
|
|
467
|
+
const idleHoldMs = this.spec.debounce?.idle_hold_ms ?? 0;
|
|
468
|
+
const isIdleState = evState.id === (this.spec.default_state ?? 'idle');
|
|
469
|
+
if (isIdleState && idleHoldMs > 0 && this.currentStateId !== evState.id) {
|
|
470
|
+
if (!this.idleHoldTimer) {
|
|
471
|
+
this.pendingIdleState = evState;
|
|
472
|
+
this.idleHoldTimer = setTimeout(() => {
|
|
473
|
+
this.idleHoldTimer = null;
|
|
474
|
+
const committed = this.pendingIdleState;
|
|
475
|
+
this.pendingIdleState = null;
|
|
476
|
+
if (!committed) return;
|
|
477
|
+
LOG.debug('SpecDriver', `[${this.opts.specPath.split('/').slice(-3).join('/')}] idleHold committed after ${idleHoldMs}ms`);
|
|
478
|
+
this.currentStateId = committed.id;
|
|
479
|
+
this.currentEval = ev;
|
|
480
|
+
this.pushHistory(committed.id, committed.label);
|
|
481
|
+
this.emit({
|
|
482
|
+
kind: 'state_changed',
|
|
483
|
+
state: committed,
|
|
484
|
+
modal: null,
|
|
485
|
+
controls: ev.controls.map(c => ({ id: c.id, label: c.label, action_type: c.actionType })),
|
|
486
|
+
});
|
|
487
|
+
this.armOrCancelDelegateTimers(committed.id);
|
|
488
|
+
if (this.opts.emitTrace) this.emit({ kind: 'spec_trace', entries: ev.trace });
|
|
489
|
+
}, idleHoldMs);
|
|
490
|
+
}
|
|
491
|
+
// Don't fall through to the normal changed/emit path for idle.
|
|
492
|
+
this.currentEval = ev;
|
|
493
|
+
const graceMs2 = this.spec.debounce?.startup_grace_ms ?? STARTUP_GRACE_MS;
|
|
494
|
+
if (!this.idleSeenOnce && Date.now() - this.startedAtMs >= graceMs2) {
|
|
495
|
+
this.idleSeenOnce = true;
|
|
496
|
+
const queued = this.pendingSends.splice(0);
|
|
497
|
+
for (const text of queued) setTimeout(() => this.actuallySendMessage(text), 50);
|
|
498
|
+
}
|
|
499
|
+
if (this.pickerInProgress) this.tryAdvancePicker(screen);
|
|
500
|
+
return;
|
|
420
501
|
}
|
|
421
502
|
|
|
422
503
|
const changed = forceEmit
|
|
@@ -449,6 +530,7 @@ export class SpecDriver {
|
|
|
449
530
|
}
|
|
450
531
|
if (changed) {
|
|
451
532
|
this.currentStateId = evState.id;
|
|
533
|
+
this.pushHistory(evState.id, evState.label);
|
|
452
534
|
this.emit({
|
|
453
535
|
kind: 'state_changed',
|
|
454
536
|
state: evState,
|
|
@@ -268,6 +268,12 @@ export interface CliSpec {
|
|
|
268
268
|
* Absorbs per-frame flicker in TUIs that stream output through
|
|
269
269
|
* the same region as the spinner. */
|
|
270
270
|
busy_hold_ms?: number;
|
|
271
|
+
/** Min time the idle state must remain matched before it is
|
|
272
|
+
* committed. Filters transient idle flickers that appear during
|
|
273
|
+
* approval dismissals, layout reflows, or brief spinner gaps.
|
|
274
|
+
* Any non-idle reading within the window cancels the transition.
|
|
275
|
+
* When omitted the idle transition is immediate (legacy behaviour). */
|
|
276
|
+
idle_hold_ms?: number;
|
|
271
277
|
/** Min time after start() before a send_message is allowed to
|
|
272
278
|
* reach the PTY. Banner paints + auth flows + skill listings
|
|
273
279
|
* can keep the agent unable to accept input for several seconds
|