@adhdev/daemon-core 0.9.82-rc.342 → 0.9.82-rc.343

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.
@@ -177,6 +177,10 @@ export declare class FsmDriver implements ISpecDriver {
177
177
  * after a re-prime we don't re-inject until the screen changes (which
178
178
  * resets the stall reference) or another full stall window lapses. */
179
179
  private lastRefocusAt;
180
+ /** Timer driving the win32 verification-based submit resend loop (see
181
+ * WIN32_SUBMIT_* and scheduleWin32Submit). Re-arms itself until the FSM
182
+ * leaves idle (submitted) or the resend budget is spent. */
183
+ private win32SubmitTimer;
180
184
  private currentEval;
181
185
  private stateHistory;
182
186
  private prevStateAt;
@@ -294,6 +298,18 @@ export declare class FsmDriver implements ISpecDriver {
294
298
  private fireDelegate;
295
299
  private handleSendMessage;
296
300
  private actuallySendMessage;
301
+ /** The agent's current coarse status, derived from the FSM node we're in. */
302
+ private currentStatus;
303
+ /**
304
+ * win32 verification-based submit. Sends the submit key, waits a gap, and if
305
+ * the FSM is still 'idle' (the prompt did not submit — the CR was absorbed as
306
+ * a multiline-paste newline) resends, up to WIN32_SUBMIT_MAX_RESENDS. The
307
+ * first CR always fires (so a stale/edge status never suppresses the submit);
308
+ * subsequent resends are gated on still being idle, and stop the instant the
309
+ * agent leaves idle (submitted → generating / approval). This converges the
310
+ * nondeterministic multiline window without spamming Enter into the next turn.
311
+ */
312
+ private scheduleWin32Submit;
297
313
  private handleClickControl;
298
314
  private handleClickModalButton;
299
315
  private handleAttachImage;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.342",
3
+ "version": "0.9.82-rc.343",
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.342",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.343",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -369,9 +369,23 @@ export function tryAssignQueueTask(
369
369
  }).then(() => {
370
370
  updateSessionDeliveryStatus(delivery.id, 'delivered');
371
371
  }).catch((e: any) => {
372
+ // Mirror the remote-dispatch catch above: a local dispatch failure is most often a
373
+ // transient busy/refusal (e.g. the adapter rejected send_chat while mid-generation),
374
+ // not a permanent task failure. Marking the task terminal 'failed' here with no ledger
375
+ // and no retry permanently killed tasks that a later tick would have delivered fine.
376
+ // Return the task to 'pending' and record a retryable dispatch_failed ledger entry so
377
+ // the reconcile loop re-dispatches it, exactly as the remote branch does.
372
378
  LOG.error('MeshQueue', `Failed to dispatch task locally to node ${nodeId}: ${e?.message}`);
373
379
  updateSessionDeliveryStatus(delivery.id, 'failed', { lastError: e?.message, incrementAttempt: true });
374
- updateTaskStatus(meshId, task.id, 'failed');
380
+ updateTaskStatus(meshId, task.id, 'pending');
381
+ try {
382
+ appendLedgerEntry(meshId, {
383
+ kind: 'dispatch_failed' as any,
384
+ nodeId,
385
+ sessionId,
386
+ payload: { taskId: task.id, deliveryId: delivery.id, error: e?.message, retryable: true },
387
+ });
388
+ } catch { /* ledger write is best-effort */ }
375
389
  });
376
390
 
377
391
  return true;
