@threadbase-sh/streamer 1.38.0 → 1.39.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
@@ -1490,6 +1490,9 @@ function isPermissionMode(value) {
1490
1490
  function isDangerousPermissionMode(mode) {
1491
1491
  return DANGEROUS_PERMISSION_MODES.includes(mode);
1492
1492
  }
1493
+ function isEffortLevel(value) {
1494
+ return typeof value === "string" && EFFORT_LEVELS.includes(value);
1495
+ }
1493
1496
  function findFlag(id) {
1494
1497
  return CLAUDE_FLAGS.find((f2) => f2.id === id);
1495
1498
  }
@@ -1554,7 +1557,7 @@ function buildFlagArgs(values, extraArgs) {
1554
1557
  const args = [];
1555
1558
  const safe = validateFlagValues(values ?? {});
1556
1559
  for (const def of CLAUDE_FLAGS) {
1557
- if (def.id === "permissionMode") continue;
1560
+ if (SPAWN_POSITIONAL_FLAG_IDS.has(def.id)) continue;
1558
1561
  const value = safe[def.id];
1559
1562
  if (value === void 0) continue;
1560
1563
  if (def.valueType === "boolean") {
@@ -1577,7 +1580,7 @@ function buildSettingsJson(permissionMode) {
1577
1580
  }
1578
1581
  return JSON.stringify(settings);
1579
1582
  }
1580
- var PERMISSION_MODES, DANGEROUS_PERMISSION_MODES, CLAUDE_FLAGS;
1583
+ var PERMISSION_MODES, DANGEROUS_PERMISSION_MODES, EFFORT_LEVELS, CLAUDE_FLAGS, SPAWN_POSITIONAL_FLAG_IDS;
1581
1584
  var init_claude_flags = __esm({
1582
1585
  "src/claude-flags.ts"() {
1583
1586
  "use strict";
@@ -1593,6 +1596,7 @@ var init_claude_flags = __esm({
1593
1596
  "bypassPermissions",
1594
1597
  "dontAsk"
1595
1598
  ];
1599
+ EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
1596
1600
  CLAUDE_FLAGS = [
1597
1601
  {
1598
1602
  id: "permissionMode",
@@ -1604,9 +1608,10 @@ var init_claude_flags = __esm({
1604
1608
  { id: "addDir", flag: "--add-dir", valueType: "list", risk: "elevated" },
1605
1609
  { id: "allowedTools", flag: "--allowedTools", valueType: "list", risk: "elevated" },
1606
1610
  { id: "disallowedTools", flag: "--disallowedTools", valueType: "list", risk: "low" },
1607
- { id: "maxBudgetUsd", flag: "--max-budget-usd", valueType: "string", risk: "low" },
1608
- { id: "fallbackModel", flag: "--fallback-model", valueType: "string", risk: "low" }
1611
+ { id: "model", flag: "--model", valueType: "string", risk: "low" },
1612
+ { id: "effort", flag: "--effort", valueType: "enum", enumValues: EFFORT_LEVELS, risk: "low" }
1609
1613
  ];
1614
+ SPAWN_POSITIONAL_FLAG_IDS = /* @__PURE__ */ new Set(["permissionMode", "model", "effort"]);
1610
1615
  }
1611
1616
  });
1612
1617
 
@@ -139499,6 +139504,14 @@ var createSessionRoutes = (deps) => {
139499
139504
  await deps.handleSetSessionName(c.req.param("id"), c.env.incoming, c.env.outgoing);
139500
139505
  return alreadyHandled6();
139501
139506
  });
139507
+ app.patch("/:id/model", async (c) => {
139508
+ await deps.handleSetSessionModel(c.req.param("id"), c.env.incoming, c.env.outgoing);
139509
+ return alreadyHandled6();
139510
+ });
139511
+ app.patch("/:id/effort", async (c) => {
139512
+ await deps.handleSetSessionEffort(c.req.param("id"), c.env.incoming, c.env.outgoing);
139513
+ return alreadyHandled6();
139514
+ });
139502
139515
  app.post("/:id/adopt", async (c) => {
139503
139516
  await deps.handleAdopt(c.req.param("id"), c.env.outgoing);
139504
139517
  return alreadyHandled6();
@@ -139838,6 +139851,7 @@ CREATE TABLE IF NOT EXISTS conversation_meta (
139838
139851
  );
139839
139852
  CREATE INDEX IF NOT EXISTS idx_meta_last_activity ON conversation_meta(last_activity DESC);
139840
139853
  CREATE INDEX IF NOT EXISTS idx_meta_project ON conversation_meta(project_path);
139854
+ CREATE INDEX IF NOT EXISTS idx_meta_file_path ON conversation_meta(file_path);
139841
139855
 
139842
139856
  CREATE TABLE IF NOT EXISTS conversation_tail (
139843
139857
  conversation_id TEXT PRIMARY KEY REFERENCES conversation_meta(id) ON DELETE CASCADE,
@@ -145915,6 +145929,24 @@ var WSHub = class {
145915
145929
  this.clients.delete(client);
145916
145930
  }
145917
145931
  }
145932
+ // Scoped broadcast for high-frequency per-session messages (terminal_output,
145933
+ // user_message). Sending to every connected client for every PTY output
145934
+ // chunk made broadcast() cost scale with connections x active sessions;
145935
+ // this bounds it to only that session's subscribers.
145936
+ broadcastToClients(clients, message) {
145937
+ const data = JSON.stringify(message);
145938
+ for (const client of clients) {
145939
+ try {
145940
+ if (client.readyState === client.OPEN) {
145941
+ client.send(data);
145942
+ } else {
145943
+ this.clients.delete(client);
145944
+ }
145945
+ } catch {
145946
+ this.clients.delete(client);
145947
+ }
145948
+ }
145949
+ }
145918
145950
  unicast(ws2, message) {
145919
145951
  try {
145920
145952
  if (ws2.readyState === ws2.OPEN) {
@@ -145977,6 +146009,7 @@ var ADOPT_KILL_TIMEOUT_MS = 5e3;
145977
146009
  var ADOPT_KILL_POLL_MS = 100;
145978
146010
  var REFRESH_TTL_MS = 2e3;
145979
146011
  var START_READY_TIMEOUT_MS = 1e4;
146012
+ var MODEL_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
145980
146013
  var EXTERNAL_TAIL_RECENCY_MS = RESUME_BUSY_WINDOW_MS;
145981
146014
  var EXTERNAL_TAIL_MAX = 32;
145982
146015
  var EXTERNAL_TAIL_IDLE_MS = 3e5;
@@ -146080,6 +146113,8 @@ var StreamerServer = class {
146080
146113
  dbPool = null;
146081
146114
  dbInstanceId = null;
146082
146115
  disableDb = false;
146116
+ // Skip the startup warm-up scan (test hook; see ServerConfig.skipStartupWarmup).
146117
+ skipStartupWarmup;
146083
146118
  browseRoot = null;
146084
146119
  publicUrl = null;
146085
146120
  browserCors;
@@ -146119,6 +146154,11 @@ var StreamerServer = class {
146119
146154
  // every provider; read only by the idle reaper. Entries are dropped when the
146120
146155
  // session leaves the runner (reap/exit/hold).
146121
146156
  lastAgentChunkAt = /* @__PURE__ */ new Map();
146157
+ // sessionId → last terminal_output seq broadcast (starts at 1, per session).
146158
+ // Stamped on every terminal_output/terminal_replay so a client can detect a
146159
+ // stale chunk delivered after a reconnect race instead of trusting raw WS
146160
+ // arrival order. Entries dropped alongside lastAgentChunkAt.
146161
+ terminalSeq = /* @__PURE__ */ new Map();
146122
146162
  // Recently accepted input idempotency keys (C4). A retried POST replays its
146123
146163
  // original outcome instead of submitting the prompt to the agent twice.
146124
146164
  idempotency = new IdempotencyStore();
@@ -146186,6 +146226,7 @@ var StreamerServer = class {
146186
146226
  }
146187
146227
  this.verbose = config2.verbose ?? false;
146188
146228
  this.disableDb = config2.disableDb ?? false;
146229
+ this.skipStartupWarmup = config2.skipStartupWarmup ?? false;
146189
146230
  this.scannerPersistenceDisabled = config2.scannerPersistent === false;
146190
146231
  this.scanProfiles = config2.scanProfiles;
146191
146232
  this.codexRoots = config2.codexRoots ?? [(0, import_path29.join)((0, import_os12.homedir)(), ".codex", "sessions")];
@@ -146334,10 +146375,22 @@ var StreamerServer = class {
146334
146375
  logger: getLogger("pty"),
146335
146376
  onOutput: (sessionId, data) => {
146336
146377
  this.lastAgentChunkAt.set(sessionId, Date.now());
146337
- this.wsHub.broadcast({ type: "terminal_output", sessionId, data });
146378
+ const seq = (this.terminalSeq.get(sessionId) ?? 0) + 1;
146379
+ this.terminalSeq.set(sessionId, seq);
146380
+ this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], {
146381
+ type: "terminal_output",
146382
+ sessionId,
146383
+ data,
146384
+ seq
146385
+ });
146338
146386
  },
146339
146387
  onUserMessage: (sessionId, text, ts2) => {
146340
- this.wsHub.broadcast({ type: "user_message", sessionId, text, ts: ts2 });
146388
+ this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], {
146389
+ type: "user_message",
146390
+ sessionId,
146391
+ text,
146392
+ ts: ts2
146393
+ });
146341
146394
  },
146342
146395
  onPermissionChange: (sessionId, gate) => {
146343
146396
  this.handlePermissionChange(sessionId, gate);
@@ -146474,6 +146527,8 @@ var StreamerServer = class {
146474
146527
  handleCancel: (id, res) => this.handleCancel(id, res),
146475
146528
  handleStopSession: (id, res) => this.handleStopSession(id, res),
146476
146529
  handleSetSessionName: (id, req, res) => this.handleSetSessionName(id, req, res),
146530
+ handleSetSessionModel: (id, req, res) => this.applyLiveSessionSetting(id, req, res, "model"),
146531
+ handleSetSessionEffort: (id, req, res) => this.applyLiveSessionSetting(id, req, res, "effort"),
146477
146532
  handleUploadFile: (id, req, res) => this.handleUploadFile(id, req, res),
146478
146533
  handleAdopt: (id, res) => this.handleAdopt(id, res),
146479
146534
  handleResume: (req, res) => this.handleResume(req, res),
@@ -146520,7 +146575,8 @@ var StreamerServer = class {
146520
146575
  type: "terminal_replay",
146521
146576
  sessionId: msg.sessionId,
146522
146577
  lines,
146523
- userMessages
146578
+ userMessages,
146579
+ seq: this.terminalSeq.get(msg.sessionId)
146524
146580
  })
146525
146581
  );
146526
146582
  }
@@ -146880,6 +146936,7 @@ var StreamerServer = class {
146880
146936
  );
146881
146937
  this.ptyManager.putOnHold(session.id);
146882
146938
  this.lastAgentChunkAt.delete(session.id);
146939
+ this.terminalSeq.delete(session.id);
146883
146940
  this.idempotency.clear(session.id);
146884
146941
  this.sessionSubscribers.delete(session.id);
146885
146942
  reaped.push(session.id);
@@ -147054,6 +147111,14 @@ var StreamerServer = class {
147054
147111
  );
147055
147112
  this.scannerPersistenceDisabled = true;
147056
147113
  }
147114
+ if (this.skipStartupWarmup) {
147115
+ this.log.debug?.("startup warm-up scan skipped (skipStartupWarmup)", {
147116
+ event: "cache.warmup_skipped"
147117
+ });
147118
+ this.finishWarmup(0);
147119
+ resolveWarm();
147120
+ return;
147121
+ }
147057
147122
  const warmupStatCache = this.buildStatCache(null);
147058
147123
  const warmupScanner = this.newScanner(warmupStatCache ? { persistent: false } : void 0);
147059
147124
  this.allScanners.add(warmupScanner);
@@ -147245,6 +147310,7 @@ var StreamerServer = class {
147245
147310
  this.idleReaperTimer = null;
147246
147311
  }
147247
147312
  this.lastAgentChunkAt.clear();
147313
+ this.terminalSeq.clear();
147248
147314
  this.recordShutdownState();
147249
147315
  this.markScannerStaleDebounced.cancel();
147250
147316
  await Promise.all([...this.inFlightCacheWrites]);
@@ -147426,6 +147492,30 @@ var StreamerServer = class {
147426
147492
  persisted: this.claudeFlagsPersistable
147427
147493
  };
147428
147494
  }
147495
+ /**
147496
+ * The three spawn options that a configured claude-flag can override, with
147497
+ * the boot-time CLI/yaml default as the fallback. Spread into every
147498
+ * start/resume/adopt call so all three paths agree.
147499
+ *
147500
+ * These ids are excluded from buildFlagArgs (SPAWN_POSITIONAL_FLAG_IDS)
147501
+ * precisely because they arrive here instead — the PTY spawn paths pass them
147502
+ * as explicit positionals, so emitting them from the allowlist too would
147503
+ * duplicate the flag.
147504
+ *
147505
+ * Narrowed with the type guards rather than cast: ClaudeFlagValues is a loose
147506
+ * Record by design, and while validateFlagValues already guarantees the shape
147507
+ * on the way in, TypeScript cannot see that through the record.
147508
+ */
147509
+ spawnFlagOverrides() {
147510
+ const mode = this.claudeFlags.permissionMode;
147511
+ const model = this.claudeFlags.model;
147512
+ const effort = this.claudeFlags.effort;
147513
+ return {
147514
+ permissionMode: isPermissionMode(mode) ? mode : this.defaultPermissionMode,
147515
+ model: typeof model === "string" ? model : this.defaultModel,
147516
+ effort: isEffortLevel(effort) ? effort : this.defaultEffort
147517
+ };
147518
+ }
147429
147519
  checkRateLimit(map2, key, limit, windowMs) {
147430
147520
  const now = Date.now();
147431
147521
  const arr = (map2.get(key) ?? []).filter((t) => now - t < windowMs);
@@ -148575,11 +148665,9 @@ var StreamerServer = class {
148575
148665
  projectPath,
148576
148666
  projectName: body.projectName,
148577
148667
  branch: body.branch,
148578
- permissionMode: this.defaultPermissionMode,
148579
148668
  claudeFlags: this.claudeFlags,
148580
148669
  claudeExtraArgs: this.claudeExtraArgs,
148581
- model: this.defaultModel,
148582
- effort: this.defaultEffort
148670
+ ...this.spawnFlagOverrides()
148583
148671
  });
148584
148672
  this.sessionStore.addManaged(session);
148585
148673
  this.recordSessionSpawn(session);
@@ -149026,11 +149114,9 @@ var StreamerServer = class {
149026
149114
  projectPath,
149027
149115
  projectName,
149028
149116
  branch,
149029
- permissionMode: this.defaultPermissionMode,
149030
149117
  claudeFlags: this.claudeFlags,
149031
149118
  claudeExtraArgs: this.claudeExtraArgs,
149032
- model: this.defaultModel,
149033
- effort: this.defaultEffort
149119
+ ...this.spawnFlagOverrides()
149034
149120
  });
149035
149121
  this.sessionStore.addManaged(session);
149036
149122
  this.recordSessionSpawn(session);
@@ -149103,11 +149189,9 @@ var StreamerServer = class {
149103
149189
  projectPath: resolvedPath,
149104
149190
  projectName: body.projectName,
149105
149191
  ...includeSystemPrompt && { systemPrompt: systemPromptParts.join("\n") },
149106
- permissionMode: this.defaultPermissionMode,
149107
149192
  claudeFlags: this.claudeFlags,
149108
149193
  claudeExtraArgs: this.claudeExtraArgs,
149109
- model: this.defaultModel,
149110
- effort: this.defaultEffort
149194
+ ...this.spawnFlagOverrides()
149111
149195
  });
149112
149196
  this.sessionStore.addManaged(session);
149113
149197
  this.recordSessionSpawn(session);
@@ -149467,6 +149551,87 @@ var StreamerServer = class {
149467
149551
  this.cache.upsertSessionName(sessionId, name);
149468
149552
  json2(res, 200, { ok: true });
149469
149553
  }
149554
+ /**
149555
+ * Retarget a LIVE session's model or effort by typing the corresponding
149556
+ * Claude Code slash command into its PTY.
149557
+ *
149558
+ * There is no CLI or IPC channel for this — `--model`/`--effort` are spawn
149559
+ * arguments — so the interactive `/model <x>` / `/effort <y>` commands are the
149560
+ * only way to change a session already running. Both accept an argument and
149561
+ * apply it without opening the picker (verified against Claude Code v2.1.220).
149562
+ *
149563
+ * Answers 202, not 200: the value is applied by the TUI on its next render, so
149564
+ * there is nothing truthful to echo back synchronously. Clients confirm with
149565
+ * `GET /api/sessions/:id`, which scrapes the applied value off the live status
149566
+ * line.
149567
+ */
149568
+ async applyLiveSessionSetting(sessionId, req, res, setting) {
149569
+ const session = this.ptyManager.getSession(sessionId);
149570
+ if (!session) {
149571
+ const known = this.sessionStore.getManaged(sessionId);
149572
+ if (known) {
149573
+ json2(res, 409, {
149574
+ error: "Session has no live PTY; resume it first",
149575
+ code: "SESSION_IDLE"
149576
+ });
149577
+ return;
149578
+ }
149579
+ json2(res, 404, { error: "Session not found" });
149580
+ return;
149581
+ }
149582
+ if ((session.provider ?? CLAUDE_CODE_PROVIDER2) !== CLAUDE_CODE_PROVIDER2) {
149583
+ json2(res, 501, {
149584
+ error: `Setting ${setting} on a ${session.provider} session is not supported`,
149585
+ code: "UNSUPPORTED_PROVIDER"
149586
+ });
149587
+ return;
149588
+ }
149589
+ if (session.status === "running") {
149590
+ json2(res, 409, {
149591
+ error: "Session is mid-turn; retry once it is waiting for input",
149592
+ code: "SESSION_BUSY"
149593
+ });
149594
+ return;
149595
+ }
149596
+ let parsed;
149597
+ try {
149598
+ parsed = await readBody(req);
149599
+ } catch {
149600
+ json2(res, 400, { error: "Invalid JSON" });
149601
+ return;
149602
+ }
149603
+ let value;
149604
+ if (setting === "effort") {
149605
+ if (!isEffortLevel(parsed.effort)) {
149606
+ json2(res, 400, {
149607
+ error: `effort must be one of ${EFFORT_LEVELS.join(", ")}`
149608
+ });
149609
+ return;
149610
+ }
149611
+ value = parsed.effort;
149612
+ } else {
149613
+ if (typeof parsed.model !== "string" || !MODEL_NAME_RE.test(parsed.model)) {
149614
+ json2(res, 400, {
149615
+ error: "model must be an alias or full model name (letters, digits, dot, dash, underscore)"
149616
+ });
149617
+ return;
149618
+ }
149619
+ value = parsed.model;
149620
+ }
149621
+ try {
149622
+ this.ptyManager.sendKeys(sessionId, `/${setting} ${value}\r`);
149623
+ } catch (err) {
149624
+ json2(res, 400, { error: err instanceof Error ? err.message : "Failed to write to session" });
149625
+ return;
149626
+ }
149627
+ this.log.info(`Live session ${setting} set to ${value}`, {
149628
+ event: "session.setting_applied",
149629
+ sessionId,
149630
+ setting,
149631
+ value
149632
+ });
149633
+ json2(res, 202, { id: sessionId, [setting]: value });
149634
+ }
149470
149635
  handleGetSessionNames(res) {
149471
149636
  if (!this.cache) {
149472
149637
  json2(res, 200, {});
@@ -153557,10 +153722,9 @@ program2.command("serve").description("Start the streamer server").option("-p, -
153557
153722
  }
153558
153723
  featureFlags = parsed.values;
153559
153724
  }
153560
- const validEfforts = ["low", "medium", "high", "xhigh", "max"];
153561
- if (opts.defaultEffort !== void 0 && !validEfforts.includes(opts.defaultEffort)) {
153725
+ if (opts.defaultEffort !== void 0 && !isEffortLevel(opts.defaultEffort)) {
153562
153726
  log11.error(
153563
- `Invalid --default-effort: ${opts.defaultEffort} (expected one of ${validEfforts.join(", ")})`,
153727
+ `Invalid --default-effort: ${opts.defaultEffort} (expected one of ${EFFORT_LEVELS.join(", ")})`,
153564
153728
  void 0,
153565
153729
  "console"
153566
153730
  );