@threadbase-sh/streamer 1.44.3 → 1.45.0

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.cjs CHANGED
@@ -2173,14 +2173,19 @@ function scrapePermissionGate(lines) {
2173
2173
  const options = [];
2174
2174
  let cursor;
2175
2175
  let firstOptionLine = -1;
2176
- for (let i = 0; i < lines.length; i++) {
2177
- const m = OPTION_RE.exec(stripGutter(lines[i]));
2178
- if (!m) continue;
2179
- const index = Number.parseInt(m[2], 10);
2180
- if (!Number.isFinite(index)) continue;
2181
- if (firstOptionLine === -1) firstOptionLine = i;
2182
- if (m[1]) cursor = index;
2183
- options.push({ index, label: m[3] });
2176
+ for (let i = lines.length - 1; i >= 0; i--) {
2177
+ const stripped = stripGutter(lines[i]);
2178
+ const m = OPTION_RE.exec(stripped);
2179
+ if (m) {
2180
+ const index = Number.parseInt(m[2], 10);
2181
+ if (!Number.isFinite(index)) continue;
2182
+ firstOptionLine = i;
2183
+ if (m[1]) cursor = index;
2184
+ options.unshift({ index, label: m[3] });
2185
+ continue;
2186
+ }
2187
+ if (options.length === 0) continue;
2188
+ if (stripped.trim().length === 0 || BOX_ONLY_RE.test(lines[i].trim())) break;
2184
2189
  }
2185
2190
  if (options.length === 0) return null;
2186
2191
  let prompt;
@@ -2220,6 +2225,17 @@ function scrapeDetail(lines, promptLine) {
2220
2225
  }
2221
2226
  return collected.length > 0 ? collected.reverse().join("\n") : void 0;
2222
2227
  }
2228
+ var GATE_FOOTER_RE = /esc to cancel/i;
2229
+ var ASK_MENU_FOOTER_RE = /Enter to select/i;
2230
+ var YES_NO_LABEL_RE = /^(yes|no)\b/i;
2231
+ function detectGateScreen(lines) {
2232
+ if (lines.some((l) => ASK_MENU_FOOTER_RE.test(l))) return null;
2233
+ if (!lines.some((l) => GATE_FOOTER_RE.test(l))) return null;
2234
+ const gate = scrapePermissionGate(lines);
2235
+ if (!gate || gate.options.length < 2) return null;
2236
+ if (!gate.options.some((o) => YES_NO_LABEL_RE.test(o.label))) return null;
2237
+ return gate;
2238
+ }
2223
2239
 
2224
2240
  // src/services/questions/detectQuestionFromScreen.ts
2225
2241
  var ASK_FOOTER_RE = /Enter to select/i;
@@ -2367,6 +2383,8 @@ var PTY_ROWS2 = 40;
2367
2383
  var SCREEN_SCROLLBACK2 = 1e3;
2368
2384
  var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
2369
2385
  var QUIET_DETECT_MS2 = 500;
2386
+ var OSC_TAIL_CHARS = 128;
2387
+ var SCRAPE_THROTTLE_MS = 300;
2370
2388
  var CLAUDE_READY_FALLBACK_MS = 8e3;
2371
2389
  function buildPasteBytes(input) {
2372
2390
  return `\x1B[200~${input}\x1B[201~`;
@@ -2425,6 +2443,12 @@ var PTYManager = class {
2425
2443
  // the next prompt-ready without a fresh 777 (gate closed). Prevents
2426
2444
  // re-broadcasting open/close on every chunk.
2427
2445
  permissionOpen = /* @__PURE__ */ new Set();
2446
+ // Last chunk's raw tail per session — prepended to the next chunk before
2447
+ // the OSC regex test so a split escape still matches. Consumed on match.
2448
+ oscTail = /* @__PURE__ */ new Map();
2449
+ // When the last full detection pass ran per session (any trigger) — the
2450
+ // clock the SCRAPE_THROTTLE_MS ceiling is measured against.
2451
+ lastDetectAt = /* @__PURE__ */ new Map();
2428
2452
  // Content key of the last AskUserQuestion broadcast from the rendered screen,
2429
2453
  // per session — de-dupes the same menu firing on consecutive repaints.
2430
2454
  lastScreenQuestionKey = /* @__PURE__ */ new Map();
@@ -2433,6 +2457,13 @@ var PTYManager = class {
2433
2457
  // on a prompt-ready/marker return and de-dupe consecutive repaints. Modelled
2434
2458
  // on permissionOpen but keyed by content (a shell prompt has no OSC trigger).
2435
2459
  shellPromptOpen = /* @__PURE__ */ new Map();
2460
+ // Content key (permissionContentKey, cursor excluded — see arm 3) of the
2461
+ // gate that was still painted on screen when arm 2 closed it. Suppresses
2462
+ // arm 3 re-claiming that same box from a later trigger-less/throttled pass
2463
+ // before Claude erases it — otherwise the paint-time claim reopens the
2464
+ // card it just closed. Cleared once detectGateScreen sees the box is gone,
2465
+ // so a genuinely new (even content-identical) gate is claimed normally.
2466
+ closedGateKey = /* @__PURE__ */ new Map();
2436
2467
  // Tracks sessions (both fresh and resume) whose PTY has spawned but Claude
2437
2468
  // hasn't yet reached an interactive prompt — i.e. onReady hasn't fired.
2438
2469
  pendingReady = /* @__PURE__ */ new Set();
@@ -2786,8 +2817,11 @@ var PTYManager = class {
2786
2817
  this.queuedInputs.delete(sessionId);
2787
2818
  this.firstChunkAt.delete(sessionId);
2788
2819
  this.permissionOpen.delete(sessionId);
2820
+ this.oscTail.delete(sessionId);
2821
+ this.lastDetectAt.delete(sessionId);
2789
2822
  this.lastScreenQuestionKey.delete(sessionId);
2790
2823
  this.shellPromptOpen.delete(sessionId);
2824
+ this.closedGateKey.delete(sessionId);
2791
2825
  this.quietCheckers.get(sessionId)?.cancel();
2792
2826
  this.quietCheckers.delete(sessionId);
2793
2827
  this.clearReadyFallback(sessionId);
@@ -2882,8 +2916,11 @@ var PTYManager = class {
2882
2916
  for (const timer of this.readyFallbackTimers.values()) clearTimeout(timer);
2883
2917
  this.readyFallbackTimers.clear();
2884
2918
  this.permissionOpen.clear();
2919
+ this.oscTail.clear();
2920
+ this.lastDetectAt.clear();
2885
2921
  this.lastScreenQuestionKey.clear();
2886
2922
  this.shellPromptOpen.clear();
2923
+ this.closedGateKey.clear();
2887
2924
  }
2888
2925
  handleOutput(sessionId, data) {
2889
2926
  const session = this.sessions.get(sessionId);
@@ -2947,16 +2984,24 @@ var PTYManager = class {
2947
2984
  async detectLivePrompts(sessionId, rawData, stripped) {
2948
2985
  const session = this.sessions.get(sessionId);
2949
2986
  if (!session) return;
2950
- const oscPermission = hasPermissionOsc(rawData);
2951
- const oscWaitingForInput = hasWaitingForInputOsc(rawData);
2987
+ const oscWindow = (this.oscTail.get(sessionId) ?? "") + rawData;
2988
+ const oscPermission = hasPermissionOsc(oscWindow);
2989
+ const oscWaitingForInput = hasWaitingForInputOsc(oscWindow);
2990
+ if (rawData !== "") {
2991
+ if (oscPermission || oscWaitingForInput) this.oscTail.delete(sessionId);
2992
+ else this.oscTail.set(sessionId, oscWindow.slice(-OSC_TAIL_CHARS));
2993
+ }
2952
2994
  const hasAskFooter = /Enter to select/i.test(stripped);
2953
2995
  const hasPromptMarker = CLAUDE_PROMPT_MARKERS.some((m) => stripped.includes(m));
2954
2996
  const hasShellPromptHint = /[[(]\s*y\s*\/\s*n\s*[\])]|press\s+(enter|return|any key)|\bcontinue\b\s*\?|^\s*(?:❯|>)?\s*\d+[.)]\s+\S/im.test(
2955
2997
  stripped
2956
2998
  );
2957
- if (!oscPermission && !oscWaitingForInput && !hasAskFooter && !hasShellPromptHint && !this.permissionOpen.has(sessionId) && !this.shellPromptOpen.has(sessionId) && !this.lastScreenQuestionKey.has(sessionId)) {
2999
+ const nowMs = Date.now();
3000
+ const scrapeDue = nowMs - (this.lastDetectAt.get(sessionId) ?? 0) >= SCRAPE_THROTTLE_MS;
3001
+ if (!oscPermission && !oscWaitingForInput && !hasAskFooter && !hasShellPromptHint && !scrapeDue && !this.permissionOpen.has(sessionId) && !this.shellPromptOpen.has(sessionId) && !this.lastScreenQuestionKey.has(sessionId)) {
2958
3002
  return;
2959
3003
  }
3004
+ this.lastDetectAt.set(sessionId, nowMs);
2960
3005
  const lines = await this.getOutputLines(sessionId, 60);
2961
3006
  const askFooterOnScreen = lines.some((l) => /Enter to select/i.test(l));
2962
3007
  if (oscPermission || hasAskFooter || askFooterOnScreen) {
@@ -2976,13 +3021,30 @@ var PTYManager = class {
2976
3021
  this.permissionOpen.add(sessionId);
2977
3022
  this.onPermissionChange?.(sessionId, gate ?? { options: [] });
2978
3023
  } else if (this.permissionOpen.has(sessionId) && !askFooterOnScreen) {
2979
- const gate = scrapePermissionGate(lines);
2980
- if (oscWaitingForInput || !gate && hasPromptMarker) {
3024
+ const gate = detectGateScreen(lines);
3025
+ const stillPainted = gate !== null || scrapePermissionGate(lines) !== null;
3026
+ if (oscWaitingForInput || !stillPainted && hasPromptMarker) {
2981
3027
  this.permissionOpen.delete(sessionId);
2982
3028
  this.onPermissionChange?.(sessionId, null);
3029
+ if (gate) {
3030
+ this.closedGateKey.set(sessionId, permissionContentKey({ ...gate, cursor: void 0 }));
3031
+ } else {
3032
+ this.closedGateKey.delete(sessionId);
3033
+ }
2983
3034
  } else if (gate) {
2984
3035
  this.onPermissionChange?.(sessionId, gate);
2985
3036
  }
3037
+ } else if (!askFooterOnScreen && !oscWaitingForInput) {
3038
+ const gate = detectGateScreen(lines);
3039
+ if (gate) {
3040
+ const key = permissionContentKey({ ...gate, cursor: void 0 });
3041
+ if (this.closedGateKey.get(sessionId) !== key) {
3042
+ this.permissionOpen.add(sessionId);
3043
+ this.onPermissionChange?.(sessionId, gate);
3044
+ }
3045
+ } else {
3046
+ this.closedGateKey.delete(sessionId);
3047
+ }
2986
3048
  }
2987
3049
  if (askFooterOnScreen) {
2988
3050
  const detected = detectQuestionFromScreen(lines);
@@ -3125,8 +3187,11 @@ var PTYManager = class {
3125
3187
  this.queuedInputs.delete(sessionId);
3126
3188
  this.firstChunkAt.delete(sessionId);
3127
3189
  this.permissionOpen.delete(sessionId);
3190
+ this.oscTail.delete(sessionId);
3191
+ this.lastDetectAt.delete(sessionId);
3128
3192
  this.lastScreenQuestionKey.delete(sessionId);
3129
3193
  this.shellPromptOpen.delete(sessionId);
3194
+ this.closedGateKey.delete(sessionId);
3130
3195
  this.quietCheckers.get(sessionId)?.cancel();
3131
3196
  this.quietCheckers.delete(sessionId);
3132
3197
  this.clearReadyFallback(sessionId);
@@ -9811,7 +9876,12 @@ function managedToResponse(s, ptyAttached) {
9811
9876
  // the durable registry, so it is a previous run's session with no process
9812
9877
  // behind it — `resumable`, and `historical` rather than `managed`
9813
9878
  // (docs/plans/live-sessions-persistence-plan.md §4, Phase 1).
9814
- lifecycle: ptyAttached ? "attached" : s.rehydrated ? "resumable" : s.failureReason != null ? "failed" : "completed",
9879
+ // A session this run itself put on hold (grace timer, explicit hold_session,
9880
+ // idle reaper) is the other exception: the PTY is gone the same way an exit
9881
+ // leaves it gone, but the conversation is still resumable, not terminal —
9882
+ // `putOnHold` records that by leaving `statusSource: "shutdown"` (the only
9883
+ // place either runner sets it), so it is checked here alongside `rehydrated`.
9884
+ lifecycle: ptyAttached ? "attached" : s.rehydrated || s.statusSource === "shutdown" ? "resumable" : s.failureReason != null ? "failed" : "completed",
9815
9885
  lifecycleSource: ptyAttached ? s.reconciled ? "reconcile" : "spawn" : s.rehydrated ? "reconcile" : "exit",
9816
9886
  // We own its PTY, so `status` is the authoritative signal — no inferred
9817
9887
  // `activity` is attached for managed sessions.
@@ -10662,7 +10732,14 @@ var StreamerServer = class {
10662
10732
  // even though the registry has one — the name only appeared after a
10663
10733
  // restart rebuilt the row as a stub. Guarded so a runner that has not
10664
10734
  // derived one yet cannot blank a name set by enrichResumedSessionAsync.
10665
- ...session.sessionName != null && { sessionName: session.sessionName }
10735
+ ...session.sessionName != null && { sessionName: session.sessionName },
10736
+ // Without mirroring this, SessionStore's copy is frozen at whatever
10737
+ // `addManaged` saw at spawn ("spawn") forever, because updateManaged
10738
+ // is a partial merge — so managedToResponse can never tell a
10739
+ // grace-timer/idle-reaper hold (statusSource "shutdown") apart from a
10740
+ // genuine process exit ("process-exit"), and reports both as
10741
+ // `lifecycle: "completed"`. See managedToResponse in session-store.ts.
10742
+ ...session.statusSource != null && { statusSource: session.statusSource }
10666
10743
  });
10667
10744
  this.managedSessionsRepo?.recordStatus(
10668
10745
  session.id,