@adhdev/daemon-core 0.9.82-rc.385 → 0.9.82-rc.386

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.
@@ -66,6 +66,7 @@ export declare class CliProviderInstance implements ProviderInstance {
66
66
  private context;
67
67
  private events;
68
68
  private lastStatus;
69
+ private agentReadyEmitted;
69
70
  private generatingStartedAt;
70
71
  private settings;
71
72
  private monitor;
@@ -220,6 +221,13 @@ export declare class CliProviderInstance implements ProviderInstance {
220
221
  * the approval decision; the next real PTY frame refreshes visible status.
221
222
  */
222
223
  private recheckAutoApproveSettled;
224
+ /**
225
+ * Emit the queue-claim agent:ready event at most once per session. Both the
226
+ * boot-time starting→idle one-shot and the fsmReadySeen re-arm call this; the
227
+ * agentReadyEmitted guard ensures the second caller is a no-op so a worker is
228
+ * never claimed twice and a queued task is never double-dispatched.
229
+ */
230
+ private emitAgentReadyOnce;
223
231
  private detectStatusTransition;
224
232
  private pushEvent;
225
233
  private flushEvents;
@@ -134,6 +134,7 @@ export interface ISpecDriver {
134
134
  }> | null;
135
135
  getLastBusyAt(): number;
136
136
  hasIdleHoldPending(): boolean;
137
+ hasSeenReady(): boolean;
137
138
  getCompletionIdleDebounceState(): {
138
139
  active: boolean;
139
140
  ageMs: number;
@@ -264,6 +265,15 @@ export declare class FsmDriver implements ISpecDriver {
264
265
  * working without branching on driver type. */
265
266
  getLastBusyAt(): number;
266
267
  hasIdleHoldPending(): boolean;
268
+ /**
269
+ * True once the machine has reached its first non-initial idle state (the
270
+ * prompt is genuinely drawn — see maybeMarkReady). The cli-adapter surfaces
271
+ * this on its idle status so CliProviderInstance can re-arm the queue-claim
272
+ * agent:ready on the first genuine ready, independent of the boot-time
273
+ * starting→idle one-shot (which is consumed too early for specs whose
274
+ * initial state already reports idle).
275
+ */
276
+ hasSeenReady(): boolean;
267
277
  getCompletionIdleDebounceState(): {
268
278
  active: boolean;
269
279
  ageMs: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.385",
3
+ "version": "0.9.82-rc.386",
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.385",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.386",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -28,6 +28,18 @@ export interface CliAdapterStatus {
28
28
  providerSessionId?: string;
29
29
  errorMessage?: string;
30
30
  errorReason?: string;
31
+ /**
32
+ * FSM-spec adapters only: true once the driver has observed its first
33
+ * non-initial idle state (the prompt is genuinely drawn — see
34
+ * FsmDriver.maybeMarkReady / readySeenOnce). Used by CliProviderInstance to
35
+ * re-arm the queue-claim `agent:ready` event on the first genuine ready,
36
+ * independent of the boot-time starting→idle one-shot. That one-shot is
37
+ * consumed too early for providers whose INITIAL FSM state already reports
38
+ * status 'idle' (e.g. antigravity-cli), so without this re-arm the worker
39
+ * never claims its queued task and the coordinator relaunch-loops. Absent
40
+ * (undefined) for non-FSM adapters — they keep the boot one-shot behavior.
41
+ */
42
+ fsmReadySeen?: boolean;
31
43
  }
32
44
 
33
45
  export interface AcpAdapterHandle {
@@ -60,6 +60,17 @@ export interface CliSessionStatus {
60
60
  errorMessage?: string;
61
61
  errorReason?: string;
62
62
  providerSessionId?: string;
63
+ /**
64
+ * Spec/FSM adapters only (SpecCliAdapter): true once the driver has observed
65
+ * its first non-initial idle state — the prompt is genuinely drawn. The
66
+ * legacy ProviderCliAdapter never sets it (undefined). CliProviderInstance
67
+ * uses it to re-arm the queue-claim agent:ready on the first genuine ready,
68
+ * independent of the boot-time starting→idle one-shot (which is consumed too
69
+ * early for specs whose initial state already reports status 'idle', e.g.
70
+ * antigravity-cli — without the re-arm the worker never claims its queued
71
+ * task and the coordinator relaunch-loops).
72
+ */
73
+ fsmReadySeen?: boolean;
63
74
  /**
64
75
  * Timestamp (ms) of the most recent raw PTY output chunk. Advances on every
65
76
  * byte the process emits, including tool/build output that produces no
@@ -430,6 +430,14 @@ export class CliProviderInstance implements ProviderInstance {
430
430
  private context: InstanceContext | null = null;
431
431
  private events: ProviderEvent[] = [];
432
432
  private lastStatus: string = 'starting';
433
+ // Idempotency guard for the queue-claim agent:ready event. agent:ready is the
434
+ // sole signal the mesh coordinator's tryAssignQueueTask waits on to hand a
435
+ // queued task to this worker. It is emitted in two places: the boot-time
436
+ // starting→idle one-shot, and the readySeen re-arm below. This flag makes the
437
+ // event fire AT MOST ONCE per session so a worker is never claimed twice and a
438
+ // queued task is never double-dispatched/double-injected. Whichever path fires
439
+ // first sets it; the other becomes a no-op.
440
+ private agentReadyEmitted = false;
433
441
  private generatingStartedAt: number = 0;
434
442
  private settings: Record<string, any> = {};
435
443
  private monitor: StatusMonitor;
@@ -1920,6 +1928,18 @@ export class CliProviderInstance implements ProviderInstance {
1920
1928
  } catch { /* adapter gone / transient — next frame retries */ }
1921
1929
  }
1922
1930
 
1931
+ /**
1932
+ * Emit the queue-claim agent:ready event at most once per session. Both the
1933
+ * boot-time starting→idle one-shot and the fsmReadySeen re-arm call this; the
1934
+ * agentReadyEmitted guard ensures the second caller is a no-op so a worker is
1935
+ * never claimed twice and a queued task is never double-dispatched.
1936
+ */
1937
+ private emitAgentReadyOnce(chatTitle: string, now: number): void {
1938
+ if (this.agentReadyEmitted) return;
1939
+ this.agentReadyEmitted = true;
1940
+ this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
1941
+ }
1942
+
1923
1943
  private detectStatusTransition(): void {
1924
1944
  const now = Date.now();
1925
1945
  // Status-change handling is a hot path: PTY output can fire it many times
@@ -2142,7 +2162,7 @@ export class CliProviderInstance implements ProviderInstance {
2142
2162
  this.scheduleCompletedDebounceFlush(flushDelay);
2143
2163
  }
2144
2164
  } else if (newStatus === 'idle' && this.lastStatus === 'starting') {
2145
- this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
2165
+ this.emitAgentReadyOnce(chatTitle, now);
2146
2166
  } else if (newStatus === 'error') {
2147
2167
  if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
2148
2168
  this.generatingDebouncePending = null;
@@ -2171,6 +2191,28 @@ export class CliProviderInstance implements ProviderInstance {
2171
2191
  this.lastStatus = newStatus;
2172
2192
  }
2173
2193
 
2194
+ // Re-arm the queue-claim agent:ready on the FSM's first GENUINE ready.
2195
+ //
2196
+ // The boot-time starting→idle one-shot above is the historical claim
2197
+ // trigger, but it is consumed too early for FSM-spec providers whose
2198
+ // INITIAL state already reports status 'idle' (e.g. antigravity-cli): the
2199
+ // adapter reports idle before maybeMarkReady has fired, so lastStatus
2200
+ // advances starting→idle while the prompt is not yet drawn and the worker
2201
+ // cannot yet claim. Subsequent state-driven idle frames are idle→idle (no
2202
+ // status change), so the one-shot never re-fires and the worker strands its
2203
+ // queued task — the coordinator then relaunch-loops every ~90s.
2204
+ //
2205
+ // The adapter surfaces fsmReadySeen=true exactly when the FSM reaches its
2206
+ // first non-initial idle (the prompt is genuinely up). On that signal we
2207
+ // emit agent:ready once more. emitAgentReadyOnce is idempotent (guarded by
2208
+ // agentReadyEmitted), so providers whose boot one-shot already landed on a
2209
+ // real ready (claude-cli / codex-cli / hermes-cli, which use a startup
2210
+ // grace and whose initial state is not idle) treat this as a no-op — no
2211
+ // double claim, no double task injection.
2212
+ if (newStatus === 'idle' && adapterStatus.fsmReadySeen === true && !this.agentReadyEmitted) {
2213
+ this.emitAgentReadyOnce(chatTitle, now);
2214
+ }
2215
+
2174
2216
  this.applyProviderResponse(parsedStatus, {
2175
2217
  phase: (newStatus === 'idle' && (previousStatus === 'generating' || previousStatus === 'waiting_approval'))
2176
2218
  ? 'turn_completed'
@@ -206,7 +206,12 @@ export class SpecCliAdapter implements CliAdapter {
206
206
  if (state.status === 'generating') {
207
207
  return { status: 'generating', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
208
208
  }
209
- return { status: 'idle', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
209
+ // fsmReadySeen lets CliProviderInstance re-arm the queue-claim agent:ready
210
+ // on the first genuine ready (prompt drawn), independent of the boot-time
211
+ // starting→idle one-shot that the provider-instance otherwise relies on.
212
+ // Surfaced only on idle so the provider-instance fires agent:ready exactly
213
+ // when the worker is actually ready to claim.
214
+ return { status: 'idle', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, fsmReadySeen: this.driver.hasSeenReady?.() ?? false, ...sessionFields };
210
215
  }
211
216
 
212
217
  private maybeRefreshNativeHistory(): void {
@@ -125,6 +125,7 @@ export interface ISpecDriver {
125
125
  getSections(): Array<{ id: string; text: string }> | null;
126
126
  getLastBusyAt(): number;
127
127
  hasIdleHoldPending(): boolean;
128
+ hasSeenReady(): boolean;
128
129
  getCompletionIdleDebounceState(): { active: boolean; ageMs: number; holdMs: number; forceAfterMs: number } | null;
129
130
  getFsmDebug?(): unknown;
130
131
  getFsmSnapshotHistory?(): ReadonlyArray<FsmSnapshotEntry>;
@@ -466,6 +467,17 @@ export class FsmDriver implements ISpecDriver {
466
467
  // Report true while any outgoing transition is hold-blocked.
467
468
  return (this.lastFsmEval?.transitions ?? []).some(t => !t.holdSatisfied && t.condResult);
468
469
  }
470
+ /**
471
+ * True once the machine has reached its first non-initial idle state (the
472
+ * prompt is genuinely drawn — see maybeMarkReady). The cli-adapter surfaces
473
+ * this on its idle status so CliProviderInstance can re-arm the queue-claim
474
+ * agent:ready on the first genuine ready, independent of the boot-time
475
+ * starting→idle one-shot (which is consumed too early for specs whose
476
+ * initial state already reports idle).
477
+ */
478
+ hasSeenReady(): boolean {
479
+ return this.readySeenOnce;
480
+ }
469
481
  getCompletionIdleDebounceState(): { active: boolean; ageMs: number; holdMs: number; forceAfterMs: number } | null {
470
482
  // Surface the busy→ready transition's stable countdown, if any, so the
471
483
  // existing panel field stays meaningful.