nemoris 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/.env.example +49 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/SECURITY.md +119 -0
- package/bin/nemoris +46 -0
- package/config/agents/agent.toml.example +28 -0
- package/config/agents/default.toml +22 -0
- package/config/agents/orchestrator.toml +18 -0
- package/config/delivery.toml +73 -0
- package/config/embeddings.toml +5 -0
- package/config/identity/default-purpose.md +1 -0
- package/config/identity/default-soul.md +3 -0
- package/config/identity/orchestrator-purpose.md +1 -0
- package/config/identity/orchestrator-soul.md +1 -0
- package/config/improvement-targets.toml +15 -0
- package/config/jobs/heartbeat-check.toml +30 -0
- package/config/jobs/memory-rollup.toml +46 -0
- package/config/jobs/workspace-health.toml +63 -0
- package/config/mcp.toml +16 -0
- package/config/output-contracts.toml +17 -0
- package/config/peers.toml +32 -0
- package/config/peers.toml.example +32 -0
- package/config/policies/memory-default.toml +10 -0
- package/config/policies/memory-heartbeat.toml +5 -0
- package/config/policies/memory-ops.toml +10 -0
- package/config/policies/tools-heartbeat-minimal.toml +8 -0
- package/config/policies/tools-interactive-safe.toml +8 -0
- package/config/policies/tools-ops-bounded.toml +8 -0
- package/config/policies/tools-orchestrator.toml +7 -0
- package/config/providers/anthropic.toml +15 -0
- package/config/providers/ollama.toml +5 -0
- package/config/providers/openai-codex.toml +9 -0
- package/config/providers/openrouter.toml +5 -0
- package/config/router.toml +22 -0
- package/config/runtime.toml +114 -0
- package/config/skills/self-improvement.toml +15 -0
- package/config/skills/telegram-onboarding-spec.md +240 -0
- package/config/skills/workspace-monitor.toml +15 -0
- package/config/task-router.toml +42 -0
- package/install.sh +50 -0
- package/package.json +90 -0
- package/src/auth/auth-profiles.js +169 -0
- package/src/auth/openai-codex-oauth.js +285 -0
- package/src/battle.js +449 -0
- package/src/cli/help.js +265 -0
- package/src/cli/output-filter.js +49 -0
- package/src/cli/runtime-control.js +704 -0
- package/src/cli-main.js +2763 -0
- package/src/cli.js +78 -0
- package/src/config/loader.js +332 -0
- package/src/config/schema-validator.js +214 -0
- package/src/config/toml-lite.js +8 -0
- package/src/daemon/action-handlers.js +71 -0
- package/src/daemon/healing-tick.js +87 -0
- package/src/daemon/health-probes.js +90 -0
- package/src/daemon/notifier.js +57 -0
- package/src/daemon/nurse.js +218 -0
- package/src/daemon/repair-log.js +106 -0
- package/src/daemon/rule-staging.js +90 -0
- package/src/daemon/rules.js +29 -0
- package/src/daemon/telegram-commands.js +54 -0
- package/src/daemon/updater.js +85 -0
- package/src/jobs/job-runner.js +78 -0
- package/src/mcp/consumer.js +129 -0
- package/src/memory/active-recall.js +171 -0
- package/src/memory/backend-manager.js +97 -0
- package/src/memory/backends/file-backend.js +38 -0
- package/src/memory/backends/qmd-backend.js +219 -0
- package/src/memory/embedding-guards.js +24 -0
- package/src/memory/embedding-index.js +118 -0
- package/src/memory/embedding-service.js +179 -0
- package/src/memory/file-index.js +177 -0
- package/src/memory/memory-signature.js +5 -0
- package/src/memory/memory-store.js +648 -0
- package/src/memory/retrieval-planner.js +66 -0
- package/src/memory/scoring.js +145 -0
- package/src/memory/simhash.js +78 -0
- package/src/memory/sqlite-active-store.js +824 -0
- package/src/memory/write-policy.js +36 -0
- package/src/onboarding/aliases.js +33 -0
- package/src/onboarding/auth/api-key.js +224 -0
- package/src/onboarding/auth/ollama-detect.js +42 -0
- package/src/onboarding/clack-prompter.js +77 -0
- package/src/onboarding/doctor.js +530 -0
- package/src/onboarding/lock.js +42 -0
- package/src/onboarding/model-catalog.js +344 -0
- package/src/onboarding/phases/auth.js +589 -0
- package/src/onboarding/phases/build.js +130 -0
- package/src/onboarding/phases/choose.js +82 -0
- package/src/onboarding/phases/detect.js +98 -0
- package/src/onboarding/phases/hatch.js +216 -0
- package/src/onboarding/phases/identity.js +79 -0
- package/src/onboarding/phases/ollama.js +345 -0
- package/src/onboarding/phases/scaffold.js +99 -0
- package/src/onboarding/phases/telegram.js +377 -0
- package/src/onboarding/phases/validate.js +204 -0
- package/src/onboarding/phases/verify.js +206 -0
- package/src/onboarding/platform.js +482 -0
- package/src/onboarding/status-bar.js +95 -0
- package/src/onboarding/templates.js +794 -0
- package/src/onboarding/toml-writer.js +38 -0
- package/src/onboarding/tui.js +250 -0
- package/src/onboarding/uninstall.js +153 -0
- package/src/onboarding/wizard.js +499 -0
- package/src/providers/anthropic.js +168 -0
- package/src/providers/base.js +247 -0
- package/src/providers/circuit-breaker.js +136 -0
- package/src/providers/ollama.js +163 -0
- package/src/providers/openai-codex.js +149 -0
- package/src/providers/openrouter.js +136 -0
- package/src/providers/registry.js +36 -0
- package/src/providers/router.js +16 -0
- package/src/runtime/bootstrap-cache.js +47 -0
- package/src/runtime/capabilities-prompt.js +25 -0
- package/src/runtime/completion-ping.js +99 -0
- package/src/runtime/config-validator.js +121 -0
- package/src/runtime/context-ledger.js +360 -0
- package/src/runtime/cutover-readiness.js +42 -0
- package/src/runtime/daemon.js +729 -0
- package/src/runtime/delivery-ack.js +195 -0
- package/src/runtime/delivery-adapters/local-file.js +41 -0
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
- package/src/runtime/delivery-adapters/shadow.js +13 -0
- package/src/runtime/delivery-adapters/standalone-http.js +98 -0
- package/src/runtime/delivery-adapters/telegram.js +104 -0
- package/src/runtime/delivery-adapters/tui.js +128 -0
- package/src/runtime/delivery-manager.js +807 -0
- package/src/runtime/delivery-store.js +168 -0
- package/src/runtime/dependency-health.js +118 -0
- package/src/runtime/envelope.js +114 -0
- package/src/runtime/evaluation.js +1089 -0
- package/src/runtime/exec-approvals.js +216 -0
- package/src/runtime/executor.js +500 -0
- package/src/runtime/failure-ping.js +67 -0
- package/src/runtime/flows.js +83 -0
- package/src/runtime/guards.js +45 -0
- package/src/runtime/handoff.js +51 -0
- package/src/runtime/identity-cache.js +28 -0
- package/src/runtime/improvement-engine.js +109 -0
- package/src/runtime/improvement-harness.js +581 -0
- package/src/runtime/input-sanitiser.js +72 -0
- package/src/runtime/interaction-contract.js +347 -0
- package/src/runtime/lane-readiness.js +226 -0
- package/src/runtime/migration.js +323 -0
- package/src/runtime/model-resolution.js +78 -0
- package/src/runtime/network.js +64 -0
- package/src/runtime/notification-store.js +97 -0
- package/src/runtime/notifier.js +256 -0
- package/src/runtime/orchestrator.js +53 -0
- package/src/runtime/orphan-reaper.js +41 -0
- package/src/runtime/output-contract-schema.js +139 -0
- package/src/runtime/output-contract-validator.js +439 -0
- package/src/runtime/peer-readiness.js +69 -0
- package/src/runtime/peer-registry.js +133 -0
- package/src/runtime/pilot-status.js +108 -0
- package/src/runtime/prompt-builder.js +261 -0
- package/src/runtime/provider-attempt.js +582 -0
- package/src/runtime/report-fallback.js +71 -0
- package/src/runtime/result-normalizer.js +183 -0
- package/src/runtime/retention.js +74 -0
- package/src/runtime/review.js +244 -0
- package/src/runtime/route-job.js +15 -0
- package/src/runtime/run-store.js +38 -0
- package/src/runtime/schedule.js +88 -0
- package/src/runtime/scheduler-state.js +434 -0
- package/src/runtime/scheduler.js +656 -0
- package/src/runtime/session-compactor.js +182 -0
- package/src/runtime/session-search.js +155 -0
- package/src/runtime/slack-inbound.js +249 -0
- package/src/runtime/ssrf.js +102 -0
- package/src/runtime/status-aggregator.js +330 -0
- package/src/runtime/task-contract.js +140 -0
- package/src/runtime/task-packet.js +107 -0
- package/src/runtime/task-router.js +140 -0
- package/src/runtime/telegram-inbound.js +1565 -0
- package/src/runtime/token-counter.js +134 -0
- package/src/runtime/token-estimator.js +59 -0
- package/src/runtime/tool-loop.js +200 -0
- package/src/runtime/transport-server.js +311 -0
- package/src/runtime/tui-server.js +411 -0
- package/src/runtime/ulid.js +44 -0
- package/src/security/ssrf-check.js +197 -0
- package/src/setup.js +369 -0
- package/src/shadow/bridge.js +303 -0
- package/src/skills/loader.js +84 -0
- package/src/tools/catalog.json +49 -0
- package/src/tools/cli-delegate.js +44 -0
- package/src/tools/mcp-client.js +106 -0
- package/src/tools/micro/cancel-task.js +6 -0
- package/src/tools/micro/complete-task.js +6 -0
- package/src/tools/micro/fail-task.js +6 -0
- package/src/tools/micro/http-fetch.js +74 -0
- package/src/tools/micro/index.js +36 -0
- package/src/tools/micro/lcm-recall.js +60 -0
- package/src/tools/micro/list-dir.js +17 -0
- package/src/tools/micro/list-skills.js +46 -0
- package/src/tools/micro/load-skill.js +38 -0
- package/src/tools/micro/memory-search.js +45 -0
- package/src/tools/micro/read-file.js +11 -0
- package/src/tools/micro/session-search.js +54 -0
- package/src/tools/micro/shell-exec.js +43 -0
- package/src/tools/micro/trigger-job.js +79 -0
- package/src/tools/micro/web-search.js +58 -0
- package/src/tools/micro/workspace-paths.js +39 -0
- package/src/tools/micro/write-file.js +14 -0
- package/src/tools/micro/write-memory.js +41 -0
- package/src/tools/registry.js +348 -0
- package/src/tools/tool-result-contract.js +36 -0
- package/src/tui/chat.js +835 -0
- package/src/tui/renderer.js +175 -0
- package/src/tui/socket-client.js +217 -0
- package/src/utils/canonical-json.js +29 -0
- package/src/utils/compaction.js +30 -0
- package/src/utils/env-loader.js +5 -0
- package/src/utils/errors.js +80 -0
- package/src/utils/fs.js +101 -0
- package/src/utils/ids.js +5 -0
- package/src/utils/model-context-limits.js +30 -0
- package/src/utils/token-budget.js +74 -0
- package/src/utils/usage-cost.js +25 -0
- package/src/utils/usage-metrics.js +14 -0
- package/vendor/smol-toml-1.5.2.tgz +0 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { formatRuntimeError, RuntimeError } from "../utils/errors.js";
|
|
3
|
+
import { appendJsonLine, ensureDir, listFiles, readJson, readJsonLines, removePath, writeJson } from "../utils/fs.js";
|
|
4
|
+
import { DEFAULT_RETRIEVAL_BLEND, computeLexicalScore, scoreMemory } from "./scoring.js";
|
|
5
|
+
import { evaluateDurableWrite } from "./write-policy.js";
|
|
6
|
+
import { SqliteActiveStore, buildDuplicateSignature } from "./sqlite-active-store.js";
|
|
7
|
+
import { buildResultSignature } from "./memory-signature.js";
|
|
8
|
+
import { createRuntimeId } from "../utils/ids.js";
|
|
9
|
+
import { computeSimhash, simhashToHex } from "./simhash.js";
|
|
10
|
+
|
|
11
|
+
function nowIso() {
|
|
12
|
+
return new Date().toISOString();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// --- Secret write guard ---
|
|
16
|
+
const SECRET_PATTERN = /^(sk|key|token|secret|password)[_-][a-zA-Z0-9]{20,}/i;
|
|
17
|
+
const KNOWN_PREFIXES = [
|
|
18
|
+
/\bsk-ant-[a-zA-Z0-9_-]{10,}/,
|
|
19
|
+
/\bsk-[a-zA-Z0-9]{20,}/,
|
|
20
|
+
/\bkey-[a-zA-Z0-9]{20,}/
|
|
21
|
+
];
|
|
22
|
+
const ENV_REF_PATTERN = /^\$\{?\w+\}?$/;
|
|
23
|
+
|
|
24
|
+
export function containsPlaintextSecret(value) {
|
|
25
|
+
if (value == null) return false;
|
|
26
|
+
if (typeof value === "object") {
|
|
27
|
+
return Object.values(value).some((v) => containsPlaintextSecret(v));
|
|
28
|
+
}
|
|
29
|
+
if (typeof value !== "string") return false;
|
|
30
|
+
const trimmed = value.trim();
|
|
31
|
+
if (ENV_REF_PATTERN.test(trimmed)) return false;
|
|
32
|
+
if (SECRET_PATTERN.test(trimmed)) return true;
|
|
33
|
+
for (const re of KNOWN_PREFIXES) {
|
|
34
|
+
if (re.test(trimmed)) return true;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function guardAgainstSecrets(record) {
|
|
40
|
+
if (containsPlaintextSecret(record)) {
|
|
41
|
+
const err = new RuntimeError(
|
|
42
|
+
"Rejected: plaintext secret detected in memory write. Use env-backed SecretRef pattern instead.",
|
|
43
|
+
{ category: "security", context: { guard: "secret_write" }, recoverable: false }
|
|
44
|
+
);
|
|
45
|
+
throw new Error(formatRuntimeError(err));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- Cross-agent memory isolation ---
|
|
50
|
+
function guardAgentAccess(storeAgentId, requestedAgentId, options = {}) {
|
|
51
|
+
if (storeAgentId == null) return; // no owner configured — unrestricted
|
|
52
|
+
if (requestedAgentId === storeAgentId) return;
|
|
53
|
+
if (options.allowCrossAgentRead) return;
|
|
54
|
+
const err = new RuntimeError(
|
|
55
|
+
`Rejected: cross-agent memory access denied. Agent '${requestedAgentId}' cannot access memory of agent '${storeAgentId}'.`,
|
|
56
|
+
{ category: "memory", context: { storeAgentId, requestedAgentId }, recoverable: false }
|
|
57
|
+
);
|
|
58
|
+
throw new Error(formatRuntimeError(err));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- Protected path guard (inspired by Codex security model) ---
|
|
62
|
+
const PROTECTED_PATH_PATTERNS = [
|
|
63
|
+
/\bconfig\//,
|
|
64
|
+
/\bTRUST_POLICY\.md\b/i,
|
|
65
|
+
/\bruntime\.toml\b/i,
|
|
66
|
+
/(?:^|[\s/])\.github\//,
|
|
67
|
+
/\bpackage\.json\b/i,
|
|
68
|
+
/\bpackage-lock\.json\b/i,
|
|
69
|
+
/(?:^|[\s/])\.env\b/
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
export function isProtectedPath(value) {
|
|
73
|
+
if (value == null) return false;
|
|
74
|
+
const str = String(value);
|
|
75
|
+
return PROTECTED_PATH_PATTERNS.some((re) => re.test(str));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const PATH_LIKE_FIELDS = new Set(["title", "filePath", "path", "target", "ref"]);
|
|
79
|
+
|
|
80
|
+
function guardProtectedPaths(record) {
|
|
81
|
+
if (record == null) return;
|
|
82
|
+
if (typeof record === "string") {
|
|
83
|
+
if (isProtectedPath(record)) throwProtectedPathError(record);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (typeof record !== "object") return;
|
|
87
|
+
for (const [key, v] of Object.entries(record)) {
|
|
88
|
+
if (typeof v !== "string") continue;
|
|
89
|
+
// Only check path-like fields and short values that look like file references
|
|
90
|
+
if (PATH_LIKE_FIELDS.has(key) || v.length < 120) {
|
|
91
|
+
if (isProtectedPath(v)) throwProtectedPathError(v);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function throwProtectedPathError(path) {
|
|
97
|
+
const err = new RuntimeError(
|
|
98
|
+
`Rejected: memory write references protected path "${path}". Config, trust policy, and environment files are immutable from agent operations.`,
|
|
99
|
+
{ category: "security", context: { guard: "protected_path", path }, recoverable: false }
|
|
100
|
+
);
|
|
101
|
+
throw new Error(formatRuntimeError(err));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sleep(ms) {
|
|
105
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function basePaths(rootDir, agentId) {
|
|
109
|
+
const agentRoot = path.join(rootDir, agentId);
|
|
110
|
+
return {
|
|
111
|
+
agentRoot,
|
|
112
|
+
eventsFile: path.join(agentRoot, "events.jsonl"),
|
|
113
|
+
factsFile: path.join(agentRoot, "facts.jsonl"),
|
|
114
|
+
summariesFile: path.join(agentRoot, "summaries.jsonl"),
|
|
115
|
+
scratchpadsDir: path.join(agentRoot, "scratchpads"),
|
|
116
|
+
checkpointsDir: path.join(agentRoot, "checkpoints"),
|
|
117
|
+
countersFile: path.join(agentRoot, "counters.json"),
|
|
118
|
+
sqliteFile: path.join(agentRoot, "memory.sqlite"),
|
|
119
|
+
lockDir: path.join(agentRoot, ".lock")
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class MemoryStore {
|
|
124
|
+
constructor({ rootDir, agentId = null, lockConfig = {}, embeddingIndex = null, embeddingConfig = null }) {
|
|
125
|
+
this.rootDir = rootDir;
|
|
126
|
+
this.agentId = agentId;
|
|
127
|
+
this.sqliteStores = new Map();
|
|
128
|
+
this.embeddingIndex = embeddingIndex;
|
|
129
|
+
this.embeddingConfig = embeddingConfig || {};
|
|
130
|
+
this.lockConfig = {
|
|
131
|
+
ttlMs: Number(lockConfig.ttlMs ?? 15000),
|
|
132
|
+
retryDelayMs: Number(lockConfig.retryDelayMs ?? 25),
|
|
133
|
+
maxRetries: Number(lockConfig.maxRetries ?? 40)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fire-and-forget: embed newly written entries if index_on_write is enabled.
|
|
139
|
+
* Never blocks the write path. Logs warnings on failure.
|
|
140
|
+
*/
|
|
141
|
+
_maybeIndexOnWrite(agentId, records) {
|
|
142
|
+
if (!this.embeddingIndex) return;
|
|
143
|
+
if (!this.embeddingConfig.indexOnWrite) return;
|
|
144
|
+
const items = Array.isArray(records) ? records : [records];
|
|
145
|
+
const valid = items.filter((r) => r && r.type && !r.skipped);
|
|
146
|
+
if (valid.length === 0) return;
|
|
147
|
+
|
|
148
|
+
// Fire-and-forget — do not await
|
|
149
|
+
this.embeddingIndex.rebuild(agentId, valid).catch((err) => {
|
|
150
|
+
console.warn(`[nemoris] index-on-write failed for ${agentId}: ${err?.message || err}`);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async initAgent(agentId) {
|
|
155
|
+
const paths = basePaths(this.rootDir, agentId);
|
|
156
|
+
await ensureDir(paths.agentRoot);
|
|
157
|
+
await ensureDir(paths.scratchpadsDir);
|
|
158
|
+
await ensureDir(paths.checkpointsDir);
|
|
159
|
+
const counters = await readJson(paths.countersFile, null);
|
|
160
|
+
if (!counters) {
|
|
161
|
+
await writeJson(paths.countersFile, { durableWrites: 0, reads: 0 });
|
|
162
|
+
}
|
|
163
|
+
return paths;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
getSqliteStore(paths) {
|
|
167
|
+
if (!this.sqliteStores.has(paths.sqliteFile)) {
|
|
168
|
+
this.sqliteStores.set(paths.sqliteFile, new SqliteActiveStore({ dbPath: paths.sqliteFile }));
|
|
169
|
+
}
|
|
170
|
+
return this.sqliteStores.get(paths.sqliteFile);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async rebuildSqliteStore(paths) {
|
|
174
|
+
const [facts, summaries, events] = await Promise.all([
|
|
175
|
+
readJsonLines(paths.factsFile),
|
|
176
|
+
readJsonLines(paths.summariesFile),
|
|
177
|
+
readJsonLines(paths.eventsFile)
|
|
178
|
+
]);
|
|
179
|
+
const scratchpads = await this.readScratchpadsFromFiles(paths);
|
|
180
|
+
const sqlite = this.getSqliteStore(paths);
|
|
181
|
+
sqlite.clearAll();
|
|
182
|
+
const seenFactSignatures = new Set();
|
|
183
|
+
const seenSummarySignatures = new Set();
|
|
184
|
+
const seenEventDedupeKeys = new Set();
|
|
185
|
+
|
|
186
|
+
for (const record of facts) {
|
|
187
|
+
const duplicateSignature = buildDuplicateSignature(record, "fact");
|
|
188
|
+
if (duplicateSignature && seenFactSignatures.has(duplicateSignature)) continue;
|
|
189
|
+
sqlite.insert(record, {
|
|
190
|
+
duplicateSignature
|
|
191
|
+
});
|
|
192
|
+
if (duplicateSignature) seenFactSignatures.add(duplicateSignature);
|
|
193
|
+
}
|
|
194
|
+
for (const record of summaries) {
|
|
195
|
+
const duplicateSignature = buildDuplicateSignature(record, "summary");
|
|
196
|
+
if (duplicateSignature && seenSummarySignatures.has(duplicateSignature)) continue;
|
|
197
|
+
sqlite.insert(record, {
|
|
198
|
+
duplicateSignature
|
|
199
|
+
});
|
|
200
|
+
if (duplicateSignature) seenSummarySignatures.add(duplicateSignature);
|
|
201
|
+
}
|
|
202
|
+
for (const record of events) {
|
|
203
|
+
if (record.dedupeKey) {
|
|
204
|
+
if (seenEventDedupeKeys.has(record.dedupeKey)) continue;
|
|
205
|
+
seenEventDedupeKeys.add(record.dedupeKey);
|
|
206
|
+
}
|
|
207
|
+
sqlite.insert(record);
|
|
208
|
+
}
|
|
209
|
+
for (const { scratchpadId, record } of scratchpads) {
|
|
210
|
+
sqlite.upsertScratchpad(scratchpadId, record);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return sqlite;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async ensureSqliteStore(paths) {
|
|
217
|
+
const sqlite = this.getSqliteStore(paths);
|
|
218
|
+
if (sqlite.count() === 0) {
|
|
219
|
+
await this.rebuildSqliteStore(paths);
|
|
220
|
+
}
|
|
221
|
+
return sqlite;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async readScratchpadsFromFiles(paths) {
|
|
225
|
+
const files = await listFiles(paths.scratchpadsDir);
|
|
226
|
+
return Promise.all(
|
|
227
|
+
files.map(async (fileName) => ({
|
|
228
|
+
scratchpadId: path.basename(fileName, ".json"),
|
|
229
|
+
record: await readJson(path.join(paths.scratchpadsDir, fileName), null),
|
|
230
|
+
filePath: path.join(paths.scratchpadsDir, fileName)
|
|
231
|
+
}))
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async appendEvent(agentId, event, options = {}) {
|
|
236
|
+
guardAgentAccess(this.agentId, agentId, options);
|
|
237
|
+
const paths = await this.initAgent(agentId);
|
|
238
|
+
return this.withAgentLock(paths, "memory-write", async () => {
|
|
239
|
+
const sqlite = await this.ensureSqliteStore(paths);
|
|
240
|
+
const record = {
|
|
241
|
+
type: "event",
|
|
242
|
+
timestamp: nowIso(),
|
|
243
|
+
salience: 0.35,
|
|
244
|
+
...event
|
|
245
|
+
};
|
|
246
|
+
if (event.dedupeKey && sqlite.hasEventDedupe(event.dedupeKey)) {
|
|
247
|
+
return { skipped: true, record: null };
|
|
248
|
+
}
|
|
249
|
+
await appendJsonLine(paths.eventsFile, record);
|
|
250
|
+
sqlite.insert(record);
|
|
251
|
+
return record;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async writeSummary(agentId, summary, options = {}) {
|
|
256
|
+
guardAgentAccess(this.agentId, agentId, options);
|
|
257
|
+
const paths = await this.initAgent(agentId);
|
|
258
|
+
return this.withAgentLock(paths, "memory-write", async () => {
|
|
259
|
+
const sqlite = await this.ensureSqliteStore(paths);
|
|
260
|
+
guardAgainstSecrets(summary);
|
|
261
|
+
guardProtectedPaths(summary);
|
|
262
|
+
const record = {
|
|
263
|
+
type: "summary",
|
|
264
|
+
timestamp: nowIso(),
|
|
265
|
+
salience: 0.7,
|
|
266
|
+
accessCount: 0,
|
|
267
|
+
...summary
|
|
268
|
+
};
|
|
269
|
+
const duplicateSignature = buildDuplicateSignature(record, "summary");
|
|
270
|
+
if (sqlite.hasDuplicateSignature("summary", duplicateSignature)) {
|
|
271
|
+
return { skipped: true, record: null };
|
|
272
|
+
}
|
|
273
|
+
// Simhash near-duplicate check
|
|
274
|
+
const hash = computeSimhash((summary.content || "") + " " + (summary.title || ""));
|
|
275
|
+
const simhashHex = simhashToHex(hash);
|
|
276
|
+
const nearDupes = sqlite.findNearDuplicates("summary", simhashHex, 3);
|
|
277
|
+
if (nearDupes.length > 0) {
|
|
278
|
+
const best = nearDupes[0];
|
|
279
|
+
sqlite.boostSalience(best.entryId, 0.15);
|
|
280
|
+
return { accepted: true, skipped: true, nearDuplicate: true, boostedEntryId: best.entryId, record: null };
|
|
281
|
+
}
|
|
282
|
+
record.simhash = simhashHex;
|
|
283
|
+
await appendJsonLine(paths.summariesFile, record);
|
|
284
|
+
sqlite.insert(record, { duplicateSignature });
|
|
285
|
+
this._maybeIndexOnWrite(agentId, record);
|
|
286
|
+
return record;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async writeFact(agentId, fact, policy, options = {}) {
|
|
291
|
+
guardAgentAccess(this.agentId, agentId, options);
|
|
292
|
+
const paths = await this.initAgent(agentId);
|
|
293
|
+
const writesThisRun = options.writesThisRun ?? 0;
|
|
294
|
+
return this.withAgentLock(paths, "memory-write", async () => {
|
|
295
|
+
guardAgainstSecrets(fact);
|
|
296
|
+
guardProtectedPaths(fact);
|
|
297
|
+
const counters = await readJson(paths.countersFile, { durableWrites: 0, reads: 0 });
|
|
298
|
+
const sqlite = await this.ensureSqliteStore(paths);
|
|
299
|
+
const evaluation = evaluateDurableWrite(fact, policy, writesThisRun);
|
|
300
|
+
if (!evaluation.accepted) {
|
|
301
|
+
return {
|
|
302
|
+
accepted: false,
|
|
303
|
+
reasons: evaluation.reasons
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const record = {
|
|
308
|
+
type: "fact",
|
|
309
|
+
timestamp: nowIso(),
|
|
310
|
+
salience: 0.8,
|
|
311
|
+
accessCount: 0,
|
|
312
|
+
...fact
|
|
313
|
+
};
|
|
314
|
+
const duplicateSignature = buildDuplicateSignature(record, "fact");
|
|
315
|
+
if (sqlite.hasDuplicateSignature("fact", duplicateSignature)) {
|
|
316
|
+
return {
|
|
317
|
+
accepted: true,
|
|
318
|
+
skipped: true,
|
|
319
|
+
record: null
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
// Simhash near-duplicate check
|
|
323
|
+
const hash = computeSimhash((fact.content || "") + " " + (fact.title || ""));
|
|
324
|
+
const simhashHex = simhashToHex(hash);
|
|
325
|
+
const nearDupes = sqlite.findNearDuplicates("fact", simhashHex, 3);
|
|
326
|
+
if (nearDupes.length > 0) {
|
|
327
|
+
const best = nearDupes[0];
|
|
328
|
+
sqlite.boostSalience(best.entryId, 0.15);
|
|
329
|
+
return {
|
|
330
|
+
accepted: true,
|
|
331
|
+
skipped: true,
|
|
332
|
+
nearDuplicate: true,
|
|
333
|
+
boostedEntryId: best.entryId,
|
|
334
|
+
record: null
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
record.simhash = simhashHex;
|
|
338
|
+
await appendJsonLine(paths.factsFile, record);
|
|
339
|
+
sqlite.insert(record, { duplicateSignature });
|
|
340
|
+
await writeJson(paths.countersFile, {
|
|
341
|
+
...counters,
|
|
342
|
+
durableWrites: counters.durableWrites + 1
|
|
343
|
+
});
|
|
344
|
+
this._maybeIndexOnWrite(agentId, record);
|
|
345
|
+
return {
|
|
346
|
+
accepted: true,
|
|
347
|
+
record
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async putScratchpad(agentId, scratchpadId, scratchpad, options = {}) {
|
|
353
|
+
guardAgentAccess(this.agentId, agentId, options);
|
|
354
|
+
const paths = await this.initAgent(agentId);
|
|
355
|
+
return this.withAgentLock(paths, "memory-write", async () => {
|
|
356
|
+
const sqlite = await this.ensureSqliteStore(paths);
|
|
357
|
+
const record = {
|
|
358
|
+
type: "scratchpad",
|
|
359
|
+
timestamp: nowIso(),
|
|
360
|
+
expiresAt: scratchpad.expiresAt,
|
|
361
|
+
salience: scratchpad.salience ?? 0.5,
|
|
362
|
+
...scratchpad
|
|
363
|
+
};
|
|
364
|
+
const filePath = path.join(paths.scratchpadsDir, `${scratchpadId}.json`);
|
|
365
|
+
await writeJson(filePath, record);
|
|
366
|
+
sqlite.upsertScratchpad(scratchpadId, record);
|
|
367
|
+
return record;
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async getScratchpads(agentId, now = Date.now(), options = {}) {
|
|
372
|
+
guardAgentAccess(this.agentId, agentId, options);
|
|
373
|
+
const paths = await this.initAgent(agentId);
|
|
374
|
+
const sqlite = await this.ensureSqliteStore(paths);
|
|
375
|
+
const nowIsoValue = new Date(now).toISOString();
|
|
376
|
+
const expiredEntryIds = sqlite.listExpiredScratchpads(nowIsoValue);
|
|
377
|
+
|
|
378
|
+
if (expiredEntryIds.length) {
|
|
379
|
+
await this.withAgentLock(paths, "memory-write", async () => {
|
|
380
|
+
for (const entryId of expiredEntryIds) {
|
|
381
|
+
const scratchpadId = entryId.replace(/^scratchpad:/, "");
|
|
382
|
+
const filePath = path.join(paths.scratchpadsDir, `${scratchpadId}.json`);
|
|
383
|
+
sqlite.deleteByEntryId(entryId);
|
|
384
|
+
await removePath(filePath);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return sqlite
|
|
389
|
+
.listAll(nowIsoValue)
|
|
390
|
+
.filter((entry) => entry.type === "scratchpad");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async saveCheckpoint(agentId, checkpointId, checkpoint, options = {}) {
|
|
394
|
+
guardAgentAccess(this.agentId, agentId, options);
|
|
395
|
+
const paths = await this.initAgent(agentId);
|
|
396
|
+
const record = {
|
|
397
|
+
timestamp: nowIso(),
|
|
398
|
+
...checkpoint
|
|
399
|
+
};
|
|
400
|
+
await writeJson(path.join(paths.checkpointsDir, `${checkpointId}.json`), record);
|
|
401
|
+
return record;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async loadCheckpoint(agentId, checkpointId = "latest", options = {}) {
|
|
405
|
+
guardAgentAccess(this.agentId, agentId, options);
|
|
406
|
+
const paths = await this.initAgent(agentId);
|
|
407
|
+
return readJson(path.join(paths.checkpointsDir, `${checkpointId}.json`), null);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async listAll(agentId, now = Date.now(), options = {}) {
|
|
411
|
+
guardAgentAccess(this.agentId, agentId, options);
|
|
412
|
+
const paths = await this.initAgent(agentId);
|
|
413
|
+
await this.getScratchpads(agentId, now, { allowCrossAgentRead: true });
|
|
414
|
+
const sqlite = await this.ensureSqliteStore(paths);
|
|
415
|
+
return sqlite.listAll(new Date(now).toISOString());
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async sqliteStatus(agentId, options = {}) {
|
|
419
|
+
guardAgentAccess(this.agentId, agentId, options);
|
|
420
|
+
const paths = await this.initAgent(agentId);
|
|
421
|
+
const sqlite = await this.ensureSqliteStore(paths);
|
|
422
|
+
return sqlite.getWalStatus(options.mode || "passive");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async manageSqlite(agentId, options = {}) {
|
|
426
|
+
guardAgentAccess(this.agentId, agentId, options);
|
|
427
|
+
const paths = await this.initAgent(agentId);
|
|
428
|
+
const sqlite = await this.ensureSqliteStore(paths);
|
|
429
|
+
return sqlite.manageWal(options);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async getEmbeddingHealth(agentId, options = {}) {
|
|
433
|
+
guardAgentAccess(this.agentId, agentId, options);
|
|
434
|
+
const paths = await this.initAgent(agentId);
|
|
435
|
+
const sqlite = await this.ensureSqliteStore(paths);
|
|
436
|
+
let probe = null;
|
|
437
|
+
|
|
438
|
+
if (options.probe && options.embeddingIndex) {
|
|
439
|
+
try {
|
|
440
|
+
const embedded = await options.embeddingIndex.embedQuery(options.probeText || "nemoris embedding health probe");
|
|
441
|
+
if (embedded?.config?.provider) sqlite.setRuntimeMeta("embedding_last_provider", embedded.config.provider);
|
|
442
|
+
if (embedded?.config?.model) sqlite.setRuntimeMeta("embedding_last_model", embedded.config.model);
|
|
443
|
+
sqlite.setRuntimeMeta("embedding_last_success_at", nowIso());
|
|
444
|
+
sqlite.setRuntimeMeta("embedding_last_query_mode", embedded?.vector ? "embedding_probe" : "lexical_only");
|
|
445
|
+
sqlite.clearRuntimeMeta("embedding_last_error");
|
|
446
|
+
sqlite.clearRuntimeMeta("embedding_last_error_at");
|
|
447
|
+
probe = {
|
|
448
|
+
ok: true,
|
|
449
|
+
dimensions: Array.isArray(embedded?.vector) ? embedded.vector.length : 0,
|
|
450
|
+
config: embedded?.config || null
|
|
451
|
+
};
|
|
452
|
+
} catch (error) {
|
|
453
|
+
const message = error?.message || String(error);
|
|
454
|
+
sqlite.setRuntimeMeta("embedding_last_error", message);
|
|
455
|
+
sqlite.setRuntimeMeta("embedding_last_error_at", nowIso());
|
|
456
|
+
sqlite.setRuntimeMeta("embedding_last_query_mode", "embedding_probe_failed");
|
|
457
|
+
probe = {
|
|
458
|
+
ok: false,
|
|
459
|
+
error: message
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
agentId,
|
|
466
|
+
totalEntries: sqlite.count(),
|
|
467
|
+
embeddingHealth: sqlite.getEmbeddingHealth(),
|
|
468
|
+
probe
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async rebuildEmbeddings(agentId, options = {}) {
|
|
473
|
+
guardAgentAccess(this.agentId, agentId, options);
|
|
474
|
+
if (!options.embeddingIndex) {
|
|
475
|
+
throw new Error("No embedding index configured for rebuild");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const paths = await this.initAgent(agentId);
|
|
479
|
+
const sqlite = await this.ensureSqliteStore(paths);
|
|
480
|
+
const items = await this.listAll(agentId);
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
const result = await options.embeddingIndex.rebuild(agentId, items);
|
|
484
|
+
if (result?.config?.provider) sqlite.setRuntimeMeta("embedding_last_provider", result.config.provider);
|
|
485
|
+
if (result?.config?.model) sqlite.setRuntimeMeta("embedding_last_model", result.config.model);
|
|
486
|
+
sqlite.setRuntimeMeta("embedding_last_success_at", nowIso());
|
|
487
|
+
sqlite.setRuntimeMeta("embedding_last_query_mode", "embedding_rebuild");
|
|
488
|
+
sqlite.clearRuntimeMeta("embedding_last_error");
|
|
489
|
+
sqlite.clearRuntimeMeta("embedding_last_error_at");
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
agentId,
|
|
493
|
+
itemCount: items.length,
|
|
494
|
+
updatedAt: result.updatedAt,
|
|
495
|
+
config: result.config,
|
|
496
|
+
embeddingHealth: sqlite.getEmbeddingHealth()
|
|
497
|
+
};
|
|
498
|
+
} catch (error) {
|
|
499
|
+
const message = error?.message || String(error);
|
|
500
|
+
sqlite.setRuntimeMeta("embedding_last_error", message);
|
|
501
|
+
sqlite.setRuntimeMeta("embedding_last_error_at", nowIso());
|
|
502
|
+
sqlite.setRuntimeMeta("embedding_last_query_mode", "embedding_rebuild_failed");
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async query(agentId, query, options = {}) {
|
|
508
|
+
guardAgentAccess(this.agentId, agentId, options);
|
|
509
|
+
const limit = options.limit ?? 8;
|
|
510
|
+
const now = options.now ?? Date.now();
|
|
511
|
+
const retrievalBlend = {
|
|
512
|
+
...DEFAULT_RETRIEVAL_BLEND,
|
|
513
|
+
...(options.retrievalBlend || {})
|
|
514
|
+
};
|
|
515
|
+
const paths = await this.initAgent(agentId);
|
|
516
|
+
await this.getScratchpads(agentId, now, { allowCrossAgentRead: true });
|
|
517
|
+
const sqlite = await this.ensureSqliteStore(paths);
|
|
518
|
+
let queryVector = null;
|
|
519
|
+
let embeddingConfig = null;
|
|
520
|
+
let embeddingError = null;
|
|
521
|
+
let embeddingQueryMode = "lexical_only";
|
|
522
|
+
if (options.embeddingIndex) {
|
|
523
|
+
try {
|
|
524
|
+
const embedded = await options.embeddingIndex.embedQuery(query);
|
|
525
|
+
queryVector = embedded.vector;
|
|
526
|
+
embeddingConfig = embedded.config || null;
|
|
527
|
+
embeddingQueryMode = queryVector ? "embedding_query" : "lexical_only";
|
|
528
|
+
if (embeddingConfig?.provider) sqlite.setRuntimeMeta("embedding_last_provider", embeddingConfig.provider);
|
|
529
|
+
if (embeddingConfig?.model) sqlite.setRuntimeMeta("embedding_last_model", embeddingConfig.model);
|
|
530
|
+
sqlite.setRuntimeMeta("embedding_last_success_at", nowIso());
|
|
531
|
+
sqlite.setRuntimeMeta("embedding_last_query_mode", embeddingQueryMode);
|
|
532
|
+
sqlite.clearRuntimeMeta("embedding_last_error");
|
|
533
|
+
sqlite.clearRuntimeMeta("embedding_last_error_at");
|
|
534
|
+
} catch (error) {
|
|
535
|
+
queryVector = null;
|
|
536
|
+
embeddingConfig = null;
|
|
537
|
+
embeddingError = error?.message || String(error);
|
|
538
|
+
embeddingQueryMode = "lexical_fallback";
|
|
539
|
+
sqlite.setRuntimeMeta("embedding_last_error", embeddingError);
|
|
540
|
+
sqlite.setRuntimeMeta("embedding_last_error_at", nowIso());
|
|
541
|
+
sqlite.setRuntimeMeta("embedding_last_query_mode", embeddingQueryMode);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const retrievalCandidates = sqlite.queryRetrievalCandidates(query, {
|
|
545
|
+
nowIso: new Date(now).toISOString(),
|
|
546
|
+
limit,
|
|
547
|
+
queryVector
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
const ranked = retrievalCandidates
|
|
551
|
+
.map((candidate) => ({
|
|
552
|
+
...candidate.item,
|
|
553
|
+
...scoreMemory(
|
|
554
|
+
{
|
|
555
|
+
...candidate.item,
|
|
556
|
+
lexicalScore: computeLexicalScore(
|
|
557
|
+
query,
|
|
558
|
+
[candidate.item.title, candidate.item.content, candidate.item.summary, candidate.item.reason, candidate.item.category]
|
|
559
|
+
.filter(Boolean)
|
|
560
|
+
.join(" ")
|
|
561
|
+
),
|
|
562
|
+
embeddingSimilarity: candidate.embeddingSimilarity,
|
|
563
|
+
embeddingFreshness: candidate.embeddingFreshness
|
|
564
|
+
},
|
|
565
|
+
query,
|
|
566
|
+
now,
|
|
567
|
+
retrievalBlend
|
|
568
|
+
),
|
|
569
|
+
retrievalSources: candidate.retrievalSources,
|
|
570
|
+
embeddingFreshness: candidate.embeddingFreshness,
|
|
571
|
+
embeddingProvider: candidate.embeddingProvider,
|
|
572
|
+
embeddingModel: candidate.embeddingModel,
|
|
573
|
+
candidateSource: candidate.retrievalSources.includes("semantic") && candidate.retrievalSources.includes("lexical")
|
|
574
|
+
? "indexed+semantic"
|
|
575
|
+
: candidate.retrievalSources.includes("semantic")
|
|
576
|
+
? "semantic"
|
|
577
|
+
: "indexed"
|
|
578
|
+
}))
|
|
579
|
+
.sort((a, b) => b.score - a.score);
|
|
580
|
+
|
|
581
|
+
const deduped = [];
|
|
582
|
+
const seen = new Set();
|
|
583
|
+
for (const item of ranked) {
|
|
584
|
+
const key = buildResultSignature(item);
|
|
585
|
+
if (seen.has(key)) continue;
|
|
586
|
+
seen.add(key);
|
|
587
|
+
deduped.push(item);
|
|
588
|
+
if (deduped.length >= limit) break;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
query,
|
|
593
|
+
totalCandidates: sqlite.count(),
|
|
594
|
+
indexedCandidates: retrievalCandidates.filter((item) => item.lexicalMatch).length,
|
|
595
|
+
semanticCandidates: retrievalCandidates.filter((item) => item.embeddingSimilarity > 0).length,
|
|
596
|
+
retrieval: {
|
|
597
|
+
blend: retrievalBlend,
|
|
598
|
+
embeddingQueryAvailable: Boolean(queryVector),
|
|
599
|
+
embeddingConfig,
|
|
600
|
+
embeddingQueryMode,
|
|
601
|
+
embeddingError,
|
|
602
|
+
embeddingHealth: sqlite.getEmbeddingHealth(),
|
|
603
|
+
candidates: retrievalCandidates.map((candidate) => ({
|
|
604
|
+
entryId: candidate.item.entryId,
|
|
605
|
+
title: candidate.item.title,
|
|
606
|
+
retrievalSources: candidate.retrievalSources,
|
|
607
|
+
lexicalMatch: candidate.lexicalMatch,
|
|
608
|
+
lexicalRank: candidate.lexicalRank,
|
|
609
|
+
embeddingSimilarity: Number(candidate.embeddingSimilarity.toFixed(4)),
|
|
610
|
+
embeddingFreshness: candidate.embeddingFreshness
|
|
611
|
+
}))
|
|
612
|
+
},
|
|
613
|
+
items: deduped
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async withAgentLock(paths, lockKey, fn) {
|
|
618
|
+
const sqlite = this.getSqliteStore(paths);
|
|
619
|
+
const ownerId = createRuntimeId("memory-lock");
|
|
620
|
+
const ttlMs = this.lockConfig.ttlMs;
|
|
621
|
+
const retryDelayMs = this.lockConfig.retryDelayMs;
|
|
622
|
+
const maxRetries = this.lockConfig.maxRetries;
|
|
623
|
+
|
|
624
|
+
let acquired = false;
|
|
625
|
+
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
626
|
+
const acquiredAt = new Date();
|
|
627
|
+
const expiresAt = new Date(acquiredAt.getTime() + ttlMs);
|
|
628
|
+
acquired = sqlite.tryAcquireLock(lockKey, {
|
|
629
|
+
ownerId,
|
|
630
|
+
acquiredAt: acquiredAt.toISOString(),
|
|
631
|
+
expiresAt: expiresAt.toISOString()
|
|
632
|
+
});
|
|
633
|
+
if (acquired) break;
|
|
634
|
+
if (attempt === maxRetries) {
|
|
635
|
+
throw new Error(`Timed out waiting for SQLite memory lock ${lockKey}`);
|
|
636
|
+
}
|
|
637
|
+
await sleep(retryDelayMs);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
return await fn();
|
|
642
|
+
} finally {
|
|
643
|
+
if (acquired) {
|
|
644
|
+
sqlite.releaseLock(lockKey, ownerId);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|