@threadbase-sh/streamer 1.24.7 → 1.25.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 +419 -128
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +241 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +241 -68
- package/dist/index.js.map +1 -1
- package/dist/migrations/007_add_scanner_warmup_cache.sql +15 -0
- package/dist/seed-claude-config.cjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -393,6 +393,16 @@ function loadTailSize() {
|
|
|
393
393
|
}
|
|
394
394
|
return void 0;
|
|
395
395
|
}
|
|
396
|
+
function loadDefaultPermissionMode() {
|
|
397
|
+
try {
|
|
398
|
+
const content = (0, import_fs.readFileSync)(configFile(), "utf-8");
|
|
399
|
+
const match = content.match(/default_permission_mode:\s*(\S+)/);
|
|
400
|
+
const value = match?.[1]?.trim();
|
|
401
|
+
if (value === "acceptEdits" || value === "manual") return value;
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
return void 0;
|
|
405
|
+
}
|
|
396
406
|
function validatePublicUrl(raw) {
|
|
397
407
|
let parsed;
|
|
398
408
|
try {
|
|
@@ -1353,6 +1363,37 @@ function detectShellPrompt(lines) {
|
|
|
1353
1363
|
return null;
|
|
1354
1364
|
}
|
|
1355
1365
|
|
|
1366
|
+
// src/utils/debounce.ts
|
|
1367
|
+
function debounce(fn, waitMs) {
|
|
1368
|
+
let timer = null;
|
|
1369
|
+
let lastArgs = null;
|
|
1370
|
+
const run2 = () => {
|
|
1371
|
+
timer = null;
|
|
1372
|
+
if (lastArgs) {
|
|
1373
|
+
const args = lastArgs;
|
|
1374
|
+
lastArgs = null;
|
|
1375
|
+
fn(...args);
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
const debounced = (...args) => {
|
|
1379
|
+
lastArgs = args;
|
|
1380
|
+
if (timer) clearTimeout(timer);
|
|
1381
|
+
timer = setTimeout(run2, waitMs);
|
|
1382
|
+
};
|
|
1383
|
+
debounced.cancel = () => {
|
|
1384
|
+
if (timer) clearTimeout(timer);
|
|
1385
|
+
timer = null;
|
|
1386
|
+
lastArgs = null;
|
|
1387
|
+
};
|
|
1388
|
+
debounced.flush = () => {
|
|
1389
|
+
if (timer) {
|
|
1390
|
+
clearTimeout(timer);
|
|
1391
|
+
run2();
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
return debounced;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1356
1397
|
// src/pty-manager.ts
|
|
1357
1398
|
var OUTPUT_BUFFER_MAX2 = 65536;
|
|
1358
1399
|
var PTY_COLS2 = 120;
|
|
@@ -1360,11 +1401,13 @@ var PTY_ROWS2 = 40;
|
|
|
1360
1401
|
var SCREEN_SCROLLBACK2 = 1e3;
|
|
1361
1402
|
var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
|
|
1362
1403
|
var PROMPT_MARKER_FALLBACK_MS = 1e4;
|
|
1404
|
+
var QUIET_DETECT_MS = 500;
|
|
1363
1405
|
function buildPasteBytes(input) {
|
|
1364
1406
|
return `\x1B[200~${input}\x1B[201~`;
|
|
1365
1407
|
}
|
|
1366
1408
|
var SUBMIT_BYTES2 = "\r";
|
|
1367
1409
|
var SUBMIT_DELAY_MS = 16;
|
|
1410
|
+
var SUBMIT_MAX_WAIT_MS = 500;
|
|
1368
1411
|
function digestBytes2(s) {
|
|
1369
1412
|
const escaped = s.replace(new RegExp(String.fromCharCode(27), "g"), "\\x1b").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
|
1370
1413
|
if (escaped.length <= 200) return escaped;
|
|
@@ -1441,6 +1484,10 @@ var PTYManager = class {
|
|
|
1441
1484
|
// to a given input or fell silent. Reset on dispose().
|
|
1442
1485
|
chunkIndex = /* @__PURE__ */ new Map();
|
|
1443
1486
|
lastChunkAt = /* @__PURE__ */ new Map();
|
|
1487
|
+
// Per-session debounced "went quiet" checker, re-armed on every chunk. Fires
|
|
1488
|
+
// QUIET_DETECT_MS after the last chunk so ready/prompt detection doesn't
|
|
1489
|
+
// wait for another chunk that may never arrive (Claude blocked on input).
|
|
1490
|
+
quietCheckers = /* @__PURE__ */ new Map();
|
|
1444
1491
|
// In-flight start()/startFresh() calls keyed by sessionId. A second
|
|
1445
1492
|
// concurrent resume for the same session (double-tap, client retry) awaits
|
|
1446
1493
|
// the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
|
|
@@ -1456,16 +1503,19 @@ var PTYManager = class {
|
|
|
1456
1503
|
}
|
|
1457
1504
|
// Resume an existing Claude conversation. sessionId is the JSONL UUID.
|
|
1458
1505
|
//
|
|
1459
|
-
//
|
|
1460
|
-
// Both suppress
|
|
1461
|
-
//
|
|
1462
|
-
//
|
|
1463
|
-
//
|
|
1464
|
-
//
|
|
1506
|
+
// options.permissionMode defaults to `acceptEdits` rather than
|
|
1507
|
+
// `--dangerously-skip-permissions`/`bypassPermissions`. Both suppress
|
|
1508
|
+
// file-edit prompts, but in an interactive (TUI) launch the skip-permissions
|
|
1509
|
+
// flag renders a blocking "Bypass Permissions mode" warning menu on every
|
|
1510
|
+
// boot that no known ~/.claude.json flag suppressed (as of Claude CLI
|
|
1511
|
+
// v2.1.x) — the session never reaches a usable prompt, so the mobile app
|
|
1512
|
+
// shows an empty/stuck screen. `acceptEdits` auto-approves file edits
|
|
1465
1513
|
// without that warning gate, while still prompting for shell commands.
|
|
1514
|
+
// `manual` (prompt for everything) is the only other mode callers may pass;
|
|
1515
|
+
// `bypassPermissions`/`plan`/`dontAsk`/`auto` are not supported here.
|
|
1466
1516
|
// (The other first-run gates — onboarding/theme, workspace trust,
|
|
1467
1517
|
// custom-API-key — are cleared by the seeded ~/.claude.json in
|
|
1468
|
-
// docker/entrypoint.sh.) startFresh() uses the same
|
|
1518
|
+
// docker/entrypoint.sh.) startFresh() uses the same default for the same reason.
|
|
1469
1519
|
async start(sessionId, options) {
|
|
1470
1520
|
const existing = this.sessions.get(sessionId);
|
|
1471
1521
|
if (existing) return toPublicSession2(existing);
|
|
@@ -1484,7 +1534,7 @@ var PTYManager = class {
|
|
|
1484
1534
|
resolveClaudeExe(),
|
|
1485
1535
|
[
|
|
1486
1536
|
"--permission-mode",
|
|
1487
|
-
"acceptEdits",
|
|
1537
|
+
options.permissionMode ?? "acceptEdits",
|
|
1488
1538
|
"--settings",
|
|
1489
1539
|
'{"spinnerTipsEnabled":false}',
|
|
1490
1540
|
"--resume",
|
|
@@ -1533,7 +1583,7 @@ var PTYManager = class {
|
|
|
1533
1583
|
const projectName = options.projectName ?? (0, import_path5.basename)(options.projectPath);
|
|
1534
1584
|
const args = [
|
|
1535
1585
|
"--permission-mode",
|
|
1536
|
-
"acceptEdits",
|
|
1586
|
+
options.permissionMode ?? "acceptEdits",
|
|
1537
1587
|
"--settings",
|
|
1538
1588
|
'{"spinnerTipsEnabled":false}',
|
|
1539
1589
|
"--session-id",
|
|
@@ -1627,9 +1677,20 @@ var PTYManager = class {
|
|
|
1627
1677
|
session.promptCount++;
|
|
1628
1678
|
return session.promptCount;
|
|
1629
1679
|
}
|
|
1630
|
-
// Two-step paste-then-submit. Writes the bracketed-paste body,
|
|
1631
|
-
//
|
|
1632
|
-
//
|
|
1680
|
+
// Two-step paste-then-submit. Writes the bracketed-paste body, then waits
|
|
1681
|
+
// for the PTY to go quiet before writing \r. See buildPasteBytes() for why
|
|
1682
|
+
// the split matters.
|
|
1683
|
+
//
|
|
1684
|
+
// The wait is quiescence-based, not a flat delay: a fixed SUBMIT_DELAY_MS
|
|
1685
|
+
// timer (the original fix) still races a TUI that's mid-redraw of its own
|
|
1686
|
+
// output (e.g. re-painting right after posting a question) when the paste
|
|
1687
|
+
// lands — the timer can elapse and fire \r while the TUI is still busy,
|
|
1688
|
+
// and that \r gets absorbed by the redraw instead of submitting (see
|
|
1689
|
+
// 2026-07 session 14dda340: a "Yes" reply was accepted into the input line
|
|
1690
|
+
// but never landed as a submitted JSONL turn). Polling in SUBMIT_DELAY_MS
|
|
1691
|
+
// steps and only submitting once lastChunkAt hasn't advanced for a full
|
|
1692
|
+
// step gives the TUI as many extra ticks as it needs, capped at
|
|
1693
|
+
// SUBMIT_MAX_WAIT_MS so a silent/wedged PTY still gets its \r eventually.
|
|
1633
1694
|
writeSubmit(sessionId, session, input, path, promptCount) {
|
|
1634
1695
|
const pasteBytes = buildPasteBytes(input);
|
|
1635
1696
|
this.log.info(
|
|
@@ -1644,12 +1705,21 @@ var PTYManager = class {
|
|
|
1644
1705
|
phase: "paste"
|
|
1645
1706
|
}
|
|
1646
1707
|
);
|
|
1708
|
+
const pasteAt = Date.now();
|
|
1647
1709
|
session.process.write(pasteBytes);
|
|
1648
|
-
|
|
1710
|
+
const trySubmit = () => {
|
|
1649
1711
|
const current = this.sessions.get(sessionId);
|
|
1650
1712
|
if (!current || current !== session) return;
|
|
1713
|
+
const now = Date.now();
|
|
1714
|
+
const lastChunk = this.lastChunkAt.get(sessionId) ?? pasteAt;
|
|
1715
|
+
const quiet = now - lastChunk >= SUBMIT_DELAY_MS;
|
|
1716
|
+
const timedOut = now - pasteAt >= SUBMIT_MAX_WAIT_MS;
|
|
1717
|
+
if (!quiet && !timedOut) {
|
|
1718
|
+
setTimeout(trySubmit, SUBMIT_DELAY_MS);
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1651
1721
|
this.log.info(
|
|
1652
|
-
`[pty.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\r`,
|
|
1722
|
+
`[pty.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\r waitedMs=${now - pasteAt} timedOut=${timedOut}`,
|
|
1653
1723
|
{
|
|
1654
1724
|
event: "pty.input_write",
|
|
1655
1725
|
sessionId,
|
|
@@ -1657,11 +1727,14 @@ var PTYManager = class {
|
|
|
1657
1727
|
byteLen: SUBMIT_BYTES2.length,
|
|
1658
1728
|
digest: "\\r",
|
|
1659
1729
|
path,
|
|
1660
|
-
phase: "submit"
|
|
1730
|
+
phase: "submit",
|
|
1731
|
+
waitedMs: now - pasteAt,
|
|
1732
|
+
timedOut
|
|
1661
1733
|
}
|
|
1662
1734
|
);
|
|
1663
1735
|
current.process.write(SUBMIT_BYTES2);
|
|
1664
|
-
}
|
|
1736
|
+
};
|
|
1737
|
+
setTimeout(trySubmit, SUBMIT_DELAY_MS);
|
|
1665
1738
|
}
|
|
1666
1739
|
// Drain any inputs that were sent while the session was still pendingReady,
|
|
1667
1740
|
// writing them in arrival order now that Claude is at its prompt.
|
|
@@ -1710,6 +1783,8 @@ var PTYManager = class {
|
|
|
1710
1783
|
this.permissionOpen.delete(sessionId);
|
|
1711
1784
|
this.lastScreenQuestionKey.delete(sessionId);
|
|
1712
1785
|
this.shellPromptOpen.delete(sessionId);
|
|
1786
|
+
this.quietCheckers.get(sessionId)?.cancel();
|
|
1787
|
+
this.quietCheckers.delete(sessionId);
|
|
1713
1788
|
try {
|
|
1714
1789
|
session.process.kill("SIGINT");
|
|
1715
1790
|
} catch {
|
|
@@ -1769,6 +1844,8 @@ var PTYManager = class {
|
|
|
1769
1844
|
this.firstChunkAt.clear();
|
|
1770
1845
|
this.chunkIndex.clear();
|
|
1771
1846
|
this.lastChunkAt.clear();
|
|
1847
|
+
for (const quiet of this.quietCheckers.values()) quiet.cancel();
|
|
1848
|
+
this.quietCheckers.clear();
|
|
1772
1849
|
this.permissionOpen.clear();
|
|
1773
1850
|
this.lastScreenQuestionKey.clear();
|
|
1774
1851
|
this.shellPromptOpen.clear();
|
|
@@ -1822,6 +1899,12 @@ var PTYManager = class {
|
|
|
1822
1899
|
err
|
|
1823
1900
|
});
|
|
1824
1901
|
});
|
|
1902
|
+
let quiet = this.quietCheckers.get(sessionId);
|
|
1903
|
+
if (!quiet) {
|
|
1904
|
+
quiet = debounce(() => this.handleQuiet(sessionId), QUIET_DETECT_MS);
|
|
1905
|
+
this.quietCheckers.set(sessionId, quiet);
|
|
1906
|
+
}
|
|
1907
|
+
quiet();
|
|
1825
1908
|
}
|
|
1826
1909
|
// Detect permission gates (OSC 777 + scraped options) and AskUserQuestion
|
|
1827
1910
|
// menus from the rendered screen, firing the additive callbacks. Async because
|
|
@@ -1897,6 +1980,24 @@ var PTYManager = class {
|
|
|
1897
1980
|
}
|
|
1898
1981
|
}
|
|
1899
1982
|
}
|
|
1983
|
+
// Fired QUIET_DETECT_MS after the last PTY chunk. Re-runs the same
|
|
1984
|
+
// ready/prompt detection handleOutput() runs per-chunk, using the last
|
|
1985
|
+
// rendered output — a session blocked on a prompt (or an unmarked boot
|
|
1986
|
+
// screen) may never produce another chunk to trigger detection otherwise.
|
|
1987
|
+
handleQuiet(sessionId) {
|
|
1988
|
+
const session = this.sessions.get(sessionId);
|
|
1989
|
+
if (session?.status !== "running") return;
|
|
1990
|
+
if (this.pendingReady.has(sessionId)) {
|
|
1991
|
+
this.markReady(sessionId, session, "quiet:timeout");
|
|
1992
|
+
}
|
|
1993
|
+
this.detectLivePrompts(sessionId, "", session.lastOutput).catch((err) => {
|
|
1994
|
+
this.log.warn("[pty.prompt_detect] failed", {
|
|
1995
|
+
event: "pty.prompt_detect_failed",
|
|
1996
|
+
sessionId,
|
|
1997
|
+
err
|
|
1998
|
+
});
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
1900
2001
|
// Transition a session from "running" to "waiting_input", clear pendingReady,
|
|
1901
2002
|
// and flush any queued input. Idempotent: callers can invoke at any chunk.
|
|
1902
2003
|
markReady(sessionId, session, reason) {
|
|
@@ -1937,6 +2038,8 @@ var PTYManager = class {
|
|
|
1937
2038
|
this.permissionOpen.delete(sessionId);
|
|
1938
2039
|
this.lastScreenQuestionKey.delete(sessionId);
|
|
1939
2040
|
this.shellPromptOpen.delete(sessionId);
|
|
2041
|
+
this.quietCheckers.get(sessionId)?.cancel();
|
|
2042
|
+
this.quietCheckers.delete(sessionId);
|
|
1940
2043
|
}
|
|
1941
2044
|
};
|
|
1942
2045
|
function toPublicSession2(s) {
|
|
@@ -3225,11 +3328,11 @@ var ConversationCache = class _ConversationCache {
|
|
|
3225
3328
|
INSERT INTO conversation_meta
|
|
3226
3329
|
(id, file_path, project_path, project_name, title, model, account, branch,
|
|
3227
3330
|
message_count, last_activity, first_message, last_message, preview, updated_at,
|
|
3228
|
-
mtime_ms, file_size, provider)
|
|
3331
|
+
mtime_ms, file_size, provider, scanner_meta_json)
|
|
3229
3332
|
VALUES
|
|
3230
3333
|
(@id, @file_path, @project_path, @project_name, @title, @model, @account, @branch,
|
|
3231
3334
|
@message_count, @last_activity, @first_message, @last_message, @preview, @updated_at,
|
|
3232
|
-
@mtime_ms, @file_size, @provider)
|
|
3335
|
+
@mtime_ms, @file_size, @provider, @scanner_meta_json)
|
|
3233
3336
|
ON CONFLICT(id) DO UPDATE SET
|
|
3234
3337
|
file_path = excluded.file_path,
|
|
3235
3338
|
project_path = excluded.project_path,
|
|
@@ -3249,7 +3352,8 @@ var ConversationCache = class _ConversationCache {
|
|
|
3249
3352
|
updated_at = excluded.updated_at,
|
|
3250
3353
|
mtime_ms = excluded.mtime_ms,
|
|
3251
3354
|
file_size = excluded.file_size,
|
|
3252
|
-
provider = excluded.provider
|
|
3355
|
+
provider = excluded.provider,
|
|
3356
|
+
scanner_meta_json = excluded.scanner_meta_json
|
|
3253
3357
|
WHERE conversation_meta.updated_at < excluded.updated_at
|
|
3254
3358
|
`),
|
|
3255
3359
|
getTail: db.prepare("SELECT * FROM conversation_tail WHERE conversation_id = ?"),
|
|
@@ -3286,6 +3390,27 @@ var ConversationCache = class _ConversationCache {
|
|
|
3286
3390
|
allFileStats: db.prepare(
|
|
3287
3391
|
"SELECT file_path, mtime_ms, file_size FROM conversation_meta WHERE mtime_ms IS NOT NULL AND file_size IS NOT NULL"
|
|
3288
3392
|
),
|
|
3393
|
+
allScannerStatCacheRows: db.prepare(
|
|
3394
|
+
"SELECT file_path, mtime_ms, file_size, scanner_meta_json FROM conversation_meta WHERE mtime_ms IS NOT NULL AND file_size IS NOT NULL AND scanner_meta_json IS NOT NULL"
|
|
3395
|
+
),
|
|
3396
|
+
updateScannerCache: db.prepare(
|
|
3397
|
+
"UPDATE conversation_meta SET mtime_ms = ?, file_size = ?, scanner_meta_json = ? WHERE id = ?"
|
|
3398
|
+
),
|
|
3399
|
+
getFileMetadata: db.prepare(
|
|
3400
|
+
"SELECT mtime_ms, file_size, is_agent, agent_entrypoints_key FROM conversation_file_metadata WHERE file_path = ?"
|
|
3401
|
+
),
|
|
3402
|
+
upsertFileMetadata: db.prepare(`
|
|
3403
|
+
INSERT INTO conversation_file_metadata
|
|
3404
|
+
(file_path, mtime_ms, file_size, is_agent, agent_entrypoints_key, updated_at)
|
|
3405
|
+
VALUES
|
|
3406
|
+
(@file_path, @mtime_ms, @file_size, @is_agent, @agent_entrypoints_key, @updated_at)
|
|
3407
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
3408
|
+
mtime_ms = excluded.mtime_ms,
|
|
3409
|
+
file_size = excluded.file_size,
|
|
3410
|
+
is_agent = excluded.is_agent,
|
|
3411
|
+
agent_entrypoints_key = excluded.agent_entrypoints_key,
|
|
3412
|
+
updated_at = excluded.updated_at
|
|
3413
|
+
`),
|
|
3289
3414
|
upsertSessionName: db.prepare(`
|
|
3290
3415
|
INSERT INTO session_names (session_id, name, updated_at)
|
|
3291
3416
|
VALUES (?, ?, ?)
|
|
@@ -3326,8 +3451,35 @@ var ConversationCache = class _ConversationCache {
|
|
|
3326
3451
|
getDatabase() {
|
|
3327
3452
|
return this.db;
|
|
3328
3453
|
}
|
|
3329
|
-
|
|
3330
|
-
return this.agentEntrypoints;
|
|
3454
|
+
agentEntrypointsKey() {
|
|
3455
|
+
return [...this.agentEntrypoints].sort().join(",");
|
|
3456
|
+
}
|
|
3457
|
+
classifyAgentFile(filePath, mtimeMs, fileSize) {
|
|
3458
|
+
if (this.agentEntrypoints.size === 0) return false;
|
|
3459
|
+
const entrypointsKey = this.agentEntrypointsKey();
|
|
3460
|
+
const cached2 = this.stmts.getFileMetadata.get(filePath);
|
|
3461
|
+
if (cached2 && cached2.mtime_ms === mtimeMs && cached2.file_size === fileSize && cached2.agent_entrypoints_key === entrypointsKey) {
|
|
3462
|
+
return cached2.is_agent === 1;
|
|
3463
|
+
}
|
|
3464
|
+
const isAgent = isAgentFile(filePath, this.agentEntrypoints);
|
|
3465
|
+
this.stmts.upsertFileMetadata.run({
|
|
3466
|
+
file_path: filePath,
|
|
3467
|
+
mtime_ms: mtimeMs,
|
|
3468
|
+
file_size: fileSize,
|
|
3469
|
+
is_agent: isAgent ? 1 : 0,
|
|
3470
|
+
agent_entrypoints_key: entrypointsKey,
|
|
3471
|
+
updated_at: Date.now()
|
|
3472
|
+
});
|
|
3473
|
+
return isAgent;
|
|
3474
|
+
}
|
|
3475
|
+
isAgentFileCached(filePath) {
|
|
3476
|
+
let s;
|
|
3477
|
+
try {
|
|
3478
|
+
s = (0, import_fs8.statSync)(filePath);
|
|
3479
|
+
} catch {
|
|
3480
|
+
return false;
|
|
3481
|
+
}
|
|
3482
|
+
return this.classifyAgentFile(filePath, s.mtimeMs, s.size);
|
|
3331
3483
|
}
|
|
3332
3484
|
static open(dbPath, tailSize = 10, migrationsDir, options) {
|
|
3333
3485
|
(0, import_fs8.mkdirSync)((0, import_path10.dirname)(dbPath), { recursive: true });
|
|
@@ -3503,11 +3655,9 @@ var ConversationCache = class _ConversationCache {
|
|
|
3503
3655
|
// in conversation-cache.test.ts).
|
|
3504
3656
|
upsertFromScannerMeta(metas) {
|
|
3505
3657
|
const filter = this.filterAgentConversations;
|
|
3506
|
-
const entrypoints = this.agentEntrypoints;
|
|
3507
3658
|
const upsertedIds = [];
|
|
3508
3659
|
const run2 = this.db.transaction((items) => {
|
|
3509
3660
|
for (const m of items) {
|
|
3510
|
-
if (filter && isAgentFile(m.filePath, entrypoints)) continue;
|
|
3511
3661
|
const id = m.sessionId || m.id.split("/").pop()?.replace(/\.jsonl$/, "") || m.id;
|
|
3512
3662
|
const lastActivityMs = m.timestamp ? new Date(m.timestamp).getTime() : null;
|
|
3513
3663
|
let mtimeMs = null;
|
|
@@ -3519,6 +3669,10 @@ var ConversationCache = class _ConversationCache {
|
|
|
3519
3669
|
} catch {
|
|
3520
3670
|
}
|
|
3521
3671
|
const seq = ++this.tailSeq;
|
|
3672
|
+
if (filter && mtimeMs !== null && fileSize !== null && this.classifyAgentFile(m.filePath, mtimeMs, fileSize)) {
|
|
3673
|
+
continue;
|
|
3674
|
+
}
|
|
3675
|
+
const scannerMetaJson = JSON.stringify(m);
|
|
3522
3676
|
this.stmts.upsertFull.run({
|
|
3523
3677
|
id,
|
|
3524
3678
|
file_path: m.filePath,
|
|
@@ -3536,8 +3690,10 @@ var ConversationCache = class _ConversationCache {
|
|
|
3536
3690
|
updated_at: seq,
|
|
3537
3691
|
mtime_ms: mtimeMs,
|
|
3538
3692
|
file_size: fileSize,
|
|
3539
|
-
provider: m.provider ?? CLAUDE_CODE_PROVIDER
|
|
3693
|
+
provider: m.provider ?? CLAUDE_CODE_PROVIDER,
|
|
3694
|
+
scanner_meta_json: scannerMetaJson
|
|
3540
3695
|
});
|
|
3696
|
+
this.stmts.updateScannerCache.run(mtimeMs, fileSize, scannerMetaJson, id);
|
|
3541
3697
|
if (this.fileIndexLoaded) this.fileIndex.set(m.filePath, id);
|
|
3542
3698
|
upsertedIds.push(id);
|
|
3543
3699
|
}
|
|
@@ -3661,6 +3817,21 @@ var ConversationCache = class _ConversationCache {
|
|
|
3661
3817
|
}
|
|
3662
3818
|
return map;
|
|
3663
3819
|
}
|
|
3820
|
+
getScannerStatCache() {
|
|
3821
|
+
const rows = this.stmts.allScannerStatCacheRows.all();
|
|
3822
|
+
const map = /* @__PURE__ */ new Map();
|
|
3823
|
+
for (const r of rows) {
|
|
3824
|
+
try {
|
|
3825
|
+
const meta = JSON.parse(r.scanner_meta_json);
|
|
3826
|
+
map.set(r.file_path, {
|
|
3827
|
+
stat: { mtimeMs: r.mtime_ms, size: r.file_size },
|
|
3828
|
+
meta
|
|
3829
|
+
});
|
|
3830
|
+
} catch {
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
return map;
|
|
3834
|
+
}
|
|
3664
3835
|
getMetaById(id) {
|
|
3665
3836
|
const row = this.stmts.getFullById.get(id);
|
|
3666
3837
|
if (!row) return null;
|
|
@@ -4327,7 +4498,7 @@ function pruneAgentConversations(cache) {
|
|
|
4327
4498
|
missing += 1;
|
|
4328
4499
|
continue;
|
|
4329
4500
|
}
|
|
4330
|
-
if (
|
|
4501
|
+
if (cache.isAgentFileCached(row.file_path)) {
|
|
4331
4502
|
cache.deleteByFilePath(row.file_path);
|
|
4332
4503
|
pruned += 1;
|
|
4333
4504
|
}
|
|
@@ -4721,37 +4892,6 @@ function computeConversationEtag({
|
|
|
4721
4892
|
return `"${digest}"`;
|
|
4722
4893
|
}
|
|
4723
4894
|
|
|
4724
|
-
// src/utils/debounce.ts
|
|
4725
|
-
function debounce(fn, waitMs) {
|
|
4726
|
-
let timer = null;
|
|
4727
|
-
let lastArgs = null;
|
|
4728
|
-
const run2 = () => {
|
|
4729
|
-
timer = null;
|
|
4730
|
-
if (lastArgs) {
|
|
4731
|
-
const args = lastArgs;
|
|
4732
|
-
lastArgs = null;
|
|
4733
|
-
fn(...args);
|
|
4734
|
-
}
|
|
4735
|
-
};
|
|
4736
|
-
const debounced = (...args) => {
|
|
4737
|
-
lastArgs = args;
|
|
4738
|
-
if (timer) clearTimeout(timer);
|
|
4739
|
-
timer = setTimeout(run2, waitMs);
|
|
4740
|
-
};
|
|
4741
|
-
debounced.cancel = () => {
|
|
4742
|
-
if (timer) clearTimeout(timer);
|
|
4743
|
-
timer = null;
|
|
4744
|
-
lastArgs = null;
|
|
4745
|
-
};
|
|
4746
|
-
debounced.flush = () => {
|
|
4747
|
-
if (timer) {
|
|
4748
|
-
clearTimeout(timer);
|
|
4749
|
-
run2();
|
|
4750
|
-
}
|
|
4751
|
-
};
|
|
4752
|
-
return debounced;
|
|
4753
|
-
}
|
|
4754
|
-
|
|
4755
4895
|
// src/utils/dates.ts
|
|
4756
4896
|
var import_date_fns = require("date-fns");
|
|
4757
4897
|
function parseIsoDateOrNull(value) {
|
|
@@ -4898,6 +5038,7 @@ var WSHub = class {
|
|
|
4898
5038
|
var BROWSE_SYSTEM_PROMPT = (browseRoot) => `You are working within the project boundary: ${browseRoot}. Do not read, write, or execute commands that access files or directories outside this boundary.`;
|
|
4899
5039
|
var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
|
|
4900
5040
|
var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
5041
|
+
var START_READY_TIMEOUT_MS = 15e3;
|
|
4901
5042
|
function parseIncludeAgentsEnv(raw) {
|
|
4902
5043
|
if (raw === void 0) return false;
|
|
4903
5044
|
const v = raw.trim().toLowerCase();
|
|
@@ -4966,6 +5107,7 @@ var StreamerServer = class {
|
|
|
4966
5107
|
sessionInputAttempts = /* @__PURE__ */ new Map();
|
|
4967
5108
|
ptyGracePeriodMs;
|
|
4968
5109
|
defaultSystemPrompt;
|
|
5110
|
+
defaultPermissionMode;
|
|
4969
5111
|
// Map of sessionId → grace timer; fires to kill PTY after WS disconnect
|
|
4970
5112
|
ptyGraceTimers = /* @__PURE__ */ new Map();
|
|
4971
5113
|
// Map of sessionId → set of subscribed WS clients
|
|
@@ -5014,6 +5156,7 @@ var StreamerServer = class {
|
|
|
5014
5156
|
this.codexRoots = config.codexRoots ?? [(0, import_path13.join)((0, import_os6.homedir)(), ".codex", "sessions")];
|
|
5015
5157
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
5016
5158
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
5159
|
+
this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
|
|
5017
5160
|
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path13.join)((0, import_os6.homedir)(), ".threadbase", "cache");
|
|
5018
5161
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
5019
5162
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
@@ -5443,9 +5586,9 @@ var StreamerServer = class {
|
|
|
5443
5586
|
);
|
|
5444
5587
|
this.scannerPersistenceDisabled = true;
|
|
5445
5588
|
}
|
|
5446
|
-
const warmupScanner = this.newScanner();
|
|
5447
|
-
this.allScanners.add(warmupScanner);
|
|
5448
5589
|
const warmupStatCache = this.buildStatCache(null);
|
|
5590
|
+
const warmupScanner = this.newScanner(warmupStatCache ? { persistent: false } : void 0);
|
|
5591
|
+
this.allScanners.add(warmupScanner);
|
|
5449
5592
|
const shouldEmitProgress = createScanProgressThrottle();
|
|
5450
5593
|
const scanOpts = {
|
|
5451
5594
|
...this.scanProfiles ? { profiles: this.scanProfiles } : {},
|
|
@@ -5885,6 +6028,10 @@ var StreamerServer = class {
|
|
|
5885
6028
|
}
|
|
5886
6029
|
buildStatCache(previousScanner) {
|
|
5887
6030
|
if (!this.cache) return void 0;
|
|
6031
|
+
if (!previousScanner) {
|
|
6032
|
+
const persisted = this.cache.getScannerStatCache();
|
|
6033
|
+
return persisted.size > 0 ? persisted : void 0;
|
|
6034
|
+
}
|
|
5888
6035
|
const dbStats = this.cache.getFileStats();
|
|
5889
6036
|
if (dbStats.size === 0) return void 0;
|
|
5890
6037
|
const metaByPath = /* @__PURE__ */ new Map();
|
|
@@ -5914,9 +6061,9 @@ var StreamerServer = class {
|
|
|
5914
6061
|
// this — its per-file refreshFile (in findConversationByUuid) already
|
|
5915
6062
|
// reconciles the one conversation being requested, so paying a full-tree
|
|
5916
6063
|
// rescan just because some OTHER file changed is the stall this avoids.
|
|
5917
|
-
newScanner() {
|
|
6064
|
+
newScanner(options) {
|
|
5918
6065
|
return new import_scanner2.ConversationScanner(
|
|
5919
|
-
this.scannerPersistenceDisabled ? { persistent: false } : void 0
|
|
6066
|
+
options ?? (this.scannerPersistenceDisabled ? { persistent: false } : void 0)
|
|
5920
6067
|
);
|
|
5921
6068
|
}
|
|
5922
6069
|
async getScanner(skipStaleRescan = false) {
|
|
@@ -5935,7 +6082,7 @@ var StreamerServer = class {
|
|
|
5935
6082
|
}
|
|
5936
6083
|
this.scannerStale = false;
|
|
5937
6084
|
const statCache = this.buildStatCache(this.scanner);
|
|
5938
|
-
this.scanner = this.newScanner();
|
|
6085
|
+
this.scanner = this.newScanner(statCache ? { persistent: false } : void 0);
|
|
5939
6086
|
this.allScanners.add(this.scanner);
|
|
5940
6087
|
this.scannerReady = this.scanner.scan({
|
|
5941
6088
|
...this.scanProfiles ? { profiles: this.scanProfiles } : {},
|
|
@@ -5958,9 +6105,8 @@ var StreamerServer = class {
|
|
|
5958
6105
|
this.scannerReady = null;
|
|
5959
6106
|
return this.getScanner();
|
|
5960
6107
|
}
|
|
5961
|
-
// refresh=1's scan: reuse the WARM
|
|
5962
|
-
//
|
|
5963
|
-
// with fullRescan:true — the escape hatch that bypasses the scanner's
|
|
6108
|
+
// refresh=1's scan: reuse the WARM scanner and re-run its scan with
|
|
6109
|
+
// fullRescan:true — the escape hatch that bypasses the scanner's
|
|
5964
6110
|
// dir-mtime discovery gate, since an explicit user pull-to-refresh is exactly
|
|
5965
6111
|
// the "don't trust the gate, check disk for real" signal. Unlike
|
|
5966
6112
|
// getFreshScanner() this does NOT discard the warm scanner. scannerReady is
|
|
@@ -6348,7 +6494,8 @@ var StreamerServer = class {
|
|
|
6348
6494
|
provider,
|
|
6349
6495
|
projectPath,
|
|
6350
6496
|
projectName: body.projectName,
|
|
6351
|
-
branch: body.branch
|
|
6497
|
+
branch: body.branch,
|
|
6498
|
+
permissionMode: this.defaultPermissionMode
|
|
6352
6499
|
});
|
|
6353
6500
|
this.sessionStore.addManaged(session);
|
|
6354
6501
|
void this.watchConversationFile(sessionId);
|
|
@@ -6672,7 +6819,8 @@ var StreamerServer = class {
|
|
|
6672
6819
|
const session = await this.ptyManager.start(convId, {
|
|
6673
6820
|
projectPath,
|
|
6674
6821
|
projectName,
|
|
6675
|
-
branch
|
|
6822
|
+
branch,
|
|
6823
|
+
permissionMode: this.defaultPermissionMode
|
|
6676
6824
|
});
|
|
6677
6825
|
this.sessionStore.addManaged(session);
|
|
6678
6826
|
void this.watchConversationFile(session.id);
|
|
@@ -6742,10 +6890,35 @@ var StreamerServer = class {
|
|
|
6742
6890
|
provider,
|
|
6743
6891
|
projectPath: resolvedPath,
|
|
6744
6892
|
projectName: body.projectName,
|
|
6745
|
-
systemPrompt: systemPromptParts.join("\n")
|
|
6893
|
+
systemPrompt: systemPromptParts.join("\n"),
|
|
6894
|
+
permissionMode: this.defaultPermissionMode
|
|
6746
6895
|
});
|
|
6747
6896
|
this.sessionStore.addManaged(session);
|
|
6748
|
-
|
|
6897
|
+
const readyOrFailed = new Promise((resolve2) => {
|
|
6898
|
+
const handler = (status) => {
|
|
6899
|
+
if (status === "waiting_input" || status === "idle") {
|
|
6900
|
+
this.sessionStatusBus.off(`status:${session.id}`, handler);
|
|
6901
|
+
resolve2(status === "waiting_input" ? "ready" : "failed");
|
|
6902
|
+
}
|
|
6903
|
+
};
|
|
6904
|
+
this.sessionStatusBus.on(`status:${session.id}`, handler);
|
|
6905
|
+
});
|
|
6906
|
+
const timeoutPromise = new Promise(
|
|
6907
|
+
(resolve2) => setTimeout(() => resolve2("timeout"), START_READY_TIMEOUT_MS)
|
|
6908
|
+
);
|
|
6909
|
+
const outcome = await Promise.race([readyOrFailed, timeoutPromise]);
|
|
6910
|
+
const current = this.sessionStore.get(session.id, this.ptyAttachedIds());
|
|
6911
|
+
if (outcome === "ready" && current) {
|
|
6912
|
+
json(res, 200, { session: current });
|
|
6913
|
+
} else if (outcome === "failed" && current) {
|
|
6914
|
+
json(res, 502, {
|
|
6915
|
+
id: session.id,
|
|
6916
|
+
status: "idle",
|
|
6917
|
+
error: current.failureReason ?? "Session exited before becoming ready"
|
|
6918
|
+
});
|
|
6919
|
+
} else {
|
|
6920
|
+
json(res, 202, { id: session.id, status: "pending" });
|
|
6921
|
+
}
|
|
6749
6922
|
if (provider === CODEX_CLI_PROVIDER) {
|
|
6750
6923
|
this.watchForCodexRollout(session.id, resolvedPath);
|
|
6751
6924
|
} else {
|