@threadbase-sh/streamer 1.53.1 → 1.54.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 CHANGED
@@ -103179,12 +103179,15 @@ function detectCodexBlockingPrompt(lines) {
103179
103179
  function codexStatusBarLine(lines) {
103180
103180
  return [...lines].reverse().find((l) => l.trim() !== "") ?? "";
103181
103181
  }
103182
- function codexScreenBlocksComposer(lines) {
103182
+ function codexScreenPreTurn(lines) {
103183
103183
  const screenText = lines.join("\n");
103184
103184
  if (CODEX_HOOKS_GATE_REGEX.test(screenText) || CODEX_TRUST_GATE_REGEX.test(screenText)) {
103185
103185
  return true;
103186
103186
  }
103187
- if (CODEX_MCP_BOOT_RE.test(screenText)) return true;
103187
+ return CODEX_MCP_BOOT_RE.test(screenText);
103188
+ }
103189
+ function codexScreenBlocksComposer(lines) {
103190
+ if (codexScreenPreTurn(lines)) return true;
103188
103191
  return CODEX_BUSY_STATUS_RE.test(codexStatusBarLine(lines));
103189
103192
  }
103190
103193
  function codexScreenShowsReady(lines) {
@@ -103230,12 +103233,13 @@ function gateCard(gate, lines) {
103230
103233
  ]
103231
103234
  };
103232
103235
  }
103233
- var CODEX_PROMPT_READY_TEXT, CODEX_BUSY_STATUS_RE, CODEX_MCP_BOOT_RE, CODEX_TRUST_GATE_REGEX, CODEX_HOOKS_GATE_REGEX, CODEX_ACTIVE_WRITER_RE, CODEX_ACTIVE_WRITER_CODE, CODEX_USAGE_LIMIT_RE, CODEX_USAGE_RESET_TIP_RE, CODEX_RATE_LIMIT_MENU_RE;
103236
+ var CODEX_PROMPT_READY_TEXT, CODEX_BUSY_STATUS_RE, CODEX_WORKING_STATUS_RE, CODEX_MCP_BOOT_RE, CODEX_TRUST_GATE_REGEX, CODEX_HOOKS_GATE_REGEX, CODEX_ACTIVE_WRITER_RE, CODEX_ACTIVE_WRITER_CODE, CODEX_USAGE_LIMIT_RE, CODEX_USAGE_RESET_TIP_RE, CODEX_RATE_LIMIT_MENU_RE;
103234
103237
  var init_codexScreen = __esm({
103235
103238
  "src/services/questions/codexScreen.ts"() {
103236
103239
  "use strict";
103237
103240
  CODEX_PROMPT_READY_TEXT = "Ready";
103238
103241
  CODEX_BUSY_STATUS_RE = /\b(?:Starting|Working)\b/;
103242
+ CODEX_WORKING_STATUS_RE = /\bWorking\b/;
103239
103243
  CODEX_MCP_BOOT_RE = /Booting MCP|Starting MCP servers/i;
103240
103244
  CODEX_TRUST_GATE_REGEX = /trust the contents/i;
103241
103245
  CODEX_HOOKS_GATE_REGEX = /hooks need review/i;
@@ -120480,6 +120484,27 @@ var init_codexGateAnswers = __esm({
120480
120484
  }
120481
120485
  });
120482
120486
 
120487
+ // src/services/questions/parseAgentPhase.ts
120488
+ function codexPhase(lines) {
120489
+ if (codexScreenPreTurn(lines)) return null;
120490
+ return CODEX_WORKING_STATUS_RE.test(codexStatusBarLine(lines)) ? "working" : null;
120491
+ }
120492
+ function claudePhase(_lines) {
120493
+ return null;
120494
+ }
120495
+ function parseAgentPhase(lines, provider) {
120496
+ if (provider === CODEX_CLI_PROVIDER) return codexPhase(lines);
120497
+ if (provider === CLAUDE_CODE_PROVIDER) return claudePhase(lines);
120498
+ return null;
120499
+ }
120500
+ var init_parseAgentPhase = __esm({
120501
+ "src/services/questions/parseAgentPhase.ts"() {
120502
+ "use strict";
120503
+ init_providers();
120504
+ init_codexScreen();
120505
+ }
120506
+ });
120507
+
120483
120508
  // src/utils/debounce.ts
120484
120509
  function debounce(fn, waitMs) {
120485
120510
  let timer = null;
@@ -120534,7 +120559,11 @@ function toPublicSession(s3) {
120534
120559
  ...s3.lastActivityAt != null && { lastActivityAt: s3.lastActivityAt },
120535
120560
  ...s3.statusSource != null && { statusSource: s3.statusSource },
120536
120561
  ...s3.statusUpdatedAt != null && { statusUpdatedAt: s3.statusUpdatedAt },
120537
- ...s3.filePath != null && { filePath: s3.filePath }
120562
+ ...s3.filePath != null && { filePath: s3.filePath },
120563
+ // Unconditional — see PTYManager's toPublicSession: a streamer re-adopting
120564
+ // a surviving pty-host's sessions mid-turn has no other source for the
120565
+ // phase, and the host's change guard will not re-emit it.
120566
+ subStatus: s3.subStatus ?? null
120538
120567
  };
120539
120568
  }
120540
120569
  var import_crypto9, import_fs25, import_path25, OUTPUT_BUFFER_MAX, INPUT_HISTORY_MAX, QUIET_DETECT_MS, CODEX_READY_FALLBACK_MS, SUBMIT_BYTES, CODEX_SUBMIT_DELAY_MS, CODEX_SUBMIT_MAX_WAIT_MS, CODEX_SUBMIT_STALE_MS, CodexPtyRunner;
@@ -120550,6 +120579,7 @@ var init_codex_pty_runner = __esm({
120550
120579
  init_pty_shared();
120551
120580
  init_codexGateAnswers();
120552
120581
  init_codexScreen();
120582
+ init_parseAgentPhase();
120553
120583
  init_debounce();
120554
120584
  OUTPUT_BUFFER_MAX = 65536;
120555
120585
  INPUT_HISTORY_MAX = 50;
@@ -120563,6 +120593,7 @@ var init_codex_pty_runner = __esm({
120563
120593
  sessions = /* @__PURE__ */ new Map();
120564
120594
  onOutput;
120565
120595
  onStatusChange;
120596
+ onPhaseChange;
120566
120597
  onReady;
120567
120598
  // Broadcasts Codex's blocking startup gates (directory trust, hooks review)
120568
120599
  // as question cards; null dismisses the card once the gate leaves the screen.
@@ -120611,6 +120642,7 @@ var init_codex_pty_runner = __esm({
120611
120642
  constructor(options = {}) {
120612
120643
  this.onOutput = options.onOutput;
120613
120644
  this.onStatusChange = options.onStatusChange;
120645
+ this.onPhaseChange = options.onPhaseChange;
120614
120646
  this.onReady = options.onReady;
120615
120647
  this.onPermissionChange = options.onPermissionChange;
120616
120648
  this.onLiveQuestion = options.onLiveQuestion;
@@ -121178,6 +121210,9 @@ var init_codex_pty_runner = __esm({
121178
121210
  );
121179
121211
  return;
121180
121212
  }
121213
+ if (session.status === "running") {
121214
+ this.setPhase(sessionId, session, parseAgentPhase(lines, CODEX_CLI_PROVIDER));
121215
+ }
121181
121216
  const gate = CODEX_HOOKS_GATE_REGEX.test(screenText) ? "hooks" : CODEX_TRUST_GATE_REGEX.test(screenText) ? "trust" : null;
121182
121217
  if (gate) {
121183
121218
  this.handleGate(sessionId, session, gate, lines);
@@ -121292,11 +121327,25 @@ var init_codex_pty_runner = __esm({
121292
121327
  });
121293
121328
  this.onPermissionChange?.(sessionId, card);
121294
121329
  }
121330
+ /**
121331
+ * Record the agent's phase and notify only on a real change. The guard is
121332
+ * load-bearing, not an optimisation: detectScreenState runs on every chunk,
121333
+ * so an unguarded setter would fire the WS frame behind this several times a
121334
+ * second for a whole turn while reporting the same value. Same contract as
121335
+ * PTYManager.setPhase — the two runners deliberately stay separate classes.
121336
+ */
121337
+ setPhase(sessionId, session, phase) {
121338
+ const next = phase ?? null;
121339
+ if ((session.subStatus ?? null) === next) return;
121340
+ session.subStatus = next;
121341
+ this.onPhaseChange?.(sessionId, next);
121342
+ }
121295
121343
  markReady(sessionId, session, source, reason) {
121296
121344
  session.lastActivityAt = /* @__PURE__ */ new Date();
121297
121345
  session.status = "waiting_input";
121298
121346
  session.statusSource = source;
121299
121347
  session.statusUpdatedAt = /* @__PURE__ */ new Date();
121348
+ this.setPhase(sessionId, session, null);
121300
121349
  this.log.info(`[codex.ready] ${sessionId.slice(0, 8)} ${reason}`, {
121301
121350
  event: "codex.ready",
121302
121351
  sessionId,
@@ -121394,7 +121443,7 @@ var PTY_HOST_PROTOCOL_VERSION, SESSION_DATE_FIELDS, LineDecoder;
121394
121443
  var init_protocol = __esm({
121395
121444
  "src/pty-host/protocol.ts"() {
121396
121445
  "use strict";
121397
- PTY_HOST_PROTOCOL_VERSION = 2;
121446
+ PTY_HOST_PROTOCOL_VERSION = 3;
121398
121447
  SESSION_DATE_FIELDS = [
121399
121448
  "startedAt",
121400
121449
  "completedAt",
@@ -121624,6 +121673,9 @@ var init_remote_session_runner = __esm({
121624
121673
  case "permission-change":
121625
121674
  this.options.onPermissionChange?.(event.sessionId, event.gate);
121626
121675
  break;
121676
+ case "phase-change":
121677
+ this.options.onPhaseChange?.(event.sessionId, event.phase);
121678
+ break;
121627
121679
  case "live-question":
121628
121680
  this.options.onLiveQuestion?.(event.sessionId, event.questions);
121629
121681
  break;
@@ -121884,7 +121936,12 @@ function toPublicSession2(s3) {
121884
121936
  ...s3.statusUpdatedAt != null && { statusUpdatedAt: s3.statusUpdatedAt },
121885
121937
  ...s3.filePath != null && { filePath: s3.filePath },
121886
121938
  ...s3.sessionName != null && { sessionName: s3.sessionName },
121887
- ...s3.firstMessageText != null && { firstMessageText: s3.firstMessageText }
121939
+ ...s3.firstMessageText != null && { firstMessageText: s3.firstMessageText },
121940
+ // Unconditional: this shape crosses the pty-host boundary, and a streamer
121941
+ // re-adopting a surviving host's sessions mid-turn has no other source for
121942
+ // the phase — no snapshot or replay event carries it, and setPhase's change
121943
+ // guard means the host will never re-emit it for the rest of that turn.
121944
+ subStatus: s3.subStatus ?? null
121888
121945
  };
121889
121946
  }
121890
121947
  var import_crypto10, import_fs26, import_path26, OUTPUT_BUFFER_MAX2, INPUT_HISTORY_MAX2, CLAUDE_PROMPT_MARKERS, QUIET_DETECT_MS2, OSC_TAIL_CHARS, SCRAPE_THROTTLE_MS, CLAUDE_READY_FALLBACK_MS, SUBMIT_BYTES2, SUBMIT_DELAY_MS, SUBMIT_MAX_WAIT_MS, PTYManager;
@@ -121902,6 +121959,7 @@ var init_pty_manager = __esm({
121902
121959
  init_detectPermissionGate();
121903
121960
  init_detectQuestionFromScreen();
121904
121961
  init_detectShellPrompt();
121962
+ init_parseAgentPhase();
121905
121963
  init_debounce();
121906
121964
  init_deriveSessionName();
121907
121965
  OUTPUT_BUFFER_MAX2 = 65536;
@@ -121920,6 +121978,7 @@ var init_pty_manager = __esm({
121920
121978
  onStatusChange;
121921
121979
  onReady;
121922
121980
  onPermissionChange;
121981
+ onPhaseChange;
121923
121982
  onLiveQuestion;
121924
121983
  onLiveQuestionGone;
121925
121984
  onUserMessage;
@@ -121981,6 +122040,7 @@ var init_pty_manager = __esm({
121981
122040
  this.onStatusChange = options.onStatusChange;
121982
122041
  this.onReady = options.onReady;
121983
122042
  this.onPermissionChange = options.onPermissionChange;
122043
+ this.onPhaseChange = options.onPhaseChange;
121984
122044
  this.onLiveQuestion = options.onLiveQuestion;
121985
122045
  this.onLiveQuestionGone = options.onLiveQuestionGone;
121986
122046
  this.onUserMessage = options.onUserMessage;
@@ -122487,6 +122547,10 @@ var init_pty_manager = __esm({
122487
122547
  }
122488
122548
  this.lastDetectAt.set(sessionId, nowMs);
122489
122549
  const lines = await this.getOutputLines(sessionId, 60);
122550
+ const phaseSession = this.sessions.get(sessionId);
122551
+ if (phaseSession?.status === "running") {
122552
+ this.setPhase(sessionId, phaseSession, parseAgentPhase(lines, CLAUDE_CODE_PROVIDER));
122553
+ }
122490
122554
  const askFooterOnScreen = lines.some((l) => /Enter to select/i.test(l));
122491
122555
  if (oscPermission || hasAskFooter || askFooterOnScreen) {
122492
122556
  this.log.debug?.(`[pty.prompt_detect] ${sessionId.slice(0, 8)} trigger`, {
@@ -122630,12 +122694,27 @@ var init_pty_manager = __esm({
122630
122694
  }
122631
122695
  // Transition a session from "running" to "waiting_input", clear pendingReady,
122632
122696
  // and flush any queued input. Idempotent: callers can invoke at any chunk.
122697
+ /**
122698
+ * Record the agent's phase and notify only on a real change.
122699
+ *
122700
+ * The change guard is load-bearing, not an optimisation: the scrape pass runs
122701
+ * on every chunk, so an unguarded setter would fire the callback — and the
122702
+ * WS frame behind it — several times a second for the entire duration of a
122703
+ * turn while reporting the same value.
122704
+ */
122705
+ setPhase(sessionId, session, phase) {
122706
+ const next = phase ?? null;
122707
+ if ((session.subStatus ?? null) === next) return;
122708
+ session.subStatus = next;
122709
+ this.onPhaseChange?.(sessionId, next);
122710
+ }
122633
122711
  markReady(sessionId, session, source, reason) {
122634
122712
  this.clearReadyFallback(sessionId);
122635
122713
  session.lastActivityAt = /* @__PURE__ */ new Date();
122636
122714
  session.status = "waiting_input";
122637
122715
  session.statusSource = source;
122638
122716
  session.statusUpdatedAt = /* @__PURE__ */ new Date();
122717
+ this.setPhase(sessionId, session, null);
122639
122718
  const elapsedMs = Date.now() - (this.firstChunkAt.get(sessionId) ?? Date.now());
122640
122719
  this.log.info(`[pty.ready] ${sessionId.slice(0, 8)} ${reason} (elapsed=${elapsedMs}ms)`, {
122641
122720
  event: "pty.ready",
@@ -128105,6 +128184,7 @@ var init_host = __esm({
128105
128184
  onStatusChange: (session) => this.emit({ type: "event", event: "status-change", session }),
128106
128185
  onReady: (session) => this.emit({ type: "event", event: "ready", session }),
128107
128186
  onPermissionChange: (sessionId, gate) => this.emit({ type: "event", event: "permission-change", sessionId, gate }),
128187
+ onPhaseChange: (sessionId, phase) => this.emit({ type: "event", event: "phase-change", sessionId, phase }),
128108
128188
  onLiveQuestion: (sessionId, questions) => this.emit({ type: "event", event: "live-question", sessionId, questions }),
128109
128189
  onLiveQuestionGone: (sessionId) => this.emit({ type: "event", event: "live-question-gone", sessionId }),
128110
128190
  onUserMessage: (sessionId, text, ts2) => this.emit({ type: "event", event: "user-message", sessionId, text, ts: ts2 })
@@ -145287,6 +145367,12 @@ function conversationToResumableSession(c) {
145287
145367
  branch: c.branch ?? void 0,
145288
145368
  lastOutput: "",
145289
145369
  elapsedMs: 0,
145370
+ // No PTY behind a cached conversation, so there is no phase — emitted
145371
+ // explicitly for the same reason as in managedToResponse/discoveredToResponse:
145372
+ // the client merges session frames, so an absent key keeps the previous
145373
+ // value and the indicator latches. `GET /api/sessions/:id` serves this
145374
+ // shape whenever the id is a conversation rather than a live session.
145375
+ subStatus: null,
145290
145376
  promptCount: c.messageCount,
145291
145377
  startedAt: c.lastActivity,
145292
145378
  completedAt: null,
@@ -149714,6 +149800,15 @@ function createLiveSessionOptions(deps) {
149714
149800
  seq
149715
149801
  });
149716
149802
  },
149803
+ onPhaseChange: (sessionId, phase) => {
149804
+ deps.sessionStore.updateManaged(sessionId, { subStatus: phase });
149805
+ deps.wsHub.broadcastToClients(deps.sessionSubscribers.get(sessionId) ?? [], {
149806
+ type: "session_phase",
149807
+ sessionId,
149808
+ phase,
149809
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
149810
+ });
149811
+ },
149717
149812
  onUserMessage: (sessionId, text, ts2) => {
149718
149813
  deps.wsHub.broadcastToClients(deps.sessionSubscribers.get(sessionId) ?? [], {
149719
149814
  type: "user_message",
@@ -151786,6 +151881,7 @@ var SessionStore = class {
151786
151881
  const session = this.managed.get(sessionId);
151787
151882
  if (!session) return null;
151788
151883
  Object.assign(session, updates);
151884
+ if (updates.status != null && updates.status !== "running") session.subStatus = null;
151789
151885
  return session;
151790
151886
  }
151791
151887
  removeManaged(sessionId) {
@@ -151966,6 +152062,13 @@ function managedToResponse(s3, ptyAttached) {
151966
152062
  promptCount: s3.promptCount,
151967
152063
  startedAt: s3.startedAt.toISOString(),
151968
152064
  completedAt: s3.completedAt?.toISOString() ?? null,
152065
+ // Unconditional, like completedAt above — do NOT move this into the
152066
+ // `...(x != null && { x })` block below. That guard uses loose `!=`, which
152067
+ // catches null as well as undefined, and would turn an explicit "no phase"
152068
+ // back into an absent key. The client merges session frames, so an absent
152069
+ // key keeps the previous value and the indicator latches on a finished
152070
+ // turn — the tb-mobile PR #647 bug, arriving through the serialiser.
152071
+ subStatus: s3.subStatus ?? null,
151969
152072
  ptyAttached,
151970
152073
  ...s3.projectId != null && { projectId: s3.projectId },
151971
152074
  ...s3.sessionName != null && { sessionName: s3.sessionName },
@@ -152016,6 +152119,10 @@ function discoveredToResponse(d, conversationId) {
152016
152119
  // cannot see the process's prompt state.
152017
152120
  lifecycle: "detached",
152018
152121
  lifecycleSource: "probe",
152122
+ // No PTY here, so nothing to scrape and no phase to report. Emitted
152123
+ // explicitly rather than omitted, for the same reason as in
152124
+ // managedToResponse: absence must never be a third state on the wire.
152125
+ subStatus: null,
152019
152126
  projectPath: d.projectPath,
152020
152127
  projectName: d.projectName,
152021
152128
  branch: d.branch,