@threadbase-sh/streamer 1.37.0 → 1.39.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
@@ -81,6 +81,9 @@ interface Logger {
81
81
  /** Claude Code `--permission-mode` values, as accepted by CLI v2.1.x. */
82
82
  declare const PERMISSION_MODES: readonly ["acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan"];
83
83
  type PermissionMode = (typeof PERMISSION_MODES)[number];
84
+ /** Claude Code `--effort` levels, as accepted by CLI v2.1.x. */
85
+ declare const EFFORT_LEVELS: readonly ["low", "medium", "high", "xhigh", "max"];
86
+ type EffortLevel = (typeof EFFORT_LEVELS)[number];
84
87
  type FlagValueType = "boolean" | "string" | "enum" | "list";
85
88
  /** How risky enabling a flag is. Drives the client's confirmation UX. */
86
89
  type FlagRisk = "low" | "elevated" | "dangerous";
@@ -102,6 +105,17 @@ interface FlagDefinition {
102
105
  type ClaudeFlagValue = string | string[] | boolean;
103
106
  type ClaudeFlagValues = Record<string, ClaudeFlagValue>;
104
107
 
108
+ interface FeatureFlagDefinition {
109
+ /** Stable config/wire key. Used in server.yaml, on the CLI, and over HTTP. */
110
+ id: string;
111
+ /** Shipped to clients alongside the values so a UI can render it. */
112
+ description: string;
113
+ default: boolean;
114
+ /** Full env var name. */
115
+ env: string;
116
+ }
117
+ type FeatureFlagValues = Record<string, boolean>;
118
+
105
119
  declare const CLAUDE_CODE_PROVIDER: "claude-code";
106
120
  declare const CODEX_CLI_PROVIDER: "codex-cli";
107
121
  type ProviderName = typeof CLAUDE_CODE_PROVIDER | typeof CODEX_CLI_PROVIDER;
@@ -234,6 +248,7 @@ type WSMessage = {
234
248
  type: "terminal_output";
235
249
  sessionId: string;
236
250
  data: string;
251
+ seq?: number;
237
252
  } | {
238
253
  type: "session_update";
239
254
  session?: SessionResponse;
@@ -292,6 +307,7 @@ type WSMessage = {
292
307
  sessionId: string;
293
308
  lines: string[];
294
309
  userMessages?: UserMessage[];
310
+ seq?: number;
295
311
  } | {
296
312
  type: "session_ready";
297
313
  session: SessionResponse;
@@ -482,14 +498,17 @@ interface ServerConfig {
482
498
  }>;
483
499
  codexRoots?: string[];
484
500
  scannerPersistent?: boolean;
501
+ skipStartupWarmup?: boolean;
485
502
  ptyGracePeriodMs?: number;
486
503
  cacheDir?: string;
487
504
  tailSize?: number;
488
505
  directoryScanDebounceMs?: number;
489
506
  defaultSystemPrompt?: string;
507
+ codexSystemPromptEnabled?: boolean;
508
+ featureFlags?: FeatureFlagValues;
490
509
  defaultPermissionMode?: PermissionMode;
491
510
  defaultModel?: string;
492
- defaultEffort?: "low" | "medium" | "high" | "xhigh" | "max";
511
+ defaultEffort?: EffortLevel;
493
512
  claudeFlags?: ClaudeFlagValues;
494
513
  claudeExtraArgs?: string;
495
514
  }
@@ -514,7 +533,7 @@ interface StartSessionOptions {
514
533
  branch?: string;
515
534
  permissionMode?: PermissionMode;
516
535
  model?: string;
517
- effort?: "low" | "medium" | "high" | "xhigh" | "max";
536
+ effort?: EffortLevel;
518
537
  claudeFlags?: ClaudeFlagValues;
519
538
  claudeExtraArgs?: string;
520
539
  }
@@ -524,7 +543,7 @@ interface StartFreshSessionOptions {
524
543
  systemPrompt?: string;
525
544
  permissionMode?: PermissionMode;
526
545
  model?: string;
527
- effort?: "low" | "medium" | "high" | "xhigh" | "max";
546
+ effort?: EffortLevel;
528
547
  claudeFlags?: ClaudeFlagValues;
529
548
  claudeExtraArgs?: string;
530
549
  }
@@ -1045,6 +1064,18 @@ declare class ProjectsRepository {
1045
1064
  upsertProjectByPath(rawPath: string, input?: UpsertProjectInput): Project;
1046
1065
  }
1047
1066
 
1067
+ /**
1068
+ * Token kinds. A device supplies three non-interchangeable types, and
1069
+ * conflating them fails only at send time with no signal at registration:
1070
+ *
1071
+ * - `expo` — Expo relay token, for ordinary push notifications.
1072
+ * - `liveactivity_start` — ActivityKit push-to-start token. App-wide, one per
1073
+ * device, long-lived. Starts an activity when none exists.
1074
+ * - `liveactivity_update` — ActivityKit per-activity update token, issued by
1075
+ * iOS after an activity starts, scoped to that one activity, short-lived.
1076
+ */
1077
+ declare const PUSH_TOKEN_KINDS: readonly ["expo", "liveactivity_start", "liveactivity_update"];
1078
+ type PushTokenKind = (typeof PUSH_TOKEN_KINDS)[number];
1048
1079
  interface PushTokenRow {
1049
1080
  token: string;
1050
1081
  platform: string;
@@ -1055,6 +1086,13 @@ interface PushTokenRow {
1055
1086
  last_failure_code: string | null;
1056
1087
  failure_streak: number;
1057
1088
  revoked_at: number | null;
1089
+ kind: PushTokenKind;
1090
+ activity_id: string | null;
1091
+ session_id: string | null;
1092
+ expires_at: number | null;
1093
+ stale_date: number | null;
1094
+ started_at: number | null;
1095
+ renewed_at: number | null;
1058
1096
  }
1059
1097
  /**
1060
1098
  * Health as reported to a client. Deliberately omits the token itself — a push
@@ -1071,10 +1109,16 @@ interface PushTokenHealth {
1071
1109
  failureStreak: number;
1072
1110
  revokedAt: number | null;
1073
1111
  /**
1074
- * Never delivered vs delivering vs failing vs revoked. The distinction the
1075
- * user actually needs: "not yet" and "broken" look identical without it.
1112
+ * Never delivered vs delivering vs failing vs revoked vs expired. The
1113
+ * distinction the user actually needs: "not yet" and "broken" look identical
1114
+ * without it.
1076
1115
  */
1077
- state: "never-delivered" | "healthy" | "failing" | "dead" | "revoked";
1116
+ state: "never-delivered" | "healthy" | "failing" | "dead" | "revoked" | "expired";
1117
+ kind: PushTokenKind;
1118
+ /** Present only for per-activity Live Activity tokens. */
1119
+ activityId: string | null;
1120
+ sessionId: string | null;
1121
+ expiresAt: number | null;
1078
1122
  }
1079
1123
  declare class PushRepository {
1080
1124
  private upsertStmt;
@@ -1086,18 +1130,78 @@ declare class PushRepository {
1086
1130
  private revokeStmt;
1087
1131
  private claimEventStmt;
1088
1132
  private markDeliveredStmt;
1133
+ private listByKindSessionStmt;
1134
+ private listByKindStmt;
1135
+ private listRenewableStmt;
1136
+ private claimRenewalStmt;
1137
+ private expireStmt;
1138
+ private expireSessionActivitiesStmt;
1089
1139
  constructor(db: Database.Database);
1140
+ /**
1141
+ * Register or refresh a token.
1142
+ *
1143
+ * `kind` defaults to Expo so a released client posting `{ token, platform }`
1144
+ * keeps working — tb-mobile cannot be force-updated, and every client
1145
+ * predating Live Activities is registering an Expo relay token.
1146
+ *
1147
+ * Several rows per device is normal and intended: a device runs one activity
1148
+ * per live session, each with its own update token. The token itself is the
1149
+ * primary key, so distinct activities never collide.
1150
+ */
1090
1151
  register(args: {
1091
1152
  token: string;
1092
1153
  platform: string;
1093
1154
  deviceId?: string | null;
1155
+ kind?: PushTokenKind;
1156
+ activityId?: string | null;
1157
+ sessionId?: string | null;
1158
+ expiresAt?: number | null;
1159
+ staleDate?: number | null;
1160
+ startedAt?: number | null;
1094
1161
  now?: number;
1095
1162
  }): void;
1096
1163
  get(token: string): PushTokenRow | null;
1097
- /** Tokens eligible for delivery — not revoked, not past the failure limit. */
1164
+ /**
1165
+ * Expo tokens eligible for delivery — not revoked, not past the failure limit.
1166
+ *
1167
+ * Deliberately Expo-only. ActivityKit tokens go over direct APNs with a
1168
+ * different topic and are rejected by Expo's relay, so the ordinary
1169
+ * notification fan-out must not see them.
1170
+ */
1098
1171
  listDeliverable(): PushTokenRow[];
1172
+ /** Live-activity tokens for one session, eligible for delivery. */
1173
+ listForSession(kind: PushTokenKind, sessionId: string, now?: number): PushTokenRow[];
1174
+ /**
1175
+ * Every deliverable token of one kind.
1176
+ *
1177
+ * Used for push-to-start, which is app-wide rather than session-scoped: the
1178
+ * activity does not exist yet, so there is no per-activity token to look up.
1179
+ */
1180
+ listByKind(kind: PushTokenKind, now?: number): PushTokenRow[];
1181
+ /** Unrenewed activities with a renewal deadline, soonest first. */
1182
+ listRenewable(): PushTokenRow[];
1183
+ /**
1184
+ * Claim a row for renewal.
1185
+ *
1186
+ * Returns true exactly once per row. A restart re-arms timers from the
1187
+ * persisted deadline, so the same renewal can be attempted twice; the loser
1188
+ * gets false and must not send. Doing this as a conditional UPDATE rather
1189
+ * than read-then-write avoids the race where both attempts observe
1190
+ * "not yet renewed".
1191
+ */
1192
+ claimRenewal(token: string, now?: number): boolean;
1193
+ /** Mark one token expired, so it stops being a delivery target. */
1194
+ expire(token: string, now?: number): void;
1195
+ /**
1196
+ * Expire every live activity for a session.
1197
+ *
1198
+ * Called when the session ends. Without this, a per-activity token outlives
1199
+ * its session and a later renewal sweep would resurrect an activity for a
1200
+ * session that is already gone.
1201
+ */
1202
+ expireSessionActivities(sessionId: string, now?: number): void;
1099
1203
  /** Every token, including dead and revoked ones, for the health report. */
1100
- listHealth(): PushTokenHealth[];
1204
+ listHealth(now?: number): PushTokenHealth[];
1101
1205
  recordSuccess(token: string, now?: number): void;
1102
1206
  recordFailure(token: string, code: string, now?: number): void;
1103
1207
  revoke(token: string, now?: number): boolean;
@@ -1179,6 +1283,7 @@ declare class WSHub {
1179
1283
  private pongTimers;
1180
1284
  addClient(ws: WebSocket): void;
1181
1285
  broadcast(message: WSMessage): void;
1286
+ broadcastToClients(clients: Iterable<WebSocket>, message: WSMessage): void;
1182
1287
  unicast(ws: WebSocket, message: WSMessage): void;
1183
1288
  get connectionCount(): number;
1184
1289
  dispose(): void;
@@ -1288,6 +1393,10 @@ type ApiDeps = {
1288
1393
  extraArgs: string | null;
1289
1394
  persisted: boolean;
1290
1395
  };
1396
+ featureFlagsConfig: () => {
1397
+ registry: readonly FeatureFlagDefinition[];
1398
+ values: FeatureFlagValues;
1399
+ };
1291
1400
  publicUrl: string | null;
1292
1401
  browseRoot: string | null;
1293
1402
  browserCors: string | undefined;
@@ -1316,6 +1425,8 @@ type ApiDeps = {
1316
1425
  handleCancel: (sessionId: string, res: ServerResponse) => void;
1317
1426
  handleStopSession: (sessionId: string, res: ServerResponse) => Promise<void>;
1318
1427
  handleSetSessionName: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1428
+ handleSetSessionModel: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1429
+ handleSetSessionEffort: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1319
1430
  handleUploadFile: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1320
1431
  handleAdopt: (sessionId: string, res: ServerResponse) => Promise<void>;
1321
1432
  handleResume: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
@@ -1486,6 +1597,7 @@ declare class StreamerServer {
1486
1597
  private dbPool;
1487
1598
  private dbInstanceId;
1488
1599
  private disableDb;
1600
+ private skipStartupWarmup;
1489
1601
  private browseRoot;
1490
1602
  private publicUrl;
1491
1603
  private browserCors;
@@ -1495,6 +1607,8 @@ declare class StreamerServer {
1495
1607
  private sessionInputAttempts;
1496
1608
  private ptyGracePeriodMs;
1497
1609
  private defaultSystemPrompt;
1610
+ private featureFlags;
1611
+ private codexSystemPromptEnabled;
1498
1612
  private defaultPermissionMode;
1499
1613
  private defaultModel;
1500
1614
  private defaultEffort;
@@ -1505,6 +1619,7 @@ declare class StreamerServer {
1505
1619
  private ptyGraceDeferCounts;
1506
1620
  private sessionSubscribers;
1507
1621
  private lastAgentChunkAt;
1622
+ private terminalSeq;
1508
1623
  private idempotency;
1509
1624
  private sessionLifecycles;
1510
1625
  private idleReaperTimer;
@@ -1520,6 +1635,9 @@ declare class StreamerServer {
1520
1635
  private cacheMetadataRepo;
1521
1636
  private pushRepo;
1522
1637
  private devicesRepo;
1638
+ private apnsClient;
1639
+ private liveActivityNotifier;
1640
+ private liveActivityRenewal;
1523
1641
  private discoveryCache;
1524
1642
  private cacheDir;
1525
1643
  private tailSize;
@@ -1560,6 +1678,17 @@ declare class StreamerServer {
1560
1678
  */
1561
1679
  private withReconciledLifecycle;
1562
1680
  private addSessionSubscriber;
1681
+ /**
1682
+ * Bring up Live Activity push, if credentials are present (Feature 12).
1683
+ *
1684
+ * APNS_KEY absent is the ordinary case on a dev machine and in CI, so this
1685
+ * logs once at info and leaves the feature off rather than failing: the server
1686
+ * must not refuse to boot over a missing optional push credential.
1687
+ *
1688
+ * The key is read from the environment as PEM contents and never from a path
1689
+ * on disk; neither it nor any device token is ever logged.
1690
+ */
1691
+ private initLiveActivityPush;
1563
1692
  /**
1564
1693
  * Classify sessions left behind by previous streamer runs (C1 Phase 3a).
1565
1694
  *
@@ -1643,6 +1772,14 @@ declare class StreamerServer {
1643
1772
  private handlePairStart;
1644
1773
  private handlePairExchange;
1645
1774
  private rotateApiKey;
1775
+ /**
1776
+ * The registry ships with the values so a client renders the list from one
1777
+ * round-trip, same as getClaudeFlagsConfig().
1778
+ *
1779
+ * Deliberately no `persisted` field: unlike claude-flags there is no PUT, and
1780
+ * the absence of that field is the signal that this endpoint is read-only.
1781
+ */
1782
+ private getFeatureFlagsConfig;
1646
1783
  private getClaudeFlagsConfig;
1647
1784
  /**
1648
1785
  * Replace the per-server flag set. Applies to the NEXT spawn — a live PTY
@@ -1656,6 +1793,21 @@ declare class StreamerServer {
1656
1793
  * permission prompts entirely, so it needs a forensic trail.
1657
1794
  */
1658
1795
  private setClaudeFlagsConfig;
1796
+ /**
1797
+ * The three spawn options that a configured claude-flag can override, with
1798
+ * the boot-time CLI/yaml default as the fallback. Spread into every
1799
+ * start/resume/adopt call so all three paths agree.
1800
+ *
1801
+ * These ids are excluded from buildFlagArgs (SPAWN_POSITIONAL_FLAG_IDS)
1802
+ * precisely because they arrive here instead — the PTY spawn paths pass them
1803
+ * as explicit positionals, so emitting them from the allowlist too would
1804
+ * duplicate the flag.
1805
+ *
1806
+ * Narrowed with the type guards rather than cast: ClaudeFlagValues is a loose
1807
+ * Record by design, and while validateFlagValues already guarantees the shape
1808
+ * on the way in, TypeScript cannot see that through the record.
1809
+ */
1810
+ private spawnFlagOverrides;
1659
1811
  private checkRateLimit;
1660
1812
  private checkExchangeRateLimit;
1661
1813
  private checkSessionStartRateLimit;
@@ -1776,6 +1928,21 @@ declare class StreamerServer {
1776
1928
  private handleBrowse;
1777
1929
  private handleMkdir;
1778
1930
  private handleSetSessionName;
1931
+ /**
1932
+ * Retarget a LIVE session's model or effort by typing the corresponding
1933
+ * Claude Code slash command into its PTY.
1934
+ *
1935
+ * There is no CLI or IPC channel for this — `--model`/`--effort` are spawn
1936
+ * arguments — so the interactive `/model <x>` / `/effort <y>` commands are the
1937
+ * only way to change a session already running. Both accept an argument and
1938
+ * apply it without opening the picker (verified against Claude Code v2.1.220).
1939
+ *
1940
+ * Answers 202, not 200: the value is applied by the TUI on its next render, so
1941
+ * there is nothing truthful to echo back synchronously. Clients confirm with
1942
+ * `GET /api/sessions/:id`, which scrapes the applied value off the live status
1943
+ * line.
1944
+ */
1945
+ private applyLiveSessionSetting;
1779
1946
  private handleGetSessionNames;
1780
1947
  }
1781
1948
 
package/dist/index.d.ts CHANGED
@@ -81,6 +81,9 @@ interface Logger {
81
81
  /** Claude Code `--permission-mode` values, as accepted by CLI v2.1.x. */
82
82
  declare const PERMISSION_MODES: readonly ["acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan"];
83
83
  type PermissionMode = (typeof PERMISSION_MODES)[number];
84
+ /** Claude Code `--effort` levels, as accepted by CLI v2.1.x. */
85
+ declare const EFFORT_LEVELS: readonly ["low", "medium", "high", "xhigh", "max"];
86
+ type EffortLevel = (typeof EFFORT_LEVELS)[number];
84
87
  type FlagValueType = "boolean" | "string" | "enum" | "list";
85
88
  /** How risky enabling a flag is. Drives the client's confirmation UX. */
86
89
  type FlagRisk = "low" | "elevated" | "dangerous";
@@ -102,6 +105,17 @@ interface FlagDefinition {
102
105
  type ClaudeFlagValue = string | string[] | boolean;
103
106
  type ClaudeFlagValues = Record<string, ClaudeFlagValue>;
104
107
 
108
+ interface FeatureFlagDefinition {
109
+ /** Stable config/wire key. Used in server.yaml, on the CLI, and over HTTP. */
110
+ id: string;
111
+ /** Shipped to clients alongside the values so a UI can render it. */
112
+ description: string;
113
+ default: boolean;
114
+ /** Full env var name. */
115
+ env: string;
116
+ }
117
+ type FeatureFlagValues = Record<string, boolean>;
118
+
105
119
  declare const CLAUDE_CODE_PROVIDER: "claude-code";
106
120
  declare const CODEX_CLI_PROVIDER: "codex-cli";
107
121
  type ProviderName = typeof CLAUDE_CODE_PROVIDER | typeof CODEX_CLI_PROVIDER;
@@ -234,6 +248,7 @@ type WSMessage = {
234
248
  type: "terminal_output";
235
249
  sessionId: string;
236
250
  data: string;
251
+ seq?: number;
237
252
  } | {
238
253
  type: "session_update";
239
254
  session?: SessionResponse;
@@ -292,6 +307,7 @@ type WSMessage = {
292
307
  sessionId: string;
293
308
  lines: string[];
294
309
  userMessages?: UserMessage[];
310
+ seq?: number;
295
311
  } | {
296
312
  type: "session_ready";
297
313
  session: SessionResponse;
@@ -482,14 +498,17 @@ interface ServerConfig {
482
498
  }>;
483
499
  codexRoots?: string[];
484
500
  scannerPersistent?: boolean;
501
+ skipStartupWarmup?: boolean;
485
502
  ptyGracePeriodMs?: number;
486
503
  cacheDir?: string;
487
504
  tailSize?: number;
488
505
  directoryScanDebounceMs?: number;
489
506
  defaultSystemPrompt?: string;
507
+ codexSystemPromptEnabled?: boolean;
508
+ featureFlags?: FeatureFlagValues;
490
509
  defaultPermissionMode?: PermissionMode;
491
510
  defaultModel?: string;
492
- defaultEffort?: "low" | "medium" | "high" | "xhigh" | "max";
511
+ defaultEffort?: EffortLevel;
493
512
  claudeFlags?: ClaudeFlagValues;
494
513
  claudeExtraArgs?: string;
495
514
  }
@@ -514,7 +533,7 @@ interface StartSessionOptions {
514
533
  branch?: string;
515
534
  permissionMode?: PermissionMode;
516
535
  model?: string;
517
- effort?: "low" | "medium" | "high" | "xhigh" | "max";
536
+ effort?: EffortLevel;
518
537
  claudeFlags?: ClaudeFlagValues;
519
538
  claudeExtraArgs?: string;
520
539
  }
@@ -524,7 +543,7 @@ interface StartFreshSessionOptions {
524
543
  systemPrompt?: string;
525
544
  permissionMode?: PermissionMode;
526
545
  model?: string;
527
- effort?: "low" | "medium" | "high" | "xhigh" | "max";
546
+ effort?: EffortLevel;
528
547
  claudeFlags?: ClaudeFlagValues;
529
548
  claudeExtraArgs?: string;
530
549
  }
@@ -1045,6 +1064,18 @@ declare class ProjectsRepository {
1045
1064
  upsertProjectByPath(rawPath: string, input?: UpsertProjectInput): Project;
1046
1065
  }
1047
1066
 
1067
+ /**
1068
+ * Token kinds. A device supplies three non-interchangeable types, and
1069
+ * conflating them fails only at send time with no signal at registration:
1070
+ *
1071
+ * - `expo` — Expo relay token, for ordinary push notifications.
1072
+ * - `liveactivity_start` — ActivityKit push-to-start token. App-wide, one per
1073
+ * device, long-lived. Starts an activity when none exists.
1074
+ * - `liveactivity_update` — ActivityKit per-activity update token, issued by
1075
+ * iOS after an activity starts, scoped to that one activity, short-lived.
1076
+ */
1077
+ declare const PUSH_TOKEN_KINDS: readonly ["expo", "liveactivity_start", "liveactivity_update"];
1078
+ type PushTokenKind = (typeof PUSH_TOKEN_KINDS)[number];
1048
1079
  interface PushTokenRow {
1049
1080
  token: string;
1050
1081
  platform: string;
@@ -1055,6 +1086,13 @@ interface PushTokenRow {
1055
1086
  last_failure_code: string | null;
1056
1087
  failure_streak: number;
1057
1088
  revoked_at: number | null;
1089
+ kind: PushTokenKind;
1090
+ activity_id: string | null;
1091
+ session_id: string | null;
1092
+ expires_at: number | null;
1093
+ stale_date: number | null;
1094
+ started_at: number | null;
1095
+ renewed_at: number | null;
1058
1096
  }
1059
1097
  /**
1060
1098
  * Health as reported to a client. Deliberately omits the token itself — a push
@@ -1071,10 +1109,16 @@ interface PushTokenHealth {
1071
1109
  failureStreak: number;
1072
1110
  revokedAt: number | null;
1073
1111
  /**
1074
- * Never delivered vs delivering vs failing vs revoked. The distinction the
1075
- * user actually needs: "not yet" and "broken" look identical without it.
1112
+ * Never delivered vs delivering vs failing vs revoked vs expired. The
1113
+ * distinction the user actually needs: "not yet" and "broken" look identical
1114
+ * without it.
1076
1115
  */
1077
- state: "never-delivered" | "healthy" | "failing" | "dead" | "revoked";
1116
+ state: "never-delivered" | "healthy" | "failing" | "dead" | "revoked" | "expired";
1117
+ kind: PushTokenKind;
1118
+ /** Present only for per-activity Live Activity tokens. */
1119
+ activityId: string | null;
1120
+ sessionId: string | null;
1121
+ expiresAt: number | null;
1078
1122
  }
1079
1123
  declare class PushRepository {
1080
1124
  private upsertStmt;
@@ -1086,18 +1130,78 @@ declare class PushRepository {
1086
1130
  private revokeStmt;
1087
1131
  private claimEventStmt;
1088
1132
  private markDeliveredStmt;
1133
+ private listByKindSessionStmt;
1134
+ private listByKindStmt;
1135
+ private listRenewableStmt;
1136
+ private claimRenewalStmt;
1137
+ private expireStmt;
1138
+ private expireSessionActivitiesStmt;
1089
1139
  constructor(db: Database.Database);
1140
+ /**
1141
+ * Register or refresh a token.
1142
+ *
1143
+ * `kind` defaults to Expo so a released client posting `{ token, platform }`
1144
+ * keeps working — tb-mobile cannot be force-updated, and every client
1145
+ * predating Live Activities is registering an Expo relay token.
1146
+ *
1147
+ * Several rows per device is normal and intended: a device runs one activity
1148
+ * per live session, each with its own update token. The token itself is the
1149
+ * primary key, so distinct activities never collide.
1150
+ */
1090
1151
  register(args: {
1091
1152
  token: string;
1092
1153
  platform: string;
1093
1154
  deviceId?: string | null;
1155
+ kind?: PushTokenKind;
1156
+ activityId?: string | null;
1157
+ sessionId?: string | null;
1158
+ expiresAt?: number | null;
1159
+ staleDate?: number | null;
1160
+ startedAt?: number | null;
1094
1161
  now?: number;
1095
1162
  }): void;
1096
1163
  get(token: string): PushTokenRow | null;
1097
- /** Tokens eligible for delivery — not revoked, not past the failure limit. */
1164
+ /**
1165
+ * Expo tokens eligible for delivery — not revoked, not past the failure limit.
1166
+ *
1167
+ * Deliberately Expo-only. ActivityKit tokens go over direct APNs with a
1168
+ * different topic and are rejected by Expo's relay, so the ordinary
1169
+ * notification fan-out must not see them.
1170
+ */
1098
1171
  listDeliverable(): PushTokenRow[];
1172
+ /** Live-activity tokens for one session, eligible for delivery. */
1173
+ listForSession(kind: PushTokenKind, sessionId: string, now?: number): PushTokenRow[];
1174
+ /**
1175
+ * Every deliverable token of one kind.
1176
+ *
1177
+ * Used for push-to-start, which is app-wide rather than session-scoped: the
1178
+ * activity does not exist yet, so there is no per-activity token to look up.
1179
+ */
1180
+ listByKind(kind: PushTokenKind, now?: number): PushTokenRow[];
1181
+ /** Unrenewed activities with a renewal deadline, soonest first. */
1182
+ listRenewable(): PushTokenRow[];
1183
+ /**
1184
+ * Claim a row for renewal.
1185
+ *
1186
+ * Returns true exactly once per row. A restart re-arms timers from the
1187
+ * persisted deadline, so the same renewal can be attempted twice; the loser
1188
+ * gets false and must not send. Doing this as a conditional UPDATE rather
1189
+ * than read-then-write avoids the race where both attempts observe
1190
+ * "not yet renewed".
1191
+ */
1192
+ claimRenewal(token: string, now?: number): boolean;
1193
+ /** Mark one token expired, so it stops being a delivery target. */
1194
+ expire(token: string, now?: number): void;
1195
+ /**
1196
+ * Expire every live activity for a session.
1197
+ *
1198
+ * Called when the session ends. Without this, a per-activity token outlives
1199
+ * its session and a later renewal sweep would resurrect an activity for a
1200
+ * session that is already gone.
1201
+ */
1202
+ expireSessionActivities(sessionId: string, now?: number): void;
1099
1203
  /** Every token, including dead and revoked ones, for the health report. */
1100
- listHealth(): PushTokenHealth[];
1204
+ listHealth(now?: number): PushTokenHealth[];
1101
1205
  recordSuccess(token: string, now?: number): void;
1102
1206
  recordFailure(token: string, code: string, now?: number): void;
1103
1207
  revoke(token: string, now?: number): boolean;
@@ -1179,6 +1283,7 @@ declare class WSHub {
1179
1283
  private pongTimers;
1180
1284
  addClient(ws: WebSocket): void;
1181
1285
  broadcast(message: WSMessage): void;
1286
+ broadcastToClients(clients: Iterable<WebSocket>, message: WSMessage): void;
1182
1287
  unicast(ws: WebSocket, message: WSMessage): void;
1183
1288
  get connectionCount(): number;
1184
1289
  dispose(): void;
@@ -1288,6 +1393,10 @@ type ApiDeps = {
1288
1393
  extraArgs: string | null;
1289
1394
  persisted: boolean;
1290
1395
  };
1396
+ featureFlagsConfig: () => {
1397
+ registry: readonly FeatureFlagDefinition[];
1398
+ values: FeatureFlagValues;
1399
+ };
1291
1400
  publicUrl: string | null;
1292
1401
  browseRoot: string | null;
1293
1402
  browserCors: string | undefined;
@@ -1316,6 +1425,8 @@ type ApiDeps = {
1316
1425
  handleCancel: (sessionId: string, res: ServerResponse) => void;
1317
1426
  handleStopSession: (sessionId: string, res: ServerResponse) => Promise<void>;
1318
1427
  handleSetSessionName: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1428
+ handleSetSessionModel: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1429
+ handleSetSessionEffort: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1319
1430
  handleUploadFile: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
1320
1431
  handleAdopt: (sessionId: string, res: ServerResponse) => Promise<void>;
1321
1432
  handleResume: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
@@ -1486,6 +1597,7 @@ declare class StreamerServer {
1486
1597
  private dbPool;
1487
1598
  private dbInstanceId;
1488
1599
  private disableDb;
1600
+ private skipStartupWarmup;
1489
1601
  private browseRoot;
1490
1602
  private publicUrl;
1491
1603
  private browserCors;
@@ -1495,6 +1607,8 @@ declare class StreamerServer {
1495
1607
  private sessionInputAttempts;
1496
1608
  private ptyGracePeriodMs;
1497
1609
  private defaultSystemPrompt;
1610
+ private featureFlags;
1611
+ private codexSystemPromptEnabled;
1498
1612
  private defaultPermissionMode;
1499
1613
  private defaultModel;
1500
1614
  private defaultEffort;
@@ -1505,6 +1619,7 @@ declare class StreamerServer {
1505
1619
  private ptyGraceDeferCounts;
1506
1620
  private sessionSubscribers;
1507
1621
  private lastAgentChunkAt;
1622
+ private terminalSeq;
1508
1623
  private idempotency;
1509
1624
  private sessionLifecycles;
1510
1625
  private idleReaperTimer;
@@ -1520,6 +1635,9 @@ declare class StreamerServer {
1520
1635
  private cacheMetadataRepo;
1521
1636
  private pushRepo;
1522
1637
  private devicesRepo;
1638
+ private apnsClient;
1639
+ private liveActivityNotifier;
1640
+ private liveActivityRenewal;
1523
1641
  private discoveryCache;
1524
1642
  private cacheDir;
1525
1643
  private tailSize;
@@ -1560,6 +1678,17 @@ declare class StreamerServer {
1560
1678
  */
1561
1679
  private withReconciledLifecycle;
1562
1680
  private addSessionSubscriber;
1681
+ /**
1682
+ * Bring up Live Activity push, if credentials are present (Feature 12).
1683
+ *
1684
+ * APNS_KEY absent is the ordinary case on a dev machine and in CI, so this
1685
+ * logs once at info and leaves the feature off rather than failing: the server
1686
+ * must not refuse to boot over a missing optional push credential.
1687
+ *
1688
+ * The key is read from the environment as PEM contents and never from a path
1689
+ * on disk; neither it nor any device token is ever logged.
1690
+ */
1691
+ private initLiveActivityPush;
1563
1692
  /**
1564
1693
  * Classify sessions left behind by previous streamer runs (C1 Phase 3a).
1565
1694
  *
@@ -1643,6 +1772,14 @@ declare class StreamerServer {
1643
1772
  private handlePairStart;
1644
1773
  private handlePairExchange;
1645
1774
  private rotateApiKey;
1775
+ /**
1776
+ * The registry ships with the values so a client renders the list from one
1777
+ * round-trip, same as getClaudeFlagsConfig().
1778
+ *
1779
+ * Deliberately no `persisted` field: unlike claude-flags there is no PUT, and
1780
+ * the absence of that field is the signal that this endpoint is read-only.
1781
+ */
1782
+ private getFeatureFlagsConfig;
1646
1783
  private getClaudeFlagsConfig;
1647
1784
  /**
1648
1785
  * Replace the per-server flag set. Applies to the NEXT spawn — a live PTY
@@ -1656,6 +1793,21 @@ declare class StreamerServer {
1656
1793
  * permission prompts entirely, so it needs a forensic trail.
1657
1794
  */
1658
1795
  private setClaudeFlagsConfig;
1796
+ /**
1797
+ * The three spawn options that a configured claude-flag can override, with
1798
+ * the boot-time CLI/yaml default as the fallback. Spread into every
1799
+ * start/resume/adopt call so all three paths agree.
1800
+ *
1801
+ * These ids are excluded from buildFlagArgs (SPAWN_POSITIONAL_FLAG_IDS)
1802
+ * precisely because they arrive here instead — the PTY spawn paths pass them
1803
+ * as explicit positionals, so emitting them from the allowlist too would
1804
+ * duplicate the flag.
1805
+ *
1806
+ * Narrowed with the type guards rather than cast: ClaudeFlagValues is a loose
1807
+ * Record by design, and while validateFlagValues already guarantees the shape
1808
+ * on the way in, TypeScript cannot see that through the record.
1809
+ */
1810
+ private spawnFlagOverrides;
1659
1811
  private checkRateLimit;
1660
1812
  private checkExchangeRateLimit;
1661
1813
  private checkSessionStartRateLimit;
@@ -1776,6 +1928,21 @@ declare class StreamerServer {
1776
1928
  private handleBrowse;
1777
1929
  private handleMkdir;
1778
1930
  private handleSetSessionName;
1931
+ /**
1932
+ * Retarget a LIVE session's model or effort by typing the corresponding
1933
+ * Claude Code slash command into its PTY.
1934
+ *
1935
+ * There is no CLI or IPC channel for this — `--model`/`--effort` are spawn
1936
+ * arguments — so the interactive `/model <x>` / `/effort <y>` commands are the
1937
+ * only way to change a session already running. Both accept an argument and
1938
+ * apply it without opening the picker (verified against Claude Code v2.1.220).
1939
+ *
1940
+ * Answers 202, not 200: the value is applied by the TUI on its next render, so
1941
+ * there is nothing truthful to echo back synchronously. Clients confirm with
1942
+ * `GET /api/sessions/:id`, which scrapes the applied value off the live status
1943
+ * line.
1944
+ */
1945
+ private applyLiveSessionSetting;
1779
1946
  private handleGetSessionNames;
1780
1947
  }
1781
1948