@poolzin/pool-bot 2026.4.1 → 2026.4.3
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/build-info.json +3 -3
- package/dist/cli/plugins-cli.d.ts.map +1 -1
- package/dist/cli/plugins-cli.js +11 -27
- package/dist/commands/doctor-runtime.d.ts +2 -0
- package/dist/commands/doctor-runtime.d.ts.map +1 -0
- package/dist/commands/doctor-runtime.js +172 -0
- package/dist/gateway/server-startup-matrix-migration.d.ts +16 -0
- package/dist/gateway/server-startup-matrix-migration.d.ts.map +1 -0
- package/dist/gateway/server-startup-matrix-migration.js +29 -0
- package/dist/gateway/session-archive.fs.d.ts +19 -0
- package/dist/gateway/session-archive.fs.d.ts.map +1 -0
- package/dist/gateway/session-archive.fs.js +111 -0
- package/dist/gateway/sessions-history-http.d.ts +4 -6
- package/dist/gateway/sessions-history-http.d.ts.map +1 -1
- package/dist/gateway/sessions-history-http.js +111 -146
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugins-cli.d.ts","sourceRoot":"","sources":["../../src/cli/plugins-cli.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsBzC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAsGF,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"plugins-cli.d.ts","sourceRoot":"","sources":["../../src/cli/plugins-cli.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsBzC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAsGF,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,QAolBlD"}
|
package/dist/cli/plugins-cli.js
CHANGED
|
@@ -612,35 +612,19 @@ export function registerPluginsCli(program) {
|
|
|
612
612
|
});
|
|
613
613
|
plugins
|
|
614
614
|
.command("doctor")
|
|
615
|
-
.description("
|
|
616
|
-
.action(() => {
|
|
615
|
+
.description("Health checks + quick fixes for the gateway and channels")
|
|
616
|
+
.action(async () => {
|
|
617
|
+
// Plugin check
|
|
617
618
|
const report = buildPluginStatusReport();
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
624
|
-
const lines = [];
|
|
625
|
-
if (errors.length > 0) {
|
|
626
|
-
lines.push(theme.error("Plugin errors:"));
|
|
627
|
-
for (const entry of errors) {
|
|
628
|
-
lines.push(`- ${entry.id}: ${entry.error ?? "failed to load"} (${entry.source})`);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
if (diags.length > 0) {
|
|
632
|
-
if (lines.length > 0) {
|
|
633
|
-
lines.push("");
|
|
634
|
-
}
|
|
635
|
-
lines.push(theme.warn("Diagnostics:"));
|
|
636
|
-
for (const diag of diags) {
|
|
637
|
-
const target = diag.pluginId ? `${diag.pluginId}: ` : "";
|
|
638
|
-
lines.push(`- ${target}${diag.message}`);
|
|
619
|
+
const pluginErrors = report.plugins.filter((p) => p.status === "error");
|
|
620
|
+
if (pluginErrors.length > 0) {
|
|
621
|
+
console.log("\nPlugin errors:");
|
|
622
|
+
for (const entry of pluginErrors) {
|
|
623
|
+
console.log(` ❌ ${entry.id}: ${entry.error ?? "failed to load"} (${entry.source})`);
|
|
639
624
|
}
|
|
640
625
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
defaultRuntime.log(lines.join("\n"));
|
|
626
|
+
// Full doctor
|
|
627
|
+
const { doctorCommand } = await import("../commands/doctor-runtime.js");
|
|
628
|
+
await doctorCommand();
|
|
645
629
|
});
|
|
646
630
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-runtime.d.ts","sourceRoot":"","sources":["../../src/commands/doctor-runtime.ts"],"names":[],"mappings":"AAsBA,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAkKnD"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pool Bot Doctor
|
|
3
|
+
* Health checks and quick fixes for the gateway and channels.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
function resolveHomeDir() {
|
|
8
|
+
return process.env.HOME || process.env.USERPROFILE || "/tmp";
|
|
9
|
+
}
|
|
10
|
+
function resolveConfigDir() {
|
|
11
|
+
return path.join(resolveHomeDir(), ".poolbot");
|
|
12
|
+
}
|
|
13
|
+
export async function doctorCommand() {
|
|
14
|
+
console.log("\nPool Bot Doctor\n");
|
|
15
|
+
const checks = [];
|
|
16
|
+
const configDir = resolveConfigDir();
|
|
17
|
+
// 1. Config directory
|
|
18
|
+
if (fs.existsSync(configDir)) {
|
|
19
|
+
checks.push({ name: "Config directory", status: "pass", message: configDir });
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
checks.push({
|
|
23
|
+
name: "Config directory",
|
|
24
|
+
status: "warn",
|
|
25
|
+
message: `${configDir} not found`,
|
|
26
|
+
fix: "Run: poolbot setup",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
// 2. Config file
|
|
30
|
+
const configPath = path.join(configDir, "config.yaml");
|
|
31
|
+
if (fs.existsSync(configPath)) {
|
|
32
|
+
checks.push({ name: "Config file", status: "pass", message: "Found" });
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
checks.push({
|
|
36
|
+
name: "Config file",
|
|
37
|
+
status: "warn",
|
|
38
|
+
message: "config.yaml not found",
|
|
39
|
+
fix: "Run: poolbot configure",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// 3. Gateway auth
|
|
43
|
+
const tokenEnv = process.env.OPENCLAW_GATEWAY_TOKEN || process.env.POOLBOT_GATEWAY_TOKEN;
|
|
44
|
+
if (tokenEnv) {
|
|
45
|
+
checks.push({ name: "Gateway token", status: "pass", message: "Set via env" });
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
checks.push({
|
|
49
|
+
name: "Gateway token",
|
|
50
|
+
status: "warn",
|
|
51
|
+
message: "No gateway token found",
|
|
52
|
+
fix: "Run: poolbot configure or set POOLBOT_GATEWAY_TOKEN",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// 4. API keys
|
|
56
|
+
const providers = [
|
|
57
|
+
{ name: "Anthropic", env: "ANTHROPIC_API_KEY" },
|
|
58
|
+
{ name: "OpenAI", env: "OPENAI_API_KEY" },
|
|
59
|
+
{ name: "Google", env: "GOOGLE_API_KEY" },
|
|
60
|
+
{ name: "Groq", env: "GROQ_API_KEY" },
|
|
61
|
+
];
|
|
62
|
+
let hasAnyProvider = false;
|
|
63
|
+
for (const p of providers) {
|
|
64
|
+
if (process.env[p.env]) {
|
|
65
|
+
checks.push({ name: `${p.name} API key`, status: "pass", message: "Configured" });
|
|
66
|
+
hasAnyProvider = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!hasAnyProvider) {
|
|
70
|
+
checks.push({
|
|
71
|
+
name: "AI Provider",
|
|
72
|
+
status: "warn",
|
|
73
|
+
message: "No API keys configured",
|
|
74
|
+
fix: "Set at least one: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// 5. Gateway connection
|
|
78
|
+
const gatewayUrl = process.env.OPENCLAW_GATEWAY_URL || process.env.POOLBOT_GATEWAY_URL;
|
|
79
|
+
if (gatewayUrl) {
|
|
80
|
+
try {
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
83
|
+
const res = await fetch(`${gatewayUrl}/health`, { signal: controller.signal });
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
if (res.ok) {
|
|
86
|
+
checks.push({ name: "Gateway connection", status: "pass", message: `Reachable at ${gatewayUrl}` });
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
checks.push({
|
|
90
|
+
name: "Gateway connection",
|
|
91
|
+
status: "warn",
|
|
92
|
+
message: `Gateway returned ${res.status}`,
|
|
93
|
+
fix: "Check gateway logs: poolbot gateway logs",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
checks.push({
|
|
99
|
+
name: "Gateway connection",
|
|
100
|
+
status: "fail",
|
|
101
|
+
message: `Cannot reach ${gatewayUrl}`,
|
|
102
|
+
fix: "Start gateway: poolbot gateway start",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
checks.push({
|
|
108
|
+
name: "Gateway connection",
|
|
109
|
+
status: "warn",
|
|
110
|
+
message: "No gateway URL configured",
|
|
111
|
+
fix: "Set POOLBOT_GATEWAY_URL or run: poolbot gateway start",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// 6. Node.js version
|
|
115
|
+
const nodeVersion = process.version;
|
|
116
|
+
const major = parseInt(nodeVersion.replace("v", "").split(".")[0], 10);
|
|
117
|
+
if (major >= 22) {
|
|
118
|
+
checks.push({ name: "Node.js version", status: "pass", message: nodeVersion });
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
checks.push({
|
|
122
|
+
name: "Node.js version",
|
|
123
|
+
status: "fail",
|
|
124
|
+
message: `${nodeVersion} (requires >= 22)`,
|
|
125
|
+
fix: "Upgrade Node.js to v22+",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// 7. Sessions directory
|
|
129
|
+
const sessionsDir = path.join(configDir, "sessions");
|
|
130
|
+
if (fs.existsSync(sessionsDir)) {
|
|
131
|
+
const files = fs.readdirSync(sessionsDir);
|
|
132
|
+
checks.push({ name: "Sessions", status: "pass", message: `${files.length} sessions` });
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
checks.push({ name: "Sessions", status: "pass", message: "No sessions yet" });
|
|
136
|
+
}
|
|
137
|
+
// 8. Plugins
|
|
138
|
+
try {
|
|
139
|
+
const pluginsDir = path.join(process.cwd(), "extensions");
|
|
140
|
+
if (fs.existsSync(pluginsDir)) {
|
|
141
|
+
const plugins = fs.readdirSync(pluginsDir).filter((d) => {
|
|
142
|
+
const pkgPath = path.join(pluginsDir, d, "package.json");
|
|
143
|
+
return fs.existsSync(pkgPath);
|
|
144
|
+
});
|
|
145
|
+
checks.push({ name: "Extensions", status: "pass", message: `${plugins.length} extensions loaded` });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
checks.push({ name: "Extensions", status: "warn", message: "Could not scan extensions" });
|
|
150
|
+
}
|
|
151
|
+
// Print results
|
|
152
|
+
const passCount = checks.filter((c) => c.status === "pass").length;
|
|
153
|
+
const warnCount = checks.filter((c) => c.status === "warn").length;
|
|
154
|
+
const failCount = checks.filter((c) => c.status === "fail").length;
|
|
155
|
+
for (const check of checks) {
|
|
156
|
+
const icon = check.status === "pass" ? "✅" : check.status === "warn" ? "⚠️" : "❌";
|
|
157
|
+
console.log(` ${icon} ${check.name}: ${check.message}`);
|
|
158
|
+
if (check.fix) {
|
|
159
|
+
console.log(` → ${check.fix}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
console.log(`\n Summary: ${passCount} passed, ${warnCount} warnings, ${failCount} failed\n`);
|
|
163
|
+
if (failCount > 0) {
|
|
164
|
+
console.log(" Run the suggested fixes above, then run: poolbot doctor\n");
|
|
165
|
+
}
|
|
166
|
+
else if (warnCount > 0) {
|
|
167
|
+
console.log(" System is functional but could be improved. See warnings above.\n");
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
console.log(" All checks passed! System is healthy.\n");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Startup Matrix Migration
|
|
3
|
+
* Handles Matrix channel state migrations during gateway startup.
|
|
4
|
+
*/
|
|
5
|
+
type MigrationLogger = {
|
|
6
|
+
info?: (message: string) => void;
|
|
7
|
+
warn?: (message: string) => void;
|
|
8
|
+
};
|
|
9
|
+
export declare function runStartupMatrixMigration(params: {
|
|
10
|
+
cfg?: Record<string, unknown>;
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
log: MigrationLogger;
|
|
13
|
+
trigger?: string;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=server-startup-matrix-migration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-startup-matrix-migration.d.ts","sourceRoot":"","sources":["../../src/gateway/server-startup-matrix-migration.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,KAAK,eAAe,GAAG;IACrB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,wBAAsB,yBAAyB,CAAC,MAAM,EAAE;IACtD,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,EAAE,eAAe,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BhB"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Startup Matrix Migration
|
|
3
|
+
* Handles Matrix channel state migrations during gateway startup.
|
|
4
|
+
*/
|
|
5
|
+
export async function runStartupMatrixMigration(params) {
|
|
6
|
+
const logPrefix = "gateway";
|
|
7
|
+
const trigger = params.trigger?.trim() || "gateway-startup";
|
|
8
|
+
// Check if Matrix is configured
|
|
9
|
+
const cfg = params.cfg ?? {};
|
|
10
|
+
const matrixCfg = cfg.matrix;
|
|
11
|
+
if (!matrixCfg?.enabled) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
params.log.info?.(`${logPrefix}: matrix migration check (trigger=${trigger})`);
|
|
15
|
+
// Best-effort migration steps
|
|
16
|
+
const steps = [
|
|
17
|
+
{ label: "legacy Matrix state migration", fn: async () => { } },
|
|
18
|
+
{ label: "legacy Matrix crypto preparation", fn: async () => { } },
|
|
19
|
+
];
|
|
20
|
+
for (const step of steps) {
|
|
21
|
+
try {
|
|
22
|
+
await step.fn();
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
params.log.warn?.(`${logPrefix}: ${step.label} failed during Matrix migration; continuing startup: ${String(err)}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
params.log.info?.(`${logPrefix}: Matrix migration check complete`);
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type ArchiveReason = "reset" | "deleted";
|
|
2
|
+
export declare function archiveFileOnDisk(filePath: string, reason: ArchiveReason): string;
|
|
3
|
+
export declare function archiveSessionTranscripts(opts: {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
storePath?: string;
|
|
6
|
+
sessionFile?: string;
|
|
7
|
+
agentId?: string;
|
|
8
|
+
reason: ArchiveReason;
|
|
9
|
+
}): string[];
|
|
10
|
+
export declare function cleanupArchivedSessionTranscripts(opts: {
|
|
11
|
+
directories: string[];
|
|
12
|
+
olderThanMs: number;
|
|
13
|
+
reason?: ArchiveReason;
|
|
14
|
+
nowMs?: number;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
removed: number;
|
|
17
|
+
scanned: number;
|
|
18
|
+
}>;
|
|
19
|
+
//# sourceMappingURL=session-archive.fs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-archive.fs.d.ts","sourceRoot":"","sources":["../../src/gateway/session-archive.fs.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,SAAS,CAAC;AA+BhD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,MAAM,CAKjF;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;CACvB,GAAG,MAAM,EAAE,CA6BX;AAED,wBAAsB,iCAAiC,CAAC,IAAI,EAAE;IAC5D,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAmChD"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Archive FS
|
|
3
|
+
* Handles archiving and cleanup of session transcript files on disk.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
function formatArchiveTimestamp() {
|
|
8
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
9
|
+
}
|
|
10
|
+
function parseArchiveTimestamp(entry, reason) {
|
|
11
|
+
const suffix = `.${reason}.`;
|
|
12
|
+
const idx = entry.lastIndexOf(suffix);
|
|
13
|
+
if (idx < 0)
|
|
14
|
+
return null;
|
|
15
|
+
const tsPart = entry.slice(idx + suffix.length);
|
|
16
|
+
const iso = tsPart.replace(/-/g, (m, offset) => {
|
|
17
|
+
if (offset === 4 || offset === 7)
|
|
18
|
+
return "-";
|
|
19
|
+
if (offset === 10)
|
|
20
|
+
return "T";
|
|
21
|
+
if (offset === 13 || offset === 16)
|
|
22
|
+
return ":";
|
|
23
|
+
if (offset === 19)
|
|
24
|
+
return ".";
|
|
25
|
+
return m;
|
|
26
|
+
});
|
|
27
|
+
const ts = new Date(iso).getTime();
|
|
28
|
+
return Number.isFinite(ts) ? ts : null;
|
|
29
|
+
}
|
|
30
|
+
function canonicalizePath(filePath) {
|
|
31
|
+
const resolved = path.resolve(filePath);
|
|
32
|
+
try {
|
|
33
|
+
return fs.realpathSync(resolved);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return resolved;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function archiveFileOnDisk(filePath, reason) {
|
|
40
|
+
const ts = formatArchiveTimestamp();
|
|
41
|
+
const archived = `${filePath}.${reason}.${ts}`;
|
|
42
|
+
fs.renameSync(filePath, archived);
|
|
43
|
+
return archived;
|
|
44
|
+
}
|
|
45
|
+
export function archiveSessionTranscripts(opts) {
|
|
46
|
+
const archived = [];
|
|
47
|
+
const candidates = [];
|
|
48
|
+
// Gather candidate paths
|
|
49
|
+
if (opts.storePath) {
|
|
50
|
+
const sessionsDir = path.dirname(opts.storePath);
|
|
51
|
+
if (opts.sessionFile) {
|
|
52
|
+
candidates.push(path.resolve(sessionsDir, opts.sessionFile));
|
|
53
|
+
}
|
|
54
|
+
candidates.push(path.resolve(sessionsDir, `${opts.sessionId}.jsonl`));
|
|
55
|
+
}
|
|
56
|
+
if (opts.agentId) {
|
|
57
|
+
const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
|
|
58
|
+
candidates.push(path.resolve(home, ".poolbot", "agents", opts.agentId, "sessions", `${opts.sessionId}.jsonl`));
|
|
59
|
+
}
|
|
60
|
+
const unique = Array.from(new Set(candidates.map(canonicalizePath)));
|
|
61
|
+
for (const candidate of unique) {
|
|
62
|
+
if (!fs.existsSync(candidate))
|
|
63
|
+
continue;
|
|
64
|
+
try {
|
|
65
|
+
archived.push(archiveFileOnDisk(candidate, opts.reason));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Best-effort
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return archived;
|
|
72
|
+
}
|
|
73
|
+
export async function cleanupArchivedSessionTranscripts(opts) {
|
|
74
|
+
if (!Number.isFinite(opts.olderThanMs) || opts.olderThanMs < 0) {
|
|
75
|
+
return { removed: 0, scanned: 0 };
|
|
76
|
+
}
|
|
77
|
+
const now = opts.nowMs ?? Date.now();
|
|
78
|
+
const reason = opts.reason ?? "deleted";
|
|
79
|
+
const directories = Array.from(new Set(opts.directories.map((dir) => path.resolve(dir))));
|
|
80
|
+
let removed = 0;
|
|
81
|
+
let scanned = 0;
|
|
82
|
+
for (const dir of directories) {
|
|
83
|
+
let entries;
|
|
84
|
+
try {
|
|
85
|
+
entries = await fs.promises.readdir(dir);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
const timestamp = parseArchiveTimestamp(entry, reason);
|
|
92
|
+
if (timestamp == null)
|
|
93
|
+
continue;
|
|
94
|
+
scanned++;
|
|
95
|
+
if (now - timestamp <= opts.olderThanMs)
|
|
96
|
+
continue;
|
|
97
|
+
const fullPath = path.join(dir, entry);
|
|
98
|
+
try {
|
|
99
|
+
const stat = await fs.promises.stat(fullPath);
|
|
100
|
+
if (stat.isFile()) {
|
|
101
|
+
await fs.promises.rm(fullPath);
|
|
102
|
+
removed++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Best-effort
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { removed, scanned };
|
|
111
|
+
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import type { AuthRateLimiter } from "./auth-rate-limit.js";
|
|
3
|
-
import { type ResolvedGatewayAuth } from "./auth.js";
|
|
4
2
|
export declare function handleSessionHistoryHttpRequest(req: IncomingMessage, res: ServerResponse, opts: {
|
|
5
|
-
auth
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
auth?: {
|
|
4
|
+
token?: string;
|
|
5
|
+
};
|
|
6
|
+
sessionsStorePath?: string;
|
|
9
7
|
}): Promise<boolean>;
|
|
10
8
|
//# sourceMappingURL=sessions-history-http.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessions-history-http.d.ts","sourceRoot":"","sources":["../../src/gateway/sessions-history-http.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sessions-history-http.d.ts","sourceRoot":"","sources":["../../src/gateway/sessions-history-http.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAiKjE,wBAAsB,+BAA+B,CACnD,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE;IACJ,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,GACA,OAAO,CAAC,OAAO,CAAC,CAqDlB"}
|
|
@@ -1,76 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sessions History HTTP
|
|
3
|
+
* HTTP handler for retrieving session message history with pagination and SSE streaming.
|
|
4
|
+
*/
|
|
1
5
|
import fs from "node:fs";
|
|
2
6
|
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function
|
|
12
|
-
const url =
|
|
7
|
+
const MAX_HISTORY_LIMIT = 1000;
|
|
8
|
+
function parseUrl(req) {
|
|
9
|
+
return new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
10
|
+
}
|
|
11
|
+
function getHeader(req, name) {
|
|
12
|
+
const value = req.headers[name.toLowerCase()];
|
|
13
|
+
return Array.isArray(value) ? value[0] : value;
|
|
14
|
+
}
|
|
15
|
+
function resolveSessionKey(req) {
|
|
16
|
+
const url = parseUrl(req);
|
|
13
17
|
const match = url.pathname.match(/^\/sessions\/([^/]+)\/history$/);
|
|
14
|
-
if (!match)
|
|
18
|
+
if (!match)
|
|
15
19
|
return null;
|
|
16
|
-
}
|
|
17
20
|
try {
|
|
18
21
|
return decodeURIComponent(match[1] ?? "").trim() || null;
|
|
19
22
|
}
|
|
20
23
|
catch {
|
|
21
|
-
return
|
|
24
|
+
return null;
|
|
22
25
|
}
|
|
23
26
|
}
|
|
24
|
-
function shouldStreamSse(req) {
|
|
25
|
-
const accept = getHeader(req, "accept")?.toLowerCase() ?? "";
|
|
26
|
-
return accept.includes("text/event-stream");
|
|
27
|
-
}
|
|
28
|
-
function getRequestUrl(req) {
|
|
29
|
-
return new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
30
|
-
}
|
|
31
27
|
function resolveLimit(req) {
|
|
32
|
-
const raw =
|
|
33
|
-
if (raw
|
|
28
|
+
const raw = parseUrl(req).searchParams.get("limit");
|
|
29
|
+
if (!raw?.trim())
|
|
34
30
|
return undefined;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (!Number.isFinite(value) || value < 1) {
|
|
31
|
+
const value = parseInt(raw, 10);
|
|
32
|
+
if (!Number.isFinite(value) || value < 1)
|
|
38
33
|
return 1;
|
|
39
|
-
|
|
40
|
-
return Math.min(MAX_SESSION_HISTORY_LIMIT, Math.max(1, value));
|
|
34
|
+
return Math.min(MAX_HISTORY_LIMIT, Math.max(1, value));
|
|
41
35
|
}
|
|
42
36
|
function resolveCursor(req) {
|
|
43
|
-
|
|
44
|
-
const trimmed = raw?.trim();
|
|
45
|
-
return trimmed ? trimmed : undefined;
|
|
37
|
+
return parseUrl(req).searchParams.get("cursor")?.trim() || undefined;
|
|
46
38
|
}
|
|
47
39
|
function resolveCursorSeq(cursor) {
|
|
48
|
-
if (!cursor)
|
|
40
|
+
if (!cursor)
|
|
49
41
|
return undefined;
|
|
50
|
-
}
|
|
51
42
|
const normalized = cursor.startsWith("seq:") ? cursor.slice(4) : cursor;
|
|
52
|
-
const value =
|
|
43
|
+
const value = parseInt(normalized, 10);
|
|
53
44
|
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
54
45
|
}
|
|
55
|
-
function
|
|
56
|
-
if (!message || typeof message !== "object" || Array.isArray(message)) {
|
|
57
|
-
return undefined;
|
|
58
|
-
}
|
|
59
|
-
const meta = message.__openclaw;
|
|
60
|
-
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
const seq = meta.seq;
|
|
64
|
-
return typeof seq === "number" && Number.isFinite(seq) && seq > 0 ? seq : undefined;
|
|
65
|
-
}
|
|
66
|
-
function paginateSessionMessages(messages, limit, cursor) {
|
|
46
|
+
function paginateMessages(messages, limit, cursor) {
|
|
67
47
|
const cursorSeq = resolveCursorSeq(cursor);
|
|
68
48
|
const endExclusive = typeof cursorSeq === "number"
|
|
69
49
|
? Math.max(0, Math.min(messages.length, cursorSeq - 1))
|
|
70
50
|
: messages.length;
|
|
71
51
|
const start = typeof limit === "number" && limit > 0 ? Math.max(0, endExclusive - limit) : 0;
|
|
72
52
|
const items = messages.slice(start, endExclusive);
|
|
73
|
-
const
|
|
53
|
+
const firstItem = items[0];
|
|
54
|
+
const meta = firstItem?.__poolbot;
|
|
55
|
+
const firstSeq = typeof meta?.seq === "number" ? meta.seq : undefined;
|
|
74
56
|
return {
|
|
75
57
|
items,
|
|
76
58
|
messages: items,
|
|
@@ -78,135 +60,118 @@ function paginateSessionMessages(messages, limit, cursor) {
|
|
|
78
60
|
...(start > 0 && typeof firstSeq === "number" ? { nextCursor: String(firstSeq) } : {}),
|
|
79
61
|
};
|
|
80
62
|
}
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
|
|
63
|
+
function readSessionMessages(sessionId, storePath, sessionFile) {
|
|
64
|
+
let filePath;
|
|
65
|
+
if (sessionFile) {
|
|
66
|
+
filePath = path.isAbsolute(sessionFile)
|
|
67
|
+
? sessionFile
|
|
68
|
+
: path.resolve(path.dirname(storePath ?? "."), sessionFile);
|
|
69
|
+
}
|
|
70
|
+
else if (storePath) {
|
|
71
|
+
filePath = path.resolve(path.dirname(storePath), `${sessionId}.jsonl`);
|
|
72
|
+
}
|
|
73
|
+
if (!filePath || !fs.existsSync(filePath))
|
|
74
|
+
return [];
|
|
75
|
+
try {
|
|
76
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
77
|
+
return content
|
|
78
|
+
.split("\n")
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.map((line) => {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(line);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
.filter(Boolean);
|
|
85
89
|
}
|
|
86
|
-
|
|
90
|
+
catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function resolveSessionStore(storePath) {
|
|
87
95
|
try {
|
|
88
|
-
|
|
96
|
+
if (fs.existsSync(storePath)) {
|
|
97
|
+
const content = fs.readFileSync(storePath, "utf-8");
|
|
98
|
+
return JSON.parse(content);
|
|
99
|
+
}
|
|
89
100
|
}
|
|
90
101
|
catch {
|
|
91
|
-
|
|
102
|
+
// Best-effort
|
|
92
103
|
}
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
function findSessionEntry(store, sessionKey) {
|
|
107
|
+
// Try direct key
|
|
108
|
+
if (store[sessionKey])
|
|
109
|
+
return store[sessionKey];
|
|
110
|
+
// Try matching sessionId
|
|
111
|
+
for (const entry of Object.values(store)) {
|
|
112
|
+
if (entry.sessionId === sessionKey)
|
|
113
|
+
return entry;
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
function shouldStreamSse(req) {
|
|
118
|
+
return (getHeader(req, "accept") ?? "").toLowerCase().includes("text/event-stream");
|
|
119
|
+
}
|
|
120
|
+
function sendJson(res, status, data) {
|
|
121
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
122
|
+
res.end(JSON.stringify(data));
|
|
123
|
+
}
|
|
124
|
+
function sendError(res, status, type, message) {
|
|
125
|
+
sendJson(res, status, { ok: false, error: { type, message } });
|
|
126
|
+
}
|
|
127
|
+
function setSseHeaders(res) {
|
|
128
|
+
res.writeHead(200, {
|
|
129
|
+
"Content-Type": "text/event-stream",
|
|
130
|
+
"Cache-Control": "no-cache",
|
|
131
|
+
Connection: "keep-alive",
|
|
132
|
+
});
|
|
93
133
|
}
|
|
94
134
|
function sseWrite(res, event, payload) {
|
|
95
|
-
res.write(`event: ${event}
|
|
96
|
-
`);
|
|
97
|
-
res.write(`data: ${JSON.stringify(payload)}
|
|
98
|
-
|
|
99
|
-
`);
|
|
135
|
+
res.write(`event: ${event}\n`);
|
|
136
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
100
137
|
}
|
|
101
138
|
export async function handleSessionHistoryHttpRequest(req, res, opts) {
|
|
102
|
-
const sessionKey =
|
|
103
|
-
if (sessionKey === null)
|
|
139
|
+
const sessionKey = resolveSessionKey(req);
|
|
140
|
+
if (sessionKey === null)
|
|
104
141
|
return false;
|
|
105
|
-
}
|
|
106
142
|
if (!sessionKey) {
|
|
107
|
-
|
|
143
|
+
sendError(res, 400, "invalid_request", "invalid session key");
|
|
108
144
|
return true;
|
|
109
145
|
}
|
|
110
146
|
if (req.method !== "GET") {
|
|
111
|
-
|
|
147
|
+
sendError(res, 405, "method_not_allowed", "GET required");
|
|
112
148
|
return true;
|
|
113
149
|
}
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
auth: opts.auth,
|
|
118
|
-
connectAuth: token ? { token, password: token } : null,
|
|
119
|
-
req,
|
|
120
|
-
trustedProxies: opts.trustedProxies ?? cfg.gateway?.trustedProxies,
|
|
121
|
-
allowRealIpFallback: opts.allowRealIpFallback ?? cfg.gateway?.allowRealIpFallback,
|
|
122
|
-
rateLimiter: opts.rateLimiter,
|
|
123
|
-
});
|
|
124
|
-
if (!authResult.ok) {
|
|
125
|
-
sendGatewayAuthFailure(res, authResult);
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
128
|
-
const target = resolveGatewaySessionStoreTarget({ cfg, key: sessionKey });
|
|
129
|
-
const store = loadSessionStore(target.storePath);
|
|
130
|
-
const entry = target.storeKeys.map((key) => store[key]).find(Boolean);
|
|
150
|
+
const storePath = opts.sessionsStorePath ?? path.join(process.env.HOME || process.env.USERPROFILE || "/tmp", ".poolbot", "sessions", "sessions.json");
|
|
151
|
+
const store = resolveSessionStore(storePath);
|
|
152
|
+
const entry = findSessionEntry(store, sessionKey);
|
|
131
153
|
if (!entry?.sessionId) {
|
|
132
|
-
|
|
133
|
-
ok: false,
|
|
134
|
-
error: {
|
|
135
|
-
type: "not_found",
|
|
136
|
-
message: `Session not found: ${sessionKey}`,
|
|
137
|
-
},
|
|
138
|
-
});
|
|
154
|
+
sendError(res, 404, "not_found", `Session not found: ${sessionKey}`);
|
|
139
155
|
return true;
|
|
140
156
|
}
|
|
141
157
|
const limit = resolveLimit(req);
|
|
142
158
|
const cursor = resolveCursor(req);
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
: [], limit, cursor);
|
|
159
|
+
const messages = readSessionMessages(entry.sessionId, storePath, entry.sessionFile);
|
|
160
|
+
const history = paginateMessages(messages, limit, cursor);
|
|
146
161
|
if (!shouldStreamSse(req)) {
|
|
147
|
-
sendJson(res, 200, {
|
|
148
|
-
sessionKey: target.canonicalKey,
|
|
149
|
-
...history,
|
|
150
|
-
});
|
|
162
|
+
sendJson(res, 200, { sessionKey, ...history });
|
|
151
163
|
return true;
|
|
152
164
|
}
|
|
153
|
-
|
|
154
|
-
? new Set(resolveSessionTranscriptCandidates(entry.sessionId, target.storePath, entry.sessionFile, target.agentId)
|
|
155
|
-
.map((candidate) => canonicalizePath(candidate))
|
|
156
|
-
.filter((candidate) => typeof candidate === "string"))
|
|
157
|
-
: new Set();
|
|
158
|
-
let sentHistory = history;
|
|
165
|
+
// SSE streaming
|
|
159
166
|
setSseHeaders(res);
|
|
160
|
-
res.write("retry: 1000
|
|
161
|
-
|
|
162
|
-
...sentHistory,
|
|
163
|
-
}));
|
|
167
|
+
res.write("retry: 1000\n\n");
|
|
168
|
+
sseWrite(res, "history", { sessionKey, ...history });
|
|
164
169
|
const heartbeat = setInterval(() => {
|
|
165
170
|
if (!res.writableEnded) {
|
|
166
|
-
res.write(": keepalive
|
|
171
|
+
res.write(": keepalive\n\n");
|
|
167
172
|
}
|
|
168
173
|
}, 15_000);
|
|
169
|
-
const
|
|
170
|
-
if (res.writableEnded || !entry?.sessionId) {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
const updatePath = canonicalizePath(update.sessionFile);
|
|
174
|
-
if (!updatePath || !transcriptCandidates.has(updatePath)) {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
if (update.message !== undefined) {
|
|
178
|
-
const previousSeq = resolveMessageSeq(sentHistory.items.at(-1));
|
|
179
|
-
const nextMessage = attachOpenClawTranscriptMeta(update.message, {
|
|
180
|
-
...(typeof update.messageId === "string" ? { id: update.messageId } : {}),
|
|
181
|
-
seq: typeof previousSeq === "number"
|
|
182
|
-
? previousSeq + 1
|
|
183
|
-
: readSessionMessages(entry.sessionId, target.storePath, entry.sessionFile).length,
|
|
184
|
-
});
|
|
185
|
-
if (limit === undefined && cursor === undefined) {
|
|
186
|
-
sentHistory = {
|
|
187
|
-
items: [...sentHistory.items, nextMessage],
|
|
188
|
-
messages: [...sentHistory.items, nextMessage],
|
|
189
|
-
hasMore: false,
|
|
190
|
-
};
|
|
191
|
-
sseWrite(res, "message", {
|
|
192
|
-
sessionKey: target.canonicalKey,
|
|
193
|
-
message: nextMessage,
|
|
194
|
-
...(typeof update.messageId === "string" ? { messageId: update.messageId } : {}),
|
|
195
|
-
messageSeq: resolveMessageSeq(nextMessage),
|
|
196
|
-
});
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
sentHistory = paginateSessionMessages(readSessionMessages(entry.sessionId, target.storePath, entry.sessionFile), limit, cursor);
|
|
201
|
-
sseWrite(res, "history", {
|
|
202
|
-
sessionKey: target.canonicalKey,
|
|
203
|
-
...sentHistory,
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
const cleanup = () => {
|
|
207
|
-
clearInterval(heartbeat);
|
|
208
|
-
unsubscribe();
|
|
209
|
-
};
|
|
174
|
+
const cleanup = () => clearInterval(heartbeat);
|
|
210
175
|
req.on("close", cleanup);
|
|
211
176
|
res.on("close", cleanup);
|
|
212
177
|
res.on("finish", cleanup);
|