@threadbase-sh/streamer 1.24.6 → 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 +3670 -4356
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +245 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +245 -69
- 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 +2 -2
package/dist/index.js
CHANGED
|
@@ -342,6 +342,16 @@ function loadTailSize() {
|
|
|
342
342
|
}
|
|
343
343
|
return void 0;
|
|
344
344
|
}
|
|
345
|
+
function loadDefaultPermissionMode() {
|
|
346
|
+
try {
|
|
347
|
+
const content = readFileSync(configFile(), "utf-8");
|
|
348
|
+
const match = content.match(/default_permission_mode:\s*(\S+)/);
|
|
349
|
+
const value = match?.[1]?.trim();
|
|
350
|
+
if (value === "acceptEdits" || value === "manual") return value;
|
|
351
|
+
} catch {
|
|
352
|
+
}
|
|
353
|
+
return void 0;
|
|
354
|
+
}
|
|
345
355
|
function validatePublicUrl(raw) {
|
|
346
356
|
let parsed;
|
|
347
357
|
try {
|
|
@@ -1301,6 +1311,37 @@ function detectShellPrompt(lines) {
|
|
|
1301
1311
|
return null;
|
|
1302
1312
|
}
|
|
1303
1313
|
|
|
1314
|
+
// src/utils/debounce.ts
|
|
1315
|
+
function debounce(fn, waitMs) {
|
|
1316
|
+
let timer = null;
|
|
1317
|
+
let lastArgs = null;
|
|
1318
|
+
const run2 = () => {
|
|
1319
|
+
timer = null;
|
|
1320
|
+
if (lastArgs) {
|
|
1321
|
+
const args = lastArgs;
|
|
1322
|
+
lastArgs = null;
|
|
1323
|
+
fn(...args);
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
const debounced = (...args) => {
|
|
1327
|
+
lastArgs = args;
|
|
1328
|
+
if (timer) clearTimeout(timer);
|
|
1329
|
+
timer = setTimeout(run2, waitMs);
|
|
1330
|
+
};
|
|
1331
|
+
debounced.cancel = () => {
|
|
1332
|
+
if (timer) clearTimeout(timer);
|
|
1333
|
+
timer = null;
|
|
1334
|
+
lastArgs = null;
|
|
1335
|
+
};
|
|
1336
|
+
debounced.flush = () => {
|
|
1337
|
+
if (timer) {
|
|
1338
|
+
clearTimeout(timer);
|
|
1339
|
+
run2();
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
return debounced;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1304
1345
|
// src/pty-manager.ts
|
|
1305
1346
|
var OUTPUT_BUFFER_MAX2 = 65536;
|
|
1306
1347
|
var PTY_COLS2 = 120;
|
|
@@ -1308,11 +1349,13 @@ var PTY_ROWS2 = 40;
|
|
|
1308
1349
|
var SCREEN_SCROLLBACK2 = 1e3;
|
|
1309
1350
|
var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
|
|
1310
1351
|
var PROMPT_MARKER_FALLBACK_MS = 1e4;
|
|
1352
|
+
var QUIET_DETECT_MS = 500;
|
|
1311
1353
|
function buildPasteBytes(input) {
|
|
1312
1354
|
return `\x1B[200~${input}\x1B[201~`;
|
|
1313
1355
|
}
|
|
1314
1356
|
var SUBMIT_BYTES2 = "\r";
|
|
1315
1357
|
var SUBMIT_DELAY_MS = 16;
|
|
1358
|
+
var SUBMIT_MAX_WAIT_MS = 500;
|
|
1316
1359
|
function digestBytes2(s) {
|
|
1317
1360
|
const escaped = s.replace(new RegExp(String.fromCharCode(27), "g"), "\\x1b").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
|
1318
1361
|
if (escaped.length <= 200) return escaped;
|
|
@@ -1389,6 +1432,10 @@ var PTYManager = class {
|
|
|
1389
1432
|
// to a given input or fell silent. Reset on dispose().
|
|
1390
1433
|
chunkIndex = /* @__PURE__ */ new Map();
|
|
1391
1434
|
lastChunkAt = /* @__PURE__ */ new Map();
|
|
1435
|
+
// Per-session debounced "went quiet" checker, re-armed on every chunk. Fires
|
|
1436
|
+
// QUIET_DETECT_MS after the last chunk so ready/prompt detection doesn't
|
|
1437
|
+
// wait for another chunk that may never arrive (Claude blocked on input).
|
|
1438
|
+
quietCheckers = /* @__PURE__ */ new Map();
|
|
1392
1439
|
// In-flight start()/startFresh() calls keyed by sessionId. A second
|
|
1393
1440
|
// concurrent resume for the same session (double-tap, client retry) awaits
|
|
1394
1441
|
// the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
|
|
@@ -1404,16 +1451,19 @@ var PTYManager = class {
|
|
|
1404
1451
|
}
|
|
1405
1452
|
// Resume an existing Claude conversation. sessionId is the JSONL UUID.
|
|
1406
1453
|
//
|
|
1407
|
-
//
|
|
1408
|
-
// Both suppress
|
|
1409
|
-
//
|
|
1410
|
-
//
|
|
1411
|
-
//
|
|
1412
|
-
//
|
|
1454
|
+
// options.permissionMode defaults to `acceptEdits` rather than
|
|
1455
|
+
// `--dangerously-skip-permissions`/`bypassPermissions`. Both suppress
|
|
1456
|
+
// file-edit prompts, but in an interactive (TUI) launch the skip-permissions
|
|
1457
|
+
// flag renders a blocking "Bypass Permissions mode" warning menu on every
|
|
1458
|
+
// boot that no known ~/.claude.json flag suppressed (as of Claude CLI
|
|
1459
|
+
// v2.1.x) — the session never reaches a usable prompt, so the mobile app
|
|
1460
|
+
// shows an empty/stuck screen. `acceptEdits` auto-approves file edits
|
|
1413
1461
|
// without that warning gate, while still prompting for shell commands.
|
|
1462
|
+
// `manual` (prompt for everything) is the only other mode callers may pass;
|
|
1463
|
+
// `bypassPermissions`/`plan`/`dontAsk`/`auto` are not supported here.
|
|
1414
1464
|
// (The other first-run gates — onboarding/theme, workspace trust,
|
|
1415
1465
|
// custom-API-key — are cleared by the seeded ~/.claude.json in
|
|
1416
|
-
// docker/entrypoint.sh.) startFresh() uses the same
|
|
1466
|
+
// docker/entrypoint.sh.) startFresh() uses the same default for the same reason.
|
|
1417
1467
|
async start(sessionId, options) {
|
|
1418
1468
|
const existing = this.sessions.get(sessionId);
|
|
1419
1469
|
if (existing) return toPublicSession2(existing);
|
|
@@ -1432,7 +1482,7 @@ var PTYManager = class {
|
|
|
1432
1482
|
resolveClaudeExe(),
|
|
1433
1483
|
[
|
|
1434
1484
|
"--permission-mode",
|
|
1435
|
-
"acceptEdits",
|
|
1485
|
+
options.permissionMode ?? "acceptEdits",
|
|
1436
1486
|
"--settings",
|
|
1437
1487
|
'{"spinnerTipsEnabled":false}',
|
|
1438
1488
|
"--resume",
|
|
@@ -1481,7 +1531,7 @@ var PTYManager = class {
|
|
|
1481
1531
|
const projectName = options.projectName ?? basename2(options.projectPath);
|
|
1482
1532
|
const args = [
|
|
1483
1533
|
"--permission-mode",
|
|
1484
|
-
"acceptEdits",
|
|
1534
|
+
options.permissionMode ?? "acceptEdits",
|
|
1485
1535
|
"--settings",
|
|
1486
1536
|
'{"spinnerTipsEnabled":false}',
|
|
1487
1537
|
"--session-id",
|
|
@@ -1575,9 +1625,20 @@ var PTYManager = class {
|
|
|
1575
1625
|
session.promptCount++;
|
|
1576
1626
|
return session.promptCount;
|
|
1577
1627
|
}
|
|
1578
|
-
// Two-step paste-then-submit. Writes the bracketed-paste body,
|
|
1579
|
-
//
|
|
1580
|
-
//
|
|
1628
|
+
// Two-step paste-then-submit. Writes the bracketed-paste body, then waits
|
|
1629
|
+
// for the PTY to go quiet before writing \r. See buildPasteBytes() for why
|
|
1630
|
+
// the split matters.
|
|
1631
|
+
//
|
|
1632
|
+
// The wait is quiescence-based, not a flat delay: a fixed SUBMIT_DELAY_MS
|
|
1633
|
+
// timer (the original fix) still races a TUI that's mid-redraw of its own
|
|
1634
|
+
// output (e.g. re-painting right after posting a question) when the paste
|
|
1635
|
+
// lands — the timer can elapse and fire \r while the TUI is still busy,
|
|
1636
|
+
// and that \r gets absorbed by the redraw instead of submitting (see
|
|
1637
|
+
// 2026-07 session 14dda340: a "Yes" reply was accepted into the input line
|
|
1638
|
+
// but never landed as a submitted JSONL turn). Polling in SUBMIT_DELAY_MS
|
|
1639
|
+
// steps and only submitting once lastChunkAt hasn't advanced for a full
|
|
1640
|
+
// step gives the TUI as many extra ticks as it needs, capped at
|
|
1641
|
+
// SUBMIT_MAX_WAIT_MS so a silent/wedged PTY still gets its \r eventually.
|
|
1581
1642
|
writeSubmit(sessionId, session, input, path, promptCount) {
|
|
1582
1643
|
const pasteBytes = buildPasteBytes(input);
|
|
1583
1644
|
this.log.info(
|
|
@@ -1592,12 +1653,21 @@ var PTYManager = class {
|
|
|
1592
1653
|
phase: "paste"
|
|
1593
1654
|
}
|
|
1594
1655
|
);
|
|
1656
|
+
const pasteAt = Date.now();
|
|
1595
1657
|
session.process.write(pasteBytes);
|
|
1596
|
-
|
|
1658
|
+
const trySubmit = () => {
|
|
1597
1659
|
const current = this.sessions.get(sessionId);
|
|
1598
1660
|
if (!current || current !== session) return;
|
|
1661
|
+
const now = Date.now();
|
|
1662
|
+
const lastChunk = this.lastChunkAt.get(sessionId) ?? pasteAt;
|
|
1663
|
+
const quiet = now - lastChunk >= SUBMIT_DELAY_MS;
|
|
1664
|
+
const timedOut = now - pasteAt >= SUBMIT_MAX_WAIT_MS;
|
|
1665
|
+
if (!quiet && !timedOut) {
|
|
1666
|
+
setTimeout(trySubmit, SUBMIT_DELAY_MS);
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1599
1669
|
this.log.info(
|
|
1600
|
-
`[pty.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\r`,
|
|
1670
|
+
`[pty.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\r waitedMs=${now - pasteAt} timedOut=${timedOut}`,
|
|
1601
1671
|
{
|
|
1602
1672
|
event: "pty.input_write",
|
|
1603
1673
|
sessionId,
|
|
@@ -1605,11 +1675,14 @@ var PTYManager = class {
|
|
|
1605
1675
|
byteLen: SUBMIT_BYTES2.length,
|
|
1606
1676
|
digest: "\\r",
|
|
1607
1677
|
path,
|
|
1608
|
-
phase: "submit"
|
|
1678
|
+
phase: "submit",
|
|
1679
|
+
waitedMs: now - pasteAt,
|
|
1680
|
+
timedOut
|
|
1609
1681
|
}
|
|
1610
1682
|
);
|
|
1611
1683
|
current.process.write(SUBMIT_BYTES2);
|
|
1612
|
-
}
|
|
1684
|
+
};
|
|
1685
|
+
setTimeout(trySubmit, SUBMIT_DELAY_MS);
|
|
1613
1686
|
}
|
|
1614
1687
|
// Drain any inputs that were sent while the session was still pendingReady,
|
|
1615
1688
|
// writing them in arrival order now that Claude is at its prompt.
|
|
@@ -1658,6 +1731,8 @@ var PTYManager = class {
|
|
|
1658
1731
|
this.permissionOpen.delete(sessionId);
|
|
1659
1732
|
this.lastScreenQuestionKey.delete(sessionId);
|
|
1660
1733
|
this.shellPromptOpen.delete(sessionId);
|
|
1734
|
+
this.quietCheckers.get(sessionId)?.cancel();
|
|
1735
|
+
this.quietCheckers.delete(sessionId);
|
|
1661
1736
|
try {
|
|
1662
1737
|
session.process.kill("SIGINT");
|
|
1663
1738
|
} catch {
|
|
@@ -1717,6 +1792,8 @@ var PTYManager = class {
|
|
|
1717
1792
|
this.firstChunkAt.clear();
|
|
1718
1793
|
this.chunkIndex.clear();
|
|
1719
1794
|
this.lastChunkAt.clear();
|
|
1795
|
+
for (const quiet of this.quietCheckers.values()) quiet.cancel();
|
|
1796
|
+
this.quietCheckers.clear();
|
|
1720
1797
|
this.permissionOpen.clear();
|
|
1721
1798
|
this.lastScreenQuestionKey.clear();
|
|
1722
1799
|
this.shellPromptOpen.clear();
|
|
@@ -1770,6 +1847,12 @@ var PTYManager = class {
|
|
|
1770
1847
|
err
|
|
1771
1848
|
});
|
|
1772
1849
|
});
|
|
1850
|
+
let quiet = this.quietCheckers.get(sessionId);
|
|
1851
|
+
if (!quiet) {
|
|
1852
|
+
quiet = debounce(() => this.handleQuiet(sessionId), QUIET_DETECT_MS);
|
|
1853
|
+
this.quietCheckers.set(sessionId, quiet);
|
|
1854
|
+
}
|
|
1855
|
+
quiet();
|
|
1773
1856
|
}
|
|
1774
1857
|
// Detect permission gates (OSC 777 + scraped options) and AskUserQuestion
|
|
1775
1858
|
// menus from the rendered screen, firing the additive callbacks. Async because
|
|
@@ -1845,6 +1928,24 @@ var PTYManager = class {
|
|
|
1845
1928
|
}
|
|
1846
1929
|
}
|
|
1847
1930
|
}
|
|
1931
|
+
// Fired QUIET_DETECT_MS after the last PTY chunk. Re-runs the same
|
|
1932
|
+
// ready/prompt detection handleOutput() runs per-chunk, using the last
|
|
1933
|
+
// rendered output — a session blocked on a prompt (or an unmarked boot
|
|
1934
|
+
// screen) may never produce another chunk to trigger detection otherwise.
|
|
1935
|
+
handleQuiet(sessionId) {
|
|
1936
|
+
const session = this.sessions.get(sessionId);
|
|
1937
|
+
if (session?.status !== "running") return;
|
|
1938
|
+
if (this.pendingReady.has(sessionId)) {
|
|
1939
|
+
this.markReady(sessionId, session, "quiet:timeout");
|
|
1940
|
+
}
|
|
1941
|
+
this.detectLivePrompts(sessionId, "", session.lastOutput).catch((err) => {
|
|
1942
|
+
this.log.warn("[pty.prompt_detect] failed", {
|
|
1943
|
+
event: "pty.prompt_detect_failed",
|
|
1944
|
+
sessionId,
|
|
1945
|
+
err
|
|
1946
|
+
});
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1848
1949
|
// Transition a session from "running" to "waiting_input", clear pendingReady,
|
|
1849
1950
|
// and flush any queued input. Idempotent: callers can invoke at any chunk.
|
|
1850
1951
|
markReady(sessionId, session, reason) {
|
|
@@ -1885,6 +1986,8 @@ var PTYManager = class {
|
|
|
1885
1986
|
this.permissionOpen.delete(sessionId);
|
|
1886
1987
|
this.lastScreenQuestionKey.delete(sessionId);
|
|
1887
1988
|
this.shellPromptOpen.delete(sessionId);
|
|
1989
|
+
this.quietCheckers.get(sessionId)?.cancel();
|
|
1990
|
+
this.quietCheckers.delete(sessionId);
|
|
1888
1991
|
}
|
|
1889
1992
|
};
|
|
1890
1993
|
function toPublicSession2(s) {
|
|
@@ -3186,11 +3289,11 @@ var ConversationCache = class _ConversationCache {
|
|
|
3186
3289
|
INSERT INTO conversation_meta
|
|
3187
3290
|
(id, file_path, project_path, project_name, title, model, account, branch,
|
|
3188
3291
|
message_count, last_activity, first_message, last_message, preview, updated_at,
|
|
3189
|
-
mtime_ms, file_size, provider)
|
|
3292
|
+
mtime_ms, file_size, provider, scanner_meta_json)
|
|
3190
3293
|
VALUES
|
|
3191
3294
|
(@id, @file_path, @project_path, @project_name, @title, @model, @account, @branch,
|
|
3192
3295
|
@message_count, @last_activity, @first_message, @last_message, @preview, @updated_at,
|
|
3193
|
-
@mtime_ms, @file_size, @provider)
|
|
3296
|
+
@mtime_ms, @file_size, @provider, @scanner_meta_json)
|
|
3194
3297
|
ON CONFLICT(id) DO UPDATE SET
|
|
3195
3298
|
file_path = excluded.file_path,
|
|
3196
3299
|
project_path = excluded.project_path,
|
|
@@ -3210,7 +3313,8 @@ var ConversationCache = class _ConversationCache {
|
|
|
3210
3313
|
updated_at = excluded.updated_at,
|
|
3211
3314
|
mtime_ms = excluded.mtime_ms,
|
|
3212
3315
|
file_size = excluded.file_size,
|
|
3213
|
-
provider = excluded.provider
|
|
3316
|
+
provider = excluded.provider,
|
|
3317
|
+
scanner_meta_json = excluded.scanner_meta_json
|
|
3214
3318
|
WHERE conversation_meta.updated_at < excluded.updated_at
|
|
3215
3319
|
`),
|
|
3216
3320
|
getTail: db.prepare("SELECT * FROM conversation_tail WHERE conversation_id = ?"),
|
|
@@ -3247,6 +3351,27 @@ var ConversationCache = class _ConversationCache {
|
|
|
3247
3351
|
allFileStats: db.prepare(
|
|
3248
3352
|
"SELECT file_path, mtime_ms, file_size FROM conversation_meta WHERE mtime_ms IS NOT NULL AND file_size IS NOT NULL"
|
|
3249
3353
|
),
|
|
3354
|
+
allScannerStatCacheRows: db.prepare(
|
|
3355
|
+
"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"
|
|
3356
|
+
),
|
|
3357
|
+
updateScannerCache: db.prepare(
|
|
3358
|
+
"UPDATE conversation_meta SET mtime_ms = ?, file_size = ?, scanner_meta_json = ? WHERE id = ?"
|
|
3359
|
+
),
|
|
3360
|
+
getFileMetadata: db.prepare(
|
|
3361
|
+
"SELECT mtime_ms, file_size, is_agent, agent_entrypoints_key FROM conversation_file_metadata WHERE file_path = ?"
|
|
3362
|
+
),
|
|
3363
|
+
upsertFileMetadata: db.prepare(`
|
|
3364
|
+
INSERT INTO conversation_file_metadata
|
|
3365
|
+
(file_path, mtime_ms, file_size, is_agent, agent_entrypoints_key, updated_at)
|
|
3366
|
+
VALUES
|
|
3367
|
+
(@file_path, @mtime_ms, @file_size, @is_agent, @agent_entrypoints_key, @updated_at)
|
|
3368
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
3369
|
+
mtime_ms = excluded.mtime_ms,
|
|
3370
|
+
file_size = excluded.file_size,
|
|
3371
|
+
is_agent = excluded.is_agent,
|
|
3372
|
+
agent_entrypoints_key = excluded.agent_entrypoints_key,
|
|
3373
|
+
updated_at = excluded.updated_at
|
|
3374
|
+
`),
|
|
3250
3375
|
upsertSessionName: db.prepare(`
|
|
3251
3376
|
INSERT INTO session_names (session_id, name, updated_at)
|
|
3252
3377
|
VALUES (?, ?, ?)
|
|
@@ -3287,8 +3412,35 @@ var ConversationCache = class _ConversationCache {
|
|
|
3287
3412
|
getDatabase() {
|
|
3288
3413
|
return this.db;
|
|
3289
3414
|
}
|
|
3290
|
-
|
|
3291
|
-
return this.agentEntrypoints;
|
|
3415
|
+
agentEntrypointsKey() {
|
|
3416
|
+
return [...this.agentEntrypoints].sort().join(",");
|
|
3417
|
+
}
|
|
3418
|
+
classifyAgentFile(filePath, mtimeMs, fileSize) {
|
|
3419
|
+
if (this.agentEntrypoints.size === 0) return false;
|
|
3420
|
+
const entrypointsKey = this.agentEntrypointsKey();
|
|
3421
|
+
const cached2 = this.stmts.getFileMetadata.get(filePath);
|
|
3422
|
+
if (cached2 && cached2.mtime_ms === mtimeMs && cached2.file_size === fileSize && cached2.agent_entrypoints_key === entrypointsKey) {
|
|
3423
|
+
return cached2.is_agent === 1;
|
|
3424
|
+
}
|
|
3425
|
+
const isAgent = isAgentFile(filePath, this.agentEntrypoints);
|
|
3426
|
+
this.stmts.upsertFileMetadata.run({
|
|
3427
|
+
file_path: filePath,
|
|
3428
|
+
mtime_ms: mtimeMs,
|
|
3429
|
+
file_size: fileSize,
|
|
3430
|
+
is_agent: isAgent ? 1 : 0,
|
|
3431
|
+
agent_entrypoints_key: entrypointsKey,
|
|
3432
|
+
updated_at: Date.now()
|
|
3433
|
+
});
|
|
3434
|
+
return isAgent;
|
|
3435
|
+
}
|
|
3436
|
+
isAgentFileCached(filePath) {
|
|
3437
|
+
let s;
|
|
3438
|
+
try {
|
|
3439
|
+
s = statSync2(filePath);
|
|
3440
|
+
} catch {
|
|
3441
|
+
return false;
|
|
3442
|
+
}
|
|
3443
|
+
return this.classifyAgentFile(filePath, s.mtimeMs, s.size);
|
|
3292
3444
|
}
|
|
3293
3445
|
static open(dbPath, tailSize = 10, migrationsDir, options) {
|
|
3294
3446
|
mkdirSync2(dirname6(dbPath), { recursive: true });
|
|
@@ -3464,11 +3616,9 @@ var ConversationCache = class _ConversationCache {
|
|
|
3464
3616
|
// in conversation-cache.test.ts).
|
|
3465
3617
|
upsertFromScannerMeta(metas) {
|
|
3466
3618
|
const filter = this.filterAgentConversations;
|
|
3467
|
-
const entrypoints = this.agentEntrypoints;
|
|
3468
3619
|
const upsertedIds = [];
|
|
3469
3620
|
const run2 = this.db.transaction((items) => {
|
|
3470
3621
|
for (const m of items) {
|
|
3471
|
-
if (filter && isAgentFile(m.filePath, entrypoints)) continue;
|
|
3472
3622
|
const id = m.sessionId || m.id.split("/").pop()?.replace(/\.jsonl$/, "") || m.id;
|
|
3473
3623
|
const lastActivityMs = m.timestamp ? new Date(m.timestamp).getTime() : null;
|
|
3474
3624
|
let mtimeMs = null;
|
|
@@ -3480,6 +3630,10 @@ var ConversationCache = class _ConversationCache {
|
|
|
3480
3630
|
} catch {
|
|
3481
3631
|
}
|
|
3482
3632
|
const seq = ++this.tailSeq;
|
|
3633
|
+
if (filter && mtimeMs !== null && fileSize !== null && this.classifyAgentFile(m.filePath, mtimeMs, fileSize)) {
|
|
3634
|
+
continue;
|
|
3635
|
+
}
|
|
3636
|
+
const scannerMetaJson = JSON.stringify(m);
|
|
3483
3637
|
this.stmts.upsertFull.run({
|
|
3484
3638
|
id,
|
|
3485
3639
|
file_path: m.filePath,
|
|
@@ -3497,8 +3651,10 @@ var ConversationCache = class _ConversationCache {
|
|
|
3497
3651
|
updated_at: seq,
|
|
3498
3652
|
mtime_ms: mtimeMs,
|
|
3499
3653
|
file_size: fileSize,
|
|
3500
|
-
provider: m.provider ?? CLAUDE_CODE_PROVIDER
|
|
3654
|
+
provider: m.provider ?? CLAUDE_CODE_PROVIDER,
|
|
3655
|
+
scanner_meta_json: scannerMetaJson
|
|
3501
3656
|
});
|
|
3657
|
+
this.stmts.updateScannerCache.run(mtimeMs, fileSize, scannerMetaJson, id);
|
|
3502
3658
|
if (this.fileIndexLoaded) this.fileIndex.set(m.filePath, id);
|
|
3503
3659
|
upsertedIds.push(id);
|
|
3504
3660
|
}
|
|
@@ -3622,6 +3778,21 @@ var ConversationCache = class _ConversationCache {
|
|
|
3622
3778
|
}
|
|
3623
3779
|
return map;
|
|
3624
3780
|
}
|
|
3781
|
+
getScannerStatCache() {
|
|
3782
|
+
const rows = this.stmts.allScannerStatCacheRows.all();
|
|
3783
|
+
const map = /* @__PURE__ */ new Map();
|
|
3784
|
+
for (const r of rows) {
|
|
3785
|
+
try {
|
|
3786
|
+
const meta = JSON.parse(r.scanner_meta_json);
|
|
3787
|
+
map.set(r.file_path, {
|
|
3788
|
+
stat: { mtimeMs: r.mtime_ms, size: r.file_size },
|
|
3789
|
+
meta
|
|
3790
|
+
});
|
|
3791
|
+
} catch {
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
return map;
|
|
3795
|
+
}
|
|
3625
3796
|
getMetaById(id) {
|
|
3626
3797
|
const row = this.stmts.getFullById.get(id);
|
|
3627
3798
|
if (!row) return null;
|
|
@@ -4288,7 +4459,7 @@ function pruneAgentConversations(cache) {
|
|
|
4288
4459
|
missing += 1;
|
|
4289
4460
|
continue;
|
|
4290
4461
|
}
|
|
4291
|
-
if (
|
|
4462
|
+
if (cache.isAgentFileCached(row.file_path)) {
|
|
4292
4463
|
cache.deleteByFilePath(row.file_path);
|
|
4293
4464
|
pruned += 1;
|
|
4294
4465
|
}
|
|
@@ -4682,37 +4853,6 @@ function computeConversationEtag({
|
|
|
4682
4853
|
return `"${digest}"`;
|
|
4683
4854
|
}
|
|
4684
4855
|
|
|
4685
|
-
// src/utils/debounce.ts
|
|
4686
|
-
function debounce(fn, waitMs) {
|
|
4687
|
-
let timer = null;
|
|
4688
|
-
let lastArgs = null;
|
|
4689
|
-
const run2 = () => {
|
|
4690
|
-
timer = null;
|
|
4691
|
-
if (lastArgs) {
|
|
4692
|
-
const args = lastArgs;
|
|
4693
|
-
lastArgs = null;
|
|
4694
|
-
fn(...args);
|
|
4695
|
-
}
|
|
4696
|
-
};
|
|
4697
|
-
const debounced = (...args) => {
|
|
4698
|
-
lastArgs = args;
|
|
4699
|
-
if (timer) clearTimeout(timer);
|
|
4700
|
-
timer = setTimeout(run2, waitMs);
|
|
4701
|
-
};
|
|
4702
|
-
debounced.cancel = () => {
|
|
4703
|
-
if (timer) clearTimeout(timer);
|
|
4704
|
-
timer = null;
|
|
4705
|
-
lastArgs = null;
|
|
4706
|
-
};
|
|
4707
|
-
debounced.flush = () => {
|
|
4708
|
-
if (timer) {
|
|
4709
|
-
clearTimeout(timer);
|
|
4710
|
-
run2();
|
|
4711
|
-
}
|
|
4712
|
-
};
|
|
4713
|
-
return debounced;
|
|
4714
|
-
}
|
|
4715
|
-
|
|
4716
4856
|
// src/utils/dates.ts
|
|
4717
4857
|
import { compareDesc, isValid, parseISO } from "date-fns";
|
|
4718
4858
|
function parseIsoDateOrNull(value) {
|
|
@@ -4859,6 +4999,7 @@ var WSHub = class {
|
|
|
4859
4999
|
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.`;
|
|
4860
5000
|
var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
|
|
4861
5001
|
var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
5002
|
+
var START_READY_TIMEOUT_MS = 15e3;
|
|
4862
5003
|
function parseIncludeAgentsEnv(raw) {
|
|
4863
5004
|
if (raw === void 0) return false;
|
|
4864
5005
|
const v = raw.trim().toLowerCase();
|
|
@@ -4884,7 +5025,9 @@ var StreamerServer = class {
|
|
|
4884
5025
|
pendingPermission = /* @__PURE__ */ new Map();
|
|
4885
5026
|
scanner = null;
|
|
4886
5027
|
// Set when better-sqlite3 is unusable (e.g. node ABI mismatch made
|
|
4887
|
-
// ConversationCache.open throw)
|
|
5028
|
+
// ConversationCache.open throw), or when config.scannerPersistent is false
|
|
5029
|
+
// (test isolation — the scanner's default SQLite index is a single shared
|
|
5030
|
+
// file unscoped by scanProfiles). All scanners are then built with
|
|
4888
5031
|
// persistent: false so requests serve from disk instead of 500ing on
|
|
4889
5032
|
// every touch of the scanner's own SQLite index.
|
|
4890
5033
|
scannerPersistenceDisabled = false;
|
|
@@ -4925,6 +5068,7 @@ var StreamerServer = class {
|
|
|
4925
5068
|
sessionInputAttempts = /* @__PURE__ */ new Map();
|
|
4926
5069
|
ptyGracePeriodMs;
|
|
4927
5070
|
defaultSystemPrompt;
|
|
5071
|
+
defaultPermissionMode;
|
|
4928
5072
|
// Map of sessionId → grace timer; fires to kill PTY after WS disconnect
|
|
4929
5073
|
ptyGraceTimers = /* @__PURE__ */ new Map();
|
|
4930
5074
|
// Map of sessionId → set of subscribed WS clients
|
|
@@ -4968,10 +5112,12 @@ var StreamerServer = class {
|
|
|
4968
5112
|
}
|
|
4969
5113
|
this.verbose = config.verbose ?? false;
|
|
4970
5114
|
this.disableDb = config.disableDb ?? false;
|
|
5115
|
+
this.scannerPersistenceDisabled = config.scannerPersistent === false;
|
|
4971
5116
|
this.scanProfiles = config.scanProfiles;
|
|
4972
5117
|
this.codexRoots = config.codexRoots ?? [join12(homedir5(), ".codex", "sessions")];
|
|
4973
5118
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
4974
5119
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
5120
|
+
this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
|
|
4975
5121
|
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join12(homedir5(), ".threadbase", "cache");
|
|
4976
5122
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
4977
5123
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
@@ -5401,9 +5547,9 @@ var StreamerServer = class {
|
|
|
5401
5547
|
);
|
|
5402
5548
|
this.scannerPersistenceDisabled = true;
|
|
5403
5549
|
}
|
|
5404
|
-
const warmupScanner = this.newScanner();
|
|
5405
|
-
this.allScanners.add(warmupScanner);
|
|
5406
5550
|
const warmupStatCache = this.buildStatCache(null);
|
|
5551
|
+
const warmupScanner = this.newScanner(warmupStatCache ? { persistent: false } : void 0);
|
|
5552
|
+
this.allScanners.add(warmupScanner);
|
|
5407
5553
|
const shouldEmitProgress = createScanProgressThrottle();
|
|
5408
5554
|
const scanOpts = {
|
|
5409
5555
|
...this.scanProfiles ? { profiles: this.scanProfiles } : {},
|
|
@@ -5843,6 +5989,10 @@ var StreamerServer = class {
|
|
|
5843
5989
|
}
|
|
5844
5990
|
buildStatCache(previousScanner) {
|
|
5845
5991
|
if (!this.cache) return void 0;
|
|
5992
|
+
if (!previousScanner) {
|
|
5993
|
+
const persisted = this.cache.getScannerStatCache();
|
|
5994
|
+
return persisted.size > 0 ? persisted : void 0;
|
|
5995
|
+
}
|
|
5846
5996
|
const dbStats = this.cache.getFileStats();
|
|
5847
5997
|
if (dbStats.size === 0) return void 0;
|
|
5848
5998
|
const metaByPath = /* @__PURE__ */ new Map();
|
|
@@ -5872,9 +6022,9 @@ var StreamerServer = class {
|
|
|
5872
6022
|
// this — its per-file refreshFile (in findConversationByUuid) already
|
|
5873
6023
|
// reconciles the one conversation being requested, so paying a full-tree
|
|
5874
6024
|
// rescan just because some OTHER file changed is the stall this avoids.
|
|
5875
|
-
newScanner() {
|
|
6025
|
+
newScanner(options) {
|
|
5876
6026
|
return new ConversationScanner(
|
|
5877
|
-
this.scannerPersistenceDisabled ? { persistent: false } : void 0
|
|
6027
|
+
options ?? (this.scannerPersistenceDisabled ? { persistent: false } : void 0)
|
|
5878
6028
|
);
|
|
5879
6029
|
}
|
|
5880
6030
|
async getScanner(skipStaleRescan = false) {
|
|
@@ -5893,7 +6043,7 @@ var StreamerServer = class {
|
|
|
5893
6043
|
}
|
|
5894
6044
|
this.scannerStale = false;
|
|
5895
6045
|
const statCache = this.buildStatCache(this.scanner);
|
|
5896
|
-
this.scanner = this.newScanner();
|
|
6046
|
+
this.scanner = this.newScanner(statCache ? { persistent: false } : void 0);
|
|
5897
6047
|
this.allScanners.add(this.scanner);
|
|
5898
6048
|
this.scannerReady = this.scanner.scan({
|
|
5899
6049
|
...this.scanProfiles ? { profiles: this.scanProfiles } : {},
|
|
@@ -5916,9 +6066,8 @@ var StreamerServer = class {
|
|
|
5916
6066
|
this.scannerReady = null;
|
|
5917
6067
|
return this.getScanner();
|
|
5918
6068
|
}
|
|
5919
|
-
// refresh=1's scan: reuse the WARM
|
|
5920
|
-
//
|
|
5921
|
-
// with fullRescan:true — the escape hatch that bypasses the scanner's
|
|
6069
|
+
// refresh=1's scan: reuse the WARM scanner and re-run its scan with
|
|
6070
|
+
// fullRescan:true — the escape hatch that bypasses the scanner's
|
|
5922
6071
|
// dir-mtime discovery gate, since an explicit user pull-to-refresh is exactly
|
|
5923
6072
|
// the "don't trust the gate, check disk for real" signal. Unlike
|
|
5924
6073
|
// getFreshScanner() this does NOT discard the warm scanner. scannerReady is
|
|
@@ -6306,7 +6455,8 @@ var StreamerServer = class {
|
|
|
6306
6455
|
provider,
|
|
6307
6456
|
projectPath,
|
|
6308
6457
|
projectName: body.projectName,
|
|
6309
|
-
branch: body.branch
|
|
6458
|
+
branch: body.branch,
|
|
6459
|
+
permissionMode: this.defaultPermissionMode
|
|
6310
6460
|
});
|
|
6311
6461
|
this.sessionStore.addManaged(session);
|
|
6312
6462
|
void this.watchConversationFile(sessionId);
|
|
@@ -6630,7 +6780,8 @@ var StreamerServer = class {
|
|
|
6630
6780
|
const session = await this.ptyManager.start(convId, {
|
|
6631
6781
|
projectPath,
|
|
6632
6782
|
projectName,
|
|
6633
|
-
branch
|
|
6783
|
+
branch,
|
|
6784
|
+
permissionMode: this.defaultPermissionMode
|
|
6634
6785
|
});
|
|
6635
6786
|
this.sessionStore.addManaged(session);
|
|
6636
6787
|
void this.watchConversationFile(session.id);
|
|
@@ -6700,10 +6851,35 @@ var StreamerServer = class {
|
|
|
6700
6851
|
provider,
|
|
6701
6852
|
projectPath: resolvedPath,
|
|
6702
6853
|
projectName: body.projectName,
|
|
6703
|
-
systemPrompt: systemPromptParts.join("\n")
|
|
6854
|
+
systemPrompt: systemPromptParts.join("\n"),
|
|
6855
|
+
permissionMode: this.defaultPermissionMode
|
|
6704
6856
|
});
|
|
6705
6857
|
this.sessionStore.addManaged(session);
|
|
6706
|
-
|
|
6858
|
+
const readyOrFailed = new Promise((resolve2) => {
|
|
6859
|
+
const handler = (status) => {
|
|
6860
|
+
if (status === "waiting_input" || status === "idle") {
|
|
6861
|
+
this.sessionStatusBus.off(`status:${session.id}`, handler);
|
|
6862
|
+
resolve2(status === "waiting_input" ? "ready" : "failed");
|
|
6863
|
+
}
|
|
6864
|
+
};
|
|
6865
|
+
this.sessionStatusBus.on(`status:${session.id}`, handler);
|
|
6866
|
+
});
|
|
6867
|
+
const timeoutPromise = new Promise(
|
|
6868
|
+
(resolve2) => setTimeout(() => resolve2("timeout"), START_READY_TIMEOUT_MS)
|
|
6869
|
+
);
|
|
6870
|
+
const outcome = await Promise.race([readyOrFailed, timeoutPromise]);
|
|
6871
|
+
const current = this.sessionStore.get(session.id, this.ptyAttachedIds());
|
|
6872
|
+
if (outcome === "ready" && current) {
|
|
6873
|
+
json(res, 200, { session: current });
|
|
6874
|
+
} else if (outcome === "failed" && current) {
|
|
6875
|
+
json(res, 502, {
|
|
6876
|
+
id: session.id,
|
|
6877
|
+
status: "idle",
|
|
6878
|
+
error: current.failureReason ?? "Session exited before becoming ready"
|
|
6879
|
+
});
|
|
6880
|
+
} else {
|
|
6881
|
+
json(res, 202, { id: session.id, status: "pending" });
|
|
6882
|
+
}
|
|
6707
6883
|
if (provider === CODEX_CLI_PROVIDER) {
|
|
6708
6884
|
this.watchForCodexRollout(session.id, resolvedPath);
|
|
6709
6885
|
} else {
|