@askexenow/exe-os 0.9.66 → 0.9.68
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/deploy/stack-manifests/v0.9.json +54 -5
- package/dist/bin/age-ontology-load.js +324 -0
- package/dist/bin/agentic-ontology-backfill.js +4869 -0
- package/dist/bin/agentic-reflection-backfill.js +4299 -0
- package/dist/bin/{exe-link.js → agentic-semantic-label.js} +1672 -2263
- package/dist/bin/backfill-conversations.js +506 -20
- package/dist/bin/backfill-responses.js +506 -20
- package/dist/bin/backfill-vectors.js +409 -33
- package/dist/bin/bulk-sync-postgres.js +4876 -0
- package/dist/bin/cleanup-stale-review-tasks.js +507 -21
- package/dist/bin/cli.js +2819 -1579
- package/dist/bin/exe-assign.js +506 -20
- package/dist/bin/exe-boot.js +410 -60
- package/dist/bin/exe-cloud.js +795 -105
- package/dist/bin/exe-dispatch.js +516 -22
- package/dist/bin/exe-doctor.js +587 -30
- package/dist/bin/exe-export-behaviors.js +518 -24
- package/dist/bin/exe-forget.js +507 -21
- package/dist/bin/exe-gateway.js +571 -25
- package/dist/bin/exe-heartbeat.js +518 -24
- package/dist/bin/exe-kill.js +507 -21
- package/dist/bin/exe-launch-agent.js +2312 -1069
- package/dist/bin/exe-new-employee.js +197 -165
- package/dist/bin/exe-pending-messages.js +507 -21
- package/dist/bin/exe-pending-notifications.js +507 -21
- package/dist/bin/exe-pending-reviews.js +507 -21
- package/dist/bin/exe-rename.js +507 -21
- package/dist/bin/exe-review.js +507 -21
- package/dist/bin/exe-search.js +518 -24
- package/dist/bin/exe-session-cleanup.js +516 -22
- package/dist/bin/exe-settings.js +4 -0
- package/dist/bin/exe-start-codex.js +682 -143
- package/dist/bin/exe-start-opencode.js +627 -79
- package/dist/bin/exe-status.js +507 -21
- package/dist/bin/exe-team.js +507 -21
- package/dist/bin/git-sweep.js +516 -22
- package/dist/bin/graph-backfill.js +727 -31
- package/dist/bin/graph-export.js +507 -21
- package/dist/bin/graph-layer-benchmark.js +109 -0
- package/dist/bin/install.js +305 -288
- package/dist/bin/intercom-check.js +516 -22
- package/dist/bin/postgres-agentic-reflection-backfill.js +457 -0
- package/dist/bin/postgres-agentic-semantic-backfill.js +507 -0
- package/dist/bin/scan-tasks.js +516 -22
- package/dist/bin/setup.js +412 -62
- package/dist/bin/shard-migrate.js +506 -20
- package/dist/gateway/index.js +569 -23
- package/dist/hooks/bug-report-worker.js +519 -25
- package/dist/hooks/codex-stop-task-finalizer.js +516 -22
- package/dist/hooks/commit-complete.js +516 -22
- package/dist/hooks/error-recall.js +518 -24
- package/dist/hooks/ingest.js +516 -22
- package/dist/hooks/instructions-loaded.js +507 -21
- package/dist/hooks/notification.js +507 -21
- package/dist/hooks/post-compact.js +507 -21
- package/dist/hooks/post-tool-combined.js +519 -25
- package/dist/hooks/pre-compact.js +516 -22
- package/dist/hooks/pre-tool-use.js +507 -21
- package/dist/hooks/prompt-submit.js +519 -25
- package/dist/hooks/session-end.js +516 -22
- package/dist/hooks/session-start.js +520 -26
- package/dist/hooks/stop.js +517 -23
- package/dist/hooks/subagent-stop.js +507 -21
- package/dist/hooks/summary-worker.js +411 -61
- package/dist/index.js +569 -23
- package/dist/lib/cloud-sync.js +391 -53
- package/dist/lib/config.js +13 -1
- package/dist/lib/consolidation.js +1 -1
- package/dist/lib/database.js +124 -0
- package/dist/lib/db.js +124 -0
- package/dist/lib/device-registry.js +124 -0
- package/dist/lib/embedder.js +13 -1
- package/dist/lib/exe-daemon.js +2744 -715
- package/dist/lib/hybrid-search.js +518 -24
- package/dist/lib/identity.js +3 -0
- package/dist/lib/keychain.js +178 -22
- package/dist/lib/messaging.js +3 -0
- package/dist/lib/reminders.js +3 -0
- package/dist/lib/schedules.js +233 -20
- package/dist/lib/skill-learning.js +16 -1
- package/dist/lib/store.js +506 -20
- package/dist/lib/tasks.js +16 -1
- package/dist/lib/tmux-routing.js +16 -1
- package/dist/lib/token-spend.js +3 -0
- package/dist/mcp/server.js +1757 -428
- package/dist/mcp/tools/complete-reminder.js +3 -0
- package/dist/mcp/tools/create-reminder.js +3 -0
- package/dist/mcp/tools/create-task.js +16 -1
- package/dist/mcp/tools/deactivate-behavior.js +3 -0
- package/dist/mcp/tools/list-reminders.js +3 -0
- package/dist/mcp/tools/list-tasks.js +3 -0
- package/dist/mcp/tools/send-message.js +3 -0
- package/dist/mcp/tools/update-task.js +16 -1
- package/dist/runtime/index.js +516 -22
- package/dist/tui/App.js +594 -29
- package/package.json +8 -5
- package/src/commands/exe/cloud.md +6 -10
- package/stack.release.json +3 -3
- package/src/commands/exe/link.md +0 -18
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"latest": "0.9.
|
|
3
|
+
"latest": "0.9.2",
|
|
4
4
|
"stacks": {
|
|
5
5
|
"0.9.0": {
|
|
6
6
|
"version": "0.9.0",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"exed": {
|
|
31
31
|
"env": "EXED_IMAGE_TAG",
|
|
32
|
-
"image": "ghcr.io/askexe/exed:v0.9.
|
|
32
|
+
"image": "ghcr.io/askexe/exed:v0.9.66",
|
|
33
33
|
"healthUrl": "http://127.0.0.1:8765/health"
|
|
34
34
|
},
|
|
35
35
|
"gateway": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
45
|
"releaseDescriptors": {
|
|
46
|
-
"exed": "AskExe/exe-os@0.9.
|
|
46
|
+
"exed": "AskExe/exe-os@0.9.66:stack.release.json",
|
|
47
47
|
"crm": "AskExe/exe-crm@0.9.0:stack.release.json",
|
|
48
48
|
"wiki": "AskExe/exe-wiki@0.9.0:stack.release.json",
|
|
49
49
|
"gateway": "AskExe/exe-gateway@0.9.0:stack.release.json",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
},
|
|
79
79
|
"exed": {
|
|
80
80
|
"env": "EXED_IMAGE_TAG",
|
|
81
|
-
"image": "ghcr.io/askexe/exed:v0.9.
|
|
81
|
+
"image": "ghcr.io/askexe/exed:v0.9.66",
|
|
82
82
|
"healthUrl": "http://127.0.0.1:8765/health"
|
|
83
83
|
},
|
|
84
84
|
"gateway": {
|
|
@@ -92,13 +92,62 @@
|
|
|
92
92
|
}
|
|
93
93
|
},
|
|
94
94
|
"releaseDescriptors": {
|
|
95
|
-
"exed": "AskExe/exe-os@0.9.
|
|
95
|
+
"exed": "AskExe/exe-os@0.9.66:stack.release.json",
|
|
96
96
|
"crm": "AskExe/exe-crm@0.9.1:stack.release.json",
|
|
97
97
|
"wiki": "AskExe/exe-wiki@0.9.1:stack.release.json",
|
|
98
98
|
"gateway": "AskExe/exe-gateway@0.9.1:stack.release.json",
|
|
99
99
|
"db": "AskExe/exe-db@0.9.1:stack.release.json",
|
|
100
100
|
"monitorAgent": "AskExe/exe-monitor@0.9.1:stack.release.json"
|
|
101
101
|
}
|
|
102
|
+
},
|
|
103
|
+
"0.9.2": {
|
|
104
|
+
"version": "0.9.2",
|
|
105
|
+
"releasedAt": "2026-05-12T00:00:00Z",
|
|
106
|
+
"notes": "Hygo private/customer pilot stack release. Aligns all deployable service, image, and package versions on 0.9.2 after exe-db-jkt dogfood fixes.",
|
|
107
|
+
"breakingChanges": [
|
|
108
|
+
{
|
|
109
|
+
"id": "whatsapp_relink_required",
|
|
110
|
+
"title": "WhatsApp QR re-link required for Baileys v7",
|
|
111
|
+
"description": "exe-gateway uses Baileys v7. Existing WhatsApp 6.x linked-device auth state must be backed up and re-linked once with a new QR code.",
|
|
112
|
+
"requiredAction": "Open https://<gateway-domain>/pair/default?token=<admin-token> and scan from WhatsApp \u2192 Linked Devices after the update.",
|
|
113
|
+
"expectedDowntimeMinutes": "2-5",
|
|
114
|
+
"requiresConfirmation": true
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
"services": {
|
|
118
|
+
"crm": {
|
|
119
|
+
"env": "CRM_IMAGE_TAG",
|
|
120
|
+
"image": "ghcr.io/askexe/exe-crm:v0.9.2",
|
|
121
|
+
"healthUrl": "http://127.0.0.1:3000/healthz"
|
|
122
|
+
},
|
|
123
|
+
"wiki": {
|
|
124
|
+
"env": "WIKI_IMAGE_TAG",
|
|
125
|
+
"image": "ghcr.io/askexe/exe-wiki:v0.9.2",
|
|
126
|
+
"healthUrl": "http://127.0.0.1:3001/api/ping"
|
|
127
|
+
},
|
|
128
|
+
"exed": {
|
|
129
|
+
"env": "EXED_IMAGE_TAG",
|
|
130
|
+
"image": "ghcr.io/askexe/exed:v0.9.2",
|
|
131
|
+
"healthUrl": "http://127.0.0.1:8765/health"
|
|
132
|
+
},
|
|
133
|
+
"gateway": {
|
|
134
|
+
"env": "GATEWAY_IMAGE_TAG",
|
|
135
|
+
"image": "ghcr.io/askexe/exe-gateway:v0.9.2",
|
|
136
|
+
"healthUrl": "http://127.0.0.1:3100/health"
|
|
137
|
+
},
|
|
138
|
+
"monitorAgent": {
|
|
139
|
+
"env": "MONITOR_AGENT_IMAGE_TAG",
|
|
140
|
+
"image": "ghcr.io/askexe/exe-monitor-agent:v0.9.2"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"releaseDescriptors": {
|
|
144
|
+
"exed": "AskExe/exe-os@0.9.2:stack.release.json",
|
|
145
|
+
"crm": "AskExe/exe-crm@0.9.2:stack.release.json",
|
|
146
|
+
"wiki": "AskExe/exe-wiki@0.9.2:stack.release.json",
|
|
147
|
+
"gateway": "AskExe/exe-gateway@0.9.2:stack.release.json",
|
|
148
|
+
"db": "AskExe/exe-db@0.9.2:stack.release.json",
|
|
149
|
+
"monitorAgent": "AskExe/exe-monitor@0.9.2:stack.release.json"
|
|
150
|
+
}
|
|
102
151
|
}
|
|
103
152
|
}
|
|
104
153
|
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin/age-ontology-load.ts
|
|
4
|
+
import { Client } from "pg";
|
|
5
|
+
|
|
6
|
+
// src/lib/background-jobs.ts
|
|
7
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync, unlinkSync } from "fs";
|
|
8
|
+
import { execFileSync } from "child_process";
|
|
9
|
+
import os2 from "os";
|
|
10
|
+
import path2 from "path";
|
|
11
|
+
|
|
12
|
+
// src/lib/config.ts
|
|
13
|
+
import { readFile, writeFile } from "fs/promises";
|
|
14
|
+
import { readFileSync, existsSync as existsSync2, renameSync } from "fs";
|
|
15
|
+
import path from "path";
|
|
16
|
+
import os from "os";
|
|
17
|
+
|
|
18
|
+
// src/lib/secure-files.ts
|
|
19
|
+
import { chmodSync, existsSync, mkdirSync } from "fs";
|
|
20
|
+
import { chmod, mkdir } from "fs/promises";
|
|
21
|
+
|
|
22
|
+
// src/lib/config.ts
|
|
23
|
+
function resolveDataDir() {
|
|
24
|
+
if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
|
|
25
|
+
if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
|
|
26
|
+
const newDir = path.join(os.homedir(), ".exe-os");
|
|
27
|
+
const legacyDir = path.join(os.homedir(), ".exe-mem");
|
|
28
|
+
if (!existsSync2(newDir) && existsSync2(legacyDir)) {
|
|
29
|
+
try {
|
|
30
|
+
renameSync(legacyDir, newDir);
|
|
31
|
+
process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
|
|
32
|
+
`);
|
|
33
|
+
} catch {
|
|
34
|
+
return legacyDir;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return newDir;
|
|
38
|
+
}
|
|
39
|
+
var EXE_AI_DIR = resolveDataDir();
|
|
40
|
+
var DB_PATH = path.join(EXE_AI_DIR, "memories.db");
|
|
41
|
+
var MODELS_DIR = path.join(EXE_AI_DIR, "models");
|
|
42
|
+
var CONFIG_PATH = path.join(EXE_AI_DIR, "config.json");
|
|
43
|
+
var LEGACY_LANCE_PATH = path.join(EXE_AI_DIR, "local.lance");
|
|
44
|
+
var CURRENT_CONFIG_VERSION = 1;
|
|
45
|
+
var DEFAULT_CONFIG = {
|
|
46
|
+
config_version: CURRENT_CONFIG_VERSION,
|
|
47
|
+
dbPath: DB_PATH,
|
|
48
|
+
modelFile: "jina-embeddings-v5-small-q4_k_m.gguf",
|
|
49
|
+
embeddingDim: 1024,
|
|
50
|
+
batchSize: 20,
|
|
51
|
+
flushIntervalMs: 1e4,
|
|
52
|
+
autoIngestion: true,
|
|
53
|
+
autoRetrieval: true,
|
|
54
|
+
searchMode: "hybrid",
|
|
55
|
+
hookSearchMode: "hybrid",
|
|
56
|
+
fileGrepEnabled: true,
|
|
57
|
+
splashEffect: true,
|
|
58
|
+
consolidationEnabled: true,
|
|
59
|
+
consolidationIntervalMs: 6 * 60 * 60 * 1e3,
|
|
60
|
+
consolidationModel: "claude-haiku-4-5-20251001",
|
|
61
|
+
consolidationMaxCallsPerRun: 20,
|
|
62
|
+
selfQueryRouter: true,
|
|
63
|
+
selfQueryModel: "claude-haiku-4-5-20251001",
|
|
64
|
+
rerankerEnabled: true,
|
|
65
|
+
scalingRoadmap: {
|
|
66
|
+
rerankerAutoTrigger: {
|
|
67
|
+
enabled: true,
|
|
68
|
+
broadQueryMinCardinality: 5e4,
|
|
69
|
+
fetchTopK: 200,
|
|
70
|
+
returnTopK: 20
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
graphRagEnabled: true,
|
|
74
|
+
wikiEnabled: false,
|
|
75
|
+
wikiUrl: "",
|
|
76
|
+
wikiApiKey: "",
|
|
77
|
+
wikiSyncIntervalMs: 30 * 60 * 1e3,
|
|
78
|
+
wikiWorkspaceMapping: {},
|
|
79
|
+
wikiAutoUpdate: true,
|
|
80
|
+
wikiAutoUpdateThreshold: 0.5,
|
|
81
|
+
wikiAutoUpdateCreateNew: true,
|
|
82
|
+
skillLearning: true,
|
|
83
|
+
skillThreshold: 3,
|
|
84
|
+
skillModel: "claude-haiku-4-5-20251001",
|
|
85
|
+
exeHeartbeat: {
|
|
86
|
+
enabled: true,
|
|
87
|
+
intervalSeconds: 60,
|
|
88
|
+
staleInProgressThresholdHours: 2
|
|
89
|
+
},
|
|
90
|
+
sessionLifecycle: {
|
|
91
|
+
idleKillEnabled: true,
|
|
92
|
+
idleKillTicksRequired: 3,
|
|
93
|
+
idleKillIntercomAckWindowMs: 1e4,
|
|
94
|
+
maxAutoInstances: 10
|
|
95
|
+
},
|
|
96
|
+
autoUpdate: {
|
|
97
|
+
checkOnBoot: true,
|
|
98
|
+
autoInstall: false,
|
|
99
|
+
checkIntervalMs: 24 * 60 * 60 * 1e3
|
|
100
|
+
},
|
|
101
|
+
orchestration: {
|
|
102
|
+
phase: "phase_1_coo",
|
|
103
|
+
phaseSetBy: "default"
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// src/lib/background-jobs.ts
|
|
108
|
+
var JOB_DIR = path2.join(EXE_AI_DIR, "jobs");
|
|
109
|
+
var JOBS_FILE = path2.join(JOB_DIR, "jobs.json");
|
|
110
|
+
var LOCK_DIR = path2.join(JOB_DIR, "locks");
|
|
111
|
+
var DEFAULT_LOCK_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
112
|
+
var MAX_HISTORY = 200;
|
|
113
|
+
function ensureDirs() {
|
|
114
|
+
mkdirSync2(LOCK_DIR, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
function now() {
|
|
117
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
118
|
+
}
|
|
119
|
+
function isAlive(pid) {
|
|
120
|
+
if (!pid || pid <= 0) return false;
|
|
121
|
+
try {
|
|
122
|
+
process.kill(pid, 0);
|
|
123
|
+
return true;
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function readJobsRaw() {
|
|
129
|
+
ensureDirs();
|
|
130
|
+
if (!existsSync3(JOBS_FILE)) return [];
|
|
131
|
+
try {
|
|
132
|
+
const parsed = JSON.parse(readFileSync2(JOBS_FILE, "utf8"));
|
|
133
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
134
|
+
} catch {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function writeJobsRaw(jobs) {
|
|
139
|
+
ensureDirs();
|
|
140
|
+
const running = jobs.filter((j) => j.status === "running");
|
|
141
|
+
const rest = jobs.filter((j) => j.status !== "running").slice(-MAX_HISTORY);
|
|
142
|
+
writeFileSync(JOBS_FILE, JSON.stringify([...rest, ...running], null, 2) + "\n");
|
|
143
|
+
}
|
|
144
|
+
function lockPath(type) {
|
|
145
|
+
return path2.join(LOCK_DIR, `${type.replace(/[^a-zA-Z0-9_.-]/g, "_")}.lock`);
|
|
146
|
+
}
|
|
147
|
+
function acquireJobLock(type, ttlMs = DEFAULT_LOCK_TTL_MS) {
|
|
148
|
+
ensureDirs();
|
|
149
|
+
const file = lockPath(type);
|
|
150
|
+
if (existsSync3(file)) {
|
|
151
|
+
try {
|
|
152
|
+
const lock = JSON.parse(readFileSync2(file, "utf8"));
|
|
153
|
+
const age = Date.now() - Date.parse(lock.updatedAt ?? "");
|
|
154
|
+
if (lock.pid && isAlive(lock.pid) && Number.isFinite(age) && age < ttlMs) return false;
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
unlinkSync(file);
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
writeFileSync(file, JSON.stringify({ pid: process.pid, updatedAt: now() }, null, 2) + "\n", { flag: "wx" });
|
|
164
|
+
return true;
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function releaseJobLock(type) {
|
|
170
|
+
const file = lockPath(type);
|
|
171
|
+
try {
|
|
172
|
+
if (!existsSync3(file)) return;
|
|
173
|
+
const lock = JSON.parse(readFileSync2(file, "utf8"));
|
|
174
|
+
if (lock.pid === process.pid || !lock.pid || !isAlive(lock.pid)) unlinkSync(file);
|
|
175
|
+
} catch {
|
|
176
|
+
try {
|
|
177
|
+
unlinkSync(file);
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function startManagedJob(options) {
|
|
183
|
+
const lowPriority = options.lowPriority ?? true;
|
|
184
|
+
if (!acquireJobLock(options.type, options.lockTtlMs)) return null;
|
|
185
|
+
if (lowPriority) {
|
|
186
|
+
try {
|
|
187
|
+
os2.setPriority(process.pid, 10);
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const id = `${options.type}-${Date.now()}-${process.pid}`.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
192
|
+
const record = {
|
|
193
|
+
id,
|
|
194
|
+
type: options.type,
|
|
195
|
+
name: options.name,
|
|
196
|
+
pid: process.pid,
|
|
197
|
+
command: options.command ?? process.argv.join(" "),
|
|
198
|
+
cwd: process.cwd(),
|
|
199
|
+
status: "running",
|
|
200
|
+
startedAt: now(),
|
|
201
|
+
updatedAt: now(),
|
|
202
|
+
lastHeartbeatAt: now(),
|
|
203
|
+
cancelCommand: `exe-os jobs cancel ${id}`,
|
|
204
|
+
lowPriority
|
|
205
|
+
};
|
|
206
|
+
const upsert = (patch) => {
|
|
207
|
+
const jobs = readJobsRaw().filter((j) => j.id !== id);
|
|
208
|
+
Object.assign(record, patch, { updatedAt: now() });
|
|
209
|
+
writeJobsRaw([...jobs, record]);
|
|
210
|
+
const file = lockPath(options.type);
|
|
211
|
+
try {
|
|
212
|
+
writeFileSync(file, JSON.stringify({ pid: process.pid, jobId: id, updatedAt: record.updatedAt }, null, 2) + "\n");
|
|
213
|
+
} catch {
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
upsert({});
|
|
217
|
+
const timer = setInterval(() => upsert({ lastHeartbeatAt: now() }), 3e4);
|
|
218
|
+
timer.unref?.();
|
|
219
|
+
const cleanup = (status, error) => {
|
|
220
|
+
clearInterval(timer);
|
|
221
|
+
upsert({ status, error, lastHeartbeatAt: now() });
|
|
222
|
+
releaseJobLock(options.type);
|
|
223
|
+
};
|
|
224
|
+
process.once("SIGTERM", () => {
|
|
225
|
+
cleanup("cancelled");
|
|
226
|
+
process.exit(0);
|
|
227
|
+
});
|
|
228
|
+
process.once("SIGINT", () => {
|
|
229
|
+
cleanup("cancelled");
|
|
230
|
+
process.exit(130);
|
|
231
|
+
});
|
|
232
|
+
process.once("exit", () => releaseJobLock(options.type));
|
|
233
|
+
return {
|
|
234
|
+
id,
|
|
235
|
+
update(progress) {
|
|
236
|
+
upsert({ progressCurrent: progress.current, progressTotal: progress.total, progressLabel: progress.label, lastHeartbeatAt: now() });
|
|
237
|
+
},
|
|
238
|
+
complete() {
|
|
239
|
+
cleanup("completed");
|
|
240
|
+
},
|
|
241
|
+
fail(err) {
|
|
242
|
+
cleanup("failed", err instanceof Error ? err.message : String(err));
|
|
243
|
+
},
|
|
244
|
+
cancel() {
|
|
245
|
+
cleanup("cancelled");
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
async function politeBatchPause(ms = 250) {
|
|
250
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/bin/age-ontology-load.ts
|
|
254
|
+
function q(value) {
|
|
255
|
+
return `'${String(value ?? "").replace(/\u0000/g, "").slice(0, 500).replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
256
|
+
}
|
|
257
|
+
function sqlString(value) {
|
|
258
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
259
|
+
}
|
|
260
|
+
async function main() {
|
|
261
|
+
const job = startManagedJob({ type: "age-ontology-load", name: "Apache AGE ontology loader", lowPriority: true });
|
|
262
|
+
if (!job) {
|
|
263
|
+
process.stderr.write("[age-ontology-load] Another AGE ontology load is already running.\n");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const sourceUrl = process.env.DATABASE_URL || process.env.EXED_DATABASE_URL;
|
|
267
|
+
const ageUrl = process.env.AGE_DATABASE_URL;
|
|
268
|
+
if (!sourceUrl) throw new Error("DATABASE_URL or EXED_DATABASE_URL is required for canonical source");
|
|
269
|
+
if (!ageUrl) throw new Error("AGE_DATABASE_URL is required for Apache AGE target");
|
|
270
|
+
const graph = process.env.AGE_GRAPH_NAME || "exe_ontology";
|
|
271
|
+
const limit = Number(process.argv[process.argv.indexOf("--limit") + 1] || "1000");
|
|
272
|
+
const source = new Client({ connectionString: sourceUrl });
|
|
273
|
+
const age = new Client({ connectionString: ageUrl });
|
|
274
|
+
await source.connect();
|
|
275
|
+
await age.connect();
|
|
276
|
+
try {
|
|
277
|
+
await age.query(`CREATE EXTENSION IF NOT EXISTS age`);
|
|
278
|
+
await age.query(`LOAD 'age'`);
|
|
279
|
+
await age.query(`SET search_path = ag_catalog, "$user", public`);
|
|
280
|
+
if (process.argv.includes("--reset")) {
|
|
281
|
+
try {
|
|
282
|
+
await age.query(`SELECT drop_graph(${sqlString(graph)}, true)`);
|
|
283
|
+
} catch {
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
await age.query(`SELECT create_graph(${sqlString(graph)})`);
|
|
288
|
+
} catch {
|
|
289
|
+
}
|
|
290
|
+
const entities = await source.query(`SELECT id, name, type FROM memory.entities ORDER BY last_seen DESC NULLS LAST, id LIMIT $1`, [limit]);
|
|
291
|
+
let nodeCount = 0;
|
|
292
|
+
for (const entity of entities.rows) {
|
|
293
|
+
const cypher = `CREATE (:Entity {id: ${q(entity.id)}, name: ${q(entity.name)}, type: ${q(entity.type)}})`;
|
|
294
|
+
await age.query(`SELECT * FROM cypher(${sqlString(graph)}, $$ ${cypher} $$) AS (v agtype)`);
|
|
295
|
+
nodeCount++;
|
|
296
|
+
if (nodeCount % 250 === 0) {
|
|
297
|
+
job.update({ current: nodeCount, total: limit, label: `Loaded ${nodeCount} AGE nodes` });
|
|
298
|
+
await politeBatchPause(250);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const relationships = await source.query(`SELECT source_entity_id, target_entity_id, type, confidence FROM memory.relationships ORDER BY id LIMIT $1`, [limit * 2]);
|
|
302
|
+
let edgeCount = 0;
|
|
303
|
+
for (const rel of relationships.rows) {
|
|
304
|
+
const cypher = `MATCH (a:Entity {id: ${q(rel.source_entity_id)}}), (b:Entity {id: ${q(rel.target_entity_id)}}) CREATE (a)-[:RELATED {type: ${q(rel.type)}}]->(b)`;
|
|
305
|
+
await age.query(`SELECT * FROM cypher(${sqlString(graph)}, $$ ${cypher} $$) AS (e agtype)`);
|
|
306
|
+
edgeCount++;
|
|
307
|
+
if (edgeCount % 500 === 0) {
|
|
308
|
+
job.update({ current: nodeCount + edgeCount, total: limit * 3, label: `Loaded ${nodeCount} nodes, ${edgeCount} edges` });
|
|
309
|
+
await politeBatchPause(250);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
process.stderr.write(`[age-ontology-load] Loaded ${nodeCount} nodes and ${edgeCount} edges into ${graph}.
|
|
313
|
+
`);
|
|
314
|
+
job.complete();
|
|
315
|
+
} finally {
|
|
316
|
+
await age.end();
|
|
317
|
+
await source.end();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
main().catch((err) => {
|
|
321
|
+
process.stderr.write(`[age-ontology-load] FATAL: ${err instanceof Error ? err.message : String(err)}
|
|
322
|
+
`);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
});
|