@threadbase-sh/streamer 1.30.0 → 1.31.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
@@ -168,6 +168,10 @@ interface PermissionOption {
168
168
  label: string;
169
169
  answerKeys?: string;
170
170
  }
171
+ interface UserMessage {
172
+ text: string;
173
+ ts: number;
174
+ }
171
175
  type WSMessage = {
172
176
  type: "terminal_output";
173
177
  sessionId: string;
@@ -214,10 +218,16 @@ type WSMessage = {
214
218
  } | {
215
219
  type: "ping";
216
220
  ts: number;
221
+ } | {
222
+ type: "user_message";
223
+ sessionId: string;
224
+ text: string;
225
+ ts: number;
217
226
  } | {
218
227
  type: "terminal_replay";
219
228
  sessionId: string;
220
229
  lines: string[];
230
+ userMessages?: UserMessage[];
221
231
  } | {
222
232
  type: "session_ready";
223
233
  session: SessionResponse;
@@ -321,7 +331,6 @@ interface ServerConfig {
321
331
  codexRoots?: string[];
322
332
  scannerPersistent?: boolean;
323
333
  ptyGracePeriodMs?: number;
324
- wsAuthTimeoutMs?: number;
325
334
  cacheDir?: string;
326
335
  tailSize?: number;
327
336
  directoryScanDebounceMs?: number;
@@ -340,6 +349,7 @@ interface PTYManagerOptions {
340
349
  } | null) => void;
341
350
  onLiveQuestion?: (sessionId: string, questions: AskQuestion[]) => void;
342
351
  onLiveQuestionGone?: (sessionId: string) => void;
352
+ onUserMessage?: (sessionId: string, text: string, ts: number) => void;
343
353
  logger?: Logger;
344
354
  }
345
355
  interface StartSessionOptions {
@@ -364,6 +374,7 @@ interface SessionRunner {
364
374
  putOnHold(sessionId: string): void;
365
375
  getOutput(sessionId: string): string;
366
376
  getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
377
+ getInputHistory(sessionId: string): UserMessage[];
367
378
  getSession(sessionId: string): ManagedSession | null;
368
379
  hasSession(sessionId: string): boolean;
369
380
  listSessions(): ManagedSession[];
@@ -794,6 +805,7 @@ declare class LiveSessionManager {
794
805
  putOnHold(sessionId: string): void;
795
806
  getOutput(sessionId: string): string;
796
807
  getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
808
+ getInputHistory(sessionId: string): UserMessage[];
797
809
  getSession(sessionId: string): ManagedSession | null;
798
810
  hasSession(sessionId: string): boolean;
799
811
  listSessions(): ManagedSession[];
@@ -860,7 +872,7 @@ type ApiDeps = {
860
872
  handlePairExchange: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
861
873
  handleBrowse: (url: URL, res: ServerResponse) => Promise<void>;
862
874
  handleMkdir: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
863
- handleWsOpen: (ws: WebSocket, preAuthed: boolean) => void;
875
+ handleWsOpen: (ws: WebSocket) => void;
864
876
  handleWsMessage: (ws: WebSocket, raw: unknown) => void;
865
877
  handleWsClose: (ws: WebSocket) => void;
866
878
  agentClient: AgentClient | null;
@@ -938,6 +950,7 @@ declare class PTYManager implements SessionRunner {
938
950
  private onPermissionChange;
939
951
  private onLiveQuestion;
940
952
  private onLiveQuestionGone;
953
+ private onUserMessage;
941
954
  private permissionOpen;
942
955
  private lastScreenQuestionKey;
943
956
  private shellPromptOpen;
@@ -962,6 +975,8 @@ declare class PTYManager implements SessionRunner {
962
975
  putOnHold(sessionId: string): void;
963
976
  getOutput(sessionId: string): string;
964
977
  getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
978
+ getInputHistory(sessionId: string): UserMessage[];
979
+ private recordUserMessage;
965
980
  getSession(sessionId: string): ManagedSession | null;
966
981
  hasSession(sessionId: string): boolean;
967
982
  listSessions(): ManagedSession[];
@@ -1016,9 +1031,6 @@ declare class StreamerServer {
1016
1031
  private sessionSubscribers;
1017
1032
  private clientIdToWs;
1018
1033
  private wsToClientId;
1019
- private wsAuthed;
1020
- private wsAuthPending;
1021
- private wsAuthTimeoutMs;
1022
1034
  private cache;
1023
1035
  private projectsRepo;
1024
1036
  private conversationsRepo;
@@ -1047,7 +1059,6 @@ declare class StreamerServer {
1047
1059
  * a full broadcast if no match exists (old clients, or no WS registered yet).
1048
1060
  */
1049
1061
  private broadcastOrUnicastSessionList;
1050
- private completeWsAuth;
1051
1062
  private addSessionSubscriber;
1052
1063
  private startGraceTimer;
1053
1064
  get port(): number;
@@ -1197,4 +1208,4 @@ declare class ConversationWatcher {
1197
1208
  private readNewLines;
1198
1209
  }
1199
1210
 
1200
- export { type AgentClient, type AgentClientOpts, type AgentConfig, type AppendArgs, type AskOption, type AskQuestion, CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, LiveSessionManager, type ManagedSession, PTYManager, type PTYManagerOptions, type PermissionOption, type ProgressDedupeLRU, type ProviderName, type ServerConfig, type SessionCursor, type SessionListPage, type SessionListQuery, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartFreshSessionOptions, type StartSessionOptions, StreamerServer, WSHub, type WSMessage, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
1211
+ export { type AgentClient, type AgentClientOpts, type AgentConfig, type AppendArgs, type AskOption, type AskQuestion, CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, LiveSessionManager, type ManagedSession, PTYManager, type PTYManagerOptions, type PermissionOption, type ProgressDedupeLRU, type ProviderName, type ServerConfig, type SessionCursor, type SessionListPage, type SessionListQuery, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartFreshSessionOptions, type StartSessionOptions, StreamerServer, type UserMessage, WSHub, type WSMessage, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
package/dist/index.d.ts CHANGED
@@ -168,6 +168,10 @@ interface PermissionOption {
168
168
  label: string;
169
169
  answerKeys?: string;
170
170
  }
171
+ interface UserMessage {
172
+ text: string;
173
+ ts: number;
174
+ }
171
175
  type WSMessage = {
172
176
  type: "terminal_output";
173
177
  sessionId: string;
@@ -214,10 +218,16 @@ type WSMessage = {
214
218
  } | {
215
219
  type: "ping";
216
220
  ts: number;
221
+ } | {
222
+ type: "user_message";
223
+ sessionId: string;
224
+ text: string;
225
+ ts: number;
217
226
  } | {
218
227
  type: "terminal_replay";
219
228
  sessionId: string;
220
229
  lines: string[];
230
+ userMessages?: UserMessage[];
221
231
  } | {
222
232
  type: "session_ready";
223
233
  session: SessionResponse;
@@ -321,7 +331,6 @@ interface ServerConfig {
321
331
  codexRoots?: string[];
322
332
  scannerPersistent?: boolean;
323
333
  ptyGracePeriodMs?: number;
324
- wsAuthTimeoutMs?: number;
325
334
  cacheDir?: string;
326
335
  tailSize?: number;
327
336
  directoryScanDebounceMs?: number;
@@ -340,6 +349,7 @@ interface PTYManagerOptions {
340
349
  } | null) => void;
341
350
  onLiveQuestion?: (sessionId: string, questions: AskQuestion[]) => void;
342
351
  onLiveQuestionGone?: (sessionId: string) => void;
352
+ onUserMessage?: (sessionId: string, text: string, ts: number) => void;
343
353
  logger?: Logger;
344
354
  }
345
355
  interface StartSessionOptions {
@@ -364,6 +374,7 @@ interface SessionRunner {
364
374
  putOnHold(sessionId: string): void;
365
375
  getOutput(sessionId: string): string;
366
376
  getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
377
+ getInputHistory(sessionId: string): UserMessage[];
367
378
  getSession(sessionId: string): ManagedSession | null;
368
379
  hasSession(sessionId: string): boolean;
369
380
  listSessions(): ManagedSession[];
@@ -794,6 +805,7 @@ declare class LiveSessionManager {
794
805
  putOnHold(sessionId: string): void;
795
806
  getOutput(sessionId: string): string;
796
807
  getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
808
+ getInputHistory(sessionId: string): UserMessage[];
797
809
  getSession(sessionId: string): ManagedSession | null;
798
810
  hasSession(sessionId: string): boolean;
799
811
  listSessions(): ManagedSession[];
@@ -860,7 +872,7 @@ type ApiDeps = {
860
872
  handlePairExchange: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
861
873
  handleBrowse: (url: URL, res: ServerResponse) => Promise<void>;
862
874
  handleMkdir: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
863
- handleWsOpen: (ws: WebSocket, preAuthed: boolean) => void;
875
+ handleWsOpen: (ws: WebSocket) => void;
864
876
  handleWsMessage: (ws: WebSocket, raw: unknown) => void;
865
877
  handleWsClose: (ws: WebSocket) => void;
866
878
  agentClient: AgentClient | null;
@@ -938,6 +950,7 @@ declare class PTYManager implements SessionRunner {
938
950
  private onPermissionChange;
939
951
  private onLiveQuestion;
940
952
  private onLiveQuestionGone;
953
+ private onUserMessage;
941
954
  private permissionOpen;
942
955
  private lastScreenQuestionKey;
943
956
  private shellPromptOpen;
@@ -962,6 +975,8 @@ declare class PTYManager implements SessionRunner {
962
975
  putOnHold(sessionId: string): void;
963
976
  getOutput(sessionId: string): string;
964
977
  getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
978
+ getInputHistory(sessionId: string): UserMessage[];
979
+ private recordUserMessage;
965
980
  getSession(sessionId: string): ManagedSession | null;
966
981
  hasSession(sessionId: string): boolean;
967
982
  listSessions(): ManagedSession[];
@@ -1016,9 +1031,6 @@ declare class StreamerServer {
1016
1031
  private sessionSubscribers;
1017
1032
  private clientIdToWs;
1018
1033
  private wsToClientId;
1019
- private wsAuthed;
1020
- private wsAuthPending;
1021
- private wsAuthTimeoutMs;
1022
1034
  private cache;
1023
1035
  private projectsRepo;
1024
1036
  private conversationsRepo;
@@ -1047,7 +1059,6 @@ declare class StreamerServer {
1047
1059
  * a full broadcast if no match exists (old clients, or no WS registered yet).
1048
1060
  */
1049
1061
  private broadcastOrUnicastSessionList;
1050
- private completeWsAuth;
1051
1062
  private addSessionSubscriber;
1052
1063
  private startGraceTimer;
1053
1064
  get port(): number;
@@ -1197,4 +1208,4 @@ declare class ConversationWatcher {
1197
1208
  private readNewLines;
1198
1209
  }
1199
1210
 
1200
- export { type AgentClient, type AgentClientOpts, type AgentConfig, type AppendArgs, type AskOption, type AskQuestion, CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, LiveSessionManager, type ManagedSession, PTYManager, type PTYManagerOptions, type PermissionOption, type ProgressDedupeLRU, type ProviderName, type ServerConfig, type SessionCursor, type SessionListPage, type SessionListQuery, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartFreshSessionOptions, type StartSessionOptions, StreamerServer, WSHub, type WSMessage, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
1211
+ export { type AgentClient, type AgentClientOpts, type AgentConfig, type AppendArgs, type AskOption, type AskQuestion, CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, LiveSessionManager, type ManagedSession, PTYManager, type PTYManagerOptions, type PermissionOption, type ProgressDedupeLRU, type ProviderName, type ServerConfig, type SessionCursor, type SessionListPage, type SessionListQuery, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartFreshSessionOptions, type StartSessionOptions, StreamerServer, type UserMessage, WSHub, type WSMessage, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
package/dist/index.js CHANGED
@@ -164,7 +164,7 @@ function verifySignature(rawBody, signature, secret) {
164
164
  }
165
165
  }
166
166
  function isWithinSkew(timestampHeader, skewSeconds) {
167
- if (!timestampHeader) return false;
167
+ if (!timestampHeader) return true;
168
168
  const t = Number(timestampHeader);
169
169
  if (!Number.isFinite(t)) return false;
170
170
  const now = Math.floor(Date.now() / 1e3);
@@ -745,6 +745,7 @@ function debounce(fn, waitMs) {
745
745
 
746
746
  // src/codex-pty-runner.ts
747
747
  var OUTPUT_BUFFER_MAX = 65536;
748
+ var INPUT_HISTORY_MAX = 50;
748
749
  var PTY_COLS = 120;
749
750
  var PTY_ROWS = 40;
750
751
  var SCREEN_SCROLLBACK = 1e3;
@@ -825,6 +826,7 @@ var CodexPtyRunner = class {
825
826
  onPermissionChange;
826
827
  onLiveQuestion;
827
828
  onLiveQuestionGone;
829
+ onUserMessage;
828
830
  log;
829
831
  // Tracks sessions whose PTY has spawned but Codex hasn't yet reached its
830
832
  // "Ready" status bar — i.e. onReady hasn't fired.
@@ -855,6 +857,7 @@ var CodexPtyRunner = class {
855
857
  this.onPermissionChange = options.onPermissionChange;
856
858
  this.onLiveQuestion = options.onLiveQuestion;
857
859
  this.onLiveQuestionGone = options.onLiveQuestionGone;
860
+ this.onUserMessage = options.onUserMessage;
858
861
  this.log = options.logger ?? getLogger("codex-pty");
859
862
  }
860
863
  // Resume an existing Codex session. sessionId is the Codex-persisted
@@ -898,7 +901,8 @@ var CodexPtyRunner = class {
898
901
  lastOutput: "",
899
902
  process: proc,
900
903
  outputBuffer: Buffer.alloc(0),
901
- screen: createScreen()
904
+ screen: createScreen(),
905
+ inputHistory: []
902
906
  };
903
907
  this.sessions.set(sessionId, session);
904
908
  this.pendingReady.add(sessionId);
@@ -944,7 +948,8 @@ var CodexPtyRunner = class {
944
948
  lastOutput: "",
945
949
  process: proc,
946
950
  outputBuffer: Buffer.alloc(0),
947
- screen: createScreen()
951
+ screen: createScreen(),
952
+ inputHistory: []
948
953
  };
949
954
  this.sessions.set(sessionId, session);
950
955
  this.pendingReady.add(sessionId);
@@ -1062,6 +1067,7 @@ var CodexPtyRunner = class {
1062
1067
  // confirmed Codex accepts plain keystrokes), then submit \r after a short
1063
1068
  // delay so Codex's TUI gets an event-loop tick to process the input first.
1064
1069
  writeSubmit(sessionId, session, input, path, promptCount) {
1070
+ this.recordUserMessage(session, input);
1065
1071
  this.log.info(
1066
1072
  `[codex.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${input.length} digest=${digestBytes(input)}`,
1067
1073
  {
@@ -1191,6 +1197,19 @@ var CodexPtyRunner = class {
1191
1197
  }
1192
1198
  return lines.slice(-maxLines);
1193
1199
  }
1200
+ getInputHistory(sessionId) {
1201
+ return this.sessions.get(sessionId)?.inputHistory ?? [];
1202
+ }
1203
+ // Record a submitted user message as ground truth and fire onUserMessage.
1204
+ // Called from writeSubmit (direct and flush paths) — never from sendKeys.
1205
+ recordUserMessage(session, text) {
1206
+ const ts = Date.now();
1207
+ session.inputHistory.push({ text, ts });
1208
+ if (session.inputHistory.length > INPUT_HISTORY_MAX) {
1209
+ session.inputHistory.shift();
1210
+ }
1211
+ this.onUserMessage?.(session.id, text, ts);
1212
+ }
1194
1213
  getSession(sessionId) {
1195
1214
  const session = this.sessions.get(sessionId);
1196
1215
  return session ? toPublicSession(session) : null;
@@ -1568,6 +1587,7 @@ function detectShellPrompt(lines) {
1568
1587
 
1569
1588
  // src/pty-manager.ts
1570
1589
  var OUTPUT_BUFFER_MAX2 = 65536;
1590
+ var INPUT_HISTORY_MAX2 = 50;
1571
1591
  var PTY_COLS2 = 120;
1572
1592
  var PTY_ROWS2 = 40;
1573
1593
  var SCREEN_SCROLLBACK2 = 1e3;
@@ -1626,6 +1646,7 @@ var PTYManager = class {
1626
1646
  onPermissionChange;
1627
1647
  onLiveQuestion;
1628
1648
  onLiveQuestionGone;
1649
+ onUserMessage;
1629
1650
  // Per-session permission-gate state. True between an OSC 777 (gate open) and
1630
1651
  // the next prompt-ready without a fresh 777 (gate closed). Prevents
1631
1652
  // re-broadcasting open/close on every chunk.
@@ -1671,6 +1692,7 @@ var PTYManager = class {
1671
1692
  this.onPermissionChange = options.onPermissionChange;
1672
1693
  this.onLiveQuestion = options.onLiveQuestion;
1673
1694
  this.onLiveQuestionGone = options.onLiveQuestionGone;
1695
+ this.onUserMessage = options.onUserMessage;
1674
1696
  this.log = options.logger ?? getLogger("pty");
1675
1697
  }
1676
1698
  // Resume an existing Claude conversation. sessionId is the JSONL UUID.
@@ -1733,7 +1755,8 @@ var PTYManager = class {
1733
1755
  lastOutput: "",
1734
1756
  process: proc,
1735
1757
  outputBuffer: Buffer.alloc(0),
1736
- screen: createScreen2()
1758
+ screen: createScreen2(),
1759
+ inputHistory: []
1737
1760
  };
1738
1761
  this.sessions.set(sessionId, session);
1739
1762
  this.pendingReady.add(sessionId);
@@ -1784,7 +1807,8 @@ var PTYManager = class {
1784
1807
  lastOutput: "",
1785
1808
  process: proc,
1786
1809
  outputBuffer: Buffer.alloc(0),
1787
- screen: createScreen2()
1810
+ screen: createScreen2(),
1811
+ inputHistory: []
1788
1812
  };
1789
1813
  this.sessions.set(sessionId, session);
1790
1814
  this.pendingReady.add(sessionId);
@@ -1864,6 +1888,7 @@ var PTYManager = class {
1864
1888
  // step gives the TUI as many extra ticks as it needs, capped at
1865
1889
  // SUBMIT_MAX_WAIT_MS so a silent/wedged PTY still gets its \r eventually.
1866
1890
  writeSubmit(sessionId, session, input, path, promptCount) {
1891
+ this.recordUserMessage(session, input);
1867
1892
  const pasteBytes = buildPasteBytes(input);
1868
1893
  this.log.info(
1869
1894
  `[pty.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${pasteBytes.length} digest=${digestBytes2(pasteBytes)}`,
@@ -1994,6 +2019,20 @@ var PTYManager = class {
1994
2019
  }
1995
2020
  return lines.slice(-maxLines);
1996
2021
  }
2022
+ getInputHistory(sessionId) {
2023
+ return this.sessions.get(sessionId)?.inputHistory ?? [];
2024
+ }
2025
+ // Record a submitted user message as ground truth and fire onUserMessage.
2026
+ // Called from writeSubmit (both direct and flush paths) — never from
2027
+ // sendKeys, so raw keystrokes aren't logged as messages.
2028
+ recordUserMessage(session, text) {
2029
+ const ts = Date.now();
2030
+ session.inputHistory.push({ text, ts });
2031
+ if (session.inputHistory.length > INPUT_HISTORY_MAX2) {
2032
+ session.inputHistory.shift();
2033
+ }
2034
+ this.onUserMessage?.(session.id, text, ts);
2035
+ }
1997
2036
  getSession(sessionId) {
1998
2037
  const session = this.sessions.get(sessionId);
1999
2038
  return session ? toPublicSession2(session) : null;
@@ -2290,6 +2329,9 @@ var LiveSessionManager = class {
2290
2329
  getOutputLines(sessionId, maxLines) {
2291
2330
  return this.runnerFor(sessionId).getOutputLines(sessionId, maxLines);
2292
2331
  }
2332
+ getInputHistory(sessionId) {
2333
+ return this.runnerFor(sessionId).getInputHistory(sessionId);
2334
+ }
2293
2335
  getSession(sessionId) {
2294
2336
  for (const runner of this.runners.values()) {
2295
2337
  const session = runner.getSession(sessionId);
@@ -2763,7 +2805,7 @@ function isLocalRequest(remoteAddr) {
2763
2805
  const addr = remoteAddr ?? "";
2764
2806
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
2765
2807
  }
2766
- var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz", "/ws"]);
2808
+ var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz"]);
2767
2809
  var LOCAL_ONLY_PATHS = /* @__PURE__ */ new Set(["/api/logs", "/api/logs/meta"]);
2768
2810
  var PUBLIC_POST_PATHS = /* @__PURE__ */ new Set(["/api/pair/exchange", "/api/__update"]);
2769
2811
  var PUBLIC_POST_PREFIXES = ["/internal/sessions/"];
@@ -3360,16 +3402,14 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
3360
3402
  const app = new Hono11();
3361
3403
  app.get(
3362
3404
  "/ws",
3363
- upgradeWebSocket((c) => {
3364
- const key = c.req.query("key");
3365
- const preAuthed = typeof key === "string" && validateApiKey(key, deps.apiKey);
3405
+ upgradeWebSocket(() => {
3366
3406
  let openWs = null;
3367
3407
  return {
3368
3408
  onOpen(_evt, ws) {
3369
3409
  const raw = ws.raw;
3370
3410
  if (!raw) return;
3371
3411
  openWs = raw;
3372
- deps.handleWsOpen(raw, preAuthed);
3412
+ deps.handleWsOpen(raw);
3373
3413
  },
3374
3414
  onMessage(evt, _ws) {
3375
3415
  if (openWs) deps.handleWsMessage(openWs, evt.data);
@@ -5277,13 +5317,6 @@ function deriveProjectChatTitle(input) {
5277
5317
  return `Untitled \xB7 ${input.id.slice(0, 8)}`;
5278
5318
  }
5279
5319
 
5280
- // src/services/questions/permissionAnswerKeys.ts
5281
- var ANSWER_KEYS_ALLOWLIST = /^(?:\r|[yn]\r|\x03|\d+\r)$/;
5282
- function sanitizeAnswerKeys(keys) {
5283
- if (keys === void 0) return void 0;
5284
- return ANSWER_KEYS_ALLOWLIST.test(keys) ? keys : void 0;
5285
- }
5286
-
5287
5320
  // src/services/questions/detectAskUserQuestion.ts
5288
5321
  function normalizeContent2(raw) {
5289
5322
  if (Array.isArray(raw)) return raw;
@@ -5888,8 +5921,6 @@ var WSHub = class {
5888
5921
  var BROWSE_SYSTEM_PROMPT = (browseRoot) => `You are working within the project boundary: ${browseRoot}. Do not read, write, or execute commands that access files or directories outside this boundary.`;
5889
5922
  var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
5890
5923
  var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
5891
- var DEFAULT_WS_AUTH_TIMEOUT_MS = 5e3;
5892
- var WS_CLOSE_UNAUTHORIZED = 4401;
5893
5924
  var REFRESH_TTL_MS = 2e3;
5894
5925
  var START_READY_TIMEOUT_MS = 1e4;
5895
5926
  function parseIncludeAgentsEnv(raw) {
@@ -5980,14 +6011,6 @@ var StreamerServer = class {
5980
6011
  clientIdToWs = /* @__PURE__ */ new Map();
5981
6012
  // Reverse map for cleanup on close
5982
6013
  wsToClientId = /* @__PURE__ */ new Map();
5983
- // M1 — WS auth. Sockets that have authenticated (via ?key= at upgrade OR a
5984
- // { type: "auth", token } first message). Only authed sockets are added to
5985
- // the hub and receive broadcasts.
5986
- // Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
5987
- wsAuthed = /* @__PURE__ */ new Set();
5988
- // Keyless sockets awaiting their first-message auth handshake → close timer.
5989
- wsAuthPending = /* @__PURE__ */ new Map();
5990
- wsAuthTimeoutMs;
5991
6014
  cache = null;
5992
6015
  projectsRepo = null;
5993
6016
  conversationsRepo = null;
@@ -6027,7 +6050,6 @@ var StreamerServer = class {
6027
6050
  this.scanProfiles = config.scanProfiles;
6028
6051
  this.codexRoots = config.codexRoots ?? [join15(homedir7(), ".codex", "sessions")];
6029
6052
  this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
6030
- this.wsAuthTimeoutMs = config.wsAuthTimeoutMs ?? DEFAULT_WS_AUTH_TIMEOUT_MS;
6031
6053
  this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
6032
6054
  this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
6033
6055
  this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join15(homedir7(), ".threadbase", "cache");
@@ -6156,6 +6178,9 @@ var StreamerServer = class {
6156
6178
  onOutput: (sessionId, data) => {
6157
6179
  this.wsHub.broadcast({ type: "terminal_output", sessionId, data });
6158
6180
  },
6181
+ onUserMessage: (sessionId, text, ts) => {
6182
+ this.wsHub.broadcast({ type: "user_message", sessionId, text, ts });
6183
+ },
6159
6184
  onPermissionChange: (sessionId, gate) => {
6160
6185
  this.handlePermissionChange(sessionId, gate);
6161
6186
  },
@@ -6285,39 +6310,17 @@ var StreamerServer = class {
6285
6310
  handlePairExchange: (req, res) => this.handlePairExchange(req, res),
6286
6311
  handleBrowse: (url, res) => this.handleBrowse(url, res),
6287
6312
  handleMkdir: (req, res) => this.handleMkdir(req, res),
6288
- handleWsOpen: (ws, preAuthed) => {
6289
- if (preAuthed) {
6290
- this.completeWsAuth(ws);
6291
- return;
6313
+ handleWsOpen: (ws) => {
6314
+ this.wsHub.addClient(ws);
6315
+ const sessions = this.sessionStore.list(this.ptyAttachedIds());
6316
+ ws.send(JSON.stringify({ type: "session_list", sessions }));
6317
+ if (this.cacheReady) {
6318
+ ws.send(JSON.stringify({ type: "cache_ready" }));
6292
6319
  }
6293
- const timer = setTimeout(() => {
6294
- this.wsAuthPending.delete(ws);
6295
- try {
6296
- ws.close(WS_CLOSE_UNAUTHORIZED, "auth timeout");
6297
- } catch {
6298
- }
6299
- }, this.wsAuthTimeoutMs);
6300
- this.wsAuthPending.set(ws, timer);
6301
6320
  },
6302
6321
  handleWsMessage: async (ws, raw) => {
6303
6322
  try {
6304
6323
  const msg = JSON.parse(String(raw));
6305
- if (!this.wsAuthed.has(ws)) {
6306
- if (msg.type === "auth" && typeof msg.token === "string") {
6307
- const t = this.wsAuthPending.get(ws);
6308
- if (t) clearTimeout(t);
6309
- this.wsAuthPending.delete(ws);
6310
- if (validateApiKey(msg.token, this.apiKey)) {
6311
- this.completeWsAuth(ws);
6312
- } else {
6313
- try {
6314
- ws.close(WS_CLOSE_UNAUTHORIZED, "unauthorized");
6315
- } catch {
6316
- }
6317
- }
6318
- }
6319
- return;
6320
- }
6321
6324
  if (msg.type === "register" && typeof msg.clientId === "string") {
6322
6325
  const oldClientId = this.wsToClientId.get(ws);
6323
6326
  if (oldClientId) this.clientIdToWs.delete(oldClientId);
@@ -6328,7 +6331,15 @@ var StreamerServer = class {
6328
6331
  this.addSessionSubscriber(msg.sessionId, ws);
6329
6332
  if (this.ptyManager.hasSession(msg.sessionId)) {
6330
6333
  const lines = await this.ptyManager.getOutputLines(msg.sessionId, 200);
6331
- ws.send(JSON.stringify({ type: "terminal_replay", sessionId: msg.sessionId, lines }));
6334
+ const userMessages = this.ptyManager.getInputHistory(msg.sessionId);
6335
+ ws.send(
6336
+ JSON.stringify({
6337
+ type: "terminal_replay",
6338
+ sessionId: msg.sessionId,
6339
+ lines,
6340
+ userMessages
6341
+ })
6342
+ );
6332
6343
  }
6333
6344
  const pendingGate = this.pendingPermission.get(msg.sessionId);
6334
6345
  if (pendingGate) {
@@ -6364,20 +6375,12 @@ var StreamerServer = class {
6364
6375
  }
6365
6376
  }
6366
6377
  if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
6367
- if (this.sessionSubscribers.get(msg.sessionId)?.has(ws)) {
6368
- this.startGraceTimer(msg.sessionId, 0);
6369
- }
6378
+ this.startGraceTimer(msg.sessionId, 0);
6370
6379
  }
6371
6380
  } catch {
6372
6381
  }
6373
6382
  },
6374
6383
  handleWsClose: (ws) => {
6375
- const pendingTimer = this.wsAuthPending.get(ws);
6376
- if (pendingTimer) {
6377
- clearTimeout(pendingTimer);
6378
- this.wsAuthPending.delete(ws);
6379
- }
6380
- this.wsAuthed.delete(ws);
6381
6384
  const clientId = this.wsToClientId.get(ws);
6382
6385
  if (clientId) {
6383
6386
  this.clientIdToWs.delete(clientId);
@@ -6447,20 +6450,6 @@ var StreamerServer = class {
6447
6450
  this.wsHub.broadcast(payload);
6448
6451
  }
6449
6452
  }
6450
- // M1: finalize a WebSocket auth (via ?key= at upgrade or a first-message
6451
- // handshake) — register it with the hub and send the initial snapshot. Only
6452
- // authed sockets reach this, so no unauthenticated client ever receives a
6453
- // broadcast.
6454
- // Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
6455
- completeWsAuth(ws) {
6456
- this.wsAuthed.add(ws);
6457
- this.wsHub.addClient(ws);
6458
- const sessions = this.sessionStore.list(this.ptyAttachedIds());
6459
- ws.send(JSON.stringify({ type: "session_list", sessions }));
6460
- if (this.cacheReady) {
6461
- ws.send(JSON.stringify({ type: "cache_ready" }));
6462
- }
6463
- }
6464
6453
  addSessionSubscriber(sessionId, ws) {
6465
6454
  let subs = this.sessionSubscribers.get(sessionId);
6466
6455
  if (!subs) {
@@ -6747,9 +6736,6 @@ var StreamerServer = class {
6747
6736
  this.ptyManager.dispose();
6748
6737
  this.fileWatcher.dispose();
6749
6738
  this.wsHub.dispose();
6750
- for (const timer of this.wsAuthPending.values()) clearTimeout(timer);
6751
- this.wsAuthPending.clear();
6752
- this.wsAuthed.clear();
6753
6739
  this.pairTokens.dispose();
6754
6740
  if (this.dbPool) {
6755
6741
  await this.dbPool.end();
@@ -7876,10 +7862,6 @@ var StreamerServer = class {
7876
7862
  return;
7877
7863
  }
7878
7864
  this.pendingPermission.set(sessionId, gate);
7879
- const safeOptions = gate.options.map((o) => {
7880
- const answerKeys = sanitizeAnswerKeys(o.answerKeys);
7881
- return answerKeys === void 0 ? { index: o.index, label: o.label } : { ...o, answerKeys };
7882
- });
7883
7865
  const subscriberCount = this.sessionSubscribers.get(sessionId)?.size ?? 0;
7884
7866
  this.log.info(
7885
7867
  `[ws.broadcast_permission] ${sessionId.slice(0, 8)} subscribers=${subscriberCount}`,
@@ -7890,7 +7872,7 @@ var StreamerServer = class {
7890
7872
  sessionId,
7891
7873
  ...gate.prompt ? { prompt: gate.prompt } : {},
7892
7874
  ...gate.detail ? { detail: gate.detail } : {},
7893
- options: safeOptions,
7875
+ options: gate.options,
7894
7876
  ...gate.cursor !== void 0 ? { cursor: gate.cursor } : {}
7895
7877
  });
7896
7878
  }