@lazyingart/agintiflow 0.20.131 → 0.20.133
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/package.json +4 -2
- package/scripts/smoke-runtime-compat.js +110 -0
- package/src/cli.js +5 -0
- package/src/docker-setup.js +6 -0
- package/src/platform.js +3 -0
- package/src/project.js +4 -0
- package/src/session-index.js +119 -18
- package/src/sqlite.js +112 -4
- package/src/web-db.js +154 -29
- package/web.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.133",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"scripts/smoke-platform.js",
|
|
67
67
|
"scripts/smoke-perception-research.js",
|
|
68
68
|
"scripts/smoke-permission-modes.js",
|
|
69
|
+
"scripts/smoke-runtime-compat.js",
|
|
69
70
|
"scripts/smoke-skills.js",
|
|
70
71
|
"scripts/smoke-skillmesh.js",
|
|
71
72
|
"scripts/smoke-tmux-tools.js",
|
|
@@ -104,6 +105,7 @@
|
|
|
104
105
|
"smoke:platform": "node scripts/smoke-platform.js",
|
|
105
106
|
"smoke:perception-research": "node scripts/smoke-perception-research.js",
|
|
106
107
|
"smoke:permission-modes": "node scripts/smoke-permission-modes.js",
|
|
108
|
+
"smoke:runtime-compat": "node scripts/smoke-runtime-compat.js",
|
|
107
109
|
"smoke:tmux-tools": "node scripts/smoke-tmux-tools.js",
|
|
108
110
|
"smoke:web-api": "node scripts/smoke-web-api.js",
|
|
109
111
|
"smoke:web-autostart": "node scripts/smoke-web-autostart.js",
|
|
@@ -114,7 +116,7 @@
|
|
|
114
116
|
"postinstall": "node scripts/postinstall-webapp.js",
|
|
115
117
|
"supervision:seed": "node scripts/seed-supervised-homework.js",
|
|
116
118
|
"storage:migrate": "node bin/aginti-cli.js storage migrate",
|
|
117
|
-
"test": "npm run check && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:web-autostart && npm run smoke:webapp-command && npm run smoke:web-port-fallback && npm run smoke:docker-command && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:auth && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:tmux-tools && npm run smoke:cli-chat && npm run smoke:inbox",
|
|
119
|
+
"test": "npm run check && npm run smoke:runtime-compat && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:web-autostart && npm run smoke:webapp-command && npm run smoke:web-port-fallback && npm run smoke:docker-command && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:auth && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:tmux-tools && npm run smoke:cli-chat && npm run smoke:inbox",
|
|
118
120
|
"pack:dry-run": "npm pack --dry-run",
|
|
119
121
|
"smoke:capabilities": "node scripts/smoke-capabilities.js"
|
|
120
122
|
},
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-runtime-compat-"));
|
|
11
|
+
const agintiflowHome = path.join(tempRoot, ".agintiflow-home");
|
|
12
|
+
process.env.AGINTIFLOW_HOME = agintiflowHome;
|
|
13
|
+
process.env.AGINTIFLOW_FORCE_JSON_DB = "1";
|
|
14
|
+
|
|
15
|
+
const { formatNodeSqliteRecovery, nodeSqliteStatus } = await import("../src/sqlite.js");
|
|
16
|
+
const { WebDatabase } = await import("../src/web-db.js");
|
|
17
|
+
const { deleteSessionIndex, listSessionIndex, renameSessionIndex, upsertSessionIndex } = await import("../src/session-index.js");
|
|
18
|
+
|
|
19
|
+
const sqliteStatus = nodeSqliteStatus();
|
|
20
|
+
assert.equal(sqliteStatus.ok, false, "forced sqlite fallback should report unavailable node:sqlite");
|
|
21
|
+
assert.equal(sqliteStatus.forcedJson, true, "forced sqlite fallback should be marked as forced");
|
|
22
|
+
const recovery = formatNodeSqliteRecovery(sqliteStatus);
|
|
23
|
+
assert.match(recovery, /Node\.js 22\+/);
|
|
24
|
+
assert.match(recovery, /npm install -g @lazyingart\/agintiflow@latest/);
|
|
25
|
+
|
|
26
|
+
const db = new WebDatabase(tempRoot);
|
|
27
|
+
assert.equal(db.driver, "json", "WebDatabase should use JSON fallback when node:sqlite is unavailable");
|
|
28
|
+
|
|
29
|
+
const preferences = db.getPreferences();
|
|
30
|
+
db.savePreferences({ ...preferences, model: "runtime-compat-model" });
|
|
31
|
+
assert.equal(db.getPreferences().model, "runtime-compat-model", "JSON fallback preferences should persist");
|
|
32
|
+
|
|
33
|
+
const session = {
|
|
34
|
+
sessionId: "runtime-compat-session",
|
|
35
|
+
projectRoot: tempRoot,
|
|
36
|
+
commandCwd: tempRoot,
|
|
37
|
+
provider: "mock",
|
|
38
|
+
model: "mock-model",
|
|
39
|
+
goal: "runtime compatibility",
|
|
40
|
+
title: "Runtime Compatibility",
|
|
41
|
+
status: "finished",
|
|
42
|
+
startedAt: "2026-05-14T00:00:00.000Z",
|
|
43
|
+
updatedAt: "2026-05-14T00:01:00.000Z",
|
|
44
|
+
result: "ok",
|
|
45
|
+
};
|
|
46
|
+
db.upsertSession(session);
|
|
47
|
+
assert.equal(db.getSession(session.sessionId)?.title, "Runtime Compatibility", "JSON fallback session should persist");
|
|
48
|
+
assert.equal(db.listSessions(5).length, 1, "JSON fallback session listing should work");
|
|
49
|
+
assert.equal(db.renameSession(session.sessionId, "Renamed Runtime Compatibility"), true, "JSON fallback rename should work");
|
|
50
|
+
assert.equal(db.getSession(session.sessionId)?.title, "Renamed Runtime Compatibility", "JSON fallback rename should persist");
|
|
51
|
+
assert.equal(db.deleteSession(session.sessionId), true, "JSON fallback delete should work");
|
|
52
|
+
assert.equal(db.getSession(session.sessionId), null, "JSON fallback delete should remove the session");
|
|
53
|
+
|
|
54
|
+
assert.equal(upsertSessionIndex(session), true, "JSON fallback global index upsert should work");
|
|
55
|
+
assert.equal(listSessionIndex({ projectRoot: tempRoot }).length, 1, "JSON fallback global index listing should work");
|
|
56
|
+
assert.equal(renameSessionIndex(session.sessionId, "Indexed Runtime Compatibility"), true, "JSON fallback global index rename should work");
|
|
57
|
+
assert.equal(deleteSessionIndex(session.sessionId), true, "JSON fallback global index delete should work");
|
|
58
|
+
|
|
59
|
+
const port = 45200 + Math.floor(Math.random() * 500);
|
|
60
|
+
const server = spawn(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"), "web", "--port", String(port), "--host", "127.0.0.1"], {
|
|
61
|
+
cwd: tempRoot,
|
|
62
|
+
env: {
|
|
63
|
+
...process.env,
|
|
64
|
+
AGINTIFLOW_HOME: agintiflowHome,
|
|
65
|
+
AGINTIFLOW_RUNTIME_DIR: tempRoot,
|
|
66
|
+
AGINTIFLOW_FORCE_JSON_DB: "1",
|
|
67
|
+
},
|
|
68
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
let stdout = "";
|
|
72
|
+
let stderr = "";
|
|
73
|
+
server.stdout.on("data", (chunk) => {
|
|
74
|
+
stdout += chunk.toString();
|
|
75
|
+
});
|
|
76
|
+
server.stderr.on("data", (chunk) => {
|
|
77
|
+
stderr += chunk.toString();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function delay(ms) {
|
|
81
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function waitForHealth() {
|
|
85
|
+
const deadline = Date.now() + 15000;
|
|
86
|
+
while (Date.now() < deadline) {
|
|
87
|
+
if (server.exitCode !== null) break;
|
|
88
|
+
try {
|
|
89
|
+
const response = await fetch(`http://127.0.0.1:${port}/health`);
|
|
90
|
+
const health = await response.json();
|
|
91
|
+
if (health.ok) return health;
|
|
92
|
+
} catch {
|
|
93
|
+
await delay(250);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`forced JSON web health failed. stdout=${stdout.slice(-500)} stderr=${stderr.slice(-500)}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const health = await waitForHealth();
|
|
101
|
+
assert.equal(health.storageDriver, "json", "web health should advertise JSON fallback storage");
|
|
102
|
+
assert.equal(health.runtimeDir, tempRoot, "web runtime should use the requested project root");
|
|
103
|
+
} finally {
|
|
104
|
+
if (server.exitCode === null && !server.killed) {
|
|
105
|
+
server.kill("SIGTERM");
|
|
106
|
+
await new Promise((resolve) => server.once("exit", resolve));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log("runtime compatibility smoke ok");
|
package/src/cli.js
CHANGED
|
@@ -1098,6 +1098,7 @@ function parseInitOptions(argv = []) {
|
|
|
1098
1098
|
function printDoctorReport(report) {
|
|
1099
1099
|
console.log(`AgInTiFlow ${report.package.version} (npm latest: ${report.package.npmLatest})`);
|
|
1100
1100
|
console.log(`node=${report.node.version} ok=${report.node.ok}`);
|
|
1101
|
+
console.log(`nodeSqlite=${report.node.sqlite?.ok ? "available" : "fallback-json"} storage=${report.node.sqlite?.ok ? "sqlite" : "json"}`);
|
|
1101
1102
|
console.log(`platform=${report.platform.label} family=${report.platform.linuxFamily || report.platform.platform}`);
|
|
1102
1103
|
console.log(`project=${report.project.root}`);
|
|
1103
1104
|
console.log(`instructions=${report.project.instructionsPath} present=${report.project.instructionsPresent}`);
|
|
@@ -1126,6 +1127,10 @@ function printDoctorReport(report) {
|
|
|
1126
1127
|
console.log("platform setup hints:");
|
|
1127
1128
|
for (const hint of report.platform.setupHints) console.log(`- ${hint}`);
|
|
1128
1129
|
}
|
|
1130
|
+
if (report.node.recovery?.length) {
|
|
1131
|
+
console.log("node runtime recovery:");
|
|
1132
|
+
for (const line of report.node.recovery) console.log(`- ${line}`);
|
|
1133
|
+
}
|
|
1129
1134
|
}
|
|
1130
1135
|
|
|
1131
1136
|
async function readStdin() {
|
package/src/docker-setup.js
CHANGED
|
@@ -91,6 +91,12 @@ export function formatDockerSetupText(summary = {}) {
|
|
|
91
91
|
if (!summary.dockerAvailable) {
|
|
92
92
|
lines.push("Docker is not available.");
|
|
93
93
|
lines.push(`Install path: ${summary.install?.command || "Install Docker, then run: aginti docker setup"}`);
|
|
94
|
+
if (summary.install?.supported) {
|
|
95
|
+
lines.push("Install command: aginti docker install-host --yes");
|
|
96
|
+
lines.push("After install, open a fresh shell or run newgrp docker, then run: aginti docker setup");
|
|
97
|
+
} else {
|
|
98
|
+
lines.push("After installing Docker for this platform, run: aginti docker setup");
|
|
99
|
+
}
|
|
94
100
|
} else if (!summary.imageReady || summary.preflightOk === false) {
|
|
95
101
|
lines.push("Next: aginti docker setup");
|
|
96
102
|
} else {
|
package/src/platform.js
CHANGED
|
@@ -58,6 +58,7 @@ export function platformSetupHints(info = platformInfo()) {
|
|
|
58
58
|
if (info.isWsl) {
|
|
59
59
|
return [
|
|
60
60
|
"Use the Linux/WSL shell as the primary AgInTiFlow environment; keep projects under the WSL filesystem for best file and Docker performance.",
|
|
61
|
+
"Use Node.js 22+ from nvm/fnm/NodeSource; if an old ~/.npm-global/bin/aginti is earlier on PATH, reinstall AgInTiFlow after switching Node.",
|
|
61
62
|
"Use Docker Desktop with WSL integration or Docker Engine inside WSL for docker-workspace mode.",
|
|
62
63
|
"For LaTeX, reuse WSL latexmk/pdflatex when installed, or use the companion Docker sandbox image.",
|
|
63
64
|
];
|
|
@@ -65,6 +66,7 @@ export function platformSetupHints(info = platformInfo()) {
|
|
|
65
66
|
if (info.isMac) {
|
|
66
67
|
return [
|
|
67
68
|
"Use Node.js 22+ from Homebrew, nvm, fnm, or the official installer.",
|
|
69
|
+
"After changing Node versions, verify `node -v`, `which aginti`, and reinstall with `npm install -g @lazyingart/agintiflow@latest` so the CLI uses the same Node that installed it.",
|
|
68
70
|
"Use Docker Desktop or Colima for docker-workspace mode; the Ubuntu Docker installer is intentionally not used on macOS.",
|
|
69
71
|
"For LaTeX, reuse MacTeX/BasicTeX when latexmk and pdflatex are on PATH; otherwise use the companion Docker sandbox image.",
|
|
70
72
|
"Use Homebrew for optional host tools such as ripgrep, git, python, and latexmk when you choose host mode.",
|
|
@@ -80,6 +82,7 @@ export function platformSetupHints(info = platformInfo()) {
|
|
|
80
82
|
if (info.linuxFamily === "debian") {
|
|
81
83
|
return [
|
|
82
84
|
"Use Node.js 22+ from nvm/fnm, NodeSource, or distro packages.",
|
|
85
|
+
"After changing Node versions, verify `node -v`, `which aginti`, and reinstall with `npm install -g @lazyingart/agintiflow@latest` to avoid stale npm-global shims.",
|
|
83
86
|
"Docker can be installed with scripts/install-docker-ubuntu.sh on Ubuntu/Debian-like hosts.",
|
|
84
87
|
"For LaTeX, reuse host latexmk/pdflatex when available; otherwise use the companion Docker sandbox image.",
|
|
85
88
|
];
|
package/src/project.js
CHANGED
|
@@ -6,6 +6,7 @@ import { promisify } from "node:util";
|
|
|
6
6
|
import { listAgentWrappers } from "./tool-wrappers.js";
|
|
7
7
|
import { getDockerSandboxStatus } from "./docker-sandbox.js";
|
|
8
8
|
import { platformInfo, platformLabel, platformSetupHints } from "./platform.js";
|
|
9
|
+
import { nodeSqliteRecoveryLines, nodeSqliteStatus } from "./sqlite.js";
|
|
9
10
|
import { buildAgintiInstructions, normalizeInstructionTemplate } from "./behavior-contract.js";
|
|
10
11
|
import {
|
|
11
12
|
LEGACY_PROJECT_SESSIONS_DIR_NAME,
|
|
@@ -758,6 +759,7 @@ export async function doctorReport(projectRoot, packageVersion, config) {
|
|
|
758
759
|
const paths = projectPaths(projectRoot);
|
|
759
760
|
const keyStatus = providerKeyStatus(projectRoot);
|
|
760
761
|
const platform = platformInfo();
|
|
762
|
+
const sqliteStatus = nodeSqliteStatus();
|
|
761
763
|
const [sessions, dockerStatus, latestVersion, instructions] = await Promise.all([
|
|
762
764
|
listProjectSessions(projectRoot, 8),
|
|
763
765
|
getDockerSandboxStatus(config).catch((error) => ({ ok: false, error: error.message })),
|
|
@@ -775,6 +777,8 @@ export async function doctorReport(projectRoot, packageVersion, config) {
|
|
|
775
777
|
node: {
|
|
776
778
|
version: process.version,
|
|
777
779
|
ok: Number(process.versions.node.split(".")[0]) >= 22,
|
|
780
|
+
sqlite: sqliteStatus,
|
|
781
|
+
recovery: sqliteStatus.ok ? [] : nodeSqliteRecoveryLines(sqliteStatus),
|
|
778
782
|
},
|
|
779
783
|
platform: {
|
|
780
784
|
...platform,
|
package/src/session-index.js
CHANGED
|
@@ -17,6 +17,7 @@ export function globalSessionPaths(sessionId = "") {
|
|
|
17
17
|
home,
|
|
18
18
|
sessionsDir,
|
|
19
19
|
indexDbPath: path.join(sessionsDir, "index.sqlite"),
|
|
20
|
+
indexJsonPath: path.join(sessionsDir, "index.json"),
|
|
20
21
|
sessionDir: sessionId ? path.join(sessionsDir, sessionId) : "",
|
|
21
22
|
};
|
|
22
23
|
}
|
|
@@ -29,7 +30,8 @@ export function isSafeSessionId(sessionId) {
|
|
|
29
30
|
function ensureIndexDb() {
|
|
30
31
|
const paths = globalSessionPaths();
|
|
31
32
|
fs.mkdirSync(paths.sessionsDir, { recursive: true });
|
|
32
|
-
const DatabaseSync = loadDatabaseSync();
|
|
33
|
+
const DatabaseSync = loadDatabaseSync({ optional: true });
|
|
34
|
+
if (!DatabaseSync) return null;
|
|
33
35
|
const db = new DatabaseSync(paths.indexDbPath);
|
|
34
36
|
db.exec(`
|
|
35
37
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -57,14 +59,86 @@ function ensureIndexDb() {
|
|
|
57
59
|
return db;
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
function emptyIndexState() {
|
|
63
|
+
return {
|
|
64
|
+
sessions: {},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readIndexJson() {
|
|
69
|
+
const paths = globalSessionPaths();
|
|
70
|
+
fs.mkdirSync(paths.sessionsDir, { recursive: true });
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(fs.readFileSync(paths.indexJsonPath, "utf8"));
|
|
73
|
+
return {
|
|
74
|
+
...emptyIndexState(),
|
|
75
|
+
...parsed,
|
|
76
|
+
sessions: parsed && typeof parsed.sessions === "object" && parsed.sessions ? parsed.sessions : {},
|
|
77
|
+
};
|
|
78
|
+
} catch {
|
|
79
|
+
return emptyIndexState();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function writeIndexJson(state) {
|
|
84
|
+
const paths = globalSessionPaths();
|
|
85
|
+
fs.mkdirSync(paths.sessionsDir, { recursive: true });
|
|
86
|
+
const tmpPath = `${paths.indexJsonPath}.${process.pid}.${Date.now()}.tmp`;
|
|
87
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify(state, null, 2)}\n`);
|
|
88
|
+
fs.renameSync(tmpPath, paths.indexJsonPath);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeIndexRecord(record = {}, sessionId = "") {
|
|
92
|
+
const now = new Date().toISOString();
|
|
93
|
+
const paths = globalSessionPaths(sessionId);
|
|
94
|
+
const createdAt = record.createdAt || record.startedAt || record.created_at || now;
|
|
95
|
+
const updatedAt = record.updatedAt || record.updated_at || createdAt || now;
|
|
96
|
+
return {
|
|
97
|
+
sessionId,
|
|
98
|
+
projectRoot: String(record.projectRoot || record.project_root || ""),
|
|
99
|
+
commandCwd: String(record.commandCwd || record.command_cwd || record.projectRoot || record.project_root || ""),
|
|
100
|
+
projectSessionsDir: String(record.projectSessionsDir || record.project_sessions_dir || ""),
|
|
101
|
+
sessionDir: String(record.sessionDir || record.session_dir || paths.sessionDir),
|
|
102
|
+
provider: String(record.provider || ""),
|
|
103
|
+
model: String(record.model || ""),
|
|
104
|
+
goal: String(record.goal || ""),
|
|
105
|
+
title: String(record.title || ""),
|
|
106
|
+
status: String(record.status || ""),
|
|
107
|
+
createdAt,
|
|
108
|
+
updatedAt,
|
|
109
|
+
endedAt: record.endedAt || record.ended_at || null,
|
|
110
|
+
result: String(record.result || ""),
|
|
111
|
+
error: String(record.error || ""),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
60
115
|
export function upsertSessionIndex(record = {}) {
|
|
61
116
|
const sessionId = String(record.sessionId || record.session_id || "").trim();
|
|
62
117
|
if (!isSafeSessionId(sessionId)) return false;
|
|
63
|
-
const now = new Date().toISOString();
|
|
64
118
|
const db = ensureIndexDb();
|
|
119
|
+
const normalized = normalizeIndexRecord(record, sessionId);
|
|
120
|
+
if (!db) {
|
|
121
|
+
const state = readIndexJson();
|
|
122
|
+
const previous = state.sessions[sessionId] || {};
|
|
123
|
+
state.sessions[sessionId] = {
|
|
124
|
+
...previous,
|
|
125
|
+
...normalized,
|
|
126
|
+
projectRoot: normalized.projectRoot || previous.projectRoot || "",
|
|
127
|
+
commandCwd: normalized.commandCwd || previous.commandCwd || "",
|
|
128
|
+
projectSessionsDir: normalized.projectSessionsDir || previous.projectSessionsDir || "",
|
|
129
|
+
sessionDir: normalized.sessionDir || previous.sessionDir || globalSessionPaths(sessionId).sessionDir,
|
|
130
|
+
provider: normalized.provider || previous.provider || "",
|
|
131
|
+
model: normalized.model || previous.model || "",
|
|
132
|
+
goal: normalized.goal || previous.goal || "",
|
|
133
|
+
title: normalized.title || previous.title || "",
|
|
134
|
+
status: normalized.status || previous.status || "",
|
|
135
|
+
result: normalized.result || previous.result || "",
|
|
136
|
+
error: normalized.error || previous.error || "",
|
|
137
|
+
};
|
|
138
|
+
writeIndexJson(state);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
65
141
|
const paths = globalSessionPaths(sessionId);
|
|
66
|
-
const createdAt = record.createdAt || record.startedAt || record.created_at || now;
|
|
67
|
-
const updatedAt = record.updatedAt || record.updated_at || createdAt || now;
|
|
68
142
|
db.prepare(
|
|
69
143
|
`INSERT INTO sessions (
|
|
70
144
|
session_id, project_root, command_cwd, project_sessions_dir, session_dir,
|
|
@@ -86,20 +160,20 @@ export function upsertSessionIndex(record = {}) {
|
|
|
86
160
|
error = CASE WHEN excluded.error != '' THEN excluded.error ELSE sessions.error END`
|
|
87
161
|
).run(
|
|
88
162
|
sessionId,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
createdAt,
|
|
99
|
-
updatedAt,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
163
|
+
normalized.projectRoot,
|
|
164
|
+
normalized.commandCwd,
|
|
165
|
+
normalized.projectSessionsDir,
|
|
166
|
+
normalized.sessionDir || paths.sessionDir,
|
|
167
|
+
normalized.provider,
|
|
168
|
+
normalized.model,
|
|
169
|
+
normalized.goal,
|
|
170
|
+
normalized.title,
|
|
171
|
+
normalized.status,
|
|
172
|
+
normalized.createdAt,
|
|
173
|
+
normalized.updatedAt,
|
|
174
|
+
normalized.endedAt,
|
|
175
|
+
normalized.result,
|
|
176
|
+
normalized.error
|
|
103
177
|
);
|
|
104
178
|
return true;
|
|
105
179
|
}
|
|
@@ -107,6 +181,14 @@ export function upsertSessionIndex(record = {}) {
|
|
|
107
181
|
export function renameSessionIndex(sessionId, title) {
|
|
108
182
|
if (!isSafeSessionId(sessionId)) return false;
|
|
109
183
|
const db = ensureIndexDb();
|
|
184
|
+
if (!db) {
|
|
185
|
+
const state = readIndexJson();
|
|
186
|
+
if (!state.sessions[sessionId]) return false;
|
|
187
|
+
state.sessions[sessionId].title = String(title || "").trim();
|
|
188
|
+
state.sessions[sessionId].updatedAt = new Date().toISOString();
|
|
189
|
+
writeIndexJson(state);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
110
192
|
const result = db
|
|
111
193
|
.prepare("UPDATE sessions SET title = ?, updated_at = ? WHERE session_id = ?")
|
|
112
194
|
.run(String(title || "").trim(), new Date().toISOString(), sessionId);
|
|
@@ -116,6 +198,13 @@ export function renameSessionIndex(sessionId, title) {
|
|
|
116
198
|
export function deleteSessionIndex(sessionId) {
|
|
117
199
|
if (!isSafeSessionId(sessionId)) return false;
|
|
118
200
|
const db = ensureIndexDb();
|
|
201
|
+
if (!db) {
|
|
202
|
+
const state = readIndexJson();
|
|
203
|
+
if (!state.sessions[sessionId]) return false;
|
|
204
|
+
delete state.sessions[sessionId];
|
|
205
|
+
writeIndexJson(state);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
119
208
|
const result = db.prepare("DELETE FROM sessions WHERE session_id = ?").run(sessionId);
|
|
120
209
|
return result.changes > 0;
|
|
121
210
|
}
|
|
@@ -123,6 +212,18 @@ export function deleteSessionIndex(sessionId) {
|
|
|
123
212
|
export function listSessionIndex({ projectRoot = "", commandCwd = "", limit = 100 } = {}) {
|
|
124
213
|
const db = ensureIndexDb();
|
|
125
214
|
const maxRows = Math.min(Math.max(Number(limit) || 100, 1), 1000);
|
|
215
|
+
if (!db) {
|
|
216
|
+
const resolvedProjectRoot = projectRoot ? path.resolve(projectRoot) : "";
|
|
217
|
+
const resolvedCommandCwd = commandCwd ? path.resolve(commandCwd) : "";
|
|
218
|
+
return Object.values(readIndexJson().sessions)
|
|
219
|
+
.filter((session) => {
|
|
220
|
+
if (resolvedProjectRoot && session.projectRoot !== resolvedProjectRoot) return false;
|
|
221
|
+
if (resolvedCommandCwd && session.commandCwd !== resolvedCommandCwd) return false;
|
|
222
|
+
return true;
|
|
223
|
+
})
|
|
224
|
+
.sort((left, right) => String(right.updatedAt || "").localeCompare(String(left.updatedAt || "")))
|
|
225
|
+
.slice(0, maxRows);
|
|
226
|
+
}
|
|
126
227
|
const columns = `session_id AS sessionId, project_root AS projectRoot, command_cwd AS commandCwd, project_sessions_dir AS projectSessionsDir,
|
|
127
228
|
session_dir AS sessionDir, provider, model, goal, title, status,
|
|
128
229
|
created_at AS createdAt, updated_at AS updatedAt, ended_at AS endedAt, result, error`;
|
package/src/sqlite.js
CHANGED
|
@@ -2,18 +2,126 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
|
|
3
3
|
const require = createRequire(import.meta.url);
|
|
4
4
|
let cachedDatabaseSync = null;
|
|
5
|
+
let cachedUnavailableError = null;
|
|
5
6
|
|
|
6
|
-
export
|
|
7
|
-
|
|
7
|
+
export const NODE_SQLITE_UNAVAILABLE_CODE = "AGINTIFLOW_NODE_SQLITE_UNAVAILABLE";
|
|
8
|
+
|
|
9
|
+
function nodeMajorVersion(version = process.versions.node) {
|
|
10
|
+
return Number(String(version || "").split(".")[0]) || 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function requireNodeSqliteSilently() {
|
|
8
14
|
const originalEmitWarning = process.emitWarning;
|
|
9
15
|
process.emitWarning = function filteredSqliteWarning(warning, ...args) {
|
|
10
16
|
if (String(warning || "").includes("SQLite is an experimental feature")) return;
|
|
11
17
|
return originalEmitWarning.call(process, warning, ...args);
|
|
12
18
|
};
|
|
13
19
|
try {
|
|
14
|
-
|
|
15
|
-
return cachedDatabaseSync;
|
|
20
|
+
return require("node:sqlite");
|
|
16
21
|
} finally {
|
|
17
22
|
process.emitWarning = originalEmitWarning;
|
|
18
23
|
}
|
|
19
24
|
}
|
|
25
|
+
|
|
26
|
+
export function nodeSqliteStatus() {
|
|
27
|
+
const nodeVersion = process.version;
|
|
28
|
+
const nodeMajor = nodeMajorVersion();
|
|
29
|
+
const forcedJson = process.env.AGINTIFLOW_FORCE_JSON_DB === "1" || process.env.AGINTIFLOW_FORCE_SQLITE_UNAVAILABLE === "1";
|
|
30
|
+
if (forcedJson) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
forcedJson: true,
|
|
34
|
+
nodeVersion,
|
|
35
|
+
nodeMajor,
|
|
36
|
+
requiredNodeMajor: 22,
|
|
37
|
+
code: NODE_SQLITE_UNAVAILABLE_CODE,
|
|
38
|
+
message: "AGINTIFLOW_FORCE_JSON_DB is enabled; using JSON fallback storage.",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
requireNodeSqliteSilently();
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
forcedJson: false,
|
|
46
|
+
nodeVersion,
|
|
47
|
+
nodeMajor,
|
|
48
|
+
requiredNodeMajor: 22,
|
|
49
|
+
code: "",
|
|
50
|
+
message: "node:sqlite is available.",
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
forcedJson: false,
|
|
56
|
+
nodeVersion,
|
|
57
|
+
nodeMajor,
|
|
58
|
+
requiredNodeMajor: 22,
|
|
59
|
+
code: NODE_SQLITE_UNAVAILABLE_CODE,
|
|
60
|
+
errorCode: error?.code || "",
|
|
61
|
+
message: error?.message || String(error),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function nodeSqliteRecoveryLines(status = nodeSqliteStatus()) {
|
|
67
|
+
const lines = [
|
|
68
|
+
`AgInTiFlow web/session SQLite storage needs Node.js 22+ with node:sqlite; current node is ${status.nodeVersion || process.version}.`,
|
|
69
|
+
"AgInTiFlow will use a project-local JSON fallback when possible, but Node 22+ is still recommended for the full durable session index.",
|
|
70
|
+
"Recommended fix after installing Node 22+: npm install -g @lazyingart/agintiflow@latest",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
if (process.platform === "win32") {
|
|
74
|
+
lines.push("Windows: prefer WSL2 with Node 22+ and Docker Desktop WSL integration, or install Node 22+ with the official installer/nvm-windows.");
|
|
75
|
+
} else {
|
|
76
|
+
lines.push('nvm quick fix: export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"; nvm install 22; nvm alias default 22; nvm use 22');
|
|
77
|
+
lines.push("If `which aginti` points to ~/.npm-global/bin/aginti after switching Node, remove the stale install or put the nvm bin directory before ~/.npm-global/bin in PATH.");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (process.platform === "darwin") {
|
|
81
|
+
lines.push("macOS alternative: brew install node@22, then reinstall AgInTiFlow globally with that node on PATH.");
|
|
82
|
+
} else if (process.platform === "linux") {
|
|
83
|
+
lines.push("Linux alternative: install Node 22+ from NodeSource/fnm/nvm/distro packages, then open a new shell and verify `node -v` and `which aginti`.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return lines;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function formatNodeSqliteRecovery(status = nodeSqliteStatus()) {
|
|
90
|
+
return nodeSqliteRecoveryLines(status).join("\n");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class AgintiSqliteUnavailableError extends Error {
|
|
94
|
+
constructor(cause, status = nodeSqliteStatus()) {
|
|
95
|
+
super(formatNodeSqliteRecovery(status));
|
|
96
|
+
this.name = "AgintiSqliteUnavailableError";
|
|
97
|
+
this.code = NODE_SQLITE_UNAVAILABLE_CODE;
|
|
98
|
+
this.status = status;
|
|
99
|
+
if (cause) this.cause = cause;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function unavailableError(cause = null) {
|
|
104
|
+
cachedUnavailableError = new AgintiSqliteUnavailableError(cause);
|
|
105
|
+
return cachedUnavailableError;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function loadDatabaseSync(options = {}) {
|
|
109
|
+
if (cachedUnavailableError) {
|
|
110
|
+
if (options.optional) return null;
|
|
111
|
+
throw cachedUnavailableError;
|
|
112
|
+
}
|
|
113
|
+
if (cachedDatabaseSync) return cachedDatabaseSync;
|
|
114
|
+
if (process.env.AGINTIFLOW_FORCE_JSON_DB === "1" || process.env.AGINTIFLOW_FORCE_SQLITE_UNAVAILABLE === "1") {
|
|
115
|
+
const error = unavailableError(new Error("Forced JSON database fallback."));
|
|
116
|
+
if (options.optional) return null;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
cachedDatabaseSync = requireNodeSqliteSilently().DatabaseSync;
|
|
121
|
+
return cachedDatabaseSync;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const wrapped = unavailableError(error);
|
|
124
|
+
if (options.optional) return null;
|
|
125
|
+
throw wrapped;
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/web-db.js
CHANGED
|
@@ -9,6 +9,38 @@ import { permissionModeDefaults } from "./permission-modes.js";
|
|
|
9
9
|
|
|
10
10
|
const PREFERENCES_SCHEMA_VERSION = 8;
|
|
11
11
|
|
|
12
|
+
function jsonStatePath(dbPath) {
|
|
13
|
+
return dbPath.replace(/\.sqlite$/i, ".json");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function emptyJsonState() {
|
|
17
|
+
return {
|
|
18
|
+
preferences: {},
|
|
19
|
+
sessions: {},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readJsonFile(filePath) {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
26
|
+
return {
|
|
27
|
+
...emptyJsonState(),
|
|
28
|
+
...parsed,
|
|
29
|
+
preferences: parsed && typeof parsed.preferences === "object" && parsed.preferences ? parsed.preferences : {},
|
|
30
|
+
sessions: parsed && typeof parsed.sessions === "object" && parsed.sessions ? parsed.sessions : {},
|
|
31
|
+
};
|
|
32
|
+
} catch {
|
|
33
|
+
return emptyJsonState();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeJsonFile(filePath, state) {
|
|
38
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
39
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
40
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify(state, null, 2)}\n`);
|
|
41
|
+
fs.renameSync(tmpPath, filePath);
|
|
42
|
+
}
|
|
43
|
+
|
|
12
44
|
function defaultPreferences(baseDir) {
|
|
13
45
|
const presets = getModelPresets();
|
|
14
46
|
const roles = getModelRoleDefaults();
|
|
@@ -63,33 +95,40 @@ export class WebDatabase {
|
|
|
63
95
|
this.paths = projectPaths(this.baseDir);
|
|
64
96
|
this.dbDir = this.paths.sessionsDir;
|
|
65
97
|
this.dbPath = this.paths.sessionDbPath;
|
|
98
|
+
this.jsonPath = jsonStatePath(this.dbPath);
|
|
66
99
|
fs.mkdirSync(this.dbDir, { recursive: true });
|
|
67
|
-
const DatabaseSync = loadDatabaseSync();
|
|
68
|
-
this.
|
|
69
|
-
this.db.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
100
|
+
const DatabaseSync = loadDatabaseSync({ optional: true });
|
|
101
|
+
this.driver = DatabaseSync ? "sqlite" : "json";
|
|
102
|
+
this.db = DatabaseSync ? new DatabaseSync(this.dbPath) : null;
|
|
103
|
+
if (this.db) {
|
|
104
|
+
this.db.exec(`
|
|
105
|
+
CREATE TABLE IF NOT EXISTS preferences (
|
|
106
|
+
key TEXT PRIMARY KEY,
|
|
107
|
+
value TEXT NOT NULL,
|
|
108
|
+
updated_at TEXT NOT NULL
|
|
109
|
+
);
|
|
75
110
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
111
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
112
|
+
session_id TEXT PRIMARY KEY,
|
|
113
|
+
provider TEXT NOT NULL,
|
|
114
|
+
model TEXT NOT NULL,
|
|
115
|
+
goal TEXT NOT NULL,
|
|
116
|
+
status TEXT NOT NULL,
|
|
117
|
+
started_at TEXT NOT NULL,
|
|
118
|
+
updated_at TEXT NOT NULL,
|
|
119
|
+
ended_at TEXT,
|
|
120
|
+
result TEXT,
|
|
121
|
+
error TEXT
|
|
122
|
+
);
|
|
123
|
+
`);
|
|
124
|
+
} else if (!fs.existsSync(this.jsonPath)) {
|
|
125
|
+
writeJsonFile(this.jsonPath, emptyJsonState());
|
|
126
|
+
}
|
|
89
127
|
this.migrate();
|
|
90
128
|
}
|
|
91
129
|
|
|
92
130
|
migrate() {
|
|
131
|
+
if (!this.db) return;
|
|
93
132
|
const sessionColumns = this.db.prepare("PRAGMA table_info(sessions)").all();
|
|
94
133
|
if (!sessionColumns.some((column) => column.name === "title")) {
|
|
95
134
|
this.db.exec("ALTER TABLE sessions ADD COLUMN title TEXT NOT NULL DEFAULT ''");
|
|
@@ -108,12 +147,29 @@ export class WebDatabase {
|
|
|
108
147
|
}
|
|
109
148
|
}
|
|
110
149
|
|
|
150
|
+
readJsonState() {
|
|
151
|
+
return readJsonFile(this.jsonPath);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
writeJsonState(state) {
|
|
155
|
+
writeJsonFile(this.jsonPath, state);
|
|
156
|
+
}
|
|
157
|
+
|
|
111
158
|
getPreferences() {
|
|
159
|
+
if (!this.db) {
|
|
160
|
+
const raw = this.readJsonState().preferences.ui;
|
|
161
|
+
if (!raw) return defaultPreferences(this.baseDir);
|
|
162
|
+
return this.normalizePreferences(raw);
|
|
163
|
+
}
|
|
112
164
|
const row = this.db.prepare("SELECT value FROM preferences WHERE key = ?").get("ui");
|
|
113
165
|
if (!row) return defaultPreferences(this.baseDir);
|
|
114
166
|
|
|
167
|
+
return this.normalizePreferences(row.value);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
normalizePreferences(value) {
|
|
115
171
|
try {
|
|
116
|
-
const parsed = JSON.parse(
|
|
172
|
+
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
117
173
|
const preferences = {
|
|
118
174
|
...defaultPreferences(this.baseDir),
|
|
119
175
|
...parsed,
|
|
@@ -177,6 +233,13 @@ export class WebDatabase {
|
|
|
177
233
|
savePreferences(preferences) {
|
|
178
234
|
const value = JSON.stringify(preferences);
|
|
179
235
|
const updatedAt = new Date().toISOString();
|
|
236
|
+
if (!this.db) {
|
|
237
|
+
const state = this.readJsonState();
|
|
238
|
+
state.preferences.ui = value;
|
|
239
|
+
state.preferences.uiUpdatedAt = updatedAt;
|
|
240
|
+
this.writeJsonState(state);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
180
243
|
this.db
|
|
181
244
|
.prepare(
|
|
182
245
|
`INSERT INTO preferences (key, value, updated_at)
|
|
@@ -192,6 +255,35 @@ export class WebDatabase {
|
|
|
192
255
|
const commandCwd = session.commandCwd || projectRoot;
|
|
193
256
|
const projectSessionsDir = session.projectSessionsDir || this.paths.sessionsDir;
|
|
194
257
|
const sessionDir = session.sessionDir || path.join(this.paths.globalSessionsDir, session.sessionId);
|
|
258
|
+
const record = {
|
|
259
|
+
sessionId: session.sessionId,
|
|
260
|
+
projectRoot,
|
|
261
|
+
commandCwd,
|
|
262
|
+
projectSessionsDir,
|
|
263
|
+
sessionDir,
|
|
264
|
+
provider: session.provider,
|
|
265
|
+
model: session.model,
|
|
266
|
+
goal: session.goal,
|
|
267
|
+
title: session.title || "",
|
|
268
|
+
status: session.status,
|
|
269
|
+
startedAt: session.startedAt,
|
|
270
|
+
updatedAt,
|
|
271
|
+
endedAt: session.endedAt || null,
|
|
272
|
+
result: session.result || "",
|
|
273
|
+
error: session.error || "",
|
|
274
|
+
};
|
|
275
|
+
if (!this.db) {
|
|
276
|
+
const state = this.readJsonState();
|
|
277
|
+
const previous = state.sessions[session.sessionId] || {};
|
|
278
|
+
state.sessions[session.sessionId] = {
|
|
279
|
+
...previous,
|
|
280
|
+
...record,
|
|
281
|
+
title: record.title || previous.title || "",
|
|
282
|
+
};
|
|
283
|
+
this.writeJsonState(state);
|
|
284
|
+
this.syncSessionIndex(record);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
195
287
|
this.db
|
|
196
288
|
.prepare(
|
|
197
289
|
`INSERT INTO sessions (
|
|
@@ -230,15 +322,14 @@ export class WebDatabase {
|
|
|
230
322
|
session.result || "",
|
|
231
323
|
session.error || ""
|
|
232
324
|
);
|
|
325
|
+
this.syncSessionIndex(record);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
syncSessionIndex(record) {
|
|
233
329
|
try {
|
|
234
330
|
upsertSessionIndex({
|
|
235
|
-
...
|
|
236
|
-
|
|
237
|
-
commandCwd,
|
|
238
|
-
projectSessionsDir,
|
|
239
|
-
sessionDir,
|
|
240
|
-
createdAt: session.startedAt,
|
|
241
|
-
updatedAt,
|
|
331
|
+
...record,
|
|
332
|
+
createdAt: record.startedAt,
|
|
242
333
|
});
|
|
243
334
|
} catch {
|
|
244
335
|
// The project-local database remains the fallback if the global index cannot be updated.
|
|
@@ -246,6 +337,9 @@ export class WebDatabase {
|
|
|
246
337
|
}
|
|
247
338
|
|
|
248
339
|
getSession(sessionId) {
|
|
340
|
+
if (!this.db) {
|
|
341
|
+
return this.readJsonState().sessions[sessionId] || null;
|
|
342
|
+
}
|
|
249
343
|
return (
|
|
250
344
|
this.db
|
|
251
345
|
.prepare(
|
|
@@ -273,6 +367,12 @@ export class WebDatabase {
|
|
|
273
367
|
}
|
|
274
368
|
|
|
275
369
|
listSessions(limit = 20) {
|
|
370
|
+
if (!this.db) {
|
|
371
|
+
const maxRows = Math.min(Math.max(Number(limit) || 20, 1), 1000);
|
|
372
|
+
return Object.values(this.readJsonState().sessions)
|
|
373
|
+
.sort((left, right) => String(right.updatedAt || "").localeCompare(String(left.updatedAt || "")))
|
|
374
|
+
.slice(0, maxRows);
|
|
375
|
+
}
|
|
276
376
|
return this.db
|
|
277
377
|
.prepare(
|
|
278
378
|
`SELECT
|
|
@@ -300,6 +400,19 @@ export class WebDatabase {
|
|
|
300
400
|
|
|
301
401
|
renameSession(sessionId, title) {
|
|
302
402
|
const updatedAt = new Date().toISOString();
|
|
403
|
+
if (!this.db) {
|
|
404
|
+
const state = this.readJsonState();
|
|
405
|
+
if (!state.sessions[sessionId]) return false;
|
|
406
|
+
state.sessions[sessionId].title = title;
|
|
407
|
+
state.sessions[sessionId].updatedAt = updatedAt;
|
|
408
|
+
this.writeJsonState(state);
|
|
409
|
+
try {
|
|
410
|
+
renameSessionIndex(sessionId, title);
|
|
411
|
+
} catch {
|
|
412
|
+
// Keep the project-local JSON store as the source of truth.
|
|
413
|
+
}
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
303
416
|
const result = this.db
|
|
304
417
|
.prepare("UPDATE sessions SET title = ?, updated_at = ? WHERE session_id = ?")
|
|
305
418
|
.run(title, updatedAt, sessionId);
|
|
@@ -308,6 +421,18 @@ export class WebDatabase {
|
|
|
308
421
|
}
|
|
309
422
|
|
|
310
423
|
deleteSession(sessionId) {
|
|
424
|
+
if (!this.db) {
|
|
425
|
+
const state = this.readJsonState();
|
|
426
|
+
if (!state.sessions[sessionId]) return false;
|
|
427
|
+
delete state.sessions[sessionId];
|
|
428
|
+
this.writeJsonState(state);
|
|
429
|
+
try {
|
|
430
|
+
deleteSessionIndex(sessionId);
|
|
431
|
+
} catch {
|
|
432
|
+
// Keep the project-local JSON store as the source of truth.
|
|
433
|
+
}
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
311
436
|
const result = this.db.prepare("DELETE FROM sessions WHERE session_id = ?").run(sessionId);
|
|
312
437
|
if (result.changes > 0) deleteSessionIndex(sessionId);
|
|
313
438
|
return result.changes > 0;
|