@adhdev/daemon-core 0.9.82-rc.359 → 0.9.82-rc.360

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.
@@ -337,13 +337,16 @@ export declare class FsmDriver implements ISpecDriver {
337
337
  /**
338
338
  * win32 submit. Two phases:
339
339
  *
340
- * Phase 1 (settle-gate): hold the first CR until the PTY output has been quiet
341
- * for WIN32_SUBMIT_SETTLE_MS after the last input write i.e. the full
342
- * (possibly multi-KB / multiline) body has finished arriving in the composer
343
- * and echoing. Honors an initial minimum delay and is bounded by
344
- * WIN32_SUBMIT_MAX_SETTLE_WAIT_MS so a noisy screen can never hang the submit.
345
- * This is what stops a long message from being submitted half-arrived (its
346
- * leading lines lost). A short message settles almost immediately.
340
+ * Phase 1 (echo-gate): hold the first CR until the body text is CONFIRMED in the
341
+ * composer (its whitespace-collapsed tail appears in the rendered screen) AND the
342
+ * PTY output is then quiet for WIN32_SUBMIT_SETTLE_MS (the full, possibly multi-KB
343
+ * / multiline body has finished arriving). This replaces a bare output-quiet
344
+ * settle: an early write that races claude's boot is buffered, not dropped, so the
345
+ * screen can go quiet with the body NOT YET in the composer — a quiet-only gate
346
+ * would then fire a CR into an empty composer (no submit). Waiting on the echo
347
+ * closes that. Honors an initial minimum delay and a generous WIN32_ECHO_MAX_WAIT_MS
348
+ * last-resort blind fire so a body that truly never confirms still submits (carried
349
+ * by phase 2) rather than hanging. A settled session echoes immediately → no delay.
347
350
  *
348
351
  * Phase 2 (verified resend — unchanged): send the submit key, wait a gap, and
349
352
  * if the FSM is still 'idle' (the CR was absorbed as a multiline-paste
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.359",
3
+ "version": "0.9.82-rc.360",
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.359",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.360",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -163,16 +163,11 @@ const SUBMIT_DELAY_FLOOR_MS = 200;
163
163
  // trimmed by the TUI on submit.
164
164
  const WIN32_SUBMIT_RESEND_GAP_MS = 350;
165
165
  const WIN32_SUBMIT_MAX_RESENDS = 14;
166
- // Settle-gate for the win32 FIRST submit CR. Hold the CR until the PTY output has
167
- // gone quiet for WIN32_SUBMIT_SETTLE_MS after the last input write i.e. the full
168
- // (possibly multi-KB / multiline) message body has finished arriving in the
169
- // composer and echoing back. A long message waits until it actually lands; a short
170
- // one settles almost immediately. WIN32_SUBMIT_MAX_SETTLE_WAIT_MS bounds the wait
171
- // so a perpetually-noisy screen can never hang the submit. This is what stops a
172
- // blind fixed-delay CR from submitting a half-arrived prompt and dropping its
173
- // leading lines. The phase-2 verified-resend loop (below) is unchanged.
166
+ // Quiet window the win32 echo-gate (below) requires AFTER the body is seen in the
167
+ // composer, so a CR fires only once the FULL (possibly multi-KB / multiline) body has
168
+ // finished arriving and echoing not mid-arrival. POLL_MS is the gate's recheck
169
+ // cadence while it waits for the body to echo.
174
170
  const WIN32_SUBMIT_SETTLE_MS = 500;
175
- const WIN32_SUBMIT_MAX_SETTLE_WAIT_MS = 10_000;
176
171
  const WIN32_SUBMIT_SETTLE_POLL_MS = 120;
177
172
  // Defensive paced PTY write. A single unbounded ConPTY write can overflow the
178
173
  // input pipe and drop leading bytes; split a large body into bounded chunks with a
@@ -180,6 +175,25 @@ const WIN32_SUBMIT_SETTLE_POLL_MS = 120;
180
175
  // write in one shot.
181
176
  const WIN32_PTY_WRITE_CHUNK_CHARS = 1024;
182
177
  const WIN32_PTY_WRITE_CHUNK_GAP_MS = 8;
178
+ // Echo-gate for the win32 FIRST submit CR (supersedes the bare output-quiet settle).
179
+ // The body write can race claude-cli's boot — its stdin reader is not wired until the
180
+ // composer renders (~5–7s in, later under load), so a too-early write is buffered and
181
+ // the output goes quiet with a still-EMPTY composer; a quiet-only gate then fires a CR
182
+ // into nothing. Instead, hold the CR until the body text has actually ECHOED into the
183
+ // composer (effect-confirmed, not readiness-signal-guessed) — the buffered write lands
184
+ // once claude wires up, just late. WIN32_ECHO_MAX_WAIT_MS bounds the wait so a body
185
+ // that truly never confirms still fires a blind CR (resend net) rather than hanging; it
186
+ // is set generously so even a slow/contended boot lands its body before the blind fire.
187
+ const WIN32_ECHO_PROBE_CHARS = 16;
188
+ const WIN32_ECHO_MAX_WAIT_MS = 20_000;
189
+
190
+ /** Collapse a string to its non-whitespace characters for echo comparison: the
191
+ * composer wraps, indents, and prefixes the body (with the `❯ ` prompt), so a raw
192
+ * substring test against the rendered screen fails. Stripping all whitespace makes
193
+ * "❯ hello world" reliably contain the probe "helloworld". */
194
+ function normalizeForEcho(s: string): string {
195
+ return s.replace(/\s+/g, '');
196
+ }
183
197
 
184
198
  export function resolveSubmitDelayMs(specBeforeSubmit: number | undefined, text: string): number {
185
199
  const lines = countNewlines(text);
@@ -927,7 +941,7 @@ export class FsmDriver implements ISpecDriver {
927
941
  // over the typing visual there.
928
942
  if (process.platform === 'win32') {
929
943
  this.writeWin32Body(text);
930
- this.scheduleWin32Submit(sm.submit_key, beforeSubmit);
944
+ this.scheduleWin32Submit(sm.submit_key, beforeSubmit, text);
931
945
  return;
932
946
  }
933
947
 
@@ -1000,13 +1014,16 @@ export class FsmDriver implements ISpecDriver {
1000
1014
  /**
1001
1015
  * win32 submit. Two phases:
1002
1016
  *
1003
- * Phase 1 (settle-gate): hold the first CR until the PTY output has been quiet
1004
- * for WIN32_SUBMIT_SETTLE_MS after the last input write i.e. the full
1005
- * (possibly multi-KB / multiline) body has finished arriving in the composer
1006
- * and echoing. Honors an initial minimum delay and is bounded by
1007
- * WIN32_SUBMIT_MAX_SETTLE_WAIT_MS so a noisy screen can never hang the submit.
1008
- * This is what stops a long message from being submitted half-arrived (its
1009
- * leading lines lost). A short message settles almost immediately.
1017
+ * Phase 1 (echo-gate): hold the first CR until the body text is CONFIRMED in the
1018
+ * composer (its whitespace-collapsed tail appears in the rendered screen) AND the
1019
+ * PTY output is then quiet for WIN32_SUBMIT_SETTLE_MS (the full, possibly multi-KB
1020
+ * / multiline body has finished arriving). This replaces a bare output-quiet
1021
+ * settle: an early write that races claude's boot is buffered, not dropped, so the
1022
+ * screen can go quiet with the body NOT YET in the composer — a quiet-only gate
1023
+ * would then fire a CR into an empty composer (no submit). Waiting on the echo
1024
+ * closes that. Honors an initial minimum delay and a generous WIN32_ECHO_MAX_WAIT_MS
1025
+ * last-resort blind fire so a body that truly never confirms still submits (carried
1026
+ * by phase 2) rather than hanging. A settled session echoes immediately → no delay.
1010
1027
  *
1011
1028
  * Phase 2 (verified resend — unchanged): send the submit key, wait a gap, and
1012
1029
  * if the FSM is still 'idle' (the CR was absorbed as a multiline-paste
@@ -1015,10 +1032,21 @@ export class FsmDriver implements ISpecDriver {
1015
1032
  * idle and stop the instant the agent leaves idle (submitted → generating /
1016
1033
  * approval). This preserves the win32 lone-CR-swallow handling.
1017
1034
  */
1018
- private scheduleWin32Submit(submitKey: string, initialDelayMs: number): void {
1035
+ private scheduleWin32Submit(submitKey: string, initialDelayMs: number, body: string): void {
1019
1036
  if (this.win32SubmitTimer) { clearTimeout(this.win32SubmitTimer); this.win32SubmitTimer = null; }
1020
1037
  const startedAt = Date.now();
1021
1038
 
1039
+ // Distinctive whitespace-collapsed TAIL of the body — what we look for in the
1040
+ // composer to confirm the body landed. The tail (not the head) because a long
1041
+ // multiline body scrolls its leading lines out of the composer viewport while
1042
+ // the cursor — and therefore the body's end — stays visible. Empty body →
1043
+ // nothing to confirm.
1044
+ const probe = normalizeForEcho(body).slice(-WIN32_ECHO_PROBE_CHARS);
1045
+ const bodyEchoed = (): boolean => {
1046
+ if (!probe) return true;
1047
+ return normalizeForEcho(this.adapter.snapshot()).includes(probe);
1048
+ };
1049
+
1022
1050
  const fire = (attempt: number): void => {
1023
1051
  this.win32SubmitTimer = null;
1024
1052
  this.adapter.send_keys(submitKey);
@@ -1030,21 +1058,30 @@ export class FsmDriver implements ISpecDriver {
1030
1058
  }, WIN32_SUBMIT_RESEND_GAP_MS);
1031
1059
  };
1032
1060
 
1033
- const waitForSettle = (): void => {
1061
+ // Echo-gate: hold the first CR until the body is CONFIRMED in the composer, not
1062
+ // merely until output goes quiet — a quiet screen with a not-yet-arrived body is
1063
+ // the exact empty-submit failure this replaces. The body is NOT re-written: on
1064
+ // the session-host IPC transport an early write is buffered, not dropped, so it
1065
+ // lands once claude's stdin reader wires up (just late on a slow/contended boot);
1066
+ // re-writing would risk a duplicated body when several buffered writes drain
1067
+ // together. So we simply WAIT for the echo. The hard upper bound only fires a
1068
+ // blind CR if the body never confirms at all (resend net carries it), set
1069
+ // generously so a slow boot still lands its body first.
1070
+ const waitForEcho = (): void => {
1034
1071
  this.win32SubmitTimer = null;
1035
1072
  const now = Date.now();
1036
1073
  const quietFor = now - this.lastWin32InputActivityAt();
1037
1074
  const waited = now - startedAt;
1038
- if (quietFor >= WIN32_SUBMIT_SETTLE_MS || waited >= WIN32_SUBMIT_MAX_SETTLE_WAIT_MS) {
1039
- fire(0);
1040
- return;
1041
- }
1042
- const recheckIn = Math.min(WIN32_SUBMIT_SETTLE_MS - quietFor, WIN32_SUBMIT_SETTLE_POLL_MS);
1043
- this.win32SubmitTimer = setTimeout(waitForSettle, Math.max(recheckIn, 30));
1075
+ const settled = quietFor >= WIN32_SUBMIT_SETTLE_MS;
1076
+ // Body present in the composer AND output quiet (full body arrived) → submit.
1077
+ if (bodyEchoed() && settled) { fire(0); return; }
1078
+ // Last-resort blind fire so a body that truly never confirms cannot hang.
1079
+ if (waited >= WIN32_ECHO_MAX_WAIT_MS) { fire(0); return; }
1080
+ this.win32SubmitTimer = setTimeout(waitForEcho, WIN32_SUBMIT_SETTLE_POLL_MS);
1044
1081
  };
1045
1082
 
1046
- if (initialDelayMs > 0) this.win32SubmitTimer = setTimeout(waitForSettle, initialDelayMs);
1047
- else waitForSettle();
1083
+ if (initialDelayMs > 0) this.win32SubmitTimer = setTimeout(waitForEcho, initialDelayMs);
1084
+ else waitForEcho();
1048
1085
  }
1049
1086
 
1050
1087
  private handleClickControl(controlId: string, payload?: unknown): void {