@threadbase-sh/streamer 1.43.0 → 1.44.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/dist/cli.cjs +1379 -483
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +853 -109
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -4
- package/dist/index.d.ts +44 -4
- package/dist/index.js +894 -150
- package/dist/index.js.map +1 -1
- package/dist/launchd-entry.cjs +8 -5
- package/dist/launchd-entry.cjs.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -102,14 +102,14 @@ function createConversationWriter(opts) {
|
|
|
102
102
|
}
|
|
103
103
|
const file = join(baseDir, `${args.sessionId}.jsonl`);
|
|
104
104
|
await mkdir(dirname(file), { recursive: true });
|
|
105
|
-
const
|
|
105
|
+
const record2 = {
|
|
106
106
|
role: "assistant",
|
|
107
107
|
turnId: args.turnId,
|
|
108
108
|
content: args.content,
|
|
109
109
|
timestamp: Date.now(),
|
|
110
110
|
...args.reviewerOverruled ? { reviewerOverruled: true } : {}
|
|
111
111
|
};
|
|
112
|
-
const line = `${JSON.stringify(
|
|
112
|
+
const line = `${JSON.stringify(record2)}
|
|
113
113
|
`;
|
|
114
114
|
await appendFile(file, line, { encoding: "utf8" });
|
|
115
115
|
}
|
|
@@ -416,6 +416,9 @@ var baseLogger = pino({
|
|
|
416
416
|
censor: "[redacted]"
|
|
417
417
|
}
|
|
418
418
|
});
|
|
419
|
+
function defaultDest() {
|
|
420
|
+
return process.stdout.isTTY ? "console" : "pino";
|
|
421
|
+
}
|
|
419
422
|
function emit(pinoChild, level, msg, fields, dest) {
|
|
420
423
|
if (dest === "pino" || dest === "both") {
|
|
421
424
|
if (fields && Object.keys(fields).length > 0) pinoChild[level](fields, msg);
|
|
@@ -428,11 +431,11 @@ function emit(pinoChild, level, msg, fields, dest) {
|
|
|
428
431
|
}
|
|
429
432
|
function build(pinoChild) {
|
|
430
433
|
return {
|
|
431
|
-
debug: (m, f, d =
|
|
432
|
-
info: (m, f, d =
|
|
433
|
-
warn: (m, f, d =
|
|
434
|
-
error: (m, f, d =
|
|
435
|
-
log: (lvl, m, f, d =
|
|
434
|
+
debug: (m, f, d = defaultDest()) => emit(pinoChild, "debug", m, f, d),
|
|
435
|
+
info: (m, f, d = defaultDest()) => emit(pinoChild, "info", m, f, d),
|
|
436
|
+
warn: (m, f, d = defaultDest()) => emit(pinoChild, "warn", m, f, d),
|
|
437
|
+
error: (m, f, d = defaultDest()) => emit(pinoChild, "error", m, f, d),
|
|
438
|
+
log: (lvl, m, f, d = defaultDest()) => emit(pinoChild, lvl, m, f, d),
|
|
436
439
|
pino: pinoChild
|
|
437
440
|
};
|
|
438
441
|
}
|
|
@@ -454,6 +457,12 @@ var FEATURE_FLAGS = [
|
|
|
454
457
|
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
458
|
default: true,
|
|
456
459
|
env: "THREADBASE_FEATURE_SESSION_REHYDRATION"
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
id: "ptyHost",
|
|
463
|
+
description: "Keep live PTYs in a separate host process so a streamer restart can reconnect without restarting the agents. Off by default until cross-platform behavior is qualified.",
|
|
464
|
+
default: false,
|
|
465
|
+
env: "THREADBASE_FEATURE_PTY_HOST"
|
|
457
466
|
}
|
|
458
467
|
];
|
|
459
468
|
function findFeatureFlag(id) {
|
|
@@ -819,7 +828,16 @@ import { existsSync } from "fs";
|
|
|
819
828
|
import { homedir as homedir2, platform } from "os";
|
|
820
829
|
import { join as join4 } from "path";
|
|
821
830
|
var isWindows = platform() === "win32";
|
|
831
|
+
var WINDOWS_EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([".exe", ".cmd", ".bat"]);
|
|
832
|
+
function isWindowsExecutablePath(path) {
|
|
833
|
+
const dot = path.lastIndexOf(".");
|
|
834
|
+
if (dot < 0) return false;
|
|
835
|
+
return WINDOWS_EXECUTABLE_EXTENSIONS.has(path.slice(dot).toLowerCase());
|
|
836
|
+
}
|
|
822
837
|
var _claudeExe;
|
|
838
|
+
function clearClaudeExeCache() {
|
|
839
|
+
_claudeExe = void 0;
|
|
840
|
+
}
|
|
823
841
|
function resolveClaudeExe() {
|
|
824
842
|
if (_claudeExe !== void 0) return _claudeExe;
|
|
825
843
|
if (isWindows) {
|
|
@@ -828,7 +846,7 @@ function resolveClaudeExe() {
|
|
|
828
846
|
encoding: "utf-8",
|
|
829
847
|
windowsHide: true,
|
|
830
848
|
timeout: 3e3
|
|
831
|
-
}).trim().split("\n")
|
|
849
|
+
}).trim().split("\n").map((line) => line.trim()).find(isWindowsExecutablePath);
|
|
832
850
|
if (found) {
|
|
833
851
|
_claudeExe = found;
|
|
834
852
|
return _claudeExe;
|
|
@@ -878,6 +896,9 @@ function resolveClaudeExe() {
|
|
|
878
896
|
return _claudeExe;
|
|
879
897
|
}
|
|
880
898
|
var _codexExe;
|
|
899
|
+
function clearCodexExeCache() {
|
|
900
|
+
_codexExe = void 0;
|
|
901
|
+
}
|
|
881
902
|
function resolveCodexExe() {
|
|
882
903
|
if (_codexExe !== void 0) return _codexExe;
|
|
883
904
|
if (isWindows) {
|
|
@@ -886,7 +907,7 @@ function resolveCodexExe() {
|
|
|
886
907
|
encoding: "utf-8",
|
|
887
908
|
windowsHide: true,
|
|
888
909
|
timeout: 3e3
|
|
889
|
-
}).trim().split("\n")
|
|
910
|
+
}).trim().split("\n").map((line) => line.trim()).find(isWindowsExecutablePath);
|
|
890
911
|
if (found) {
|
|
891
912
|
_codexExe = found;
|
|
892
913
|
return _codexExe;
|
|
@@ -1146,20 +1167,26 @@ var CodexPtyRunner = class {
|
|
|
1146
1167
|
async doStart(sessionId, options) {
|
|
1147
1168
|
const nodePty = await loadPty();
|
|
1148
1169
|
const projectName = options.projectName ?? basename(options.projectPath);
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1170
|
+
let proc;
|
|
1171
|
+
try {
|
|
1172
|
+
proc = nodePty.spawn(
|
|
1173
|
+
resolveCodexExe(),
|
|
1174
|
+
// `sessionId` stays the runner's map key — only argv carries the
|
|
1175
|
+
// provider-side id, so a resumed Codex session keeps the placeholder id
|
|
1176
|
+
// its client already navigated to.
|
|
1177
|
+
["resume", options.resumeId ?? sessionId, "--cd", options.projectPath, "--no-alt-screen"],
|
|
1178
|
+
{
|
|
1179
|
+
name: "xterm-256color",
|
|
1180
|
+
cols: PTY_COLS,
|
|
1181
|
+
rows: PTY_ROWS,
|
|
1182
|
+
cwd: options.projectPath,
|
|
1183
|
+
env: process.env
|
|
1184
|
+
}
|
|
1185
|
+
);
|
|
1186
|
+
} catch (err) {
|
|
1187
|
+
clearCodexExeCache();
|
|
1188
|
+
throw err;
|
|
1189
|
+
}
|
|
1163
1190
|
const session = {
|
|
1164
1191
|
id: sessionId,
|
|
1165
1192
|
provider: CODEX_CLI_PROVIDER,
|
|
@@ -1202,13 +1229,19 @@ var CodexPtyRunner = class {
|
|
|
1202
1229
|
if (options.systemPrompt) {
|
|
1203
1230
|
args.push(options.systemPrompt);
|
|
1204
1231
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1232
|
+
let proc;
|
|
1233
|
+
try {
|
|
1234
|
+
proc = nodePty.spawn(resolveCodexExe(), args, {
|
|
1235
|
+
name: "xterm-256color",
|
|
1236
|
+
cols: PTY_COLS,
|
|
1237
|
+
rows: PTY_ROWS,
|
|
1238
|
+
cwd: options.projectPath,
|
|
1239
|
+
env: process.env
|
|
1240
|
+
});
|
|
1241
|
+
} catch (err) {
|
|
1242
|
+
clearCodexExeCache();
|
|
1243
|
+
throw err;
|
|
1244
|
+
}
|
|
1212
1245
|
const session = {
|
|
1213
1246
|
id: sessionId,
|
|
1214
1247
|
provider: CODEX_CLI_PROVIDER,
|
|
@@ -1683,6 +1716,381 @@ function stripAnsi(str) {
|
|
|
1683
1716
|
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
|
|
1684
1717
|
}
|
|
1685
1718
|
|
|
1719
|
+
// src/pty-host/protocol.ts
|
|
1720
|
+
var PTY_HOST_PROTOCOL_VERSION = 2;
|
|
1721
|
+
function isHostEvent(message) {
|
|
1722
|
+
return "type" in message && message.type === "event";
|
|
1723
|
+
}
|
|
1724
|
+
var SESSION_DATE_FIELDS = [
|
|
1725
|
+
"startedAt",
|
|
1726
|
+
"completedAt",
|
|
1727
|
+
"statusUpdatedAt",
|
|
1728
|
+
"lastActivityAt",
|
|
1729
|
+
"firstMessageAt",
|
|
1730
|
+
"lastMessageAt"
|
|
1731
|
+
];
|
|
1732
|
+
function reviveSession(raw) {
|
|
1733
|
+
const s = { ...raw };
|
|
1734
|
+
for (const field of SESSION_DATE_FIELDS) {
|
|
1735
|
+
const value = s[field];
|
|
1736
|
+
if (typeof value === "string") s[field] = new Date(value);
|
|
1737
|
+
}
|
|
1738
|
+
return s;
|
|
1739
|
+
}
|
|
1740
|
+
function encodeMessage(message) {
|
|
1741
|
+
return `${JSON.stringify(message)}
|
|
1742
|
+
`;
|
|
1743
|
+
}
|
|
1744
|
+
var LineDecoder = class {
|
|
1745
|
+
buffer = "";
|
|
1746
|
+
push(chunk) {
|
|
1747
|
+
this.buffer += chunk;
|
|
1748
|
+
const lines = this.buffer.split("\n");
|
|
1749
|
+
this.buffer = lines.pop() ?? "";
|
|
1750
|
+
return lines.filter((line) => line.length > 0);
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
|
|
1754
|
+
// src/pty-host/remote-session-runner.ts
|
|
1755
|
+
var PtyHostProtocolMismatchError = class extends Error {
|
|
1756
|
+
constructor(hostVersion, streamerVersion) {
|
|
1757
|
+
super(
|
|
1758
|
+
`pty-host protocol ${hostVersion} is incompatible with streamer protocol ${streamerVersion}`
|
|
1759
|
+
);
|
|
1760
|
+
this.hostVersion = hostVersion;
|
|
1761
|
+
this.streamerVersion = streamerVersion;
|
|
1762
|
+
this.name = "PtyHostProtocolMismatchError";
|
|
1763
|
+
}
|
|
1764
|
+
hostVersion;
|
|
1765
|
+
streamerVersion;
|
|
1766
|
+
};
|
|
1767
|
+
var HOST_HEARTBEAT_INTERVAL_MS = 1e4;
|
|
1768
|
+
var HOST_HEARTBEAT_REQUEST_TIMEOUT_MS = 5e3;
|
|
1769
|
+
var HOST_SHUTDOWN_REQUEST_TIMEOUT_MS = 1e3;
|
|
1770
|
+
var RemoteSessionRunner = class _RemoteSessionRunner {
|
|
1771
|
+
transport;
|
|
1772
|
+
options;
|
|
1773
|
+
decoder = new LineDecoder();
|
|
1774
|
+
nextRequestId = 1;
|
|
1775
|
+
pending = /* @__PURE__ */ new Map();
|
|
1776
|
+
/** The mirror. Rebuilt wholesale by `status`, patched by events. */
|
|
1777
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1778
|
+
/** Ring buffers, fed by `output` events so `getOutput` stays synchronous. */
|
|
1779
|
+
output = /* @__PURE__ */ new Map();
|
|
1780
|
+
inputHistory = /* @__PURE__ */ new Map();
|
|
1781
|
+
/** Fixed for a session's lifetime, so only spawn and status carry it. */
|
|
1782
|
+
pids = /* @__PURE__ */ new Map();
|
|
1783
|
+
closed = false;
|
|
1784
|
+
heartbeatTimer = null;
|
|
1785
|
+
heartbeatInFlight = false;
|
|
1786
|
+
/**
|
|
1787
|
+
* The only supported way to build one: a runner whose mirror has not been
|
|
1788
|
+
* seeded yet would answer `hasSession` with a confident, wrong `false` for
|
|
1789
|
+
* every session the host is holding — which reads as "the agent is gone" and
|
|
1790
|
+
* routes the user to start a new one.
|
|
1791
|
+
*/
|
|
1792
|
+
static async connect(transport, options = {}) {
|
|
1793
|
+
const runner = new _RemoteSessionRunner(transport, options);
|
|
1794
|
+
const status = await runner.readStatus();
|
|
1795
|
+
if (status.protocolVersion !== PTY_HOST_PROTOCOL_VERSION) {
|
|
1796
|
+
try {
|
|
1797
|
+
await runner.request({ type: "shutdown-host" }, HOST_SHUTDOWN_REQUEST_TIMEOUT_MS);
|
|
1798
|
+
} catch (err) {
|
|
1799
|
+
options.logger?.warn("[pty-host] incompatible host did not acknowledge shutdown", {
|
|
1800
|
+
event: "pty_host.shutdown_failed",
|
|
1801
|
+
err
|
|
1802
|
+
});
|
|
1803
|
+
} finally {
|
|
1804
|
+
runner.dispose();
|
|
1805
|
+
}
|
|
1806
|
+
throw new PtyHostProtocolMismatchError(status.protocolVersion, PTY_HOST_PROTOCOL_VERSION);
|
|
1807
|
+
}
|
|
1808
|
+
await runner.request({ type: "subscribe" });
|
|
1809
|
+
runner.refreshMirror(status);
|
|
1810
|
+
return runner;
|
|
1811
|
+
}
|
|
1812
|
+
constructor(transport, options) {
|
|
1813
|
+
this.transport = transport;
|
|
1814
|
+
this.options = options;
|
|
1815
|
+
transport.onLine((line) => this.handleLine(line));
|
|
1816
|
+
transport.onClose(() => this.handleClose());
|
|
1817
|
+
}
|
|
1818
|
+
// ─── Transport plumbing ──────────────────────────────────────────
|
|
1819
|
+
handleLine(line) {
|
|
1820
|
+
for (const complete of this.decoder.push(line)) {
|
|
1821
|
+
let message;
|
|
1822
|
+
try {
|
|
1823
|
+
message = JSON.parse(complete);
|
|
1824
|
+
} catch {
|
|
1825
|
+
this.options.logger?.warn("[pty-host] dropped unparseable message", {
|
|
1826
|
+
event: "pty_host.bad_message"
|
|
1827
|
+
});
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
if (isHostEvent(message)) {
|
|
1831
|
+
this.handleEvent(message);
|
|
1832
|
+
continue;
|
|
1833
|
+
}
|
|
1834
|
+
const waiter = this.pending.get(message.id);
|
|
1835
|
+
if (!waiter) continue;
|
|
1836
|
+
this.pending.delete(message.id);
|
|
1837
|
+
if (waiter.timeout) clearTimeout(waiter.timeout);
|
|
1838
|
+
if (message.ok) waiter.resolve(message.result);
|
|
1839
|
+
else waiter.reject(new Error(message.error));
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Fail every in-flight request when the socket drops.
|
|
1844
|
+
*
|
|
1845
|
+
* Without this each one stays pending forever and the caller — a session
|
|
1846
|
+
* start, an input write — hangs rather than erroring. PR 9 adds reconnection;
|
|
1847
|
+
* until then a dropped host is a hard failure that says so.
|
|
1848
|
+
*/
|
|
1849
|
+
handleClose() {
|
|
1850
|
+
if (this.closed) return;
|
|
1851
|
+
this.closed = true;
|
|
1852
|
+
this.stopHeartbeat();
|
|
1853
|
+
const err = new Error("pty-host connection closed");
|
|
1854
|
+
for (const waiter of this.pending.values()) {
|
|
1855
|
+
if (waiter.timeout) clearTimeout(waiter.timeout);
|
|
1856
|
+
waiter.reject(err);
|
|
1857
|
+
}
|
|
1858
|
+
this.pending.clear();
|
|
1859
|
+
}
|
|
1860
|
+
request(body, timeoutMs) {
|
|
1861
|
+
if (this.closed) return Promise.reject(new Error("pty-host connection closed"));
|
|
1862
|
+
const id = this.nextRequestId++;
|
|
1863
|
+
return new Promise((resolve2, reject) => {
|
|
1864
|
+
const timeout = timeoutMs === void 0 ? null : setTimeout(() => {
|
|
1865
|
+
if (!this.pending.delete(id)) return;
|
|
1866
|
+
reject(new Error(`${body.type} timed out after ${timeoutMs}ms`));
|
|
1867
|
+
}, timeoutMs);
|
|
1868
|
+
timeout?.unref?.();
|
|
1869
|
+
this.pending.set(id, { resolve: resolve2, reject, timeout });
|
|
1870
|
+
this.transport.send(encodeMessage({ ...body, id }));
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1874
|
+
* Fire-and-forget for the synchronous parts of `SessionRunner`.
|
|
1875
|
+
*
|
|
1876
|
+
* `sendKeys`, `cancel`, `killPid` and `putOnHold` all return void, so there is
|
|
1877
|
+
* no channel to report a failure through even if we waited for one. The
|
|
1878
|
+
* response is still consumed — an unhandled rejection would take the process
|
|
1879
|
+
* down over a keystroke that failed to land.
|
|
1880
|
+
*/
|
|
1881
|
+
fireAndForget(body) {
|
|
1882
|
+
this.request(body).catch((err) => {
|
|
1883
|
+
this.options.logger?.warn("[pty-host] request failed", {
|
|
1884
|
+
event: "pty_host.request_failed",
|
|
1885
|
+
type: body.type,
|
|
1886
|
+
err
|
|
1887
|
+
});
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
async readStatus() {
|
|
1891
|
+
return await this.request({ type: "status" });
|
|
1892
|
+
}
|
|
1893
|
+
refreshMirror(status) {
|
|
1894
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
1895
|
+
this.pids = /* @__PURE__ */ new Map();
|
|
1896
|
+
for (const entry of status.sessions) {
|
|
1897
|
+
const session = reviveSession(entry.session);
|
|
1898
|
+
this.sessions.set(session.id, session);
|
|
1899
|
+
this.pids.set(session.id, entry.pid);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
async heartbeat(state, timeoutMs = HOST_HEARTBEAT_REQUEST_TIMEOUT_MS) {
|
|
1903
|
+
await this.request({ type: "heartbeat", ...state }, timeoutMs);
|
|
1904
|
+
}
|
|
1905
|
+
startHeartbeat(getState, intervalMs = HOST_HEARTBEAT_INTERVAL_MS) {
|
|
1906
|
+
this.stopHeartbeat();
|
|
1907
|
+
const send = () => {
|
|
1908
|
+
if (this.closed || this.heartbeatInFlight) return;
|
|
1909
|
+
this.heartbeatInFlight = true;
|
|
1910
|
+
void Promise.resolve().then(() => this.heartbeat(getState())).catch((err) => {
|
|
1911
|
+
if (this.closed) return;
|
|
1912
|
+
this.options.logger?.warn("[pty-host] heartbeat failed", {
|
|
1913
|
+
event: "pty_host.heartbeat_failed",
|
|
1914
|
+
err
|
|
1915
|
+
});
|
|
1916
|
+
}).finally(() => {
|
|
1917
|
+
this.heartbeatInFlight = false;
|
|
1918
|
+
});
|
|
1919
|
+
};
|
|
1920
|
+
send();
|
|
1921
|
+
this.heartbeatTimer = setInterval(send, intervalMs);
|
|
1922
|
+
this.heartbeatTimer.unref?.();
|
|
1923
|
+
}
|
|
1924
|
+
stopHeartbeat() {
|
|
1925
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
1926
|
+
this.heartbeatTimer = null;
|
|
1927
|
+
}
|
|
1928
|
+
// ─── Events ──────────────────────────────────────────────────────
|
|
1929
|
+
handleEvent(event) {
|
|
1930
|
+
switch (event.event) {
|
|
1931
|
+
case "output": {
|
|
1932
|
+
this.output.set(event.sessionId, (this.output.get(event.sessionId) ?? "") + event.data);
|
|
1933
|
+
this.options.onOutput?.(event.sessionId, event.data);
|
|
1934
|
+
break;
|
|
1935
|
+
}
|
|
1936
|
+
case "status-change": {
|
|
1937
|
+
const session = reviveSession(event.session);
|
|
1938
|
+
if (session.status === "idle" && session.completedAt != null) {
|
|
1939
|
+
this.sessions.delete(session.id);
|
|
1940
|
+
this.pids.delete(session.id);
|
|
1941
|
+
this.output.delete(session.id);
|
|
1942
|
+
this.inputHistory.delete(session.id);
|
|
1943
|
+
} else {
|
|
1944
|
+
this.sessions.set(session.id, session);
|
|
1945
|
+
}
|
|
1946
|
+
this.options.onStatusChange?.(session);
|
|
1947
|
+
break;
|
|
1948
|
+
}
|
|
1949
|
+
case "ready": {
|
|
1950
|
+
const session = reviveSession(event.session);
|
|
1951
|
+
this.sessions.set(session.id, session);
|
|
1952
|
+
this.options.onReady?.(session);
|
|
1953
|
+
break;
|
|
1954
|
+
}
|
|
1955
|
+
case "permission-change":
|
|
1956
|
+
this.options.onPermissionChange?.(event.sessionId, event.gate);
|
|
1957
|
+
break;
|
|
1958
|
+
case "live-question":
|
|
1959
|
+
this.options.onLiveQuestion?.(event.sessionId, event.questions);
|
|
1960
|
+
break;
|
|
1961
|
+
case "live-question-gone":
|
|
1962
|
+
this.options.onLiveQuestionGone?.(event.sessionId);
|
|
1963
|
+
break;
|
|
1964
|
+
case "user-message": {
|
|
1965
|
+
const history = this.inputHistory.get(event.sessionId) ?? [];
|
|
1966
|
+
history.push({ text: event.text, ts: event.ts });
|
|
1967
|
+
this.inputHistory.set(event.sessionId, history);
|
|
1968
|
+
this.options.onUserMessage?.(event.sessionId, event.text, event.ts);
|
|
1969
|
+
break;
|
|
1970
|
+
}
|
|
1971
|
+
case "exit": {
|
|
1972
|
+
this.sessions.delete(event.sessionId);
|
|
1973
|
+
this.pids.delete(event.sessionId);
|
|
1974
|
+
this.output.delete(event.sessionId);
|
|
1975
|
+
this.inputHistory.delete(event.sessionId);
|
|
1976
|
+
break;
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
// ─── SessionRunner ───────────────────────────────────────────────
|
|
1981
|
+
async start(sessionId, options) {
|
|
1982
|
+
const provider = options.provider ?? "claude-code";
|
|
1983
|
+
return this.adopt(await this.request({ type: "spawn", provider, sessionId, options }));
|
|
1984
|
+
}
|
|
1985
|
+
async startFresh(options) {
|
|
1986
|
+
const provider = options.provider ?? "claude-code";
|
|
1987
|
+
return this.adopt(await this.request({ type: "spawn", provider, sessionId: null, options }));
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Take a spawn answer into the mirror.
|
|
1991
|
+
*
|
|
1992
|
+
* The pid lands here and nowhere else in the live path: `recordSessionSpawn`
|
|
1993
|
+
* reads it immediately after start to write the durable registry row, and a
|
|
1994
|
+
* null there costs the next boot its ability to probe whether the agent
|
|
1995
|
+
* outlived us.
|
|
1996
|
+
*/
|
|
1997
|
+
adopt(raw) {
|
|
1998
|
+
const entry = raw;
|
|
1999
|
+
const session = reviveSession(entry.session);
|
|
2000
|
+
this.sessions.set(session.id, session);
|
|
2001
|
+
this.pids.set(session.id, entry.pid);
|
|
2002
|
+
return session;
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Returns the mirror's promptCount, optimistically incremented.
|
|
2006
|
+
*
|
|
2007
|
+
* The interface is synchronous, so there is no way to return the host's
|
|
2008
|
+
* authoritative count. The increment matches what an in-process runner does
|
|
2009
|
+
* for the same call, and the next `status-change` event overwrites it — so a
|
|
2010
|
+
* mirror that guessed wrong is corrected within one round trip rather than
|
|
2011
|
+
* drifting.
|
|
2012
|
+
*/
|
|
2013
|
+
sendInput(sessionId, input) {
|
|
2014
|
+
const session = this.requireSession(sessionId);
|
|
2015
|
+
this.fireAndForget({ type: "write", sessionId, input });
|
|
2016
|
+
session.promptCount += 1;
|
|
2017
|
+
return session.promptCount;
|
|
2018
|
+
}
|
|
2019
|
+
sendKeys(sessionId, keys) {
|
|
2020
|
+
this.requireSession(sessionId);
|
|
2021
|
+
this.fireAndForget({ type: "keys", sessionId, keys });
|
|
2022
|
+
}
|
|
2023
|
+
cancel(sessionId) {
|
|
2024
|
+
this.fireAndForget({ type: "cancel", sessionId });
|
|
2025
|
+
}
|
|
2026
|
+
killPid(pid) {
|
|
2027
|
+
this.fireAndForget({ type: "kill", pid });
|
|
2028
|
+
}
|
|
2029
|
+
putOnHold(sessionId) {
|
|
2030
|
+
this.fireAndForget({ type: "kill", sessionId, hold: true });
|
|
2031
|
+
this.sessions.delete(sessionId);
|
|
2032
|
+
this.pids.delete(sessionId);
|
|
2033
|
+
}
|
|
2034
|
+
getOutput(sessionId) {
|
|
2035
|
+
this.requireSession(sessionId);
|
|
2036
|
+
return this.output.get(sessionId) ?? "";
|
|
2037
|
+
}
|
|
2038
|
+
async getOutputLines(sessionId, maxLines) {
|
|
2039
|
+
const result = await this.request({ type: "replay", sessionId, maxLines });
|
|
2040
|
+
if (typeof result.output === "string") this.output.set(sessionId, result.output);
|
|
2041
|
+
return result.lines;
|
|
2042
|
+
}
|
|
2043
|
+
/**
|
|
2044
|
+
* Synchronous, so it answers from the mirror rather than the host.
|
|
2045
|
+
*
|
|
2046
|
+
* Seeded lazily: `user-message` events append as they arrive, and a session
|
|
2047
|
+
* this streamer did not start has none until `hydrateInputHistory` fetches
|
|
2048
|
+
* them. Empty is the same answer an in-process runner gives for an unknown
|
|
2049
|
+
* session, so a caller cannot tell "none yet" from "not fetched" — which is
|
|
2050
|
+
* why the fetch is explicit rather than hidden behind this getter.
|
|
2051
|
+
*/
|
|
2052
|
+
getInputHistory(sessionId) {
|
|
2053
|
+
return this.inputHistory.get(sessionId) ?? [];
|
|
2054
|
+
}
|
|
2055
|
+
/** Pull a session's recorded messages from the host into the mirror. */
|
|
2056
|
+
async hydrateInputHistory(sessionId) {
|
|
2057
|
+
const result = await this.request({
|
|
2058
|
+
type: "input-history",
|
|
2059
|
+
sessionId
|
|
2060
|
+
});
|
|
2061
|
+
this.inputHistory.set(sessionId, result.history);
|
|
2062
|
+
return result.history;
|
|
2063
|
+
}
|
|
2064
|
+
getPid(sessionId) {
|
|
2065
|
+
return this.pids.get(sessionId) ?? null;
|
|
2066
|
+
}
|
|
2067
|
+
getSession(sessionId) {
|
|
2068
|
+
return this.sessions.get(sessionId) ?? null;
|
|
2069
|
+
}
|
|
2070
|
+
hasSession(sessionId) {
|
|
2071
|
+
return this.sessions.has(sessionId);
|
|
2072
|
+
}
|
|
2073
|
+
listSessions() {
|
|
2074
|
+
return [...this.sessions.values()];
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Drops this streamer's connection and nothing else.
|
|
2078
|
+
*
|
|
2079
|
+
* Emphatically NOT the in-process `dispose()`, which signals every child. The
|
|
2080
|
+
* entire point of the host is that its PTYs outlive the streamer, so tearing
|
|
2081
|
+
* them down here would spend the feature to implement a method name.
|
|
2082
|
+
*/
|
|
2083
|
+
dispose() {
|
|
2084
|
+
this.handleClose();
|
|
2085
|
+
this.transport.close();
|
|
2086
|
+
}
|
|
2087
|
+
requireSession(sessionId) {
|
|
2088
|
+
const session = this.sessions.get(sessionId);
|
|
2089
|
+
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
2090
|
+
return session;
|
|
2091
|
+
}
|
|
2092
|
+
};
|
|
2093
|
+
|
|
1686
2094
|
// src/pty-manager.ts
|
|
1687
2095
|
import { Terminal as Terminal2 } from "@xterm/headless";
|
|
1688
2096
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -2054,13 +2462,19 @@ var PTYManager = class {
|
|
|
2054
2462
|
sessionId
|
|
2055
2463
|
];
|
|
2056
2464
|
args.push(...buildFlagArgs(options.claudeFlags, options.claudeExtraArgs));
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2465
|
+
let proc;
|
|
2466
|
+
try {
|
|
2467
|
+
proc = nodePty.spawn(resolveClaudeExe(), args, {
|
|
2468
|
+
name: "xterm-256color",
|
|
2469
|
+
cols: 120,
|
|
2470
|
+
rows: 40,
|
|
2471
|
+
cwd: options.projectPath,
|
|
2472
|
+
env: buildSpawnEnv()
|
|
2473
|
+
});
|
|
2474
|
+
} catch (err) {
|
|
2475
|
+
clearClaudeExeCache();
|
|
2476
|
+
throw err;
|
|
2477
|
+
}
|
|
2064
2478
|
const session = {
|
|
2065
2479
|
id: sessionId,
|
|
2066
2480
|
provider: CLAUDE_CODE_PROVIDER,
|
|
@@ -2115,13 +2529,19 @@ var PTYManager = class {
|
|
|
2115
2529
|
args.push("--system-prompt", options.systemPrompt);
|
|
2116
2530
|
}
|
|
2117
2531
|
args.push(...buildFlagArgs(options.claudeFlags, options.claudeExtraArgs));
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2532
|
+
let proc;
|
|
2533
|
+
try {
|
|
2534
|
+
proc = nodePty.spawn(resolveClaudeExe(), args, {
|
|
2535
|
+
name: "xterm-256color",
|
|
2536
|
+
cols: 120,
|
|
2537
|
+
rows: 40,
|
|
2538
|
+
cwd: options.projectPath,
|
|
2539
|
+
env: buildSpawnEnv()
|
|
2540
|
+
});
|
|
2541
|
+
} catch (err) {
|
|
2542
|
+
clearClaudeExeCache();
|
|
2543
|
+
throw err;
|
|
2544
|
+
}
|
|
2125
2545
|
const session = {
|
|
2126
2546
|
id: sessionId,
|
|
2127
2547
|
provider: CLAUDE_CODE_PROVIDER,
|
|
@@ -2687,12 +3107,30 @@ function stripAnsi2(str) {
|
|
|
2687
3107
|
// src/live-session-manager.ts
|
|
2688
3108
|
var LiveSessionManager = class {
|
|
2689
3109
|
runners;
|
|
3110
|
+
remoteRunner = null;
|
|
3111
|
+
options;
|
|
2690
3112
|
constructor(options = {}) {
|
|
3113
|
+
this.options = options;
|
|
2691
3114
|
this.runners = /* @__PURE__ */ new Map([
|
|
2692
3115
|
[CLAUDE_CODE_PROVIDER, new PTYManager(options)],
|
|
2693
3116
|
[CODEX_CLI_PROVIDER, new CodexPtyRunner(options)]
|
|
2694
3117
|
]);
|
|
2695
3118
|
}
|
|
3119
|
+
async useRemoteRunner(transport) {
|
|
3120
|
+
const remote = await RemoteSessionRunner.connect(transport, this.options);
|
|
3121
|
+
await Promise.all(
|
|
3122
|
+
remote.listSessions().map((session) => remote.hydrateInputHistory(session.id))
|
|
3123
|
+
);
|
|
3124
|
+
for (const runner of this.runners.values()) runner.dispose();
|
|
3125
|
+
this.remoteRunner = remote;
|
|
3126
|
+
return remote.listSessions();
|
|
3127
|
+
}
|
|
3128
|
+
isRemote() {
|
|
3129
|
+
return this.remoteRunner !== null;
|
|
3130
|
+
}
|
|
3131
|
+
startRemoteHeartbeat(getState) {
|
|
3132
|
+
this.remoteRunner?.startHeartbeat(getState);
|
|
3133
|
+
}
|
|
2696
3134
|
async start(sessionId, options) {
|
|
2697
3135
|
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
2698
3136
|
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
@@ -2713,7 +3151,7 @@ var LiveSessionManager = class {
|
|
|
2713
3151
|
this.runnerFor(sessionId).cancel(sessionId);
|
|
2714
3152
|
}
|
|
2715
3153
|
killPid(pid) {
|
|
2716
|
-
for (const runner of this.
|
|
3154
|
+
for (const runner of this.activeRunners()) {
|
|
2717
3155
|
runner.killPid(pid);
|
|
2718
3156
|
}
|
|
2719
3157
|
}
|
|
@@ -2723,13 +3161,13 @@ var LiveSessionManager = class {
|
|
|
2723
3161
|
// every runner rather than throwing; this matches the pre-extraction
|
|
2724
3162
|
// behavior of delegating straight through with no existence check.
|
|
2725
3163
|
putOnHold(sessionId) {
|
|
2726
|
-
for (const runner of this.
|
|
3164
|
+
for (const runner of this.activeRunners()) {
|
|
2727
3165
|
if (runner.hasSession(sessionId) || runner.getSession(sessionId)) {
|
|
2728
3166
|
runner.putOnHold(sessionId);
|
|
2729
3167
|
return;
|
|
2730
3168
|
}
|
|
2731
3169
|
}
|
|
2732
|
-
for (const runner of this.
|
|
3170
|
+
for (const runner of this.activeRunners()) {
|
|
2733
3171
|
runner.putOnHold(sessionId);
|
|
2734
3172
|
}
|
|
2735
3173
|
}
|
|
@@ -2743,7 +3181,7 @@ var LiveSessionManager = class {
|
|
|
2743
3181
|
return this.runnerFor(sessionId).getInputHistory(sessionId);
|
|
2744
3182
|
}
|
|
2745
3183
|
getSession(sessionId) {
|
|
2746
|
-
for (const runner of this.
|
|
3184
|
+
for (const runner of this.activeRunners()) {
|
|
2747
3185
|
const session = runner.getSession(sessionId);
|
|
2748
3186
|
if (session) return session;
|
|
2749
3187
|
}
|
|
@@ -2753,23 +3191,23 @@ var LiveSessionManager = class {
|
|
|
2753
3191
|
// best-effort basis, so an unknown session must return null rather than
|
|
2754
3192
|
// throw the way the input-routing methods do.
|
|
2755
3193
|
getPid(sessionId) {
|
|
2756
|
-
for (const runner of this.
|
|
3194
|
+
for (const runner of this.activeRunners()) {
|
|
2757
3195
|
const pid = runner.getPid(sessionId);
|
|
2758
3196
|
if (pid != null) return pid;
|
|
2759
3197
|
}
|
|
2760
3198
|
return null;
|
|
2761
3199
|
}
|
|
2762
3200
|
hasSession(sessionId) {
|
|
2763
|
-
for (const runner of this.
|
|
3201
|
+
for (const runner of this.activeRunners()) {
|
|
2764
3202
|
if (runner.hasSession(sessionId)) return true;
|
|
2765
3203
|
}
|
|
2766
3204
|
return false;
|
|
2767
3205
|
}
|
|
2768
3206
|
listSessions() {
|
|
2769
|
-
return
|
|
3207
|
+
return this.activeRunners().flatMap((runner) => runner.listSessions());
|
|
2770
3208
|
}
|
|
2771
3209
|
dispose() {
|
|
2772
|
-
for (const runner of this.
|
|
3210
|
+
for (const runner of this.activeRunners()) {
|
|
2773
3211
|
runner.dispose();
|
|
2774
3212
|
}
|
|
2775
3213
|
}
|
|
@@ -2777,12 +3215,13 @@ var LiveSessionManager = class {
|
|
|
2777
3215
|
// this is a linear scan across hasSession()/getSession() rather than a
|
|
2778
3216
|
// separate session→provider index — see task-1-brief.md.
|
|
2779
3217
|
runnerFor(sessionId) {
|
|
2780
|
-
for (const runner of this.
|
|
3218
|
+
for (const runner of this.activeRunners()) {
|
|
2781
3219
|
if (runner.hasSession(sessionId) || runner.getSession(sessionId)) return runner;
|
|
2782
3220
|
}
|
|
2783
3221
|
throw new Error(`Session not found: ${sessionId}`);
|
|
2784
3222
|
}
|
|
2785
3223
|
assertSupportedProvider(provider, projectPath) {
|
|
3224
|
+
if (this.remoteRunner) return this.remoteRunner;
|
|
2786
3225
|
const runner = this.runners.get(provider);
|
|
2787
3226
|
if (runner) return runner;
|
|
2788
3227
|
const err = new Error(
|
|
@@ -2791,6 +3230,9 @@ var LiveSessionManager = class {
|
|
|
2791
3230
|
err.statusCode = 501;
|
|
2792
3231
|
throw err;
|
|
2793
3232
|
}
|
|
3233
|
+
activeRunners() {
|
|
3234
|
+
return this.remoteRunner ? [this.remoteRunner] : [...this.runners.values()];
|
|
3235
|
+
}
|
|
2794
3236
|
};
|
|
2795
3237
|
|
|
2796
3238
|
// src/process-discovery.ts
|
|
@@ -3080,9 +3522,9 @@ import {
|
|
|
3080
3522
|
statSync as statSync9
|
|
3081
3523
|
} from "fs";
|
|
3082
3524
|
import { realpath as realpath2 } from "fs/promises";
|
|
3083
|
-
import { createServer } from "http";
|
|
3084
|
-
import { homedir as
|
|
3085
|
-
import { basename as basename5, dirname as dirname9, join as
|
|
3525
|
+
import { createServer as createServer2 } from "http";
|
|
3526
|
+
import { homedir as homedir10, hostname as hostname3 } from "os";
|
|
3527
|
+
import { basename as basename5, dirname as dirname9, join as join19 } from "path";
|
|
3086
3528
|
import { createInterface } from "readline";
|
|
3087
3529
|
|
|
3088
3530
|
// node_modules/nanoid/index.js
|
|
@@ -5205,21 +5647,53 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
5205
5647
|
};
|
|
5206
5648
|
|
|
5207
5649
|
// src/api/app.ts
|
|
5650
|
+
var ALREADY_HANDLED7 = 597;
|
|
5651
|
+
function summarizeQuery(query) {
|
|
5652
|
+
const keys = Object.keys(query).sort();
|
|
5653
|
+
if (keys.length === 0) return void 0;
|
|
5654
|
+
return keys.map((k) => `${k}=${/^-?\d+$/.test(query[k]) ? query[k] : "_"}`).join("&");
|
|
5655
|
+
}
|
|
5656
|
+
function countResponseBytes(res) {
|
|
5657
|
+
let bytes = 0;
|
|
5658
|
+
const add = (chunk) => {
|
|
5659
|
+
if (typeof chunk === "string") bytes += Buffer.byteLength(chunk);
|
|
5660
|
+
else if (chunk instanceof Uint8Array) bytes += chunk.byteLength;
|
|
5661
|
+
};
|
|
5662
|
+
const write = res.write;
|
|
5663
|
+
const end = res.end;
|
|
5664
|
+
res.write = function(...args) {
|
|
5665
|
+
add(args[0]);
|
|
5666
|
+
return write.apply(this, args);
|
|
5667
|
+
};
|
|
5668
|
+
res.end = function(...args) {
|
|
5669
|
+
add(args[0]);
|
|
5670
|
+
return end.apply(this, args);
|
|
5671
|
+
};
|
|
5672
|
+
return () => bytes;
|
|
5673
|
+
}
|
|
5208
5674
|
var createHonoApp = (deps, upgradeWebSocket) => {
|
|
5209
5675
|
const app = new Hono18();
|
|
5210
5676
|
const httpLog = getLogger("http");
|
|
5211
5677
|
app.use("*", async (c, next) => {
|
|
5212
5678
|
const start = Date.now();
|
|
5213
5679
|
const ua = c.req.header("user-agent") ?? "";
|
|
5680
|
+
const outgoing = c.env?.outgoing;
|
|
5681
|
+
const bytesWritten = outgoing && countResponseBytes(outgoing);
|
|
5214
5682
|
await next();
|
|
5215
5683
|
if (!deps.logMenubarRequests && c.req.header("x-client") === "menubar") return;
|
|
5216
5684
|
const ms = Date.now() - start;
|
|
5217
|
-
|
|
5685
|
+
const handled = c.res.status === ALREADY_HANDLED7 && outgoing !== void 0;
|
|
5686
|
+
const status = handled ? outgoing.statusCode : c.res.status;
|
|
5687
|
+
const qs = summarizeQuery(c.req.query());
|
|
5688
|
+
const bytes = handled && bytesWritten ? bytesWritten() : void 0;
|
|
5689
|
+
httpLog.info(`[req] ${c.req.method} ${c.req.path} \u2192 ${status} ${ms}ms`, {
|
|
5218
5690
|
method: c.req.method,
|
|
5219
5691
|
path: c.req.path,
|
|
5220
|
-
status
|
|
5692
|
+
status,
|
|
5221
5693
|
ms,
|
|
5222
5694
|
ua,
|
|
5695
|
+
...qs ? { qs } : {},
|
|
5696
|
+
...bytes === void 0 ? {} : { bytes },
|
|
5223
5697
|
event: "http.request"
|
|
5224
5698
|
});
|
|
5225
5699
|
});
|
|
@@ -5310,6 +5784,74 @@ import { open as openAsync } from "fs/promises";
|
|
|
5310
5784
|
import { dirname as dirname7 } from "path";
|
|
5311
5785
|
import { setImmediate as yieldToEventLoop } from "timers/promises";
|
|
5312
5786
|
|
|
5787
|
+
// src/db/query-timing.ts
|
|
5788
|
+
var log3 = getLogger("db");
|
|
5789
|
+
var DEFAULT_SLOW_QUERY_MS = 35;
|
|
5790
|
+
var LABEL = /* @__PURE__ */ Symbol("tbQueryLabel");
|
|
5791
|
+
function deriveLabel(sql) {
|
|
5792
|
+
const verb = /^\s*(\w+)/.exec(sql)?.[1]?.toLowerCase() ?? "sql";
|
|
5793
|
+
const table = /(?:from|into|update)\s+([A-Za-z_]\w*)/i.exec(sql)?.[1] ?? "?";
|
|
5794
|
+
return `${verb}:${table}`;
|
|
5795
|
+
}
|
|
5796
|
+
function resolveSlowMs() {
|
|
5797
|
+
const raw = process.env.THREADBASE_DB_SLOW_QUERY_MS;
|
|
5798
|
+
if (raw === void 0 || raw === "") return DEFAULT_SLOW_QUERY_MS;
|
|
5799
|
+
const parsed = Number(raw);
|
|
5800
|
+
return Number.isFinite(parsed) ? parsed : DEFAULT_SLOW_QUERY_MS;
|
|
5801
|
+
}
|
|
5802
|
+
function record(label, ms, rows, slowMs) {
|
|
5803
|
+
if (slowMs > 0 && ms >= slowMs) {
|
|
5804
|
+
log3.warn(
|
|
5805
|
+
`[db] slow query ${label} ${ms.toFixed(1)}ms rows=${rows}`,
|
|
5806
|
+
{ event: "db.slow_query", stmt: label, ms: Math.round(ms * 100) / 100, rows },
|
|
5807
|
+
"pino"
|
|
5808
|
+
);
|
|
5809
|
+
return;
|
|
5810
|
+
}
|
|
5811
|
+
if (log3.pino.isLevelEnabled("debug")) {
|
|
5812
|
+
log3.debug(
|
|
5813
|
+
`[db] ${label} ${ms.toFixed(2)}ms rows=${rows}`,
|
|
5814
|
+
{ event: "db.query", stmt: label, ms: Math.round(ms * 100) / 100, rows },
|
|
5815
|
+
"pino"
|
|
5816
|
+
);
|
|
5817
|
+
}
|
|
5818
|
+
}
|
|
5819
|
+
function rowsOf(method, result) {
|
|
5820
|
+
if (method === "all") return Array.isArray(result) ? result.length : 0;
|
|
5821
|
+
if (method === "run") return result?.changes ?? 0;
|
|
5822
|
+
return result === void 0 ? 0 : 1;
|
|
5823
|
+
}
|
|
5824
|
+
function instrumentDatabase(db, options = {}) {
|
|
5825
|
+
const slowMs = options.slowMs ?? resolveSlowMs();
|
|
5826
|
+
const prepare = db.prepare.bind(db);
|
|
5827
|
+
db.prepare = ((sql) => {
|
|
5828
|
+
const stmt = prepare(sql);
|
|
5829
|
+
const box = { label: deriveLabel(sql) };
|
|
5830
|
+
Object.defineProperty(stmt, LABEL, { value: box, configurable: true });
|
|
5831
|
+
for (const method of ["get", "all", "run"]) {
|
|
5832
|
+
const original = stmt[method].bind(stmt);
|
|
5833
|
+
Object.defineProperty(stmt, method, {
|
|
5834
|
+
configurable: true,
|
|
5835
|
+
writable: true,
|
|
5836
|
+
value: (...args) => {
|
|
5837
|
+
const started = performance.now();
|
|
5838
|
+
const result = original(...args);
|
|
5839
|
+
record(box.label, performance.now() - started, rowsOf(method, result), slowMs);
|
|
5840
|
+
return result;
|
|
5841
|
+
}
|
|
5842
|
+
});
|
|
5843
|
+
}
|
|
5844
|
+
return stmt;
|
|
5845
|
+
});
|
|
5846
|
+
return db;
|
|
5847
|
+
}
|
|
5848
|
+
function labelStatements(statements) {
|
|
5849
|
+
for (const [name, stmt] of Object.entries(statements)) {
|
|
5850
|
+
const box = stmt?.[LABEL];
|
|
5851
|
+
if (box) box.label = name;
|
|
5852
|
+
}
|
|
5853
|
+
}
|
|
5854
|
+
|
|
5313
5855
|
// src/db/sqlite-migrate.ts
|
|
5314
5856
|
import { readdirSync as readdirSync2, readFileSync as readFileSync7 } from "fs";
|
|
5315
5857
|
import { dirname as dirname6, join as join12 } from "path";
|
|
@@ -5519,6 +6061,7 @@ CREATE TABLE IF NOT EXISTS session_names (
|
|
|
5519
6061
|
updated_at INTEGER NOT NULL
|
|
5520
6062
|
);
|
|
5521
6063
|
`;
|
|
6064
|
+
var cacheLog = getLogger("cache");
|
|
5522
6065
|
var ConversationCache = class _ConversationCache {
|
|
5523
6066
|
db;
|
|
5524
6067
|
tailSize;
|
|
@@ -5748,6 +6291,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
5748
6291
|
"SELECT COUNT(*) as cnt FROM conversation_message_index WHERE conversation_id = ?"
|
|
5749
6292
|
)
|
|
5750
6293
|
};
|
|
6294
|
+
labelStatements(this.stmts);
|
|
5751
6295
|
}
|
|
5752
6296
|
/**
|
|
5753
6297
|
* Expose the underlying handle so projects/cache_metadata repositories can
|
|
@@ -5911,6 +6455,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
5911
6455
|
return walk;
|
|
5912
6456
|
}
|
|
5913
6457
|
async runBackfill(filePath) {
|
|
6458
|
+
const startedAt = performance.now();
|
|
5914
6459
|
const convId = _ConversationCache.conversationIdForFile(filePath);
|
|
5915
6460
|
this.deleteFileIndex(filePath, convId);
|
|
5916
6461
|
this.indexParseState.delete(filePath);
|
|
@@ -5971,6 +6516,18 @@ var ConversationCache = class _ConversationCache {
|
|
|
5971
6516
|
last_message_index: nextIndex - 1
|
|
5972
6517
|
});
|
|
5973
6518
|
this.indexParseState.set(filePath, state);
|
|
6519
|
+
const ms = Math.round(performance.now() - startedAt);
|
|
6520
|
+
cacheLog.info(
|
|
6521
|
+
`[cache] offset-index backfilled ${convId} ${ms}ms`,
|
|
6522
|
+
{
|
|
6523
|
+
event: "offset_index.backfill_ok",
|
|
6524
|
+
conversationId: convId,
|
|
6525
|
+
ms,
|
|
6526
|
+
rows: nextIndex,
|
|
6527
|
+
bytes: stat3.size
|
|
6528
|
+
},
|
|
6529
|
+
"pino"
|
|
6530
|
+
);
|
|
5974
6531
|
}
|
|
5975
6532
|
/**
|
|
5976
6533
|
* Windowed detail read straight from the offset index — the hot path.
|
|
@@ -6056,7 +6613,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
6056
6613
|
}
|
|
6057
6614
|
static open(dbPath, tailSize = 10, migrationsDir, options) {
|
|
6058
6615
|
mkdirSync3(dirname7(dbPath), { recursive: true });
|
|
6059
|
-
const db = new Database(dbPath);
|
|
6616
|
+
const db = instrumentDatabase(new Database(dbPath));
|
|
6060
6617
|
db.pragma("journal_mode = WAL");
|
|
6061
6618
|
db.pragma("foreign_keys = ON");
|
|
6062
6619
|
return new _ConversationCache(db, tailSize, migrationsDir, options);
|
|
@@ -7068,7 +7625,7 @@ var RuntimeStore = class _RuntimeStore {
|
|
|
7068
7625
|
}
|
|
7069
7626
|
db;
|
|
7070
7627
|
static open(dbPath, migrationsDir) {
|
|
7071
|
-
const db = new Database2(dbPath);
|
|
7628
|
+
const db = instrumentDatabase(new Database2(dbPath));
|
|
7072
7629
|
db.pragma("journal_mode = WAL");
|
|
7073
7630
|
runSqliteMigrations(db, migrationsDir ?? resolveMigrationsDir("runtime-migrations"));
|
|
7074
7631
|
return new _RuntimeStore(db);
|
|
@@ -7200,14 +7757,14 @@ var PairTokenStore = class {
|
|
|
7200
7757
|
};
|
|
7201
7758
|
}
|
|
7202
7759
|
consume(token) {
|
|
7203
|
-
const
|
|
7204
|
-
if (!
|
|
7205
|
-
if (Date.now() >
|
|
7760
|
+
const record2 = this.current;
|
|
7761
|
+
if (!record2 || record2.token !== token) return { ok: false, reason: "unknown" };
|
|
7762
|
+
if (Date.now() > record2.expiresAt) {
|
|
7206
7763
|
this.current = null;
|
|
7207
7764
|
return { ok: false, reason: "expired" };
|
|
7208
7765
|
}
|
|
7209
|
-
if (
|
|
7210
|
-
|
|
7766
|
+
if (record2.used) return { ok: false, reason: "used" };
|
|
7767
|
+
record2.used = true;
|
|
7211
7768
|
return { ok: true };
|
|
7212
7769
|
}
|
|
7213
7770
|
peek() {
|
|
@@ -7228,6 +7785,88 @@ var PairTokenStore = class {
|
|
|
7228
7785
|
}
|
|
7229
7786
|
};
|
|
7230
7787
|
|
|
7788
|
+
// src/pty-host/spawn-host.ts
|
|
7789
|
+
import { spawn as spawn2 } from "child_process";
|
|
7790
|
+
|
|
7791
|
+
// src/pty-host/socket.ts
|
|
7792
|
+
import { createConnection, createServer } from "net";
|
|
7793
|
+
import { homedir as homedir7 } from "os";
|
|
7794
|
+
import { join as join14 } from "path";
|
|
7795
|
+
function hostSocketPath(instanceId) {
|
|
7796
|
+
if (process.platform === "win32") {
|
|
7797
|
+
return `\\\\.\\pipe\\threadbase-pty-host-${instanceId}`;
|
|
7798
|
+
}
|
|
7799
|
+
const dir = process.env.THREADBASE_CONFIG_DIR ?? join14(homedir7(), ".threadbase");
|
|
7800
|
+
return join14(dir, "run", `pty-host-${instanceId}.sock`);
|
|
7801
|
+
}
|
|
7802
|
+
function socketTransport(socket) {
|
|
7803
|
+
socket.setEncoding("utf8");
|
|
7804
|
+
return {
|
|
7805
|
+
send(line) {
|
|
7806
|
+
socket.write(line);
|
|
7807
|
+
},
|
|
7808
|
+
onLine(handler) {
|
|
7809
|
+
socket.on("data", (chunk) => handler(chunk));
|
|
7810
|
+
},
|
|
7811
|
+
onClose(handler) {
|
|
7812
|
+
let handled = false;
|
|
7813
|
+
const handleClose = () => {
|
|
7814
|
+
if (handled) return;
|
|
7815
|
+
handled = true;
|
|
7816
|
+
handler();
|
|
7817
|
+
};
|
|
7818
|
+
socket.once("close", handleClose);
|
|
7819
|
+
socket.once("error", handleClose);
|
|
7820
|
+
},
|
|
7821
|
+
close() {
|
|
7822
|
+
socket.destroy();
|
|
7823
|
+
}
|
|
7824
|
+
};
|
|
7825
|
+
}
|
|
7826
|
+
function connectToHost(socketPath) {
|
|
7827
|
+
return new Promise((resolve2, reject) => {
|
|
7828
|
+
const socket = createConnection(socketPath);
|
|
7829
|
+
socket.once("error", reject);
|
|
7830
|
+
socket.once("connect", () => {
|
|
7831
|
+
socket.removeListener("error", reject);
|
|
7832
|
+
resolve2(socketTransport(socket));
|
|
7833
|
+
});
|
|
7834
|
+
});
|
|
7835
|
+
}
|
|
7836
|
+
|
|
7837
|
+
// src/pty-host/spawn-host.ts
|
|
7838
|
+
var HOST_READY_TIMEOUT_MS = 5e3;
|
|
7839
|
+
var HOST_POLL_INTERVAL_MS = 50;
|
|
7840
|
+
async function connectOrSpawnHost(options) {
|
|
7841
|
+
const socketPath = hostSocketPath(options.instanceId);
|
|
7842
|
+
try {
|
|
7843
|
+
return await connectToHost(socketPath);
|
|
7844
|
+
} catch {
|
|
7845
|
+
}
|
|
7846
|
+
spawnDetachedHost(socketPath, options.entryPoint);
|
|
7847
|
+
const deadline = Date.now() + (options.timeoutMs ?? HOST_READY_TIMEOUT_MS);
|
|
7848
|
+
let lastError;
|
|
7849
|
+
while (Date.now() < deadline) {
|
|
7850
|
+
try {
|
|
7851
|
+
return await connectToHost(socketPath);
|
|
7852
|
+
} catch (err) {
|
|
7853
|
+
lastError = err;
|
|
7854
|
+
await new Promise((r) => setTimeout(r, HOST_POLL_INTERVAL_MS));
|
|
7855
|
+
}
|
|
7856
|
+
}
|
|
7857
|
+
throw new Error(
|
|
7858
|
+
`pty-host did not accept a connection on ${socketPath} within ${options.timeoutMs ?? HOST_READY_TIMEOUT_MS}ms` + (lastError instanceof Error ? `: ${lastError.message}` : "")
|
|
7859
|
+
);
|
|
7860
|
+
}
|
|
7861
|
+
function spawnDetachedHost(socketPath, entryPoint) {
|
|
7862
|
+
const child = spawn2(
|
|
7863
|
+
process.execPath,
|
|
7864
|
+
[entryPoint ?? process.argv[1], "pty-host", "--socket", socketPath],
|
|
7865
|
+
{ detached: true, stdio: "ignore" }
|
|
7866
|
+
);
|
|
7867
|
+
child.unref();
|
|
7868
|
+
}
|
|
7869
|
+
|
|
7231
7870
|
// src/seal.ts
|
|
7232
7871
|
import nacl from "tweetnacl";
|
|
7233
7872
|
import naclUtil from "tweetnacl-util";
|
|
@@ -7263,11 +7902,11 @@ import { existsSync as existsSync9 } from "fs";
|
|
|
7263
7902
|
|
|
7264
7903
|
// src/services/cache-integrity/alertStore.ts
|
|
7265
7904
|
import { mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
|
|
7266
|
-
import { homedir as
|
|
7267
|
-
import { dirname as dirname8, join as
|
|
7905
|
+
import { homedir as homedir8 } from "os";
|
|
7906
|
+
import { dirname as dirname8, join as join15 } from "path";
|
|
7268
7907
|
function alertStatePath() {
|
|
7269
|
-
const dir = process.env.THREADBASE_CONFIG_DIR ??
|
|
7270
|
-
return
|
|
7908
|
+
const dir = process.env.THREADBASE_CONFIG_DIR ?? join15(homedir8(), ".threadbase");
|
|
7909
|
+
return join15(dir, "cache-alert.json");
|
|
7271
7910
|
}
|
|
7272
7911
|
function loadAlertState() {
|
|
7273
7912
|
try {
|
|
@@ -7286,7 +7925,7 @@ function saveAlertState(state) {
|
|
|
7286
7925
|
|
|
7287
7926
|
// src/services/cache-integrity/backup.ts
|
|
7288
7927
|
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync4, statSync as statSync5, unlinkSync } from "fs";
|
|
7289
|
-
import { join as
|
|
7928
|
+
import { join as join16 } from "path";
|
|
7290
7929
|
var DEFAULT_RETAIN = 3;
|
|
7291
7930
|
function retainCount() {
|
|
7292
7931
|
const parsed = Number.parseInt(process.env.THREADBASE_CACHE_BACKUP_RETAIN ?? "", 10);
|
|
@@ -7297,13 +7936,13 @@ function timestamp(d) {
|
|
|
7297
7936
|
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
7298
7937
|
}
|
|
7299
7938
|
async function backupCacheDb(db, cacheDir) {
|
|
7300
|
-
const backupsDir =
|
|
7939
|
+
const backupsDir = join16(cacheDir, "backups");
|
|
7301
7940
|
mkdirSync5(backupsDir, { recursive: true });
|
|
7302
|
-
const destPath =
|
|
7941
|
+
const destPath = join16(backupsDir, `cache-${timestamp(/* @__PURE__ */ new Date())}.db`);
|
|
7303
7942
|
await db.backup(destPath);
|
|
7304
7943
|
const retain = retainCount();
|
|
7305
7944
|
const backups = readdirSync4(backupsDir).filter((f) => f.startsWith("cache-") && f.endsWith(".db")).map((f) => {
|
|
7306
|
-
const full =
|
|
7945
|
+
const full = join16(backupsDir, f);
|
|
7307
7946
|
return { full, mtime: statSync5(full).mtimeMs };
|
|
7308
7947
|
}).sort((a, b) => b.mtime - a.mtime);
|
|
7309
7948
|
for (const stale of backups.slice(retain)) {
|
|
@@ -7326,10 +7965,10 @@ function fingerprintOf(ids) {
|
|
|
7326
7965
|
return `sha256:${createHash3("sha256").update(sorted.join("\n")).digest("hex")}`;
|
|
7327
7966
|
}
|
|
7328
7967
|
var CacheIntegrityMonitor = class {
|
|
7329
|
-
constructor(cache, wsHub,
|
|
7968
|
+
constructor(cache, wsHub, log8, cacheDir, rescan, runDuringReset) {
|
|
7330
7969
|
this.cache = cache;
|
|
7331
7970
|
this.wsHub = wsHub;
|
|
7332
|
-
this.log =
|
|
7971
|
+
this.log = log8;
|
|
7333
7972
|
this.cacheDir = cacheDir;
|
|
7334
7973
|
this.rescan = rescan;
|
|
7335
7974
|
this.runDuringReset = runDuringReset;
|
|
@@ -7887,9 +8526,9 @@ function refreshConversationCache(deps) {
|
|
|
7887
8526
|
|
|
7888
8527
|
// src/services/conversations/shouldRefreshProjectsFromHdd.ts
|
|
7889
8528
|
import { readdirSync as readdirSync5, statSync as statSync7 } from "fs";
|
|
7890
|
-
import { homedir as
|
|
7891
|
-
import { join as
|
|
7892
|
-
var DEFAULT_PROJECTS_DIR =
|
|
8529
|
+
import { homedir as homedir9 } from "os";
|
|
8530
|
+
import { join as join17 } from "path";
|
|
8531
|
+
var DEFAULT_PROJECTS_DIR = join17(homedir9(), ".claude", "projects");
|
|
7893
8532
|
function maxProjectsTreeMtimeMs(projectsDir) {
|
|
7894
8533
|
let maxMs;
|
|
7895
8534
|
try {
|
|
@@ -7901,7 +8540,7 @@ function maxProjectsTreeMtimeMs(projectsDir) {
|
|
|
7901
8540
|
for (const ent of readdirSync5(projectsDir, { withFileTypes: true })) {
|
|
7902
8541
|
if (!ent.isDirectory()) continue;
|
|
7903
8542
|
try {
|
|
7904
|
-
const childMs = statSync7(
|
|
8543
|
+
const childMs = statSync7(join17(projectsDir, ent.name)).mtimeMs;
|
|
7905
8544
|
if (childMs > maxMs) maxMs = childMs;
|
|
7906
8545
|
} catch {
|
|
7907
8546
|
}
|
|
@@ -7945,7 +8584,7 @@ function deriveProjectChatTitle(input) {
|
|
|
7945
8584
|
// src/services/push/apnsClient.ts
|
|
7946
8585
|
import { createSign } from "crypto";
|
|
7947
8586
|
import { connect, constants } from "http2";
|
|
7948
|
-
var
|
|
8587
|
+
var log4 = getLogger("apns");
|
|
7949
8588
|
var APNS_HOST_SANDBOX = "api.sandbox.push.apple.com";
|
|
7950
8589
|
var APNS_MAX_PAYLOAD_BYTES = 4096;
|
|
7951
8590
|
var JWT_TTL_SECONDS = 3e3;
|
|
@@ -8029,7 +8668,7 @@ var ApnsClient = class {
|
|
|
8029
8668
|
}
|
|
8030
8669
|
const session = connect(`https://${this.creds.host}`);
|
|
8031
8670
|
session.on("error", (err) => {
|
|
8032
|
-
|
|
8671
|
+
log4.warn("apns.session_error", { event: "apns.session_error", err: String(err) });
|
|
8033
8672
|
});
|
|
8034
8673
|
this.session = session;
|
|
8035
8674
|
return session;
|
|
@@ -8114,7 +8753,7 @@ function truncateLastOutput(raw) {
|
|
|
8114
8753
|
}
|
|
8115
8754
|
|
|
8116
8755
|
// src/services/push/liveActivityNotifier.ts
|
|
8117
|
-
var
|
|
8756
|
+
var log5 = getLogger("live-activity");
|
|
8118
8757
|
function contentStateForSession(args) {
|
|
8119
8758
|
const status = toLiveActivityStatus(args.session.status);
|
|
8120
8759
|
if (!status) return null;
|
|
@@ -8174,7 +8813,7 @@ var LiveActivityNotifier = class {
|
|
|
8174
8813
|
}
|
|
8175
8814
|
await this.maybeSendName(session);
|
|
8176
8815
|
} catch (err) {
|
|
8177
|
-
|
|
8816
|
+
log5.error("live_activity.notify_failed", {
|
|
8178
8817
|
event: "live_activity.notify_failed",
|
|
8179
8818
|
sessionId: session.id,
|
|
8180
8819
|
status: session.status,
|
|
@@ -8196,7 +8835,7 @@ var LiveActivityNotifier = class {
|
|
|
8196
8835
|
});
|
|
8197
8836
|
this.openActivity.set(session.id, { sessionNameSent: session.sessionName != null });
|
|
8198
8837
|
if (outcome.attempted > 0) {
|
|
8199
|
-
|
|
8838
|
+
log5.info("live_activity.updated", {
|
|
8200
8839
|
event: "live_activity.updated",
|
|
8201
8840
|
sessionId: session.id,
|
|
8202
8841
|
status: contentState.status,
|
|
@@ -8220,7 +8859,7 @@ var LiveActivityNotifier = class {
|
|
|
8220
8859
|
});
|
|
8221
8860
|
open2.sessionNameSent = true;
|
|
8222
8861
|
if (outcome.attempted > 0) {
|
|
8223
|
-
|
|
8862
|
+
log5.info("live_activity.updated", {
|
|
8224
8863
|
event: "live_activity.updated",
|
|
8225
8864
|
sessionId: session.id,
|
|
8226
8865
|
status: contentState.status,
|
|
@@ -8239,7 +8878,7 @@ var LiveActivityNotifier = class {
|
|
|
8239
8878
|
if (!contentState) return;
|
|
8240
8879
|
const outcome = await this.sender.end({ sessionId: session.id, contentState });
|
|
8241
8880
|
if (outcome.attempted > 0) {
|
|
8242
|
-
|
|
8881
|
+
log5.info("live_activity.ended", {
|
|
8243
8882
|
event: "live_activity.ended",
|
|
8244
8883
|
sessionId: session.id,
|
|
8245
8884
|
...outcome
|
|
@@ -8253,7 +8892,7 @@ var LiveActivityNotifier = class {
|
|
|
8253
8892
|
};
|
|
8254
8893
|
|
|
8255
8894
|
// src/services/push/liveActivitySender.ts
|
|
8256
|
-
var
|
|
8895
|
+
var log6 = getLogger("live-activity");
|
|
8257
8896
|
var ACTIVITY_MAX_LIFETIME_MS = 8 * 60 * 60 * 1e3;
|
|
8258
8897
|
function buildActivityKitPayload(args) {
|
|
8259
8898
|
return {
|
|
@@ -8317,7 +8956,7 @@ var LiveActivitySender = class {
|
|
|
8317
8956
|
);
|
|
8318
8957
|
for (const { row, result, error } of results) {
|
|
8319
8958
|
if (error) {
|
|
8320
|
-
|
|
8959
|
+
log6.error("live_activity.send_failed", {
|
|
8321
8960
|
event: "live_activity.send_failed",
|
|
8322
8961
|
sessionId: args.sessionId,
|
|
8323
8962
|
activityId: row.activity_id,
|
|
@@ -8338,7 +8977,7 @@ var LiveActivitySender = class {
|
|
|
8338
8977
|
this.repo.expire(row.token, now);
|
|
8339
8978
|
outcome.retired += 1;
|
|
8340
8979
|
}
|
|
8341
|
-
|
|
8980
|
+
log6.warn("live_activity.send_rejected", {
|
|
8342
8981
|
event: "live_activity.send_rejected",
|
|
8343
8982
|
sessionId: args.sessionId,
|
|
8344
8983
|
activityId: row.activity_id,
|
|
@@ -8383,7 +9022,7 @@ var LiveActivitySender = class {
|
|
|
8383
9022
|
};
|
|
8384
9023
|
|
|
8385
9024
|
// src/services/push/liveActivityRenewal.ts
|
|
8386
|
-
var
|
|
9025
|
+
var log7 = getLogger("live-activity");
|
|
8387
9026
|
var RENEWAL_LEAD_MS = 30 * 60 * 1e3;
|
|
8388
9027
|
var MAX_TIMER_MS = 60 * 60 * 1e3;
|
|
8389
9028
|
function renewalDueAt(row) {
|
|
@@ -8431,7 +9070,7 @@ var LiveActivityRenewalScheduler = class {
|
|
|
8431
9070
|
await this.renew(row, now);
|
|
8432
9071
|
}
|
|
8433
9072
|
} catch (err) {
|
|
8434
|
-
|
|
9073
|
+
log7.error("live_activity.renewal_sweep_failed", {
|
|
8435
9074
|
event: "live_activity.renewal_sweep_failed",
|
|
8436
9075
|
err: String(err)
|
|
8437
9076
|
});
|
|
@@ -8460,7 +9099,7 @@ var LiveActivityRenewalScheduler = class {
|
|
|
8460
9099
|
if (!session || !status) {
|
|
8461
9100
|
this.deps.repo.claimRenewal(row.token, now);
|
|
8462
9101
|
this.deps.repo.expire(row.token, now);
|
|
8463
|
-
|
|
9102
|
+
log7.info("live_activity.renewal_skipped", {
|
|
8464
9103
|
event: "live_activity.renewal_skipped",
|
|
8465
9104
|
sessionId: row.session_id,
|
|
8466
9105
|
activityId: row.activity_id,
|
|
@@ -8495,7 +9134,7 @@ var LiveActivityRenewalScheduler = class {
|
|
|
8495
9134
|
startedAt,
|
|
8496
9135
|
now
|
|
8497
9136
|
});
|
|
8498
|
-
|
|
9137
|
+
log7.info("live_activity.renewed", {
|
|
8499
9138
|
event: "live_activity.renewed",
|
|
8500
9139
|
sessionId: session.id,
|
|
8501
9140
|
activityId: row.activity_id,
|
|
@@ -8505,7 +9144,7 @@ var LiveActivityRenewalScheduler = class {
|
|
|
8505
9144
|
replacementRequested: started
|
|
8506
9145
|
});
|
|
8507
9146
|
} catch (err) {
|
|
8508
|
-
|
|
9147
|
+
log7.error("live_activity.renewal_failed", {
|
|
8509
9148
|
event: "live_activity.renewal_failed",
|
|
8510
9149
|
sessionId: session.id,
|
|
8511
9150
|
activityId: row.activity_id,
|
|
@@ -8996,6 +9635,12 @@ var SessionStore = class {
|
|
|
8996
9635
|
removeManaged(sessionId) {
|
|
8997
9636
|
return this.managed.delete(sessionId);
|
|
8998
9637
|
}
|
|
9638
|
+
/**
|
|
9639
|
+
* The **live** stored record — mutating it mutates the store. Paired with
|
|
9640
|
+
* `get()`, which hands back a throwaway response copy. Prefer
|
|
9641
|
+
* `updateManaged()` for writes; this is for readers that need the internal
|
|
9642
|
+
* shape (Date fields, `rehydrated`, …) rather than the wire shape.
|
|
9643
|
+
*/
|
|
8999
9644
|
getManaged(sessionId) {
|
|
9000
9645
|
return this.managed.get(sessionId) ?? null;
|
|
9001
9646
|
}
|
|
@@ -9005,12 +9650,21 @@ var SessionStore = class {
|
|
|
9005
9650
|
this.discovered.set(proc.pid, proc);
|
|
9006
9651
|
}
|
|
9007
9652
|
}
|
|
9653
|
+
/**
|
|
9654
|
+
* The **live** stored records — mutating an element mutates the store. Paired
|
|
9655
|
+
* with `list()`, which hands back throwaway response copies.
|
|
9656
|
+
*/
|
|
9008
9657
|
listManaged() {
|
|
9009
9658
|
return Array.from(this.managed.values());
|
|
9010
9659
|
}
|
|
9011
9660
|
// Build the session list: live PTY sessions (managed) merged with externally
|
|
9012
9661
|
// discovered Claude processes. Managed sessions keyed by JSONL UUID take
|
|
9013
9662
|
// priority — discovered processes with the same UUID are skipped.
|
|
9663
|
+
//
|
|
9664
|
+
// Returns freshly constructed response objects, NOT references into the
|
|
9665
|
+
// store — hence `Readonly`: writing to one changes nothing, so the compiler
|
|
9666
|
+
// refuses it. Persist state with `updateManaged()`; to decorate a response,
|
|
9667
|
+
// build a new object (`{ ...s, … }`) as `withReconciledLifecycle` does.
|
|
9014
9668
|
list(ptyAttachedIds) {
|
|
9015
9669
|
const results = [];
|
|
9016
9670
|
const seenIds = /* @__PURE__ */ new Set();
|
|
@@ -9026,6 +9680,9 @@ var SessionStore = class {
|
|
|
9026
9680
|
}
|
|
9027
9681
|
return results;
|
|
9028
9682
|
}
|
|
9683
|
+
// A freshly constructed response object, NOT a reference into the store —
|
|
9684
|
+
// hence `Readonly`, for the same reason as `list()` above. Use `getManaged()`
|
|
9685
|
+
// when you want the live record.
|
|
9029
9686
|
get(sessionId, ptyAttachedIds) {
|
|
9030
9687
|
const managed = this.managed.get(sessionId);
|
|
9031
9688
|
if (managed) return managedToResponse(managed, ptyAttachedIds.has(sessionId));
|
|
@@ -9117,8 +9774,8 @@ function managedToResponse(s, ptyAttached) {
|
|
|
9117
9774
|
// behind it — `resumable`, and `historical` rather than `managed`
|
|
9118
9775
|
// (docs/plans/live-sessions-persistence-plan.md §4, Phase 1).
|
|
9119
9776
|
lifecycle: ptyAttached ? "attached" : s.rehydrated ? "resumable" : s.failureReason != null ? "failed" : "completed",
|
|
9120
|
-
lifecycleSource: ptyAttached ? "spawn" : s.rehydrated ? "reconcile" : "exit",
|
|
9121
|
-
// We
|
|
9777
|
+
lifecycleSource: ptyAttached ? s.reconciled ? "reconcile" : "spawn" : s.rehydrated ? "reconcile" : "exit",
|
|
9778
|
+
// We own its PTY, so `status` is the authoritative signal — no inferred
|
|
9122
9779
|
// `activity` is attached for managed sessions.
|
|
9123
9780
|
ownership: s.rehydrated ? "historical" : "managed",
|
|
9124
9781
|
projectPath: s.projectPath,
|
|
@@ -9193,7 +9850,7 @@ function discoveredToResponse(d, conversationId) {
|
|
|
9193
9850
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
9194
9851
|
import { mkdir as mkdir3, writeFile } from "fs/promises";
|
|
9195
9852
|
import heicConvert from "heic-convert";
|
|
9196
|
-
import { join as
|
|
9853
|
+
import { join as join18 } from "path";
|
|
9197
9854
|
var UPLOAD_DIR_NAME = ".threadbase-uploads";
|
|
9198
9855
|
var MAX_BYTES = 25 * 1024 * 1024;
|
|
9199
9856
|
var HEIC_MIMES = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
|
|
@@ -9226,9 +9883,9 @@ async function saveUploadFile(input) {
|
|
|
9226
9883
|
}
|
|
9227
9884
|
const id = `up_${randomBytes4(8).toString("hex")}`;
|
|
9228
9885
|
const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
|
|
9229
|
-
const dir =
|
|
9886
|
+
const dir = join18(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
|
|
9230
9887
|
await mkdir3(dir, { recursive: true });
|
|
9231
|
-
const filePath =
|
|
9888
|
+
const filePath = join18(dir, `${Date.now()}-${id}-${safeName}`);
|
|
9232
9889
|
await writeFile(filePath, buffer);
|
|
9233
9890
|
return {
|
|
9234
9891
|
id,
|
|
@@ -9770,7 +10427,7 @@ var StreamerServer = class {
|
|
|
9770
10427
|
this.autoResumeOnBoot = config.autoResumeOnBoot ?? false;
|
|
9771
10428
|
this.scannerPersistenceDisabled = config.scannerPersistent === false;
|
|
9772
10429
|
this.scanProfiles = config.scanProfiles;
|
|
9773
|
-
this.codexRoots = config.codexRoots ?? [
|
|
10430
|
+
this.codexRoots = config.codexRoots ?? [join19(homedir10(), ".codex", "sessions")];
|
|
9774
10431
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
9775
10432
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
9776
10433
|
this.featureFlags = resolveFeatureFlags({ cli: config.featureFlags, yaml: loadFeatureFlags() });
|
|
@@ -9784,8 +10441,8 @@ var StreamerServer = class {
|
|
|
9784
10441
|
this.claudeFlagsPersistable = config.claudeFlags === void 0;
|
|
9785
10442
|
this.claudeFlags = config.claudeFlags ?? loadClaudeFlags();
|
|
9786
10443
|
this.claudeExtraArgs = config.claudeExtraArgs ?? loadClaudeExtraArgs();
|
|
9787
|
-
this.cacheDir = config.cacheDir ?? loadCacheDir() ??
|
|
9788
|
-
this.runtimeDbPath = config.runtimeDbPath ?? process.env.THREADBASE_RUNTIME_DB ??
|
|
10444
|
+
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join19(homedir10(), ".threadbase", "cache");
|
|
10445
|
+
this.runtimeDbPath = config.runtimeDbPath ?? process.env.THREADBASE_RUNTIME_DB ?? join19(process.env.THREADBASE_CONFIG_DIR ?? join19(homedir10(), ".threadbase"), "runtime.db");
|
|
9789
10446
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
9790
10447
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
9791
10448
|
this.markScannerStaleDebounced = debounce(() => {
|
|
@@ -10041,7 +10698,7 @@ var StreamerServer = class {
|
|
|
10041
10698
|
temporalClient,
|
|
10042
10699
|
taskQueue: agentConfig.temporal.taskQueue
|
|
10043
10700
|
});
|
|
10044
|
-
const conversationsBaseDir = agentConfig.conversationsDir ||
|
|
10701
|
+
const conversationsBaseDir = agentConfig.conversationsDir || join19(dirname9(this.cacheDir), "conversations");
|
|
10045
10702
|
conversationWriter = createConversationWriter({
|
|
10046
10703
|
baseDir: conversationsBaseDir
|
|
10047
10704
|
});
|
|
@@ -10194,7 +10851,7 @@ var StreamerServer = class {
|
|
|
10194
10851
|
conversationWriter,
|
|
10195
10852
|
agentConfig
|
|
10196
10853
|
};
|
|
10197
|
-
this.httpServer =
|
|
10854
|
+
this.httpServer = createServer2((req, res) => this.handleRequest(req, res));
|
|
10198
10855
|
this.httpServer.on("clientError", (_err, socket) => {
|
|
10199
10856
|
try {
|
|
10200
10857
|
socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
|
|
@@ -10348,7 +11005,7 @@ var StreamerServer = class {
|
|
|
10348
11005
|
if (!this.managedSessionsRepo) return [];
|
|
10349
11006
|
let verdicts = [];
|
|
10350
11007
|
try {
|
|
10351
|
-
const rows = this.managedSessionsRepo.listNonTerminal();
|
|
11008
|
+
const rows = this.managedSessionsRepo.listNonTerminal().filter((row) => !this.ptyManager.hasSession(row.session_id));
|
|
10352
11009
|
if (rows.length === PROBE_SET_MAX) {
|
|
10353
11010
|
this.log.warn(
|
|
10354
11011
|
`[reconcile] probe set hit its cap of ${PROBE_SET_MAX} \u2014 older rows skipped`,
|
|
@@ -10595,6 +11252,26 @@ var StreamerServer = class {
|
|
|
10595
11252
|
if (session.provider !== CODEX_CLI_PROVIDER) return session.id;
|
|
10596
11253
|
return session.boundConversationId ?? session.projectPath;
|
|
10597
11254
|
}
|
|
11255
|
+
/** Restore registry-only metadata after the host mirror has been adopted. */
|
|
11256
|
+
refreshHostedSessionsFromRegistry() {
|
|
11257
|
+
for (const session of this.ptyManager.listSessions()) {
|
|
11258
|
+
const row = this.managedSessionsRepo?.get(session.id);
|
|
11259
|
+
const merged = {
|
|
11260
|
+
...session,
|
|
11261
|
+
reconciled: true,
|
|
11262
|
+
...row?.project_id != null && { projectId: row.project_id },
|
|
11263
|
+
...row?.session_name != null && { sessionName: row.session_name },
|
|
11264
|
+
...row?.bound_conversation_id != null && {
|
|
11265
|
+
boundConversationId: row.bound_conversation_id
|
|
11266
|
+
},
|
|
11267
|
+
...row?.resumed_from_conversation_id != null && {
|
|
11268
|
+
resumedFromConversationId: row.resumed_from_conversation_id
|
|
11269
|
+
}
|
|
11270
|
+
};
|
|
11271
|
+
this.sessionStore.addManaged(merged);
|
|
11272
|
+
void this.watchConversationFile(session.id, merged.boundConversationId ?? session.id);
|
|
11273
|
+
}
|
|
11274
|
+
}
|
|
10598
11275
|
/**
|
|
10599
11276
|
* Mirror a freshly-spawned session into the durable registry (C1 Phase 2).
|
|
10600
11277
|
*
|
|
@@ -10690,6 +11367,7 @@ var StreamerServer = class {
|
|
|
10690
11367
|
* of waiting on the interval.
|
|
10691
11368
|
*/
|
|
10692
11369
|
reapIdleSessions(now = Date.now()) {
|
|
11370
|
+
if (this.ptyManager.isRemote()) return [];
|
|
10693
11371
|
const reaped = [];
|
|
10694
11372
|
for (const session of this.ptyManager.listSessions()) {
|
|
10695
11373
|
if (session.status === "running") continue;
|
|
@@ -10791,6 +11469,40 @@ var StreamerServer = class {
|
|
|
10791
11469
|
return true;
|
|
10792
11470
|
}
|
|
10793
11471
|
async listen(port, opts) {
|
|
11472
|
+
if (this.featureFlags.ptyHost) {
|
|
11473
|
+
try {
|
|
11474
|
+
let sessions = null;
|
|
11475
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
11476
|
+
const transport = await connectOrSpawnHost({
|
|
11477
|
+
instanceId: process.env.THREADBASE_INSTANCE_ID ?? hostname3()
|
|
11478
|
+
});
|
|
11479
|
+
try {
|
|
11480
|
+
sessions = await this.ptyManager.useRemoteRunner(transport);
|
|
11481
|
+
break;
|
|
11482
|
+
} catch (err) {
|
|
11483
|
+
if (!(err instanceof PtyHostProtocolMismatchError) || attempt > 0) throw err;
|
|
11484
|
+
this.log.info(`[pty-host] replaced incompatible protocol ${err.hostVersion}`, {
|
|
11485
|
+
event: "pty_host.protocol_replaced",
|
|
11486
|
+
hostVersion: err.hostVersion,
|
|
11487
|
+
streamerVersion: err.streamerVersion
|
|
11488
|
+
});
|
|
11489
|
+
}
|
|
11490
|
+
}
|
|
11491
|
+
if (!sessions) throw new Error("pty-host replacement did not produce a compatible host");
|
|
11492
|
+
for (const session of sessions) {
|
|
11493
|
+
this.sessionStore.addManaged({ ...session, reconciled: true });
|
|
11494
|
+
}
|
|
11495
|
+
this.log.info(`[pty-host] re-adopted ${sessions.length} live session(s)`, {
|
|
11496
|
+
event: "pty_host.sessions_adopted",
|
|
11497
|
+
sessions: sessions.length
|
|
11498
|
+
});
|
|
11499
|
+
} catch (err) {
|
|
11500
|
+
this.log.error(
|
|
11501
|
+
"[pty-host] could not attach; falling back to in-process PTYs for this run",
|
|
11502
|
+
{ event: "pty_host.attach_failed", err }
|
|
11503
|
+
);
|
|
11504
|
+
}
|
|
11505
|
+
}
|
|
10794
11506
|
const dbConfig = this.disableDb ? null : getDbConfig();
|
|
10795
11507
|
if (dbConfig) {
|
|
10796
11508
|
this.dbPool = await createPool(dbConfig);
|
|
@@ -10805,8 +11517,10 @@ var StreamerServer = class {
|
|
|
10805
11517
|
this.log.info("Database migrations applied", { event: "db.migrations_applied" });
|
|
10806
11518
|
}
|
|
10807
11519
|
await this.bindWithRetry(port);
|
|
10808
|
-
|
|
10809
|
-
|
|
11520
|
+
if (!this.ptyManager.isRemote()) {
|
|
11521
|
+
this.idleReaperTimer = setInterval(() => this.reapIdleSessions(), IDLE_REAP_SWEEP_MS);
|
|
11522
|
+
this.idleReaperTimer.unref?.();
|
|
11523
|
+
}
|
|
10810
11524
|
const warmUp = new Promise((resolveWarm) => {
|
|
10811
11525
|
{
|
|
10812
11526
|
this.log.info(`Streamer server listening on port ${port}`, {
|
|
@@ -10824,9 +11538,18 @@ var StreamerServer = class {
|
|
|
10824
11538
|
{ error: message, abiMismatch, path: this.runtimeDbPath, event: "runtime.open_failed" }
|
|
10825
11539
|
);
|
|
10826
11540
|
}
|
|
11541
|
+
if (this.ptyManager.isRemote()) {
|
|
11542
|
+
this.ptyManager.startRemoteHeartbeat(() => {
|
|
11543
|
+
if (!this.managedSessionsRepo) {
|
|
11544
|
+
return { registryState: "unknown", referencedSessionIds: [] };
|
|
11545
|
+
}
|
|
11546
|
+
const referencedSessionIds = this.ptyManager.listSessions().filter((session) => this.managedSessionsRepo?.get(session.id)?.completed_at == null).map((session) => session.id);
|
|
11547
|
+
return { registryState: "known", referencedSessionIds };
|
|
11548
|
+
});
|
|
11549
|
+
}
|
|
10827
11550
|
try {
|
|
10828
11551
|
this.cache = ConversationCache.open(
|
|
10829
|
-
|
|
11552
|
+
join19(this.cacheDir, "cache.db"),
|
|
10830
11553
|
this.tailSize,
|
|
10831
11554
|
void 0,
|
|
10832
11555
|
{
|
|
@@ -10900,6 +11623,7 @@ var StreamerServer = class {
|
|
|
10900
11623
|
);
|
|
10901
11624
|
this.scannerPersistenceDisabled = true;
|
|
10902
11625
|
}
|
|
11626
|
+
if (this.ptyManager.isRemote()) this.refreshHostedSessionsFromRegistry();
|
|
10903
11627
|
void this.reconcilePreviousSessions().then(async (v) => {
|
|
10904
11628
|
const recoverableRows = this.rehydratePreviousSessions(v);
|
|
10905
11629
|
await this.autoResumePreviousSessions(recoverableRows);
|
|
@@ -11105,7 +11829,8 @@ var StreamerServer = class {
|
|
|
11105
11829
|
}
|
|
11106
11830
|
this.lastAgentChunkAt.clear();
|
|
11107
11831
|
this.terminalSeq.clear();
|
|
11108
|
-
this.
|
|
11832
|
+
if (this.ptyManager.isRemote()) this.ptyManager.dispose();
|
|
11833
|
+
else this.recordShutdownState();
|
|
11109
11834
|
this.markScannerStaleDebounced.cancel();
|
|
11110
11835
|
await Promise.all([...this.inFlightCacheWrites]);
|
|
11111
11836
|
await Promise.all([...this.allScanners].map((s) => s.close()));
|
|
@@ -11113,7 +11838,7 @@ var StreamerServer = class {
|
|
|
11113
11838
|
this.scanner = null;
|
|
11114
11839
|
this.cache?.close();
|
|
11115
11840
|
this.runtimeStore?.close();
|
|
11116
|
-
this.ptyManager.dispose();
|
|
11841
|
+
if (!this.ptyManager.isRemote()) this.ptyManager.dispose();
|
|
11117
11842
|
this.fileWatcher.dispose();
|
|
11118
11843
|
this.externalTails.clear();
|
|
11119
11844
|
this.wsHub.dispose();
|
|
@@ -11339,9 +12064,9 @@ var StreamerServer = class {
|
|
|
11339
12064
|
/** Project roots watched for conversation JSONLs (profiles or ~/.claude/projects). */
|
|
11340
12065
|
projectsDirsForFreshnessCheck() {
|
|
11341
12066
|
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
11342
|
-
return this.scanProfiles.filter((p) => p.enabled).map((p) =>
|
|
12067
|
+
return this.scanProfiles.filter((p) => p.enabled).map((p) => join19(p.configDir, "projects"));
|
|
11343
12068
|
}
|
|
11344
|
-
return [
|
|
12069
|
+
return [join19(homedir10(), ".claude", "projects")];
|
|
11345
12070
|
}
|
|
11346
12071
|
/**
|
|
11347
12072
|
* Full-glob scan + cache upsert/delete reconcile. Used by ?refresh=1 and by
|
|
@@ -11742,21 +12467,21 @@ var StreamerServer = class {
|
|
|
11742
12467
|
*/
|
|
11743
12468
|
projectsDirs() {
|
|
11744
12469
|
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
11745
|
-
return this.scanProfiles.filter((p) => p.enabled).map((p) =>
|
|
12470
|
+
return this.scanProfiles.filter((p) => p.enabled).map((p) => join19(p.configDir, "projects"));
|
|
11746
12471
|
}
|
|
11747
|
-
return [
|
|
12472
|
+
return [join19(homedir10(), ".claude", "projects")];
|
|
11748
12473
|
}
|
|
11749
12474
|
findJsonlPath(uuid) {
|
|
11750
12475
|
const filename = `${uuid}.jsonl`;
|
|
11751
12476
|
for (const projectsDir of this.projectsDirs()) {
|
|
11752
12477
|
if (!existsSync11(projectsDir)) continue;
|
|
11753
12478
|
for (const dir of readdirSync6(projectsDir)) {
|
|
11754
|
-
const fp =
|
|
12479
|
+
const fp = join19(projectsDir, dir, filename);
|
|
11755
12480
|
if (existsSync11(fp)) return fp;
|
|
11756
|
-
const projectDir =
|
|
12481
|
+
const projectDir = join19(projectsDir, dir);
|
|
11757
12482
|
try {
|
|
11758
12483
|
for (const sub of readdirSync6(projectDir)) {
|
|
11759
|
-
const subagentPath =
|
|
12484
|
+
const subagentPath = join19(projectDir, sub, "subagents", filename);
|
|
11760
12485
|
if (existsSync11(subagentPath)) return subagentPath;
|
|
11761
12486
|
}
|
|
11762
12487
|
} catch {
|
|
@@ -12163,6 +12888,16 @@ var StreamerServer = class {
|
|
|
12163
12888
|
const windowStart = Math.max(0, beforeIndex - scanLimit);
|
|
12164
12889
|
const indexWindow = scanLimit > 0 && !hasAnchor && indexFilePath && this.cache ? this.cache.readMessageWindow(indexFilePath, windowStart, beforeIndex) : null;
|
|
12165
12890
|
if (!indexWindow && indexFilePath && this.cache && !hasAnchor) {
|
|
12891
|
+
this.log.info(
|
|
12892
|
+
`[server] offset-index miss ${id} \u2192 scanner fallback`,
|
|
12893
|
+
{
|
|
12894
|
+
event: "offset_index.miss",
|
|
12895
|
+
conversationId: id,
|
|
12896
|
+
fromIndex: windowStart,
|
|
12897
|
+
toIndex: beforeIndex
|
|
12898
|
+
},
|
|
12899
|
+
"pino"
|
|
12900
|
+
);
|
|
12166
12901
|
this.trackCacheWrite(
|
|
12167
12902
|
this.cache.backfillIndex(indexFilePath).catch((err) => {
|
|
12168
12903
|
this.log.warn("offset-index.backfill_failed", {
|
|
@@ -12437,14 +13172,15 @@ var StreamerServer = class {
|
|
|
12437
13172
|
}
|
|
12438
13173
|
async handleGetSession(sessionId, res) {
|
|
12439
13174
|
if (this.rejectIfWarmingUp(res)) return;
|
|
12440
|
-
const
|
|
12441
|
-
if (
|
|
12442
|
-
|
|
12443
|
-
|
|
12444
|
-
|
|
12445
|
-
|
|
12446
|
-
|
|
12447
|
-
|
|
13175
|
+
const base = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
13176
|
+
if (base) {
|
|
13177
|
+
const reconciled = this.withReconciledLifecycle([base])[0];
|
|
13178
|
+
const session = {
|
|
13179
|
+
...base,
|
|
13180
|
+
...existsSync11(base.projectPath) ? {} : { failureReason: `Project directory not found: ${base.projectPath}` },
|
|
13181
|
+
lifecycle: reconciled.lifecycle,
|
|
13182
|
+
lifecycleSource: reconciled.lifecycleSource
|
|
13183
|
+
};
|
|
12448
13184
|
if (this.ptyManager.hasSession(sessionId)) {
|
|
12449
13185
|
try {
|
|
12450
13186
|
const lines = await this.ptyManager.getOutputLines(sessionId, 10);
|
|
@@ -12607,31 +13343,37 @@ var StreamerServer = class {
|
|
|
12607
13343
|
this.sessionStore.addManaged(session);
|
|
12608
13344
|
this.recordSessionSpawn(session);
|
|
12609
13345
|
void this.watchConversationFile(sessionId, historyId);
|
|
12610
|
-
const response = this.sessionStore.get(session.id, this.ptyAttachedIds());
|
|
12611
13346
|
this.enrichResumedSessionAsync(sessionId, projectPath, conv);
|
|
13347
|
+
const response = this.sessionStore.get(session.id, this.ptyAttachedIds());
|
|
12612
13348
|
return { ok: true, alreadyRunning: false, session, response };
|
|
12613
13349
|
}
|
|
12614
13350
|
enrichResumedSessionAsync(sessionId, projectPath, conv) {
|
|
12615
13351
|
try {
|
|
12616
|
-
|
|
12617
|
-
if (!session) return;
|
|
13352
|
+
if (!this.sessionStore.getManaged(sessionId)) return;
|
|
12618
13353
|
if (conv) {
|
|
12619
|
-
|
|
12620
|
-
|
|
12621
|
-
|
|
12622
|
-
|
|
13354
|
+
this.sessionStore.updateManaged(sessionId, {
|
|
13355
|
+
sessionName: conv.sessionName ?? void 0,
|
|
13356
|
+
messageCount: conv.messageCount ?? 0,
|
|
13357
|
+
account: conv.account ?? void 0,
|
|
13358
|
+
filePath: conv.filePath ?? void 0
|
|
13359
|
+
});
|
|
12623
13360
|
}
|
|
12624
13361
|
if (!this.cache || !this.projectsRepo || !this.conversationsRepo) return;
|
|
12625
13362
|
const cached3 = this.cache.getMetaById(sessionId);
|
|
12626
13363
|
if (cached3) {
|
|
12627
|
-
session.model = cached3.model ?? void 0;
|
|
12628
|
-
session.preview = cached3.preview ?? void 0;
|
|
12629
13364
|
const first = cached3.firstMessage ? JSON.parse(cached3.firstMessage) : null;
|
|
12630
13365
|
const last = cached3.lastMessage ? JSON.parse(cached3.lastMessage) : null;
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
13366
|
+
this.sessionStore.updateManaged(sessionId, {
|
|
13367
|
+
model: cached3.model ?? void 0,
|
|
13368
|
+
preview: cached3.preview ?? void 0,
|
|
13369
|
+
firstMessageText: first?.text ?? void 0,
|
|
13370
|
+
// parseIsoDateOrNull, not `new Date()`: an unparseable cached
|
|
13371
|
+
// timestamp must land as absent, not as an Invalid Date that
|
|
13372
|
+
// managedToResponse would throw on when it calls .toISOString().
|
|
13373
|
+
firstMessageAt: parseIsoDateOrNull(first?.timestamp) ?? void 0,
|
|
13374
|
+
lastMessageText: last?.text ?? void 0,
|
|
13375
|
+
lastMessageAt: parseIsoDateOrNull(last?.timestamp) ?? void 0
|
|
13376
|
+
});
|
|
12635
13377
|
}
|
|
12636
13378
|
let resolvedProjectId = cached3?.projectId ?? null;
|
|
12637
13379
|
if (!resolvedProjectId) {
|
|
@@ -12643,8 +13385,10 @@ var StreamerServer = class {
|
|
|
12643
13385
|
});
|
|
12644
13386
|
}
|
|
12645
13387
|
if (resolvedProjectId) {
|
|
12646
|
-
|
|
12647
|
-
|
|
13388
|
+
this.sessionStore.updateManaged(sessionId, {
|
|
13389
|
+
projectId: resolvedProjectId,
|
|
13390
|
+
resumedFromConversationId: sessionId
|
|
13391
|
+
});
|
|
12648
13392
|
}
|
|
12649
13393
|
} catch (err) {
|
|
12650
13394
|
console.error(`[enrichResumedSessionAsync] ${sessionId}:`, err);
|
|
@@ -13075,7 +13819,7 @@ var StreamerServer = class {
|
|
|
13075
13819
|
sessionStore: this.sessionStore,
|
|
13076
13820
|
// biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
|
|
13077
13821
|
agentClient: this.agentClient,
|
|
13078
|
-
conversationsDir: this.cacheDir ?
|
|
13822
|
+
conversationsDir: this.cacheDir ? join19(dirname9(this.cacheDir), "conversations") : "",
|
|
13079
13823
|
agentConfig: this.agentConfig
|
|
13080
13824
|
});
|
|
13081
13825
|
json(res, result.status, result.body);
|
|
@@ -13239,9 +13983,9 @@ var StreamerServer = class {
|
|
|
13239
13983
|
// was passed to Claude via --session-id so the filename matches from the start.
|
|
13240
13984
|
watchForJsonl(sessionId, projectPath) {
|
|
13241
13985
|
const encoded = projectPath.replace(/[/\\:.]/g, "-");
|
|
13242
|
-
const projectsDir =
|
|
13986
|
+
const projectsDir = join19(homedir10(), ".claude", "projects", encoded);
|
|
13243
13987
|
const expectedFile = `${sessionId}.jsonl`;
|
|
13244
|
-
const filePath =
|
|
13988
|
+
const filePath = join19(projectsDir, expectedFile);
|
|
13245
13989
|
const deadline = Date.now() + 12e4;
|
|
13246
13990
|
let watcher = null;
|
|
13247
13991
|
const cleanup = () => {
|
|
@@ -13263,10 +14007,10 @@ var StreamerServer = class {
|
|
|
13263
14007
|
if (!resolvedFilePath && existsSync11(projectsDir)) {
|
|
13264
14008
|
try {
|
|
13265
14009
|
const now = Date.now();
|
|
13266
|
-
const match = readdirSync6(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync9(
|
|
13267
|
-
({ f }) => basename5(f, ".jsonl") === sessionId || this.readFirstLineSessionId(
|
|
14010
|
+
const match = readdirSync6(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync9(join19(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
|
|
14011
|
+
({ f }) => basename5(f, ".jsonl") === sessionId || this.readFirstLineSessionId(join19(projectsDir, f)) === sessionId
|
|
13268
14012
|
).sort((a, b) => b.mtime - a.mtime)[0];
|
|
13269
|
-
if (match) resolvedFilePath =
|
|
14013
|
+
if (match) resolvedFilePath = join19(projectsDir, match.f);
|
|
13270
14014
|
} catch {
|
|
13271
14015
|
}
|
|
13272
14016
|
}
|
|
@@ -13314,7 +14058,7 @@ var StreamerServer = class {
|
|
|
13314
14058
|
watchForCodexRollout(sessionId, projectPath) {
|
|
13315
14059
|
const deadline = Date.now() + 12e4;
|
|
13316
14060
|
const now = /* @__PURE__ */ new Date();
|
|
13317
|
-
const dateDir =
|
|
14061
|
+
const dateDir = join19(
|
|
13318
14062
|
String(now.getFullYear()),
|
|
13319
14063
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
13320
14064
|
String(now.getDate()).padStart(2, "0")
|
|
@@ -13355,7 +14099,7 @@ var StreamerServer = class {
|
|
|
13355
14099
|
this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
|
|
13356
14100
|
);
|
|
13357
14101
|
for (const root of this.codexRoots) {
|
|
13358
|
-
const sessionsDir =
|
|
14102
|
+
const sessionsDir = join19(root, dateDir);
|
|
13359
14103
|
if (!existsSync11(sessionsDir)) continue;
|
|
13360
14104
|
let candidateFiles;
|
|
13361
14105
|
try {
|
|
@@ -13364,9 +14108,9 @@ var StreamerServer = class {
|
|
|
13364
14108
|
continue;
|
|
13365
14109
|
}
|
|
13366
14110
|
const nowMs = Date.now();
|
|
13367
|
-
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync9(
|
|
14111
|
+
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync9(join19(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
|
|
13368
14112
|
for (const { f } of recentCandidates) {
|
|
13369
|
-
const candidatePath =
|
|
14113
|
+
const candidatePath = join19(sessionsDir, f);
|
|
13370
14114
|
const match = matchesProjectPath(candidatePath);
|
|
13371
14115
|
if (!match) continue;
|
|
13372
14116
|
if (boundElsewhere.has(match.id)) continue;
|