shariq-pi-extensions 0.1.0
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/LICENSE +21 -0
- package/README.md +94 -0
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/docs/ARCHITECTURE.md +54 -0
- package/docs/DEVELOPMENT.md +88 -0
- package/docs/EXTENSIONS.md +89 -0
- package/extensions/antigravity-provider/LICENSE +21 -0
- package/extensions/antigravity-provider/README.md +23 -0
- package/extensions/antigravity-provider/antigravity/cloud-code-assist.ts +519 -0
- package/extensions/antigravity-provider/antigravity/index.ts +67 -0
- package/extensions/antigravity-provider/antigravity/models.ts +184 -0
- package/extensions/antigravity-provider/antigravity/oauth.ts +499 -0
- package/extensions/antigravity-provider/index.ts +3 -0
- package/extensions/ask-user/README.md +21 -0
- package/extensions/ask-user/index.ts +417 -0
- package/extensions/background-terminals/README.md +43 -0
- package/extensions/background-terminals/index.ts +400 -0
- package/extensions/background-terminals/src/manager.ts +480 -0
- package/extensions/background-terminals/src/output-buffer.ts +110 -0
- package/extensions/background-terminals/src/presentation.ts +74 -0
- package/extensions/background-terminals/src/types.ts +47 -0
- package/extensions/background-terminals/src/ui.ts +478 -0
- package/extensions/context-usage/LICENSE +21 -0
- package/extensions/context-usage/README.md +20 -0
- package/extensions/context-usage/index.ts +2006 -0
- package/extensions/context-usage/lib/ansi.ts +60 -0
- package/extensions/context-usage/lib/boundary.ts +27 -0
- package/extensions/context-usage/lib/chat.ts +190 -0
- package/extensions/context-usage/lib/config.ts +30 -0
- package/extensions/context-usage/lib/fmt.ts +59 -0
- package/extensions/context-usage/lib/style.ts +260 -0
- package/extensions/copy-all/README.md +15 -0
- package/extensions/copy-all/index.ts +72 -0
- package/extensions/factory-provider/API_KEYS.md +17 -0
- package/extensions/factory-provider/AUTH.md +15 -0
- package/extensions/factory-provider/README.md +90 -0
- package/extensions/factory-provider/REFRESH_MODELS.md +185 -0
- package/extensions/factory-provider/UPDATING.md +97 -0
- package/extensions/factory-provider/factory/api-keys.ts +407 -0
- package/extensions/factory-provider/factory/auth.ts +132 -0
- package/extensions/factory-provider/factory/constants.ts +15 -0
- package/extensions/factory-provider/factory/dashboard.ts +363 -0
- package/extensions/factory-provider/factory/droid.ts +84 -0
- package/extensions/factory-provider/factory/limits.ts +342 -0
- package/extensions/factory-provider/factory/models.ts +283 -0
- package/extensions/factory-provider/factory/responses.ts +331 -0
- package/extensions/factory-provider/factory/warm-theme.ts +53 -0
- package/extensions/factory-provider/factory/websocket.ts +130 -0
- package/extensions/factory-provider/index.ts +204 -0
- package/extensions/factory-provider/scripts/audit-reasoning-efforts.py +84 -0
- package/extensions/firecrawl-web/README.md +31 -0
- package/extensions/firecrawl-web/auth.ts +92 -0
- package/extensions/firecrawl-web/client.ts +238 -0
- package/extensions/firecrawl-web/index.ts +96 -0
- package/extensions/firecrawl-web/output.ts +125 -0
- package/extensions/git-info/README.md +25 -0
- package/extensions/git-info/index.ts +162 -0
- package/extensions/git-info/src/git.ts +206 -0
- package/extensions/git-info/src/ui.ts +291 -0
- package/extensions/goal/README.md +44 -0
- package/extensions/goal/constants.ts +4 -0
- package/extensions/goal/extension.ts +1064 -0
- package/extensions/goal/index.ts +2 -0
- package/extensions/goal/types.ts +56 -0
- package/extensions/goal/ui.ts +334 -0
- package/extensions/orchestration/README.md +58 -0
- package/extensions/orchestration/dashboard.ts +192 -0
- package/extensions/orchestration/engine.ts +706 -0
- package/extensions/orchestration/index.ts +243 -0
- package/extensions/orchestration/prompts.ts +124 -0
- package/extensions/orchestration/protocol.ts +116 -0
- package/extensions/orchestration/settings-ui.ts +41 -0
- package/extensions/orchestration/settings.ts +59 -0
- package/extensions/orchestration/storage.ts +49 -0
- package/extensions/orchestration/types.ts +105 -0
- package/extensions/performance-status/README.md +5 -0
- package/extensions/performance-status/index.ts +171 -0
- package/extensions/pi-memory/README.md +53 -0
- package/extensions/pi-memory/index.ts +326 -0
- package/extensions/pi-memory/src/config.ts +75 -0
- package/extensions/pi-memory/src/database.ts +617 -0
- package/extensions/pi-memory/src/extraction.ts +178 -0
- package/extensions/pi-memory/src/project.ts +80 -0
- package/extensions/pi-memory/src/projection.ts +59 -0
- package/extensions/pi-memory/src/redaction.ts +60 -0
- package/extensions/pi-memory/src/retrieval.ts +30 -0
- package/extensions/pi-memory/src/service.ts +194 -0
- package/extensions/pi-memory/src/session.ts +146 -0
- package/extensions/pi-memory/src/types.ts +82 -0
- package/extensions/shared/README.md +18 -0
- package/extensions/shared/activity-status.ts +31 -0
- package/extensions/shared/child-session.ts +150 -0
- package/extensions/shared/context-utilization.ts +47 -0
- package/extensions/shared/dashboard-state.ts +99 -0
- package/extensions/shared/tool-call-timeout.ts +104 -0
- package/extensions/shared/tui-dashboard.ts +139 -0
- package/extensions/shell-shortcuts/README.md +17 -0
- package/extensions/shell-shortcuts/features/exit-alias.ts +10 -0
- package/extensions/shell-shortcuts/index.ts +6 -0
- package/extensions/subagents/README.md +89 -0
- package/extensions/subagents/index.ts +1386 -0
- package/extensions/subagents/src/backend.ts +69 -0
- package/extensions/subagents/src/backends/pi.ts +781 -0
- package/extensions/subagents/src/catalog.ts +85 -0
- package/extensions/subagents/src/config.ts +241 -0
- package/extensions/subagents/src/context.ts +57 -0
- package/extensions/subagents/src/coordinator.ts +64 -0
- package/extensions/subagents/src/domain.ts +294 -0
- package/extensions/subagents/src/format.ts +74 -0
- package/extensions/subagents/src/manager.ts +813 -0
- package/extensions/subagents/src/prompt.ts +94 -0
- package/extensions/subagents/src/result-delivery.ts +20 -0
- package/extensions/subagents/src/runtime.ts +48 -0
- package/extensions/subagents/src/ui/takeover.ts +695 -0
- package/extensions/subagents/src/ui/transcript.ts +182 -0
- package/extensions/subagents/src/worktree.ts +175 -0
- package/extensions/web-fetch/README.md +13 -0
- package/extensions/web-fetch/index.ts +441 -0
- package/package.json +117 -0
- package/skills/background-terminals/SKILL.md +42 -0
- package/skills/orchestration/SKILL.md +33 -0
- package/skills/subagents/SKILL.md +89 -0
- package/themes/ember-warm-dark.json +85 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { redactSecrets } from "./redaction.ts";
|
|
6
|
+
import type {
|
|
7
|
+
CaptureJob,
|
|
8
|
+
CapturedSession,
|
|
9
|
+
MemoryCandidate,
|
|
10
|
+
MemoryKind,
|
|
11
|
+
MemoryRecord,
|
|
12
|
+
MemoryScope,
|
|
13
|
+
MemorySourceKind,
|
|
14
|
+
ProjectIdentity,
|
|
15
|
+
SearchResult,
|
|
16
|
+
} from "./types.ts";
|
|
17
|
+
|
|
18
|
+
const SCHEMA_VERSION = 5;
|
|
19
|
+
|
|
20
|
+
type Row = Record<string, unknown>;
|
|
21
|
+
|
|
22
|
+
function jsonStringArray(value: unknown): string[] {
|
|
23
|
+
if (typeof value !== "string") return [];
|
|
24
|
+
try {
|
|
25
|
+
const parsed: unknown = JSON.parse(value);
|
|
26
|
+
return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === "string") : [];
|
|
27
|
+
} catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function memoryFromRow(row: Row): MemoryRecord {
|
|
33
|
+
return {
|
|
34
|
+
id: Number(row.id),
|
|
35
|
+
memoryId: String(row.memory_id),
|
|
36
|
+
scope: String(row.scope) as MemoryScope,
|
|
37
|
+
projectId: row.project_id === null ? null : String(row.project_id),
|
|
38
|
+
kind: String(row.kind) as MemoryKind,
|
|
39
|
+
title: String(row.title),
|
|
40
|
+
content: String(row.content),
|
|
41
|
+
tags: jsonStringArray(row.tags),
|
|
42
|
+
importance: Number(row.importance),
|
|
43
|
+
confidence: Number(row.confidence),
|
|
44
|
+
status: String(row.status) as MemoryRecord["status"],
|
|
45
|
+
sourceKind: String(row.source_kind) as MemorySourceKind,
|
|
46
|
+
sourceRef: String(row.source_ref),
|
|
47
|
+
sourceEntryIds: jsonStringArray(row.source_entry_ids),
|
|
48
|
+
fingerprint: String(row.fingerprint),
|
|
49
|
+
createdAt: Number(row.created_at),
|
|
50
|
+
updatedAt: Number(row.updated_at),
|
|
51
|
+
lastAccessedAt: row.last_accessed_at === null ? null : Number(row.last_accessed_at),
|
|
52
|
+
accessCount: Number(row.access_count),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function memoryFingerprint(scope: MemoryScope, projectId: string | null, kind: MemoryKind, content: string): string {
|
|
57
|
+
const normalized = content.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
58
|
+
return crypto.createHash("sha256").update(`${scope}\0${projectId ?? ""}\0${kind}\0${normalized}`).digest("hex");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class MemoryDatabase {
|
|
62
|
+
readonly db: DatabaseSync;
|
|
63
|
+
readonly databasePath: string;
|
|
64
|
+
|
|
65
|
+
constructor(databasePath: string) {
|
|
66
|
+
this.databasePath = databasePath;
|
|
67
|
+
fs.mkdirSync(path.dirname(databasePath), { recursive: true, mode: 0o700 });
|
|
68
|
+
this.db = new DatabaseSync(databasePath);
|
|
69
|
+
fs.chmodSync(databasePath, 0o600);
|
|
70
|
+
this.db.exec("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000;");
|
|
71
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
72
|
+
const auxiliary = `${databasePath}${suffix}`;
|
|
73
|
+
if (fs.existsSync(auxiliary)) fs.chmodSync(auxiliary, 0o600);
|
|
74
|
+
}
|
|
75
|
+
this.migrate();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
close(): void {
|
|
79
|
+
this.db.close();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private migrate(): void {
|
|
83
|
+
this.db.exec(`
|
|
84
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
85
|
+
key TEXT PRIMARY KEY,
|
|
86
|
+
value TEXT NOT NULL
|
|
87
|
+
);
|
|
88
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
89
|
+
id TEXT PRIMARY KEY,
|
|
90
|
+
identity TEXT NOT NULL UNIQUE,
|
|
91
|
+
root_path TEXT NOT NULL,
|
|
92
|
+
display_name TEXT NOT NULL,
|
|
93
|
+
directory_name TEXT NOT NULL,
|
|
94
|
+
created_at INTEGER NOT NULL,
|
|
95
|
+
last_seen_at INTEGER NOT NULL
|
|
96
|
+
);
|
|
97
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
98
|
+
id INTEGER PRIMARY KEY,
|
|
99
|
+
memory_id TEXT NOT NULL UNIQUE,
|
|
100
|
+
scope TEXT NOT NULL CHECK(scope IN ('global', 'project')),
|
|
101
|
+
project_id TEXT REFERENCES projects(id),
|
|
102
|
+
kind TEXT NOT NULL CHECK(kind IN ('preference','decision','convention','fact','solution','warning','reference')),
|
|
103
|
+
title TEXT NOT NULL,
|
|
104
|
+
content TEXT NOT NULL,
|
|
105
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
106
|
+
importance INTEGER NOT NULL CHECK(importance BETWEEN 1 AND 5),
|
|
107
|
+
confidence REAL NOT NULL CHECK(confidence BETWEEN 0 AND 1),
|
|
108
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','superseded','deleted')),
|
|
109
|
+
source_kind TEXT NOT NULL,
|
|
110
|
+
source_ref TEXT NOT NULL,
|
|
111
|
+
source_entry_ids TEXT NOT NULL DEFAULT '[]',
|
|
112
|
+
fingerprint TEXT NOT NULL UNIQUE,
|
|
113
|
+
created_at INTEGER NOT NULL,
|
|
114
|
+
updated_at INTEGER NOT NULL,
|
|
115
|
+
last_accessed_at INTEGER,
|
|
116
|
+
access_count INTEGER NOT NULL DEFAULT 0,
|
|
117
|
+
superseded_by TEXT
|
|
118
|
+
);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS memories_scope_project_status ON memories(scope, project_id, status);
|
|
120
|
+
CREATE INDEX IF NOT EXISTS memories_updated_at ON memories(updated_at DESC);
|
|
121
|
+
CREATE TABLE IF NOT EXISTS memory_sources (
|
|
122
|
+
id INTEGER PRIMARY KEY,
|
|
123
|
+
memory_id TEXT NOT NULL REFERENCES memories(memory_id) ON DELETE CASCADE,
|
|
124
|
+
source_kind TEXT NOT NULL,
|
|
125
|
+
source_ref TEXT NOT NULL,
|
|
126
|
+
source_entry_ids TEXT NOT NULL DEFAULT '[]',
|
|
127
|
+
created_at INTEGER NOT NULL,
|
|
128
|
+
UNIQUE(memory_id, source_kind, source_ref)
|
|
129
|
+
);
|
|
130
|
+
CREATE INDEX IF NOT EXISTS memory_sources_memory ON memory_sources(memory_id);
|
|
131
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
|
|
132
|
+
title, content, tags,
|
|
133
|
+
content='memories', content_rowid='id',
|
|
134
|
+
tokenize='porter unicode61 remove_diacritics 2'
|
|
135
|
+
);
|
|
136
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
137
|
+
INSERT INTO memory_fts(rowid, title, content, tags) VALUES (new.id, new.title, new.content, new.tags);
|
|
138
|
+
END;
|
|
139
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
140
|
+
INSERT INTO memory_fts(memory_fts, rowid, title, content, tags) VALUES ('delete', old.id, old.title, old.content, old.tags);
|
|
141
|
+
END;
|
|
142
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE OF title, content, tags ON memories BEGIN
|
|
143
|
+
INSERT INTO memory_fts(memory_fts, rowid, title, content, tags) VALUES ('delete', old.id, old.title, old.content, old.tags);
|
|
144
|
+
INSERT INTO memory_fts(rowid, title, content, tags) VALUES (new.id, new.title, new.content, new.tags);
|
|
145
|
+
END;
|
|
146
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
147
|
+
id INTEGER PRIMARY KEY,
|
|
148
|
+
job_key TEXT NOT NULL UNIQUE,
|
|
149
|
+
session_id TEXT NOT NULL,
|
|
150
|
+
project_id TEXT NOT NULL,
|
|
151
|
+
status TEXT NOT NULL CHECK(status IN ('pending','running','complete','failed')),
|
|
152
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
153
|
+
next_attempt_at INTEGER NOT NULL DEFAULT 0,
|
|
154
|
+
lease_owner TEXT,
|
|
155
|
+
lease_expires_at INTEGER,
|
|
156
|
+
payload TEXT NOT NULL,
|
|
157
|
+
error TEXT,
|
|
158
|
+
created_at INTEGER NOT NULL,
|
|
159
|
+
updated_at INTEGER NOT NULL
|
|
160
|
+
);
|
|
161
|
+
CREATE INDEX IF NOT EXISTS jobs_ready ON jobs(status, next_attempt_at, lease_expires_at);
|
|
162
|
+
CREATE TABLE IF NOT EXISTS session_checkpoints (
|
|
163
|
+
session_id TEXT PRIMARY KEY,
|
|
164
|
+
leaf_id TEXT NOT NULL,
|
|
165
|
+
processed_at INTEGER NOT NULL
|
|
166
|
+
);
|
|
167
|
+
CREATE TABLE IF NOT EXISTS session_capture_watermarks (
|
|
168
|
+
session_id TEXT PRIMARY KEY,
|
|
169
|
+
leaf_id TEXT NOT NULL,
|
|
170
|
+
job_id INTEGER NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
|
171
|
+
updated_at INTEGER NOT NULL
|
|
172
|
+
);
|
|
173
|
+
`);
|
|
174
|
+
this.detachBootstrapImports();
|
|
175
|
+
this.db.exec("DROP TABLE IF EXISTS imports");
|
|
176
|
+
this.db.prepare("INSERT INTO metadata(key, value) VALUES('schema_version', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(String(SCHEMA_VERSION));
|
|
177
|
+
this.db.exec(`
|
|
178
|
+
INSERT OR IGNORE INTO memory_sources(memory_id, source_kind, source_ref, source_entry_ids, created_at)
|
|
179
|
+
SELECT memory_id, source_kind, source_ref, source_entry_ids, created_at FROM memories
|
|
180
|
+
`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private detachBootstrapImports(): void {
|
|
184
|
+
const detachedRef = (kind: string, reference: string) =>
|
|
185
|
+
`pi-bootstrap://${crypto.createHash("sha256").update(`${kind}\0${reference}`).digest("hex")}`;
|
|
186
|
+
|
|
187
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
188
|
+
try {
|
|
189
|
+
const sourceRows = this.db.prepare(`
|
|
190
|
+
SELECT id, source_kind, source_ref FROM memory_sources
|
|
191
|
+
WHERE source_kind IN ('codex-import', 'grok-import')
|
|
192
|
+
`).all() as Row[];
|
|
193
|
+
const updateSource = this.db.prepare("UPDATE memory_sources SET source_kind='bootstrap-import', source_ref=? WHERE id=?");
|
|
194
|
+
for (const row of sourceRows) updateSource.run(detachedRef(String(row.source_kind), String(row.source_ref)), Number(row.id));
|
|
195
|
+
|
|
196
|
+
const memoryRows = this.db.prepare(`
|
|
197
|
+
SELECT id, source_kind, source_ref FROM memories
|
|
198
|
+
WHERE source_kind IN ('codex-import', 'grok-import')
|
|
199
|
+
`).all() as Row[];
|
|
200
|
+
const updateMemory = this.db.prepare("UPDATE memories SET source_kind='bootstrap-import', source_ref=? WHERE id=?");
|
|
201
|
+
for (const row of memoryRows) updateMemory.run(detachedRef(String(row.source_kind), String(row.source_ref)), Number(row.id));
|
|
202
|
+
|
|
203
|
+
const formerAgentProjects = (this.db.prepare("SELECT id, root_path FROM projects").all() as Row[])
|
|
204
|
+
.filter((row) => [".codex", ".grok"].includes(path.basename(String(row.root_path))));
|
|
205
|
+
const deleteProjectMemories = this.db.prepare("DELETE FROM memories WHERE project_id=?");
|
|
206
|
+
const deleteProject = this.db.prepare("DELETE FROM projects WHERE id=?");
|
|
207
|
+
for (const project of formerAgentProjects) {
|
|
208
|
+
deleteProjectMemories.run(String(project.id));
|
|
209
|
+
deleteProject.run(String(project.id));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.db.exec("COMMIT");
|
|
213
|
+
} catch (error) {
|
|
214
|
+
this.db.exec("ROLLBACK");
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
upsertProject(project: ProjectIdentity): void {
|
|
220
|
+
const now = Date.now();
|
|
221
|
+
this.db.prepare(`
|
|
222
|
+
INSERT INTO projects(id, identity, root_path, display_name, directory_name, created_at, last_seen_at)
|
|
223
|
+
VALUES(?, ?, ?, ?, ?, ?, ?)
|
|
224
|
+
ON CONFLICT(id) DO UPDATE SET root_path=excluded.root_path, display_name=excluded.display_name,
|
|
225
|
+
directory_name=excluded.directory_name, last_seen_at=excluded.last_seen_at
|
|
226
|
+
`).run(project.id, project.identity, project.rootPath, project.displayName, project.directoryName, now, now);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
getProject(id: string): ProjectIdentity | undefined {
|
|
230
|
+
const row = this.db.prepare("SELECT * FROM projects WHERE id = ?").get(id) as Row | undefined;
|
|
231
|
+
if (!row) return undefined;
|
|
232
|
+
return {
|
|
233
|
+
id: String(row.id),
|
|
234
|
+
identity: String(row.identity),
|
|
235
|
+
rootPath: String(row.root_path),
|
|
236
|
+
displayName: String(row.display_name),
|
|
237
|
+
directoryName: String(row.directory_name),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
listProjects(): ProjectIdentity[] {
|
|
242
|
+
return (this.db.prepare("SELECT * FROM projects ORDER BY last_seen_at DESC").all() as Row[]).map((row) => ({
|
|
243
|
+
id: String(row.id), identity: String(row.identity), rootPath: String(row.root_path),
|
|
244
|
+
displayName: String(row.display_name), directoryName: String(row.directory_name),
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
addMemory(input: {
|
|
249
|
+
candidate: Omit<MemoryCandidate, "operation" | "targetId">;
|
|
250
|
+
projectId: string | null;
|
|
251
|
+
sourceKind: MemorySourceKind;
|
|
252
|
+
sourceRef: string;
|
|
253
|
+
}): MemoryRecord {
|
|
254
|
+
const candidate = {
|
|
255
|
+
...input.candidate,
|
|
256
|
+
title: redactSecrets(input.candidate.title),
|
|
257
|
+
content: redactSecrets(input.candidate.content),
|
|
258
|
+
tags: input.candidate.tags.map(redactSecrets),
|
|
259
|
+
};
|
|
260
|
+
const projectId = candidate.scope === "project" ? input.projectId : null;
|
|
261
|
+
if (candidate.scope === "project" && !projectId) throw new Error("project memory requires a project id");
|
|
262
|
+
const fingerprint = memoryFingerprint(candidate.scope, projectId, candidate.kind, candidate.content);
|
|
263
|
+
const now = Date.now();
|
|
264
|
+
const memoryId = crypto.randomUUID();
|
|
265
|
+
this.db.prepare(`
|
|
266
|
+
INSERT INTO memories(memory_id, scope, project_id, kind, title, content, tags, importance,
|
|
267
|
+
confidence, status, source_kind, source_ref, source_entry_ids, fingerprint, created_at, updated_at)
|
|
268
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)
|
|
269
|
+
ON CONFLICT(fingerprint) DO UPDATE SET
|
|
270
|
+
title=excluded.title,
|
|
271
|
+
tags=excluded.tags,
|
|
272
|
+
importance=MAX(memories.importance, excluded.importance),
|
|
273
|
+
confidence=MAX(memories.confidence, excluded.confidence),
|
|
274
|
+
source_entry_ids=excluded.source_entry_ids,
|
|
275
|
+
updated_at=excluded.updated_at,
|
|
276
|
+
status=CASE
|
|
277
|
+
WHEN memories.status='deleted' AND excluded.source_kind<>'manual' THEN 'deleted'
|
|
278
|
+
ELSE 'active'
|
|
279
|
+
END
|
|
280
|
+
`).run(
|
|
281
|
+
memoryId, candidate.scope, projectId, candidate.kind, candidate.title, candidate.content,
|
|
282
|
+
JSON.stringify(candidate.tags), candidate.importance, candidate.confidence, input.sourceKind,
|
|
283
|
+
redactSecrets(input.sourceRef), JSON.stringify(candidate.evidenceEntryIds), fingerprint, now, now,
|
|
284
|
+
);
|
|
285
|
+
const row = this.db.prepare("SELECT * FROM memories WHERE fingerprint = ?").get(fingerprint) as Row;
|
|
286
|
+
const memory = memoryFromRow(row);
|
|
287
|
+
if (memory.status === "active") {
|
|
288
|
+
this.addSource(memory.memoryId, input.sourceKind, input.sourceRef, candidate.evidenceEntryIds);
|
|
289
|
+
}
|
|
290
|
+
return memory;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
updateMemory(memoryId: string, candidate: Omit<MemoryCandidate, "operation" | "targetId">, projectId: string | null, sourceKind: MemorySourceKind, sourceRef: string): MemoryRecord | undefined {
|
|
294
|
+
const existing = this.getMemory(memoryId);
|
|
295
|
+
if (!existing || existing.status !== "active") return undefined;
|
|
296
|
+
candidate = {
|
|
297
|
+
...candidate,
|
|
298
|
+
title: redactSecrets(candidate.title),
|
|
299
|
+
content: redactSecrets(candidate.content),
|
|
300
|
+
tags: candidate.tags.map(redactSecrets),
|
|
301
|
+
};
|
|
302
|
+
const scopedProject = candidate.scope === "project" ? projectId : null;
|
|
303
|
+
if (candidate.scope === "project" && !scopedProject) return undefined;
|
|
304
|
+
const fingerprint = memoryFingerprint(candidate.scope, scopedProject, candidate.kind, candidate.content);
|
|
305
|
+
try {
|
|
306
|
+
this.db.prepare(`
|
|
307
|
+
UPDATE memories SET scope=?, project_id=?, kind=?, title=?, content=?, tags=?, importance=?, confidence=?,
|
|
308
|
+
source_ref=?, source_entry_ids=?, fingerprint=?, updated_at=? WHERE memory_id=? AND status='active'
|
|
309
|
+
`).run(candidate.scope, scopedProject, candidate.kind, candidate.title, candidate.content,
|
|
310
|
+
JSON.stringify(candidate.tags), candidate.importance, candidate.confidence, redactSecrets(sourceRef),
|
|
311
|
+
JSON.stringify(candidate.evidenceEntryIds), fingerprint, Date.now(), memoryId);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
if (error instanceof Error && error.message.includes("UNIQUE")) {
|
|
314
|
+
const collision = this.findByFingerprint(fingerprint);
|
|
315
|
+
throw new Error(
|
|
316
|
+
collision
|
|
317
|
+
? `Correction would duplicate active memory ${collision.memoryId}.`
|
|
318
|
+
: "Correction would duplicate another memory.",
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
const updated = this.getMemory(memoryId);
|
|
324
|
+
if (updated) this.addSource(updated.memoryId, sourceKind, sourceRef, candidate.evidenceEntryIds);
|
|
325
|
+
return updated;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private addSource(memoryId: string, sourceKind: MemorySourceKind, sourceRef: string, entryIds: string[]): void {
|
|
329
|
+
this.db.prepare(`
|
|
330
|
+
INSERT INTO memory_sources(memory_id, source_kind, source_ref, source_entry_ids, created_at)
|
|
331
|
+
VALUES(?, ?, ?, ?, ?) ON CONFLICT(memory_id, source_kind, source_ref) DO UPDATE SET
|
|
332
|
+
source_entry_ids=excluded.source_entry_ids
|
|
333
|
+
`).run(memoryId, sourceKind, redactSecrets(sourceRef), JSON.stringify(entryIds), Date.now());
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
listSources(memoryId: string): Array<{ sourceKind: string; sourceRef: string; sourceEntryIds: string[]; createdAt: number }> {
|
|
337
|
+
const rows = this.db.prepare("SELECT * FROM memory_sources WHERE memory_id=? ORDER BY created_at ASC").all(memoryId) as Row[];
|
|
338
|
+
return rows.map((row) => ({
|
|
339
|
+
sourceKind: String(row.source_kind),
|
|
340
|
+
sourceRef: String(row.source_ref),
|
|
341
|
+
sourceEntryIds: jsonStringArray(row.source_entry_ids),
|
|
342
|
+
createdAt: Number(row.created_at),
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
supersedeMemory(memoryId: string, replacement: Omit<MemoryCandidate, "operation" | "targetId">, projectId: string | null, sourceKind: MemorySourceKind, sourceRef: string): MemoryRecord | undefined {
|
|
347
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
348
|
+
try {
|
|
349
|
+
const created = this.addMemory({ candidate: replacement, projectId, sourceKind, sourceRef });
|
|
350
|
+
this.db.prepare("UPDATE memories SET status='superseded', superseded_by=?, updated_at=? WHERE memory_id=? AND memory_id<>?")
|
|
351
|
+
.run(created.memoryId, Date.now(), memoryId, created.memoryId);
|
|
352
|
+
this.db.exec("COMMIT");
|
|
353
|
+
return created;
|
|
354
|
+
} catch (error) {
|
|
355
|
+
this.db.exec("ROLLBACK");
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
getMemory(memoryId: string): MemoryRecord | undefined {
|
|
361
|
+
const row = this.db.prepare("SELECT * FROM memories WHERE memory_id = ?").get(memoryId) as Row | undefined;
|
|
362
|
+
return row ? memoryFromRow(row) : undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
findByFingerprint(fingerprint: string): MemoryRecord | undefined {
|
|
366
|
+
const row = this.db.prepare("SELECT * FROM memories WHERE fingerprint = ?").get(fingerprint) as Row | undefined;
|
|
367
|
+
return row ? memoryFromRow(row) : undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
removeFormerAgentReferences(): number {
|
|
371
|
+
return Number(this.db.prepare(`
|
|
372
|
+
UPDATE memories SET status='deleted', updated_at=?
|
|
373
|
+
WHERE status='active' AND (
|
|
374
|
+
instr(lower(title), 'codex')>0 OR instr(lower(content), 'codex')>0 OR
|
|
375
|
+
instr(lower(title), 'grok')>0 OR instr(lower(content), 'grok')>0
|
|
376
|
+
)
|
|
377
|
+
`).run(Date.now()).changes);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
forgetMemory(memoryId: string): boolean {
|
|
381
|
+
return this.db.prepare("UPDATE memories SET status='deleted', updated_at=? WHERE memory_id=? AND status='active'")
|
|
382
|
+
.run(Date.now(), memoryId).changes > 0;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
listActive(scope?: MemoryScope, projectId?: string | null, limit = 500): MemoryRecord[] {
|
|
386
|
+
let sql = "SELECT * FROM memories WHERE status='active'";
|
|
387
|
+
const params: Array<string | number | null> = [];
|
|
388
|
+
if (scope) { sql += " AND scope=?"; params.push(scope); }
|
|
389
|
+
if (projectId !== undefined) { sql += " AND project_id IS ?"; params.push(projectId); }
|
|
390
|
+
sql += " ORDER BY importance DESC, updated_at DESC LIMIT ?";
|
|
391
|
+
params.push(limit);
|
|
392
|
+
return (this.db.prepare(sql).all(...params) as Row[]).map(memoryFromRow);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
search(query: string, projectId: string, limit: number): SearchResult[] {
|
|
396
|
+
const tokens = [...new Set(query.toLowerCase().match(/[\p{L}\p{N}][\p{L}\p{N}_-]{1,}/gu) ?? [])]
|
|
397
|
+
.filter((token) => token.length > 1)
|
|
398
|
+
.slice(0, 24);
|
|
399
|
+
const now = Date.now();
|
|
400
|
+
const rows: Array<Row & { lexical_score?: unknown }> = [];
|
|
401
|
+
if (tokens.length > 0) {
|
|
402
|
+
const ftsQuery = tokens.map((token) => `"${token.replaceAll('"', '""')}"`).join(" OR ");
|
|
403
|
+
rows.push(...this.db.prepare(`
|
|
404
|
+
SELECT m.*, bm25(memory_fts, 5.0, 1.0, 2.0) AS lexical_score
|
|
405
|
+
FROM memory_fts JOIN memories m ON m.id = memory_fts.rowid
|
|
406
|
+
WHERE memory_fts MATCH ? AND m.status='active' AND (m.scope='global' OR m.project_id=?)
|
|
407
|
+
ORDER BY lexical_score ASC LIMIT ?
|
|
408
|
+
`).all(ftsQuery, projectId, limit * 3) as Array<Row & { lexical_score?: unknown }>);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const lexicalScores = new Map<number, number>();
|
|
412
|
+
if (rows.length > 0) {
|
|
413
|
+
const ranks = rows.map((row) => Number(row.lexical_score));
|
|
414
|
+
const best = Math.min(...ranks);
|
|
415
|
+
const worst = Math.max(...ranks);
|
|
416
|
+
const range = worst - best;
|
|
417
|
+
for (const row of rows) {
|
|
418
|
+
const rank = Number(row.lexical_score);
|
|
419
|
+
lexicalScores.set(Number(row.id), range <= Number.EPSILON ? 1 : (worst - rank) / range);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const pinned = this.db.prepare(`
|
|
424
|
+
SELECT m.*, 0.0 AS lexical_score FROM memories m
|
|
425
|
+
WHERE m.status='active' AND m.importance=5 AND (m.scope='global' OR m.project_id=?)
|
|
426
|
+
ORDER BY m.updated_at DESC LIMIT ?
|
|
427
|
+
`).all(projectId, limit) as Array<Row & { lexical_score?: unknown }>;
|
|
428
|
+
const byId = new Map<number, Row & { lexical_score?: unknown }>();
|
|
429
|
+
for (const row of [...rows, ...pinned]) if (!byId.has(Number(row.id))) byId.set(Number(row.id), row);
|
|
430
|
+
|
|
431
|
+
const queryTerms = new Set(tokens);
|
|
432
|
+
const scored = [...byId.values()].map((row) => {
|
|
433
|
+
const memory = memoryFromRow(row);
|
|
434
|
+
const lexical = lexicalScores.get(memory.id) ?? 0.22;
|
|
435
|
+
const haystack = `${memory.title} ${memory.tags.join(" ")} ${memory.content}`.toLowerCase();
|
|
436
|
+
const overlap = tokens.length === 0 ? 0 : tokens.filter((term) => haystack.includes(term)).length / tokens.length;
|
|
437
|
+
const importance = memory.importance / 5;
|
|
438
|
+
const confidence = memory.confidence;
|
|
439
|
+
const projectBoost = memory.scope === "project" ? 1.12 : 1;
|
|
440
|
+
const accessBoost = 1 + Math.log1p(memory.accessCount) * 0.025;
|
|
441
|
+
const ageDays = Math.max(0, now - memory.updatedAt) / 86_400_000;
|
|
442
|
+
const freshness = memory.kind === "fact" || memory.kind === "reference" ? Math.exp(-Math.log(2) * ageDays / 365) : 1;
|
|
443
|
+
const score = (0.48 * lexical + 0.22 * overlap + 0.16 * importance + 0.14 * confidence)
|
|
444
|
+
* projectBoost * accessBoost * (0.85 + 0.15 * freshness);
|
|
445
|
+
const terms = new Set(haystack.match(/[\p{L}\p{N}][\p{L}\p{N}_-]{1,}/gu) ?? []);
|
|
446
|
+
return { ...memory, score, _terms: terms, _queryTerms: queryTerms };
|
|
447
|
+
}).sort((a, b) => b.score - a.score);
|
|
448
|
+
|
|
449
|
+
const selected: typeof scored = [];
|
|
450
|
+
for (const candidate of scored) {
|
|
451
|
+
if (selected.length >= limit) break;
|
|
452
|
+
const redundant = selected.some((prior) => {
|
|
453
|
+
let intersection = 0;
|
|
454
|
+
for (const term of candidate._terms) if (prior._terms.has(term)) intersection += 1;
|
|
455
|
+
const union = candidate._terms.size + prior._terms.size - intersection;
|
|
456
|
+
return union > 0 && intersection / union > 0.82;
|
|
457
|
+
});
|
|
458
|
+
if (!redundant || candidate.importance === 5) selected.push(candidate);
|
|
459
|
+
}
|
|
460
|
+
return selected.map(({ _terms: _ignoredTerms, _queryTerms: _ignoredQuery, ...result }) => result);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
recordAccess(memoryIds: string[]): void {
|
|
464
|
+
if (memoryIds.length === 0) return;
|
|
465
|
+
const statement = this.db.prepare("UPDATE memories SET access_count=access_count+1, last_accessed_at=? WHERE memory_id=?");
|
|
466
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
467
|
+
try {
|
|
468
|
+
const now = Date.now();
|
|
469
|
+
for (const memoryId of memoryIds) statement.run(now, memoryId);
|
|
470
|
+
this.db.exec("COMMIT");
|
|
471
|
+
} catch (error) {
|
|
472
|
+
this.db.exec("ROLLBACK");
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
rebuildSearchIndex(): void {
|
|
478
|
+
this.db.exec("INSERT INTO memory_fts(memory_fts) VALUES('rebuild')");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
countMemories(): number {
|
|
482
|
+
const row = this.db.prepare("SELECT COUNT(*) AS count FROM memories WHERE status='active'").get() as Row;
|
|
483
|
+
return Number(row.count);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
countJobs(): Record<string, number> {
|
|
487
|
+
const rows = this.db.prepare("SELECT status, COUNT(*) AS count FROM jobs GROUP BY status").all() as Row[];
|
|
488
|
+
return Object.fromEntries(rows.map((row) => [String(row.status), Number(row.count)]));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
enqueueCapture(capture: CapturedSession): boolean {
|
|
492
|
+
const jobKey = crypto.createHash("sha256").update(`${capture.sessionId}\0${capture.leafId}`).digest("hex");
|
|
493
|
+
const now = Date.now();
|
|
494
|
+
const sanitizedCapture: CapturedSession = {
|
|
495
|
+
...capture,
|
|
496
|
+
transcript: redactSecrets(capture.transcript),
|
|
497
|
+
queryText: redactSecrets(capture.queryText),
|
|
498
|
+
};
|
|
499
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
500
|
+
try {
|
|
501
|
+
const inserted = this.db.prepare(`
|
|
502
|
+
INSERT INTO jobs(job_key, session_id, project_id, status, payload, created_at, updated_at)
|
|
503
|
+
VALUES(?, ?, ?, 'pending', ?, ?, ?) ON CONFLICT(job_key) DO NOTHING
|
|
504
|
+
`).run(jobKey, capture.sessionId, capture.project.id, JSON.stringify(sanitizedCapture), now, now);
|
|
505
|
+
const job = this.db.prepare("SELECT id FROM jobs WHERE job_key=?").get(jobKey) as Row;
|
|
506
|
+
this.db.prepare(`
|
|
507
|
+
INSERT INTO session_capture_watermarks(session_id, leaf_id, job_id, updated_at) VALUES(?, ?, ?, ?)
|
|
508
|
+
ON CONFLICT(session_id) DO UPDATE SET leaf_id=excluded.leaf_id, job_id=excluded.job_id, updated_at=excluded.updated_at
|
|
509
|
+
`).run(capture.sessionId, capture.leafId, Number(job.id), now);
|
|
510
|
+
this.db.exec("COMMIT");
|
|
511
|
+
return inserted.changes > 0;
|
|
512
|
+
} catch (error) {
|
|
513
|
+
this.db.exec("ROLLBACK");
|
|
514
|
+
throw error;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
claimNextJob(owner: string, leaseMs: number, maxAttempts: number): CaptureJob | undefined {
|
|
519
|
+
const now = Date.now();
|
|
520
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
521
|
+
try {
|
|
522
|
+
const row = this.db.prepare(`
|
|
523
|
+
SELECT candidate.* FROM jobs candidate
|
|
524
|
+
WHERE candidate.attempts < ? AND candidate.next_attempt_at <= ? AND (
|
|
525
|
+
candidate.status='pending' OR (candidate.status='running' AND COALESCE(candidate.lease_expires_at, 0) < ?)
|
|
526
|
+
) AND NOT EXISTS (
|
|
527
|
+
SELECT 1 FROM jobs active
|
|
528
|
+
WHERE active.session_id=candidate.session_id AND active.id<>candidate.id
|
|
529
|
+
AND active.status='running' AND COALESCE(active.lease_expires_at, 0) >= ?
|
|
530
|
+
)
|
|
531
|
+
ORDER BY candidate.created_at ASC LIMIT 1
|
|
532
|
+
`).get(maxAttempts, now, now, now) as Row | undefined;
|
|
533
|
+
if (!row) { this.db.exec("COMMIT"); return undefined; }
|
|
534
|
+
const updated = this.db.prepare(`
|
|
535
|
+
UPDATE jobs SET status='running', attempts=attempts+1, lease_owner=?, lease_expires_at=?, updated_at=?
|
|
536
|
+
WHERE id=? AND (status='pending' OR COALESCE(lease_expires_at, 0) < ?)
|
|
537
|
+
`).run(owner, now + leaseMs, now, Number(row.id), now);
|
|
538
|
+
if (updated.changes === 0) { this.db.exec("COMMIT"); return undefined; }
|
|
539
|
+
const claimed = this.db.prepare("SELECT * FROM jobs WHERE id=?").get(Number(row.id)) as Row;
|
|
540
|
+
this.db.exec("COMMIT");
|
|
541
|
+
return {
|
|
542
|
+
id: Number(claimed.id), jobKey: String(claimed.job_key), sessionId: String(claimed.session_id),
|
|
543
|
+
projectId: String(claimed.project_id), status: String(claimed.status) as CaptureJob["status"],
|
|
544
|
+
attempts: Number(claimed.attempts), payload: JSON.parse(String(claimed.payload)) as CapturedSession,
|
|
545
|
+
};
|
|
546
|
+
} catch (error) {
|
|
547
|
+
this.db.exec("ROLLBACK");
|
|
548
|
+
throw error;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
renewJobLease(jobId: number, owner: string, leaseMs: number): boolean {
|
|
553
|
+
const now = Date.now();
|
|
554
|
+
return this.db.prepare(`
|
|
555
|
+
UPDATE jobs SET lease_expires_at=?, updated_at=?
|
|
556
|
+
WHERE id=? AND status='running' AND lease_owner=? AND COALESCE(lease_expires_at, 0)>=?
|
|
557
|
+
`).run(now + leaseMs, now, jobId, owner, now).changes > 0;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
completeJob(job: CaptureJob, owner: string): boolean {
|
|
561
|
+
const now = Date.now();
|
|
562
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
563
|
+
try {
|
|
564
|
+
const completed = this.db.prepare("UPDATE jobs SET status='complete', lease_owner=NULL, lease_expires_at=NULL, error=NULL, updated_at=? WHERE id=? AND status='running' AND lease_owner=? AND COALESCE(lease_expires_at, 0)>=?")
|
|
565
|
+
.run(now, job.id, owner, now);
|
|
566
|
+
if (completed.changes === 0) {
|
|
567
|
+
this.db.exec("ROLLBACK");
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
this.db.prepare(`
|
|
571
|
+
INSERT INTO session_checkpoints(session_id, leaf_id, processed_at) VALUES(?, ?, ?)
|
|
572
|
+
ON CONFLICT(session_id) DO UPDATE SET leaf_id=excluded.leaf_id, processed_at=excluded.processed_at
|
|
573
|
+
WHERE excluded.processed_at >= session_checkpoints.processed_at
|
|
574
|
+
`).run(job.sessionId, job.payload.leafId, job.payload.capturedAt);
|
|
575
|
+
this.db.exec("COMMIT");
|
|
576
|
+
return true;
|
|
577
|
+
} catch (error) {
|
|
578
|
+
this.db.exec("ROLLBACK");
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
failJob(job: CaptureJob, owner: string, error: string, maxAttempts: number): boolean {
|
|
584
|
+
const now = Date.now();
|
|
585
|
+
const terminal = job.attempts >= maxAttempts;
|
|
586
|
+
const backoff = Math.min(60 * 60 * 1_000, 30_000 * 2 ** Math.max(0, job.attempts - 1));
|
|
587
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
588
|
+
try {
|
|
589
|
+
const failed = this.db.prepare(`
|
|
590
|
+
UPDATE jobs SET status=?, next_attempt_at=?, lease_owner=NULL, lease_expires_at=NULL, error=?, updated_at=?
|
|
591
|
+
WHERE id=? AND status='running' AND lease_owner=? AND COALESCE(lease_expires_at, 0)>=?
|
|
592
|
+
`).run(terminal ? "failed" : "pending", now + backoff, error.slice(0, 2_000), now, job.id, owner, now);
|
|
593
|
+
if (terminal && failed.changes > 0) {
|
|
594
|
+
this.db.prepare("DELETE FROM jobs WHERE session_id=? AND status='pending' AND created_at>=?")
|
|
595
|
+
.run(job.sessionId, job.payload.capturedAt);
|
|
596
|
+
this.db.prepare("DELETE FROM session_capture_watermarks WHERE session_id=?").run(job.sessionId);
|
|
597
|
+
}
|
|
598
|
+
this.db.exec("COMMIT");
|
|
599
|
+
return failed.changes > 0;
|
|
600
|
+
} catch (failure) {
|
|
601
|
+
this.db.exec("ROLLBACK");
|
|
602
|
+
throw failure;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
getCaptureCheckpoint(sessionId: string): string | undefined {
|
|
607
|
+
const watermark = this.db.prepare("SELECT leaf_id FROM session_capture_watermarks WHERE session_id=?").get(sessionId) as Row | undefined;
|
|
608
|
+
if (watermark) return String(watermark.leaf_id);
|
|
609
|
+
return this.getCheckpoint(sessionId);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
getCheckpoint(sessionId: string): string | undefined {
|
|
613
|
+
const row = this.db.prepare("SELECT leaf_id FROM session_checkpoints WHERE session_id=?").get(sessionId) as Row | undefined;
|
|
614
|
+
return row ? String(row.leaf_id) : undefined;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
}
|