@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/index.d.cts CHANGED
@@ -81,6 +81,9 @@ interface Logger {
81
81
  /** Claude Code `--permission-mode` values, as accepted by CLI v2.1.x. */
82
82
  declare const PERMISSION_MODES: readonly ["acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan"];
83
83
  type PermissionMode = (typeof PERMISSION_MODES)[number];
84
+ /** Claude Code `--effort` levels, as accepted by CLI v2.1.x. */
85
+ declare const EFFORT_LEVELS: readonly ["low", "medium", "high", "xhigh", "max"];
86
+ type EffortLevel = (typeof EFFORT_LEVELS)[number];
84
87
  type FlagValueType = "boolean" | "string" | "enum" | "list";
85
88
  /** How risky enabling a flag is. Drives the client's confirmation UX. */
86
89
  type FlagRisk = "low" | "elevated" | "dangerous";
@@ -245,6 +248,7 @@ type WSMessage = {
245
248
  type: "terminal_output";
246
249
  sessionId: string;
247
250
  data: string;
251
+ seq?: number;
248
252
  } | {
249
253
  type: "session_update";
250
254
  session?: SessionResponse;
@@ -303,6 +307,7 @@ type WSMessage = {
303
307
  sessionId: string;
304
308
  lines: string[];
305
309
  userMessages?: UserMessage[];
310
+ seq?: number;
306
311
  } | {
307
312
  type: "session_ready";
308
313
  session: SessionResponse;
@@ -493,6 +498,7 @@ interface ServerConfig {
493
498
  }>;
494
499
  codexRoots?: string[];
495
500
  scannerPersistent?: boolean;
501
+ skipStartupWarmup?: boolean;
496
502
  ptyGracePeriodMs?: number;
497
503
  cacheDir?: string;
498
504
  tailSize?: number;
@@ -502,7 +508,7 @@ interface ServerConfig {
502
508
  featureFlags?: FeatureFlagValues;
503
509
  defaultPermissionMode?: PermissionMode;
504
510
  defaultModel?: string;
505
- defaultEffort?: "low" | "medium" | "high" | "xhigh" | "max";
511
+ defaultEffort?: EffortLevel;
506
512
  claudeFlags?: ClaudeFlagValues;
507
513
  claudeExtraArgs?: string;
508
514
  }
@@ -527,7 +533,7 @@ interface StartSessionOptions {
527
533
  branch?: string;
528
534
  permissionMode?: PermissionMode;
529
535
  model?: string;
530
- effort?: "low" | "medium" | "high" | "xhigh" | "max";
536
+ effort?: EffortLevel;
531
537
  claudeFlags?: ClaudeFlagValues;
532
538
  claudeExtraArgs?: string;
533
539
  }
@@ -537,7 +543,7 @@ interface StartFreshSessionOptions {
537
543
  systemPrompt?: string;
538
544
  permissionMode?: PermissionMode;
539
545
  model?: string;
540
- effort?: "low" | "medium" | "high" | "xhigh" | "max";
546
+ effort?: EffortLevel;
541
547
  claudeFlags?: ClaudeFlagValues;
542
548
  claudeExtraArgs?: string;
543
549
  }
@@ -1277,6 +1283,7 @@ declare class WSHub {
1277
1283
  private pongTimers;
1278
1284
  addClient(ws: WebSocket): void;
1279
1285
  broadcast(message: WSMessage): void;
1286
+ broadcastToClients(clients: Iterable<WebSocket>, message: WSMessage): void;
1280
1287
  unicast(ws: WebSocket, message: WSMessage): void;
1281
1288
  get connectionCount(): number;
1282
1289
  dispose(): void;
@@ -1418,6 +1425,8 @@ type ApiDeps = {
1418
1425
  handleCancel: (sessionId: string, res: ServerResponse) => void;
1419
1426
  handleStopSession: (sessionId: string, res: ServerResponse) => Promise<void>;
1420
1427
  handleSetSessionName: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1428
+ handleSetSessionModel: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1429
+ handleSetSessionEffort: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1421
1430
  handleUploadFile: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1422
1431
  handleAdopt: (sessionId: string, res: ServerResponse) => Promise<void>;
1423
1432
  handleResume: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
@@ -1588,6 +1597,7 @@ declare class StreamerServer {
1588
1597
  private dbPool;
1589
1598
  private dbInstanceId;
1590
1599
  private disableDb;
1600
+ private skipStartupWarmup;
1591
1601
  private browseRoot;
1592
1602
  private publicUrl;
1593
1603
  private browserCors;
@@ -1609,6 +1619,7 @@ declare class StreamerServer {
1609
1619
  private ptyGraceDeferCounts;
1610
1620
  private sessionSubscribers;
1611
1621
  private lastAgentChunkAt;
1622
+ private terminalSeq;
1612
1623
  private idempotency;
1613
1624
  private sessionLifecycles;
1614
1625
  private idleReaperTimer;
@@ -1782,6 +1793,21 @@ declare class StreamerServer {
1782
1793
  * permission prompts entirely, so it needs a forensic trail.
1783
1794
  */
1784
1795
  private setClaudeFlagsConfig;
1796
+ /**
1797
+ * The three spawn options that a configured claude-flag can override, with
1798
+ * the boot-time CLI/yaml default as the fallback. Spread into every
1799
+ * start/resume/adopt call so all three paths agree.
1800
+ *
1801
+ * These ids are excluded from buildFlagArgs (SPAWN_POSITIONAL_FLAG_IDS)
1802
+ * precisely because they arrive here instead — the PTY spawn paths pass them
1803
+ * as explicit positionals, so emitting them from the allowlist too would
1804
+ * duplicate the flag.
1805
+ *
1806
+ * Narrowed with the type guards rather than cast: ClaudeFlagValues is a loose
1807
+ * Record by design, and while validateFlagValues already guarantees the shape
1808
+ * on the way in, TypeScript cannot see that through the record.
1809
+ */
1810
+ private spawnFlagOverrides;
1785
1811
  private checkRateLimit;
1786
1812
  private checkExchangeRateLimit;
1787
1813
  private checkSessionStartRateLimit;
@@ -1902,6 +1928,21 @@ declare class StreamerServer {
1902
1928
  private handleBrowse;
1903
1929
  private handleMkdir;
1904
1930
  private handleSetSessionName;
1931
+ /**
1932
+ * Retarget a LIVE session's model or effort by typing the corresponding
1933
+ * Claude Code slash command into its PTY.
1934
+ *
1935
+ * There is no CLI or IPC channel for this — `--model`/`--effort` are spawn
1936
+ * arguments — so the interactive `/model <x>` / `/effort <y>` commands are the
1937
+ * only way to change a session already running. Both accept an argument and
1938
+ * apply it without opening the picker (verified against Claude Code v2.1.220).
1939
+ *
1940
+ * Answers 202, not 200: the value is applied by the TUI on its next render, so
1941
+ * there is nothing truthful to echo back synchronously. Clients confirm with
1942
+ * `GET /api/sessions/:id`, which scrapes the applied value off the live status
1943
+ * line.
1944
+ */
1945
+ private applyLiveSessionSetting;
1905
1946
  private handleGetSessionNames;
1906
1947
  }
1907
1948
 
package/dist/index.d.ts CHANGED
@@ -81,6 +81,9 @@ interface Logger {
81
81
  /** Claude Code `--permission-mode` values, as accepted by CLI v2.1.x. */
82
82
  declare const PERMISSION_MODES: readonly ["acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan"];
83
83
  type PermissionMode = (typeof PERMISSION_MODES)[number];
84
+ /** Claude Code `--effort` levels, as accepted by CLI v2.1.x. */
85
+ declare const EFFORT_LEVELS: readonly ["low", "medium", "high", "xhigh", "max"];
86
+ type EffortLevel = (typeof EFFORT_LEVELS)[number];
84
87
  type FlagValueType = "boolean" | "string" | "enum" | "list";
85
88
  /** How risky enabling a flag is. Drives the client's confirmation UX. */
86
89
  type FlagRisk = "low" | "elevated" | "dangerous";
@@ -245,6 +248,7 @@ type WSMessage = {
245
248
  type: "terminal_output";
246
249
  sessionId: string;
247
250
  data: string;
251
+ seq?: number;
248
252
  } | {
249
253
  type: "session_update";
250
254
  session?: SessionResponse;
@@ -303,6 +307,7 @@ type WSMessage = {
303
307
  sessionId: string;
304
308
  lines: string[];
305
309
  userMessages?: UserMessage[];
310
+ seq?: number;
306
311
  } | {
307
312
  type: "session_ready";
308
313
  session: SessionResponse;
@@ -493,6 +498,7 @@ interface ServerConfig {
493
498
  }>;
494
499
  codexRoots?: string[];
495
500
  scannerPersistent?: boolean;
501
+ skipStartupWarmup?: boolean;
496
502
  ptyGracePeriodMs?: number;
497
503
  cacheDir?: string;
498
504
  tailSize?: number;
@@ -502,7 +508,7 @@ interface ServerConfig {
502
508
  featureFlags?: FeatureFlagValues;
503
509
  defaultPermissionMode?: PermissionMode;
504
510
  defaultModel?: string;
505
- defaultEffort?: "low" | "medium" | "high" | "xhigh" | "max";
511
+ defaultEffort?: EffortLevel;
506
512
  claudeFlags?: ClaudeFlagValues;
507
513
  claudeExtraArgs?: string;
508
514
  }
@@ -527,7 +533,7 @@ interface StartSessionOptions {
527
533
  branch?: string;
528
534
  permissionMode?: PermissionMode;
529
535
  model?: string;
530
- effort?: "low" | "medium" | "high" | "xhigh" | "max";
536
+ effort?: EffortLevel;
531
537
  claudeFlags?: ClaudeFlagValues;
532
538
  claudeExtraArgs?: string;
533
539
  }
@@ -537,7 +543,7 @@ interface StartFreshSessionOptions {
537
543
  systemPrompt?: string;
538
544
  permissionMode?: PermissionMode;
539
545
  model?: string;
540
- effort?: "low" | "medium" | "high" | "xhigh" | "max";
546
+ effort?: EffortLevel;
541
547
  claudeFlags?: ClaudeFlagValues;
542
548
  claudeExtraArgs?: string;
543
549
  }
@@ -1277,6 +1283,7 @@ declare class WSHub {
1277
1283
  private pongTimers;
1278
1284
  addClient(ws: WebSocket): void;
1279
1285
  broadcast(message: WSMessage): void;
1286
+ broadcastToClients(clients: Iterable<WebSocket>, message: WSMessage): void;
1280
1287
  unicast(ws: WebSocket, message: WSMessage): void;
1281
1288
  get connectionCount(): number;
1282
1289
  dispose(): void;
@@ -1418,6 +1425,8 @@ type ApiDeps = {
1418
1425
  handleCancel: (sessionId: string, res: ServerResponse) => void;
1419
1426
  handleStopSession: (sessionId: string, res: ServerResponse) => Promise<void>;
1420
1427
  handleSetSessionName: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1428
+ handleSetSessionModel: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1429
+ handleSetSessionEffort: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1421
1430
  handleUploadFile: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1422
1431
  handleAdopt: (sessionId: string, res: ServerResponse) => Promise<void>;
1423
1432
  handleResume: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
@@ -1588,6 +1597,7 @@ declare class StreamerServer {
1588
1597
  private dbPool;
1589
1598
  private dbInstanceId;
1590
1599
  private disableDb;
1600
+ private skipStartupWarmup;
1591
1601
  private browseRoot;
1592
1602
  private publicUrl;
1593
1603
  private browserCors;
@@ -1609,6 +1619,7 @@ declare class StreamerServer {
1609
1619
  private ptyGraceDeferCounts;
1610
1620
  private sessionSubscribers;
1611
1621
  private lastAgentChunkAt;
1622
+ private terminalSeq;
1612
1623
  private idempotency;
1613
1624
  private sessionLifecycles;
1614
1625
  private idleReaperTimer;
@@ -1782,6 +1793,21 @@ declare class StreamerServer {
1782
1793
  * permission prompts entirely, so it needs a forensic trail.
1783
1794
  */
1784
1795
  private setClaudeFlagsConfig;
1796
+ /**
1797
+ * The three spawn options that a configured claude-flag can override, with
1798
+ * the boot-time CLI/yaml default as the fallback. Spread into every
1799
+ * start/resume/adopt call so all three paths agree.
1800
+ *
1801
+ * These ids are excluded from buildFlagArgs (SPAWN_POSITIONAL_FLAG_IDS)
1802
+ * precisely because they arrive here instead — the PTY spawn paths pass them
1803
+ * as explicit positionals, so emitting them from the allowlist too would
1804
+ * duplicate the flag.
1805
+ *
1806
+ * Narrowed with the type guards rather than cast: ClaudeFlagValues is a loose
1807
+ * Record by design, and while validateFlagValues already guarantees the shape
1808
+ * on the way in, TypeScript cannot see that through the record.
1809
+ */
1810
+ private spawnFlagOverrides;
1785
1811
  private checkRateLimit;
1786
1812
  private checkExchangeRateLimit;
1787
1813
  private checkSessionStartRateLimit;
@@ -1902,6 +1928,21 @@ declare class StreamerServer {
1902
1928
  private handleBrowse;
1903
1929
  private handleMkdir;
1904
1930
  private handleSetSessionName;
1931
+ /**
1932
+ * Retarget a LIVE session's model or effort by typing the corresponding
1933
+ * Claude Code slash command into its PTY.
1934
+ *
1935
+ * There is no CLI or IPC channel for this — `--model`/`--effort` are spawn
1936
+ * arguments — so the interactive `/model <x>` / `/effort <y>` commands are the
1937
+ * only way to change a session already running. Both accept an argument and
1938
+ * apply it without opening the picker (verified against Claude Code v2.1.220).
1939
+ *
1940
+ * Answers 202, not 200: the value is applied by the TUI on its next render, so
1941
+ * there is nothing truthful to echo back synchronously. Clients confirm with
1942
+ * `GET /api/sessions/:id`, which scrapes the applied value off the live status
1943
+ * line.
1944
+ */
1945
+ private applyLiveSessionSetting;
1905
1946
  private handleGetSessionNames;
1906
1947
  }
1907
1948
 
package/dist/index.js CHANGED
@@ -298,6 +298,10 @@ var DANGEROUS_PERMISSION_MODES = [
298
298
  function isDangerousPermissionMode(mode) {
299
299
  return DANGEROUS_PERMISSION_MODES.includes(mode);
300
300
  }
301
+ var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
302
+ function isEffortLevel(value) {
303
+ return typeof value === "string" && EFFORT_LEVELS.includes(value);
304
+ }
301
305
  var CLAUDE_FLAGS = [
302
306
  {
303
307
  id: "permissionMode",
@@ -309,9 +313,10 @@ var CLAUDE_FLAGS = [
309
313
  { id: "addDir", flag: "--add-dir", valueType: "list", risk: "elevated" },
310
314
  { id: "allowedTools", flag: "--allowedTools", valueType: "list", risk: "elevated" },
311
315
  { id: "disallowedTools", flag: "--disallowedTools", valueType: "list", risk: "low" },
312
- { id: "maxBudgetUsd", flag: "--max-budget-usd", valueType: "string", risk: "low" },
313
- { id: "fallbackModel", flag: "--fallback-model", valueType: "string", risk: "low" }
316
+ { id: "model", flag: "--model", valueType: "string", risk: "low" },
317
+ { id: "effort", flag: "--effort", valueType: "enum", enumValues: EFFORT_LEVELS, risk: "low" }
314
318
  ];
319
+ var SPAWN_POSITIONAL_FLAG_IDS = /* @__PURE__ */ new Set(["permissionMode", "model", "effort"]);
315
320
  function findFlag(id) {
316
321
  return CLAUDE_FLAGS.find((f) => f.id === id);
317
322
  }
@@ -376,7 +381,7 @@ function buildFlagArgs(values, extraArgs) {
376
381
  const args = [];
377
382
  const safe = validateFlagValues(values ?? {});
378
383
  for (const def of CLAUDE_FLAGS) {
379
- if (def.id === "permissionMode") continue;
384
+ if (SPAWN_POSITIONAL_FLAG_IDS.has(def.id)) continue;
380
385
  const value = safe[def.id];
381
386
  if (value === void 0) continue;
382
387
  if (def.valueType === "boolean") {
@@ -4671,6 +4676,14 @@ var createSessionRoutes = (deps) => {
4671
4676
  await deps.handleSetSessionName(c.req.param("id"), c.env.incoming, c.env.outgoing);
4672
4677
  return alreadyHandled6();
4673
4678
  });
4679
+ app.patch("/:id/model", async (c) => {
4680
+ await deps.handleSetSessionModel(c.req.param("id"), c.env.incoming, c.env.outgoing);
4681
+ return alreadyHandled6();
4682
+ });
4683
+ app.patch("/:id/effort", async (c) => {
4684
+ await deps.handleSetSessionEffort(c.req.param("id"), c.env.incoming, c.env.outgoing);
4685
+ return alreadyHandled6();
4686
+ });
4674
4687
  app.post("/:id/adopt", async (c) => {
4675
4688
  await deps.handleAdopt(c.req.param("id"), c.env.outgoing);
4676
4689
  return alreadyHandled6();
@@ -5008,6 +5021,7 @@ CREATE TABLE IF NOT EXISTS conversation_meta (
5008
5021
  );
5009
5022
  CREATE INDEX IF NOT EXISTS idx_meta_last_activity ON conversation_meta(last_activity DESC);
5010
5023
  CREATE INDEX IF NOT EXISTS idx_meta_project ON conversation_meta(project_path);
5024
+ CREATE INDEX IF NOT EXISTS idx_meta_file_path ON conversation_meta(file_path);
5011
5025
 
5012
5026
  CREATE TABLE IF NOT EXISTS conversation_tail (
5013
5027
  conversation_id TEXT PRIMARY KEY REFERENCES conversation_meta(id) ON DELETE CASCADE,
@@ -8665,6 +8679,24 @@ var WSHub = class {
8665
8679
  this.clients.delete(client);
8666
8680
  }
8667
8681
  }
8682
+ // Scoped broadcast for high-frequency per-session messages (terminal_output,
8683
+ // user_message). Sending to every connected client for every PTY output
8684
+ // chunk made broadcast() cost scale with connections x active sessions;
8685
+ // this bounds it to only that session's subscribers.
8686
+ broadcastToClients(clients, message) {
8687
+ const data = JSON.stringify(message);
8688
+ for (const client of clients) {
8689
+ try {
8690
+ if (client.readyState === client.OPEN) {
8691
+ client.send(data);
8692
+ } else {
8693
+ this.clients.delete(client);
8694
+ }
8695
+ } catch {
8696
+ this.clients.delete(client);
8697
+ }
8698
+ }
8699
+ }
8668
8700
  unicast(ws, message) {
8669
8701
  try {
8670
8702
  if (ws.readyState === ws.OPEN) {
@@ -8727,6 +8759,7 @@ var ADOPT_KILL_TIMEOUT_MS = 5e3;
8727
8759
  var ADOPT_KILL_POLL_MS = 100;
8728
8760
  var REFRESH_TTL_MS = 2e3;
8729
8761
  var START_READY_TIMEOUT_MS = 1e4;
8762
+ var MODEL_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
8730
8763
  var EXTERNAL_TAIL_RECENCY_MS = RESUME_BUSY_WINDOW_MS;
8731
8764
  var EXTERNAL_TAIL_MAX = 32;
8732
8765
  var EXTERNAL_TAIL_IDLE_MS = 3e5;
@@ -8830,6 +8863,8 @@ var StreamerServer = class {
8830
8863
  dbPool = null;
8831
8864
  dbInstanceId = null;
8832
8865
  disableDb = false;
8866
+ // Skip the startup warm-up scan (test hook; see ServerConfig.skipStartupWarmup).
8867
+ skipStartupWarmup;
8833
8868
  browseRoot = null;
8834
8869
  publicUrl = null;
8835
8870
  browserCors;
@@ -8869,6 +8904,11 @@ var StreamerServer = class {
8869
8904
  // every provider; read only by the idle reaper. Entries are dropped when the
8870
8905
  // session leaves the runner (reap/exit/hold).
8871
8906
  lastAgentChunkAt = /* @__PURE__ */ new Map();
8907
+ // sessionId → last terminal_output seq broadcast (starts at 1, per session).
8908
+ // Stamped on every terminal_output/terminal_replay so a client can detect a
8909
+ // stale chunk delivered after a reconnect race instead of trusting raw WS
8910
+ // arrival order. Entries dropped alongside lastAgentChunkAt.
8911
+ terminalSeq = /* @__PURE__ */ new Map();
8872
8912
  // Recently accepted input idempotency keys (C4). A retried POST replays its
8873
8913
  // original outcome instead of submitting the prompt to the agent twice.
8874
8914
  idempotency = new IdempotencyStore();
@@ -8936,6 +8976,7 @@ var StreamerServer = class {
8936
8976
  }
8937
8977
  this.verbose = config.verbose ?? false;
8938
8978
  this.disableDb = config.disableDb ?? false;
8979
+ this.skipStartupWarmup = config.skipStartupWarmup ?? false;
8939
8980
  this.scannerPersistenceDisabled = config.scannerPersistent === false;
8940
8981
  this.scanProfiles = config.scanProfiles;
8941
8982
  this.codexRoots = config.codexRoots ?? [join18(homedir9(), ".codex", "sessions")];
@@ -9084,10 +9125,22 @@ var StreamerServer = class {
9084
9125
  logger: getLogger("pty"),
9085
9126
  onOutput: (sessionId, data) => {
9086
9127
  this.lastAgentChunkAt.set(sessionId, Date.now());
9087
- this.wsHub.broadcast({ type: "terminal_output", sessionId, data });
9128
+ const seq = (this.terminalSeq.get(sessionId) ?? 0) + 1;
9129
+ this.terminalSeq.set(sessionId, seq);
9130
+ this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], {
9131
+ type: "terminal_output",
9132
+ sessionId,
9133
+ data,
9134
+ seq
9135
+ });
9088
9136
  },
9089
9137
  onUserMessage: (sessionId, text, ts) => {
9090
- this.wsHub.broadcast({ type: "user_message", sessionId, text, ts });
9138
+ this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], {
9139
+ type: "user_message",
9140
+ sessionId,
9141
+ text,
9142
+ ts
9143
+ });
9091
9144
  },
9092
9145
  onPermissionChange: (sessionId, gate) => {
9093
9146
  this.handlePermissionChange(sessionId, gate);
@@ -9224,6 +9277,8 @@ var StreamerServer = class {
9224
9277
  handleCancel: (id, res) => this.handleCancel(id, res),
9225
9278
  handleStopSession: (id, res) => this.handleStopSession(id, res),
9226
9279
  handleSetSessionName: (id, req, res) => this.handleSetSessionName(id, req, res),
9280
+ handleSetSessionModel: (id, req, res) => this.applyLiveSessionSetting(id, req, res, "model"),
9281
+ handleSetSessionEffort: (id, req, res) => this.applyLiveSessionSetting(id, req, res, "effort"),
9227
9282
  handleUploadFile: (id, req, res) => this.handleUploadFile(id, req, res),
9228
9283
  handleAdopt: (id, res) => this.handleAdopt(id, res),
9229
9284
  handleResume: (req, res) => this.handleResume(req, res),
@@ -9270,7 +9325,8 @@ var StreamerServer = class {
9270
9325
  type: "terminal_replay",
9271
9326
  sessionId: msg.sessionId,
9272
9327
  lines,
9273
- userMessages
9328
+ userMessages,
9329
+ seq: this.terminalSeq.get(msg.sessionId)
9274
9330
  })
9275
9331
  );
9276
9332
  }
@@ -9630,6 +9686,7 @@ var StreamerServer = class {
9630
9686
  );
9631
9687
  this.ptyManager.putOnHold(session.id);
9632
9688
  this.lastAgentChunkAt.delete(session.id);
9689
+ this.terminalSeq.delete(session.id);
9633
9690
  this.idempotency.clear(session.id);
9634
9691
  this.sessionSubscribers.delete(session.id);
9635
9692
  reaped.push(session.id);
@@ -9804,6 +9861,14 @@ var StreamerServer = class {
9804
9861
  );
9805
9862
  this.scannerPersistenceDisabled = true;
9806
9863
  }
9864
+ if (this.skipStartupWarmup) {
9865
+ this.log.debug?.("startup warm-up scan skipped (skipStartupWarmup)", {
9866
+ event: "cache.warmup_skipped"
9867
+ });
9868
+ this.finishWarmup(0);
9869
+ resolveWarm();
9870
+ return;
9871
+ }
9807
9872
  const warmupStatCache = this.buildStatCache(null);
9808
9873
  const warmupScanner = this.newScanner(warmupStatCache ? { persistent: false } : void 0);
9809
9874
  this.allScanners.add(warmupScanner);
@@ -9995,6 +10060,7 @@ var StreamerServer = class {
9995
10060
  this.idleReaperTimer = null;
9996
10061
  }
9997
10062
  this.lastAgentChunkAt.clear();
10063
+ this.terminalSeq.clear();
9998
10064
  this.recordShutdownState();
9999
10065
  this.markScannerStaleDebounced.cancel();
10000
10066
  await Promise.all([...this.inFlightCacheWrites]);
@@ -10176,6 +10242,30 @@ var StreamerServer = class {
10176
10242
  persisted: this.claudeFlagsPersistable
10177
10243
  };
10178
10244
  }
10245
+ /**
10246
+ * The three spawn options that a configured claude-flag can override, with
10247
+ * the boot-time CLI/yaml default as the fallback. Spread into every
10248
+ * start/resume/adopt call so all three paths agree.
10249
+ *
10250
+ * These ids are excluded from buildFlagArgs (SPAWN_POSITIONAL_FLAG_IDS)
10251
+ * precisely because they arrive here instead — the PTY spawn paths pass them
10252
+ * as explicit positionals, so emitting them from the allowlist too would
10253
+ * duplicate the flag.
10254
+ *
10255
+ * Narrowed with the type guards rather than cast: ClaudeFlagValues is a loose
10256
+ * Record by design, and while validateFlagValues already guarantees the shape
10257
+ * on the way in, TypeScript cannot see that through the record.
10258
+ */
10259
+ spawnFlagOverrides() {
10260
+ const mode = this.claudeFlags.permissionMode;
10261
+ const model = this.claudeFlags.model;
10262
+ const effort = this.claudeFlags.effort;
10263
+ return {
10264
+ permissionMode: isPermissionMode(mode) ? mode : this.defaultPermissionMode,
10265
+ model: typeof model === "string" ? model : this.defaultModel,
10266
+ effort: isEffortLevel(effort) ? effort : this.defaultEffort
10267
+ };
10268
+ }
10179
10269
  checkRateLimit(map, key, limit, windowMs) {
10180
10270
  const now = Date.now();
10181
10271
  const arr = (map.get(key) ?? []).filter((t) => now - t < windowMs);
@@ -11325,11 +11415,9 @@ var StreamerServer = class {
11325
11415
  projectPath,
11326
11416
  projectName: body.projectName,
11327
11417
  branch: body.branch,
11328
- permissionMode: this.defaultPermissionMode,
11329
11418
  claudeFlags: this.claudeFlags,
11330
11419
  claudeExtraArgs: this.claudeExtraArgs,
11331
- model: this.defaultModel,
11332
- effort: this.defaultEffort
11420
+ ...this.spawnFlagOverrides()
11333
11421
  });
11334
11422
  this.sessionStore.addManaged(session);
11335
11423
  this.recordSessionSpawn(session);
@@ -11776,11 +11864,9 @@ var StreamerServer = class {
11776
11864
  projectPath,
11777
11865
  projectName,
11778
11866
  branch,
11779
- permissionMode: this.defaultPermissionMode,
11780
11867
  claudeFlags: this.claudeFlags,
11781
11868
  claudeExtraArgs: this.claudeExtraArgs,
11782
- model: this.defaultModel,
11783
- effort: this.defaultEffort
11869
+ ...this.spawnFlagOverrides()
11784
11870
  });
11785
11871
  this.sessionStore.addManaged(session);
11786
11872
  this.recordSessionSpawn(session);
@@ -11853,11 +11939,9 @@ var StreamerServer = class {
11853
11939
  projectPath: resolvedPath,
11854
11940
  projectName: body.projectName,
11855
11941
  ...includeSystemPrompt && { systemPrompt: systemPromptParts.join("\n") },
11856
- permissionMode: this.defaultPermissionMode,
11857
11942
  claudeFlags: this.claudeFlags,
11858
11943
  claudeExtraArgs: this.claudeExtraArgs,
11859
- model: this.defaultModel,
11860
- effort: this.defaultEffort
11944
+ ...this.spawnFlagOverrides()
11861
11945
  });
11862
11946
  this.sessionStore.addManaged(session);
11863
11947
  this.recordSessionSpawn(session);
@@ -12217,6 +12301,87 @@ var StreamerServer = class {
12217
12301
  this.cache.upsertSessionName(sessionId, name);
12218
12302
  json(res, 200, { ok: true });
12219
12303
  }
12304
+ /**
12305
+ * Retarget a LIVE session's model or effort by typing the corresponding
12306
+ * Claude Code slash command into its PTY.
12307
+ *
12308
+ * There is no CLI or IPC channel for this — `--model`/`--effort` are spawn
12309
+ * arguments — so the interactive `/model <x>` / `/effort <y>` commands are the
12310
+ * only way to change a session already running. Both accept an argument and
12311
+ * apply it without opening the picker (verified against Claude Code v2.1.220).
12312
+ *
12313
+ * Answers 202, not 200: the value is applied by the TUI on its next render, so
12314
+ * there is nothing truthful to echo back synchronously. Clients confirm with
12315
+ * `GET /api/sessions/:id`, which scrapes the applied value off the live status
12316
+ * line.
12317
+ */
12318
+ async applyLiveSessionSetting(sessionId, req, res, setting) {
12319
+ const session = this.ptyManager.getSession(sessionId);
12320
+ if (!session) {
12321
+ const known = this.sessionStore.getManaged(sessionId);
12322
+ if (known) {
12323
+ json(res, 409, {
12324
+ error: "Session has no live PTY; resume it first",
12325
+ code: "SESSION_IDLE"
12326
+ });
12327
+ return;
12328
+ }
12329
+ json(res, 404, { error: "Session not found" });
12330
+ return;
12331
+ }
12332
+ if ((session.provider ?? CLAUDE_CODE_PROVIDER) !== CLAUDE_CODE_PROVIDER) {
12333
+ json(res, 501, {
12334
+ error: `Setting ${setting} on a ${session.provider} session is not supported`,
12335
+ code: "UNSUPPORTED_PROVIDER"
12336
+ });
12337
+ return;
12338
+ }
12339
+ if (session.status === "running") {
12340
+ json(res, 409, {
12341
+ error: "Session is mid-turn; retry once it is waiting for input",
12342
+ code: "SESSION_BUSY"
12343
+ });
12344
+ return;
12345
+ }
12346
+ let parsed;
12347
+ try {
12348
+ parsed = await readBody(req);
12349
+ } catch {
12350
+ json(res, 400, { error: "Invalid JSON" });
12351
+ return;
12352
+ }
12353
+ let value;
12354
+ if (setting === "effort") {
12355
+ if (!isEffortLevel(parsed.effort)) {
12356
+ json(res, 400, {
12357
+ error: `effort must be one of ${EFFORT_LEVELS.join(", ")}`
12358
+ });
12359
+ return;
12360
+ }
12361
+ value = parsed.effort;
12362
+ } else {
12363
+ if (typeof parsed.model !== "string" || !MODEL_NAME_RE.test(parsed.model)) {
12364
+ json(res, 400, {
12365
+ error: "model must be an alias or full model name (letters, digits, dot, dash, underscore)"
12366
+ });
12367
+ return;
12368
+ }
12369
+ value = parsed.model;
12370
+ }
12371
+ try {
12372
+ this.ptyManager.sendKeys(sessionId, `/${setting} ${value}\r`);
12373
+ } catch (err) {
12374
+ json(res, 400, { error: err instanceof Error ? err.message : "Failed to write to session" });
12375
+ return;
12376
+ }
12377
+ this.log.info(`Live session ${setting} set to ${value}`, {
12378
+ event: "session.setting_applied",
12379
+ sessionId,
12380
+ setting,
12381
+ value
12382
+ });
12383
+ json(res, 202, { id: sessionId, [setting]: value });
12384
+ }
12220
12385
  handleGetSessionNames(res) {
12221
12386
  if (!this.cache) {
12222
12387
  json(res, 200, {});