@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/cli.cjs +94 -17
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +92 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +92 -15
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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 =
|
|
2177
|
-
const
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
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
|
|
2951
|
-
const
|
|
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
|
-
|
|
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 =
|
|
2980
|
-
|
|
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
|
-
|
|
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,
|