@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/cli.cjs +1487 -387
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +1223 -181
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +175 -8
- package/dist/index.d.ts +175 -8
- package/dist/index.js +1221 -179
- package/dist/index.js.map +1 -1
- package/dist/launchd-entry.cjs +113 -30
- package/dist/launchd-entry.cjs.map +1 -1
- package/dist/migrations/013_add_push_token_kind.sql +63 -0
- package/dist/migrations/014_add_conversation_meta_file_path_index.sql +5 -0
- package/dist/pg-migrations/007_create_push_tokens.sql +93 -0
- package/package.json +1 -1
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?:
|
|
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?:
|
|
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?:
|
|
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
|
|
1075
|
-
* user actually needs: "not yet" and "broken" look identical
|
|
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
|
-
/**
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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
|
|
1075
|
-
* user actually needs: "not yet" and "broken" look identical
|
|
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
|
-
/**
|
|
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
|
|