@threadbase-sh/streamer 1.38.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 +183 -19
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +180 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -3
- package/dist/index.d.ts +44 -3
- package/dist/index.js +180 -15
- package/dist/index.js.map +1 -1
- package/dist/migrations/014_add_conversation_meta_file_path_index.sql +5 -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";
|
|
@@ -245,6 +248,7 @@ type WSMessage = {
|
|
|
245
248
|
type: "terminal_output";
|
|
246
249
|
sessionId: string;
|
|
247
250
|
data: string;
|
|
251
|
+
seq?: number;
|
|
248
252
|
} | {
|
|
249
253
|
type: "session_update";
|
|
250
254
|
session?: SessionResponse;
|
|
@@ -303,6 +307,7 @@ type WSMessage = {
|
|
|
303
307
|
sessionId: string;
|
|
304
308
|
lines: string[];
|
|
305
309
|
userMessages?: UserMessage[];
|
|
310
|
+
seq?: number;
|
|
306
311
|
} | {
|
|
307
312
|
type: "session_ready";
|
|
308
313
|
session: SessionResponse;
|
|
@@ -493,6 +498,7 @@ interface ServerConfig {
|
|
|
493
498
|
}>;
|
|
494
499
|
codexRoots?: string[];
|
|
495
500
|
scannerPersistent?: boolean;
|
|
501
|
+
skipStartupWarmup?: boolean;
|
|
496
502
|
ptyGracePeriodMs?: number;
|
|
497
503
|
cacheDir?: string;
|
|
498
504
|
tailSize?: number;
|
|
@@ -502,7 +508,7 @@ interface ServerConfig {
|
|
|
502
508
|
featureFlags?: FeatureFlagValues;
|
|
503
509
|
defaultPermissionMode?: PermissionMode;
|
|
504
510
|
defaultModel?: string;
|
|
505
|
-
defaultEffort?:
|
|
511
|
+
defaultEffort?: EffortLevel;
|
|
506
512
|
claudeFlags?: ClaudeFlagValues;
|
|
507
513
|
claudeExtraArgs?: string;
|
|
508
514
|
}
|
|
@@ -527,7 +533,7 @@ interface StartSessionOptions {
|
|
|
527
533
|
branch?: string;
|
|
528
534
|
permissionMode?: PermissionMode;
|
|
529
535
|
model?: string;
|
|
530
|
-
effort?:
|
|
536
|
+
effort?: EffortLevel;
|
|
531
537
|
claudeFlags?: ClaudeFlagValues;
|
|
532
538
|
claudeExtraArgs?: string;
|
|
533
539
|
}
|
|
@@ -537,7 +543,7 @@ interface StartFreshSessionOptions {
|
|
|
537
543
|
systemPrompt?: string;
|
|
538
544
|
permissionMode?: PermissionMode;
|
|
539
545
|
model?: string;
|
|
540
|
-
effort?:
|
|
546
|
+
effort?: EffortLevel;
|
|
541
547
|
claudeFlags?: ClaudeFlagValues;
|
|
542
548
|
claudeExtraArgs?: string;
|
|
543
549
|
}
|
|
@@ -1277,6 +1283,7 @@ declare class WSHub {
|
|
|
1277
1283
|
private pongTimers;
|
|
1278
1284
|
addClient(ws: WebSocket): void;
|
|
1279
1285
|
broadcast(message: WSMessage): void;
|
|
1286
|
+
broadcastToClients(clients: Iterable<WebSocket>, message: WSMessage): void;
|
|
1280
1287
|
unicast(ws: WebSocket, message: WSMessage): void;
|
|
1281
1288
|
get connectionCount(): number;
|
|
1282
1289
|
dispose(): void;
|
|
@@ -1418,6 +1425,8 @@ type ApiDeps = {
|
|
|
1418
1425
|
handleCancel: (sessionId: string, res: ServerResponse) => void;
|
|
1419
1426
|
handleStopSession: (sessionId: string, res: ServerResponse) => Promise<void>;
|
|
1420
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>;
|
|
1421
1430
|
handleUploadFile: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
1422
1431
|
handleAdopt: (sessionId: string, res: ServerResponse) => Promise<void>;
|
|
1423
1432
|
handleResume: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
@@ -1588,6 +1597,7 @@ declare class StreamerServer {
|
|
|
1588
1597
|
private dbPool;
|
|
1589
1598
|
private dbInstanceId;
|
|
1590
1599
|
private disableDb;
|
|
1600
|
+
private skipStartupWarmup;
|
|
1591
1601
|
private browseRoot;
|
|
1592
1602
|
private publicUrl;
|
|
1593
1603
|
private browserCors;
|
|
@@ -1609,6 +1619,7 @@ declare class StreamerServer {
|
|
|
1609
1619
|
private ptyGraceDeferCounts;
|
|
1610
1620
|
private sessionSubscribers;
|
|
1611
1621
|
private lastAgentChunkAt;
|
|
1622
|
+
private terminalSeq;
|
|
1612
1623
|
private idempotency;
|
|
1613
1624
|
private sessionLifecycles;
|
|
1614
1625
|
private idleReaperTimer;
|
|
@@ -1782,6 +1793,21 @@ declare class StreamerServer {
|
|
|
1782
1793
|
* permission prompts entirely, so it needs a forensic trail.
|
|
1783
1794
|
*/
|
|
1784
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;
|
|
1785
1811
|
private checkRateLimit;
|
|
1786
1812
|
private checkExchangeRateLimit;
|
|
1787
1813
|
private checkSessionStartRateLimit;
|
|
@@ -1902,6 +1928,21 @@ declare class StreamerServer {
|
|
|
1902
1928
|
private handleBrowse;
|
|
1903
1929
|
private handleMkdir;
|
|
1904
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;
|
|
1905
1946
|
private handleGetSessionNames;
|
|
1906
1947
|
}
|
|
1907
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";
|
|
@@ -245,6 +248,7 @@ type WSMessage = {
|
|
|
245
248
|
type: "terminal_output";
|
|
246
249
|
sessionId: string;
|
|
247
250
|
data: string;
|
|
251
|
+
seq?: number;
|
|
248
252
|
} | {
|
|
249
253
|
type: "session_update";
|
|
250
254
|
session?: SessionResponse;
|
|
@@ -303,6 +307,7 @@ type WSMessage = {
|
|
|
303
307
|
sessionId: string;
|
|
304
308
|
lines: string[];
|
|
305
309
|
userMessages?: UserMessage[];
|
|
310
|
+
seq?: number;
|
|
306
311
|
} | {
|
|
307
312
|
type: "session_ready";
|
|
308
313
|
session: SessionResponse;
|
|
@@ -493,6 +498,7 @@ interface ServerConfig {
|
|
|
493
498
|
}>;
|
|
494
499
|
codexRoots?: string[];
|
|
495
500
|
scannerPersistent?: boolean;
|
|
501
|
+
skipStartupWarmup?: boolean;
|
|
496
502
|
ptyGracePeriodMs?: number;
|
|
497
503
|
cacheDir?: string;
|
|
498
504
|
tailSize?: number;
|
|
@@ -502,7 +508,7 @@ interface ServerConfig {
|
|
|
502
508
|
featureFlags?: FeatureFlagValues;
|
|
503
509
|
defaultPermissionMode?: PermissionMode;
|
|
504
510
|
defaultModel?: string;
|
|
505
|
-
defaultEffort?:
|
|
511
|
+
defaultEffort?: EffortLevel;
|
|
506
512
|
claudeFlags?: ClaudeFlagValues;
|
|
507
513
|
claudeExtraArgs?: string;
|
|
508
514
|
}
|
|
@@ -527,7 +533,7 @@ interface StartSessionOptions {
|
|
|
527
533
|
branch?: string;
|
|
528
534
|
permissionMode?: PermissionMode;
|
|
529
535
|
model?: string;
|
|
530
|
-
effort?:
|
|
536
|
+
effort?: EffortLevel;
|
|
531
537
|
claudeFlags?: ClaudeFlagValues;
|
|
532
538
|
claudeExtraArgs?: string;
|
|
533
539
|
}
|
|
@@ -537,7 +543,7 @@ interface StartFreshSessionOptions {
|
|
|
537
543
|
systemPrompt?: string;
|
|
538
544
|
permissionMode?: PermissionMode;
|
|
539
545
|
model?: string;
|
|
540
|
-
effort?:
|
|
546
|
+
effort?: EffortLevel;
|
|
541
547
|
claudeFlags?: ClaudeFlagValues;
|
|
542
548
|
claudeExtraArgs?: string;
|
|
543
549
|
}
|
|
@@ -1277,6 +1283,7 @@ declare class WSHub {
|
|
|
1277
1283
|
private pongTimers;
|
|
1278
1284
|
addClient(ws: WebSocket): void;
|
|
1279
1285
|
broadcast(message: WSMessage): void;
|
|
1286
|
+
broadcastToClients(clients: Iterable<WebSocket>, message: WSMessage): void;
|
|
1280
1287
|
unicast(ws: WebSocket, message: WSMessage): void;
|
|
1281
1288
|
get connectionCount(): number;
|
|
1282
1289
|
dispose(): void;
|
|
@@ -1418,6 +1425,8 @@ type ApiDeps = {
|
|
|
1418
1425
|
handleCancel: (sessionId: string, res: ServerResponse) => void;
|
|
1419
1426
|
handleStopSession: (sessionId: string, res: ServerResponse) => Promise<void>;
|
|
1420
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>;
|
|
1421
1430
|
handleUploadFile: (sessionId: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
1422
1431
|
handleAdopt: (sessionId: string, res: ServerResponse) => Promise<void>;
|
|
1423
1432
|
handleResume: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
@@ -1588,6 +1597,7 @@ declare class StreamerServer {
|
|
|
1588
1597
|
private dbPool;
|
|
1589
1598
|
private dbInstanceId;
|
|
1590
1599
|
private disableDb;
|
|
1600
|
+
private skipStartupWarmup;
|
|
1591
1601
|
private browseRoot;
|
|
1592
1602
|
private publicUrl;
|
|
1593
1603
|
private browserCors;
|
|
@@ -1609,6 +1619,7 @@ declare class StreamerServer {
|
|
|
1609
1619
|
private ptyGraceDeferCounts;
|
|
1610
1620
|
private sessionSubscribers;
|
|
1611
1621
|
private lastAgentChunkAt;
|
|
1622
|
+
private terminalSeq;
|
|
1612
1623
|
private idempotency;
|
|
1613
1624
|
private sessionLifecycles;
|
|
1614
1625
|
private idleReaperTimer;
|
|
@@ -1782,6 +1793,21 @@ declare class StreamerServer {
|
|
|
1782
1793
|
* permission prompts entirely, so it needs a forensic trail.
|
|
1783
1794
|
*/
|
|
1784
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;
|
|
1785
1811
|
private checkRateLimit;
|
|
1786
1812
|
private checkExchangeRateLimit;
|
|
1787
1813
|
private checkSessionStartRateLimit;
|
|
@@ -1902,6 +1928,21 @@ declare class StreamerServer {
|
|
|
1902
1928
|
private handleBrowse;
|
|
1903
1929
|
private handleMkdir;
|
|
1904
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;
|
|
1905
1946
|
private handleGetSessionNames;
|
|
1906
1947
|
}
|
|
1907
1948
|
|
package/dist/index.js
CHANGED
|
@@ -298,6 +298,10 @@ var DANGEROUS_PERMISSION_MODES = [
|
|
|
298
298
|
function isDangerousPermissionMode(mode) {
|
|
299
299
|
return DANGEROUS_PERMISSION_MODES.includes(mode);
|
|
300
300
|
}
|
|
301
|
+
var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
302
|
+
function isEffortLevel(value) {
|
|
303
|
+
return typeof value === "string" && EFFORT_LEVELS.includes(value);
|
|
304
|
+
}
|
|
301
305
|
var CLAUDE_FLAGS = [
|
|
302
306
|
{
|
|
303
307
|
id: "permissionMode",
|
|
@@ -309,9 +313,10 @@ var CLAUDE_FLAGS = [
|
|
|
309
313
|
{ id: "addDir", flag: "--add-dir", valueType: "list", risk: "elevated" },
|
|
310
314
|
{ id: "allowedTools", flag: "--allowedTools", valueType: "list", risk: "elevated" },
|
|
311
315
|
{ id: "disallowedTools", flag: "--disallowedTools", valueType: "list", risk: "low" },
|
|
312
|
-
{ id: "
|
|
313
|
-
{ id: "
|
|
316
|
+
{ id: "model", flag: "--model", valueType: "string", risk: "low" },
|
|
317
|
+
{ id: "effort", flag: "--effort", valueType: "enum", enumValues: EFFORT_LEVELS, risk: "low" }
|
|
314
318
|
];
|
|
319
|
+
var SPAWN_POSITIONAL_FLAG_IDS = /* @__PURE__ */ new Set(["permissionMode", "model", "effort"]);
|
|
315
320
|
function findFlag(id) {
|
|
316
321
|
return CLAUDE_FLAGS.find((f) => f.id === id);
|
|
317
322
|
}
|
|
@@ -376,7 +381,7 @@ function buildFlagArgs(values, extraArgs) {
|
|
|
376
381
|
const args = [];
|
|
377
382
|
const safe = validateFlagValues(values ?? {});
|
|
378
383
|
for (const def of CLAUDE_FLAGS) {
|
|
379
|
-
if (def.id
|
|
384
|
+
if (SPAWN_POSITIONAL_FLAG_IDS.has(def.id)) continue;
|
|
380
385
|
const value = safe[def.id];
|
|
381
386
|
if (value === void 0) continue;
|
|
382
387
|
if (def.valueType === "boolean") {
|
|
@@ -4671,6 +4676,14 @@ var createSessionRoutes = (deps) => {
|
|
|
4671
4676
|
await deps.handleSetSessionName(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
4672
4677
|
return alreadyHandled6();
|
|
4673
4678
|
});
|
|
4679
|
+
app.patch("/:id/model", async (c) => {
|
|
4680
|
+
await deps.handleSetSessionModel(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
4681
|
+
return alreadyHandled6();
|
|
4682
|
+
});
|
|
4683
|
+
app.patch("/:id/effort", async (c) => {
|
|
4684
|
+
await deps.handleSetSessionEffort(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
4685
|
+
return alreadyHandled6();
|
|
4686
|
+
});
|
|
4674
4687
|
app.post("/:id/adopt", async (c) => {
|
|
4675
4688
|
await deps.handleAdopt(c.req.param("id"), c.env.outgoing);
|
|
4676
4689
|
return alreadyHandled6();
|
|
@@ -5008,6 +5021,7 @@ CREATE TABLE IF NOT EXISTS conversation_meta (
|
|
|
5008
5021
|
);
|
|
5009
5022
|
CREATE INDEX IF NOT EXISTS idx_meta_last_activity ON conversation_meta(last_activity DESC);
|
|
5010
5023
|
CREATE INDEX IF NOT EXISTS idx_meta_project ON conversation_meta(project_path);
|
|
5024
|
+
CREATE INDEX IF NOT EXISTS idx_meta_file_path ON conversation_meta(file_path);
|
|
5011
5025
|
|
|
5012
5026
|
CREATE TABLE IF NOT EXISTS conversation_tail (
|
|
5013
5027
|
conversation_id TEXT PRIMARY KEY REFERENCES conversation_meta(id) ON DELETE CASCADE,
|
|
@@ -8665,6 +8679,24 @@ var WSHub = class {
|
|
|
8665
8679
|
this.clients.delete(client);
|
|
8666
8680
|
}
|
|
8667
8681
|
}
|
|
8682
|
+
// Scoped broadcast for high-frequency per-session messages (terminal_output,
|
|
8683
|
+
// user_message). Sending to every connected client for every PTY output
|
|
8684
|
+
// chunk made broadcast() cost scale with connections x active sessions;
|
|
8685
|
+
// this bounds it to only that session's subscribers.
|
|
8686
|
+
broadcastToClients(clients, message) {
|
|
8687
|
+
const data = JSON.stringify(message);
|
|
8688
|
+
for (const client of clients) {
|
|
8689
|
+
try {
|
|
8690
|
+
if (client.readyState === client.OPEN) {
|
|
8691
|
+
client.send(data);
|
|
8692
|
+
} else {
|
|
8693
|
+
this.clients.delete(client);
|
|
8694
|
+
}
|
|
8695
|
+
} catch {
|
|
8696
|
+
this.clients.delete(client);
|
|
8697
|
+
}
|
|
8698
|
+
}
|
|
8699
|
+
}
|
|
8668
8700
|
unicast(ws, message) {
|
|
8669
8701
|
try {
|
|
8670
8702
|
if (ws.readyState === ws.OPEN) {
|
|
@@ -8727,6 +8759,7 @@ var ADOPT_KILL_TIMEOUT_MS = 5e3;
|
|
|
8727
8759
|
var ADOPT_KILL_POLL_MS = 100;
|
|
8728
8760
|
var REFRESH_TTL_MS = 2e3;
|
|
8729
8761
|
var START_READY_TIMEOUT_MS = 1e4;
|
|
8762
|
+
var MODEL_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
8730
8763
|
var EXTERNAL_TAIL_RECENCY_MS = RESUME_BUSY_WINDOW_MS;
|
|
8731
8764
|
var EXTERNAL_TAIL_MAX = 32;
|
|
8732
8765
|
var EXTERNAL_TAIL_IDLE_MS = 3e5;
|
|
@@ -8830,6 +8863,8 @@ var StreamerServer = class {
|
|
|
8830
8863
|
dbPool = null;
|
|
8831
8864
|
dbInstanceId = null;
|
|
8832
8865
|
disableDb = false;
|
|
8866
|
+
// Skip the startup warm-up scan (test hook; see ServerConfig.skipStartupWarmup).
|
|
8867
|
+
skipStartupWarmup;
|
|
8833
8868
|
browseRoot = null;
|
|
8834
8869
|
publicUrl = null;
|
|
8835
8870
|
browserCors;
|
|
@@ -8869,6 +8904,11 @@ var StreamerServer = class {
|
|
|
8869
8904
|
// every provider; read only by the idle reaper. Entries are dropped when the
|
|
8870
8905
|
// session leaves the runner (reap/exit/hold).
|
|
8871
8906
|
lastAgentChunkAt = /* @__PURE__ */ new Map();
|
|
8907
|
+
// sessionId → last terminal_output seq broadcast (starts at 1, per session).
|
|
8908
|
+
// Stamped on every terminal_output/terminal_replay so a client can detect a
|
|
8909
|
+
// stale chunk delivered after a reconnect race instead of trusting raw WS
|
|
8910
|
+
// arrival order. Entries dropped alongside lastAgentChunkAt.
|
|
8911
|
+
terminalSeq = /* @__PURE__ */ new Map();
|
|
8872
8912
|
// Recently accepted input idempotency keys (C4). A retried POST replays its
|
|
8873
8913
|
// original outcome instead of submitting the prompt to the agent twice.
|
|
8874
8914
|
idempotency = new IdempotencyStore();
|
|
@@ -8936,6 +8976,7 @@ var StreamerServer = class {
|
|
|
8936
8976
|
}
|
|
8937
8977
|
this.verbose = config.verbose ?? false;
|
|
8938
8978
|
this.disableDb = config.disableDb ?? false;
|
|
8979
|
+
this.skipStartupWarmup = config.skipStartupWarmup ?? false;
|
|
8939
8980
|
this.scannerPersistenceDisabled = config.scannerPersistent === false;
|
|
8940
8981
|
this.scanProfiles = config.scanProfiles;
|
|
8941
8982
|
this.codexRoots = config.codexRoots ?? [join18(homedir9(), ".codex", "sessions")];
|
|
@@ -9084,10 +9125,22 @@ var StreamerServer = class {
|
|
|
9084
9125
|
logger: getLogger("pty"),
|
|
9085
9126
|
onOutput: (sessionId, data) => {
|
|
9086
9127
|
this.lastAgentChunkAt.set(sessionId, Date.now());
|
|
9087
|
-
this.
|
|
9128
|
+
const seq = (this.terminalSeq.get(sessionId) ?? 0) + 1;
|
|
9129
|
+
this.terminalSeq.set(sessionId, seq);
|
|
9130
|
+
this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], {
|
|
9131
|
+
type: "terminal_output",
|
|
9132
|
+
sessionId,
|
|
9133
|
+
data,
|
|
9134
|
+
seq
|
|
9135
|
+
});
|
|
9088
9136
|
},
|
|
9089
9137
|
onUserMessage: (sessionId, text, ts) => {
|
|
9090
|
-
this.wsHub.
|
|
9138
|
+
this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], {
|
|
9139
|
+
type: "user_message",
|
|
9140
|
+
sessionId,
|
|
9141
|
+
text,
|
|
9142
|
+
ts
|
|
9143
|
+
});
|
|
9091
9144
|
},
|
|
9092
9145
|
onPermissionChange: (sessionId, gate) => {
|
|
9093
9146
|
this.handlePermissionChange(sessionId, gate);
|
|
@@ -9224,6 +9277,8 @@ var StreamerServer = class {
|
|
|
9224
9277
|
handleCancel: (id, res) => this.handleCancel(id, res),
|
|
9225
9278
|
handleStopSession: (id, res) => this.handleStopSession(id, res),
|
|
9226
9279
|
handleSetSessionName: (id, req, res) => this.handleSetSessionName(id, req, res),
|
|
9280
|
+
handleSetSessionModel: (id, req, res) => this.applyLiveSessionSetting(id, req, res, "model"),
|
|
9281
|
+
handleSetSessionEffort: (id, req, res) => this.applyLiveSessionSetting(id, req, res, "effort"),
|
|
9227
9282
|
handleUploadFile: (id, req, res) => this.handleUploadFile(id, req, res),
|
|
9228
9283
|
handleAdopt: (id, res) => this.handleAdopt(id, res),
|
|
9229
9284
|
handleResume: (req, res) => this.handleResume(req, res),
|
|
@@ -9270,7 +9325,8 @@ var StreamerServer = class {
|
|
|
9270
9325
|
type: "terminal_replay",
|
|
9271
9326
|
sessionId: msg.sessionId,
|
|
9272
9327
|
lines,
|
|
9273
|
-
userMessages
|
|
9328
|
+
userMessages,
|
|
9329
|
+
seq: this.terminalSeq.get(msg.sessionId)
|
|
9274
9330
|
})
|
|
9275
9331
|
);
|
|
9276
9332
|
}
|
|
@@ -9630,6 +9686,7 @@ var StreamerServer = class {
|
|
|
9630
9686
|
);
|
|
9631
9687
|
this.ptyManager.putOnHold(session.id);
|
|
9632
9688
|
this.lastAgentChunkAt.delete(session.id);
|
|
9689
|
+
this.terminalSeq.delete(session.id);
|
|
9633
9690
|
this.idempotency.clear(session.id);
|
|
9634
9691
|
this.sessionSubscribers.delete(session.id);
|
|
9635
9692
|
reaped.push(session.id);
|
|
@@ -9804,6 +9861,14 @@ var StreamerServer = class {
|
|
|
9804
9861
|
);
|
|
9805
9862
|
this.scannerPersistenceDisabled = true;
|
|
9806
9863
|
}
|
|
9864
|
+
if (this.skipStartupWarmup) {
|
|
9865
|
+
this.log.debug?.("startup warm-up scan skipped (skipStartupWarmup)", {
|
|
9866
|
+
event: "cache.warmup_skipped"
|
|
9867
|
+
});
|
|
9868
|
+
this.finishWarmup(0);
|
|
9869
|
+
resolveWarm();
|
|
9870
|
+
return;
|
|
9871
|
+
}
|
|
9807
9872
|
const warmupStatCache = this.buildStatCache(null);
|
|
9808
9873
|
const warmupScanner = this.newScanner(warmupStatCache ? { persistent: false } : void 0);
|
|
9809
9874
|
this.allScanners.add(warmupScanner);
|
|
@@ -9995,6 +10060,7 @@ var StreamerServer = class {
|
|
|
9995
10060
|
this.idleReaperTimer = null;
|
|
9996
10061
|
}
|
|
9997
10062
|
this.lastAgentChunkAt.clear();
|
|
10063
|
+
this.terminalSeq.clear();
|
|
9998
10064
|
this.recordShutdownState();
|
|
9999
10065
|
this.markScannerStaleDebounced.cancel();
|
|
10000
10066
|
await Promise.all([...this.inFlightCacheWrites]);
|
|
@@ -10176,6 +10242,30 @@ var StreamerServer = class {
|
|
|
10176
10242
|
persisted: this.claudeFlagsPersistable
|
|
10177
10243
|
};
|
|
10178
10244
|
}
|
|
10245
|
+
/**
|
|
10246
|
+
* The three spawn options that a configured claude-flag can override, with
|
|
10247
|
+
* the boot-time CLI/yaml default as the fallback. Spread into every
|
|
10248
|
+
* start/resume/adopt call so all three paths agree.
|
|
10249
|
+
*
|
|
10250
|
+
* These ids are excluded from buildFlagArgs (SPAWN_POSITIONAL_FLAG_IDS)
|
|
10251
|
+
* precisely because they arrive here instead — the PTY spawn paths pass them
|
|
10252
|
+
* as explicit positionals, so emitting them from the allowlist too would
|
|
10253
|
+
* duplicate the flag.
|
|
10254
|
+
*
|
|
10255
|
+
* Narrowed with the type guards rather than cast: ClaudeFlagValues is a loose
|
|
10256
|
+
* Record by design, and while validateFlagValues already guarantees the shape
|
|
10257
|
+
* on the way in, TypeScript cannot see that through the record.
|
|
10258
|
+
*/
|
|
10259
|
+
spawnFlagOverrides() {
|
|
10260
|
+
const mode = this.claudeFlags.permissionMode;
|
|
10261
|
+
const model = this.claudeFlags.model;
|
|
10262
|
+
const effort = this.claudeFlags.effort;
|
|
10263
|
+
return {
|
|
10264
|
+
permissionMode: isPermissionMode(mode) ? mode : this.defaultPermissionMode,
|
|
10265
|
+
model: typeof model === "string" ? model : this.defaultModel,
|
|
10266
|
+
effort: isEffortLevel(effort) ? effort : this.defaultEffort
|
|
10267
|
+
};
|
|
10268
|
+
}
|
|
10179
10269
|
checkRateLimit(map, key, limit, windowMs) {
|
|
10180
10270
|
const now = Date.now();
|
|
10181
10271
|
const arr = (map.get(key) ?? []).filter((t) => now - t < windowMs);
|
|
@@ -11325,11 +11415,9 @@ var StreamerServer = class {
|
|
|
11325
11415
|
projectPath,
|
|
11326
11416
|
projectName: body.projectName,
|
|
11327
11417
|
branch: body.branch,
|
|
11328
|
-
permissionMode: this.defaultPermissionMode,
|
|
11329
11418
|
claudeFlags: this.claudeFlags,
|
|
11330
11419
|
claudeExtraArgs: this.claudeExtraArgs,
|
|
11331
|
-
|
|
11332
|
-
effort: this.defaultEffort
|
|
11420
|
+
...this.spawnFlagOverrides()
|
|
11333
11421
|
});
|
|
11334
11422
|
this.sessionStore.addManaged(session);
|
|
11335
11423
|
this.recordSessionSpawn(session);
|
|
@@ -11776,11 +11864,9 @@ var StreamerServer = class {
|
|
|
11776
11864
|
projectPath,
|
|
11777
11865
|
projectName,
|
|
11778
11866
|
branch,
|
|
11779
|
-
permissionMode: this.defaultPermissionMode,
|
|
11780
11867
|
claudeFlags: this.claudeFlags,
|
|
11781
11868
|
claudeExtraArgs: this.claudeExtraArgs,
|
|
11782
|
-
|
|
11783
|
-
effort: this.defaultEffort
|
|
11869
|
+
...this.spawnFlagOverrides()
|
|
11784
11870
|
});
|
|
11785
11871
|
this.sessionStore.addManaged(session);
|
|
11786
11872
|
this.recordSessionSpawn(session);
|
|
@@ -11853,11 +11939,9 @@ var StreamerServer = class {
|
|
|
11853
11939
|
projectPath: resolvedPath,
|
|
11854
11940
|
projectName: body.projectName,
|
|
11855
11941
|
...includeSystemPrompt && { systemPrompt: systemPromptParts.join("\n") },
|
|
11856
|
-
permissionMode: this.defaultPermissionMode,
|
|
11857
11942
|
claudeFlags: this.claudeFlags,
|
|
11858
11943
|
claudeExtraArgs: this.claudeExtraArgs,
|
|
11859
|
-
|
|
11860
|
-
effort: this.defaultEffort
|
|
11944
|
+
...this.spawnFlagOverrides()
|
|
11861
11945
|
});
|
|
11862
11946
|
this.sessionStore.addManaged(session);
|
|
11863
11947
|
this.recordSessionSpawn(session);
|
|
@@ -12217,6 +12301,87 @@ var StreamerServer = class {
|
|
|
12217
12301
|
this.cache.upsertSessionName(sessionId, name);
|
|
12218
12302
|
json(res, 200, { ok: true });
|
|
12219
12303
|
}
|
|
12304
|
+
/**
|
|
12305
|
+
* Retarget a LIVE session's model or effort by typing the corresponding
|
|
12306
|
+
* Claude Code slash command into its PTY.
|
|
12307
|
+
*
|
|
12308
|
+
* There is no CLI or IPC channel for this — `--model`/`--effort` are spawn
|
|
12309
|
+
* arguments — so the interactive `/model <x>` / `/effort <y>` commands are the
|
|
12310
|
+
* only way to change a session already running. Both accept an argument and
|
|
12311
|
+
* apply it without opening the picker (verified against Claude Code v2.1.220).
|
|
12312
|
+
*
|
|
12313
|
+
* Answers 202, not 200: the value is applied by the TUI on its next render, so
|
|
12314
|
+
* there is nothing truthful to echo back synchronously. Clients confirm with
|
|
12315
|
+
* `GET /api/sessions/:id`, which scrapes the applied value off the live status
|
|
12316
|
+
* line.
|
|
12317
|
+
*/
|
|
12318
|
+
async applyLiveSessionSetting(sessionId, req, res, setting) {
|
|
12319
|
+
const session = this.ptyManager.getSession(sessionId);
|
|
12320
|
+
if (!session) {
|
|
12321
|
+
const known = this.sessionStore.getManaged(sessionId);
|
|
12322
|
+
if (known) {
|
|
12323
|
+
json(res, 409, {
|
|
12324
|
+
error: "Session has no live PTY; resume it first",
|
|
12325
|
+
code: "SESSION_IDLE"
|
|
12326
|
+
});
|
|
12327
|
+
return;
|
|
12328
|
+
}
|
|
12329
|
+
json(res, 404, { error: "Session not found" });
|
|
12330
|
+
return;
|
|
12331
|
+
}
|
|
12332
|
+
if ((session.provider ?? CLAUDE_CODE_PROVIDER) !== CLAUDE_CODE_PROVIDER) {
|
|
12333
|
+
json(res, 501, {
|
|
12334
|
+
error: `Setting ${setting} on a ${session.provider} session is not supported`,
|
|
12335
|
+
code: "UNSUPPORTED_PROVIDER"
|
|
12336
|
+
});
|
|
12337
|
+
return;
|
|
12338
|
+
}
|
|
12339
|
+
if (session.status === "running") {
|
|
12340
|
+
json(res, 409, {
|
|
12341
|
+
error: "Session is mid-turn; retry once it is waiting for input",
|
|
12342
|
+
code: "SESSION_BUSY"
|
|
12343
|
+
});
|
|
12344
|
+
return;
|
|
12345
|
+
}
|
|
12346
|
+
let parsed;
|
|
12347
|
+
try {
|
|
12348
|
+
parsed = await readBody(req);
|
|
12349
|
+
} catch {
|
|
12350
|
+
json(res, 400, { error: "Invalid JSON" });
|
|
12351
|
+
return;
|
|
12352
|
+
}
|
|
12353
|
+
let value;
|
|
12354
|
+
if (setting === "effort") {
|
|
12355
|
+
if (!isEffortLevel(parsed.effort)) {
|
|
12356
|
+
json(res, 400, {
|
|
12357
|
+
error: `effort must be one of ${EFFORT_LEVELS.join(", ")}`
|
|
12358
|
+
});
|
|
12359
|
+
return;
|
|
12360
|
+
}
|
|
12361
|
+
value = parsed.effort;
|
|
12362
|
+
} else {
|
|
12363
|
+
if (typeof parsed.model !== "string" || !MODEL_NAME_RE.test(parsed.model)) {
|
|
12364
|
+
json(res, 400, {
|
|
12365
|
+
error: "model must be an alias or full model name (letters, digits, dot, dash, underscore)"
|
|
12366
|
+
});
|
|
12367
|
+
return;
|
|
12368
|
+
}
|
|
12369
|
+
value = parsed.model;
|
|
12370
|
+
}
|
|
12371
|
+
try {
|
|
12372
|
+
this.ptyManager.sendKeys(sessionId, `/${setting} ${value}\r`);
|
|
12373
|
+
} catch (err) {
|
|
12374
|
+
json(res, 400, { error: err instanceof Error ? err.message : "Failed to write to session" });
|
|
12375
|
+
return;
|
|
12376
|
+
}
|
|
12377
|
+
this.log.info(`Live session ${setting} set to ${value}`, {
|
|
12378
|
+
event: "session.setting_applied",
|
|
12379
|
+
sessionId,
|
|
12380
|
+
setting,
|
|
12381
|
+
value
|
|
12382
|
+
});
|
|
12383
|
+
json(res, 202, { id: sessionId, [setting]: value });
|
|
12384
|
+
}
|
|
12220
12385
|
handleGetSessionNames(res) {
|
|
12221
12386
|
if (!this.cache) {
|
|
12222
12387
|
json(res, 200, {});
|