@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.js
CHANGED
|
@@ -448,6 +448,12 @@ var FEATURE_FLAGS = [
|
|
|
448
448
|
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.",
|
|
449
449
|
default: false,
|
|
450
450
|
env: "THREADBASE_FEATURE_CODEX_SYSTEM_PROMPT"
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
id: "sessionRehydration",
|
|
454
|
+
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.",
|
|
455
|
+
default: true,
|
|
456
|
+
env: "THREADBASE_FEATURE_SESSION_REHYDRATION"
|
|
451
457
|
}
|
|
452
458
|
];
|
|
453
459
|
function findFeatureFlag(id) {
|
|
@@ -1883,6 +1889,12 @@ function detectShellPrompt(lines) {
|
|
|
1883
1889
|
return null;
|
|
1884
1890
|
}
|
|
1885
1891
|
|
|
1892
|
+
// src/utils/deriveSessionName.ts
|
|
1893
|
+
function deriveSessionName(firstMessageText) {
|
|
1894
|
+
const firstLine = firstMessageText.split("\n", 1)[0]?.trim() ?? "";
|
|
1895
|
+
return firstLine.slice(0, 80);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1886
1898
|
// src/pty-manager.ts
|
|
1887
1899
|
var OUTPUT_BUFFER_MAX2 = 65536;
|
|
1888
1900
|
var INPUT_HISTORY_MAX2 = 50;
|
|
@@ -1890,8 +1902,8 @@ var PTY_COLS2 = 120;
|
|
|
1890
1902
|
var PTY_ROWS2 = 40;
|
|
1891
1903
|
var SCREEN_SCROLLBACK2 = 1e3;
|
|
1892
1904
|
var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
|
|
1893
|
-
var PROMPT_MARKER_FALLBACK_MS = 1e4;
|
|
1894
1905
|
var QUIET_DETECT_MS2 = 500;
|
|
1906
|
+
var CLAUDE_READY_FALLBACK_MS = 8e3;
|
|
1895
1907
|
function buildPasteBytes(input) {
|
|
1896
1908
|
return `\x1B[200~${input}\x1B[201~`;
|
|
1897
1909
|
}
|
|
@@ -1967,8 +1979,8 @@ var PTYManager = class {
|
|
|
1967
1979
|
// silently lost — the "dot bug".
|
|
1968
1980
|
queuedInputs = /* @__PURE__ */ new Map();
|
|
1969
1981
|
log;
|
|
1970
|
-
// Timestamp of first PTY chunk per session;
|
|
1971
|
-
//
|
|
1982
|
+
// Timestamp of first PTY chunk per session; used for the [pty.ready] elapsed
|
|
1983
|
+
// measurement so a slow boot is visible in the logs.
|
|
1972
1984
|
firstChunkAt = /* @__PURE__ */ new Map();
|
|
1973
1985
|
// Per-session chunk counter and last-chunk timestamp. Diagnostic-only,
|
|
1974
1986
|
// feeds the [pty.chunk] log lines so we can trace whether Claude responded
|
|
@@ -1979,6 +1991,8 @@ var PTYManager = class {
|
|
|
1979
1991
|
// QUIET_DETECT_MS after the last chunk so ready/prompt detection doesn't
|
|
1980
1992
|
// wait for another chunk that may never arrive (Claude blocked on input).
|
|
1981
1993
|
quietCheckers = /* @__PURE__ */ new Map();
|
|
1994
|
+
// Per-session flat backstop from the first chunk (CLAUDE_READY_FALLBACK_MS).
|
|
1995
|
+
readyFallbackTimers = /* @__PURE__ */ new Map();
|
|
1982
1996
|
// In-flight start()/startFresh() calls keyed by sessionId. A second
|
|
1983
1997
|
// concurrent resume for the same session (double-tap, client retry) awaits
|
|
1984
1998
|
// the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
|
|
@@ -2064,6 +2078,7 @@ var PTYManager = class {
|
|
|
2064
2078
|
};
|
|
2065
2079
|
this.sessions.set(sessionId, session);
|
|
2066
2080
|
this.pendingReady.add(sessionId);
|
|
2081
|
+
this.armReadyFallback(sessionId);
|
|
2067
2082
|
proc.onData((data) => {
|
|
2068
2083
|
this.handleOutput(sessionId, data);
|
|
2069
2084
|
});
|
|
@@ -2124,6 +2139,7 @@ var PTYManager = class {
|
|
|
2124
2139
|
};
|
|
2125
2140
|
this.sessions.set(sessionId, session);
|
|
2126
2141
|
this.pendingReady.add(sessionId);
|
|
2142
|
+
this.armReadyFallback(sessionId);
|
|
2127
2143
|
proc.onData((data) => {
|
|
2128
2144
|
this.handleOutput(sessionId, data);
|
|
2129
2145
|
});
|
|
@@ -2298,6 +2314,7 @@ var PTYManager = class {
|
|
|
2298
2314
|
this.shellPromptOpen.delete(sessionId);
|
|
2299
2315
|
this.quietCheckers.get(sessionId)?.cancel();
|
|
2300
2316
|
this.quietCheckers.delete(sessionId);
|
|
2317
|
+
this.clearReadyFallback(sessionId);
|
|
2301
2318
|
try {
|
|
2302
2319
|
session.process.kill("SIGINT");
|
|
2303
2320
|
} catch {
|
|
@@ -2356,6 +2373,10 @@ var PTYManager = class {
|
|
|
2356
2373
|
if (session.inputHistory.length > INPUT_HISTORY_MAX2) {
|
|
2357
2374
|
session.inputHistory.shift();
|
|
2358
2375
|
}
|
|
2376
|
+
if (session.firstMessageText === void 0) {
|
|
2377
|
+
session.firstMessageText = text;
|
|
2378
|
+
session.sessionName = deriveSessionName(text);
|
|
2379
|
+
}
|
|
2359
2380
|
this.onUserMessage?.(session.id, text, ts);
|
|
2360
2381
|
}
|
|
2361
2382
|
getSession(sessionId) {
|
|
@@ -2382,6 +2403,8 @@ var PTYManager = class {
|
|
|
2382
2403
|
this.lastChunkAt.clear();
|
|
2383
2404
|
for (const quiet of this.quietCheckers.values()) quiet.cancel();
|
|
2384
2405
|
this.quietCheckers.clear();
|
|
2406
|
+
for (const timer of this.readyFallbackTimers.values()) clearTimeout(timer);
|
|
2407
|
+
this.readyFallbackTimers.clear();
|
|
2385
2408
|
this.permissionOpen.clear();
|
|
2386
2409
|
this.lastScreenQuestionKey.clear();
|
|
2387
2410
|
this.shellPromptOpen.clear();
|
|
@@ -2424,8 +2447,6 @@ var PTYManager = class {
|
|
|
2424
2447
|
const matchedMarker = CLAUDE_PROMPT_MARKERS.find((m) => stripped.includes(m));
|
|
2425
2448
|
if (session.status === "running" && matchedMarker) {
|
|
2426
2449
|
this.markReady(sessionId, session, "prompt-marker", `marker:${matchedMarker}`);
|
|
2427
|
-
} else if (session.status === "running" && this.pendingReady.has(sessionId) && now - (this.firstChunkAt.get(sessionId) ?? now) >= PROMPT_MARKER_FALLBACK_MS) {
|
|
2428
|
-
this.markReady(sessionId, session, "timeout-fallback", "fallback:timeout");
|
|
2429
2450
|
}
|
|
2430
2451
|
this.onOutput?.(sessionId, data);
|
|
2431
2452
|
this.detectLivePrompts(sessionId, data, stripped).catch((err) => {
|
|
@@ -2525,7 +2546,13 @@ var PTYManager = class {
|
|
|
2525
2546
|
const session = this.sessions.get(sessionId);
|
|
2526
2547
|
if (session?.status !== "running") return;
|
|
2527
2548
|
if (this.pendingReady.has(sessionId)) {
|
|
2528
|
-
this.
|
|
2549
|
+
this.recheckReadyFromScreen(sessionId).catch((err) => {
|
|
2550
|
+
this.log.warn("[pty.ready] boot screen recheck failed", {
|
|
2551
|
+
event: "pty.ready_recheck_failed",
|
|
2552
|
+
sessionId,
|
|
2553
|
+
err
|
|
2554
|
+
});
|
|
2555
|
+
});
|
|
2529
2556
|
} else {
|
|
2530
2557
|
this.recheckReadyFromScreen(sessionId).catch((err) => {
|
|
2531
2558
|
this.log.warn("[pty.ready] screen recheck failed", {
|
|
@@ -2557,9 +2584,32 @@ var PTYManager = class {
|
|
|
2557
2584
|
this.markReady(sessionId, session, "screen-marker", `quiet:screen-marker:${matchedMarker}`);
|
|
2558
2585
|
}
|
|
2559
2586
|
}
|
|
2587
|
+
// Flat backstop from the first chunk: if neither a prompt marker nor the
|
|
2588
|
+
// screen recheck settles the session within CLAUDE_READY_FALLBACK_MS, mark it
|
|
2589
|
+
// ready anyway so start requests resolve and queued input is not held
|
|
2590
|
+
// forever. This is what makes the quiet-checker safe to be strict — a boot
|
|
2591
|
+
// variant whose marker we cannot see still recovers, just 8s later instead of
|
|
2592
|
+
// 500ms sooner and wrong. unref() so it never holds the process open.
|
|
2593
|
+
armReadyFallback(sessionId) {
|
|
2594
|
+
const timer = setTimeout(() => {
|
|
2595
|
+
this.readyFallbackTimers.delete(sessionId);
|
|
2596
|
+
const session = this.sessions.get(sessionId);
|
|
2597
|
+
if (session?.status === "running" && this.pendingReady.has(sessionId)) {
|
|
2598
|
+
this.markReady(sessionId, session, "timeout-fallback", "fallback:timeout");
|
|
2599
|
+
}
|
|
2600
|
+
}, CLAUDE_READY_FALLBACK_MS);
|
|
2601
|
+
timer.unref?.();
|
|
2602
|
+
this.readyFallbackTimers.set(sessionId, timer);
|
|
2603
|
+
}
|
|
2604
|
+
clearReadyFallback(sessionId) {
|
|
2605
|
+
const timer = this.readyFallbackTimers.get(sessionId);
|
|
2606
|
+
if (timer) clearTimeout(timer);
|
|
2607
|
+
this.readyFallbackTimers.delete(sessionId);
|
|
2608
|
+
}
|
|
2560
2609
|
// Transition a session from "running" to "waiting_input", clear pendingReady,
|
|
2561
2610
|
// and flush any queued input. Idempotent: callers can invoke at any chunk.
|
|
2562
2611
|
markReady(sessionId, session, source, reason) {
|
|
2612
|
+
this.clearReadyFallback(sessionId);
|
|
2563
2613
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
2564
2614
|
session.status = "waiting_input";
|
|
2565
2615
|
session.statusSource = source;
|
|
@@ -2603,6 +2653,7 @@ var PTYManager = class {
|
|
|
2603
2653
|
this.shellPromptOpen.delete(sessionId);
|
|
2604
2654
|
this.quietCheckers.get(sessionId)?.cancel();
|
|
2605
2655
|
this.quietCheckers.delete(sessionId);
|
|
2656
|
+
this.clearReadyFallback(sessionId);
|
|
2606
2657
|
}
|
|
2607
2658
|
};
|
|
2608
2659
|
function toPublicSession2(s) {
|
|
@@ -2621,7 +2672,9 @@ function toPublicSession2(s) {
|
|
|
2621
2672
|
...s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt },
|
|
2622
2673
|
...s.statusSource != null && { statusSource: s.statusSource },
|
|
2623
2674
|
...s.statusUpdatedAt != null && { statusUpdatedAt: s.statusUpdatedAt },
|
|
2624
|
-
...s.filePath != null && { filePath: s.filePath }
|
|
2675
|
+
...s.filePath != null && { filePath: s.filePath },
|
|
2676
|
+
...s.sessionName != null && { sessionName: s.sessionName },
|
|
2677
|
+
...s.firstMessageText != null && { firstMessageText: s.firstMessageText }
|
|
2625
2678
|
};
|
|
2626
2679
|
}
|
|
2627
2680
|
function stripAnsi2(str) {
|
|
@@ -5141,6 +5194,9 @@ function getMigrationsDir2() {
|
|
|
5141
5194
|
}
|
|
5142
5195
|
return __dirname;
|
|
5143
5196
|
}
|
|
5197
|
+
function resolveMigrationsDir(name = "migrations") {
|
|
5198
|
+
return join12(getMigrationsDir2(), name);
|
|
5199
|
+
}
|
|
5144
5200
|
var SCHEMA_MIGRATIONS_SQL = `
|
|
5145
5201
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
5146
5202
|
id TEXT PRIMARY KEY,
|
|
@@ -5149,7 +5205,7 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
|
5149
5205
|
`;
|
|
5150
5206
|
function runSqliteMigrations(db, migrationsDir) {
|
|
5151
5207
|
db.exec(SCHEMA_MIGRATIONS_SQL);
|
|
5152
|
-
const dir = migrationsDir ??
|
|
5208
|
+
const dir = migrationsDir ?? resolveMigrationsDir();
|
|
5153
5209
|
const files = readdirSync2(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
5154
5210
|
const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
|
|
5155
5211
|
const appliedSet = new Set(appliedRows.map((r) => r.id));
|
|
@@ -6560,6 +6616,7 @@ var ManagedSessionsRepository = class {
|
|
|
6560
6616
|
updateStatusStmt;
|
|
6561
6617
|
getStmt;
|
|
6562
6618
|
listNonTerminalStmt;
|
|
6619
|
+
listRecoverableStmt;
|
|
6563
6620
|
deleteStmt;
|
|
6564
6621
|
constructor(db) {
|
|
6565
6622
|
this.upsertStmt = db.prepare(`
|
|
@@ -6603,7 +6660,8 @@ var ManagedSessionsRepository = class {
|
|
|
6603
6660
|
completed_at = @completed_at,
|
|
6604
6661
|
last_activity_at = @last_activity_at,
|
|
6605
6662
|
prompt_count = @prompt_count,
|
|
6606
|
-
failure_reason = COALESCE(@failure_reason, failure_reason)
|
|
6663
|
+
failure_reason = COALESCE(@failure_reason, failure_reason),
|
|
6664
|
+
session_name = COALESCE(@session_name, session_name)
|
|
6607
6665
|
WHERE session_id = @session_id
|
|
6608
6666
|
`);
|
|
6609
6667
|
this.getStmt = db.prepare("SELECT * FROM managed_sessions WHERE session_id = ?");
|
|
@@ -6612,6 +6670,13 @@ var ManagedSessionsRepository = class {
|
|
|
6612
6670
|
WHERE completed_at IS NULL
|
|
6613
6671
|
ORDER BY started_at ASC
|
|
6614
6672
|
`);
|
|
6673
|
+
this.listRecoverableStmt = db.prepare(`
|
|
6674
|
+
SELECT * FROM managed_sessions
|
|
6675
|
+
WHERE (completed_at IS NULL OR status_source = 'shutdown')
|
|
6676
|
+
AND status_updated_at >= @since
|
|
6677
|
+
ORDER BY status_updated_at DESC
|
|
6678
|
+
LIMIT @limit
|
|
6679
|
+
`);
|
|
6615
6680
|
this.deleteStmt = db.prepare("DELETE FROM managed_sessions WHERE session_id = ?");
|
|
6616
6681
|
}
|
|
6617
6682
|
/** Record a session at spawn, or refresh every field of an existing row. */
|
|
@@ -6653,7 +6718,8 @@ var ManagedSessionsRepository = class {
|
|
|
6653
6718
|
completed_at: fields.completedAt?.getTime() ?? null,
|
|
6654
6719
|
last_activity_at: fields.lastActivityAt?.getTime() ?? null,
|
|
6655
6720
|
prompt_count: fields.promptCount ?? 0,
|
|
6656
|
-
failure_reason: fields.failureReason ?? null
|
|
6721
|
+
failure_reason: fields.failureReason ?? null,
|
|
6722
|
+
session_name: fields.sessionName ?? null
|
|
6657
6723
|
});
|
|
6658
6724
|
}
|
|
6659
6725
|
get(sessionId) {
|
|
@@ -6663,6 +6729,14 @@ var ManagedSessionsRepository = class {
|
|
|
6663
6729
|
listNonTerminal() {
|
|
6664
6730
|
return this.listNonTerminalStmt.all();
|
|
6665
6731
|
}
|
|
6732
|
+
/**
|
|
6733
|
+
* Rows a restart could bring back: still open, or closed by our own shutdown,
|
|
6734
|
+
* and touched no longer ago than `sinceMs`. Newest first, capped — the caller
|
|
6735
|
+
* decides which of these actually deserve rehydrating (`shouldRehydrate`).
|
|
6736
|
+
*/
|
|
6737
|
+
listRecoverable({ sinceMs, limit }) {
|
|
6738
|
+
return this.listRecoverableStmt.all({ since: sinceMs, limit });
|
|
6739
|
+
}
|
|
6666
6740
|
delete(sessionId) {
|
|
6667
6741
|
this.deleteStmt.run(sessionId);
|
|
6668
6742
|
}
|
|
@@ -6798,6 +6872,54 @@ var SessionsRepository = class {
|
|
|
6798
6872
|
}
|
|
6799
6873
|
};
|
|
6800
6874
|
|
|
6875
|
+
// src/db/runtime-store.ts
|
|
6876
|
+
import Database2 from "better-sqlite3";
|
|
6877
|
+
var RuntimeStore = class _RuntimeStore {
|
|
6878
|
+
constructor(db) {
|
|
6879
|
+
this.db = db;
|
|
6880
|
+
}
|
|
6881
|
+
db;
|
|
6882
|
+
static open(dbPath, migrationsDir) {
|
|
6883
|
+
const db = new Database2(dbPath);
|
|
6884
|
+
db.pragma("journal_mode = WAL");
|
|
6885
|
+
runSqliteMigrations(db, migrationsDir ?? resolveMigrationsDir("runtime-migrations"));
|
|
6886
|
+
return new _RuntimeStore(db);
|
|
6887
|
+
}
|
|
6888
|
+
getDatabase() {
|
|
6889
|
+
return this.db;
|
|
6890
|
+
}
|
|
6891
|
+
/**
|
|
6892
|
+
* One-time move of `managed_sessions` rows out of a pre-split `cache.db`.
|
|
6893
|
+
*
|
|
6894
|
+
* Non-destructive by design: the source table is left in place so an older
|
|
6895
|
+
* streamer rolled back onto the same machine still finds its registry. Runs
|
|
6896
|
+
* only when this file's table is empty, so a second boot is a no-op rather
|
|
6897
|
+
* than a re-copy that would resurrect rows deleted since.
|
|
6898
|
+
*
|
|
6899
|
+
* Returns the number of rows copied.
|
|
6900
|
+
*/
|
|
6901
|
+
importLegacyManagedSessions(source) {
|
|
6902
|
+
const existing = this.db.prepare("SELECT COUNT(*) AS n FROM managed_sessions").get();
|
|
6903
|
+
if (existing.n > 0) return 0;
|
|
6904
|
+
const hasTable = source.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'managed_sessions'").get();
|
|
6905
|
+
if (!hasTable) return 0;
|
|
6906
|
+
const rows = source.prepare("SELECT * FROM managed_sessions").all();
|
|
6907
|
+
if (rows.length === 0) return 0;
|
|
6908
|
+
const columns = Object.keys(rows[0]);
|
|
6909
|
+
const insert = this.db.prepare(
|
|
6910
|
+
`INSERT OR IGNORE INTO managed_sessions (${columns.join(", ")})
|
|
6911
|
+
VALUES (${columns.map((c) => `@${c}`).join(", ")})`
|
|
6912
|
+
);
|
|
6913
|
+
this.db.transaction((batch) => {
|
|
6914
|
+
for (const row of batch) insert.run(row);
|
|
6915
|
+
})(rows);
|
|
6916
|
+
return rows.length;
|
|
6917
|
+
}
|
|
6918
|
+
close() {
|
|
6919
|
+
this.db.close();
|
|
6920
|
+
}
|
|
6921
|
+
};
|
|
6922
|
+
|
|
6801
6923
|
// src/db/upload-records.ts
|
|
6802
6924
|
async function recordUpload(pool2, instanceId, row) {
|
|
6803
6925
|
if (!pool2) return;
|
|
@@ -7815,7 +7937,8 @@ function contentStateForSession(args) {
|
|
|
7815
7937
|
status,
|
|
7816
7938
|
startedAt: args.startedAtOverride ?? args.session.startedAt.getTime(),
|
|
7817
7939
|
lastOutput: truncateLastOutput(args.session.lastOutput ?? ""),
|
|
7818
|
-
...args.serverLabel != null && { serverLabel: args.serverLabel }
|
|
7940
|
+
...args.serverLabel != null && { serverLabel: args.serverLabel },
|
|
7941
|
+
...args.session.sessionName != null && { sessionName: args.session.sessionName }
|
|
7819
7942
|
};
|
|
7820
7943
|
}
|
|
7821
7944
|
var LiveActivityNotifier = class {
|
|
@@ -7828,14 +7951,17 @@ var LiveActivityNotifier = class {
|
|
|
7828
7951
|
serverId;
|
|
7829
7952
|
serverLabel;
|
|
7830
7953
|
/**
|
|
7831
|
-
*
|
|
7954
|
+
* Sessions with a currently open (pushed) activity.
|
|
7832
7955
|
*
|
|
7833
|
-
*
|
|
7834
|
-
*
|
|
7835
|
-
*
|
|
7836
|
-
*
|
|
7956
|
+
* An activity opens on a `waiting_input → running` edge (the user sent a
|
|
7957
|
+
* prompt) and closes on the matching `running → waiting_input` edge (the
|
|
7958
|
+
* response, including any sub-agents, finished) — so this set is what makes
|
|
7959
|
+
* the notifier per-turn rather than per-session. A session's very first
|
|
7960
|
+
* `running` (right after spawn, before any user prompt) has no prior
|
|
7961
|
+
* `waiting_input` and therefore no edge, so it never opens an activity —
|
|
7962
|
+
* this is what keeps a fresh/idle session from pushing anything.
|
|
7837
7963
|
*/
|
|
7838
|
-
|
|
7964
|
+
openActivity = /* @__PURE__ */ new Map();
|
|
7839
7965
|
/**
|
|
7840
7966
|
* React to a session status change.
|
|
7841
7967
|
*
|
|
@@ -7843,34 +7969,22 @@ var LiveActivityNotifier = class {
|
|
|
7843
7969
|
* transition, so this returns a promise the caller may ignore and every error
|
|
7844
7970
|
* is logged rather than propagated.
|
|
7845
7971
|
*/
|
|
7846
|
-
async onStatusChange(session) {
|
|
7972
|
+
async onStatusChange(session, previousStatus) {
|
|
7847
7973
|
const status = toLiveActivityStatus(session.status);
|
|
7848
7974
|
try {
|
|
7849
7975
|
if (!status) {
|
|
7850
|
-
await this.endFor(session);
|
|
7976
|
+
if (this.openActivity.has(session.id)) await this.endFor(session);
|
|
7851
7977
|
return;
|
|
7852
7978
|
}
|
|
7853
|
-
if (
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
const outcome = await this.sender.send({
|
|
7861
|
-
sessionId: session.id,
|
|
7862
|
-
event: "update",
|
|
7863
|
-
contentState
|
|
7864
|
-
});
|
|
7865
|
-
this.lastPushed.set(session.id, status);
|
|
7866
|
-
if (outcome.attempted > 0) {
|
|
7867
|
-
log4.info("live_activity.updated", {
|
|
7868
|
-
event: "live_activity.updated",
|
|
7869
|
-
sessionId: session.id,
|
|
7870
|
-
status,
|
|
7871
|
-
...outcome
|
|
7872
|
-
});
|
|
7979
|
+
if (status === "running" && previousStatus === "waiting_input") {
|
|
7980
|
+
await this.startTurn(session);
|
|
7981
|
+
return;
|
|
7982
|
+
}
|
|
7983
|
+
if (status === "waiting_input" && previousStatus === "running") {
|
|
7984
|
+
if (this.openActivity.has(session.id)) await this.endFor(session);
|
|
7985
|
+
return;
|
|
7873
7986
|
}
|
|
7987
|
+
await this.maybeSendName(session);
|
|
7874
7988
|
} catch (err) {
|
|
7875
7989
|
log4.error("live_activity.notify_failed", {
|
|
7876
7990
|
event: "live_activity.notify_failed",
|
|
@@ -7880,14 +7994,57 @@ var LiveActivityNotifier = class {
|
|
|
7880
7994
|
});
|
|
7881
7995
|
}
|
|
7882
7996
|
}
|
|
7997
|
+
async startTurn(session) {
|
|
7998
|
+
const contentState = contentStateForSession({
|
|
7999
|
+
session,
|
|
8000
|
+
serverId: this.serverId,
|
|
8001
|
+
serverLabel: this.serverLabel
|
|
8002
|
+
});
|
|
8003
|
+
if (!contentState) return;
|
|
8004
|
+
const outcome = await this.sender.send({
|
|
8005
|
+
sessionId: session.id,
|
|
8006
|
+
event: "update",
|
|
8007
|
+
contentState
|
|
8008
|
+
});
|
|
8009
|
+
this.openActivity.set(session.id, { sessionNameSent: session.sessionName != null });
|
|
8010
|
+
if (outcome.attempted > 0) {
|
|
8011
|
+
log4.info("live_activity.updated", {
|
|
8012
|
+
event: "live_activity.updated",
|
|
8013
|
+
sessionId: session.id,
|
|
8014
|
+
status: contentState.status,
|
|
8015
|
+
...outcome
|
|
8016
|
+
});
|
|
8017
|
+
}
|
|
8018
|
+
}
|
|
8019
|
+
async maybeSendName(session) {
|
|
8020
|
+
const open2 = this.openActivity.get(session.id);
|
|
8021
|
+
if (!open2 || open2.sessionNameSent || session.sessionName == null) return;
|
|
8022
|
+
const contentState = contentStateForSession({
|
|
8023
|
+
session,
|
|
8024
|
+
serverId: this.serverId,
|
|
8025
|
+
serverLabel: this.serverLabel
|
|
8026
|
+
});
|
|
8027
|
+
if (!contentState) return;
|
|
8028
|
+
const outcome = await this.sender.send({
|
|
8029
|
+
sessionId: session.id,
|
|
8030
|
+
event: "update",
|
|
8031
|
+
contentState
|
|
8032
|
+
});
|
|
8033
|
+
open2.sessionNameSent = true;
|
|
8034
|
+
if (outcome.attempted > 0) {
|
|
8035
|
+
log4.info("live_activity.updated", {
|
|
8036
|
+
event: "live_activity.updated",
|
|
8037
|
+
sessionId: session.id,
|
|
8038
|
+
status: contentState.status,
|
|
8039
|
+
...outcome
|
|
8040
|
+
});
|
|
8041
|
+
}
|
|
8042
|
+
}
|
|
7883
8043
|
async endFor(session) {
|
|
7884
|
-
|
|
7885
|
-
|
|
8044
|
+
this.openActivity.delete(session.id);
|
|
8045
|
+
const status = toLiveActivityStatus(session.status);
|
|
7886
8046
|
const contentState = contentStateForSession({
|
|
7887
|
-
session: {
|
|
7888
|
-
...session,
|
|
7889
|
-
status: lastStatus === "waiting_input" ? "waiting_input" : "running"
|
|
7890
|
-
},
|
|
8047
|
+
session: { ...session, status: status ?? "waiting_input" },
|
|
7891
8048
|
serverId: this.serverId,
|
|
7892
8049
|
serverLabel: this.serverLabel
|
|
7893
8050
|
});
|
|
@@ -7901,9 +8058,9 @@ var LiveActivityNotifier = class {
|
|
|
7901
8058
|
});
|
|
7902
8059
|
}
|
|
7903
8060
|
}
|
|
7904
|
-
/** Drop cached state for a session, so a resume re-
|
|
8061
|
+
/** Drop cached state for a session, so a resume re-opens on its next turn. */
|
|
7905
8062
|
forget(sessionId) {
|
|
7906
|
-
this.
|
|
8063
|
+
this.openActivity.delete(sessionId);
|
|
7907
8064
|
}
|
|
7908
8065
|
};
|
|
7909
8066
|
|
|
@@ -8134,7 +8291,8 @@ var LiveActivityRenewalScheduler = class {
|
|
|
8134
8291
|
status,
|
|
8135
8292
|
startedAt,
|
|
8136
8293
|
lastOutput: session.lastOutput ?? "",
|
|
8137
|
-
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel }
|
|
8294
|
+
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel },
|
|
8295
|
+
...session.sessionName != null && { sessionName: session.sessionName }
|
|
8138
8296
|
};
|
|
8139
8297
|
try {
|
|
8140
8298
|
await this.deps.sender.send({
|
|
@@ -8194,7 +8352,8 @@ var LiveActivityRenewalScheduler = class {
|
|
|
8194
8352
|
// Carried through unchanged — the whole point of the renewal.
|
|
8195
8353
|
startedAt: args.startedAt,
|
|
8196
8354
|
lastOutput: truncateLastOutput(session.lastOutput ?? ""),
|
|
8197
|
-
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel }
|
|
8355
|
+
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel },
|
|
8356
|
+
...session.sessionName != null && { sessionName: session.sessionName }
|
|
8198
8357
|
},
|
|
8199
8358
|
now: args.now,
|
|
8200
8359
|
staleDate: args.startedAt + ACTIVITY_MAX_LIFETIME_MS
|
|
@@ -8581,6 +8740,50 @@ async function reconcileSessions(rows, probe, currentInstanceId) {
|
|
|
8581
8740
|
return Promise.all(rows.map((row) => classifySession(row, probe, currentInstanceId)));
|
|
8582
8741
|
}
|
|
8583
8742
|
|
|
8743
|
+
// src/services/sessions/rehydrateSessions.ts
|
|
8744
|
+
var REHYDRATE_MAX = 25;
|
|
8745
|
+
var REHYDRATE_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
8746
|
+
var AGENT_EXIT_SOURCES = /* @__PURE__ */ new Set(["exit", "process-exit"]);
|
|
8747
|
+
function shouldRehydrate(row, opts) {
|
|
8748
|
+
if (!opts.projectExists(row.project_path)) return false;
|
|
8749
|
+
if (opts.now - row.status_updated_at > REHYDRATE_WINDOW_MS) return false;
|
|
8750
|
+
if (AGENT_EXIT_SOURCES.has(row.status_source) && row.failure_reason == null) return false;
|
|
8751
|
+
return true;
|
|
8752
|
+
}
|
|
8753
|
+
function rowToStubSession(row) {
|
|
8754
|
+
return {
|
|
8755
|
+
id: row.session_id,
|
|
8756
|
+
provider: row.provider,
|
|
8757
|
+
projectPath: row.project_path,
|
|
8758
|
+
projectName: row.project_name,
|
|
8759
|
+
branch: row.branch,
|
|
8760
|
+
// No PTY exists for a stub, so this is the only truthful status.
|
|
8761
|
+
status: "idle",
|
|
8762
|
+
startedAt: new Date(row.started_at),
|
|
8763
|
+
completedAt: row.completed_at != null ? new Date(row.completed_at) : null,
|
|
8764
|
+
promptCount: row.prompt_count,
|
|
8765
|
+
lastOutput: "",
|
|
8766
|
+
rehydrated: true,
|
|
8767
|
+
...row.session_name != null && { sessionName: row.session_name },
|
|
8768
|
+
...row.project_id != null && { projectId: row.project_id },
|
|
8769
|
+
...row.bound_conversation_id != null && { boundConversationId: row.bound_conversation_id },
|
|
8770
|
+
...row.resumed_from_conversation_id != null && {
|
|
8771
|
+
resumedFromConversationId: row.resumed_from_conversation_id
|
|
8772
|
+
},
|
|
8773
|
+
...row.failure_reason != null && { failureReason: row.failure_reason },
|
|
8774
|
+
...row.last_activity_at != null && { lastActivityAt: new Date(row.last_activity_at) },
|
|
8775
|
+
// Only `shutdown` crosses over. It is the one registry source that is also a
|
|
8776
|
+
// wire StatusSource *and* that genuinely describes the `idle` above — the
|
|
8777
|
+
// streamer stopped this session. A crashed row still says `transition` over
|
|
8778
|
+
// a `running` status, and copying that here would attach observed-confidence
|
|
8779
|
+
// provenance to a status we derived at boot, so leave it unset instead.
|
|
8780
|
+
...row.status_source === "shutdown" && {
|
|
8781
|
+
statusSource: "shutdown",
|
|
8782
|
+
statusUpdatedAt: new Date(row.status_updated_at)
|
|
8783
|
+
}
|
|
8784
|
+
};
|
|
8785
|
+
}
|
|
8786
|
+
|
|
8584
8787
|
// src/types.ts
|
|
8585
8788
|
function confidenceForSource(source) {
|
|
8586
8789
|
return source === "timeout-fallback" || source === "quiet-fallback" ? "inferred" : "observed";
|
|
@@ -8728,14 +8931,15 @@ function managedToResponse(s, ptyAttached) {
|
|
|
8728
8931
|
// Lifecycle for a session this run knows about. `attached` while we hold
|
|
8729
8932
|
// its PTY; once the PTY is gone the session is terminal from this run's
|
|
8730
8933
|
// perspective — `failed` when it recorded a reason, else `completed`.
|
|
8731
|
-
//
|
|
8732
|
-
//
|
|
8733
|
-
//
|
|
8734
|
-
|
|
8735
|
-
|
|
8934
|
+
// A `rehydrated` stub is the exception: the boot rehydrator seeded it from
|
|
8935
|
+
// the durable registry, so it is a previous run's session with no process
|
|
8936
|
+
// behind it — `resumable`, and `historical` rather than `managed`
|
|
8937
|
+
// (docs/plans/live-sessions-persistence-plan.md §4, Phase 1).
|
|
8938
|
+
lifecycle: ptyAttached ? "attached" : s.rehydrated ? "resumable" : s.failureReason != null ? "failed" : "completed",
|
|
8939
|
+
lifecycleSource: ptyAttached ? "spawn" : s.rehydrated ? "reconcile" : "exit",
|
|
8736
8940
|
// We spawned it, so `status` is the authoritative signal — no inferred
|
|
8737
8941
|
// `activity` is attached for managed sessions.
|
|
8738
|
-
ownership: "managed",
|
|
8942
|
+
ownership: s.rehydrated ? "historical" : "managed",
|
|
8739
8943
|
projectPath: s.projectPath,
|
|
8740
8944
|
projectName: s.projectName,
|
|
8741
8945
|
branch: s.branch,
|
|
@@ -9325,10 +9529,13 @@ var StreamerServer = class {
|
|
|
9325
9529
|
projectsRepo = null;
|
|
9326
9530
|
conversationsRepo = null;
|
|
9327
9531
|
sessionsRepo = null;
|
|
9328
|
-
// Durable session registry (C1 Phase 2). Null when
|
|
9329
|
-
//
|
|
9330
|
-
// taking the server down with it, so every write goes through `?.`.
|
|
9532
|
+
// Durable session registry (C1 Phase 2). Null when runtime.db failed to open
|
|
9533
|
+
// — persistence degrades to today's in-memory-only behaviour rather than
|
|
9534
|
+
// taking the server down with it, so every write goes through `?.`. Note the
|
|
9535
|
+
// handle is runtime.db, NOT the conversation cache: a cache failure used to
|
|
9536
|
+
// null this repo and silently disable all session persistence.
|
|
9331
9537
|
managedSessionsRepo = null;
|
|
9538
|
+
runtimeStore = null;
|
|
9332
9539
|
// Identifies this streamer run. A registry row carrying a different id is a
|
|
9333
9540
|
// session that outlived the process that started it.
|
|
9334
9541
|
streamerInstanceId = randomUUID5();
|
|
@@ -9347,6 +9554,7 @@ var StreamerServer = class {
|
|
|
9347
9554
|
liveActivityRenewal = null;
|
|
9348
9555
|
discoveryCache = null;
|
|
9349
9556
|
cacheDir;
|
|
9557
|
+
runtimeDbPath;
|
|
9350
9558
|
tailSize;
|
|
9351
9559
|
directoryDebounceMs;
|
|
9352
9560
|
// Trailing-debounced trigger that flags the scanner stale after a quiet
|
|
@@ -9393,6 +9601,7 @@ var StreamerServer = class {
|
|
|
9393
9601
|
this.claudeFlags = config.claudeFlags ?? loadClaudeFlags();
|
|
9394
9602
|
this.claudeExtraArgs = config.claudeExtraArgs ?? loadClaudeExtraArgs();
|
|
9395
9603
|
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join18(homedir9(), ".threadbase", "cache");
|
|
9604
|
+
this.runtimeDbPath = config.runtimeDbPath ?? process.env.THREADBASE_RUNTIME_DB ?? join18(process.env.THREADBASE_CONFIG_DIR ?? join18(homedir9(), ".threadbase"), "runtime.db");
|
|
9396
9605
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
9397
9606
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
9398
9607
|
this.markScannerStaleDebounced = debounce(() => {
|
|
@@ -9563,6 +9772,7 @@ var StreamerServer = class {
|
|
|
9563
9772
|
if (resp) this.wsHub.broadcast({ type: "session_ready", session: resp });
|
|
9564
9773
|
},
|
|
9565
9774
|
onStatusChange: (session) => {
|
|
9775
|
+
const previousStatus = this.sessionStore.getManaged(session.id)?.status;
|
|
9566
9776
|
this.sessionStore.updateManaged(session.id, {
|
|
9567
9777
|
status: session.status,
|
|
9568
9778
|
completedAt: session.completedAt,
|
|
@@ -9576,7 +9786,11 @@ var StreamerServer = class {
|
|
|
9576
9786
|
completedAt: session.completedAt,
|
|
9577
9787
|
lastActivityAt: session.lastActivityAt ?? null,
|
|
9578
9788
|
promptCount: session.promptCount,
|
|
9579
|
-
failureReason: session.failureReason ?? null
|
|
9789
|
+
failureReason: session.failureReason ?? null,
|
|
9790
|
+
// Derived from the first user message, so it does not exist yet at
|
|
9791
|
+
// recordSpawn. The input that produces it also flips
|
|
9792
|
+
// waiting_input→running, which lands here.
|
|
9793
|
+
sessionName: session.sessionName ?? null
|
|
9580
9794
|
}
|
|
9581
9795
|
);
|
|
9582
9796
|
if (session.status === "waiting_input" || session.status === "idle") {
|
|
@@ -9617,7 +9831,7 @@ var StreamerServer = class {
|
|
|
9617
9831
|
if (resp) {
|
|
9618
9832
|
this.wsHub.broadcast({ type: "session_update", session: resp });
|
|
9619
9833
|
}
|
|
9620
|
-
void this.liveActivityNotifier?.onStatusChange(session);
|
|
9834
|
+
void this.liveActivityNotifier?.onStatusChange(session, previousStatus);
|
|
9621
9835
|
this.sessionStatusBus.emit(`status:${session.id}`, session.status);
|
|
9622
9836
|
}
|
|
9623
9837
|
});
|
|
@@ -9668,6 +9882,7 @@ var StreamerServer = class {
|
|
|
9668
9882
|
conversationsRepo: () => this.conversationsRepo,
|
|
9669
9883
|
sessionsRepo: () => this.sessionsRepo,
|
|
9670
9884
|
cacheMetadataRepo: () => this.cacheMetadataRepo,
|
|
9885
|
+
runtimeStore: () => this.runtimeStore,
|
|
9671
9886
|
ptyAttachedIds: () => this.ptyAttachedIds(),
|
|
9672
9887
|
handleListSessions: (url, res) => this.handleListSessions(url, res),
|
|
9673
9888
|
handleSessionsCount: (res) => this.handleSessionsCount(res),
|
|
@@ -9967,6 +10182,58 @@ var StreamerServer = class {
|
|
|
9967
10182
|
}
|
|
9968
10183
|
return verdicts;
|
|
9969
10184
|
}
|
|
10185
|
+
/**
|
|
10186
|
+
* Seed the session list with what previous runs left behind (persistence plan
|
|
10187
|
+
* Phase 1, gaps G1/G2/G8).
|
|
10188
|
+
*
|
|
10189
|
+
* Reconciliation classifies rows and stops there; a verdict is overlaid onto a
|
|
10190
|
+
* SessionResponse that already exists, and after a clean restart none does —
|
|
10191
|
+
* `SessionStore` starts empty. So the user's session did not become
|
|
10192
|
+
* `resumable`, it became *absent*. This is the half that puts it back.
|
|
10193
|
+
*
|
|
10194
|
+
* The seeded stubs hold no PTY and are never handed to `LiveSessionManager`,
|
|
10195
|
+
* so `reapIdleSessions` and `startGraceTimer` — both of which iterate
|
|
10196
|
+
* `ptyManager.listSessions()` — cannot observe them. A later resume calls
|
|
10197
|
+
* `sessionStore.addManaged` with the real session, which overwrites the stub
|
|
10198
|
+
* by id rather than duplicating it.
|
|
10199
|
+
*/
|
|
10200
|
+
rehydratePreviousSessions(verdicts) {
|
|
10201
|
+
if (!this.featureFlags.sessionRehydration || !this.managedSessionsRepo) return;
|
|
10202
|
+
try {
|
|
10203
|
+
const now = Date.now();
|
|
10204
|
+
const rows = this.managedSessionsRepo.listRecoverable({
|
|
10205
|
+
sinceMs: now - REHYDRATE_WINDOW_MS,
|
|
10206
|
+
limit: REHYDRATE_MAX + 1
|
|
10207
|
+
});
|
|
10208
|
+
const truncated = rows.length > REHYDRATE_MAX;
|
|
10209
|
+
const candidates = truncated ? rows.slice(0, REHYDRATE_MAX) : rows;
|
|
10210
|
+
if (candidates.length === 0) return;
|
|
10211
|
+
const lifecycleByVerdict = new Map(verdicts.map((v) => [v.sessionId, v.lifecycle]));
|
|
10212
|
+
let rehydrated = 0;
|
|
10213
|
+
for (const row of candidates) {
|
|
10214
|
+
if (this.sessionStore.getManaged(row.session_id)) continue;
|
|
10215
|
+
if (!shouldRehydrate(row, { now, projectExists: existsSync11 })) continue;
|
|
10216
|
+
this.sessionStore.addManaged(rowToStubSession(row));
|
|
10217
|
+
this.sessionLifecycles.set(
|
|
10218
|
+
row.session_id,
|
|
10219
|
+
lifecycleByVerdict.get(row.session_id) ?? "resumable"
|
|
10220
|
+
);
|
|
10221
|
+
if (row.completed_at != null) this.selfPtyEndedAt.set(row.session_id, row.completed_at);
|
|
10222
|
+
rehydrated++;
|
|
10223
|
+
}
|
|
10224
|
+
this.log.info(`[rehydrate] recovered ${rehydrated} session(s) from the registry`, {
|
|
10225
|
+
event: "sessions.rehydrated",
|
|
10226
|
+
rehydrated,
|
|
10227
|
+
skipped: candidates.length - rehydrated,
|
|
10228
|
+
truncated
|
|
10229
|
+
});
|
|
10230
|
+
} catch (err) {
|
|
10231
|
+
this.log.warn("[rehydrate] failed to rehydrate previous sessions", {
|
|
10232
|
+
event: "sessions.rehydrate_failed",
|
|
10233
|
+
err
|
|
10234
|
+
});
|
|
10235
|
+
}
|
|
10236
|
+
}
|
|
9970
10237
|
/**
|
|
9971
10238
|
* Pick a token guaranteed to appear in the spawned process's argv, for the
|
|
9972
10239
|
* reconciler's pid-reuse guard.
|
|
@@ -10200,6 +10467,17 @@ var StreamerServer = class {
|
|
|
10200
10467
|
port,
|
|
10201
10468
|
event: "server.listening"
|
|
10202
10469
|
});
|
|
10470
|
+
try {
|
|
10471
|
+
this.runtimeStore = RuntimeStore.open(this.runtimeDbPath);
|
|
10472
|
+
this.managedSessionsRepo = new ManagedSessionsRepository(this.runtimeStore.getDatabase());
|
|
10473
|
+
} catch (err) {
|
|
10474
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
10475
|
+
const abiMismatch = message.includes("NODE_MODULE_VERSION") || message.includes("was compiled against a different Node.js version");
|
|
10476
|
+
this.log.error(
|
|
10477
|
+
`Runtime store failed to open \u2014 session persistence DISABLED; sessions will not survive a restart.` + (abiMismatch ? ` Fix: npm rebuild better-sqlite3` : "") + ` (${message})`,
|
|
10478
|
+
{ error: message, abiMismatch, path: this.runtimeDbPath, event: "runtime.open_failed" }
|
|
10479
|
+
);
|
|
10480
|
+
}
|
|
10203
10481
|
try {
|
|
10204
10482
|
this.cache = ConversationCache.open(
|
|
10205
10483
|
join18(this.cacheDir, "cache.db"),
|
|
@@ -10227,8 +10505,20 @@ var StreamerServer = class {
|
|
|
10227
10505
|
this.projectsRepo = new ProjectsRepository(db);
|
|
10228
10506
|
this.conversationsRepo = new ConversationsRepository(this.cache);
|
|
10229
10507
|
this.sessionsRepo = new SessionsRepository(this.sessionStore);
|
|
10230
|
-
|
|
10231
|
-
|
|
10508
|
+
try {
|
|
10509
|
+
const copied = this.runtimeStore?.importLegacyManagedSessions(db) ?? 0;
|
|
10510
|
+
if (copied > 0) {
|
|
10511
|
+
this.log.info(`Copied ${copied} managed session row(s) from cache.db to runtime.db`, {
|
|
10512
|
+
copied,
|
|
10513
|
+
event: "runtime.legacy_import"
|
|
10514
|
+
});
|
|
10515
|
+
}
|
|
10516
|
+
} catch (err) {
|
|
10517
|
+
this.log.warn("[registry] legacy managed_sessions copy failed", {
|
|
10518
|
+
event: "runtime.legacy_import_failed",
|
|
10519
|
+
err
|
|
10520
|
+
});
|
|
10521
|
+
}
|
|
10232
10522
|
this.cacheMetadataRepo = new CacheMetadataRepository(db);
|
|
10233
10523
|
this.pushRepo = new PushRepository(db);
|
|
10234
10524
|
this.devicesRepo = new DevicesRepository(db);
|
|
@@ -10264,6 +10554,7 @@ var StreamerServer = class {
|
|
|
10264
10554
|
);
|
|
10265
10555
|
this.scannerPersistenceDisabled = true;
|
|
10266
10556
|
}
|
|
10557
|
+
void this.reconcilePreviousSessions().then((v) => this.rehydratePreviousSessions(v));
|
|
10267
10558
|
if (this.skipStartupWarmup) {
|
|
10268
10559
|
this.log.debug?.("startup warm-up scan skipped (skipStartupWarmup)", {
|
|
10269
10560
|
event: "cache.warmup_skipped"
|
|
@@ -10471,6 +10762,7 @@ var StreamerServer = class {
|
|
|
10471
10762
|
this.allScanners.clear();
|
|
10472
10763
|
this.scanner = null;
|
|
10473
10764
|
this.cache?.close();
|
|
10765
|
+
this.runtimeStore?.close();
|
|
10474
10766
|
this.ptyManager.dispose();
|
|
10475
10767
|
this.fileWatcher.dispose();
|
|
10476
10768
|
this.externalTails.clear();
|
|
@@ -10918,7 +11210,8 @@ var StreamerServer = class {
|
|
|
10918
11210
|
}
|
|
10919
11211
|
handleSessionsCount(res) {
|
|
10920
11212
|
if (this.rejectIfWarmingUp(res)) return;
|
|
10921
|
-
|
|
11213
|
+
const total = this.sessionStore.list(this.ptyAttachedIds()).filter((s) => s.ownership !== "historical").length;
|
|
11214
|
+
json(res, 200, { total });
|
|
10922
11215
|
}
|
|
10923
11216
|
handleGetRecentSessions(url, res) {
|
|
10924
11217
|
if (this.rejectIfWarmingUp(res)) return;
|