@@ -145,13 +145,23 @@ function countNewlines(s: string): number {
145
145
 
146
146
  const SUBMIT_DELAY_FLOOR_MS = 200;
147
147
 
148
- // win32 ConPTY submit reliability: after the text settles we write the submit
149
- // key (CR) as its OWN keystroke, then repeat it once after a short gap. The
150
- // repeat is the safety net for the case where ConPTY drops/coalesces the first
151
- // lone CR; on an already-submitted prompt the second CR lands on an empty input
152
- // and is a harmless no-op. The gap must be long enough that the TUI has redrawn
153
- // after the first CR before the second arrives.
154
- const WIN32_SUBMIT_REPEAT_GAP_MS = 300;
148
+ // win32 ConPTY submit reliability. A MULTILINE message creates an Ink
149
+ // paste/newline-accumulation window during which a lone CR is absorbed as a
150
+ // literal newline instead of submitting; the window's length is
151
+ // nondeterministic (observed 0–~2s, driven by ConPTY byte timing), so neither a
152
+ // fixed delay nor a fixed CR count reliably submits. (A/B PTY testing on win32
153
+ // ConPTY: single-line submits on the FIRST CR; multiline needs a *variable*
154
+ // number of CRs as the window expires — a fixed double-CR fails outright, and
155
+ // bracketed-paste wrapping does NOT help.) So we VERIFY instead of guessing:
156
+ // write the text, then resend the submit key on a fixed cadence until the FSM
157
+ // observes the agent has actually left the idle composer (status flips away from
158
+ // 'idle' — i.e. it submitted and is generating / showing a modal), bounded by a
159
+ // retry budget. Once submission is observed we stop so we don't spam Enter into
160
+ // the next turn. Single-line messages satisfy the check after the first CR, so
161
+ // their behaviour is unchanged. CRs absorbed as newlines during the window are
162
+ // trimmed by the TUI on submit.
163
+ const WIN32_SUBMIT_RESEND_GAP_MS = 350;
164
+ const WIN32_SUBMIT_MAX_RESENDS = 14;
155
165
 
156
166
  export function resolveSubmitDelayMs(specBeforeSubmit: number | undefined, text: string): number {
157
167
  const lines = countNewlines(text);
@@ -217,6 +227,10 @@ export class FsmDriver implements ISpecDriver {
217
227
  * after a re-prime we don't re-inject until the screen changes (which
218
228
  * resets the stall reference) or another full stall window lapses. */
219
229
  private lastRefocusAt = 0;
230
+ /** Timer driving the win32 verification-based submit resend loop (see
231
+ * WIN32_SUBMIT_* and scheduleWin32Submit). Re-arms itself until the FSM
232
+ * leaves idle (submitted) or the resend budget is spent. */
233
+ private win32SubmitTimer: ReturnType<typeof setTimeout> | null = null;
220
234
 
221
235
  private currentEval: CurrentEval | null = null;
222
236
  private stateHistory: HistoryEntry[] = [];
@@ -334,6 +348,7 @@ export class FsmDriver implements ISpecDriver {
334
348
  this.delegateTimers.clear();
335
349
  if (this.wakeTimer) { clearTimeout(this.wakeTimer); this.wakeTimer = null; }
336
350
  if (this.stallTimer) { clearTimeout(this.stallTimer); this.stallTimer = null; }
351
+ if (this.win32SubmitTimer) { clearTimeout(this.win32SubmitTimer); this.win32SubmitTimer = null; }
337
352
  this.specWatcher?.close();
338
353
  this.adapter.kill();
339
354
  }
@@ -841,25 +856,19 @@ export class FsmDriver implements ISpecDriver {
841
856
  const beforeSubmit = resolveSubmitDelayMs(sm.delay_ms_before_submit, text);
842
857
 
843
858
  // win32 ConPTY submit: the text and the submit key (CR) must NOT be
844
- // combined into one PTY write. Ink-based TUIs (claude-cli) treat a single
845
- // write that carries text + a trailing CR as a bracketed/multi-line paste
846
- // and absorb the CR as a literal newline in the input box instead of a
847
- // submit keystroke the prompt sits typed-but-unsent until the user hits
848
- // Enter manually. (A previous "atomic write" attempt regressed exactly
849
- // this way.) Instead, write the text, let it settle so the TUI leaves any
850
- // paste-accumulation state, then deliver the CR as its OWN keystroke. We
851
- // send the CR twice with a short gap: ConPTY can drop/coalesce the first
852
- // lone CR, and a second CR on an already-submitted (now empty) prompt is a
853
- // harmless no-op. perChar typing simulation is skipped on win32;
854
- // correctness of submission wins over the typing visual there.
859
+ // combined into one PTY write Ink-based TUIs (claude-cli) treat a
860
+ // single write that carries text + a trailing CR as a bracketed/multi-line
861
+ // paste and absorb the CR as a literal newline. So we write the text on
862
+ // its own, then resend the submit key on a fixed cadence, VERIFYING after
863
+ // each that the agent actually left the idle composer (status flipped away
864
+ // from 'idle'). This handles the nondeterministic multiline
865
+ // paste-accumulation window where a variable number of CRs is needed; a
866
+ // fixed double-CR fails for multiline (see WIN32_SUBMIT_* above). perChar
867
+ // typing simulation is skipped on win32; correctness of submission wins
868
+ // over the typing visual there.
855
869
  if (process.platform === 'win32') {
856
- const submitTwice = (): void => {
857
- this.adapter.send_keys(sm.submit_key);
858
- setTimeout(() => this.adapter.send_keys(sm.submit_key), WIN32_SUBMIT_REPEAT_GAP_MS);
859
- };
860
870
  this.adapter.send_keys(text);
861
- if (beforeSubmit > 0) setTimeout(submitTwice, beforeSubmit);
862
- else submitTwice();
871
+ this.scheduleWin32Submit(sm.submit_key, beforeSubmit);
863
872
  return;
864
873
  }
865
874
 
@@ -881,6 +890,37 @@ export class FsmDriver implements ISpecDriver {
881
890
  }, perChar);
882
891
  }
883
892
 
893
+ /** The agent's current coarse status, derived from the FSM node we're in. */
894
+ private currentStatus(): 'idle' | 'generating' | 'approval' {
895
+ const st = stateById(this.spec, this.currentStateId);
896
+ return st ? statusForState(st) : 'idle';
897
+ }
898
+
899
+ /**
900
+ * win32 verification-based submit. Sends the submit key, waits a gap, and if
901
+ * the FSM is still 'idle' (the prompt did not submit — the CR was absorbed as
902
+ * a multiline-paste newline) resends, up to WIN32_SUBMIT_MAX_RESENDS. The
903
+ * first CR always fires (so a stale/edge status never suppresses the submit);
904
+ * subsequent resends are gated on still being idle, and stop the instant the
905
+ * agent leaves idle (submitted → generating / approval). This converges the
906
+ * nondeterministic multiline window without spamming Enter into the next turn.
907
+ */
908
+ private scheduleWin32Submit(submitKey: string, initialDelayMs: number): void {
909
+ if (this.win32SubmitTimer) { clearTimeout(this.win32SubmitTimer); this.win32SubmitTimer = null; }
910
+ const fire = (attempt: number): void => {
911
+ this.win32SubmitTimer = null;
912
+ this.adapter.send_keys(submitKey);
913
+ if (attempt + 1 >= WIN32_SUBMIT_MAX_RESENDS) return;
914
+ this.win32SubmitTimer = setTimeout(() => {
915
+ // Left the idle composer → it submitted; stop resending.
916
+ if (this.currentStatus() !== 'idle') { this.win32SubmitTimer = null; return; }
917
+ fire(attempt + 1);
918
+ }, WIN32_SUBMIT_RESEND_GAP_MS);
919
+ };
920
+ if (initialDelayMs > 0) this.win32SubmitTimer = setTimeout(() => fire(0), initialDelayMs);
921
+ else fire(0);
922
+ }
923
+
884
924
  private handleClickControl(controlId: string, payload?: unknown): void {
885
925
  const ctl = (this.spec.control_bar ?? []).find(c => c.id === controlId);
886
926
  if (!ctl) return;