@adhdev/daemon-core 0.9.82-rc.341 → 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.341",
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.341",
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,6 +145,24 @@ function countNewlines(s: string): number {
145
145
 
146
146
  const SUBMIT_DELAY_FLOOR_MS = 200;
147
147
 
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;
165
+
148
166
  export function resolveSubmitDelayMs(specBeforeSubmit: number | undefined, text: string): number {
149
167
  const lines = countNewlines(text);
150
168
  const linesBonus = Math.min(800, lines * 80);
@@ -209,6 +227,10 @@ export class FsmDriver implements ISpecDriver {
209
227
  * after a re-prime we don't re-inject until the screen changes (which
210
228
  * resets the stall reference) or another full stall window lapses. */
211
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;
212
234
 
213
235
  private currentEval: CurrentEval | null = null;
214
236
  private stateHistory: HistoryEntry[] = [];
@@ -326,6 +348,7 @@ export class FsmDriver implements ISpecDriver {
326
348
  this.delegateTimers.clear();
327
349
  if (this.wakeTimer) { clearTimeout(this.wakeTimer); this.wakeTimer = null; }
328
350
  if (this.stallTimer) { clearTimeout(this.stallTimer); this.stallTimer = null; }
351
+ if (this.win32SubmitTimer) { clearTimeout(this.win32SubmitTimer); this.win32SubmitTimer = null; }
329
352
  this.specWatcher?.close();
330
353
  this.adapter.kill();
331
354
  }
@@ -832,24 +855,20 @@ export class FsmDriver implements ISpecDriver {
832
855
  const perChar = sm.delay_ms_per_char ?? 0;
833
856
  const beforeSubmit = resolveSubmitDelayMs(sm.delay_ms_before_submit, text);
834
857
 
835
- // win32 ConPTY swallows a lone submit key (CR) that arrives in its own
836
- // PTY chunk after a delay: ConPTY does not treat a delayed, standalone
837
- // CR as a submit, so the prompt sits typed-but-unsent until the user
838
- // hits Enter manually. mac/linux PTYs do recognise the split write, so
839
- // this asymmetry is win32-only. The fix mirrors the legacy
840
- // ProviderCliAdapter.forceSendMessage fix: honour the settle delay so
841
- // the TUI input handler is ready, then write `text + submit_key` as ONE
842
- // PTY write keeping the submit key in the same write unit as the text
843
- // is the invariant ConPTY needs to recognise the submit. perChar typing
844
- // simulation is skipped on win32 (a per-char loop necessarily ends with a
845
- // separate trailing CR); correctness of submission wins over the typing
846
- // visual there.
858
+ // win32 ConPTY submit: the text and the submit key (CR) must NOT be
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.
847
869
  if (process.platform === 'win32') {
848
- if (beforeSubmit > 0) {
849
- setTimeout(() => this.adapter.send_keys(text + sm.submit_key), beforeSubmit);
850
- } else {
851
- this.adapter.send_keys(text + sm.submit_key);
852
- }
870
+ this.adapter.send_keys(text);
871
+ this.scheduleWin32Submit(sm.submit_key, beforeSubmit);
853
872
  return;
854
873
  }
855
874
 
@@ -871,6 +890,37 @@ export class FsmDriver implements ISpecDriver {
871
890
  }, perChar);
872
891
  }
873
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
+
874
924
  private handleClickControl(controlId: string, payload?: unknown): void {
875
925
  const ctl = (this.spec.control_bar ?? []).find(c => c.id === controlId);
876
926
  if (!ctl) return;