@threadbase-sh/streamer 1.41.1 → 1.42.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
@@ -137,7 +137,7 @@ type SessionLifecycle = "attached" | "detached" | "orphaned" | "resumable" | "co
137
137
  * line and discarded, so a status reached by a timer expiring was indistinguishable
138
138
  * on the wire from one reached by observing a prompt marker.
139
139
  */
140
- type StatusSource = "spawn" | "prompt-marker" | "screen-marker" | "user-input" | "process-exit" | "timeout-fallback" | "quiet-fallback" | "shutdown";
140
+ type StatusSource$1 = "spawn" | "prompt-marker" | "screen-marker" | "user-input" | "process-exit" | "timeout-fallback" | "quiet-fallback" | "shutdown";
141
141
  /**
142
142
  * How much to trust the status.
143
143
  *
@@ -150,7 +150,7 @@ type StatusSource = "spawn" | "prompt-marker" | "screen-marker" | "user-input" |
150
150
  */
151
151
  type StatusConfidence = "observed" | "inferred";
152
152
  /** Confidence implied by each source. Inference is exactly the timer-driven paths. */
153
- declare function confidenceForSource(source: StatusSource): StatusConfidence;
153
+ declare function confidenceForSource(source: StatusSource$1): StatusConfidence;
154
154
  interface ManagedSession {
155
155
  id: string;
156
156
  provider?: ProviderName;
@@ -178,7 +178,7 @@ interface ManagedSession {
178
178
  * How `status` was derived, and when (C3). Confidence is derived from the
179
179
  * source via confidenceForSource — storing both would let them disagree.
180
180
  */
181
- statusSource?: StatusSource;
181
+ statusSource?: StatusSource$1;
182
182
  statusUpdatedAt?: Date;
183
183
  filePath?: string;
184
184
  resumedFromConversationId?: string;
@@ -192,6 +192,13 @@ interface ManagedSession {
192
192
  * Cleared implicitly when a resume overwrites the stub with a real session.
193
193
  */
194
194
  rehydrated?: boolean;
195
+ /**
196
+ * What this session was doing when the streamer stopped it, for a stub whose
197
+ * `status` had to flatten to `idle`. Only ever set from a registry row whose
198
+ * `status_source` is `shutdown` — the one source that means "we ended this",
199
+ * as opposed to the agent finishing or a crash freezing the row mid-turn.
200
+ */
201
+ interruptedStatus?: "running" | "waiting_input";
195
202
  /**
196
203
  * Set once a live session's underlying persisted conversation file is
197
204
  * discovered after the fact (currently: fresh Codex sessions, whose
@@ -399,13 +406,22 @@ interface SessionResponse {
399
406
  * An `inferred` confidence means a timer expired and we assumed — not that
400
407
  * anything in the stream confirmed the state.
401
408
  */
402
- statusSource?: StatusSource;
409
+ statusSource?: StatusSource$1;
403
410
  statusConfidence?: StatusConfidence;
404
411
  statusUpdatedAt?: string;
405
412
  lifecycle?: SessionLifecycle;
406
413
  /** How `lifecycle` was determined, so stale values are visible not implied. */
407
414
  lifecycleSource?: "spawn" | "exit" | "probe" | "reconcile";
408
415
  lifecycleUpdatedAt?: string;
416
+ /**
417
+ * ADDITIVE (plan Phase 5). What a recovered session was doing when the
418
+ * streamer stopped it, so a client can say "interrupted mid-response" instead
419
+ * of the `idle` its `status` is forced to report — a stub holds no PTY, and a
420
+ * novel SessionStatus value would be dropped by `?status=` filtering on
421
+ * already-shipped clients. Present only on rows whose `status_source` was
422
+ * `shutdown`; older clients ignore it.
423
+ */
424
+ interruptedStatus?: "running" | "waiting_input";
409
425
  failureReason?: string;
410
426
  pid?: number;
411
427
  sessionName?: string;
@@ -510,6 +526,15 @@ interface ServerConfig {
510
526
  scannerPersistent?: boolean;
511
527
  skipStartupWarmup?: boolean;
512
528
  ptyGracePeriodMs?: number;
529
+ /**
530
+ * Re-start interrupted sessions at boot instead of listing them for the user
531
+ * to tap (plan Phase 7). A boolean here rather than the tri-state the loader
532
+ * returns: by the time a ServerConfig is built the "never asked" case has
533
+ * already resolved to false. Precedence: explicit here → `auto_resume_on_boot`
534
+ * in server.yaml → false. Never enabled implicitly — it is the one setting
535
+ * that starts an agent nobody asked for in that moment.
536
+ */
537
+ autoResumeOnBoot?: boolean;
513
538
  cacheDir?: string;
514
539
  runtimeDbPath?: string;
515
540
  tailSize?: number;
@@ -542,6 +567,15 @@ interface StartSessionOptions {
542
567
  projectPath: string;
543
568
  projectName?: string;
544
569
  branch?: string;
570
+ /**
571
+ * Provider-side id to resume from, when it differs from `sessionId`. Codex
572
+ * keys a fresh session by a local placeholder UUID and only learns its real
573
+ * rollout id once it writes the file, so resuming needs the rollout id in
574
+ * argv while the session keeps the placeholder the client navigated to.
575
+ * Absent means "resume by the session id", which is what every Claude session
576
+ * does — PTYManager ignores this field entirely.
577
+ */
578
+ resumeId?: string;
545
579
  permissionMode?: PermissionMode;
546
580
  model?: string;
547
581
  effort?: EffortLevel;
@@ -1031,6 +1065,125 @@ declare class DevicesRepository {
1031
1065
  touch(deviceId: string, now?: number): void;
1032
1066
  }
1033
1067
 
1068
+ /**
1069
+ * Durable registry of managed sessions (C1 Phase 2).
1070
+ * See docs/architecture/2026-07-24-durable-session-runtime.md.
1071
+ *
1072
+ * Distinct from SessionsRepository, which wraps the in-memory SessionStore for
1073
+ * the live request path. This one exists so session identity and provenance
1074
+ * outlive the streamer process: without it, a restart loses startedAt,
1075
+ * promptCount, sessionName, the Codex rollout binding, and failureReason, and
1076
+ * the boot reconciler has nothing to reconcile against.
1077
+ *
1078
+ * Deliberately not stored here: the PTY output ring buffer and xterm screen.
1079
+ * Their authoritative copy is the provider's JSONL, and writing 64KiB of ANSI
1080
+ * on every chunk to duplicate it would be a write storm for no gain.
1081
+ */
1082
+ /** How a persisted status was obtained — never inferred, always recorded. */
1083
+ type StatusSource = "spawn" | "transition" | "exit" | "shutdown" | "probe" | "reconcile";
1084
+
1085
+ interface ManagedSessionRow {
1086
+ session_id: string;
1087
+ provider: string;
1088
+ pid: number | null;
1089
+ cmdline: string | null;
1090
+ project_path: string;
1091
+ project_name: string;
1092
+ branch: string;
1093
+ status: string;
1094
+ status_source: string;
1095
+ status_updated_at: number;
1096
+ started_at: number;
1097
+ completed_at: number | null;
1098
+ last_activity_at: number | null;
1099
+ prompt_count: number;
1100
+ session_name: string | null;
1101
+ project_id: string | null;
1102
+ bound_conversation_id: string | null;
1103
+ resumed_from_conversation_id: string | null;
1104
+ failure_reason: string | null;
1105
+ streamer_instance_id: string;
1106
+ /**
1107
+ * Which machine boot `pid` was recorded during (migration 002). Optional
1108
+ * because rows written before it exists read back as null/absent, which the
1109
+ * reconciler treats exactly like a mismatch — never like a match.
1110
+ */
1111
+ boot_token?: string | null;
1112
+ }
1113
+ interface RecordSpawnInput {
1114
+ session: ManagedSession;
1115
+ pid: number | null;
1116
+ cmdline: string | null;
1117
+ streamerInstanceId: string;
1118
+ }
1119
+ declare class ManagedSessionsRepository {
1120
+ private upsertStmt;
1121
+ private updateStatusStmt;
1122
+ private bindStmt;
1123
+ private getStmt;
1124
+ private listNonTerminalStmt;
1125
+ private listAllStmt;
1126
+ private pruneTerminalStmt;
1127
+ private listRecoverableStmt;
1128
+ private deleteStmt;
1129
+ constructor(db: Database.Database);
1130
+ /** Record a session at spawn, or refresh every field of an existing row. */
1131
+ recordSpawn({ session, pid, cmdline, streamerInstanceId }: RecordSpawnInput): void;
1132
+ /**
1133
+ * Persist a status transition. `source` is required rather than defaulted:
1134
+ * a status whose provenance is unknown is the thing this table exists to
1135
+ * prevent, and the reconciler reads it to decide how much to trust the value.
1136
+ */
1137
+ recordStatus(sessionId: string, status: SessionStatus, source: StatusSource, fields?: {
1138
+ completedAt?: Date | null;
1139
+ lastActivityAt?: Date | null;
1140
+ promptCount?: number;
1141
+ failureReason?: string | null;
1142
+ /** Null/omitted keeps whatever is stored — it never clears a known name. */
1143
+ sessionName?: string | null;
1144
+ }): void;
1145
+ /**
1146
+ * Persist the Codex rollout id discovered after spawn.
1147
+ *
1148
+ * Its own statement rather than a `recordSpawn` re-run: the binding arrives
1149
+ * while the session is live, and re-upserting would also rewrite `cmdline`
1150
+ * with an id that is *not* in a fresh Codex process's argv, turning the
1151
+ * reconciler's identity check into a false `orphaned`. Without this write the
1152
+ * binding lives only in memory and dies with the streamer — which is the
1153
+ * whole reason a restarted Codex session could not be resumed (G6).
1154
+ */
1155
+ recordBinding(sessionId: string, boundConversationId: string): void;
1156
+ get(sessionId: string): ManagedSessionRow | null;
1157
+ /**
1158
+ * Rows with no recorded completion — the reconciler's probe set.
1159
+ *
1160
+ * Capped. Callers must compare the result length against the limit and say so
1161
+ * when it clips: a silently truncated probe set reads as "we checked
1162
+ * everything" when it did not.
1163
+ */
1164
+ listNonTerminal(limit?: number): ManagedSessionRow[];
1165
+ /** Every row, most recently touched first, for the diagnostics surface. */
1166
+ listAll(limit?: number): ManagedSessionRow[];
1167
+ /**
1168
+ * Delete terminal rows older than `olderThanMs`, returning how many went.
1169
+ *
1170
+ * Only rows carrying a `completed_at` are eligible, so nothing the reconciler
1171
+ * or rehydrator might still want is reachable from here — a row without one
1172
+ * is by definition unfinished business, however old it looks.
1173
+ */
1174
+ pruneTerminal(olderThanMs?: number): number;
1175
+ /**
1176
+ * Rows a restart could bring back: still open, or closed by our own shutdown,
1177
+ * and touched no longer ago than `sinceMs`. Newest first, capped — the caller
1178
+ * decides which of these actually deserve rehydrating (`shouldRehydrate`).
1179
+ */
1180
+ listRecoverable({ sinceMs, limit }: {
1181
+ sinceMs: number;
1182
+ limit: number;
1183
+ }): ManagedSessionRow[];
1184
+ delete(sessionId: string): void;
1185
+ }
1186
+
1034
1187
  /**
1035
1188
  * Persisted Project row. Mirrors the projects SQLite table.
1036
1189
  */
@@ -1420,6 +1573,14 @@ declare class CacheIntegrityMonitor {
1420
1573
  resolve(fingerprint: string, action: ResolveAction, ids?: string[]): Promise<ResolveResult>;
1421
1574
  }
1422
1575
 
1576
+ /** Result for a single registry row. Pure data — nothing here acts. */
1577
+ interface ReconcileVerdict {
1578
+ sessionId: string;
1579
+ lifecycle: SessionLifecycle;
1580
+ /** Why this verdict was reached, for logs and the diagnostics surface. */
1581
+ reason: string;
1582
+ }
1583
+
1423
1584
  type ApiDeps = {
1424
1585
  apiKey: string;
1425
1586
  localNoAuth: boolean;
@@ -1461,6 +1622,10 @@ type ApiDeps = {
1461
1622
  cacheMetadataRepo: () => CacheMetadataRepository | null;
1462
1623
  /** Authoritative session registry (~/.threadbase/runtime.db). Null when it failed to open. */
1463
1624
  runtimeStore: () => RuntimeStore | null;
1625
+ /** Rows of that registry. Null when it failed to open. */
1626
+ managedSessionsRepo: () => ManagedSessionsRepository | null;
1627
+ /** This boot's reconcile verdicts, by session id. Empty before it has run. */
1628
+ sessionVerdicts: () => Map<string, ReconcileVerdict>;
1464
1629
  ptyAttachedIds: () => Set<string>;
1465
1630
  handleListSessions: (url: URL, res: ServerResponse) => Promise<void>;
1466
1631
  handleSessionsCount: (res: ServerResponse) => void;
@@ -1673,7 +1838,7 @@ declare class StreamerServer {
1673
1838
  private lastAgentChunkAt;
1674
1839
  private terminalSeq;
1675
1840
  private idempotency;
1676
- private sessionLifecycles;
1841
+ private sessionVerdicts;
1677
1842
  private idleReaperTimer;
1678
1843
  private clientIdToWs;
1679
1844
  private wsToClientId;
@@ -1755,6 +1920,14 @@ declare class StreamerServer {
1755
1920
  * signals anything. `orphaned` is a report, not a cleanup trigger.
1756
1921
  */
1757
1922
  private reconcilePreviousSessions;
1923
+ /**
1924
+ * Drop finished sessions the registry has held long enough (plan Phase 4).
1925
+ *
1926
+ * The registry is authoritative and never rebuilt from the cache, so nothing
1927
+ * else would ever remove a row: without this it grows for the life of the
1928
+ * install, and every boot pays for rows about sessions from months ago.
1929
+ */
1930
+ private pruneTerminalSessions;
1758
1931
  /**
1759
1932
  * Seed the session list with what previous runs left behind (persistence plan
1760
1933
  * Phase 1, gaps G1/G2/G8).
@@ -1980,6 +2153,19 @@ declare class StreamerServer {
1980
2153
  private handleListSessions;
1981
2154
  private handleGetSession;
1982
2155
  private handleResume;
2156
+ /**
2157
+ * Resume a session, from an HTTP request or from the boot path.
2158
+ *
2159
+ * Extracted from `handleResume` so both callers hit the **same collision
2160
+ * probe** (plan Phase 7c). The probe is what stops this streamer attaching to
2161
+ * a conversation an external terminal already owns; a second, hand-adapted
2162
+ * copy of this sequence in the boot path is how two agents end up appending
2163
+ * to one JSONL at 4am with nobody watching.
2164
+ *
2165
+ * Returns a typed reason rather than writing a response, so the HTTP caller
2166
+ * maps it to a status code and the boot caller logs it.
2167
+ */
2168
+ private resumeSession;
1983
2169
  private enrichResumedSessionAsync;
1984
2170
  private handleSendInput;
1985
2171
  private processJsonlQuestions;
@@ -2096,4 +2282,4 @@ declare class ConversationWatcher {
2096
2282
  private readNewLines;
2097
2283
  }
2098
2284
 
2099
- 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 ServerWarmingUpResponse, type ServerWarmupState, type SessionActivity, type SessionCursor, type SessionLifecycle, type SessionListPage, type SessionListQuery, type SessionOwnership, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartFreshSessionOptions, type StartSessionOptions, type StatusConfidence, type StatusSource, StreamerServer, type UserMessage, WSHub, type WSMessage, confidenceForSource, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
2285
+ 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 ServerWarmingUpResponse, type ServerWarmupState, type SessionActivity, type SessionCursor, type SessionLifecycle, type SessionListPage, type SessionListQuery, type SessionOwnership, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartFreshSessionOptions, type StartSessionOptions, type StatusConfidence, type StatusSource$1 as StatusSource, StreamerServer, type UserMessage, WSHub, type WSMessage, confidenceForSource, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
package/dist/index.d.ts CHANGED
@@ -137,7 +137,7 @@ type SessionLifecycle = "attached" | "detached" | "orphaned" | "resumable" | "co
137
137
  * line and discarded, so a status reached by a timer expiring was indistinguishable
138
138
  * on the wire from one reached by observing a prompt marker.
139
139
  */
140
- type StatusSource = "spawn" | "prompt-marker" | "screen-marker" | "user-input" | "process-exit" | "timeout-fallback" | "quiet-fallback" | "shutdown";
140
+ type StatusSource$1 = "spawn" | "prompt-marker" | "screen-marker" | "user-input" | "process-exit" | "timeout-fallback" | "quiet-fallback" | "shutdown";
141
141
  /**
142
142
  * How much to trust the status.
143
143
  *
@@ -150,7 +150,7 @@ type StatusSource = "spawn" | "prompt-marker" | "screen-marker" | "user-input" |
150
150
  */
151
151
  type StatusConfidence = "observed" | "inferred";
152
152
  /** Confidence implied by each source. Inference is exactly the timer-driven paths. */
153
- declare function confidenceForSource(source: StatusSource): StatusConfidence;
153
+ declare function confidenceForSource(source: StatusSource$1): StatusConfidence;
154
154
  interface ManagedSession {
155
155
  id: string;
156
156
  provider?: ProviderName;
@@ -178,7 +178,7 @@ interface ManagedSession {
178
178
  * How `status` was derived, and when (C3). Confidence is derived from the
179
179
  * source via confidenceForSource — storing both would let them disagree.
180
180
  */
181
- statusSource?: StatusSource;
181
+ statusSource?: StatusSource$1;
182
182
  statusUpdatedAt?: Date;
183
183
  filePath?: string;
184
184
  resumedFromConversationId?: string;
@@ -192,6 +192,13 @@ interface ManagedSession {
192
192
  * Cleared implicitly when a resume overwrites the stub with a real session.
193
193
  */
194
194
  rehydrated?: boolean;
195
+ /**
196
+ * What this session was doing when the streamer stopped it, for a stub whose
197
+ * `status` had to flatten to `idle`. Only ever set from a registry row whose
198
+ * `status_source` is `shutdown` — the one source that means "we ended this",
199
+ * as opposed to the agent finishing or a crash freezing the row mid-turn.
200
+ */
201
+ interruptedStatus?: "running" | "waiting_input";
195
202
  /**
196
203
  * Set once a live session's underlying persisted conversation file is
197
204
  * discovered after the fact (currently: fresh Codex sessions, whose
@@ -399,13 +406,22 @@ interface SessionResponse {
399
406
  * An `inferred` confidence means a timer expired and we assumed — not that
400
407
  * anything in the stream confirmed the state.
401
408
  */
402
- statusSource?: StatusSource;
409
+ statusSource?: StatusSource$1;
403
410
  statusConfidence?: StatusConfidence;
404
411
  statusUpdatedAt?: string;
405
412
  lifecycle?: SessionLifecycle;
406
413
  /** How `lifecycle` was determined, so stale values are visible not implied. */
407
414
  lifecycleSource?: "spawn" | "exit" | "probe" | "reconcile";
408
415
  lifecycleUpdatedAt?: string;
416
+ /**
417
+ * ADDITIVE (plan Phase 5). What a recovered session was doing when the
418
+ * streamer stopped it, so a client can say "interrupted mid-response" instead
419
+ * of the `idle` its `status` is forced to report — a stub holds no PTY, and a
420
+ * novel SessionStatus value would be dropped by `?status=` filtering on
421
+ * already-shipped clients. Present only on rows whose `status_source` was
422
+ * `shutdown`; older clients ignore it.
423
+ */
424
+ interruptedStatus?: "running" | "waiting_input";
409
425
  failureReason?: string;
410
426
  pid?: number;
411
427
  sessionName?: string;
@@ -510,6 +526,15 @@ interface ServerConfig {
510
526
  scannerPersistent?: boolean;
511
527
  skipStartupWarmup?: boolean;
512
528
  ptyGracePeriodMs?: number;
529
+ /**
530
+ * Re-start interrupted sessions at boot instead of listing them for the user
531
+ * to tap (plan Phase 7). A boolean here rather than the tri-state the loader
532
+ * returns: by the time a ServerConfig is built the "never asked" case has
533
+ * already resolved to false. Precedence: explicit here → `auto_resume_on_boot`
534
+ * in server.yaml → false. Never enabled implicitly — it is the one setting
535
+ * that starts an agent nobody asked for in that moment.
536
+ */
537
+ autoResumeOnBoot?: boolean;
513
538
  cacheDir?: string;
514
539
  runtimeDbPath?: string;
515
540
  tailSize?: number;
@@ -542,6 +567,15 @@ interface StartSessionOptions {
542
567
  projectPath: string;
543
568
  projectName?: string;
544
569
  branch?: string;
570
+ /**
571
+ * Provider-side id to resume from, when it differs from `sessionId`. Codex
572
+ * keys a fresh session by a local placeholder UUID and only learns its real
573
+ * rollout id once it writes the file, so resuming needs the rollout id in
574
+ * argv while the session keeps the placeholder the client navigated to.
575
+ * Absent means "resume by the session id", which is what every Claude session
576
+ * does — PTYManager ignores this field entirely.
577
+ */
578
+ resumeId?: string;
545
579
  permissionMode?: PermissionMode;
546
580
  model?: string;
547
581
  effort?: EffortLevel;
@@ -1031,6 +1065,125 @@ declare class DevicesRepository {
1031
1065
  touch(deviceId: string, now?: number): void;
1032
1066
  }
1033
1067
 
1068
+ /**
1069
+ * Durable registry of managed sessions (C1 Phase 2).
1070
+ * See docs/architecture/2026-07-24-durable-session-runtime.md.
1071
+ *
1072
+ * Distinct from SessionsRepository, which wraps the in-memory SessionStore for
1073
+ * the live request path. This one exists so session identity and provenance
1074
+ * outlive the streamer process: without it, a restart loses startedAt,
1075
+ * promptCount, sessionName, the Codex rollout binding, and failureReason, and
1076
+ * the boot reconciler has nothing to reconcile against.
1077
+ *
1078
+ * Deliberately not stored here: the PTY output ring buffer and xterm screen.
1079
+ * Their authoritative copy is the provider's JSONL, and writing 64KiB of ANSI
1080
+ * on every chunk to duplicate it would be a write storm for no gain.
1081
+ */
1082
+ /** How a persisted status was obtained — never inferred, always recorded. */
1083
+ type StatusSource = "spawn" | "transition" | "exit" | "shutdown" | "probe" | "reconcile";
1084
+
1085
+ interface ManagedSessionRow {
1086
+ session_id: string;
1087
+ provider: string;
1088
+ pid: number | null;
1089
+ cmdline: string | null;
1090
+ project_path: string;
1091
+ project_name: string;
1092
+ branch: string;
1093
+ status: string;
1094
+ status_source: string;
1095
+ status_updated_at: number;
1096
+ started_at: number;
1097
+ completed_at: number | null;
1098
+ last_activity_at: number | null;
1099
+ prompt_count: number;
1100
+ session_name: string | null;
1101
+ project_id: string | null;
1102
+ bound_conversation_id: string | null;
1103
+ resumed_from_conversation_id: string | null;
1104
+ failure_reason: string | null;
1105
+ streamer_instance_id: string;
1106
+ /**
1107
+ * Which machine boot `pid` was recorded during (migration 002). Optional
1108
+ * because rows written before it exists read back as null/absent, which the
1109
+ * reconciler treats exactly like a mismatch — never like a match.
1110
+ */
1111
+ boot_token?: string | null;
1112
+ }
1113
+ interface RecordSpawnInput {
1114
+ session: ManagedSession;
1115
+ pid: number | null;
1116
+ cmdline: string | null;
1117
+ streamerInstanceId: string;
1118
+ }
1119
+ declare class ManagedSessionsRepository {
1120
+ private upsertStmt;
1121
+ private updateStatusStmt;
1122
+ private bindStmt;
1123
+ private getStmt;
1124
+ private listNonTerminalStmt;
1125
+ private listAllStmt;
1126
+ private pruneTerminalStmt;
1127
+ private listRecoverableStmt;
1128
+ private deleteStmt;
1129
+ constructor(db: Database.Database);
1130
+ /** Record a session at spawn, or refresh every field of an existing row. */
1131
+ recordSpawn({ session, pid, cmdline, streamerInstanceId }: RecordSpawnInput): void;
1132
+ /**
1133
+ * Persist a status transition. `source` is required rather than defaulted:
1134
+ * a status whose provenance is unknown is the thing this table exists to
1135
+ * prevent, and the reconciler reads it to decide how much to trust the value.
1136
+ */
1137
+ recordStatus(sessionId: string, status: SessionStatus, source: StatusSource, fields?: {
1138
+ completedAt?: Date | null;
1139
+ lastActivityAt?: Date | null;
1140
+ promptCount?: number;
1141
+ failureReason?: string | null;
1142
+ /** Null/omitted keeps whatever is stored — it never clears a known name. */
1143
+ sessionName?: string | null;
1144
+ }): void;
1145
+ /**
1146
+ * Persist the Codex rollout id discovered after spawn.
1147
+ *
1148
+ * Its own statement rather than a `recordSpawn` re-run: the binding arrives
1149
+ * while the session is live, and re-upserting would also rewrite `cmdline`
1150
+ * with an id that is *not* in a fresh Codex process's argv, turning the
1151
+ * reconciler's identity check into a false `orphaned`. Without this write the
1152
+ * binding lives only in memory and dies with the streamer — which is the
1153
+ * whole reason a restarted Codex session could not be resumed (G6).
1154
+ */
1155
+ recordBinding(sessionId: string, boundConversationId: string): void;
1156
+ get(sessionId: string): ManagedSessionRow | null;
1157
+ /**
1158
+ * Rows with no recorded completion — the reconciler's probe set.
1159
+ *
1160
+ * Capped. Callers must compare the result length against the limit and say so
1161
+ * when it clips: a silently truncated probe set reads as "we checked
1162
+ * everything" when it did not.
1163
+ */
1164
+ listNonTerminal(limit?: number): ManagedSessionRow[];
1165
+ /** Every row, most recently touched first, for the diagnostics surface. */
1166
+ listAll(limit?: number): ManagedSessionRow[];
1167
+ /**
1168
+ * Delete terminal rows older than `olderThanMs`, returning how many went.
1169
+ *
1170
+ * Only rows carrying a `completed_at` are eligible, so nothing the reconciler
1171
+ * or rehydrator might still want is reachable from here — a row without one
1172
+ * is by definition unfinished business, however old it looks.
1173
+ */
1174
+ pruneTerminal(olderThanMs?: number): number;
1175
+ /**
1176
+ * Rows a restart could bring back: still open, or closed by our own shutdown,
1177
+ * and touched no longer ago than `sinceMs`. Newest first, capped — the caller
1178
+ * decides which of these actually deserve rehydrating (`shouldRehydrate`).
1179
+ */
1180
+ listRecoverable({ sinceMs, limit }: {
1181
+ sinceMs: number;
1182
+ limit: number;
1183
+ }): ManagedSessionRow[];
1184
+ delete(sessionId: string): void;
1185
+ }
1186
+
1034
1187
  /**
1035
1188
  * Persisted Project row. Mirrors the projects SQLite table.
1036
1189
  */
@@ -1420,6 +1573,14 @@ declare class CacheIntegrityMonitor {
1420
1573
  resolve(fingerprint: string, action: ResolveAction, ids?: string[]): Promise<ResolveResult>;
1421
1574
  }
1422
1575
 
1576
+ /** Result for a single registry row. Pure data — nothing here acts. */
1577
+ interface ReconcileVerdict {
1578
+ sessionId: string;
1579
+ lifecycle: SessionLifecycle;
1580
+ /** Why this verdict was reached, for logs and the diagnostics surface. */
1581
+ reason: string;
1582
+ }
1583
+
1423
1584
  type ApiDeps = {
1424
1585
  apiKey: string;
1425
1586
  localNoAuth: boolean;
@@ -1461,6 +1622,10 @@ type ApiDeps = {
1461
1622
  cacheMetadataRepo: () => CacheMetadataRepository | null;
1462
1623
  /** Authoritative session registry (~/.threadbase/runtime.db). Null when it failed to open. */
1463
1624
  runtimeStore: () => RuntimeStore | null;
1625
+ /** Rows of that registry. Null when it failed to open. */
1626
+ managedSessionsRepo: () => ManagedSessionsRepository | null;
1627
+ /** This boot's reconcile verdicts, by session id. Empty before it has run. */
1628
+ sessionVerdicts: () => Map<string, ReconcileVerdict>;
1464
1629
  ptyAttachedIds: () => Set<string>;
1465
1630
  handleListSessions: (url: URL, res: ServerResponse) => Promise<void>;
1466
1631
  handleSessionsCount: (res: ServerResponse) => void;
@@ -1673,7 +1838,7 @@ declare class StreamerServer {
1673
1838
  private lastAgentChunkAt;
1674
1839
  private terminalSeq;
1675
1840
  private idempotency;
1676
- private sessionLifecycles;
1841
+ private sessionVerdicts;
1677
1842
  private idleReaperTimer;
1678
1843
  private clientIdToWs;
1679
1844
  private wsToClientId;
@@ -1755,6 +1920,14 @@ declare class StreamerServer {
1755
1920
  * signals anything. `orphaned` is a report, not a cleanup trigger.
1756
1921
  */
1757
1922
  private reconcilePreviousSessions;
1923
+ /**
1924
+ * Drop finished sessions the registry has held long enough (plan Phase 4).
1925
+ *
1926
+ * The registry is authoritative and never rebuilt from the cache, so nothing
1927
+ * else would ever remove a row: without this it grows for the life of the
1928
+ * install, and every boot pays for rows about sessions from months ago.
1929
+ */
1930
+ private pruneTerminalSessions;
1758
1931
  /**
1759
1932
  * Seed the session list with what previous runs left behind (persistence plan
1760
1933
  * Phase 1, gaps G1/G2/G8).
@@ -1980,6 +2153,19 @@ declare class StreamerServer {
1980
2153
  private handleListSessions;
1981
2154
  private handleGetSession;
1982
2155
  private handleResume;
2156
+ /**
2157
+ * Resume a session, from an HTTP request or from the boot path.
2158
+ *
2159
+ * Extracted from `handleResume` so both callers hit the **same collision
2160
+ * probe** (plan Phase 7c). The probe is what stops this streamer attaching to
2161
+ * a conversation an external terminal already owns; a second, hand-adapted
2162
+ * copy of this sequence in the boot path is how two agents end up appending
2163
+ * to one JSONL at 4am with nobody watching.
2164
+ *
2165
+ * Returns a typed reason rather than writing a response, so the HTTP caller
2166
+ * maps it to a status code and the boot caller logs it.
2167
+ */
2168
+ private resumeSession;
1983
2169
  private enrichResumedSessionAsync;
1984
2170
  private handleSendInput;
1985
2171
  private processJsonlQuestions;
@@ -2096,4 +2282,4 @@ declare class ConversationWatcher {
2096
2282
  private readNewLines;
2097
2283
  }
2098
2284
 
2099
- 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 ServerWarmingUpResponse, type ServerWarmupState, type SessionActivity, type SessionCursor, type SessionLifecycle, type SessionListPage, type SessionListQuery, type SessionOwnership, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartFreshSessionOptions, type StartSessionOptions, type StatusConfidence, type StatusSource, StreamerServer, type UserMessage, WSHub, type WSMessage, confidenceForSource, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
2285
+ 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 ServerWarmingUpResponse, type ServerWarmupState, type SessionActivity, type SessionCursor, type SessionLifecycle, type SessionListPage, type SessionListQuery, type SessionOwnership, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartFreshSessionOptions, type StartSessionOptions, type StatusConfidence, type StatusSource$1 as StatusSource, StreamerServer, type UserMessage, WSHub, type WSMessage, confidenceForSource, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };