@threadbase-sh/streamer 1.41.2 → 1.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +3221 -2321
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +559 -146
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +196 -6
- package/dist/index.d.ts +196 -6
- package/dist/index.js +553 -140
- package/dist/index.js.map +1 -1
- package/dist/runtime-migrations/002_add_managed_session_boot_token.sql +10 -0
- package/package.json +1 -1
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;
|
|
@@ -1650,6 +1815,7 @@ declare class StreamerServer {
|
|
|
1650
1815
|
private dbInstanceId;
|
|
1651
1816
|
private disableDb;
|
|
1652
1817
|
private skipStartupWarmup;
|
|
1818
|
+
private autoResumeOnBoot;
|
|
1653
1819
|
private browseRoot;
|
|
1654
1820
|
private publicUrl;
|
|
1655
1821
|
private browserCors;
|
|
@@ -1673,7 +1839,7 @@ declare class StreamerServer {
|
|
|
1673
1839
|
private lastAgentChunkAt;
|
|
1674
1840
|
private terminalSeq;
|
|
1675
1841
|
private idempotency;
|
|
1676
|
-
private
|
|
1842
|
+
private sessionVerdicts;
|
|
1677
1843
|
private idleReaperTimer;
|
|
1678
1844
|
private clientIdToWs;
|
|
1679
1845
|
private wsToClientId;
|
|
@@ -1716,6 +1882,7 @@ declare class StreamerServer {
|
|
|
1716
1882
|
* a full broadcast if no match exists (old clients, or no WS registered yet).
|
|
1717
1883
|
*/
|
|
1718
1884
|
private broadcastOrUnicastSessionList;
|
|
1885
|
+
private sessionListPayload;
|
|
1719
1886
|
/**
|
|
1720
1887
|
* Overlay boot-reconciliation verdicts onto session responses.
|
|
1721
1888
|
*
|
|
@@ -1755,6 +1922,14 @@ declare class StreamerServer {
|
|
|
1755
1922
|
* signals anything. `orphaned` is a report, not a cleanup trigger.
|
|
1756
1923
|
*/
|
|
1757
1924
|
private reconcilePreviousSessions;
|
|
1925
|
+
/**
|
|
1926
|
+
* Drop finished sessions the registry has held long enough (plan Phase 4).
|
|
1927
|
+
*
|
|
1928
|
+
* The registry is authoritative and never rebuilt from the cache, so nothing
|
|
1929
|
+
* else would ever remove a row: without this it grows for the life of the
|
|
1930
|
+
* install, and every boot pays for rows about sessions from months ago.
|
|
1931
|
+
*/
|
|
1932
|
+
private pruneTerminalSessions;
|
|
1758
1933
|
/**
|
|
1759
1934
|
* Seed the session list with what previous runs left behind (persistence plan
|
|
1760
1935
|
* Phase 1, gaps G1/G2/G8).
|
|
@@ -1771,6 +1946,8 @@ declare class StreamerServer {
|
|
|
1771
1946
|
* by id rather than duplicating it.
|
|
1772
1947
|
*/
|
|
1773
1948
|
private rehydratePreviousSessions;
|
|
1949
|
+
/** Resume only the recent sessions the user explicitly allowed us to start at boot. */
|
|
1950
|
+
private autoResumePreviousSessions;
|
|
1774
1951
|
/**
|
|
1775
1952
|
* Pick a token guaranteed to appear in the spawned process's argv, for the
|
|
1776
1953
|
* reconciler's pid-reuse guard.
|
|
@@ -1980,6 +2157,19 @@ declare class StreamerServer {
|
|
|
1980
2157
|
private handleListSessions;
|
|
1981
2158
|
private handleGetSession;
|
|
1982
2159
|
private handleResume;
|
|
2160
|
+
/**
|
|
2161
|
+
* Resume a session, from an HTTP request or from the boot path.
|
|
2162
|
+
*
|
|
2163
|
+
* Extracted from `handleResume` so both callers hit the **same collision
|
|
2164
|
+
* probe** (plan Phase 7c). The probe is what stops this streamer attaching to
|
|
2165
|
+
* a conversation an external terminal already owns; a second, hand-adapted
|
|
2166
|
+
* copy of this sequence in the boot path is how two agents end up appending
|
|
2167
|
+
* to one JSONL at 4am with nobody watching.
|
|
2168
|
+
*
|
|
2169
|
+
* Returns a typed reason rather than writing a response, so the HTTP caller
|
|
2170
|
+
* maps it to a status code and the boot caller logs it.
|
|
2171
|
+
*/
|
|
2172
|
+
private resumeSession;
|
|
1983
2173
|
private enrichResumedSessionAsync;
|
|
1984
2174
|
private handleSendInput;
|
|
1985
2175
|
private processJsonlQuestions;
|
|
@@ -2096,4 +2286,4 @@ declare class ConversationWatcher {
|
|
|
2096
2286
|
private readNewLines;
|
|
2097
2287
|
}
|
|
2098
2288
|
|
|
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 };
|
|
2289
|
+
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;
|
|
@@ -1650,6 +1815,7 @@ declare class StreamerServer {
|
|
|
1650
1815
|
private dbInstanceId;
|
|
1651
1816
|
private disableDb;
|
|
1652
1817
|
private skipStartupWarmup;
|
|
1818
|
+
private autoResumeOnBoot;
|
|
1653
1819
|
private browseRoot;
|
|
1654
1820
|
private publicUrl;
|
|
1655
1821
|
private browserCors;
|
|
@@ -1673,7 +1839,7 @@ declare class StreamerServer {
|
|
|
1673
1839
|
private lastAgentChunkAt;
|
|
1674
1840
|
private terminalSeq;
|
|
1675
1841
|
private idempotency;
|
|
1676
|
-
private
|
|
1842
|
+
private sessionVerdicts;
|
|
1677
1843
|
private idleReaperTimer;
|
|
1678
1844
|
private clientIdToWs;
|
|
1679
1845
|
private wsToClientId;
|
|
@@ -1716,6 +1882,7 @@ declare class StreamerServer {
|
|
|
1716
1882
|
* a full broadcast if no match exists (old clients, or no WS registered yet).
|
|
1717
1883
|
*/
|
|
1718
1884
|
private broadcastOrUnicastSessionList;
|
|
1885
|
+
private sessionListPayload;
|
|
1719
1886
|
/**
|
|
1720
1887
|
* Overlay boot-reconciliation verdicts onto session responses.
|
|
1721
1888
|
*
|
|
@@ -1755,6 +1922,14 @@ declare class StreamerServer {
|
|
|
1755
1922
|
* signals anything. `orphaned` is a report, not a cleanup trigger.
|
|
1756
1923
|
*/
|
|
1757
1924
|
private reconcilePreviousSessions;
|
|
1925
|
+
/**
|
|
1926
|
+
* Drop finished sessions the registry has held long enough (plan Phase 4).
|
|
1927
|
+
*
|
|
1928
|
+
* The registry is authoritative and never rebuilt from the cache, so nothing
|
|
1929
|
+
* else would ever remove a row: without this it grows for the life of the
|
|
1930
|
+
* install, and every boot pays for rows about sessions from months ago.
|
|
1931
|
+
*/
|
|
1932
|
+
private pruneTerminalSessions;
|
|
1758
1933
|
/**
|
|
1759
1934
|
* Seed the session list with what previous runs left behind (persistence plan
|
|
1760
1935
|
* Phase 1, gaps G1/G2/G8).
|
|
@@ -1771,6 +1946,8 @@ declare class StreamerServer {
|
|
|
1771
1946
|
* by id rather than duplicating it.
|
|
1772
1947
|
*/
|
|
1773
1948
|
private rehydratePreviousSessions;
|
|
1949
|
+
/** Resume only the recent sessions the user explicitly allowed us to start at boot. */
|
|
1950
|
+
private autoResumePreviousSessions;
|
|
1774
1951
|
/**
|
|
1775
1952
|
* Pick a token guaranteed to appear in the spawned process's argv, for the
|
|
1776
1953
|
* reconciler's pid-reuse guard.
|
|
@@ -1980,6 +2157,19 @@ declare class StreamerServer {
|
|
|
1980
2157
|
private handleListSessions;
|
|
1981
2158
|
private handleGetSession;
|
|
1982
2159
|
private handleResume;
|
|
2160
|
+
/**
|
|
2161
|
+
* Resume a session, from an HTTP request or from the boot path.
|
|
2162
|
+
*
|
|
2163
|
+
* Extracted from `handleResume` so both callers hit the **same collision
|
|
2164
|
+
* probe** (plan Phase 7c). The probe is what stops this streamer attaching to
|
|
2165
|
+
* a conversation an external terminal already owns; a second, hand-adapted
|
|
2166
|
+
* copy of this sequence in the boot path is how two agents end up appending
|
|
2167
|
+
* to one JSONL at 4am with nobody watching.
|
|
2168
|
+
*
|
|
2169
|
+
* Returns a typed reason rather than writing a response, so the HTTP caller
|
|
2170
|
+
* maps it to a status code and the boot caller logs it.
|
|
2171
|
+
*/
|
|
2172
|
+
private resumeSession;
|
|
1983
2173
|
private enrichResumedSessionAsync;
|
|
1984
2174
|
private handleSendInput;
|
|
1985
2175
|
private processJsonlQuestions;
|
|
@@ -2096,4 +2286,4 @@ declare class ConversationWatcher {
|
|
|
2096
2286
|
private readNewLines;
|
|
2097
2287
|
}
|
|
2098
2288
|
|
|
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 };
|
|
2289
|
+
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 };
|