atlas-mcp 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 +32 -0
- package/README.md +282 -0
- package/package.json +72 -0
- package/public/app/assets/app-CxbS1w9p.js +3981 -0
- package/public/app/assets/index-BA6nxCuI.css +1 -0
- package/public/app/assets/index-BXmIRrQH.js +177 -0
- package/public/app/index.html +27 -0
- package/public/assets/brain-atlas.LICENSE.txt +16 -0
- package/public/assets/brain-atlas.glb +0 -0
- package/public/assets/brain.obj +27282 -0
- package/public/fonts/DepartureMono-Regular.woff +0 -0
- package/public/fonts/DepartureMono-Regular.woff2 +0 -0
- package/scripts/sync-memory-vectors.js +46 -0
- package/src/audit.js +9 -0
- package/src/cli/args.js +87 -0
- package/src/cli/commands/add.js +103 -0
- package/src/cli/commands/config.js +228 -0
- package/src/cli/commands/delete.js +75 -0
- package/src/cli/commands/entities.js +39 -0
- package/src/cli/commands/entity.js +47 -0
- package/src/cli/commands/get.js +46 -0
- package/src/cli/commands/list.js +53 -0
- package/src/cli/commands/related.js +56 -0
- package/src/cli/commands/search.js +68 -0
- package/src/cli/commands/update.js +58 -0
- package/src/cli/deps.js +114 -0
- package/src/cli/env-file.js +44 -0
- package/src/cli/format.js +246 -0
- package/src/cli.js +187 -0
- package/src/cognitive-worker.js +381 -0
- package/src/db.js +2674 -0
- package/src/extraction-context.js +31 -0
- package/src/ingestion-service.js +387 -0
- package/src/ingestion-worker.js +225 -0
- package/src/llm-config.js +31 -0
- package/src/llm.js +789 -0
- package/src/logger.js +51 -0
- package/src/mcp-server.js +577 -0
- package/src/memory-comparison.js +421 -0
- package/src/related-memories.js +232 -0
- package/src/run-cognitive-worker.js +12 -0
- package/src/run-ingestion-worker.js +13 -0
- package/src/run-vector-worker.js +12 -0
- package/src/schemas.js +413 -0
- package/src/semantic-validation.js +430 -0
- package/src/server.js +827 -0
- package/src/shared/brain-regions.js +61 -0
- package/src/shared/entity-lens.js +249 -0
- package/src/shared/memory-placement.js +171 -0
- package/src/shared/memory-search.js +55 -0
- package/src/shared/region-anchors.js +112 -0
- package/src/shared/region-mapper.js +247 -0
- package/src/vector-store.js +546 -0
- package/src/vector-worker.js +71 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import "dotenv/config";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { dirname, resolve } from "node:path";
|
|
8
|
+
import { parseArgs } from "./cli/args.js";
|
|
9
|
+
import { defaultDependencies } from "./cli/deps.js";
|
|
10
|
+
import { assertAtlasModeSupported } from "./vector-store.js";
|
|
11
|
+
|
|
12
|
+
process.env.LOG_STREAM = "stderr";
|
|
13
|
+
|
|
14
|
+
import * as add from "./cli/commands/add.js";
|
|
15
|
+
import * as list from "./cli/commands/list.js";
|
|
16
|
+
import * as getCmd from "./cli/commands/get.js";
|
|
17
|
+
import * as search from "./cli/commands/search.js";
|
|
18
|
+
import * as related from "./cli/commands/related.js";
|
|
19
|
+
import * as entities from "./cli/commands/entities.js";
|
|
20
|
+
import * as entityCmd from "./cli/commands/entity.js";
|
|
21
|
+
import * as update from "./cli/commands/update.js";
|
|
22
|
+
import * as del from "./cli/commands/delete.js";
|
|
23
|
+
import * as config from "./cli/commands/config.js";
|
|
24
|
+
|
|
25
|
+
const COMMANDS = {
|
|
26
|
+
add,
|
|
27
|
+
list,
|
|
28
|
+
get: getCmd,
|
|
29
|
+
search,
|
|
30
|
+
related,
|
|
31
|
+
entities,
|
|
32
|
+
entity: entityCmd,
|
|
33
|
+
update,
|
|
34
|
+
delete: del,
|
|
35
|
+
config,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function loadVersion() {
|
|
39
|
+
try {
|
|
40
|
+
const here = dirname(realpathSync(fileURLToPath(import.meta.url)));
|
|
41
|
+
let dir = here;
|
|
42
|
+
for (let i = 0; i < 5; i += 1) {
|
|
43
|
+
try {
|
|
44
|
+
const pkg = JSON.parse(
|
|
45
|
+
readFileSync(resolve(dir, "package.json"), "utf8"),
|
|
46
|
+
);
|
|
47
|
+
if (pkg.name === "atlas-mcp") return pkg.version;
|
|
48
|
+
} catch {
|
|
49
|
+
// keep climbing
|
|
50
|
+
}
|
|
51
|
+
const parent = dirname(dir);
|
|
52
|
+
if (parent === dir) break;
|
|
53
|
+
dir = parent;
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// fall through
|
|
57
|
+
}
|
|
58
|
+
return "0.0.0";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const VERSION = loadVersion();
|
|
62
|
+
|
|
63
|
+
const TOP_HELP = `atlas ${VERSION}
|
|
64
|
+
|
|
65
|
+
Store, recall, and inspect memories from the terminal. Mirrors the Atlas
|
|
66
|
+
MCP tools; uses the same data store and LLM pipeline as the web app.
|
|
67
|
+
|
|
68
|
+
Usage: atlas <command> [args] [flags]
|
|
69
|
+
|
|
70
|
+
Commands:
|
|
71
|
+
add <text> Save a new memory (runs the LLM extraction pipeline)
|
|
72
|
+
list Browse recently stored memories
|
|
73
|
+
get <id> Fetch one memory with its full extraction
|
|
74
|
+
search <query> Find memories via hybrid search
|
|
75
|
+
related <id> Find memories connected to <id>
|
|
76
|
+
entities <query> Look up canonical entities by name
|
|
77
|
+
entity <id> List memories for one entity
|
|
78
|
+
update <id> Replace a memory's summary (reindexes the vector)
|
|
79
|
+
delete <id> Permanently delete a memory
|
|
80
|
+
config View and edit configuration
|
|
81
|
+
|
|
82
|
+
Global flags:
|
|
83
|
+
--help, -h Show this help (or per-command help: atlas <cmd> --help)
|
|
84
|
+
--version, -v Print the version
|
|
85
|
+
|
|
86
|
+
Examples:
|
|
87
|
+
atlas add "I prefer dark roast coffee" --type preference --title "Coffee" --json
|
|
88
|
+
atlas search "coffee" --strategy hybrid --json
|
|
89
|
+
atlas get mem_12ab34cd --json | jq .extraction
|
|
90
|
+
atlas delete mem_12ab34cd --yes
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
function printUsageError(message) {
|
|
94
|
+
console.error(`Error: ${message}\n`);
|
|
95
|
+
console.error(`Run 'atlas --help' for a list of commands.`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function runCli(argv = process.argv.slice(2), deps = defaultDependencies()) {
|
|
99
|
+
const { positional, flags } = parseArgs(argv);
|
|
100
|
+
const version = flags.version === true || flags.v === true;
|
|
101
|
+
|
|
102
|
+
if (version) {
|
|
103
|
+
console.log(VERSION);
|
|
104
|
+
return { exitCode: 0 };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const [commandName, ...rest] = positional;
|
|
108
|
+
|
|
109
|
+
if (!commandName) {
|
|
110
|
+
console.log(TOP_HELP);
|
|
111
|
+
return { exitCode: 0 };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const command = COMMANDS[commandName];
|
|
115
|
+
if (!command) {
|
|
116
|
+
printUsageError(`unknown command: ${commandName}`);
|
|
117
|
+
return { exitCode: 2 };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// The top-level parse already separated positional from flags, so the
|
|
121
|
+
// command receives the same flag set. We only re-parse `rest` (positional
|
|
122
|
+
// args after the command name) to keep `sub.positional` available and to
|
|
123
|
+
// detect per-command `--help` placed after the command name.
|
|
124
|
+
const sub = parseArgs(rest);
|
|
125
|
+
const helpFlag = sub.flags.help === true
|
|
126
|
+
|| sub.flags.h === true
|
|
127
|
+
|| flags.help === true
|
|
128
|
+
|| flags.h === true;
|
|
129
|
+
if (helpFlag) {
|
|
130
|
+
console.log(command.meta.help);
|
|
131
|
+
return { exitCode: 0 };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const mergedFlags = { ...flags, ...sub.flags };
|
|
135
|
+
const json = mergedFlags.json === true || mergedFlags.json === "true";
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const result = await command.run({
|
|
139
|
+
positional: sub.positional,
|
|
140
|
+
flags: mergedFlags,
|
|
141
|
+
deps,
|
|
142
|
+
json,
|
|
143
|
+
});
|
|
144
|
+
return result || { exitCode: 0 };
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(`Error: ${error.message}`);
|
|
147
|
+
return { exitCode: 1 };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isInvokedAsMain() {
|
|
152
|
+
if (!process.argv[1]) return false;
|
|
153
|
+
try {
|
|
154
|
+
const real = realpathSync(process.argv[1]);
|
|
155
|
+
return import.meta.url === pathToFileURL(real).href;
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const isMain = isInvokedAsMain();
|
|
162
|
+
|
|
163
|
+
if (isMain) {
|
|
164
|
+
let deps;
|
|
165
|
+
Promise.resolve()
|
|
166
|
+
.then(() => {
|
|
167
|
+
assertAtlasModeSupported();
|
|
168
|
+
deps = defaultDependencies();
|
|
169
|
+
return runCli(process.argv.slice(2), deps);
|
|
170
|
+
})
|
|
171
|
+
.then((result) => {
|
|
172
|
+
if (result && Number.isInteger(result.exitCode)) {
|
|
173
|
+
process.exitCode = result.exitCode;
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
.catch((error) => {
|
|
177
|
+
console.error(`Error: ${error.message}`);
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
})
|
|
180
|
+
.finally(() => {
|
|
181
|
+
try {
|
|
182
|
+
deps?.closeDb();
|
|
183
|
+
} catch {
|
|
184
|
+
// Ignore: closeDb is idempotent and may not exist in tests.
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import {
|
|
2
|
+
REGION_MAPPING_VERSION,
|
|
3
|
+
mapExtractionToRegions,
|
|
4
|
+
} from "./shared/region-mapper.js";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_POLL_INTERVAL_MS = 1_000;
|
|
7
|
+
const DEFAULT_STALE_AFTER_MS = 5 * 60_000;
|
|
8
|
+
const DEFAULT_BASE_RETRY_MS = 1_000;
|
|
9
|
+
const DEFAULT_MAX_RETRY_MS = 60_000;
|
|
10
|
+
const DEFAULT_MAX_ATTEMPTS = 5;
|
|
11
|
+
|
|
12
|
+
function requiredFunction(value, name) {
|
|
13
|
+
if (typeof value !== "function") {
|
|
14
|
+
throw new TypeError(`cognitive worker requires ${name}()`);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function optionalFunction(value) {
|
|
20
|
+
return typeof value === "function" ? value : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function errorMessage(error) {
|
|
24
|
+
if (error instanceof Error) return error.message;
|
|
25
|
+
return String(error);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function jobId(job) {
|
|
29
|
+
return job.id ?? job.jobId ?? job.job_id;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function memoryId(job) {
|
|
33
|
+
return job.memoryId ?? job.memory_id ?? job.memory?.id;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function attemptCount(job) {
|
|
37
|
+
const value = Number(job.attempts ?? job.attemptCount ?? job.attempt_count);
|
|
38
|
+
return Number.isInteger(value) && value > 0 ? value : 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function maximumAttempts(job, fallback) {
|
|
42
|
+
const value = Number(
|
|
43
|
+
job.maxAttempts ?? job.max_attempts ?? job.maximumAttempts,
|
|
44
|
+
);
|
|
45
|
+
return Number.isInteger(value) && value > 0
|
|
46
|
+
? Math.min(value, fallback)
|
|
47
|
+
: fallback;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function semanticFrom(value) {
|
|
51
|
+
if (!value) return null;
|
|
52
|
+
return (
|
|
53
|
+
value.semanticExtraction ??
|
|
54
|
+
value.semantic_extraction ??
|
|
55
|
+
value.extractionJson ??
|
|
56
|
+
value.extraction_json ??
|
|
57
|
+
value.extraction ??
|
|
58
|
+
null
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseObject(value, label) {
|
|
63
|
+
if (typeof value !== "string") return value;
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(value);
|
|
66
|
+
} catch {
|
|
67
|
+
throw new TypeError(`${label} must contain valid JSON`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function validateOptions(options) {
|
|
72
|
+
for (const [name, value] of [
|
|
73
|
+
["pollIntervalMs", options.pollIntervalMs],
|
|
74
|
+
["staleAfterMs", options.staleAfterMs],
|
|
75
|
+
["baseRetryMs", options.baseRetryMs],
|
|
76
|
+
["maxRetryMs", options.maxRetryMs],
|
|
77
|
+
]) {
|
|
78
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
79
|
+
throw new RangeError(`${name} must be a non-negative finite number`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!Number.isInteger(options.maxAttempts) || options.maxAttempts < 1) {
|
|
83
|
+
throw new RangeError("maxAttempts must be a positive integer");
|
|
84
|
+
}
|
|
85
|
+
if (options.maxRetryMs < options.baseRetryMs) {
|
|
86
|
+
throw new RangeError("maxRetryMs must be at least baseRetryMs");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function calculateRetryDelay(
|
|
91
|
+
attempt,
|
|
92
|
+
{
|
|
93
|
+
baseRetryMs = DEFAULT_BASE_RETRY_MS,
|
|
94
|
+
maxRetryMs = DEFAULT_MAX_RETRY_MS,
|
|
95
|
+
} = {},
|
|
96
|
+
) {
|
|
97
|
+
const exponent = Math.max(0, Number(attempt) - 1);
|
|
98
|
+
return Math.min(maxRetryMs, baseRetryMs * 2 ** exponent);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function createCognitiveWorker({
|
|
102
|
+
db,
|
|
103
|
+
annotateMemory,
|
|
104
|
+
mapRegions = mapExtractionToRegions,
|
|
105
|
+
now = () => new Date(),
|
|
106
|
+
setTimer = setTimeout,
|
|
107
|
+
clearTimer = clearTimeout,
|
|
108
|
+
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
|
|
109
|
+
staleAfterMs = DEFAULT_STALE_AFTER_MS,
|
|
110
|
+
baseRetryMs = DEFAULT_BASE_RETRY_MS,
|
|
111
|
+
maxRetryMs = DEFAULT_MAX_RETRY_MS,
|
|
112
|
+
maxAttempts = DEFAULT_MAX_ATTEMPTS,
|
|
113
|
+
mappingVersion = REGION_MAPPING_VERSION,
|
|
114
|
+
onError = () => {},
|
|
115
|
+
} = {}) {
|
|
116
|
+
if (!db || typeof db !== "object") {
|
|
117
|
+
throw new TypeError("cognitive worker requires a db adapter");
|
|
118
|
+
}
|
|
119
|
+
requiredFunction(annotateMemory, "annotateMemory");
|
|
120
|
+
requiredFunction(mapRegions, "mapRegions");
|
|
121
|
+
|
|
122
|
+
const options = {
|
|
123
|
+
pollIntervalMs,
|
|
124
|
+
staleAfterMs,
|
|
125
|
+
baseRetryMs,
|
|
126
|
+
maxRetryMs,
|
|
127
|
+
maxAttempts,
|
|
128
|
+
};
|
|
129
|
+
validateOptions(options);
|
|
130
|
+
|
|
131
|
+
const claimJob = requiredFunction(
|
|
132
|
+
db.claimAnnotationJob ?? db.claimPendingAnnotationJob,
|
|
133
|
+
"db.claimAnnotationJob",
|
|
134
|
+
);
|
|
135
|
+
const recoverJobs = requiredFunction(
|
|
136
|
+
db.recoverAnnotationJobs ?? db.recoverStaleAnnotationJobs ??
|
|
137
|
+
db.recoverStaleProcessingJobs,
|
|
138
|
+
"db.recoverAnnotationJobs",
|
|
139
|
+
);
|
|
140
|
+
const getMemory = optionalFunction(
|
|
141
|
+
db.getMemory ?? db.getMemoryForAnnotation,
|
|
142
|
+
);
|
|
143
|
+
const getSemantic = optionalFunction(
|
|
144
|
+
db.getLatestExtraction ?? db.getSemanticExtraction ??
|
|
145
|
+
db.getLatestSemanticExtraction,
|
|
146
|
+
);
|
|
147
|
+
const saveAnnotation = requiredFunction(
|
|
148
|
+
db.saveCognitiveAnnotation ?? db.persistCognitiveAnnotation,
|
|
149
|
+
"db.saveCognitiveAnnotation",
|
|
150
|
+
);
|
|
151
|
+
const saveActivations = requiredFunction(
|
|
152
|
+
db.saveRegionActivations ?? db.replaceRegionActivations,
|
|
153
|
+
"db.saveRegionActivations",
|
|
154
|
+
);
|
|
155
|
+
const completeJob = requiredFunction(
|
|
156
|
+
db.completeAnnotationJob ?? db.markAnnotationJobCompleted,
|
|
157
|
+
"db.completeAnnotationJob",
|
|
158
|
+
);
|
|
159
|
+
const retryJob = requiredFunction(
|
|
160
|
+
db.retryAnnotationJob ?? db.rescheduleAnnotationJob,
|
|
161
|
+
"db.retryAnnotationJob",
|
|
162
|
+
);
|
|
163
|
+
const failJob = optionalFunction(
|
|
164
|
+
db.failAnnotationJob ?? db.markAnnotationJobFailed,
|
|
165
|
+
);
|
|
166
|
+
const completeAnnotation = optionalFunction(db.completeCognitiveAnnotation);
|
|
167
|
+
|
|
168
|
+
let timer = null;
|
|
169
|
+
let resolvePoll = null;
|
|
170
|
+
let loopPromise = null;
|
|
171
|
+
let stopped = true;
|
|
172
|
+
|
|
173
|
+
const currentDate = () => {
|
|
174
|
+
const value = now();
|
|
175
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
176
|
+
if (Number.isNaN(date.getTime())) {
|
|
177
|
+
throw new TypeError("now() must return a valid Date or date value");
|
|
178
|
+
}
|
|
179
|
+
return date;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const loadMemory = async (job) => {
|
|
183
|
+
const id = memoryId(job);
|
|
184
|
+
if (job.memory) return job.memory;
|
|
185
|
+
if (!id) throw new Error(`annotation job ${jobId(job)} has no memory id`);
|
|
186
|
+
if (!getMemory) {
|
|
187
|
+
throw new Error("annotation job has no memory and db.getMemory is absent");
|
|
188
|
+
}
|
|
189
|
+
const memory = await getMemory.call(db, id);
|
|
190
|
+
if (!memory) throw new Error(`memory not found: ${id}`);
|
|
191
|
+
return memory;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const loadSemantic = async (job, memory) => {
|
|
195
|
+
const embedded = semanticFrom(job) ?? semanticFrom(memory);
|
|
196
|
+
if (embedded) return parseObject(embedded, "semantic extraction");
|
|
197
|
+
if (!getSemantic) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
"annotation job has no semantic extraction and db.getSemanticExtraction is absent",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
const row = await getSemantic.call(db, memoryId(job) ?? memory.id);
|
|
203
|
+
const semantic = semanticFrom(row) ?? row;
|
|
204
|
+
if (!semantic) {
|
|
205
|
+
throw new Error(`semantic extraction not found: ${memoryId(job) ?? memory.id}`);
|
|
206
|
+
}
|
|
207
|
+
return parseObject(semantic, "semantic extraction");
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const reschedule = async (job, error) => {
|
|
211
|
+
const attempts = attemptCount(job);
|
|
212
|
+
const attemptLimit = maximumAttempts(job, maxAttempts);
|
|
213
|
+
const id = jobId(job);
|
|
214
|
+
const message = errorMessage(error);
|
|
215
|
+
const failedAt = currentDate();
|
|
216
|
+
|
|
217
|
+
if (attempts >= attemptLimit && failJob) {
|
|
218
|
+
await failJob.call(db, id, {
|
|
219
|
+
error: message,
|
|
220
|
+
attempts,
|
|
221
|
+
failedAt: failedAt.toISOString(),
|
|
222
|
+
});
|
|
223
|
+
return { status: "failed", job, error };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const delayMs = calculateRetryDelay(attempts, {
|
|
227
|
+
baseRetryMs,
|
|
228
|
+
maxRetryMs,
|
|
229
|
+
});
|
|
230
|
+
const retryAt = new Date(failedAt.getTime() + delayMs);
|
|
231
|
+
await retryJob.call(db, {
|
|
232
|
+
jobId: id,
|
|
233
|
+
error: message,
|
|
234
|
+
attempts,
|
|
235
|
+
retryAt: retryAt.toISOString(),
|
|
236
|
+
terminal: attempts >= attemptLimit,
|
|
237
|
+
updatedAt: failedAt.toISOString(),
|
|
238
|
+
});
|
|
239
|
+
return {
|
|
240
|
+
status: attempts >= attemptLimit ? "failed" : "retrying",
|
|
241
|
+
job,
|
|
242
|
+
error,
|
|
243
|
+
retryAt,
|
|
244
|
+
delayMs,
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const runOnce = async () => {
|
|
249
|
+
const claimedAt = currentDate();
|
|
250
|
+
const job = await claimJob.call(db, { now: claimedAt.toISOString() });
|
|
251
|
+
if (!job) return { status: "idle" };
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const memory = await loadMemory(job);
|
|
255
|
+
const queuedVersion = job.memoryVersion ?? job.memory_version;
|
|
256
|
+
if (queuedVersion !== undefined && memory.version !== undefined
|
|
257
|
+
&& Number(queuedVersion) !== Number(memory.version)) {
|
|
258
|
+
await completeJob.call(db, {
|
|
259
|
+
jobId: jobId(job),
|
|
260
|
+
completedAt: currentDate().toISOString(),
|
|
261
|
+
});
|
|
262
|
+
return { status: "superseded", job, memory };
|
|
263
|
+
}
|
|
264
|
+
const semantic = await loadSemantic(job, memory);
|
|
265
|
+
const semanticInput = {
|
|
266
|
+
...semantic,
|
|
267
|
+
text: semantic.text ?? memory.raw_text ?? memory.text,
|
|
268
|
+
};
|
|
269
|
+
const annotation = await annotateMemory(semanticInput, {
|
|
270
|
+
job,
|
|
271
|
+
memory,
|
|
272
|
+
});
|
|
273
|
+
if (!annotation || typeof annotation !== "object") {
|
|
274
|
+
throw new TypeError("annotateMemory() must return an annotation object");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const id = memoryId(job) ?? memory.id;
|
|
278
|
+
const activations = mapRegions({
|
|
279
|
+
...semantic,
|
|
280
|
+
...annotation,
|
|
281
|
+
});
|
|
282
|
+
const completedAt = currentDate().toISOString();
|
|
283
|
+
if (completeAnnotation) {
|
|
284
|
+
await completeAnnotation.call(db, {
|
|
285
|
+
memoryId: id,
|
|
286
|
+
annotation,
|
|
287
|
+
activations,
|
|
288
|
+
mappingVersion,
|
|
289
|
+
model: job.model,
|
|
290
|
+
schemaVersion: job.schemaVersion ?? job.schema_version,
|
|
291
|
+
jobId: jobId(job),
|
|
292
|
+
completedAt,
|
|
293
|
+
});
|
|
294
|
+
} else {
|
|
295
|
+
await saveAnnotation.call(db, {
|
|
296
|
+
memoryId: id,
|
|
297
|
+
annotation,
|
|
298
|
+
model: job.model,
|
|
299
|
+
schemaVersion: job.schemaVersion ?? job.schema_version,
|
|
300
|
+
jobId: jobId(job),
|
|
301
|
+
attempts: attemptCount(job),
|
|
302
|
+
});
|
|
303
|
+
await saveActivations.call(db, id, activations, mappingVersion);
|
|
304
|
+
await completeJob.call(db, { jobId: jobId(job), completedAt });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return { status: "completed", job, memory, annotation, activations };
|
|
308
|
+
} catch (error) {
|
|
309
|
+
return reschedule(job, error);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const waitForPoll = () =>
|
|
314
|
+
new Promise((resolve) => {
|
|
315
|
+
resolvePoll = resolve;
|
|
316
|
+
timer = setTimer(() => {
|
|
317
|
+
timer = null;
|
|
318
|
+
resolvePoll = null;
|
|
319
|
+
resolve();
|
|
320
|
+
}, pollIntervalMs);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const run = async ({ signal, recover = true } = {}) => {
|
|
324
|
+
stopped = false;
|
|
325
|
+
if (recover) {
|
|
326
|
+
const recoveredAt = currentDate();
|
|
327
|
+
await recoverJobs.call(db, {
|
|
328
|
+
now: recoveredAt.toISOString(),
|
|
329
|
+
retryAt: recoveredAt.toISOString(),
|
|
330
|
+
staleBefore: new Date(
|
|
331
|
+
recoveredAt.getTime() - staleAfterMs,
|
|
332
|
+
).toISOString(),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
while (!stopped && !signal?.aborted) {
|
|
337
|
+
const result = await runOnce();
|
|
338
|
+
if (result.status === "idle") await waitForPoll();
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const start = () => {
|
|
343
|
+
if (loopPromise) return loopPromise;
|
|
344
|
+
stopped = false;
|
|
345
|
+
loopPromise = run()
|
|
346
|
+
.catch((error) => {
|
|
347
|
+
onError(error);
|
|
348
|
+
throw error;
|
|
349
|
+
})
|
|
350
|
+
.finally(() => {
|
|
351
|
+
loopPromise = null;
|
|
352
|
+
stopped = true;
|
|
353
|
+
});
|
|
354
|
+
return loopPromise;
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const stop = async () => {
|
|
358
|
+
stopped = true;
|
|
359
|
+
if (timer !== null) {
|
|
360
|
+
clearTimer(timer);
|
|
361
|
+
timer = null;
|
|
362
|
+
resolvePoll?.();
|
|
363
|
+
resolvePoll = null;
|
|
364
|
+
}
|
|
365
|
+
await loopPromise;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
start,
|
|
370
|
+
run,
|
|
371
|
+
runOnce,
|
|
372
|
+
stop,
|
|
373
|
+
get running() {
|
|
374
|
+
return loopPromise !== null;
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export async function runCognitiveWorkerCycle(dependencies) {
|
|
380
|
+
return createCognitiveWorker(dependencies).runOnce();
|
|
381
|
+
}
|