@threadbase-sh/streamer 1.40.0 → 1.41.1
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/README.md +1 -1
- package/dist/cli.cjs +358 -65
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +356 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +356 -63
- package/dist/index.js.map +1 -1
- package/dist/{migrations/010_create_managed_sessions.sql → runtime-migrations/001_create_managed_sessions.sql} +6 -0
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -500,6 +500,12 @@ var FEATURE_FLAGS = [
|
|
|
500
500
|
description: "Send the built system prompt to fresh Codex sessions. Off by default: Codex has no --system-prompt flag, so the prompt goes in the positional [PROMPT] argument, which Codex treats as the user's opening turn rather than a system-level instruction.",
|
|
501
501
|
default: false,
|
|
502
502
|
env: "THREADBASE_FEATURE_CODEX_SYSTEM_PROMPT"
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
id: "sessionRehydration",
|
|
506
|
+
description: "Seed the session list at boot with sessions a previous streamer run left behind, so a restart leaves them one tap from resuming instead of silently gone. On by default, with a kill switch: it changes what GET /api/sessions contains.",
|
|
507
|
+
default: true,
|
|
508
|
+
env: "THREADBASE_FEATURE_SESSION_REHYDRATION"
|
|
503
509
|
}
|
|
504
510
|
];
|
|
505
511
|
function findFeatureFlag(id) {
|
|
@@ -1936,6 +1942,12 @@ function detectShellPrompt(lines) {
|
|
|
1936
1942
|
return null;
|
|
1937
1943
|
}
|
|
1938
1944
|
|
|
1945
|
+
// src/utils/deriveSessionName.ts
|
|
1946
|
+
function deriveSessionName(firstMessageText) {
|
|
1947
|
+
const firstLine = firstMessageText.split("\n", 1)[0]?.trim() ?? "";
|
|
1948
|
+
return firstLine.slice(0, 80);
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1939
1951
|
// src/pty-manager.ts
|
|
1940
1952
|
var OUTPUT_BUFFER_MAX2 = 65536;
|
|
1941
1953
|
var INPUT_HISTORY_MAX2 = 50;
|
|
@@ -1943,8 +1955,8 @@ var PTY_COLS2 = 120;
|
|
|
1943
1955
|
var PTY_ROWS2 = 40;
|
|
1944
1956
|
var SCREEN_SCROLLBACK2 = 1e3;
|
|
1945
1957
|
var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
|
|
1946
|
-
var PROMPT_MARKER_FALLBACK_MS = 1e4;
|
|
1947
1958
|
var QUIET_DETECT_MS2 = 500;
|
|
1959
|
+
var CLAUDE_READY_FALLBACK_MS = 8e3;
|
|
1948
1960
|
function buildPasteBytes(input) {
|
|
1949
1961
|
return `\x1B[200~${input}\x1B[201~`;
|
|
1950
1962
|
}
|
|
@@ -2020,8 +2032,8 @@ var PTYManager = class {
|
|
|
2020
2032
|
// silently lost — the "dot bug".
|
|
2021
2033
|
queuedInputs = /* @__PURE__ */ new Map();
|
|
2022
2034
|
log;
|
|
2023
|
-
// Timestamp of first PTY chunk per session;
|
|
2024
|
-
//
|
|
2035
|
+
// Timestamp of first PTY chunk per session; used for the [pty.ready] elapsed
|
|
2036
|
+
// measurement so a slow boot is visible in the logs.
|
|
2025
2037
|
firstChunkAt = /* @__PURE__ */ new Map();
|
|
2026
2038
|
// Per-session chunk counter and last-chunk timestamp. Diagnostic-only,
|
|
2027
2039
|
// feeds the [pty.chunk] log lines so we can trace whether Claude responded
|
|
@@ -2032,6 +2044,8 @@ var PTYManager = class {
|
|
|
2032
2044
|
// QUIET_DETECT_MS after the last chunk so ready/prompt detection doesn't
|
|
2033
2045
|
// wait for another chunk that may never arrive (Claude blocked on input).
|
|
2034
2046
|
quietCheckers = /* @__PURE__ */ new Map();
|
|
2047
|
+
// Per-session flat backstop from the first chunk (CLAUDE_READY_FALLBACK_MS).
|
|
2048
|
+
readyFallbackTimers = /* @__PURE__ */ new Map();
|
|
2035
2049
|
// In-flight start()/startFresh() calls keyed by sessionId. A second
|
|
2036
2050
|
// concurrent resume for the same session (double-tap, client retry) awaits
|
|
2037
2051
|
// the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
|
|
@@ -2117,6 +2131,7 @@ var PTYManager = class {
|
|
|
2117
2131
|
};
|
|
2118
2132
|
this.sessions.set(sessionId, session);
|
|
2119
2133
|
this.pendingReady.add(sessionId);
|
|
2134
|
+
this.armReadyFallback(sessionId);
|
|
2120
2135
|
proc.onData((data) => {
|
|
2121
2136
|
this.handleOutput(sessionId, data);
|
|
2122
2137
|
});
|
|
@@ -2177,6 +2192,7 @@ var PTYManager = class {
|
|
|
2177
2192
|
};
|
|
2178
2193
|
this.sessions.set(sessionId, session);
|
|
2179
2194
|
this.pendingReady.add(sessionId);
|
|
2195
|
+
this.armReadyFallback(sessionId);
|
|
2180
2196
|
proc.onData((data) => {
|
|
2181
2197
|
this.handleOutput(sessionId, data);
|
|
2182
2198
|
});
|
|
@@ -2351,6 +2367,7 @@ var PTYManager = class {
|
|
|
2351
2367
|
this.shellPromptOpen.delete(sessionId);
|
|
2352
2368
|
this.quietCheckers.get(sessionId)?.cancel();
|
|
2353
2369
|
this.quietCheckers.delete(sessionId);
|
|
2370
|
+
this.clearReadyFallback(sessionId);
|
|
2354
2371
|
try {
|
|
2355
2372
|
session.process.kill("SIGINT");
|
|
2356
2373
|
} catch {
|
|
@@ -2409,6 +2426,10 @@ var PTYManager = class {
|
|
|
2409
2426
|
if (session.inputHistory.length > INPUT_HISTORY_MAX2) {
|
|
2410
2427
|
session.inputHistory.shift();
|
|
2411
2428
|
}
|
|
2429
|
+
if (session.firstMessageText === void 0) {
|
|
2430
|
+
session.firstMessageText = text;
|
|
2431
|
+
session.sessionName = deriveSessionName(text);
|
|
2432
|
+
}
|
|
2412
2433
|
this.onUserMessage?.(session.id, text, ts);
|
|
2413
2434
|
}
|
|
2414
2435
|
getSession(sessionId) {
|
|
@@ -2435,6 +2456,8 @@ var PTYManager = class {
|
|
|
2435
2456
|
this.lastChunkAt.clear();
|
|
2436
2457
|
for (const quiet of this.quietCheckers.values()) quiet.cancel();
|
|
2437
2458
|
this.quietCheckers.clear();
|
|
2459
|
+
for (const timer of this.readyFallbackTimers.values()) clearTimeout(timer);
|
|
2460
|
+
this.readyFallbackTimers.clear();
|
|
2438
2461
|
this.permissionOpen.clear();
|
|
2439
2462
|
this.lastScreenQuestionKey.clear();
|
|
2440
2463
|
this.shellPromptOpen.clear();
|
|
@@ -2477,8 +2500,6 @@ var PTYManager = class {
|
|
|
2477
2500
|
const matchedMarker = CLAUDE_PROMPT_MARKERS.find((m) => stripped.includes(m));
|
|
2478
2501
|
if (session.status === "running" && matchedMarker) {
|
|
2479
2502
|
this.markReady(sessionId, session, "prompt-marker", `marker:${matchedMarker}`);
|
|
2480
|
-
} else if (session.status === "running" && this.pendingReady.has(sessionId) && now - (this.firstChunkAt.get(sessionId) ?? now) >= PROMPT_MARKER_FALLBACK_MS) {
|
|
2481
|
-
this.markReady(sessionId, session, "timeout-fallback", "fallback:timeout");
|
|
2482
2503
|
}
|
|
2483
2504
|
this.onOutput?.(sessionId, data);
|
|
2484
2505
|
this.detectLivePrompts(sessionId, data, stripped).catch((err) => {
|
|
@@ -2578,7 +2599,13 @@ var PTYManager = class {
|
|
|
2578
2599
|
const session = this.sessions.get(sessionId);
|
|
2579
2600
|
if (session?.status !== "running") return;
|
|
2580
2601
|
if (this.pendingReady.has(sessionId)) {
|
|
2581
|
-
this.
|
|
2602
|
+
this.recheckReadyFromScreen(sessionId).catch((err) => {
|
|
2603
|
+
this.log.warn("[pty.ready] boot screen recheck failed", {
|
|
2604
|
+
event: "pty.ready_recheck_failed",
|
|
2605
|
+
sessionId,
|
|
2606
|
+
err
|
|
2607
|
+
});
|
|
2608
|
+
});
|
|
2582
2609
|
} else {
|
|
2583
2610
|
this.recheckReadyFromScreen(sessionId).catch((err) => {
|
|
2584
2611
|
this.log.warn("[pty.ready] screen recheck failed", {
|
|
@@ -2610,9 +2637,32 @@ var PTYManager = class {
|
|
|
2610
2637
|
this.markReady(sessionId, session, "screen-marker", `quiet:screen-marker:${matchedMarker}`);
|
|
2611
2638
|
}
|
|
2612
2639
|
}
|
|
2640
|
+
// Flat backstop from the first chunk: if neither a prompt marker nor the
|
|
2641
|
+
// screen recheck settles the session within CLAUDE_READY_FALLBACK_MS, mark it
|
|
2642
|
+
// ready anyway so start requests resolve and queued input is not held
|
|
2643
|
+
// forever. This is what makes the quiet-checker safe to be strict — a boot
|
|
2644
|
+
// variant whose marker we cannot see still recovers, just 8s later instead of
|
|
2645
|
+
// 500ms sooner and wrong. unref() so it never holds the process open.
|
|
2646
|
+
armReadyFallback(sessionId) {
|
|
2647
|
+
const timer = setTimeout(() => {
|
|
2648
|
+
this.readyFallbackTimers.delete(sessionId);
|
|
2649
|
+
const session = this.sessions.get(sessionId);
|
|
2650
|
+
if (session?.status === "running" && this.pendingReady.has(sessionId)) {
|
|
2651
|
+
this.markReady(sessionId, session, "timeout-fallback", "fallback:timeout");
|
|
2652
|
+
}
|
|
2653
|
+
}, CLAUDE_READY_FALLBACK_MS);
|
|
2654
|
+
timer.unref?.();
|
|
2655
|
+
this.readyFallbackTimers.set(sessionId, timer);
|
|
2656
|
+
}
|
|
2657
|
+
clearReadyFallback(sessionId) {
|
|
2658
|
+
const timer = this.readyFallbackTimers.get(sessionId);
|
|
2659
|
+
if (timer) clearTimeout(timer);
|
|
2660
|
+
this.readyFallbackTimers.delete(sessionId);
|
|
2661
|
+
}
|
|
2613
2662
|
// Transition a session from "running" to "waiting_input", clear pendingReady,
|
|
2614
2663
|
// and flush any queued input. Idempotent: callers can invoke at any chunk.
|
|
2615
2664
|
markReady(sessionId, session, source, reason) {
|
|
2665
|
+
this.clearReadyFallback(sessionId);
|
|
2616
2666
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
2617
2667
|
session.status = "waiting_input";
|
|
2618
2668
|
session.statusSource = source;
|
|
@@ -2656,6 +2706,7 @@ var PTYManager = class {
|
|
|
2656
2706
|
this.shellPromptOpen.delete(sessionId);
|
|
2657
2707
|
this.quietCheckers.get(sessionId)?.cancel();
|
|
2658
2708
|
this.quietCheckers.delete(sessionId);
|
|
2709
|
+
this.clearReadyFallback(sessionId);
|
|
2659
2710
|
}
|
|
2660
2711
|
};
|
|
2661
2712
|
function toPublicSession2(s) {
|
|
@@ -2674,7 +2725,9 @@ function toPublicSession2(s) {
|
|
|
2674
2725
|
...s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt },
|
|
2675
2726
|
...s.statusSource != null && { statusSource: s.statusSource },
|
|
2676
2727
|
...s.statusUpdatedAt != null && { statusUpdatedAt: s.statusUpdatedAt },
|
|
2677
|
-
...s.filePath != null && { filePath: s.filePath }
|
|
2728
|
+
...s.filePath != null && { filePath: s.filePath },
|
|
2729
|
+
...s.sessionName != null && { sessionName: s.sessionName },
|
|
2730
|
+
...s.firstMessageText != null && { firstMessageText: s.firstMessageText }
|
|
2678
2731
|
};
|
|
2679
2732
|
}
|
|
2680
2733
|
function stripAnsi2(str) {
|
|
@@ -5178,6 +5231,9 @@ function getMigrationsDir2() {
|
|
|
5178
5231
|
}
|
|
5179
5232
|
return __dirname;
|
|
5180
5233
|
}
|
|
5234
|
+
function resolveMigrationsDir(name = "migrations") {
|
|
5235
|
+
return (0, import_path10.join)(getMigrationsDir2(), name);
|
|
5236
|
+
}
|
|
5181
5237
|
var SCHEMA_MIGRATIONS_SQL = `
|
|
5182
5238
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
5183
5239
|
id TEXT PRIMARY KEY,
|
|
@@ -5186,7 +5242,7 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
|
5186
5242
|
`;
|
|
5187
5243
|
function runSqliteMigrations(db, migrationsDir) {
|
|
5188
5244
|
db.exec(SCHEMA_MIGRATIONS_SQL);
|
|
5189
|
-
const dir = migrationsDir ?? (
|
|
5245
|
+
const dir = migrationsDir ?? resolveMigrationsDir();
|
|
5190
5246
|
const files = (0, import_fs8.readdirSync)(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
5191
5247
|
const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
|
|
5192
5248
|
const appliedSet = new Set(appliedRows.map((r) => r.id));
|
|
@@ -6597,6 +6653,7 @@ var ManagedSessionsRepository = class {
|
|
|
6597
6653
|
updateStatusStmt;
|
|
6598
6654
|
getStmt;
|
|
6599
6655
|
listNonTerminalStmt;
|
|
6656
|
+
listRecoverableStmt;
|
|
6600
6657
|
deleteStmt;
|
|
6601
6658
|
constructor(db) {
|
|
6602
6659
|
this.upsertStmt = db.prepare(`
|
|
@@ -6640,7 +6697,8 @@ var ManagedSessionsRepository = class {
|
|
|
6640
6697
|
completed_at = @completed_at,
|
|
6641
6698
|
last_activity_at = @last_activity_at,
|
|
6642
6699
|
prompt_count = @prompt_count,
|
|
6643
|
-
failure_reason = COALESCE(@failure_reason, failure_reason)
|
|
6700
|
+
failure_reason = COALESCE(@failure_reason, failure_reason),
|
|
6701
|
+
session_name = COALESCE(@session_name, session_name)
|
|
6644
6702
|
WHERE session_id = @session_id
|
|
6645
6703
|
`);
|
|
6646
6704
|
this.getStmt = db.prepare("SELECT * FROM managed_sessions WHERE session_id = ?");
|
|
@@ -6649,6 +6707,13 @@ var ManagedSessionsRepository = class {
|
|
|
6649
6707
|
WHERE completed_at IS NULL
|
|
6650
6708
|
ORDER BY started_at ASC
|
|
6651
6709
|
`);
|
|
6710
|
+
this.listRecoverableStmt = db.prepare(`
|
|
6711
|
+
SELECT * FROM managed_sessions
|
|
6712
|
+
WHERE (completed_at IS NULL OR status_source = 'shutdown')
|
|
6713
|
+
AND status_updated_at >= @since
|
|
6714
|
+
ORDER BY status_updated_at DESC
|
|
6715
|
+
LIMIT @limit
|
|
6716
|
+
`);
|
|
6652
6717
|
this.deleteStmt = db.prepare("DELETE FROM managed_sessions WHERE session_id = ?");
|
|
6653
6718
|
}
|
|
6654
6719
|
/** Record a session at spawn, or refresh every field of an existing row. */
|
|
@@ -6690,7 +6755,8 @@ var ManagedSessionsRepository = class {
|
|
|
6690
6755
|
completed_at: fields.completedAt?.getTime() ?? null,
|
|
6691
6756
|
last_activity_at: fields.lastActivityAt?.getTime() ?? null,
|
|
6692
6757
|
prompt_count: fields.promptCount ?? 0,
|
|
6693
|
-
failure_reason: fields.failureReason ?? null
|
|
6758
|
+
failure_reason: fields.failureReason ?? null,
|
|
6759
|
+
session_name: fields.sessionName ?? null
|
|
6694
6760
|
});
|
|
6695
6761
|
}
|
|
6696
6762
|
get(sessionId) {
|
|
@@ -6700,6 +6766,14 @@ var ManagedSessionsRepository = class {
|
|
|
6700
6766
|
listNonTerminal() {
|
|
6701
6767
|
return this.listNonTerminalStmt.all();
|
|
6702
6768
|
}
|
|
6769
|
+
/**
|
|
6770
|
+
* Rows a restart could bring back: still open, or closed by our own shutdown,
|
|
6771
|
+
* and touched no longer ago than `sinceMs`. Newest first, capped — the caller
|
|
6772
|
+
* decides which of these actually deserve rehydrating (`shouldRehydrate`).
|
|
6773
|
+
*/
|
|
6774
|
+
listRecoverable({ sinceMs, limit }) {
|
|
6775
|
+
return this.listRecoverableStmt.all({ since: sinceMs, limit });
|
|
6776
|
+
}
|
|
6703
6777
|
delete(sessionId) {
|
|
6704
6778
|
this.deleteStmt.run(sessionId);
|
|
6705
6779
|
}
|
|
@@ -6835,6 +6909,54 @@ var SessionsRepository = class {
|
|
|
6835
6909
|
}
|
|
6836
6910
|
};
|
|
6837
6911
|
|
|
6912
|
+
// src/db/runtime-store.ts
|
|
6913
|
+
var import_better_sqlite32 = __toESM(require("better-sqlite3"), 1);
|
|
6914
|
+
var RuntimeStore = class _RuntimeStore {
|
|
6915
|
+
constructor(db) {
|
|
6916
|
+
this.db = db;
|
|
6917
|
+
}
|
|
6918
|
+
db;
|
|
6919
|
+
static open(dbPath, migrationsDir) {
|
|
6920
|
+
const db = new import_better_sqlite32.default(dbPath);
|
|
6921
|
+
db.pragma("journal_mode = WAL");
|
|
6922
|
+
runSqliteMigrations(db, migrationsDir ?? resolveMigrationsDir("runtime-migrations"));
|
|
6923
|
+
return new _RuntimeStore(db);
|
|
6924
|
+
}
|
|
6925
|
+
getDatabase() {
|
|
6926
|
+
return this.db;
|
|
6927
|
+
}
|
|
6928
|
+
/**
|
|
6929
|
+
* One-time move of `managed_sessions` rows out of a pre-split `cache.db`.
|
|
6930
|
+
*
|
|
6931
|
+
* Non-destructive by design: the source table is left in place so an older
|
|
6932
|
+
* streamer rolled back onto the same machine still finds its registry. Runs
|
|
6933
|
+
* only when this file's table is empty, so a second boot is a no-op rather
|
|
6934
|
+
* than a re-copy that would resurrect rows deleted since.
|
|
6935
|
+
*
|
|
6936
|
+
* Returns the number of rows copied.
|
|
6937
|
+
*/
|
|
6938
|
+
importLegacyManagedSessions(source) {
|
|
6939
|
+
const existing = this.db.prepare("SELECT COUNT(*) AS n FROM managed_sessions").get();
|
|
6940
|
+
if (existing.n > 0) return 0;
|
|
6941
|
+
const hasTable = source.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'managed_sessions'").get();
|
|
6942
|
+
if (!hasTable) return 0;
|
|
6943
|
+
const rows = source.prepare("SELECT * FROM managed_sessions").all();
|
|
6944
|
+
if (rows.length === 0) return 0;
|
|
6945
|
+
const columns = Object.keys(rows[0]);
|
|
6946
|
+
const insert = this.db.prepare(
|
|
6947
|
+
`INSERT OR IGNORE INTO managed_sessions (${columns.join(", ")})
|
|
6948
|
+
VALUES (${columns.map((c) => `@${c}`).join(", ")})`
|
|
6949
|
+
);
|
|
6950
|
+
this.db.transaction((batch) => {
|
|
6951
|
+
for (const row of batch) insert.run(row);
|
|
6952
|
+
})(rows);
|
|
6953
|
+
return rows.length;
|
|
6954
|
+
}
|
|
6955
|
+
close() {
|
|
6956
|
+
this.db.close();
|
|
6957
|
+
}
|
|
6958
|
+
};
|
|
6959
|
+
|
|
6838
6960
|
// src/db/upload-records.ts
|
|
6839
6961
|
async function recordUpload(pool2, instanceId, row) {
|
|
6840
6962
|
if (!pool2) return;
|
|
@@ -7852,7 +7974,8 @@ function contentStateForSession(args) {
|
|
|
7852
7974
|
status,
|
|
7853
7975
|
startedAt: args.startedAtOverride ?? args.session.startedAt.getTime(),
|
|
7854
7976
|
lastOutput: truncateLastOutput(args.session.lastOutput ?? ""),
|
|
7855
|
-
...args.serverLabel != null && { serverLabel: args.serverLabel }
|
|
7977
|
+
...args.serverLabel != null && { serverLabel: args.serverLabel },
|
|
7978
|
+
...args.session.sessionName != null && { sessionName: args.session.sessionName }
|
|
7856
7979
|
};
|
|
7857
7980
|
}
|
|
7858
7981
|
var LiveActivityNotifier = class {
|
|
@@ -7865,14 +7988,17 @@ var LiveActivityNotifier = class {
|
|
|
7865
7988
|
serverId;
|
|
7866
7989
|
serverLabel;
|
|
7867
7990
|
/**
|
|
7868
|
-
*
|
|
7991
|
+
* Sessions with a currently open (pushed) activity.
|
|
7869
7992
|
*
|
|
7870
|
-
*
|
|
7871
|
-
*
|
|
7872
|
-
*
|
|
7873
|
-
*
|
|
7993
|
+
* An activity opens on a `waiting_input → running` edge (the user sent a
|
|
7994
|
+
* prompt) and closes on the matching `running → waiting_input` edge (the
|
|
7995
|
+
* response, including any sub-agents, finished) — so this set is what makes
|
|
7996
|
+
* the notifier per-turn rather than per-session. A session's very first
|
|
7997
|
+
* `running` (right after spawn, before any user prompt) has no prior
|
|
7998
|
+
* `waiting_input` and therefore no edge, so it never opens an activity —
|
|
7999
|
+
* this is what keeps a fresh/idle session from pushing anything.
|
|
7874
8000
|
*/
|
|
7875
|
-
|
|
8001
|
+
openActivity = /* @__PURE__ */ new Map();
|
|
7876
8002
|
/**
|
|
7877
8003
|
* React to a session status change.
|
|
7878
8004
|
*
|
|
@@ -7880,34 +8006,22 @@ var LiveActivityNotifier = class {
|
|
|
7880
8006
|
* transition, so this returns a promise the caller may ignore and every error
|
|
7881
8007
|
* is logged rather than propagated.
|
|
7882
8008
|
*/
|
|
7883
|
-
async onStatusChange(session) {
|
|
8009
|
+
async onStatusChange(session, previousStatus) {
|
|
7884
8010
|
const status = toLiveActivityStatus(session.status);
|
|
7885
8011
|
try {
|
|
7886
8012
|
if (!status) {
|
|
7887
|
-
await this.endFor(session);
|
|
8013
|
+
if (this.openActivity.has(session.id)) await this.endFor(session);
|
|
7888
8014
|
return;
|
|
7889
8015
|
}
|
|
7890
|
-
if (
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
const outcome = await this.sender.send({
|
|
7898
|
-
sessionId: session.id,
|
|
7899
|
-
event: "update",
|
|
7900
|
-
contentState
|
|
7901
|
-
});
|
|
7902
|
-
this.lastPushed.set(session.id, status);
|
|
7903
|
-
if (outcome.attempted > 0) {
|
|
7904
|
-
log4.info("live_activity.updated", {
|
|
7905
|
-
event: "live_activity.updated",
|
|
7906
|
-
sessionId: session.id,
|
|
7907
|
-
status,
|
|
7908
|
-
...outcome
|
|
7909
|
-
});
|
|
8016
|
+
if (status === "running" && previousStatus === "waiting_input") {
|
|
8017
|
+
await this.startTurn(session);
|
|
8018
|
+
return;
|
|
8019
|
+
}
|
|
8020
|
+
if (status === "waiting_input" && previousStatus === "running") {
|
|
8021
|
+
if (this.openActivity.has(session.id)) await this.endFor(session);
|
|
8022
|
+
return;
|
|
7910
8023
|
}
|
|
8024
|
+
await this.maybeSendName(session);
|
|
7911
8025
|
} catch (err) {
|
|
7912
8026
|
log4.error("live_activity.notify_failed", {
|
|
7913
8027
|
event: "live_activity.notify_failed",
|
|
@@ -7917,14 +8031,57 @@ var LiveActivityNotifier = class {
|
|
|
7917
8031
|
});
|
|
7918
8032
|
}
|
|
7919
8033
|
}
|
|
8034
|
+
async startTurn(session) {
|
|
8035
|
+
const contentState = contentStateForSession({
|
|
8036
|
+
session,
|
|
8037
|
+
serverId: this.serverId,
|
|
8038
|
+
serverLabel: this.serverLabel
|
|
8039
|
+
});
|
|
8040
|
+
if (!contentState) return;
|
|
8041
|
+
const outcome = await this.sender.send({
|
|
8042
|
+
sessionId: session.id,
|
|
8043
|
+
event: "update",
|
|
8044
|
+
contentState
|
|
8045
|
+
});
|
|
8046
|
+
this.openActivity.set(session.id, { sessionNameSent: session.sessionName != null });
|
|
8047
|
+
if (outcome.attempted > 0) {
|
|
8048
|
+
log4.info("live_activity.updated", {
|
|
8049
|
+
event: "live_activity.updated",
|
|
8050
|
+
sessionId: session.id,
|
|
8051
|
+
status: contentState.status,
|
|
8052
|
+
...outcome
|
|
8053
|
+
});
|
|
8054
|
+
}
|
|
8055
|
+
}
|
|
8056
|
+
async maybeSendName(session) {
|
|
8057
|
+
const open2 = this.openActivity.get(session.id);
|
|
8058
|
+
if (!open2 || open2.sessionNameSent || session.sessionName == null) return;
|
|
8059
|
+
const contentState = contentStateForSession({
|
|
8060
|
+
session,
|
|
8061
|
+
serverId: this.serverId,
|
|
8062
|
+
serverLabel: this.serverLabel
|
|
8063
|
+
});
|
|
8064
|
+
if (!contentState) return;
|
|
8065
|
+
const outcome = await this.sender.send({
|
|
8066
|
+
sessionId: session.id,
|
|
8067
|
+
event: "update",
|
|
8068
|
+
contentState
|
|
8069
|
+
});
|
|
8070
|
+
open2.sessionNameSent = true;
|
|
8071
|
+
if (outcome.attempted > 0) {
|
|
8072
|
+
log4.info("live_activity.updated", {
|
|
8073
|
+
event: "live_activity.updated",
|
|
8074
|
+
sessionId: session.id,
|
|
8075
|
+
status: contentState.status,
|
|
8076
|
+
...outcome
|
|
8077
|
+
});
|
|
8078
|
+
}
|
|
8079
|
+
}
|
|
7920
8080
|
async endFor(session) {
|
|
7921
|
-
|
|
7922
|
-
|
|
8081
|
+
this.openActivity.delete(session.id);
|
|
8082
|
+
const status = toLiveActivityStatus(session.status);
|
|
7923
8083
|
const contentState = contentStateForSession({
|
|
7924
|
-
session: {
|
|
7925
|
-
...session,
|
|
7926
|
-
status: lastStatus === "waiting_input" ? "waiting_input" : "running"
|
|
7927
|
-
},
|
|
8084
|
+
session: { ...session, status: status ?? "waiting_input" },
|
|
7928
8085
|
serverId: this.serverId,
|
|
7929
8086
|
serverLabel: this.serverLabel
|
|
7930
8087
|
});
|
|
@@ -7938,9 +8095,9 @@ var LiveActivityNotifier = class {
|
|
|
7938
8095
|
});
|
|
7939
8096
|
}
|
|
7940
8097
|
}
|
|
7941
|
-
/** Drop cached state for a session, so a resume re-
|
|
8098
|
+
/** Drop cached state for a session, so a resume re-opens on its next turn. */
|
|
7942
8099
|
forget(sessionId) {
|
|
7943
|
-
this.
|
|
8100
|
+
this.openActivity.delete(sessionId);
|
|
7944
8101
|
}
|
|
7945
8102
|
};
|
|
7946
8103
|
|
|
@@ -8171,7 +8328,8 @@ var LiveActivityRenewalScheduler = class {
|
|
|
8171
8328
|
status,
|
|
8172
8329
|
startedAt,
|
|
8173
8330
|
lastOutput: session.lastOutput ?? "",
|
|
8174
|
-
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel }
|
|
8331
|
+
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel },
|
|
8332
|
+
...session.sessionName != null && { sessionName: session.sessionName }
|
|
8175
8333
|
};
|
|
8176
8334
|
try {
|
|
8177
8335
|
await this.deps.sender.send({
|
|
@@ -8231,7 +8389,8 @@ var LiveActivityRenewalScheduler = class {
|
|
|
8231
8389
|
// Carried through unchanged — the whole point of the renewal.
|
|
8232
8390
|
startedAt: args.startedAt,
|
|
8233
8391
|
lastOutput: truncateLastOutput(session.lastOutput ?? ""),
|
|
8234
|
-
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel }
|
|
8392
|
+
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel },
|
|
8393
|
+
...session.sessionName != null && { sessionName: session.sessionName }
|
|
8235
8394
|
},
|
|
8236
8395
|
now: args.now,
|
|
8237
8396
|
staleDate: args.startedAt + ACTIVITY_MAX_LIFETIME_MS
|
|
@@ -8618,6 +8777,50 @@ async function reconcileSessions(rows, probe, currentInstanceId) {
|
|
|
8618
8777
|
return Promise.all(rows.map((row) => classifySession(row, probe, currentInstanceId)));
|
|
8619
8778
|
}
|
|
8620
8779
|
|
|
8780
|
+
// src/services/sessions/rehydrateSessions.ts
|
|
8781
|
+
var REHYDRATE_MAX = 25;
|
|
8782
|
+
var REHYDRATE_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
8783
|
+
var AGENT_EXIT_SOURCES = /* @__PURE__ */ new Set(["exit", "process-exit"]);
|
|
8784
|
+
function shouldRehydrate(row, opts) {
|
|
8785
|
+
if (!opts.projectExists(row.project_path)) return false;
|
|
8786
|
+
if (opts.now - row.status_updated_at > REHYDRATE_WINDOW_MS) return false;
|
|
8787
|
+
if (AGENT_EXIT_SOURCES.has(row.status_source) && row.failure_reason == null) return false;
|
|
8788
|
+
return true;
|
|
8789
|
+
}
|
|
8790
|
+
function rowToStubSession(row) {
|
|
8791
|
+
return {
|
|
8792
|
+
id: row.session_id,
|
|
8793
|
+
provider: row.provider,
|
|
8794
|
+
projectPath: row.project_path,
|
|
8795
|
+
projectName: row.project_name,
|
|
8796
|
+
branch: row.branch,
|
|
8797
|
+
// No PTY exists for a stub, so this is the only truthful status.
|
|
8798
|
+
status: "idle",
|
|
8799
|
+
startedAt: new Date(row.started_at),
|
|
8800
|
+
completedAt: row.completed_at != null ? new Date(row.completed_at) : null,
|
|
8801
|
+
promptCount: row.prompt_count,
|
|
8802
|
+
lastOutput: "",
|
|
8803
|
+
rehydrated: true,
|
|
8804
|
+
...row.session_name != null && { sessionName: row.session_name },
|
|
8805
|
+
...row.project_id != null && { projectId: row.project_id },
|
|
8806
|
+
...row.bound_conversation_id != null && { boundConversationId: row.bound_conversation_id },
|
|
8807
|
+
...row.resumed_from_conversation_id != null && {
|
|
8808
|
+
resumedFromConversationId: row.resumed_from_conversation_id
|
|
8809
|
+
},
|
|
8810
|
+
...row.failure_reason != null && { failureReason: row.failure_reason },
|
|
8811
|
+
...row.last_activity_at != null && { lastActivityAt: new Date(row.last_activity_at) },
|
|
8812
|
+
// Only `shutdown` crosses over. It is the one registry source that is also a
|
|
8813
|
+
// wire StatusSource *and* that genuinely describes the `idle` above — the
|
|
8814
|
+
// streamer stopped this session. A crashed row still says `transition` over
|
|
8815
|
+
// a `running` status, and copying that here would attach observed-confidence
|
|
8816
|
+
// provenance to a status we derived at boot, so leave it unset instead.
|
|
8817
|
+
...row.status_source === "shutdown" && {
|
|
8818
|
+
statusSource: "shutdown",
|
|
8819
|
+
statusUpdatedAt: new Date(row.status_updated_at)
|
|
8820
|
+
}
|
|
8821
|
+
};
|
|
8822
|
+
}
|
|
8823
|
+
|
|
8621
8824
|
// src/types.ts
|
|
8622
8825
|
function confidenceForSource(source) {
|
|
8623
8826
|
return source === "timeout-fallback" || source === "quiet-fallback" ? "inferred" : "observed";
|
|
@@ -8765,14 +8968,15 @@ function managedToResponse(s, ptyAttached) {
|
|
|
8765
8968
|
// Lifecycle for a session this run knows about. `attached` while we hold
|
|
8766
8969
|
// its PTY; once the PTY is gone the session is terminal from this run's
|
|
8767
8970
|
// perspective — `failed` when it recorded a reason, else `completed`.
|
|
8768
|
-
//
|
|
8769
|
-
//
|
|
8770
|
-
//
|
|
8771
|
-
|
|
8772
|
-
|
|
8971
|
+
// A `rehydrated` stub is the exception: the boot rehydrator seeded it from
|
|
8972
|
+
// the durable registry, so it is a previous run's session with no process
|
|
8973
|
+
// behind it — `resumable`, and `historical` rather than `managed`
|
|
8974
|
+
// (docs/plans/live-sessions-persistence-plan.md §4, Phase 1).
|
|
8975
|
+
lifecycle: ptyAttached ? "attached" : s.rehydrated ? "resumable" : s.failureReason != null ? "failed" : "completed",
|
|
8976
|
+
lifecycleSource: ptyAttached ? "spawn" : s.rehydrated ? "reconcile" : "exit",
|
|
8773
8977
|
// We spawned it, so `status` is the authoritative signal — no inferred
|
|
8774
8978
|
// `activity` is attached for managed sessions.
|
|
8775
|
-
ownership: "managed",
|
|
8979
|
+
ownership: s.rehydrated ? "historical" : "managed",
|
|
8776
8980
|
projectPath: s.projectPath,
|
|
8777
8981
|
projectName: s.projectName,
|
|
8778
8982
|
branch: s.branch,
|
|
@@ -9362,10 +9566,13 @@ var StreamerServer = class {
|
|
|
9362
9566
|
projectsRepo = null;
|
|
9363
9567
|
conversationsRepo = null;
|
|
9364
9568
|
sessionsRepo = null;
|
|
9365
|
-
// Durable session registry (C1 Phase 2). Null when
|
|
9366
|
-
//
|
|
9367
|
-
// taking the server down with it, so every write goes through `?.`.
|
|
9569
|
+
// Durable session registry (C1 Phase 2). Null when runtime.db failed to open
|
|
9570
|
+
// — persistence degrades to today's in-memory-only behaviour rather than
|
|
9571
|
+
// taking the server down with it, so every write goes through `?.`. Note the
|
|
9572
|
+
// handle is runtime.db, NOT the conversation cache: a cache failure used to
|
|
9573
|
+
// null this repo and silently disable all session persistence.
|
|
9368
9574
|
managedSessionsRepo = null;
|
|
9575
|
+
runtimeStore = null;
|
|
9369
9576
|
// Identifies this streamer run. A registry row carrying a different id is a
|
|
9370
9577
|
// session that outlived the process that started it.
|
|
9371
9578
|
streamerInstanceId = (0, import_crypto11.randomUUID)();
|
|
@@ -9384,6 +9591,7 @@ var StreamerServer = class {
|
|
|
9384
9591
|
liveActivityRenewal = null;
|
|
9385
9592
|
discoveryCache = null;
|
|
9386
9593
|
cacheDir;
|
|
9594
|
+
runtimeDbPath;
|
|
9387
9595
|
tailSize;
|
|
9388
9596
|
directoryDebounceMs;
|
|
9389
9597
|
// Trailing-debounced trigger that flags the scanner stale after a quiet
|
|
@@ -9430,6 +9638,7 @@ var StreamerServer = class {
|
|
|
9430
9638
|
this.claudeFlags = config.claudeFlags ?? loadClaudeFlags();
|
|
9431
9639
|
this.claudeExtraArgs = config.claudeExtraArgs ?? loadClaudeExtraArgs();
|
|
9432
9640
|
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path18.join)((0, import_os10.homedir)(), ".threadbase", "cache");
|
|
9641
|
+
this.runtimeDbPath = config.runtimeDbPath ?? process.env.THREADBASE_RUNTIME_DB ?? (0, import_path18.join)(process.env.THREADBASE_CONFIG_DIR ?? (0, import_path18.join)((0, import_os10.homedir)(), ".threadbase"), "runtime.db");
|
|
9433
9642
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
9434
9643
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
9435
9644
|
this.markScannerStaleDebounced = debounce(() => {
|
|
@@ -9600,6 +9809,7 @@ var StreamerServer = class {
|
|
|
9600
9809
|
if (resp) this.wsHub.broadcast({ type: "session_ready", session: resp });
|
|
9601
9810
|
},
|
|
9602
9811
|
onStatusChange: (session) => {
|
|
9812
|
+
const previousStatus = this.sessionStore.getManaged(session.id)?.status;
|
|
9603
9813
|
this.sessionStore.updateManaged(session.id, {
|
|
9604
9814
|
status: session.status,
|
|
9605
9815
|
completedAt: session.completedAt,
|
|
@@ -9613,7 +9823,11 @@ var StreamerServer = class {
|
|
|
9613
9823
|
completedAt: session.completedAt,
|
|
9614
9824
|
lastActivityAt: session.lastActivityAt ?? null,
|
|
9615
9825
|
promptCount: session.promptCount,
|
|
9616
|
-
failureReason: session.failureReason ?? null
|
|
9826
|
+
failureReason: session.failureReason ?? null,
|
|
9827
|
+
// Derived from the first user message, so it does not exist yet at
|
|
9828
|
+
// recordSpawn. The input that produces it also flips
|
|
9829
|
+
// waiting_input→running, which lands here.
|
|
9830
|
+
sessionName: session.sessionName ?? null
|
|
9617
9831
|
}
|
|
9618
9832
|
);
|
|
9619
9833
|
if (session.status === "waiting_input" || session.status === "idle") {
|
|
@@ -9654,7 +9868,7 @@ var StreamerServer = class {
|
|
|
9654
9868
|
if (resp) {
|
|
9655
9869
|
this.wsHub.broadcast({ type: "session_update", session: resp });
|
|
9656
9870
|
}
|
|
9657
|
-
void this.liveActivityNotifier?.onStatusChange(session);
|
|
9871
|
+
void this.liveActivityNotifier?.onStatusChange(session, previousStatus);
|
|
9658
9872
|
this.sessionStatusBus.emit(`status:${session.id}`, session.status);
|
|
9659
9873
|
}
|
|
9660
9874
|
});
|
|
@@ -9705,6 +9919,7 @@ var StreamerServer = class {
|
|
|
9705
9919
|
conversationsRepo: () => this.conversationsRepo,
|
|
9706
9920
|
sessionsRepo: () => this.sessionsRepo,
|
|
9707
9921
|
cacheMetadataRepo: () => this.cacheMetadataRepo,
|
|
9922
|
+
runtimeStore: () => this.runtimeStore,
|
|
9708
9923
|
ptyAttachedIds: () => this.ptyAttachedIds(),
|
|
9709
9924
|
handleListSessions: (url, res) => this.handleListSessions(url, res),
|
|
9710
9925
|
handleSessionsCount: (res) => this.handleSessionsCount(res),
|
|
@@ -10004,6 +10219,58 @@ var StreamerServer = class {
|
|
|
10004
10219
|
}
|
|
10005
10220
|
return verdicts;
|
|
10006
10221
|
}
|
|
10222
|
+
/**
|
|
10223
|
+
* Seed the session list with what previous runs left behind (persistence plan
|
|
10224
|
+
* Phase 1, gaps G1/G2/G8).
|
|
10225
|
+
*
|
|
10226
|
+
* Reconciliation classifies rows and stops there; a verdict is overlaid onto a
|
|
10227
|
+
* SessionResponse that already exists, and after a clean restart none does —
|
|
10228
|
+
* `SessionStore` starts empty. So the user's session did not become
|
|
10229
|
+
* `resumable`, it became *absent*. This is the half that puts it back.
|
|
10230
|
+
*
|
|
10231
|
+
* The seeded stubs hold no PTY and are never handed to `LiveSessionManager`,
|
|
10232
|
+
* so `reapIdleSessions` and `startGraceTimer` — both of which iterate
|
|
10233
|
+
* `ptyManager.listSessions()` — cannot observe them. A later resume calls
|
|
10234
|
+
* `sessionStore.addManaged` with the real session, which overwrites the stub
|
|
10235
|
+
* by id rather than duplicating it.
|
|
10236
|
+
*/
|
|
10237
|
+
rehydratePreviousSessions(verdicts) {
|
|
10238
|
+
if (!this.featureFlags.sessionRehydration || !this.managedSessionsRepo) return;
|
|
10239
|
+
try {
|
|
10240
|
+
const now = Date.now();
|
|
10241
|
+
const rows = this.managedSessionsRepo.listRecoverable({
|
|
10242
|
+
sinceMs: now - REHYDRATE_WINDOW_MS,
|
|
10243
|
+
limit: REHYDRATE_MAX + 1
|
|
10244
|
+
});
|
|
10245
|
+
const truncated = rows.length > REHYDRATE_MAX;
|
|
10246
|
+
const candidates = truncated ? rows.slice(0, REHYDRATE_MAX) : rows;
|
|
10247
|
+
if (candidates.length === 0) return;
|
|
10248
|
+
const lifecycleByVerdict = new Map(verdicts.map((v) => [v.sessionId, v.lifecycle]));
|
|
10249
|
+
let rehydrated = 0;
|
|
10250
|
+
for (const row of candidates) {
|
|
10251
|
+
if (this.sessionStore.getManaged(row.session_id)) continue;
|
|
10252
|
+
if (!shouldRehydrate(row, { now, projectExists: import_fs19.existsSync })) continue;
|
|
10253
|
+
this.sessionStore.addManaged(rowToStubSession(row));
|
|
10254
|
+
this.sessionLifecycles.set(
|
|
10255
|
+
row.session_id,
|
|
10256
|
+
lifecycleByVerdict.get(row.session_id) ?? "resumable"
|
|
10257
|
+
);
|
|
10258
|
+
if (row.completed_at != null) this.selfPtyEndedAt.set(row.session_id, row.completed_at);
|
|
10259
|
+
rehydrated++;
|
|
10260
|
+
}
|
|
10261
|
+
this.log.info(`[rehydrate] recovered ${rehydrated} session(s) from the registry`, {
|
|
10262
|
+
event: "sessions.rehydrated",
|
|
10263
|
+
rehydrated,
|
|
10264
|
+
skipped: candidates.length - rehydrated,
|
|
10265
|
+
truncated
|
|
10266
|
+
});
|
|
10267
|
+
} catch (err) {
|
|
10268
|
+
this.log.warn("[rehydrate] failed to rehydrate previous sessions", {
|
|
10269
|
+
event: "sessions.rehydrate_failed",
|
|
10270
|
+
err
|
|
10271
|
+
});
|
|
10272
|
+
}
|
|
10273
|
+
}
|
|
10007
10274
|
/**
|
|
10008
10275
|
* Pick a token guaranteed to appear in the spawned process's argv, for the
|
|
10009
10276
|
* reconciler's pid-reuse guard.
|
|
@@ -10237,6 +10504,17 @@ var StreamerServer = class {
|
|
|
10237
10504
|
port,
|
|
10238
10505
|
event: "server.listening"
|
|
10239
10506
|
});
|
|
10507
|
+
try {
|
|
10508
|
+
this.runtimeStore = RuntimeStore.open(this.runtimeDbPath);
|
|
10509
|
+
this.managedSessionsRepo = new ManagedSessionsRepository(this.runtimeStore.getDatabase());
|
|
10510
|
+
} catch (err) {
|
|
10511
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
10512
|
+
const abiMismatch = message.includes("NODE_MODULE_VERSION") || message.includes("was compiled against a different Node.js version");
|
|
10513
|
+
this.log.error(
|
|
10514
|
+
`Runtime store failed to open \u2014 session persistence DISABLED; sessions will not survive a restart.` + (abiMismatch ? ` Fix: npm rebuild better-sqlite3` : "") + ` (${message})`,
|
|
10515
|
+
{ error: message, abiMismatch, path: this.runtimeDbPath, event: "runtime.open_failed" }
|
|
10516
|
+
);
|
|
10517
|
+
}
|
|
10240
10518
|
try {
|
|
10241
10519
|
this.cache = ConversationCache.open(
|
|
10242
10520
|
(0, import_path18.join)(this.cacheDir, "cache.db"),
|
|
@@ -10264,8 +10542,20 @@ var StreamerServer = class {
|
|
|
10264
10542
|
this.projectsRepo = new ProjectsRepository(db);
|
|
10265
10543
|
this.conversationsRepo = new ConversationsRepository(this.cache);
|
|
10266
10544
|
this.sessionsRepo = new SessionsRepository(this.sessionStore);
|
|
10267
|
-
|
|
10268
|
-
|
|
10545
|
+
try {
|
|
10546
|
+
const copied = this.runtimeStore?.importLegacyManagedSessions(db) ?? 0;
|
|
10547
|
+
if (copied > 0) {
|
|
10548
|
+
this.log.info(`Copied ${copied} managed session row(s) from cache.db to runtime.db`, {
|
|
10549
|
+
copied,
|
|
10550
|
+
event: "runtime.legacy_import"
|
|
10551
|
+
});
|
|
10552
|
+
}
|
|
10553
|
+
} catch (err) {
|
|
10554
|
+
this.log.warn("[registry] legacy managed_sessions copy failed", {
|
|
10555
|
+
event: "runtime.legacy_import_failed",
|
|
10556
|
+
err
|
|
10557
|
+
});
|
|
10558
|
+
}
|
|
10269
10559
|
this.cacheMetadataRepo = new CacheMetadataRepository(db);
|
|
10270
10560
|
this.pushRepo = new PushRepository(db);
|
|
10271
10561
|
this.devicesRepo = new DevicesRepository(db);
|
|
@@ -10301,6 +10591,7 @@ var StreamerServer = class {
|
|
|
10301
10591
|
);
|
|
10302
10592
|
this.scannerPersistenceDisabled = true;
|
|
10303
10593
|
}
|
|
10594
|
+
void this.reconcilePreviousSessions().then((v) => this.rehydratePreviousSessions(v));
|
|
10304
10595
|
if (this.skipStartupWarmup) {
|
|
10305
10596
|
this.log.debug?.("startup warm-up scan skipped (skipStartupWarmup)", {
|
|
10306
10597
|
event: "cache.warmup_skipped"
|
|
@@ -10508,6 +10799,7 @@ var StreamerServer = class {
|
|
|
10508
10799
|
this.allScanners.clear();
|
|
10509
10800
|
this.scanner = null;
|
|
10510
10801
|
this.cache?.close();
|
|
10802
|
+
this.runtimeStore?.close();
|
|
10511
10803
|
this.ptyManager.dispose();
|
|
10512
10804
|
this.fileWatcher.dispose();
|
|
10513
10805
|
this.externalTails.clear();
|
|
@@ -10955,7 +11247,8 @@ var StreamerServer = class {
|
|
|
10955
11247
|
}
|
|
10956
11248
|
handleSessionsCount(res) {
|
|
10957
11249
|
if (this.rejectIfWarmingUp(res)) return;
|
|
10958
|
-
|
|
11250
|
+
const total = this.sessionStore.list(this.ptyAttachedIds()).filter((s) => s.ownership !== "historical").length;
|
|
11251
|
+
json(res, 200, { total });
|
|
10959
11252
|
}
|
|
10960
11253
|
handleGetRecentSessions(url, res) {
|
|
10961
11254
|
if (this.rejectIfWarmingUp(res)) return;
|