@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.cjs
CHANGED
|
@@ -350,6 +350,10 @@ var DANGEROUS_PERMISSION_MODES = [
|
|
|
350
350
|
function isDangerousPermissionMode(mode) {
|
|
351
351
|
return DANGEROUS_PERMISSION_MODES.includes(mode);
|
|
352
352
|
}
|
|
353
|
+
var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
354
|
+
function isEffortLevel(value) {
|
|
355
|
+
return typeof value === "string" && EFFORT_LEVELS.includes(value);
|
|
356
|
+
}
|
|
353
357
|
var CLAUDE_FLAGS = [
|
|
354
358
|
{
|
|
355
359
|
id: "permissionMode",
|
|
@@ -361,9 +365,10 @@ var CLAUDE_FLAGS = [
|
|
|
361
365
|
{ id: "addDir", flag: "--add-dir", valueType: "list", risk: "elevated" },
|
|
362
366
|
{ id: "allowedTools", flag: "--allowedTools", valueType: "list", risk: "elevated" },
|
|
363
367
|
{ id: "disallowedTools", flag: "--disallowedTools", valueType: "list", risk: "low" },
|
|
364
|
-
{ id: "
|
|
365
|
-
{ id: "
|
|
368
|
+
{ id: "model", flag: "--model", valueType: "string", risk: "low" },
|
|
369
|
+
{ id: "effort", flag: "--effort", valueType: "enum", enumValues: EFFORT_LEVELS, risk: "low" }
|
|
366
370
|
];
|
|
371
|
+
var SPAWN_POSITIONAL_FLAG_IDS = /* @__PURE__ */ new Set(["permissionMode", "model", "effort"]);
|
|
367
372
|
function findFlag(id) {
|
|
368
373
|
return CLAUDE_FLAGS.find((f) => f.id === id);
|
|
369
374
|
}
|
|
@@ -428,7 +433,7 @@ function buildFlagArgs(values, extraArgs) {
|
|
|
428
433
|
const args = [];
|
|
429
434
|
const safe = validateFlagValues(values ?? {});
|
|
430
435
|
for (const def of CLAUDE_FLAGS) {
|
|
431
|
-
if (def.id
|
|
436
|
+
if (SPAWN_POSITIONAL_FLAG_IDS.has(def.id)) continue;
|
|
432
437
|
const value = safe[def.id];
|
|
433
438
|
if (value === void 0) continue;
|
|
434
439
|
if (def.valueType === "boolean") {
|
|
@@ -4710,6 +4715,14 @@ var createSessionRoutes = (deps) => {
|
|
|
4710
4715
|
await deps.handleSetSessionName(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
4711
4716
|
return alreadyHandled6();
|
|
4712
4717
|
});
|
|
4718
|
+
app.patch("/:id/model", async (c) => {
|
|
4719
|
+
await deps.handleSetSessionModel(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
4720
|
+
return alreadyHandled6();
|
|
4721
|
+
});
|
|
4722
|
+
app.patch("/:id/effort", async (c) => {
|
|
4723
|
+
await deps.handleSetSessionEffort(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
4724
|
+
return alreadyHandled6();
|
|
4725
|
+
});
|
|
4713
4726
|
app.post("/:id/adopt", async (c) => {
|
|
4714
4727
|
await deps.handleAdopt(c.req.param("id"), c.env.outgoing);
|
|
4715
4728
|
return alreadyHandled6();
|
|
@@ -5045,6 +5058,7 @@ CREATE TABLE IF NOT EXISTS conversation_meta (
|
|
|
5045
5058
|
);
|
|
5046
5059
|
CREATE INDEX IF NOT EXISTS idx_meta_last_activity ON conversation_meta(last_activity DESC);
|
|
5047
5060
|
CREATE INDEX IF NOT EXISTS idx_meta_project ON conversation_meta(project_path);
|
|
5061
|
+
CREATE INDEX IF NOT EXISTS idx_meta_file_path ON conversation_meta(file_path);
|
|
5048
5062
|
|
|
5049
5063
|
CREATE TABLE IF NOT EXISTS conversation_tail (
|
|
5050
5064
|
conversation_id TEXT PRIMARY KEY REFERENCES conversation_meta(id) ON DELETE CASCADE,
|
|
@@ -8702,6 +8716,24 @@ var WSHub = class {
|
|
|
8702
8716
|
this.clients.delete(client);
|
|
8703
8717
|
}
|
|
8704
8718
|
}
|
|
8719
|
+
// Scoped broadcast for high-frequency per-session messages (terminal_output,
|
|
8720
|
+
// user_message). Sending to every connected client for every PTY output
|
|
8721
|
+
// chunk made broadcast() cost scale with connections x active sessions;
|
|
8722
|
+
// this bounds it to only that session's subscribers.
|
|
8723
|
+
broadcastToClients(clients, message) {
|
|
8724
|
+
const data = JSON.stringify(message);
|
|
8725
|
+
for (const client of clients) {
|
|
8726
|
+
try {
|
|
8727
|
+
if (client.readyState === client.OPEN) {
|
|
8728
|
+
client.send(data);
|
|
8729
|
+
} else {
|
|
8730
|
+
this.clients.delete(client);
|
|
8731
|
+
}
|
|
8732
|
+
} catch {
|
|
8733
|
+
this.clients.delete(client);
|
|
8734
|
+
}
|
|
8735
|
+
}
|
|
8736
|
+
}
|
|
8705
8737
|
unicast(ws, message) {
|
|
8706
8738
|
try {
|
|
8707
8739
|
if (ws.readyState === ws.OPEN) {
|
|
@@ -8764,6 +8796,7 @@ var ADOPT_KILL_TIMEOUT_MS = 5e3;
|
|
|
8764
8796
|
var ADOPT_KILL_POLL_MS = 100;
|
|
8765
8797
|
var REFRESH_TTL_MS = 2e3;
|
|
8766
8798
|
var START_READY_TIMEOUT_MS = 1e4;
|
|
8799
|
+
var MODEL_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
8767
8800
|
var EXTERNAL_TAIL_RECENCY_MS = RESUME_BUSY_WINDOW_MS;
|
|
8768
8801
|
var EXTERNAL_TAIL_MAX = 32;
|
|
8769
8802
|
var EXTERNAL_TAIL_IDLE_MS = 3e5;
|
|
@@ -8867,6 +8900,8 @@ var StreamerServer = class {
|
|
|
8867
8900
|
dbPool = null;
|
|
8868
8901
|
dbInstanceId = null;
|
|
8869
8902
|
disableDb = false;
|
|
8903
|
+
// Skip the startup warm-up scan (test hook; see ServerConfig.skipStartupWarmup).
|
|
8904
|
+
skipStartupWarmup;
|
|
8870
8905
|
browseRoot = null;
|
|
8871
8906
|
publicUrl = null;
|
|
8872
8907
|
browserCors;
|
|
@@ -8906,6 +8941,11 @@ var StreamerServer = class {
|
|
|
8906
8941
|
// every provider; read only by the idle reaper. Entries are dropped when the
|
|
8907
8942
|
// session leaves the runner (reap/exit/hold).
|
|
8908
8943
|
lastAgentChunkAt = /* @__PURE__ */ new Map();
|
|
8944
|
+
// sessionId → last terminal_output seq broadcast (starts at 1, per session).
|
|
8945
|
+
// Stamped on every terminal_output/terminal_replay so a client can detect a
|
|
8946
|
+
// stale chunk delivered after a reconnect race instead of trusting raw WS
|
|
8947
|
+
// arrival order. Entries dropped alongside lastAgentChunkAt.
|
|
8948
|
+
terminalSeq = /* @__PURE__ */ new Map();
|
|
8909
8949
|
// Recently accepted input idempotency keys (C4). A retried POST replays its
|
|
8910
8950
|
// original outcome instead of submitting the prompt to the agent twice.
|
|
8911
8951
|
idempotency = new IdempotencyStore();
|
|
@@ -8973,6 +9013,7 @@ var StreamerServer = class {
|
|
|
8973
9013
|
}
|
|
8974
9014
|
this.verbose = config.verbose ?? false;
|
|
8975
9015
|
this.disableDb = config.disableDb ?? false;
|
|
9016
|
+
this.skipStartupWarmup = config.skipStartupWarmup ?? false;
|
|
8976
9017
|
this.scannerPersistenceDisabled = config.scannerPersistent === false;
|
|
8977
9018
|
this.scanProfiles = config.scanProfiles;
|
|
8978
9019
|
this.codexRoots = config.codexRoots ?? [(0, import_path18.join)((0, import_os9.homedir)(), ".codex", "sessions")];
|
|
@@ -9121,10 +9162,22 @@ var StreamerServer = class {
|
|
|
9121
9162
|
logger: getLogger("pty"),
|
|
9122
9163
|
onOutput: (sessionId, data) => {
|
|
9123
9164
|
this.lastAgentChunkAt.set(sessionId, Date.now());
|
|
9124
|
-
this.
|
|
9165
|
+
const seq = (this.terminalSeq.get(sessionId) ?? 0) + 1;
|
|
9166
|
+
this.terminalSeq.set(sessionId, seq);
|
|
9167
|
+
this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], {
|
|
9168
|
+
type: "terminal_output",
|
|
9169
|
+
sessionId,
|
|
9170
|
+
data,
|
|
9171
|
+
seq
|
|
9172
|
+
});
|
|
9125
9173
|
},
|
|
9126
9174
|
onUserMessage: (sessionId, text, ts) => {
|
|
9127
|
-
this.wsHub.
|
|
9175
|
+
this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], {
|
|
9176
|
+
type: "user_message",
|
|
9177
|
+
sessionId,
|
|
9178
|
+
text,
|
|
9179
|
+
ts
|
|
9180
|
+
});
|
|
9128
9181
|
},
|
|
9129
9182
|
onPermissionChange: (sessionId, gate) => {
|
|
9130
9183
|
this.handlePermissionChange(sessionId, gate);
|
|
@@ -9261,6 +9314,8 @@ var StreamerServer = class {
|
|
|
9261
9314
|
handleCancel: (id, res) => this.handleCancel(id, res),
|
|
9262
9315
|
handleStopSession: (id, res) => this.handleStopSession(id, res),
|
|
9263
9316
|
handleSetSessionName: (id, req, res) => this.handleSetSessionName(id, req, res),
|
|
9317
|
+
handleSetSessionModel: (id, req, res) => this.applyLiveSessionSetting(id, req, res, "model"),
|
|
9318
|
+
handleSetSessionEffort: (id, req, res) => this.applyLiveSessionSetting(id, req, res, "effort"),
|
|
9264
9319
|
handleUploadFile: (id, req, res) => this.handleUploadFile(id, req, res),
|
|
9265
9320
|
handleAdopt: (id, res) => this.handleAdopt(id, res),
|
|
9266
9321
|
handleResume: (req, res) => this.handleResume(req, res),
|
|
@@ -9307,7 +9362,8 @@ var StreamerServer = class {
|
|
|
9307
9362
|
type: "terminal_replay",
|
|
9308
9363
|
sessionId: msg.sessionId,
|
|
9309
9364
|
lines,
|
|
9310
|
-
userMessages
|
|
9365
|
+
userMessages,
|
|
9366
|
+
seq: this.terminalSeq.get(msg.sessionId)
|
|
9311
9367
|
})
|
|
9312
9368
|
);
|
|
9313
9369
|
}
|
|
@@ -9667,6 +9723,7 @@ var StreamerServer = class {
|
|
|
9667
9723
|
);
|
|
9668
9724
|
this.ptyManager.putOnHold(session.id);
|
|
9669
9725
|
this.lastAgentChunkAt.delete(session.id);
|
|
9726
|
+
this.terminalSeq.delete(session.id);
|
|
9670
9727
|
this.idempotency.clear(session.id);
|
|
9671
9728
|
this.sessionSubscribers.delete(session.id);
|
|
9672
9729
|
reaped.push(session.id);
|
|
@@ -9841,6 +9898,14 @@ var StreamerServer = class {
|
|
|
9841
9898
|
);
|
|
9842
9899
|
this.scannerPersistenceDisabled = true;
|
|
9843
9900
|
}
|
|
9901
|
+
if (this.skipStartupWarmup) {
|
|
9902
|
+
this.log.debug?.("startup warm-up scan skipped (skipStartupWarmup)", {
|
|
9903
|
+
event: "cache.warmup_skipped"
|
|
9904
|
+
});
|
|
9905
|
+
this.finishWarmup(0);
|
|
9906
|
+
resolveWarm();
|
|
9907
|
+
return;
|
|
9908
|
+
}
|
|
9844
9909
|
const warmupStatCache = this.buildStatCache(null);
|
|
9845
9910
|
const warmupScanner = this.newScanner(warmupStatCache ? { persistent: false } : void 0);
|
|
9846
9911
|
this.allScanners.add(warmupScanner);
|
|
@@ -10032,6 +10097,7 @@ var StreamerServer = class {
|
|
|
10032
10097
|
this.idleReaperTimer = null;
|
|
10033
10098
|
}
|
|
10034
10099
|
this.lastAgentChunkAt.clear();
|
|
10100
|
+
this.terminalSeq.clear();
|
|
10035
10101
|
this.recordShutdownState();
|
|
10036
10102
|
this.markScannerStaleDebounced.cancel();
|
|
10037
10103
|
await Promise.all([...this.inFlightCacheWrites]);
|
|
@@ -10213,6 +10279,30 @@ var StreamerServer = class {
|
|
|
10213
10279
|
persisted: this.claudeFlagsPersistable
|
|
10214
10280
|
};
|
|
10215
10281
|
}
|
|
10282
|
+
/**
|
|
10283
|
+
* The three spawn options that a configured claude-flag can override, with
|
|
10284
|
+
* the boot-time CLI/yaml default as the fallback. Spread into every
|
|
10285
|
+
* start/resume/adopt call so all three paths agree.
|
|
10286
|
+
*
|
|
10287
|
+
* These ids are excluded from buildFlagArgs (SPAWN_POSITIONAL_FLAG_IDS)
|
|
10288
|
+
* precisely because they arrive here instead — the PTY spawn paths pass them
|
|
10289
|
+
* as explicit positionals, so emitting them from the allowlist too would
|
|
10290
|
+
* duplicate the flag.
|
|
10291
|
+
*
|
|
10292
|
+
* Narrowed with the type guards rather than cast: ClaudeFlagValues is a loose
|
|
10293
|
+
* Record by design, and while validateFlagValues already guarantees the shape
|
|
10294
|
+
* on the way in, TypeScript cannot see that through the record.
|
|
10295
|
+
*/
|
|
10296
|
+
spawnFlagOverrides() {
|
|
10297
|
+
const mode = this.claudeFlags.permissionMode;
|
|
10298
|
+
const model = this.claudeFlags.model;
|
|
10299
|
+
const effort = this.claudeFlags.effort;
|
|
10300
|
+
return {
|
|
10301
|
+
permissionMode: isPermissionMode(mode) ? mode : this.defaultPermissionMode,
|
|
10302
|
+
model: typeof model === "string" ? model : this.defaultModel,
|
|
10303
|
+
effort: isEffortLevel(effort) ? effort : this.defaultEffort
|
|
10304
|
+
};
|
|
10305
|
+
}
|
|
10216
10306
|
checkRateLimit(map, key, limit, windowMs) {
|
|
10217
10307
|
const now = Date.now();
|
|
10218
10308
|
const arr = (map.get(key) ?? []).filter((t) => now - t < windowMs);
|
|
@@ -11362,11 +11452,9 @@ var StreamerServer = class {
|
|
|
11362
11452
|
projectPath,
|
|
11363
11453
|
projectName: body.projectName,
|
|
11364
11454
|
branch: body.branch,
|
|
11365
|
-
permissionMode: this.defaultPermissionMode,
|
|
11366
11455
|
claudeFlags: this.claudeFlags,
|
|
11367
11456
|
claudeExtraArgs: this.claudeExtraArgs,
|
|
11368
|
-
|
|
11369
|
-
effort: this.defaultEffort
|
|
11457
|
+
...this.spawnFlagOverrides()
|
|
11370
11458
|
});
|
|
11371
11459
|
this.sessionStore.addManaged(session);
|
|
11372
11460
|
this.recordSessionSpawn(session);
|
|
@@ -11813,11 +11901,9 @@ var StreamerServer = class {
|
|
|
11813
11901
|
projectPath,
|
|
11814
11902
|
projectName,
|
|
11815
11903
|
branch,
|
|
11816
|
-
permissionMode: this.defaultPermissionMode,
|
|
11817
11904
|
claudeFlags: this.claudeFlags,
|
|
11818
11905
|
claudeExtraArgs: this.claudeExtraArgs,
|
|
11819
|
-
|
|
11820
|
-
effort: this.defaultEffort
|
|
11906
|
+
...this.spawnFlagOverrides()
|
|
11821
11907
|
});
|
|
11822
11908
|
this.sessionStore.addManaged(session);
|
|
11823
11909
|
this.recordSessionSpawn(session);
|
|
@@ -11890,11 +11976,9 @@ var StreamerServer = class {
|
|
|
11890
11976
|
projectPath: resolvedPath,
|
|
11891
11977
|
projectName: body.projectName,
|
|
11892
11978
|
...includeSystemPrompt && { systemPrompt: systemPromptParts.join("\n") },
|
|
11893
|
-
permissionMode: this.defaultPermissionMode,
|
|
11894
11979
|
claudeFlags: this.claudeFlags,
|
|
11895
11980
|
claudeExtraArgs: this.claudeExtraArgs,
|
|
11896
|
-
|
|
11897
|
-
effort: this.defaultEffort
|
|
11981
|
+
...this.spawnFlagOverrides()
|
|
11898
11982
|
});
|
|
11899
11983
|
this.sessionStore.addManaged(session);
|
|
11900
11984
|
this.recordSessionSpawn(session);
|
|
@@ -12254,6 +12338,87 @@ var StreamerServer = class {
|
|
|
12254
12338
|
this.cache.upsertSessionName(sessionId, name);
|
|
12255
12339
|
json(res, 200, { ok: true });
|
|
12256
12340
|
}
|
|
12341
|
+
/**
|
|
12342
|
+
* Retarget a LIVE session's model or effort by typing the corresponding
|
|
12343
|
+
* Claude Code slash command into its PTY.
|
|
12344
|
+
*
|
|
12345
|
+
* There is no CLI or IPC channel for this — `--model`/`--effort` are spawn
|
|
12346
|
+
* arguments — so the interactive `/model <x>` / `/effort <y>` commands are the
|
|
12347
|
+
* only way to change a session already running. Both accept an argument and
|
|
12348
|
+
* apply it without opening the picker (verified against Claude Code v2.1.220).
|
|
12349
|
+
*
|
|
12350
|
+
* Answers 202, not 200: the value is applied by the TUI on its next render, so
|
|
12351
|
+
* there is nothing truthful to echo back synchronously. Clients confirm with
|
|
12352
|
+
* `GET /api/sessions/:id`, which scrapes the applied value off the live status
|
|
12353
|
+
* line.
|
|
12354
|
+
*/
|
|
12355
|
+
async applyLiveSessionSetting(sessionId, req, res, setting) {
|
|
12356
|
+
const session = this.ptyManager.getSession(sessionId);
|
|
12357
|
+
if (!session) {
|
|
12358
|
+
const known = this.sessionStore.getManaged(sessionId);
|
|
12359
|
+
if (known) {
|
|
12360
|
+
json(res, 409, {
|
|
12361
|
+
error: "Session has no live PTY; resume it first",
|
|
12362
|
+
code: "SESSION_IDLE"
|
|
12363
|
+
});
|
|
12364
|
+
return;
|
|
12365
|
+
}
|
|
12366
|
+
json(res, 404, { error: "Session not found" });
|
|
12367
|
+
return;
|
|
12368
|
+
}
|
|
12369
|
+
if ((session.provider ?? CLAUDE_CODE_PROVIDER) !== CLAUDE_CODE_PROVIDER) {
|
|
12370
|
+
json(res, 501, {
|
|
12371
|
+
error: `Setting ${setting} on a ${session.provider} session is not supported`,
|
|
12372
|
+
code: "UNSUPPORTED_PROVIDER"
|
|
12373
|
+
});
|
|
12374
|
+
return;
|
|
12375
|
+
}
|
|
12376
|
+
if (session.status === "running") {
|
|
12377
|
+
json(res, 409, {
|
|
12378
|
+
error: "Session is mid-turn; retry once it is waiting for input",
|
|
12379
|
+
code: "SESSION_BUSY"
|
|
12380
|
+
});
|
|
12381
|
+
return;
|
|
12382
|
+
}
|
|
12383
|
+
let parsed;
|
|
12384
|
+
try {
|
|
12385
|
+
parsed = await readBody(req);
|
|
12386
|
+
} catch {
|
|
12387
|
+
json(res, 400, { error: "Invalid JSON" });
|
|
12388
|
+
return;
|
|
12389
|
+
}
|
|
12390
|
+
let value;
|
|
12391
|
+
if (setting === "effort") {
|
|
12392
|
+
if (!isEffortLevel(parsed.effort)) {
|
|
12393
|
+
json(res, 400, {
|
|
12394
|
+
error: `effort must be one of ${EFFORT_LEVELS.join(", ")}`
|
|
12395
|
+
});
|
|
12396
|
+
return;
|
|
12397
|
+
}
|
|
12398
|
+
value = parsed.effort;
|
|
12399
|
+
} else {
|
|
12400
|
+
if (typeof parsed.model !== "string" || !MODEL_NAME_RE.test(parsed.model)) {
|
|
12401
|
+
json(res, 400, {
|
|
12402
|
+
error: "model must be an alias or full model name (letters, digits, dot, dash, underscore)"
|
|
12403
|
+
});
|
|
12404
|
+
return;
|
|
12405
|
+
}
|
|
12406
|
+
value = parsed.model;
|
|
12407
|
+
}
|
|
12408
|
+
try {
|
|
12409
|
+
this.ptyManager.sendKeys(sessionId, `/${setting} ${value}\r`);
|
|
12410
|
+
} catch (err) {
|
|
12411
|
+
json(res, 400, { error: err instanceof Error ? err.message : "Failed to write to session" });
|
|
12412
|
+
return;
|
|
12413
|
+
}
|
|
12414
|
+
this.log.info(`Live session ${setting} set to ${value}`, {
|
|
12415
|
+
event: "session.setting_applied",
|
|
12416
|
+
sessionId,
|
|
12417
|
+
setting,
|
|
12418
|
+
value
|
|
12419
|
+
});
|
|
12420
|
+
json(res, 202, { id: sessionId, [setting]: value });
|
|
12421
|
+
}
|
|
12257
12422
|
handleGetSessionNames(res) {
|
|
12258
12423
|
if (!this.cache) {
|
|
12259
12424
|
json(res, 200, {});
|