@threadbase-sh/streamer 1.53.1 → 1.54.1

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