@adhdev/daemon-standalone 1.0.42-rc.1 → 1.0.42-rc.2

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 CHANGED
@@ -36556,10 +36556,10 @@ var require_dist3 = __commonJS({
36556
36556
  }
36557
36557
  function getDaemonBuildInfo() {
36558
36558
  if (cached2) return cached2;
36559
- const commit = readInjected(true ? "d424e843d3ebe663ebdd8ca6c46cd47355f45eba" : void 0) ?? "unknown";
36560
- const commitShort = readInjected(true ? "d424e843" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
36561
- const version2 = readInjected(true ? "1.0.42-rc.1" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
36562
- const builtAt = readInjected(true ? "2026-08-10T14:31:55.387Z" : void 0);
36559
+ const commit = readInjected(true ? "887aef0e08676616ce97395698e7690167982603" : void 0) ?? "unknown";
36560
+ const commitShort = readInjected(true ? "887aef0e" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
36561
+ const version2 = readInjected(true ? "1.0.42-rc.2" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
36562
+ const builtAt = readInjected(true ? "2026-08-10T17:37:45.509Z" : void 0);
36563
36563
  cached2 = builtAt ? { commit, commitShort, version: version2, builtAt } : { commit, commitShort, version: version2 };
36564
36564
  return cached2;
36565
36565
  }
@@ -93131,6 +93131,13 @@ ${marker}`,
93131
93131
  return n;
93132
93132
  }
93133
93133
  var SUBMIT_DELAY_FLOOR_MS = 200;
93134
+ var SEND_IN_FLIGHT_MAX_MS = 3e4;
93135
+ var DUPLICATE_RESEND_WINDOW_MS = 6e4;
93136
+ function hashSendText(text) {
93137
+ let h = 5381;
93138
+ for (let i = 0; i < text.length; i += 1) h = ((h << 5) + h ^ text.charCodeAt(i)) >>> 0;
93139
+ return `${h.toString(36)}:${text.length}`;
93140
+ }
93134
93141
  var fsmDriverSeq = 0;
93135
93142
  var WIN32_SUBMIT_RESEND_GAP_MS = 350;
93136
93143
  var WIN32_SUBMIT_MAX_RESENDS = 14;
@@ -93237,6 +93244,19 @@ ${marker}`,
93237
93244
  // non-busy ("ready") state — same contract as v3's idleSeenOnce.
93238
93245
  readySeenOnce = false;
93239
93246
  pendingSends = [];
93247
+ /** True while a send is written but the FSM has not yet left idle, i.e. the
93248
+ * composer is mid-submit. Blocks a second send from overwriting the first
93249
+ * before the CLI has consumed it (see handleSendMessage). */
93250
+ sendInFlight = false;
93251
+ /** Wall-clock (ms) the in-flight send was written. Bounds sendInFlight so a
93252
+ * send the CLI never visibly consumed cannot wedge the queue forever. */
93253
+ sendInFlightAt = 0;
93254
+ /** Content hash → wall-clock of the last PTY write, for the pre-write
93255
+ * duplicate gate (see isDuplicateResend). */
93256
+ recentSendHashes = /* @__PURE__ */ new Map();
93257
+ /** Pending queued-send drain timer, tracked so shutdown() can cancel it and
93258
+ * a torn-down driver never writes a queued body into a dead PTY. */
93259
+ pendingSendDrainTimer = null;
93240
93260
  pickerInProgress = null;
93241
93261
  delegateTimers = /* @__PURE__ */ new Map();
93242
93262
  specWatcher = null;
@@ -93389,6 +93409,11 @@ ${marker}`,
93389
93409
  clearTimeout(this.win32ModalConfirmTimer);
93390
93410
  this.win32ModalConfirmTimer = null;
93391
93411
  }
93412
+ if (this.pendingSendDrainTimer) {
93413
+ clearTimeout(this.pendingSendDrainTimer);
93414
+ this.pendingSendDrainTimer = null;
93415
+ }
93416
+ this.pendingSends.length = 0;
93392
93417
  this.specWatcher?.close();
93393
93418
  this.adapter.kill();
93394
93419
  }
@@ -93580,12 +93605,14 @@ ${marker}`,
93580
93605
  this.scheduleWakeForState();
93581
93606
  this.scheduleStallWatchdog();
93582
93607
  this.maybeMarkReady();
93608
+ this.drainPendingSends();
93583
93609
  return;
93584
93610
  }
93585
93611
  this.emitStateChanged(forceEmit);
93586
93612
  this.scheduleWakeForState();
93587
93613
  this.scheduleStallWatchdog();
93588
93614
  this.maybeMarkReady();
93615
+ this.drainPendingSends();
93589
93616
  }
93590
93617
  commitTransition(fired, now, ev) {
93591
93618
  const from = this.currentStateId;
@@ -93855,8 +93882,7 @@ ${marker}`,
93855
93882
  if (!st) return;
93856
93883
  if (!st.initial && statusForState(st) === "idle") {
93857
93884
  this.readySeenOnce = true;
93858
- const queued = this.pendingSends.splice(0);
93859
- for (const text of queued) setTimeout(() => this.actuallySendMessage(text), 50);
93885
+ this.drainPendingSends();
93860
93886
  }
93861
93887
  }
93862
93888
  // ────────────────────────────────────────────────────────────────────
@@ -93894,13 +93920,109 @@ ${marker}`,
93894
93920
  // ────────────────────────────────────────────────────────────────────
93895
93921
  // Dashboard commands (identical semantics to v3)
93896
93922
  // ────────────────────────────────────────────────────────────────────
93923
+ /**
93924
+ * SEND-OVERLAP gate. A send may only go straight to the PTY when the machine
93925
+ * is genuinely able to accept one: it has been ready at least once, it is
93926
+ * sitting at an idle prompt right now, and no earlier send is still in flight.
93927
+ * Anything else is queued and drained by drainPendingSends() when the machine
93928
+ * next returns to idle.
93929
+ *
93930
+ * Before the fix this consulted ONLY `readySeenOnce` — a one-shot latch — so
93931
+ * every send after the first ignored the live FSM state and could be written
93932
+ * on top of a still-generating turn, braiding the two bodies in the composer
93933
+ * (see the SEND-OVERLAP note above the constants).
93934
+ */
93897
93935
  handleSendMessage(text) {
93898
- if (!this.readySeenOnce) {
93936
+ if (this.isDuplicateResend(text)) {
93937
+ LOG2.info("FsmDriver", `[${this.specTag()}] send suppressed \u2014 duplicate resend within ${DUPLICATE_RESEND_WINDOW_MS}ms (len=${text.length})`);
93938
+ return;
93939
+ }
93940
+ if (!this.canSendNow()) {
93899
93941
  this.pendingSends.push(text);
93942
+ LOG2.info(
93943
+ "FsmDriver",
93944
+ `[${this.specTag()}] send queued \u2014 ${this.sendBlockedReason()} (len=${text.length}, queued=${this.pendingSends.length})`
93945
+ );
93900
93946
  return;
93901
93947
  }
93948
+ this.beginSend(text);
93949
+ }
93950
+ /** True when a send can be written to the PTY right now. */
93951
+ canSendNow() {
93952
+ if (!this.readySeenOnce) return false;
93953
+ if (this.isSendInFlight()) return false;
93954
+ return this.currentStatus() === "idle";
93955
+ }
93956
+ /** Human-readable reason a send was queued — logged, never used for control flow. */
93957
+ sendBlockedReason() {
93958
+ if (!this.readySeenOnce) return "machine not ready yet";
93959
+ if (this.isSendInFlight()) return `previous send still in flight (${Date.now() - this.sendInFlightAt}ms)`;
93960
+ return `machine is ${this.currentStatus()}`;
93961
+ }
93962
+ /** In-flight latch with its self-expiry applied, so a send the CLI never
93963
+ * visibly consumed cannot wedge the queue permanently. */
93964
+ isSendInFlight() {
93965
+ if (!this.sendInFlight) return false;
93966
+ if (Date.now() - this.sendInFlightAt > SEND_IN_FLIGHT_MAX_MS) {
93967
+ LOG2.warn("FsmDriver", `[${this.specTag()}] in-flight send latch expired after ${SEND_IN_FLIGHT_MAX_MS}ms \u2014 releasing`);
93968
+ this.sendInFlight = false;
93969
+ return false;
93970
+ }
93971
+ return true;
93972
+ }
93973
+ /**
93974
+ * Pre-write duplicate gate. Suppresses a repeat of text that was written to
93975
+ * the PTY within DUPLICATE_RESEND_WINDOW_MS *while that text is still being
93976
+ * processed* — i.e. a send is in flight or the machine has not returned to
93977
+ * idle. A genuine repeat typed at an idle prompt is NOT suppressed: sending
93978
+ * "continue" twice in a row is ordinary use, and silently swallowing the
93979
+ * second one would be a worse defect than the one being fixed.
93980
+ */
93981
+ isDuplicateResend(text) {
93982
+ const now = Date.now();
93983
+ const key2 = hashSendText(text);
93984
+ for (const [candidate, at] of this.recentSendHashes) {
93985
+ if (now - at > DUPLICATE_RESEND_WINDOW_MS) this.recentSendHashes.delete(candidate);
93986
+ }
93987
+ const previous = this.recentSendHashes.get(key2);
93988
+ if (previous === void 0) return false;
93989
+ if (now - previous > DUPLICATE_RESEND_WINDOW_MS) return false;
93990
+ const stillProcessing = this.isSendInFlight() || this.currentStatus() !== "idle" || this.pendingSends.length > 0;
93991
+ return stillProcessing;
93992
+ }
93993
+ /** Mark a send as in flight, record it for the duplicate gate, and write it. */
93994
+ beginSend(text) {
93995
+ this.sendInFlight = true;
93996
+ this.sendInFlightAt = Date.now();
93997
+ this.recentSendHashes.set(hashSendText(text), this.sendInFlightAt);
93902
93998
  this.actuallySendMessage(text);
93903
93999
  }
94000
+ /**
94001
+ * Release the in-flight latch and write the next queued send, if the machine
94002
+ * can take one. Called from the FSM evaluation loop, so it runs on the same
94003
+ * frame the machine reaches idle — the queue never waits for an extra PTY
94004
+ * frame that a quiet CLI would never produce (the same hazard maybeMarkReady
94005
+ * documents for the very first message).
94006
+ */
94007
+ drainPendingSends() {
94008
+ if (!this.readySeenOnce) return;
94009
+ const status = this.currentStatus();
94010
+ if (this.sendInFlight && status !== "idle") {
94011
+ this.sendInFlight = false;
94012
+ }
94013
+ if (status !== "idle") return;
94014
+ if (this.isSendInFlight()) return;
94015
+ if (this.pendingSends.length === 0) return;
94016
+ const next = this.pendingSends.shift();
94017
+ LOG2.info("FsmDriver", `[${this.specTag()}] draining queued send (len=${next.length}, remaining=${this.pendingSends.length})`);
94018
+ if (this.pendingSendDrainTimer) clearTimeout(this.pendingSendDrainTimer);
94019
+ this.pendingSendDrainTimer = setTimeout(() => {
94020
+ this.pendingSendDrainTimer = null;
94021
+ this.beginSend(next);
94022
+ }, 50);
94023
+ this.sendInFlight = true;
94024
+ this.sendInFlightAt = Date.now();
94025
+ }
93904
94026
  actuallySendMessage(text) {
93905
94027
  const sm = this.spec.send_message;
93906
94028
  const perChar = sm.delay_ms_per_char ?? 0;