@threadbase-sh/streamer 1.36.4 → 1.38.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
@@ -78,6 +78,41 @@ interface Logger {
78
78
  pino: Logger$1;
79
79
  }
80
80
 
81
+ /** Claude Code `--permission-mode` values, as accepted by CLI v2.1.x. */
82
+ declare const PERMISSION_MODES: readonly ["acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan"];
83
+ type PermissionMode = (typeof PERMISSION_MODES)[number];
84
+ type FlagValueType = "boolean" | "string" | "enum" | "list";
85
+ /** How risky enabling a flag is. Drives the client's confirmation UX. */
86
+ type FlagRisk = "low" | "elevated" | "dangerous";
87
+ interface FlagDefinition {
88
+ /** Stable config/wire key. Never the CLI spelling. */
89
+ id: string;
90
+ /** The literal CLI token, e.g. "--add-dir". */
91
+ flag: string;
92
+ valueType: FlagValueType;
93
+ /** Allowed values when valueType === "enum". */
94
+ enumValues?: readonly string[];
95
+ /**
96
+ * Baseline risk. `permissionMode` is the exception: it is only dangerous for
97
+ * the values in DANGEROUS_PERMISSION_MODES, so clients must call
98
+ * `flagValueRisk()` rather than reading this field directly.
99
+ */
100
+ risk: FlagRisk;
101
+ }
102
+ type ClaudeFlagValue = string | string[] | boolean;
103
+ type ClaudeFlagValues = Record<string, ClaudeFlagValue>;
104
+
105
+ interface FeatureFlagDefinition {
106
+ /** Stable config/wire key. Used in server.yaml, on the CLI, and over HTTP. */
107
+ id: string;
108
+ /** Shipped to clients alongside the values so a UI can render it. */
109
+ description: string;
110
+ default: boolean;
111
+ /** Full env var name. */
112
+ env: string;
113
+ }
114
+ type FeatureFlagValues = Record<string, boolean>;
115
+
81
116
  declare const CLAUDE_CODE_PROVIDER: "claude-code";
82
117
  declare const CODEX_CLI_PROVIDER: "codex-cli";
83
118
  type ProviderName = typeof CLAUDE_CODE_PROVIDER | typeof CODEX_CLI_PROVIDER;
@@ -85,6 +120,34 @@ declare function isProviderName(value: unknown): value is ProviderName;
85
120
  declare function isProviderResumable(_provider: string | null | undefined, availabilityResumable: boolean): boolean;
86
121
 
87
122
  type SessionStatus = "running" | "waiting_input" | "idle";
