@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/cli.cjs
CHANGED
|
@@ -6213,6 +6213,12 @@ var init_feature_flags = __esm({
|
|
|
6213
6213
|
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.",
|
|
6214
6214
|
default: false,
|
|
6215
6215
|
env: "THREADBASE_FEATURE_CODEX_SYSTEM_PROMPT"
|
|
6216
|
+
},
|
|
6217
|
+
{
|
|
6218
|
+
id: "sessionRehydration",
|
|
6219
|
+
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.",
|
|
6220
|
+
default: true,
|
|
6221
|
+
env: "THREADBASE_FEATURE_SESSION_REHYDRATION"
|
|
6216
6222
|
}
|
|
6217
6223
|
];
|
|
6218
6224
|
}
|
|
@@ -124077,8 +124083,8 @@ function isAbiMismatch(message) {
|
|
|
124077
124083
|
}
|
|
124078
124084
|
function checkSqliteAbi() {
|
|
124079
124085
|
try {
|
|
124080
|
-
const
|
|
124081
|
-
const db = new
|
|
124086
|
+
const Database4 = require("better-sqlite3");
|
|
124087
|
+
const db = new Database4(":memory:");
|
|
124082
124088
|
db.close();
|
|
124083
124089
|
} catch (err) {
|
|
124084
124090
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -139971,6 +139977,9 @@ function getMigrationsDir() {
|
|
|
139971
139977
|
}
|
|
139972
139978
|
return __dirname;
|
|
139973
139979
|
}
|
|
139980
|
+
function resolveMigrationsDir(name = "migrations") {
|
|
139981
|
+
return (0, import_path15.join)(getMigrationsDir(), name);
|
|
139982
|
+
}
|
|
139974
139983
|
var SCHEMA_MIGRATIONS_SQL = `
|
|
139975
139984
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
139976
139985
|
id TEXT PRIMARY KEY,
|
|
@@ -139979,7 +139988,7 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
|
139979
139988
|
`;
|
|
139980
139989
|
function runSqliteMigrations(db, migrationsDir) {
|
|
139981
139990
|
db.exec(SCHEMA_MIGRATIONS_SQL);
|
|
139982
|
-
const dir = migrationsDir ?? (
|
|
139991
|
+
const dir = migrationsDir ?? resolveMigrationsDir();
|
|
139983
139992
|
const files = (0, import_fs15.readdirSync)(dir).filter((f2) => f2.endsWith(".sql")).sort();
|
|
139984
139993
|
const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
|
|
139985
139994
|
const appliedSet = new Set(appliedRows.map((r) => r.id));
|
|
@@ -141480,6 +141489,7 @@ var ManagedSessionsRepository = class {
|
|
|
141480
141489
|
updateStatusStmt;
|
|
141481
141490
|
getStmt;
|
|
141482
141491
|
listNonTerminalStmt;
|
|
141492
|
+
listRecoverableStmt;
|
|
141483
141493
|
deleteStmt;
|
|
141484
141494
|
constructor(db) {
|
|
141485
141495
|
this.upsertStmt = db.prepare(`
|
|
@@ -141523,7 +141533,8 @@ var ManagedSessionsRepository = class {
|
|
|
141523
141533
|
completed_at = @completed_at,
|
|
141524
141534
|
last_activity_at = @last_activity_at,
|
|
141525
141535
|
prompt_count = @prompt_count,
|
|
141526
|
-
failure_reason = COALESCE(@failure_reason, failure_reason)
|
|
141536
|
+
failure_reason = COALESCE(@failure_reason, failure_reason),
|
|
141537
|
+
session_name = COALESCE(@session_name, session_name)
|
|
141527
141538
|
WHERE session_id = @session_id
|
|
141528
141539
|
`);
|
|
141529
141540
|
this.getStmt = db.prepare("SELECT * FROM managed_sessions WHERE session_id = ?");
|
|
@@ -141532,6 +141543,13 @@ var ManagedSessionsRepository = class {
|
|
|
141532
141543
|
WHERE completed_at IS NULL
|
|
141533
141544
|
ORDER BY started_at ASC
|
|
141534
141545
|
`);
|
|
141546
|
+
this.listRecoverableStmt = db.prepare(`
|
|
141547
|
+
SELECT * FROM managed_sessions
|
|
141548
|
+
WHERE (completed_at IS NULL OR status_source = 'shutdown')
|
|
141549
|
+
AND status_updated_at >= @since
|
|
141550
|
+
ORDER BY status_updated_at DESC
|
|
141551
|
+
LIMIT @limit
|
|
141552
|
+
`);
|
|
141535
141553
|
this.deleteStmt = db.prepare("DELETE FROM managed_sessions WHERE session_id = ?");
|
|
141536
141554
|
}
|
|
141537
141555
|
/** Record a session at spawn, or refresh every field of an existing row. */
|
|
@@ -141573,7 +141591,8 @@ var ManagedSessionsRepository = class {
|
|
|
141573
141591
|
completed_at: fields.completedAt?.getTime() ?? null,
|
|
141574
141592
|
last_activity_at: fields.lastActivityAt?.getTime() ?? null,
|
|
141575
141593
|
prompt_count: fields.promptCount ?? 0,
|
|
141576
|
-
failure_reason: fields.failureReason ?? null
|
|
141594
|
+
failure_reason: fields.failureReason ?? null,
|
|
141595
|
+
session_name: fields.sessionName ?? null
|
|
141577
141596
|
});
|
|
141578
141597
|
}
|
|
141579
141598
|
get(sessionId) {
|
|
@@ -141583,6 +141602,14 @@ var ManagedSessionsRepository = class {
|
|
|
141583
141602
|
listNonTerminal() {
|
|
141584
141603
|
return this.listNonTerminalStmt.all();
|
|
141585
141604
|
}
|
|
141605
|
+
/**
|
|
141606
|
+
* Rows a restart could bring back: still open, or closed by our own shutdown,
|
|
141607
|
+
* and touched no longer ago than `sinceMs`. Newest first, capped — the caller
|
|
141608
|
+
* decides which of these actually deserve rehydrating (`shouldRehydrate`).
|
|
141609
|
+
*/
|
|
141610
|
+
listRecoverable({ sinceMs, limit }) {
|
|
141611
|
+
return this.listRecoverableStmt.all({ since: sinceMs, limit });
|
|
141612
|
+
}
|
|
141586
141613
|
delete(sessionId) {
|
|
141587
141614
|
this.deleteStmt.run(sessionId);
|
|
141588
141615
|
}
|
|
@@ -141718,6 +141745,54 @@ var SessionsRepository = class {
|
|
|
141718
141745
|
}
|
|
141719
141746
|
};
|
|
141720
141747
|
|
|
141748
|
+
// src/db/runtime-store.ts
|
|
141749
|
+
var import_better_sqlite33 = __toESM(require("better-sqlite3"), 1);
|
|
141750
|
+
var RuntimeStore = class _RuntimeStore {
|
|
141751
|
+
constructor(db) {
|
|
141752
|
+
this.db = db;
|
|
141753
|
+
}
|
|
141754
|
+
db;
|
|
141755
|
+
static open(dbPath, migrationsDir) {
|
|
141756
|
+
const db = new import_better_sqlite33.default(dbPath);
|
|
141757
|
+
db.pragma("journal_mode = WAL");
|
|
141758
|
+
runSqliteMigrations(db, migrationsDir ?? resolveMigrationsDir("runtime-migrations"));
|
|
141759
|
+
return new _RuntimeStore(db);
|
|
141760
|
+
}
|
|
141761
|
+
getDatabase() {
|
|
141762
|
+
return this.db;
|
|
141763
|
+
}
|
|
141764
|
+
/**
|
|
141765
|
+
* One-time move of `managed_sessions` rows out of a pre-split `cache.db`.
|
|
141766
|
+
*
|
|
141767
|
+
* Non-destructive by design: the source table is left in place so an older
|
|
141768
|
+
* streamer rolled back onto the same machine still finds its registry. Runs
|
|
141769
|
+
* only when this file's table is empty, so a second boot is a no-op rather
|
|
141770
|
+
* than a re-copy that would resurrect rows deleted since.
|
|
141771
|
+
*
|
|
141772
|
+
* Returns the number of rows copied.
|
|
141773
|
+
*/
|
|
141774
|
+
importLegacyManagedSessions(source) {
|
|
141775
|
+
const existing = this.db.prepare("SELECT COUNT(*) AS n FROM managed_sessions").get();
|
|
141776
|
+
if (existing.n > 0) return 0;
|
|
141777
|
+
const hasTable = source.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'managed_sessions'").get();
|
|
141778
|
+
if (!hasTable) return 0;
|
|
141779
|
+
const rows = source.prepare("SELECT * FROM managed_sessions").all();
|
|
141780
|
+
if (rows.length === 0) return 0;
|
|
141781
|
+
const columns = Object.keys(rows[0]);
|
|
141782
|
+
const insert = this.db.prepare(
|
|
141783
|
+
`INSERT OR IGNORE INTO managed_sessions (${columns.join(", ")})
|
|
141784
|
+
VALUES (${columns.map((c) => `@${c}`).join(", ")})`
|
|
141785
|
+
);
|
|
141786
|
+
this.db.transaction((batch) => {
|
|
141787
|
+
for (const row of batch) insert.run(row);
|
|
141788
|
+
})(rows);
|
|
141789
|
+
return rows.length;
|
|
141790
|
+
}
|
|
141791
|
+
close() {
|
|
141792
|
+
this.db.close();
|
|
141793
|
+
}
|
|
141794
|
+
};
|
|
141795
|
+
|
|
141721
141796
|
// src/db/upload-records.ts
|
|
141722
141797
|
async function recordUpload(pool2, instanceId, row) {
|
|
141723
141798
|
if (!pool2) return;
|
|
@@ -142739,6 +142814,12 @@ function detectShellPrompt(lines) {
|
|
|
142739
142814
|
return null;
|
|
142740
142815
|
}
|
|
142741
142816
|
|
|
142817
|
+
// src/utils/deriveSessionName.ts
|
|
142818
|
+
function deriveSessionName(firstMessageText) {
|
|
142819
|
+
const firstLine = firstMessageText.split("\n", 1)[0]?.trim() ?? "";
|
|
142820
|
+
return firstLine.slice(0, 80);
|
|
142821
|
+
}
|
|
142822
|
+
|
|
142742
142823
|
// src/pty-manager.ts
|
|
142743
142824
|
var OUTPUT_BUFFER_MAX2 = 65536;
|
|
142744
142825
|
var INPUT_HISTORY_MAX2 = 50;
|
|
@@ -142746,8 +142827,8 @@ var PTY_COLS2 = 120;
|
|
|
142746
142827
|
var PTY_ROWS2 = 40;
|
|
142747
142828
|
var SCREEN_SCROLLBACK2 = 1e3;
|
|
142748
142829
|
var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
|
|
142749
|
-
var PROMPT_MARKER_FALLBACK_MS = 1e4;
|
|
142750
142830
|
var QUIET_DETECT_MS2 = 500;
|
|
142831
|
+
var CLAUDE_READY_FALLBACK_MS = 8e3;
|
|
142751
142832
|
function buildPasteBytes(input) {
|
|
142752
142833
|
return `\x1B[200~${input}\x1B[201~`;
|
|
142753
142834
|
}
|
|
@@ -142823,8 +142904,8 @@ var PTYManager = class {
|
|
|
142823
142904
|
// silently lost — the "dot bug".
|
|
142824
142905
|
queuedInputs = /* @__PURE__ */ new Map();
|
|
142825
142906
|
log;
|
|
142826
|
-
// Timestamp of first PTY chunk per session;
|
|
142827
|
-
//
|
|
142907
|
+
// Timestamp of first PTY chunk per session; used for the [pty.ready] elapsed
|
|
142908
|
+
// measurement so a slow boot is visible in the logs.
|
|
142828
142909
|
firstChunkAt = /* @__PURE__ */ new Map();
|
|
142829
142910
|
// Per-session chunk counter and last-chunk timestamp. Diagnostic-only,
|
|
142830
142911
|
// feeds the [pty.chunk] log lines so we can trace whether Claude responded
|
|
@@ -142835,6 +142916,8 @@ var PTYManager = class {
|
|
|
142835
142916
|
// QUIET_DETECT_MS after the last chunk so ready/prompt detection doesn't
|
|
142836
142917
|
// wait for another chunk that may never arrive (Claude blocked on input).
|
|
142837
142918
|
quietCheckers = /* @__PURE__ */ new Map();
|
|
142919
|
+
// Per-session flat backstop from the first chunk (CLAUDE_READY_FALLBACK_MS).
|
|
142920
|
+
readyFallbackTimers = /* @__PURE__ */ new Map();
|
|
142838
142921
|
// In-flight start()/startFresh() calls keyed by sessionId. A second
|
|
142839
142922
|
// concurrent resume for the same session (double-tap, client retry) awaits
|
|
142840
142923
|
// the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
|
|
@@ -142920,6 +143003,7 @@ var PTYManager = class {
|
|
|
142920
143003
|
};
|
|
142921
143004
|
this.sessions.set(sessionId, session);
|
|
142922
143005
|
this.pendingReady.add(sessionId);
|
|
143006
|
+
this.armReadyFallback(sessionId);
|
|
142923
143007
|
proc.onData((data) => {
|
|
142924
143008
|
this.handleOutput(sessionId, data);
|
|
142925
143009
|
});
|
|
@@ -142980,6 +143064,7 @@ var PTYManager = class {
|
|
|
142980
143064
|
};
|
|
142981
143065
|
this.sessions.set(sessionId, session);
|
|
142982
143066
|
this.pendingReady.add(sessionId);
|
|
143067
|
+
this.armReadyFallback(sessionId);
|
|
142983
143068
|
proc.onData((data) => {
|
|
142984
143069
|
this.handleOutput(sessionId, data);
|
|
142985
143070
|
});
|
|
@@ -143154,6 +143239,7 @@ var PTYManager = class {
|
|
|
143154
143239
|
this.shellPromptOpen.delete(sessionId);
|
|
143155
143240
|
this.quietCheckers.get(sessionId)?.cancel();
|
|
143156
143241
|
this.quietCheckers.delete(sessionId);
|
|
143242
|
+
this.clearReadyFallback(sessionId);
|
|
143157
143243
|
try {
|
|
143158
143244
|
session.process.kill("SIGINT");
|
|
143159
143245
|
} catch {
|
|
@@ -143212,6 +143298,10 @@ var PTYManager = class {
|
|
|
143212
143298
|
if (session.inputHistory.length > INPUT_HISTORY_MAX2) {
|
|
143213
143299
|
session.inputHistory.shift();
|
|
143214
143300
|
}
|
|
143301
|
+
if (session.firstMessageText === void 0) {
|
|
143302
|
+
session.firstMessageText = text;
|
|
143303
|
+
session.sessionName = deriveSessionName(text);
|
|
143304
|
+
}
|
|
143215
143305
|
this.onUserMessage?.(session.id, text, ts2);
|
|
143216
143306
|
}
|
|
143217
143307
|
getSession(sessionId) {
|
|
@@ -143238,6 +143328,8 @@ var PTYManager = class {
|
|
|
143238
143328
|
this.lastChunkAt.clear();
|
|
143239
143329
|
for (const quiet of this.quietCheckers.values()) quiet.cancel();
|
|
143240
143330
|
this.quietCheckers.clear();
|
|
143331
|
+
for (const timer of this.readyFallbackTimers.values()) clearTimeout(timer);
|
|
143332
|
+
this.readyFallbackTimers.clear();
|
|
143241
143333
|
this.permissionOpen.clear();
|
|
143242
143334
|
this.lastScreenQuestionKey.clear();
|
|
143243
143335
|
this.shellPromptOpen.clear();
|
|
@@ -143280,8 +143372,6 @@ var PTYManager = class {
|
|
|
143280
143372
|
const matchedMarker = CLAUDE_PROMPT_MARKERS.find((m2) => stripped.includes(m2));
|
|
143281
143373
|
if (session.status === "running" && matchedMarker) {
|
|
143282
143374
|
this.markReady(sessionId, session, "prompt-marker", `marker:${matchedMarker}`);
|
|
143283
|
-
} else if (session.status === "running" && this.pendingReady.has(sessionId) && now - (this.firstChunkAt.get(sessionId) ?? now) >= PROMPT_MARKER_FALLBACK_MS) {
|
|
143284
|
-
this.markReady(sessionId, session, "timeout-fallback", "fallback:timeout");
|
|
143285
143375
|
}
|
|
143286
143376
|
this.onOutput?.(sessionId, data);
|
|
143287
143377
|
this.detectLivePrompts(sessionId, data, stripped).catch((err) => {
|
|
@@ -143381,7 +143471,13 @@ var PTYManager = class {
|
|
|
143381
143471
|
const session = this.sessions.get(sessionId);
|
|
143382
143472
|
if (session?.status !== "running") return;
|
|
143383
143473
|
if (this.pendingReady.has(sessionId)) {
|
|
143384
|
-
this.
|
|
143474
|
+
this.recheckReadyFromScreen(sessionId).catch((err) => {
|
|
143475
|
+
this.log.warn("[pty.ready] boot screen recheck failed", {
|
|
143476
|
+
event: "pty.ready_recheck_failed",
|
|
143477
|
+
sessionId,
|
|
143478
|
+
err
|
|
143479
|
+
});
|
|
143480
|
+
});
|
|
143385
143481
|
} else {
|
|
143386
143482
|
this.recheckReadyFromScreen(sessionId).catch((err) => {
|
|
143387
143483
|
this.log.warn("[pty.ready] screen recheck failed", {
|
|
@@ -143413,9 +143509,32 @@ var PTYManager = class {
|
|
|
143413
143509
|
this.markReady(sessionId, session, "screen-marker", `quiet:screen-marker:${matchedMarker}`);
|
|
143414
143510
|
}
|
|
143415
143511
|
}
|
|
143512
|
+
// Flat backstop from the first chunk: if neither a prompt marker nor the
|
|
143513
|
+
// screen recheck settles the session within CLAUDE_READY_FALLBACK_MS, mark it
|
|
143514
|
+
// ready anyway so start requests resolve and queued input is not held
|
|
143515
|
+
// forever. This is what makes the quiet-checker safe to be strict — a boot
|
|
143516
|
+
// variant whose marker we cannot see still recovers, just 8s later instead of
|
|
143517
|
+
// 500ms sooner and wrong. unref() so it never holds the process open.
|
|
143518
|
+
armReadyFallback(sessionId) {
|
|
143519
|
+
const timer = setTimeout(() => {
|
|
143520
|
+
this.readyFallbackTimers.delete(sessionId);
|
|
143521
|
+
const session = this.sessions.get(sessionId);
|
|
143522
|
+
if (session?.status === "running" && this.pendingReady.has(sessionId)) {
|
|
143523
|
+
this.markReady(sessionId, session, "timeout-fallback", "fallback:timeout");
|
|
143524
|
+
}
|
|
143525
|
+
}, CLAUDE_READY_FALLBACK_MS);
|
|
143526
|
+
timer.unref?.();
|
|
143527
|
+
this.readyFallbackTimers.set(sessionId, timer);
|
|
143528
|
+
}
|
|
143529
|
+
clearReadyFallback(sessionId) {
|
|
143530
|
+
const timer = this.readyFallbackTimers.get(sessionId);
|
|
143531
|
+
if (timer) clearTimeout(timer);
|
|
143532
|
+
this.readyFallbackTimers.delete(sessionId);
|
|
143533
|
+
}
|
|
143416
143534
|
// Transition a session from "running" to "waiting_input", clear pendingReady,
|
|
143417
143535
|
// and flush any queued input. Idempotent: callers can invoke at any chunk.
|
|
143418
143536
|
markReady(sessionId, session, source, reason) {
|
|
143537
|
+
this.clearReadyFallback(sessionId);
|
|
143419
143538
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
143420
143539
|
session.status = "waiting_input";
|
|
143421
143540
|
session.statusSource = source;
|
|
@@ -143459,6 +143578,7 @@ var PTYManager = class {
|
|
|
143459
143578
|
this.shellPromptOpen.delete(sessionId);
|
|
143460
143579
|
this.quietCheckers.get(sessionId)?.cancel();
|
|
143461
143580
|
this.quietCheckers.delete(sessionId);
|
|
143581
|
+
this.clearReadyFallback(sessionId);
|
|
143462
143582
|
}
|
|
143463
143583
|
};
|
|
143464
143584
|
function toPublicSession2(s3) {
|
|
@@ -143477,7 +143597,9 @@ function toPublicSession2(s3) {
|
|
|
143477
143597
|
...s3.lastActivityAt != null && { lastActivityAt: s3.lastActivityAt },
|
|
143478
143598
|
...s3.statusSource != null && { statusSource: s3.statusSource },
|
|
143479
143599
|
...s3.statusUpdatedAt != null && { statusUpdatedAt: s3.statusUpdatedAt },
|
|
143480
|
-
...s3.filePath != null && { filePath: s3.filePath }
|
|
143600
|
+
...s3.filePath != null && { filePath: s3.filePath },
|
|
143601
|
+
...s3.sessionName != null && { sessionName: s3.sessionName },
|
|
143602
|
+
...s3.firstMessageText != null && { firstMessageText: s3.firstMessageText }
|
|
143481
143603
|
};
|
|
143482
143604
|
}
|
|
143483
143605
|
function stripAnsi2(str) {
|
|
@@ -145035,7 +145157,8 @@ function contentStateForSession(args) {
|
|
|
145035
145157
|
status,
|
|
145036
145158
|
startedAt: args.startedAtOverride ?? args.session.startedAt.getTime(),
|
|
145037
145159
|
lastOutput: truncateLastOutput(args.session.lastOutput ?? ""),
|
|
145038
|
-
...args.serverLabel != null && { serverLabel: args.serverLabel }
|
|
145160
|
+
...args.serverLabel != null && { serverLabel: args.serverLabel },
|
|
145161
|
+
...args.session.sessionName != null && { sessionName: args.session.sessionName }
|
|
145039
145162
|
};
|
|
145040
145163
|
}
|
|
145041
145164
|
var LiveActivityNotifier = class {
|
|
@@ -145048,14 +145171,17 @@ var LiveActivityNotifier = class {
|
|
|
145048
145171
|
serverId;
|
|
145049
145172
|
serverLabel;
|
|
145050
145173
|
/**
|
|
145051
|
-
*
|
|
145174
|
+
* Sessions with a currently open (pushed) activity.
|
|
145052
145175
|
*
|
|
145053
|
-
*
|
|
145054
|
-
*
|
|
145055
|
-
*
|
|
145056
|
-
*
|
|
145176
|
+
* An activity opens on a `waiting_input → running` edge (the user sent a
|
|
145177
|
+
* prompt) and closes on the matching `running → waiting_input` edge (the
|
|
145178
|
+
* response, including any sub-agents, finished) — so this set is what makes
|
|
145179
|
+
* the notifier per-turn rather than per-session. A session's very first
|
|
145180
|
+
* `running` (right after spawn, before any user prompt) has no prior
|
|
145181
|
+
* `waiting_input` and therefore no edge, so it never opens an activity —
|
|
145182
|
+
* this is what keeps a fresh/idle session from pushing anything.
|
|
145057
145183
|
*/
|
|
145058
|
-
|
|
145184
|
+
openActivity = /* @__PURE__ */ new Map();
|
|
145059
145185
|
/**
|
|
145060
145186
|
* React to a session status change.
|
|
145061
145187
|
*
|
|
@@ -145063,34 +145189,22 @@ var LiveActivityNotifier = class {
|
|
|
145063
145189
|
* transition, so this returns a promise the caller may ignore and every error
|
|
145064
145190
|
* is logged rather than propagated.
|
|
145065
145191
|
*/
|
|
145066
|
-
async onStatusChange(session) {
|
|
145192
|
+
async onStatusChange(session, previousStatus) {
|
|
145067
145193
|
const status = toLiveActivityStatus(session.status);
|
|
145068
145194
|
try {
|
|
145069
145195
|
if (!status) {
|
|
145070
|
-
await this.endFor(session);
|
|
145196
|
+
if (this.openActivity.has(session.id)) await this.endFor(session);
|
|
145071
145197
|
return;
|
|
145072
145198
|
}
|
|
145073
|
-
if (
|
|
145074
|
-
|
|
145075
|
-
|
|
145076
|
-
serverId: this.serverId,
|
|
145077
|
-
serverLabel: this.serverLabel
|
|
145078
|
-
});
|
|
145079
|
-
if (!contentState) return;
|
|
145080
|
-
const outcome = await this.sender.send({
|
|
145081
|
-
sessionId: session.id,
|
|
145082
|
-
event: "update",
|
|
145083
|
-
contentState
|
|
145084
|
-
});
|
|
145085
|
-
this.lastPushed.set(session.id, status);
|
|
145086
|
-
if (outcome.attempted > 0) {
|
|
145087
|
-
log4.info("live_activity.updated", {
|
|
145088
|
-
event: "live_activity.updated",
|
|
145089
|
-
sessionId: session.id,
|
|
145090
|
-
status,
|
|
145091
|
-
...outcome
|
|
145092
|
-
});
|
|
145199
|
+
if (status === "running" && previousStatus === "waiting_input") {
|
|
145200
|
+
await this.startTurn(session);
|
|
145201
|
+
return;
|
|
145093
145202
|
}
|
|
145203
|
+
if (status === "waiting_input" && previousStatus === "running") {
|
|
145204
|
+
if (this.openActivity.has(session.id)) await this.endFor(session);
|
|
145205
|
+
return;
|
|
145206
|
+
}
|
|
145207
|
+
await this.maybeSendName(session);
|
|
145094
145208
|
} catch (err) {
|
|
145095
145209
|
log4.error("live_activity.notify_failed", {
|
|
145096
145210
|
event: "live_activity.notify_failed",
|
|
@@ -145100,14 +145214,57 @@ var LiveActivityNotifier = class {
|
|
|
145100
145214
|
});
|
|
145101
145215
|
}
|
|
145102
145216
|
}
|
|
145217
|
+
async startTurn(session) {
|
|
145218
|
+
const contentState = contentStateForSession({
|
|
145219
|
+
session,
|
|
145220
|
+
serverId: this.serverId,
|
|
145221
|
+
serverLabel: this.serverLabel
|
|
145222
|
+
});
|
|
145223
|
+
if (!contentState) return;
|
|
145224
|
+
const outcome = await this.sender.send({
|
|
145225
|
+
sessionId: session.id,
|
|
145226
|
+
event: "update",
|
|
145227
|
+
contentState
|
|
145228
|
+
});
|
|
145229
|
+
this.openActivity.set(session.id, { sessionNameSent: session.sessionName != null });
|
|
145230
|
+
if (outcome.attempted > 0) {
|
|
145231
|
+
log4.info("live_activity.updated", {
|
|
145232
|
+
event: "live_activity.updated",
|
|
145233
|
+
sessionId: session.id,
|
|
145234
|
+
status: contentState.status,
|
|
145235
|
+
...outcome
|
|
145236
|
+
});
|
|
145237
|
+
}
|
|
145238
|
+
}
|
|
145239
|
+
async maybeSendName(session) {
|
|
145240
|
+
const open3 = this.openActivity.get(session.id);
|
|
145241
|
+
if (!open3 || open3.sessionNameSent || session.sessionName == null) return;
|
|
145242
|
+
const contentState = contentStateForSession({
|
|
145243
|
+
session,
|
|
145244
|
+
serverId: this.serverId,
|
|
145245
|
+
serverLabel: this.serverLabel
|
|
145246
|
+
});
|
|
145247
|
+
if (!contentState) return;
|
|
145248
|
+
const outcome = await this.sender.send({
|
|
145249
|
+
sessionId: session.id,
|
|
145250
|
+
event: "update",
|
|
145251
|
+
contentState
|
|
145252
|
+
});
|
|
145253
|
+
open3.sessionNameSent = true;
|
|
145254
|
+
if (outcome.attempted > 0) {
|
|
145255
|
+
log4.info("live_activity.updated", {
|
|
145256
|
+
event: "live_activity.updated",
|
|
145257
|
+
sessionId: session.id,
|
|
145258
|
+
status: contentState.status,
|
|
145259
|
+
...outcome
|
|
145260
|
+
});
|
|
145261
|
+
}
|
|
145262
|
+
}
|
|
145103
145263
|
async endFor(session) {
|
|
145104
|
-
|
|
145105
|
-
|
|
145264
|
+
this.openActivity.delete(session.id);
|
|
145265
|
+
const status = toLiveActivityStatus(session.status);
|
|
145106
145266
|
const contentState = contentStateForSession({
|
|
145107
|
-
session: {
|
|
145108
|
-
...session,
|
|
145109
|
-
status: lastStatus === "waiting_input" ? "waiting_input" : "running"
|
|
145110
|
-
},
|
|
145267
|
+
session: { ...session, status: status ?? "waiting_input" },
|
|
145111
145268
|
serverId: this.serverId,
|
|
145112
145269
|
serverLabel: this.serverLabel
|
|
145113
145270
|
});
|
|
@@ -145121,9 +145278,9 @@ var LiveActivityNotifier = class {
|
|
|
145121
145278
|
});
|
|
145122
145279
|
}
|
|
145123
145280
|
}
|
|
145124
|
-
/** Drop cached state for a session, so a resume re-
|
|
145281
|
+
/** Drop cached state for a session, so a resume re-opens on its next turn. */
|
|
145125
145282
|
forget(sessionId) {
|
|
145126
|
-
this.
|
|
145283
|
+
this.openActivity.delete(sessionId);
|
|
145127
145284
|
}
|
|
145128
145285
|
};
|
|
145129
145286
|
|
|
@@ -145358,7 +145515,8 @@ var LiveActivityRenewalScheduler = class {
|
|
|
145358
145515
|
status,
|
|
145359
145516
|
startedAt,
|
|
145360
145517
|
lastOutput: session.lastOutput ?? "",
|
|
145361
|
-
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel }
|
|
145518
|
+
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel },
|
|
145519
|
+
...session.sessionName != null && { sessionName: session.sessionName }
|
|
145362
145520
|
};
|
|
145363
145521
|
try {
|
|
145364
145522
|
await this.deps.sender.send({
|
|
@@ -145418,7 +145576,8 @@ var LiveActivityRenewalScheduler = class {
|
|
|
145418
145576
|
// Carried through unchanged — the whole point of the renewal.
|
|
145419
145577
|
startedAt: args.startedAt,
|
|
145420
145578
|
lastOutput: truncateLastOutput(session.lastOutput ?? ""),
|
|
145421
|
-
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel }
|
|
145579
|
+
...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel },
|
|
145580
|
+
...session.sessionName != null && { sessionName: session.sessionName }
|
|
145422
145581
|
},
|
|
145423
145582
|
now: args.now,
|
|
145424
145583
|
staleDate: args.startedAt + ACTIVITY_MAX_LIFETIME_MS
|
|
@@ -145805,6 +145964,50 @@ async function reconcileSessions(rows, probe, currentInstanceId) {
|
|
|
145805
145964
|
return Promise.all(rows.map((row) => classifySession(row, probe, currentInstanceId)));
|
|
145806
145965
|
}
|
|
145807
145966
|
|
|
145967
|
+
// src/services/sessions/rehydrateSessions.ts
|
|
145968
|
+
var REHYDRATE_MAX = 25;
|
|
145969
|
+
var REHYDRATE_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
145970
|
+
var AGENT_EXIT_SOURCES = /* @__PURE__ */ new Set(["exit", "process-exit"]);
|
|
145971
|
+
function shouldRehydrate(row, opts) {
|
|
145972
|
+
if (!opts.projectExists(row.project_path)) return false;
|
|
145973
|
+
if (opts.now - row.status_updated_at > REHYDRATE_WINDOW_MS) return false;
|
|
145974
|
+
if (AGENT_EXIT_SOURCES.has(row.status_source) && row.failure_reason == null) return false;
|
|
145975
|
+
return true;
|
|
145976
|
+
}
|
|
145977
|
+
function rowToStubSession(row) {
|
|
145978
|
+
return {
|
|
145979
|
+
id: row.session_id,
|
|
145980
|
+
provider: row.provider,
|
|
145981
|
+
projectPath: row.project_path,
|
|
145982
|
+
projectName: row.project_name,
|
|
145983
|
+
branch: row.branch,
|
|
145984
|
+
// No PTY exists for a stub, so this is the only truthful status.
|
|
145985
|
+
status: "idle",
|
|
145986
|
+
startedAt: new Date(row.started_at),
|
|
145987
|
+
completedAt: row.completed_at != null ? new Date(row.completed_at) : null,
|
|
145988
|
+
promptCount: row.prompt_count,
|
|
145989
|
+
lastOutput: "",
|
|
145990
|
+
rehydrated: true,
|
|
145991
|
+
...row.session_name != null && { sessionName: row.session_name },
|
|
145992
|
+
...row.project_id != null && { projectId: row.project_id },
|
|
145993
|
+
...row.bound_conversation_id != null && { boundConversationId: row.bound_conversation_id },
|
|
145994
|
+
...row.resumed_from_conversation_id != null && {
|
|
145995
|
+
resumedFromConversationId: row.resumed_from_conversation_id
|
|
145996
|
+
},
|
|
145997
|
+
...row.failure_reason != null && { failureReason: row.failure_reason },
|
|
145998
|
+
...row.last_activity_at != null && { lastActivityAt: new Date(row.last_activity_at) },
|
|
145999
|
+
// Only `shutdown` crosses over. It is the one registry source that is also a
|
|
146000
|
+
// wire StatusSource *and* that genuinely describes the `idle` above — the
|
|
146001
|
+
// streamer stopped this session. A crashed row still says `transition` over
|
|
146002
|
+
// a `running` status, and copying that here would attach observed-confidence
|
|
146003
|
+
// provenance to a status we derived at boot, so leave it unset instead.
|
|
146004
|
+
...row.status_source === "shutdown" && {
|
|
146005
|
+
statusSource: "shutdown",
|
|
146006
|
+
statusUpdatedAt: new Date(row.status_updated_at)
|
|
146007
|
+
}
|
|
146008
|
+
};
|
|
146009
|
+
}
|
|
146010
|
+
|
|
145808
146011
|
// src/agent/dedupe.ts
|
|
145809
146012
|
function createProgressDedupeLRU(capacity) {
|
|
145810
146013
|
if (!Number.isFinite(capacity) || capacity < 1) {
|
|
@@ -145978,14 +146181,15 @@ function managedToResponse(s3, ptyAttached) {
|
|
|
145978
146181
|
// Lifecycle for a session this run knows about. `attached` while we hold
|
|
145979
146182
|
// its PTY; once the PTY is gone the session is terminal from this run's
|
|
145980
146183
|
// perspective — `failed` when it recorded a reason, else `completed`.
|
|
145981
|
-
//
|
|
145982
|
-
//
|
|
145983
|
-
//
|
|
145984
|
-
|
|
145985
|
-
|
|
146184
|
+
// A `rehydrated` stub is the exception: the boot rehydrator seeded it from
|
|
146185
|
+
// the durable registry, so it is a previous run's session with no process
|
|
146186
|
+
// behind it — `resumable`, and `historical` rather than `managed`
|
|
146187
|
+
// (docs/plans/live-sessions-persistence-plan.md §4, Phase 1).
|
|
146188
|
+
lifecycle: ptyAttached ? "attached" : s3.rehydrated ? "resumable" : s3.failureReason != null ? "failed" : "completed",
|
|
146189
|
+
lifecycleSource: ptyAttached ? "spawn" : s3.rehydrated ? "reconcile" : "exit",
|
|
145986
146190
|
// We spawned it, so `status` is the authoritative signal — no inferred
|
|
145987
146191
|
// `activity` is attached for managed sessions.
|
|
145988
|
-
ownership: "managed",
|
|
146192
|
+
ownership: s3.rehydrated ? "historical" : "managed",
|
|
145989
146193
|
projectPath: s3.projectPath,
|
|
145990
146194
|
projectName: s3.projectName,
|
|
145991
146195
|
branch: s3.branch,
|
|
@@ -146575,10 +146779,13 @@ var StreamerServer = class {
|
|
|
146575
146779
|
projectsRepo = null;
|
|
146576
146780
|
conversationsRepo = null;
|
|
146577
146781
|
sessionsRepo = null;
|
|
146578
|
-
// Durable session registry (C1 Phase 2). Null when
|
|
146579
|
-
//
|
|
146580
|
-
// taking the server down with it, so every write goes through `?.`.
|
|
146782
|
+
// Durable session registry (C1 Phase 2). Null when runtime.db failed to open
|
|
146783
|
+
// — persistence degrades to today's in-memory-only behaviour rather than
|
|
146784
|
+
// taking the server down with it, so every write goes through `?.`. Note the
|
|
146785
|
+
// handle is runtime.db, NOT the conversation cache: a cache failure used to
|
|
146786
|
+
// null this repo and silently disable all session persistence.
|
|
146581
146787
|
managedSessionsRepo = null;
|
|
146788
|
+
runtimeStore = null;
|
|
146582
146789
|
// Identifies this streamer run. A registry row carrying a different id is a
|
|
146583
146790
|
// session that outlived the process that started it.
|
|
146584
146791
|
streamerInstanceId = (0, import_crypto13.randomUUID)();
|
|
@@ -146597,6 +146804,7 @@ var StreamerServer = class {
|
|
|
146597
146804
|
liveActivityRenewal = null;
|
|
146598
146805
|
discoveryCache = null;
|
|
146599
146806
|
cacheDir;
|
|
146807
|
+
runtimeDbPath;
|
|
146600
146808
|
tailSize;
|
|
146601
146809
|
directoryDebounceMs;
|
|
146602
146810
|
// Trailing-debounced trigger that flags the scanner stale after a quiet
|
|
@@ -146643,6 +146851,7 @@ var StreamerServer = class {
|
|
|
146643
146851
|
this.claudeFlags = config2.claudeFlags ?? loadClaudeFlags();
|
|
146644
146852
|
this.claudeExtraArgs = config2.claudeExtraArgs ?? loadClaudeExtraArgs();
|
|
146645
146853
|
this.cacheDir = config2.cacheDir ?? loadCacheDir() ?? (0, import_path29.join)((0, import_os13.homedir)(), ".threadbase", "cache");
|
|
146854
|
+
this.runtimeDbPath = config2.runtimeDbPath ?? process.env.THREADBASE_RUNTIME_DB ?? (0, import_path29.join)(process.env.THREADBASE_CONFIG_DIR ?? (0, import_path29.join)((0, import_os13.homedir)(), ".threadbase"), "runtime.db");
|
|
146646
146855
|
this.tailSize = config2.tailSize ?? loadTailSize() ?? 10;
|
|
146647
146856
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config2.directoryScanDebounceMs ?? 1e3;
|
|
146648
146857
|
this.markScannerStaleDebounced = debounce(() => {
|
|
@@ -146813,6 +147022,7 @@ var StreamerServer = class {
|
|
|
146813
147022
|
if (resp) this.wsHub.broadcast({ type: "session_ready", session: resp });
|
|
146814
147023
|
},
|
|
146815
147024
|
onStatusChange: (session) => {
|
|
147025
|
+
const previousStatus = this.sessionStore.getManaged(session.id)?.status;
|
|
146816
147026
|
this.sessionStore.updateManaged(session.id, {
|
|
146817
147027
|
status: session.status,
|
|
146818
147028
|
completedAt: session.completedAt,
|
|
@@ -146826,7 +147036,11 @@ var StreamerServer = class {
|
|
|
146826
147036
|
completedAt: session.completedAt,
|
|
146827
147037
|
lastActivityAt: session.lastActivityAt ?? null,
|
|
146828
147038
|
promptCount: session.promptCount,
|
|
146829
|
-
failureReason: session.failureReason ?? null
|
|
147039
|
+
failureReason: session.failureReason ?? null,
|
|
147040
|
+
// Derived from the first user message, so it does not exist yet at
|
|
147041
|
+
// recordSpawn. The input that produces it also flips
|
|
147042
|
+
// waiting_input→running, which lands here.
|
|
147043
|
+
sessionName: session.sessionName ?? null
|
|
146830
147044
|
}
|
|
146831
147045
|
);
|
|
146832
147046
|
if (session.status === "waiting_input" || session.status === "idle") {
|
|
@@ -146867,7 +147081,7 @@ var StreamerServer = class {
|
|
|
146867
147081
|
if (resp) {
|
|
146868
147082
|
this.wsHub.broadcast({ type: "session_update", session: resp });
|
|
146869
147083
|
}
|
|
146870
|
-
void this.liveActivityNotifier?.onStatusChange(session);
|
|
147084
|
+
void this.liveActivityNotifier?.onStatusChange(session, previousStatus);
|
|
146871
147085
|
this.sessionStatusBus.emit(`status:${session.id}`, session.status);
|
|
146872
147086
|
}
|
|
146873
147087
|
});
|
|
@@ -146918,6 +147132,7 @@ var StreamerServer = class {
|
|
|
146918
147132
|
conversationsRepo: () => this.conversationsRepo,
|
|
146919
147133
|
sessionsRepo: () => this.sessionsRepo,
|
|
146920
147134
|
cacheMetadataRepo: () => this.cacheMetadataRepo,
|
|
147135
|
+
runtimeStore: () => this.runtimeStore,
|
|
146921
147136
|
ptyAttachedIds: () => this.ptyAttachedIds(),
|
|
146922
147137
|
handleListSessions: (url2, res) => this.handleListSessions(url2, res),
|
|
146923
147138
|
handleSessionsCount: (res) => this.handleSessionsCount(res),
|
|
@@ -147217,6 +147432,58 @@ var StreamerServer = class {
|
|
|
147217
147432
|
}
|
|
147218
147433
|
return verdicts;
|
|
147219
147434
|
}
|
|
147435
|
+
/**
|
|
147436
|
+
* Seed the session list with what previous runs left behind (persistence plan
|
|
147437
|
+
* Phase 1, gaps G1/G2/G8).
|
|
147438
|
+
*
|
|
147439
|
+
* Reconciliation classifies rows and stops there; a verdict is overlaid onto a
|
|
147440
|
+
* SessionResponse that already exists, and after a clean restart none does —
|
|
147441
|
+
* `SessionStore` starts empty. So the user's session did not become
|
|
147442
|
+
* `resumable`, it became *absent*. This is the half that puts it back.
|
|
147443
|
+
*
|
|
147444
|
+
* The seeded stubs hold no PTY and are never handed to `LiveSessionManager`,
|
|
147445
|
+
* so `reapIdleSessions` and `startGraceTimer` — both of which iterate
|
|
147446
|
+
* `ptyManager.listSessions()` — cannot observe them. A later resume calls
|
|
147447
|
+
* `sessionStore.addManaged` with the real session, which overwrites the stub
|
|
147448
|
+
* by id rather than duplicating it.
|
|
147449
|
+
*/
|
|
147450
|
+
rehydratePreviousSessions(verdicts) {
|
|
147451
|
+
if (!this.featureFlags.sessionRehydration || !this.managedSessionsRepo) return;
|
|
147452
|
+
try {
|
|
147453
|
+
const now = Date.now();
|
|
147454
|
+
const rows = this.managedSessionsRepo.listRecoverable({
|
|
147455
|
+
sinceMs: now - REHYDRATE_WINDOW_MS,
|
|
147456
|
+
limit: REHYDRATE_MAX + 1
|
|
147457
|
+
});
|
|
147458
|
+
const truncated = rows.length > REHYDRATE_MAX;
|
|
147459
|
+
const candidates = truncated ? rows.slice(0, REHYDRATE_MAX) : rows;
|
|
147460
|
+
if (candidates.length === 0) return;
|
|
147461
|
+
const lifecycleByVerdict = new Map(verdicts.map((v2) => [v2.sessionId, v2.lifecycle]));
|
|
147462
|
+
let rehydrated = 0;
|
|
147463
|
+
for (const row of candidates) {
|
|
147464
|
+
if (this.sessionStore.getManaged(row.session_id)) continue;
|
|
147465
|
+
if (!shouldRehydrate(row, { now, projectExists: import_fs30.existsSync })) continue;
|
|
147466
|
+
this.sessionStore.addManaged(rowToStubSession(row));
|
|
147467
|
+
this.sessionLifecycles.set(
|
|
147468
|
+
row.session_id,
|
|
147469
|
+
lifecycleByVerdict.get(row.session_id) ?? "resumable"
|
|
147470
|
+
);
|
|
147471
|
+
if (row.completed_at != null) this.selfPtyEndedAt.set(row.session_id, row.completed_at);
|
|
147472
|
+
rehydrated++;
|
|
147473
|
+
}
|
|
147474
|
+
this.log.info(`[rehydrate] recovered ${rehydrated} session(s) from the registry`, {
|
|
147475
|
+
event: "sessions.rehydrated",
|
|
147476
|
+
rehydrated,
|
|
147477
|
+
skipped: candidates.length - rehydrated,
|
|
147478
|
+
truncated
|
|
147479
|
+
});
|
|
147480
|
+
} catch (err) {
|
|
147481
|
+
this.log.warn("[rehydrate] failed to rehydrate previous sessions", {
|
|
147482
|
+
event: "sessions.rehydrate_failed",
|
|
147483
|
+
err
|
|
147484
|
+
});
|
|
147485
|
+
}
|
|
147486
|
+
}
|
|
147220
147487
|
/**
|
|
147221
147488
|
* Pick a token guaranteed to appear in the spawned process's argv, for the
|
|
147222
147489
|
* reconciler's pid-reuse guard.
|
|
@@ -147450,6 +147717,17 @@ var StreamerServer = class {
|
|
|
147450
147717
|
port,
|
|
147451
147718
|
event: "server.listening"
|
|
147452
147719
|
});
|
|
147720
|
+
try {
|
|
147721
|
+
this.runtimeStore = RuntimeStore.open(this.runtimeDbPath);
|
|
147722
|
+
this.managedSessionsRepo = new ManagedSessionsRepository(this.runtimeStore.getDatabase());
|
|
147723
|
+
} catch (err) {
|
|
147724
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
147725
|
+
const abiMismatch = message.includes("NODE_MODULE_VERSION") || message.includes("was compiled against a different Node.js version");
|
|
147726
|
+
this.log.error(
|
|
147727
|
+
`Runtime store failed to open \u2014 session persistence DISABLED; sessions will not survive a restart.` + (abiMismatch ? ` Fix: npm rebuild better-sqlite3` : "") + ` (${message})`,
|
|
147728
|
+
{ error: message, abiMismatch, path: this.runtimeDbPath, event: "runtime.open_failed" }
|
|
147729
|
+
);
|
|
147730
|
+
}
|
|
147453
147731
|
try {
|
|
147454
147732
|
this.cache = ConversationCache.open(
|
|
147455
147733
|
(0, import_path29.join)(this.cacheDir, "cache.db"),
|
|
@@ -147477,8 +147755,20 @@ var StreamerServer = class {
|
|
|
147477
147755
|
this.projectsRepo = new ProjectsRepository(db);
|
|
147478
147756
|
this.conversationsRepo = new ConversationsRepository(this.cache);
|
|
147479
147757
|
this.sessionsRepo = new SessionsRepository(this.sessionStore);
|
|
147480
|
-
|
|
147481
|
-
|
|
147758
|
+
try {
|
|
147759
|
+
const copied = this.runtimeStore?.importLegacyManagedSessions(db) ?? 0;
|
|
147760
|
+
if (copied > 0) {
|
|
147761
|
+
this.log.info(`Copied ${copied} managed session row(s) from cache.db to runtime.db`, {
|
|
147762
|
+
copied,
|
|
147763
|
+
event: "runtime.legacy_import"
|
|
147764
|
+
});
|
|
147765
|
+
}
|
|
147766
|
+
} catch (err) {
|
|
147767
|
+
this.log.warn("[registry] legacy managed_sessions copy failed", {
|
|
147768
|
+
event: "runtime.legacy_import_failed",
|
|
147769
|
+
err
|
|
147770
|
+
});
|
|
147771
|
+
}
|
|
147482
147772
|
this.cacheMetadataRepo = new CacheMetadataRepository(db);
|
|
147483
147773
|
this.pushRepo = new PushRepository(db);
|
|
147484
147774
|
this.devicesRepo = new DevicesRepository(db);
|
|
@@ -147514,6 +147804,7 @@ var StreamerServer = class {
|
|
|
147514
147804
|
);
|
|
147515
147805
|
this.scannerPersistenceDisabled = true;
|
|
147516
147806
|
}
|
|
147807
|
+
void this.reconcilePreviousSessions().then((v2) => this.rehydratePreviousSessions(v2));
|
|
147517
147808
|
if (this.skipStartupWarmup) {
|
|
147518
147809
|
this.log.debug?.("startup warm-up scan skipped (skipStartupWarmup)", {
|
|
147519
147810
|
event: "cache.warmup_skipped"
|
|
@@ -147721,6 +148012,7 @@ var StreamerServer = class {
|
|
|
147721
148012
|
this.allScanners.clear();
|
|
147722
148013
|
this.scanner = null;
|
|
147723
148014
|
this.cache?.close();
|
|
148015
|
+
this.runtimeStore?.close();
|
|
147724
148016
|
this.ptyManager.dispose();
|
|
147725
148017
|
this.fileWatcher.dispose();
|
|
147726
148018
|
this.externalTails.clear();
|
|
@@ -148168,7 +148460,8 @@ var StreamerServer = class {
|
|
|
148168
148460
|
}
|
|
148169
148461
|
handleSessionsCount(res) {
|
|
148170
148462
|
if (this.rejectIfWarmingUp(res)) return;
|
|
148171
|
-
|
|
148463
|
+
const total = this.sessionStore.list(this.ptyAttachedIds()).filter((s3) => s3.ownership !== "historical").length;
|
|
148464
|
+
json2(res, 200, { total });
|
|
148172
148465
|
}
|
|
148173
148466
|
handleGetRecentSessions(url2, res) {
|
|
148174
148467
|
if (this.rejectIfWarmingUp(res)) return;
|