@threadbase-sh/streamer 1.32.0 → 1.34.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.ts CHANGED
@@ -196,6 +196,12 @@ type WSMessage = {
196
196
  sessionId: string;
197
197
  lines: string[];
198
198
  seqs?: (number | null)[];
199
+ } | {
200
+ type: "conversation_updated";
201
+ conversationId: string;
202
+ messageCount: number;
203
+ lastActivity: string;
204
+ ownership: "external" | "managed";
199
205
  } | {
200
206
  type: "question";
201
207
  sessionId: string;
@@ -252,7 +258,24 @@ type WSMessage = {
252
258
  type: "scan_progress";
253
259
  scanned: number;
254
260
  total: number;
261
+ } | {
262
+ type: "cache_alert";
263
+ fingerprint: string;
264
+ severity: "high" | "low";
265
+ missingCount: number;
266
+ totalRows: number;
267
+ detectedAt: string;
268
+ sample: {
269
+ id: string;
270
+ title?: string;
271
+ }[];
272
+ } | {
273
+ type: "cache_alert_resolved";
274
+ fingerprint: string;
275
+ action: CacheAlertResolveAction;
255
276
  };
277
+ /** The four cache-integrity resolution actions (POST /api/cache/alert/resolve). */
278
+ type CacheAlertResolveAction = "prune_all" | "prune_selected" | "ignore" | "reset_rescan";
256
279
  interface SessionResponse {
257
280
  id: string;
258
281
  conversationId: string;
@@ -284,6 +307,36 @@ interface SessionResponse {
284
307
  resumedFromConversationId?: string;
285
308
  /** See `ManagedSession.boundConversationId` — never repurposes `conversationId`. */
286
309
  boundConversationId?: string;
310
+ /**
311
+ * Who owns the underlying process. Additive — older clients ignore it, and it
312
+ * deliberately does NOT introduce a new `status` value: `VALID_STATUSES`
313
+ * rejects unknown values in `?status=` and the store drops sessions outside
314
+ * the requested set, so a new status string would make these sessions vanish
315
+ * from already-shipped apps.
316
+ * managed — this streamer spawned and holds the PTY
317
+ * external — a process we discovered but do not own (read-only)
318
+ * historical — a cached conversation, no process known
319
+ */
320
+ ownership?: SessionOwnership;
321
+ /**
322
+ * Whether the process is believed to be running. Only ever "alive" when
323
+ * discovery actually saw it; never guessed from file activity.
324
+ */
325
+ processLiveness?: ProcessLiveness;
326
+ /**
327
+ * INFERRED from JSONL writes, never authoritative: "active_writing" means the
328
+ * transcript grew recently, which cannot distinguish a generating agent from
329
+ * one blocked on a permission gate (gates are screen-only). Absent for
330
+ * sessions we own — their `status` is the authoritative signal.
331
+ */
332
+ activity?: SessionActivity;
333
+ }
334
+ type SessionOwnership = "managed" | "external" | "historical";
335
+ type ProcessLiveness = "alive" | "gone" | "unknown";
336
+ interface SessionActivity {
337
+ state: "active_writing" | "quiet";
338
+ lastEventAt: string;
339
+ source: "jsonl";
287
340
  }
288
341
  interface ConversationListResponse {
289
342
  conversations: unknown[];
@@ -336,6 +389,8 @@ interface ServerConfig {
336
389
  directoryScanDebounceMs?: number;
337
390
  defaultSystemPrompt?: string;
338
391
  defaultPermissionMode?: "acceptEdits" | "manual";
392
+ defaultModel?: string;
393
+ defaultEffort?: "low" | "medium" | "high" | "xhigh" | "max";
339
394
  }
340
395
  interface PTYManagerOptions {
341
396
  onOutput?: (sessionId: string, data: string) => void;
@@ -357,12 +412,16 @@ interface StartSessionOptions {
357
412
  projectName?: string;
358
413
  branch?: string;
359
414
  permissionMode?: "acceptEdits" | "manual";
415
+ model?: string;
416
+ effort?: "low" | "medium" | "high" | "xhigh" | "max";
360
417
  }
361
418
  interface StartFreshSessionOptions {
362
419
  projectPath: string;
363
420
  projectName?: string;
364
421
  systemPrompt?: string;
365
422
  permissionMode?: "acceptEdits" | "manual";
423
+ model?: string;
424
+ effort?: "low" | "medium" | "high" | "xhigh" | "max";
366
425
  }
367
426
  interface SessionRunner {
368
427
  start(sessionId: string, options: StartSessionOptions): Promise<ManagedSession>;
@@ -450,6 +509,7 @@ interface ScannerMeta {
450
509
  projectPath?: string;
451
510
  projectName?: string;
452
511
  title?: string;
512
+ sessionName?: string;
453
513
  model?: string;
454
514
  account?: string;
455
515
  gitBranch?: string;
@@ -573,7 +633,8 @@ declare class ConversationCache {
573
633
  * updateFromLine in order: the agent filter short-circuits the whole batch,
574
634
  * project context is backfilled last-wins, message_count increases by the
575
635
  * number of surviving message lines, and last_activity/last_message reflect
576
- * the final message line.
636
+ * the newest message line by timestamp (a monotonic guard keeps them from
637
+ * moving backward when an interleaved writer appends an older line — P0.3).
577
638
  */
578
639
  updateFromLines(filePath: string, rawLines: string[]): void;
579
640
  upsertFromScannerMeta(metas: ScannerMeta[]): string[];
@@ -599,6 +660,12 @@ declare class ConversationCache {
599
660
  stat: FileStatEntry;
600
661
  meta: ConversationMeta;
601
662
  }>;
663
+ /**
664
+ * Conversation id for a JSONL path, or null when no row exists yet. Resolves
665
+ * by file_path (NOT conversationIdForFile) so codex rollout files — named
666
+ * rollout-<ts>-<uuid>.jsonl, whose stem is not the row id — resolve correctly.
667
+ */
668
+ getIdByFilePath(filePath: string): string | null;
602
669
  getMetaById(id: string): ConversationListItem | null;
603
670
  setConversationProjectId(conversationId: string, projectId: string): void;
604
671
  markAsStreamer(id: string): void;
@@ -622,15 +689,15 @@ declare class ConversationCache {
622
689
  /**
623
690
  * Drop the cached row for a file. Two callers with opposite intent:
624
691
  * - a directory-watch "change" event (the file was appended to) — pass
625
- * `skipIfTailed: true`. A cached tail means the row is being actively
626
- * maintained from the file's real content by the live-tail
627
- * (updateFromLines) or warm-up path, fresher than any scanner-derived
628
- * view. Both watchers fire on the same append with no ordering guarantee;
629
- * without this guard the invalidate can land after the tail write and wipe
630
- * the just-cached row, flickering the conversation out of
631
- * /api/conversations on nearly every message (CRITICAL #2). The debounced
632
- * rescan still re-derives metadata, so skipping the eager drop loses
633
- * nothing.
692
+ * `skipIfTailed: true`, which NEVER deletes (upsert-or-leave). A change
693
+ * event fires on every external append; deleting here flickers the
694
+ * conversation out of /api/conversations whether it's a live-tailed row
695
+ * the updateFromLines/warm-up path just wrote (CRITICAL #2; both watchers
696
+ * fire on the same append with no ordering guarantee) OR a refresh-created
697
+ * untailed row (a ?refresh=1 upsert never populates a tail, so the old
698
+ * "delete when untailed" behavior made it vanish on its next append with no
699
+ * client action). The live-tail path owns the row's content and the
700
+ * debounced rescan re-derives metadata, so leaving the row loses nothing.
634
701
  * - a genuine unlink (the file is gone) — leave `skipIfTailed` false so the
635
702
  * row is always removed, otherwise a deleted session ghosts in the cache.
636
703
  */
@@ -672,6 +739,30 @@ declare class ConversationCache {
672
739
  reconcileDeletions(livePaths: Set<string>, opts?: {
673
740
  exists?: (filePath: string) => boolean;
674
741
  }): string[];
742
+ /**
743
+ * Read-only: list cached rows whose `file_path` no longer exists on disk.
744
+ * Unlike pruneGhostFiles/reconcileDeletions this mutates nothing — it just
745
+ * reports drift for the CacheIntegrityMonitor to classify. `tailed` flags
746
+ * rows that still have cached history (which pruneGhostFiles would keep).
747
+ */
748
+ listMissingFiles(exists?: (filePath: string) => boolean): {
749
+ id: string;
750
+ filePath: string;
751
+ title: string | null;
752
+ tailed: boolean;
753
+ }[];
754
+ /**
755
+ * Drop the given conversation ids outright — main row, tail, and message
756
+ * index — regardless of whether they have a tail. Used by the cache-integrity
757
+ * resolution actions (prune_all / prune_selected). Returns the count dropped.
758
+ */
759
+ dropRowsById(ids: string[]): number;
760
+ /**
761
+ * Wipe all cached conversation state — meta, tails, and message index — and
762
+ * reset the in-memory file index. Only called by the `reset_rescan`
763
+ * resolution action, which repopulates from a fresh disk scan afterward.
764
+ */
765
+ clearAll(): void;
675
766
  }
676
767
 
677
768
  type CacheMetadataKey = "last_conversation_id" | "last_conversation_created_at" | "projects_last_indexed_at" | "conversations_last_indexed_at" | "conversations_dirty";
@@ -826,6 +917,89 @@ declare class WSHub {
826
917
  private startPing;
827
918
  }
828
919
 
920
+ /** One missing conversation the alert covers. */
921
+ interface MissingEntry {
922
+ id: string;
923
+ filePath: string;
924
+ title: string | null;
925
+ tailed: boolean;
926
+ }
927
+ interface PendingAlert {
928
+ fingerprint: string;
929
+ severity: "high" | "low";
930
+ detectedAt: string;
931
+ missingCount: number;
932
+ totalRows: number;
933
+ backupPath?: string;
934
+ /** Capped at 1000 entries in the persisted file. */
935
+ missing: MissingEntry[];
936
+ }
937
+
938
+ type ResolveAction = CacheAlertResolveAction;
939
+ /** The `cache_alert` WS variant, narrowed from the WSMessage union. */
940
+ type CacheAlertWsMessage = Extract<WSMessage, {
941
+ type: "cache_alert";
942
+ }>;
943
+ type ResolveResult = {
944
+ ok: true;
945
+ action: ResolveAction;
946
+ pruned?: number;
947
+ backupPath?: string;
948
+ } | {
949
+ alreadyResolved: true;
950
+ } | {
951
+ conflict: true;
952
+ currentFingerprint: string;
953
+ };
954
+ declare class CacheIntegrityMonitor {
955
+ private readonly cache;
956
+ private readonly wsHub;
957
+ private readonly log;
958
+ private readonly cacheDir;
959
+ private readonly rescan?;
960
+ private _pending;
961
+ private ignoredIds;
962
+ private deferredUnlinks;
963
+ private unlinkTimes;
964
+ constructor(cache: ConversationCache, wsHub: WSHub, log: Logger, cacheDir: string, rescan?: (() => Promise<ScannerMeta[]>) | undefined);
965
+ get pending(): PendingAlert | null;
966
+ private persist;
967
+ private classifySeverity;
968
+ private sampleOf;
969
+ private buildWsMessage;
970
+ wsMessage(): CacheAlertWsMessage | null;
971
+ healthzField(): {
972
+ severity: "high" | "low";
973
+ missingCount: number;
974
+ fingerprint: string;
975
+ detectedAt: string;
976
+ } | undefined;
977
+ /**
978
+ * Scan the cache for rows whose file is gone, excluding ids the user chose to
979
+ * ignore. If none remain, clear any stale pending alert and return (the caller
980
+ * decides whether to run pruneGhostFiles). Otherwise classify severity, persist
981
+ * the pending record, back up on high severity, and broadcast the alert.
982
+ */
983
+ runDetection(detectedAt?: string): Promise<void>;
984
+ /** Queue an unlink while an alert is pending — the row is not invalidated. */
985
+ deferUnlink(filePath: string): void;
986
+ /**
987
+ * Record a live unlink while NO alert is pending. Crossing the storm threshold
988
+ * (>= 10 unlinks within 30s) re-triggers detection.
989
+ */
990
+ recordUnlink(filePath: string): void;
991
+ private ensureBackup;
992
+ private clearPending;
993
+ private applyDeferredUnlinks;
994
+ private broadcastResolved;
995
+ /**
996
+ * Apply the human's chosen resolution. Idempotent per fingerprint: no pending
997
+ * alert → alreadyResolved; a different fingerprint → conflict. See the spec's
998
+ * four-action semantics.
999
+ */
1000
+ resolve(fingerprint: string, action: ResolveAction, ids?: string[]): Promise<ResolveResult>;
1001
+ }
1002
+
829
1003
  type ApiDeps = {
830
1004
  apiKey: string;
831
1005
  localNoAuth: boolean;
@@ -841,6 +1015,7 @@ type ApiDeps = {
841
1015
  sessionStore: SessionStore;
842
1016
  wsHub: WSHub;
843
1017
  cache: () => ConversationCache | null;
1018
+ cacheMonitor: () => CacheIntegrityMonitor | null;
844
1019
  projectsRepo: () => ProjectsRepository | null;
845
1020
  conversationsRepo: () => ConversationsRepository | null;
846
1021
  sessionsRepo: () => SessionsRepository | null;
@@ -984,6 +1159,7 @@ declare class PTYManager implements SessionRunner {
984
1159
  private handleOutput;
985
1160
  private detectLivePrompts;
986
1161
  private handleQuiet;
1162
+ private recheckReadyFromScreen;
987
1163
  private markReady;
988
1164
  private handleExit;
989
1165
  }
@@ -995,8 +1171,11 @@ declare class StreamerServer {
995
1171
  private wsHub;
996
1172
  private fileWatcher;
997
1173
  private sessionFileMap;
1174
+ private externalTails;
998
1175
  private pendingLineSeqs;
999
1176
  private pendingQuestions;
1177
+ private contendedSessions;
1178
+ private selfPtyEndedAt;
1000
1179
  private pendingQuestionKey;
1001
1180
  private pendingPermission;
1002
1181
  private scanner;
@@ -1027,11 +1206,15 @@ declare class StreamerServer {
1027
1206
  private ptyGracePeriodMs;
1028
1207
  private defaultSystemPrompt;
1029
1208
  private defaultPermissionMode;
1209
+ private defaultModel;
1210
+ private defaultEffort;
1030
1211
  private ptyGraceTimers;
1212
+ private ptyGraceDeferCounts;
1031
1213
  private sessionSubscribers;
1032
1214
  private clientIdToWs;
1033
1215
  private wsToClientId;
1034
1216
  private cache;
1217
+ private cacheMonitor;
1035
1218
  private projectsRepo;
1036
1219
  private conversationsRepo;
1037
1220
  private sessionsRepo;
@@ -1053,6 +1236,7 @@ declare class StreamerServer {
1053
1236
  apiKey: string;
1054
1237
  });
1055
1238
  private ptyAttachedIds;
1239
+ private rememberSelfPtyEnded;
1056
1240
  /**
1057
1241
  * Send a session_list to only the client that triggered this HTTP request
1058
1242
  * (identified by X-Client-Id header → registered WS socket). Falls back to
@@ -1118,6 +1302,43 @@ declare class StreamerServer {
1118
1302
  * Claude lines pass through unchanged so seq alignment stays intact.
1119
1303
  */
1120
1304
  private broadcastConversationLines;
1305
+ /** True when a managed (PTY) session owns the tail for this canonical path. */
1306
+ private isManagedTailPath;
1307
+ /**
1308
+ * Attach a live tail to a JSONL nobody is tailing yet, when it was touched
1309
+ * recently enough to look actively written by an external agent. Capped at
1310
+ * EXTERNAL_TAIL_MAX with LRU eviction.
1311
+ */
1312
+ private maybeAttachExternalTail;
1313
+ /** Stop tailing an external file and drop its bookkeeping. */
1314
+ private detachExternalTail;
1315
+ /** Make room for one more tail by evicting the least recently active ones. */
1316
+ private evictExternalTailsIfNeeded;
1317
+ /**
1318
+ * INFERRED activity for an externally-owned conversation, derived purely from
1319
+ * how recently its JSONL grew (the external tail's bookkeeping). Returns
1320
+ * undefined when we hold no tail for it, so a session we know nothing about
1321
+ * reports no activity rather than a fabricated "quiet".
1322
+ *
1323
+ * This can never distinguish a generating agent from one blocked on a
1324
+ * permission gate — gates render on the PTY screen and never reach the JSONL —
1325
+ * which is why it is a separate field and not folded into `status`.
1326
+ */
1327
+ private externalActivityFor;
1328
+ /** Attach inferred `activity` to externally-owned sessions in a response set. */
1329
+ private withExternalActivity;
1330
+ /** Detach external tails idle past EXTERNAL_TAIL_IDLE_MS. */
1331
+ private sweepIdleExternalTails;
1332
+ /**
1333
+ * Push appended lines from an externally-owned conversation. Reuses the exact
1334
+ * conversation_events / conversation_event shapes mobile already consumes,
1335
+ * keyed by the conversation UUID — an external session has no PTY, so it must
1336
+ * never produce terminal_output / terminal_replay / session_ready, and never a
1337
+ * session_update whose session.id is a conversation UUID (that would mint a
1338
+ * phantom session row in the mobile cache). Question cards are likewise never
1339
+ * derived here: with no PTY there is nothing that could deliver an answer.
1340
+ */
1341
+ private broadcastExternalTailLines;
1121
1342
  private findConversationByUuid;
1122
1343
  private isConversationSnapshotStale;
1123
1344
  private handleGetConversation;
@@ -1128,6 +1349,7 @@ declare class StreamerServer {
1128
1349
  private handleResume;
1129
1350
  private enrichResumedSessionAsync;
1130
1351
  private handleSendInput;
1352
+ private processJsonlQuestions;
1131
1353
  private cancelPendingQuestion;
1132
1354
  private handleLiveQuestion;
1133
1355
  private handlePermissionChange;
@@ -1140,6 +1362,7 @@ declare class StreamerServer {
1140
1362
  private handleStartSession;
1141
1363
  private linkSessionToProject;
1142
1364
  private watchConversationFile;
1365
+ private readFirstLineSessionId;
1143
1366
  private watchForJsonl;
1144
1367
  private watchForCodexRollout;
1145
1368
  private handleBrowse;
@@ -1171,6 +1394,13 @@ interface ConversationWatcherEvents {
1171
1394
  onConversationChanged?: (filePath: string) => void | Promise<void>;
1172
1395
  /** Fires when a tailed file is deleted (per-file watcher unlink event). */
1173
1396
  onFileDeleted?: (filePath: string) => void;
1397
+ /**
1398
+ * Fires when a tailed file shrank below our read offset (in-place truncation
1399
+ * or replacement by a shorter file). The tail has already reset to byte 0;
1400
+ * the consumer must discard any byte-offset index built for the old content,
1401
+ * which no longer describes this file.
1402
+ */
1403
+ onTruncated?: (filePath: string) => void;
1174
1404
  /** Reported errors per file. */
1175
1405
  onError?: (filePath: string, error: Error) => void;
1176
1406
  }
@@ -1193,6 +1423,7 @@ declare class ConversationWatcher {
1193
1423
  private onNewLineSpans;
1194
1424
  private onConversationChanged;
1195
1425
  private onFileDeleted;
1426
+ private onTruncated;
1196
1427
  private onError;
1197
1428
  constructor(events?: ConversationWatcherEvents);
1198
1429
  watch(filePath: string): void;
@@ -1217,4 +1448,4 @@ declare class ConversationWatcher {
1217
1448
  private readNewLines;
1218
1449
  }
1219
1450
 
1220
- 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 };
1451
+ export { type AgentClient, type AgentClientOpts, type AgentConfig, type AppendArgs, type AskOption, type AskQuestion, CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type CacheAlertResolveAction, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, LiveSessionManager, type ManagedSession, PTYManager, type PTYManagerOptions, type PermissionOption, type ProcessLiveness, type ProgressDedupeLRU, type ProviderName, type ServerConfig, type SessionActivity, type SessionCursor, type SessionListPage, type SessionListQuery, type SessionOwnership, 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 };