123
+ /**
124
+ * Process-lifetime axis for a managed session (C1 durable session runtime).
125
+ * Orthogonal to SessionStatus — see SessionResponse.lifecycle for why the two
126
+ * are separate, and docs/architecture/2026-07-24-durable-session-runtime.md.
127
+ */
128
+ type SessionLifecycle = "attached" | "detached" | "orphaned" | "resumable" | "completed" | "failed";
129
+ /**
130
+ * How a SessionStatus was derived (C3).
131
+ * See docs/architecture/2026-07-24-session-state-confidence.md.
132
+ *
133
+ * The runners already compute this at every transition — it was written to a log
134
+ * line and discarded, so a status reached by a timer expiring was indistinguishable
135
+ * on the wire from one reached by observing a prompt marker.
136
+ */
137
+ type StatusSource = "spawn" | "prompt-marker" | "screen-marker" | "user-input" | "process-exit" | "timeout-fallback" | "quiet-fallback" | "shutdown";
138
+ /**
139
+ * How much to trust the status.
140
+ *
141
+ * `observed` — something in the stream or the process told us.
142
+ * `inferred` — a timer expired and we picked the most likely state.
143
+ *
144
+ * Deliberately two buckets rather than a numeric score: a percentage would imply
145
+ * a calibration we have no data to support. The point is that a guess must never
146
+ * be presented as an observation.
147
+ */
148
+ type StatusConfidence = "observed" | "inferred";
149
+ /** Confidence implied by each source. Inference is exactly the timer-driven paths. */
150
+ declare function confidenceForSource(source: StatusSource): StatusConfidence;
88
151
  interface ManagedSession {
89
152
  id: string;
90
153
  provider?: ProviderName;
@@ -108,6 +171,12 @@ interface ManagedSession {
108
171
  lastMessageText?: string;
109
172
  lastMessageAt?: Date;
110
173
  lastActivityAt?: Date;
174
+ /**
175
+ * How `status` was derived, and when (C3). Confidence is derived from the
176
+ * source via confidenceForSource — storing both would let them disagree.
177
+ */
178
+ statusSource?: StatusSource;
179
+ statusUpdatedAt?: Date;
111
180
  filePath?: string;
112
181
  resumedFromConversationId?: string;
113
182
  /**
@@ -297,6 +366,31 @@ interface SessionResponse {
297
366
  startedAt: string;
298
367
  completedAt: string | null;
299
368
  ptyAttached: boolean;
369
+ /**
370
+ * Process-lifetime axis, orthogonal to `status` (C1).
371
+ *
372
+ * `status` answers "what is the agent doing" (running / waiting_input /
373
+ * idle); `lifecycle` answers "does this process still exist and do we own
374
+ * it". They were conflated before: `idle` meant finished, killed-to-save-
375
+ * resources, and externally-discovered all at once, so a client could not
376
+ * tell a completed session from one we terminated.
377
+ *
378
+ * Additive and optional — `ptyAttached` keeps its meaning (=== "attached"),
379
+ * so a client that ignores this behaves exactly as it did before.
380
+ */
381
+ /**
382
+ * How `status` was derived and how far to trust it (C3). Additive: `status`
383
+ * keeps its exact meaning, so a client ignoring these behaves as before.
384
+ * An `inferred` confidence means a timer expired and we assumed — not that
385
+ * anything in the stream confirmed the state.
386
+ */
387
+ statusSource?: StatusSource;
388
+ statusConfidence?: StatusConfidence;
389
+ statusUpdatedAt?: string;
390
+ lifecycle?: SessionLifecycle;
391
+ /** How `lifecycle` was determined, so stale values are visible not implied. */
392
+ lifecycleSource?: "spawn" | "exit" | "probe" | "reconcile";
393
+ lifecycleUpdatedAt?: string;
300
394
  failureReason?: string;
301
395
  pid?: number;
302
396
  sessionName?: string;
@@ -404,9 +498,13 @@ interface ServerConfig {
404
498
  tailSize?: number;
405
499
  directoryScanDebounceMs?: number;
406
500
  defaultSystemPrompt?: string;
407
- defaultPermissionMode?: "acceptEdits" | "manual";
501
+ codexSystemPromptEnabled?: boolean;
502
+ featureFlags?: FeatureFlagValues;
503
+ defaultPermissionMode?: PermissionMode;
408
504
  defaultModel?: string;
409
505
  defaultEffort?: "low" | "medium" | "high" | "xhigh" | "max";
506
+ claudeFlags?: ClaudeFlagValues;
507
+ claudeExtraArgs?: string;
410
508
  }
411
509
  interface PTYManagerOptions {
412
510
  onOutput?: (sessionId: string, data: string) => void;
@@ -427,17 +525,21 @@ interface StartSessionOptions {
427
525
  projectPath: string;
428
526
  projectName?: string;
429
527
  branch?: string;
430
- permissionMode?: "acceptEdits" | "manual";
528
+ permissionMode?: PermissionMode;
431
529
  model?: string;
432
530
  effort?: "low" | "medium" | "high" | "xhigh" | "max";
531
+ claudeFlags?: ClaudeFlagValues;
532
+ claudeExtraArgs?: string;
433
533
  }
434
534
  interface StartFreshSessionOptions {
435
535
  projectPath: string;
436
536
  projectName?: string;
437
537
  systemPrompt?: string;
438
- permissionMode?: "acceptEdits" | "manual";
538
+ permissionMode?: PermissionMode;
439
539
  model?: string;
440
540
  effort?: "low" | "medium" | "high" | "xhigh" | "max";
541
+ claudeFlags?: ClaudeFlagValues;
542
+ claudeExtraArgs?: string;
441
543
  }
442
544
  interface SessionRunner {
443
545
  start(sessionId: string, options: StartSessionOptions): Promise<ManagedSession>;
@@ -450,12 +552,38 @@ interface SessionRunner {
450
552
  getOutput(sessionId: string): string;
451
553
  getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
452
554
  getInputHistory(sessionId: string): UserMessage[];
555
+ getPid(sessionId: string): number | null;
453
556
  getSession(sessionId: string): ManagedSession | null;
454
557
  hasSession(sessionId: string): boolean;
455
558
  listSessions(): ManagedSession[];
456
559
  dispose(): void;
457
560
  }
458
561
 
562
+ /**
563
+ * Scoped device capabilities (C5).
564
+ * See docs/architecture/2026-07-24-device-identity-and-capabilities.md.
565
+ *
566
+ * Authorization was all-or-nothing: authMiddleware asked one question — is this
567
+ * token the API key — and answered 401 or full access. A device paired merely to
568
+ * glance at session status held exactly the authority of the one driving the
569
+ * agent, because there was only ever one credential and no principal to scope.
570
+ */
571
+ declare const CAPABILITIES: readonly ["history:read", "session:control", "fs:browse", "fs:upload", "notifications", "admin"];
572
+ type Capability = (typeof CAPABILITIES)[number];
573
+ type CapabilityPreset = "full" | "read-only";
574
+ /**
575
+ * The principal behind a request.
576
+ *
577
+ * `legacy` is the shared API key: it predates device identity, so it carries the
578
+ * full preset and no device id. Keeping it working is what lets this ship
579
+ * without breaking every already-paired device.
580
+ */
581
+ interface Principal {
582
+ kind: "device" | "legacy";
583
+ deviceId?: string;
584
+ capabilities: Capability[];
585
+ }
586
+
459
587
  interface LineSpan {
460
588
  /** Absolute byte offset of the line's first byte in the file. */
461
589
  byteOffset: number;
@@ -818,6 +946,74 @@ declare class ConversationsRepository {
818
946
  hasOrphanRows(): boolean;
819
947
  }
820
948
 
949
+ /**
950
+ * Paired-device registry (C5).
951
+ * See docs/architecture/2026-07-24-device-identity-and-capabilities.md.
952
+ *
953
+ * Stores identity and authority for each paired device. Never stores a usable
954
+ * credential: only the SHA-256 of a device token, so reading this table cannot
955
+ * impersonate a device.
956
+ */
957
+ interface DeviceRow {
958
+ device_id: string;
959
+ public_key: string;
960
+ token_hash: string;
961
+ name: string | null;
962
+ capabilities: string;
963
+ created_at: number;
964
+ last_seen_at: number | null;
965
+ revoked_at: number | null;
966
+ }
967
+ /** A device as reported over the API. Deliberately carries no credential. */
968
+ interface DeviceView {
969
+ deviceId: string;
970
+ name: string | null;
971
+ capabilities: Capability[];
972
+ createdAt: number;
973
+ lastSeenAt: number | null;
974
+ revokedAt: number | null;
975
+ }
976
+ interface RegisteredDevice {
977
+ deviceId: string;
978
+ /** Returned to the client exactly once, at pairing. Never persisted raw. */
979
+ deviceToken: string;
980
+ capabilities: Capability[];
981
+ }
982
+ declare class DevicesRepository {
983
+ private insertStmt;
984
+ private byTokenHashStmt;
985
+ private byIdStmt;
986
+ private listStmt;
987
+ private revokeStmt;
988
+ private touchStmt;
989
+ constructor(db: Database.Database);
990
+ /**
991
+ * Record a newly paired device and mint its token.
992
+ *
993
+ * The raw token is returned to the caller and never stored — this is the only
994
+ * moment it exists outside the client.
995
+ */
996
+ register(args: {
997
+ publicKey: string;
998
+ name?: string | null;
999
+ preset?: CapabilityPreset;
1000
+ now?: number;
1001
+ }): RegisteredDevice;
1002
+ /**
1003
+ * Resolve a presented token to a device, or null.
1004
+ *
1005
+ * Returns null for a revoked device, so revocation takes effect on the very
1006
+ * next request with no cache to go stale.
1007
+ */
1008
+ authenticate(token: string): DeviceRow | null;
1009
+ get(deviceId: string): DeviceRow | null;
1010
+ /** All devices, including revoked ones — an audit surface needs the history. */
1011
+ list(): DeviceView[];
1012
+ /** Revoke one device. Others are untouched — no key rotation, no collateral. */
1013
+ revoke(deviceId: string, now?: number): boolean;
1014
+ touch(deviceId: string, now?: number): void;
1015
+ }
1016
+
821
1017
  /**
822
1018
  * Persisted Project row. Mirrors the projects SQLite table.
823
1019
  */
@@ -862,6 +1058,159 @@ declare class ProjectsRepository {
862
1058
  upsertProjectByPath(rawPath: string, input?: UpsertProjectInput): Project;
863
1059
  }
864
1060
 
1061
+ /**
1062
+ * Token kinds. A device supplies three non-interchangeable types, and
1063
+ * conflating them fails only at send time with no signal at registration:
1064
+ *
1065
+ * - `expo` — Expo relay token, for ordinary push notifications.
1066
+ * - `liveactivity_start` — ActivityKit push-to-start token. App-wide, one per
1067
+ * device, long-lived. Starts an activity when none exists.
1068
+ * - `liveactivity_update` — ActivityKit per-activity update token, issued by
1069
+ * iOS after an activity starts, scoped to that one activity, short-lived.
1070
+ */
1071
+ declare const PUSH_TOKEN_KINDS: readonly ["expo", "liveactivity_start", "liveactivity_update"];
1072
+ type PushTokenKind = (typeof PUSH_TOKEN_KINDS)[number];
1073
+ interface PushTokenRow {
1074
+ token: string;
1075
+ platform: string;
1076
+ device_id: string | null;
1077
+ registered_at: number;
1078
+ last_success_at: number | null;
1079
+ last_failure_at: number | null;
1080
+ last_failure_code: string | null;
1081
+ failure_streak: number;
1082
+ revoked_at: number | null;
1083
+ kind: PushTokenKind;
1084
+ activity_id: string | null;
1085
+ session_id: string | null;
1086
+ expires_at: number | null;
1087
+ stale_date: number | null;
1088
+ started_at: number | null;
1089
+ renewed_at: number | null;
1090
+ }
1091
+ /**
1092
+ * Health as reported to a client. Deliberately omits the token itself — a push
1093
+ * token is a delivery credential, and a health endpoint has no reason to echo
1094
+ * one back.
1095
+ */
1096
+ interface PushTokenHealth {
1097
+ platform: string;
1098
+ deviceId: string | null;
1099
+ registeredAt: number;
1100
+ lastSuccessAt: number | null;
1101
+ lastFailureAt: number | null;
1102
+ lastFailureCode: string | null;
1103
+ failureStreak: number;
1104
+ revokedAt: number | null;
1105
+ /**
1106
+ * Never delivered vs delivering vs failing vs revoked vs expired. The
1107
+ * distinction the user actually needs: "not yet" and "broken" look identical
1108
+ * without it.
1109
+ */
1110
+ state: "never-delivered" | "healthy" | "failing" | "dead" | "revoked" | "expired";
1111
+ kind: PushTokenKind;
1112
+ /** Present only for per-activity Live Activity tokens. */
1113
+ activityId: string | null;
1114
+ sessionId: string | null;
1115
+ expiresAt: number | null;
1116
+ }
1117
+ declare class PushRepository {
1118
+ private upsertStmt;
1119
+ private getStmt;
1120
+ private listActiveStmt;
1121
+ private listAllStmt;
1122
+ private successStmt;
1123
+ private failureStmt;
1124
+ private revokeStmt;
1125
+ private claimEventStmt;
1126
+ private markDeliveredStmt;
1127
+ private listByKindSessionStmt;
1128
+ private listByKindStmt;
1129
+ private listRenewableStmt;
1130
+ private claimRenewalStmt;
1131
+ private expireStmt;
1132
+ private expireSessionActivitiesStmt;
1133
+ constructor(db: Database.Database);
1134
+ /**
1135
+ * Register or refresh a token.
1136
+ *
1137
+ * `kind` defaults to Expo so a released client posting `{ token, platform }`
1138
+ * keeps working — tb-mobile cannot be force-updated, and every client
1139
+ * predating Live Activities is registering an Expo relay token.
1140
+ *
1141
+ * Several rows per device is normal and intended: a device runs one activity
1142
+ * per live session, each with its own update token. The token itself is the
1143
+ * primary key, so distinct activities never collide.
1144
+ */
1145
+ register(args: {
1146
+ token: string;
1147
+ platform: string;
1148
+ deviceId?: string | null;
1149
+ kind?: PushTokenKind;
1150
+ activityId?: string | null;
1151
+ sessionId?: string | null;
1152
+ expiresAt?: number | null;
1153
+ staleDate?: number | null;
1154
+ startedAt?: number | null;
1155
+ now?: number;
1156
+ }): void;
1157
+ get(token: string): PushTokenRow | null;
1158
+ /**
1159
+ * Expo tokens eligible for delivery — not revoked, not past the failure limit.
1160
+ *
1161
+ * Deliberately Expo-only. ActivityKit tokens go over direct APNs with a
1162
+ * different topic and are rejected by Expo's relay, so the ordinary
1163
+ * notification fan-out must not see them.
1164
+ */
1165
+ listDeliverable(): PushTokenRow[];
1166
+ /** Live-activity tokens for one session, eligible for delivery. */
1167
+ listForSession(kind: PushTokenKind, sessionId: string, now?: number): PushTokenRow[];
1168
+ /**
1169
+ * Every deliverable token of one kind.
1170
+ *
1171
+ * Used for push-to-start, which is app-wide rather than session-scoped: the
1172
+ * activity does not exist yet, so there is no per-activity token to look up.
1173
+ */
1174
+ listByKind(kind: PushTokenKind, now?: number): PushTokenRow[];
1175
+ /** Unrenewed activities with a renewal deadline, soonest first. */
1176
+ listRenewable(): PushTokenRow[];
1177
+ /**
1178
+ * Claim a row for renewal.
1179
+ *
1180
+ * Returns true exactly once per row. A restart re-arms timers from the
1181
+ * persisted deadline, so the same renewal can be attempted twice; the loser
1182
+ * gets false and must not send. Doing this as a conditional UPDATE rather
1183
+ * than read-then-write avoids the race where both attempts observe
1184
+ * "not yet renewed".
1185
+ */
1186
+ claimRenewal(token: string, now?: number): boolean;
1187
+ /** Mark one token expired, so it stops being a delivery target. */
1188
+ expire(token: string, now?: number): void;
1189
+ /**
1190
+ * Expire every live activity for a session.
1191
+ *
1192
+ * Called when the session ends. Without this, a per-activity token outlives
1193
+ * its session and a later renewal sweep would resurrect an activity for a
1194
+ * session that is already gone.
1195
+ */
1196
+ expireSessionActivities(sessionId: string, now?: number): void;
1197
+ /** Every token, including dead and revoked ones, for the health report. */
1198
+ listHealth(now?: number): PushTokenHealth[];
1199
+ recordSuccess(token: string, now?: number): void;
1200
+ recordFailure(token: string, code: string, now?: number): void;
1201
+ revoke(token: string, now?: number): boolean;
1202
+ /**
1203
+ * Claim an event id for delivery.
1204
+ *
1205
+ * Returns true exactly once per event id. A retry, a reconnect
1206
+ * reconciliation, or two triggers firing for the same underlying event all
1207
+ * get false and must not notify — the user should never be told twice about
1208
+ * one thing.
1209
+ */
1210
+ claimEvent(eventId: string, sessionId: string | null, now?: number): boolean;
1211
+ markDelivered(eventId: string, now?: number): void;
1212
+ }
1213
+
865
1214
  declare class SessionStore {
866
1215
  private managed;
867
1216
  private discovered;
@@ -914,6 +1263,7 @@ declare class LiveSessionManager {
914
1263
  getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
915
1264
  getInputHistory(sessionId: string): UserMessage[];
916
1265
  getSession(sessionId: string): ManagedSession | null;
1266
+ getPid(sessionId: string): number | null;
917
1267
  hasSession(sessionId: string): boolean;
918
1268
  listSessions(): ManagedSession[];
919
1269
  dispose(): void;
@@ -1025,6 +1375,21 @@ type ApiDeps = {
1025
1375
  newKey: string;
1026
1376
  persisted: boolean;
1027
1377
  };
1378
+ claudeFlagsConfig: () => {
1379
+ registry: readonly FlagDefinition[];
1380
+ values: ClaudeFlagValues;
1381
+ extraArgs: string | null;
1382
+ persisted: boolean;
1383
+ };
1384
+ setClaudeFlagsConfig: (values: ClaudeFlagValues, extraArgs: string | undefined) => {
1385
+ values: ClaudeFlagValues;
1386
+ extraArgs: string | null;
1387
+ persisted: boolean;
1388
+ };
1389
+ featureFlagsConfig: () => {
1390
+ registry: readonly FeatureFlagDefinition[];
1391
+ values: FeatureFlagValues;
1392
+ };
1028
1393
  publicUrl: string | null;
1029
1394
  browseRoot: string | null;
1030
1395
  browserCors: string | undefined;
@@ -1033,6 +1398,10 @@ type ApiDeps = {
1033
1398
  wsHub: WSHub;
1034
1399
  cache: () => ConversationCache | null;
1035
1400
  cacheMonitor: () => CacheIntegrityMonitor | null;
1401
+ /** Push registration + delivery state (C7). Null when the cache DB is unavailable. */
1402
+ pushRepo: () => PushRepository | null;
1403
+ /** Paired-device registry (C5). Null when the cache DB is unavailable. */
1404
+ devicesRepo: () => DevicesRepository | null;
1036
1405
  projectsRepo: () => ProjectsRepository | null;
1037
1406
  conversationsRepo: () => ConversationsRepository | null;
1038
1407
  sessionsRepo: () => SessionsRepository | null;
@@ -1078,6 +1447,8 @@ type AppEnv = {
1078
1447
  requestId?: string;
1079
1448
  validatedBody?: unknown;
1080
1449
  validatedQuery?: unknown;
1450
+ /** Who is making this request (C5). Set by authMiddleware. */
1451
+ principal?: Principal;
1081
1452
  };
1082
1453
  };
1083
1454
 
@@ -1168,6 +1539,7 @@ declare class PTYManager implements SessionRunner {
1168
1539
  getOutput(sessionId: string): string;
1169
1540
  getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;
1170
1541
  getInputHistory(sessionId: string): UserMessage[];
1542
+ getPid(sessionId: string): number | null;
1171
1543
  private recordUserMessage;
1172
1544
  getSession(sessionId: string): ManagedSession | null;
1173
1545
  hasSession(sessionId: string): boolean;
@@ -1225,12 +1597,21 @@ declare class StreamerServer {
1225
1597
  private sessionInputAttempts;
1226
1598
  private ptyGracePeriodMs;
1227
1599
  private defaultSystemPrompt;
1600
+ private featureFlags;
1601
+ private codexSystemPromptEnabled;
1228
1602
  private defaultPermissionMode;
1229
1603
  private defaultModel;
1230
1604
  private defaultEffort;
1605
+ private claudeFlags;
1606
+ private claudeExtraArgs;
1607
+ private claudeFlagsPersistable;
1231
1608
  private ptyGraceTimers;
1232
1609
  private ptyGraceDeferCounts;
1233
1610
  private sessionSubscribers;
1611
+ private lastAgentChunkAt;
1612
+ private idempotency;
1613
+ private sessionLifecycles;
1614
+ private idleReaperTimer;
1234
1615
  private clientIdToWs;
1235
1616
  private wsToClientId;
1236
1617
  private cache;
@@ -1238,7 +1619,14 @@ declare class StreamerServer {
1238
1619
  private projectsRepo;
1239
1620
  private conversationsRepo;
1240
1621
  private sessionsRepo;
1622
+ private managedSessionsRepo;
1623
+ private readonly streamerInstanceId;
1241
1624
  private cacheMetadataRepo;
1625
+ private pushRepo;
1626
+ private devicesRepo;
1627
+ private apnsClient;
1628
+ private liveActivityNotifier;
1629
+ private liveActivityRenewal;
1242
1630
  private discoveryCache;
1243
1631
  private cacheDir;
1244
1632
  private tailSize;
@@ -1263,7 +1651,97 @@ declare class StreamerServer {
1263
1651
  * a full broadcast if no match exists (old clients, or no WS registered yet).
1264
1652
  */
1265
1653
  private broadcastOrUnicastSessionList;
1654
+ /**
1655
+ * Overlay boot-reconciliation verdicts onto session responses.
1656
+ *
1657
+ * A session left by a previous run is not in the in-memory store, so
1658
+ * SessionStore cannot classify it — it only ever sees what this run spawned.
1659
+ * Discovery may still surface the process, in which case the reconciler knows
1660
+ * strictly more about it than discovery does: it can tell `detached` (alive
1661
+ * and confirmed ours) from `orphaned` (alive but identity unconfirmed), which
1662
+ * a pid enumeration alone cannot.
1663
+ *
1664
+ * Only applied when the session is NOT live here: a session this run owns has
1665
+ * an authoritative lifecycle already, and a stale verdict must never override
1666
+ * it.
1667
+ */
1668
+ private withReconciledLifecycle;
1266
1669
  private addSessionSubscriber;
1670
+ /**
1671
+ * Bring up Live Activity push, if credentials are present (Feature 12).
1672
+ *
1673
+ * APNS_KEY absent is the ordinary case on a dev machine and in CI, so this
1674
+ * logs once at info and leaves the feature off rather than failing: the server
1675
+ * must not refuse to boot over a missing optional push credential.
1676
+ *
1677
+ * The key is read from the environment as PEM contents and never from a path
1678
+ * on disk; neither it nor any device token is ever logged.
1679
+ */
1680
+ private initLiveActivityPush;
1681
+ /**
1682
+ * Classify sessions left behind by previous streamer runs (C1 Phase 3a).
1683
+ *
1684
+ * Agents already outlive the streamer today on the crash and dev-takeover
1685
+ * paths, which exit without reaching ptyManager.dispose() — they are just
1686
+ * invisible when they do, because nothing recorded that they existed. This
1687
+ * turns those rows into an explicit verdict per session.
1688
+ *
1689
+ * Read-only with respect to processes: it probes and classifies, and never
1690
+ * signals anything. `orphaned` is a report, not a cleanup trigger.
1691
+ */
1692
+ private reconcilePreviousSessions;
1693
+ /**
1694
+ * Pick a token guaranteed to appear in the spawned process's argv, for the
1695
+ * reconciler's pid-reuse guard.
1696
+ *
1697
+ * Claude always passes the session id (`--resume <id>` or `--session-id
1698
+ * <id>`), so it is both present and unique. Codex only does on *resume*
1699
+ * (`codex resume <id>`); a fresh Codex spawn is `codex --cd <path>
1700
+ * --no-alt-screen` with no id at all, because the rollout id does not exist
1701
+ * until the CLI writes it. boundConversationId is what distinguishes the two:
1702
+ * it is set once that rollout has been discovered.
1703
+ */
1704
+ private spawnArgvToken;
1705
+ /**
1706
+ * Mirror a freshly-spawned session into the durable registry (C1 Phase 2).
1707
+ *
1708
+ * Called at each addManaged() site rather than inside SessionStore, because
1709
+ * the store is a pure in-memory structure with no DB dependency and adding
1710
+ * one would drag persistence into every unit test that touches it.
1711
+ *
1712
+ * Best-effort by design: a failed registry write must never break session
1713
+ * start. Losing a row costs post-restart *visibility* for that session, which
1714
+ * is strictly better than refusing to run the agent at all.
1715
+ */
1716
+ private recordSessionSpawn;
1717
+ /**
1718
+ * Stamp every live session as ended-by-shutdown before dispose() kills it.
1719
+ *
1720
+ * PTYManager.dispose() signals each child directly and fires no
1721
+ * onStatusChange, so the registry would otherwise keep rows sitting at
1722
+ * `running` forever and the next boot could not tell a deliberate restart
1723
+ * from a crash. Recording `shutdown` as the status source makes that
1724
+ * distinction explicit rather than inferred.
1725
+ *
1726
+ * Not a `completed_at` write for the agent's own work — the agent did not
1727
+ * finish, we stopped it — but the session is genuinely terminal, so it must
1728
+ * leave the reconciler's probe set.
1729
+ */
1730
+ private recordShutdownState;
1731
+ /**
1732
+ * Release PTYs whose agent has been silent past IDLE_REAP_AFTER_MS.
1733
+ *
1734
+ * This is the bound that lets handleWsClose stop arming kill timers. The
1735
+ * distinction that matters: the old timer measured how long nobody was
1736
+ * *watching*, which is uncorrelated with whether work is in flight. This
1737
+ * measures how long the *agent* has produced nothing, and only ever considers
1738
+ * sessions that are already settled — a `running` PTY is skipped regardless of
1739
+ * age, so a long silent turn is never interrupted.
1740
+ *
1741
+ * Exposed (not private) so tests can drive one sweep deterministically instead
1742
+ * of waiting on the interval.
1743
+ */
1744
+ reapIdleSessions(now?: number): string[];
1267
1745
  private startGraceTimer;
1268
1746
  get port(): number;
1269
1747
  private currentWarmupState;
@@ -1283,6 +1761,27 @@ declare class StreamerServer {
1283
1761
  private handlePairStart;
1284
1762
  private handlePairExchange;
1285
1763
  private rotateApiKey;
1764
+ /**
1765
+ * The registry ships with the values so a client renders the list from one
1766
+ * round-trip, same as getClaudeFlagsConfig().
1767
+ *
1768
+ * Deliberately no `persisted` field: unlike claude-flags there is no PUT, and
1769
+ * the absence of that field is the signal that this endpoint is read-only.
1770
+ */
1771
+ private getFeatureFlagsConfig;
1772
+ private getClaudeFlagsConfig;
1773
+ /**
1774
+ * Replace the per-server flag set. Applies to the NEXT spawn — a live PTY
1775
+ * keeps the argv it was started with.
1776
+ *
1777
+ * Mirrors rotateApiKey(): when the values were pinned by a CLI flag we still
1778
+ * apply them in memory but skip the server.yaml write, because the flag would
1779
+ * win again on restart and silently revert them.
1780
+ *
1781
+ * Logged with old→new at info level on purpose: this can disable the
1782
+ * permission prompts entirely, so it needs a forensic trail.
1783
+ */
1784
+ private setClaudeFlagsConfig;
1286
1785
  private checkRateLimit;
1287
1786
  private checkExchangeRateLimit;
1288
1787
  private checkSessionStartRateLimit;
@@ -1483,4 +1982,4 @@ declare class ConversationWatcher {
1483
1982
  private readNewLines;
1484
1983
  }
1485
1984
 
1486
- 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 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 };
1985
+ 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 };