@askexenow/exe-os 0.9.67 → 0.9.69

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.
@@ -3202,10 +3202,10 @@ function evictLRU() {
3202
3202
  }
3203
3203
  }
3204
3204
  function evictIdleShards() {
3205
- const now = Date.now();
3205
+ const now2 = Date.now();
3206
3206
  const toEvict = [];
3207
3207
  for (const [name, lastAccess] of _shardLastAccess) {
3208
- if (now - lastAccess > SHARD_IDLE_MS) {
3208
+ if (now2 - lastAccess > SHARD_IDLE_MS) {
3209
3209
  toEvict.push(name);
3210
3210
  }
3211
3211
  }
@@ -3485,22 +3485,22 @@ ${sections.join("\n\n")}
3485
3485
  }
3486
3486
  async function storeGlobalProcedure(input) {
3487
3487
  const id = randomUUID2();
3488
- const now = (/* @__PURE__ */ new Date()).toISOString();
3488
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
3489
3489
  const client = getClient();
3490
3490
  await client.execute({
3491
3491
  sql: `INSERT INTO company_procedures (id, title, content, priority, domain, active, created_at, updated_at)
3492
3492
  VALUES (?, ?, ?, ?, ?, 1, ?, ?)`,
3493
- args: [id, input.title, input.content, input.priority ?? "p0", input.domain ?? null, now, now]
3493
+ args: [id, input.title, input.content, input.priority ?? "p0", input.domain ?? null, now2, now2]
3494
3494
  });
3495
3495
  await loadGlobalProcedures();
3496
3496
  return id;
3497
3497
  }
3498
3498
  async function deactivateGlobalProcedure(id) {
3499
- const now = (/* @__PURE__ */ new Date()).toISOString();
3499
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
3500
3500
  const client = getClient();
3501
3501
  const result = await client.execute({
3502
3502
  sql: "UPDATE company_procedures SET active = 0, updated_at = ? WHERE id = ?",
3503
- args: [now, id]
3503
+ args: [now2, id]
3504
3504
  });
3505
3505
  await loadGlobalProcedures();
3506
3506
  return result.rowsAffected > 0;
@@ -3593,7 +3593,7 @@ function extractMemoryCards(row) {
3593
3593
  async function insertMemoryCardsForBatch(rows) {
3594
3594
  const cards = rows.flatMap(extractMemoryCards);
3595
3595
  if (cards.length === 0) return 0;
3596
- const now = (/* @__PURE__ */ new Date()).toISOString();
3596
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
3597
3597
  const client = getClient();
3598
3598
  const stmts = cards.map((card) => ({
3599
3599
  sql: `INSERT OR IGNORE INTO memory_cards
@@ -3614,7 +3614,7 @@ async function insertMemoryCardsForBatch(rows) {
3614
3614
  card.content,
3615
3615
  card.source_ref,
3616
3616
  card.confidence,
3617
- now
3617
+ now2
3618
3618
  ]
3619
3619
  }));
3620
3620
  await client.batch(stmts, "write");
@@ -3851,7 +3851,7 @@ async function insertOntologyForMemory(row, client) {
3851
3851
  const intention = inferIntention(row);
3852
3852
  const outcome = inferOutcome(row);
3853
3853
  const eventId = stableId2("event", row.id);
3854
- const now = (/* @__PURE__ */ new Date()).toISOString();
3854
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
3855
3855
  await db.execute({
3856
3856
  sql: `INSERT INTO agent_sessions (id, agent_id, project_name, started_at, last_event_at, event_count, properties)
3857
3857
  VALUES (?, ?, ?, ?, ?, 1, ?)
@@ -3880,7 +3880,7 @@ async function insertOntologyForMemory(row, client) {
3880
3880
  row.id,
3881
3881
  row.has_error ? "negative" : outcome === "success_signal" ? "positive" : "neutral",
3882
3882
  JSON.stringify(ontologyPayload(row)),
3883
- now
3883
+ now2
3884
3884
  ]
3885
3885
  });
3886
3886
  const semantic = inferSemanticLabel(row);
@@ -3898,8 +3898,8 @@ async function insertOntologyForMemory(row, client) {
3898
3898
  semantic.schemaVersion,
3899
3899
  semantic.confidence,
3900
3900
  JSON.stringify(semantic),
3901
- now,
3902
- now
3901
+ now2,
3902
+ now2
3903
3903
  ]
3904
3904
  });
3905
3905
  for (const statement of extractGoalCandidates(row)) {
@@ -3910,19 +3910,19 @@ async function insertOntologyForMemory(row, client) {
3910
3910
  parent_goal_id, due_at, achieved_at, supersedes_id, created_at, updated_at, source_memory_id)
3911
3911
  VALUES (?, ?, ?, ?, 'open', 5, NULL, NULL, NULL, NULL, NULL, ?, ?, ?)
3912
3912
  ON CONFLICT(id) DO UPDATE SET updated_at = excluded.updated_at`,
3913
- args: [goalId, statement, row.agent_id, row.project_name, now, now, row.id]
3913
+ args: [goalId, statement, row.agent_id, row.project_name, now2, now2, row.id]
3914
3914
  });
3915
3915
  await db.execute({
3916
3916
  sql: `INSERT OR IGNORE INTO agent_goal_links
3917
3917
  (id, goal_id, link_type, target_id, target_type, created_at)
3918
3918
  VALUES (?, ?, 'evidence', ?, 'memory', ?)`,
3919
- args: [stableId2("goal_link", goalId, row.id, "memory"), goalId, row.id, now]
3919
+ args: [stableId2("goal_link", goalId, row.id, "memory"), goalId, row.id, now2]
3920
3920
  });
3921
3921
  await db.execute({
3922
3922
  sql: `INSERT OR IGNORE INTO agent_goal_links
3923
3923
  (id, goal_id, link_type, target_id, target_type, created_at)
3924
3924
  VALUES (?, ?, 'event', ?, 'event', ?)`,
3925
- args: [stableId2("goal_link", goalId, eventId, "event"), goalId, eventId, now]
3925
+ args: [stableId2("goal_link", goalId, eventId, "event"), goalId, eventId, now2]
3926
3926
  });
3927
3927
  }
3928
3928
  }
@@ -4106,8 +4106,8 @@ function deriveMachineKey() {
4106
4106
  }
4107
4107
  function readMachineId() {
4108
4108
  try {
4109
- const { readFileSync: readFileSync5 } = __require("fs");
4110
- return readFileSync5("/etc/machine-id", "utf-8").trim();
4109
+ const { readFileSync: readFileSync6 } = __require("fs");
4110
+ return readFileSync6("/etc/machine-id", "utf-8").trim();
4111
4111
  } catch {
4112
4112
  return "";
4113
4113
  }
@@ -5308,8 +5308,164 @@ async function extractBatch(client, batchSize = 50, model = "claude-haiku-4-5-20
5308
5308
  return { processed: result.rows.length, entities: totalEntities, relationships: totalRelationships };
5309
5309
  }
5310
5310
 
5311
+ // src/lib/background-jobs.ts
5312
+ init_config();
5313
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3 } from "fs";
5314
+ import { execFileSync } from "child_process";
5315
+ import os6 from "os";
5316
+ import path8 from "path";
5317
+ var JOB_DIR = path8.join(EXE_AI_DIR, "jobs");
5318
+ var JOBS_FILE = path8.join(JOB_DIR, "jobs.json");
5319
+ var LOCK_DIR = path8.join(JOB_DIR, "locks");
5320
+ var DEFAULT_LOCK_TTL_MS = 6 * 60 * 60 * 1e3;
5321
+ var MAX_HISTORY = 200;
5322
+ function ensureDirs() {
5323
+ mkdirSync3(LOCK_DIR, { recursive: true });
5324
+ }
5325
+ function now() {
5326
+ return (/* @__PURE__ */ new Date()).toISOString();
5327
+ }
5328
+ function isAlive(pid) {
5329
+ if (!pid || pid <= 0) return false;
5330
+ try {
5331
+ process.kill(pid, 0);
5332
+ return true;
5333
+ } catch {
5334
+ return false;
5335
+ }
5336
+ }
5337
+ function readJobsRaw() {
5338
+ ensureDirs();
5339
+ if (!existsSync8(JOBS_FILE)) return [];
5340
+ try {
5341
+ const parsed = JSON.parse(readFileSync5(JOBS_FILE, "utf8"));
5342
+ return Array.isArray(parsed) ? parsed : [];
5343
+ } catch {
5344
+ return [];
5345
+ }
5346
+ }
5347
+ function writeJobsRaw(jobs) {
5348
+ ensureDirs();
5349
+ const running = jobs.filter((j) => j.status === "running");
5350
+ const rest = jobs.filter((j) => j.status !== "running").slice(-MAX_HISTORY);
5351
+ writeFileSync3(JOBS_FILE, JSON.stringify([...rest, ...running], null, 2) + "\n");
5352
+ }
5353
+ function lockPath(type) {
5354
+ return path8.join(LOCK_DIR, `${type.replace(/[^a-zA-Z0-9_.-]/g, "_")}.lock`);
5355
+ }
5356
+ function acquireJobLock(type, ttlMs = DEFAULT_LOCK_TTL_MS) {
5357
+ ensureDirs();
5358
+ const file = lockPath(type);
5359
+ if (existsSync8(file)) {
5360
+ try {
5361
+ const lock = JSON.parse(readFileSync5(file, "utf8"));
5362
+ const age = Date.now() - Date.parse(lock.updatedAt ?? "");
5363
+ if (lock.pid && isAlive(lock.pid) && Number.isFinite(age) && age < ttlMs) return false;
5364
+ } catch {
5365
+ }
5366
+ try {
5367
+ unlinkSync3(file);
5368
+ } catch {
5369
+ }
5370
+ }
5371
+ try {
5372
+ writeFileSync3(file, JSON.stringify({ pid: process.pid, updatedAt: now() }, null, 2) + "\n", { flag: "wx" });
5373
+ return true;
5374
+ } catch {
5375
+ return false;
5376
+ }
5377
+ }
5378
+ function releaseJobLock(type) {
5379
+ const file = lockPath(type);
5380
+ try {
5381
+ if (!existsSync8(file)) return;
5382
+ const lock = JSON.parse(readFileSync5(file, "utf8"));
5383
+ if (lock.pid === process.pid || !lock.pid || !isAlive(lock.pid)) unlinkSync3(file);
5384
+ } catch {
5385
+ try {
5386
+ unlinkSync3(file);
5387
+ } catch {
5388
+ }
5389
+ }
5390
+ }
5391
+ function startManagedJob(options) {
5392
+ const lowPriority = options.lowPriority ?? true;
5393
+ if (!acquireJobLock(options.type, options.lockTtlMs)) return null;
5394
+ if (lowPriority) {
5395
+ try {
5396
+ os6.setPriority(process.pid, 10);
5397
+ } catch {
5398
+ }
5399
+ }
5400
+ const id = `${options.type}-${Date.now()}-${process.pid}`.replace(/[^a-zA-Z0-9_.-]/g, "_");
5401
+ const record = {
5402
+ id,
5403
+ type: options.type,
5404
+ name: options.name,
5405
+ pid: process.pid,
5406
+ command: options.command ?? process.argv.join(" "),
5407
+ cwd: process.cwd(),
5408
+ status: "running",
5409
+ startedAt: now(),
5410
+ updatedAt: now(),
5411
+ lastHeartbeatAt: now(),
5412
+ cancelCommand: `exe-os jobs cancel ${id}`,
5413
+ lowPriority
5414
+ };
5415
+ const upsert = (patch) => {
5416
+ const jobs = readJobsRaw().filter((j) => j.id !== id);
5417
+ Object.assign(record, patch, { updatedAt: now() });
5418
+ writeJobsRaw([...jobs, record]);
5419
+ const file = lockPath(options.type);
5420
+ try {
5421
+ writeFileSync3(file, JSON.stringify({ pid: process.pid, jobId: id, updatedAt: record.updatedAt }, null, 2) + "\n");
5422
+ } catch {
5423
+ }
5424
+ };
5425
+ upsert({});
5426
+ const timer = setInterval(() => upsert({ lastHeartbeatAt: now() }), 3e4);
5427
+ timer.unref?.();
5428
+ const cleanup = (status, error) => {
5429
+ clearInterval(timer);
5430
+ upsert({ status, error, lastHeartbeatAt: now() });
5431
+ releaseJobLock(options.type);
5432
+ };
5433
+ process.once("SIGTERM", () => {
5434
+ cleanup("cancelled");
5435
+ process.exit(0);
5436
+ });
5437
+ process.once("SIGINT", () => {
5438
+ cleanup("cancelled");
5439
+ process.exit(130);
5440
+ });
5441
+ process.once("exit", () => releaseJobLock(options.type));
5442
+ return {
5443
+ id,
5444
+ update(progress) {
5445
+ upsert({ progressCurrent: progress.current, progressTotal: progress.total, progressLabel: progress.label, lastHeartbeatAt: now() });
5446
+ },
5447
+ complete() {
5448
+ cleanup("completed");
5449
+ },
5450
+ fail(err) {
5451
+ cleanup("failed", err instanceof Error ? err.message : String(err));
5452
+ },
5453
+ cancel() {
5454
+ cleanup("cancelled");
5455
+ }
5456
+ };
5457
+ }
5458
+ async function politeBatchPause(ms = 250) {
5459
+ await new Promise((resolve) => setTimeout(resolve, ms));
5460
+ }
5461
+
5311
5462
  // src/bin/graph-backfill.ts
5312
5463
  async function main() {
5464
+ const job = startManagedJob({ type: "graph-backfill", name: "GraphRAG extraction backfill", lowPriority: true });
5465
+ if (!job) {
5466
+ process.stderr.write("[graph-backfill] Another GraphRAG backfill is already running.\n");
5467
+ return;
5468
+ }
5313
5469
  await initStore();
5314
5470
  const client = getClient();
5315
5471
  const countResult = await client.execute(
@@ -5336,11 +5492,14 @@ async function main() {
5336
5492
  `[graph-backfill] Progress: ${totalProcessed}/${total} | +${result.entities} entities, +${result.relationships} relationships
5337
5493
  `
5338
5494
  );
5495
+ job.update({ current: totalProcessed, total, label: `${totalEntities} entities, ${totalRelationships} relationships` });
5496
+ await politeBatchPause(500);
5339
5497
  }
5340
5498
  process.stderr.write(
5341
5499
  `[graph-backfill] Complete: ${totalProcessed} memories, ${totalEntities} entities, ${totalRelationships} relationships.
5342
5500
  `
5343
5501
  );
5502
+ job.complete();
5344
5503
  await disposeStore();
5345
5504
  }
5346
5505
  main().catch((err) => {
@@ -1,4 +1,113 @@
1
1
  #!/usr/bin/env node
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+
7
+ // src/lib/secure-files.ts
8
+ import { chmodSync, existsSync, mkdirSync } from "fs";
9
+ import { chmod, mkdir } from "fs/promises";
10
+ var init_secure_files = __esm({
11
+ "src/lib/secure-files.ts"() {
12
+ "use strict";
13
+ }
14
+ });
15
+
16
+ // src/lib/config.ts
17
+ import { readFile, writeFile } from "fs/promises";
18
+ import { readFileSync, existsSync as existsSync2, renameSync } from "fs";
19
+ import path from "path";
20
+ import os from "os";
21
+ function resolveDataDir() {
22
+ if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
23
+ if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
24
+ const newDir = path.join(os.homedir(), ".exe-os");
25
+ const legacyDir = path.join(os.homedir(), ".exe-mem");
26
+ if (!existsSync2(newDir) && existsSync2(legacyDir)) {
27
+ try {
28
+ renameSync(legacyDir, newDir);
29
+ process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
30
+ `);
31
+ } catch {
32
+ return legacyDir;
33
+ }
34
+ }
35
+ return newDir;
36
+ }
37
+ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG;
38
+ var init_config = __esm({
39
+ "src/lib/config.ts"() {
40
+ "use strict";
41
+ init_secure_files();
42
+ EXE_AI_DIR = resolveDataDir();
43
+ DB_PATH = path.join(EXE_AI_DIR, "memories.db");
44
+ MODELS_DIR = path.join(EXE_AI_DIR, "models");
45
+ CONFIG_PATH = path.join(EXE_AI_DIR, "config.json");
46
+ LEGACY_LANCE_PATH = path.join(EXE_AI_DIR, "local.lance");
47
+ CURRENT_CONFIG_VERSION = 1;
48
+ DEFAULT_CONFIG = {
49
+ config_version: CURRENT_CONFIG_VERSION,
50
+ dbPath: DB_PATH,
51
+ modelFile: "jina-embeddings-v5-small-q4_k_m.gguf",
52
+ embeddingDim: 1024,
53
+ batchSize: 20,
54
+ flushIntervalMs: 1e4,
55
+ autoIngestion: true,
56
+ autoRetrieval: true,
57
+ searchMode: "hybrid",
58
+ hookSearchMode: "hybrid",
59
+ fileGrepEnabled: true,
60
+ splashEffect: true,
61
+ consolidationEnabled: true,
62
+ consolidationIntervalMs: 6 * 60 * 60 * 1e3,
63
+ consolidationModel: "claude-haiku-4-5-20251001",
64
+ consolidationMaxCallsPerRun: 20,
65
+ selfQueryRouter: true,
66
+ selfQueryModel: "claude-haiku-4-5-20251001",
67
+ rerankerEnabled: true,
68
+ scalingRoadmap: {
69
+ rerankerAutoTrigger: {
70
+ enabled: true,
71
+ broadQueryMinCardinality: 5e4,
72
+ fetchTopK: 200,
73
+ returnTopK: 20
74
+ }
75
+ },
76
+ graphRagEnabled: true,
77
+ wikiEnabled: false,
78
+ wikiUrl: "",
79
+ wikiApiKey: "",
80
+ wikiSyncIntervalMs: 30 * 60 * 1e3,
81
+ wikiWorkspaceMapping: {},
82
+ wikiAutoUpdate: true,
83
+ wikiAutoUpdateThreshold: 0.5,
84
+ wikiAutoUpdateCreateNew: true,
85
+ skillLearning: true,
86
+ skillThreshold: 3,
87
+ skillModel: "claude-haiku-4-5-20251001",
88
+ exeHeartbeat: {
89
+ enabled: true,
90
+ intervalSeconds: 60,
91
+ staleInProgressThresholdHours: 2
92
+ },
93
+ sessionLifecycle: {
94
+ idleKillEnabled: true,
95
+ idleKillTicksRequired: 3,
96
+ idleKillIntercomAckWindowMs: 1e4,
97
+ maxAutoInstances: 10
98
+ },
99
+ autoUpdate: {
100
+ checkOnBoot: true,
101
+ autoInstall: false,
102
+ checkIntervalMs: 24 * 60 * 60 * 1e3
103
+ },
104
+ orchestration: {
105
+ phase: "phase_1_coo",
106
+ phaseSetBy: "default"
107
+ }
108
+ };
109
+ }
110
+ });
2
111
 
3
112
  // src/bin/postgres-agentic-reflection-backfill.ts
4
113
  import { Client } from "pg";
@@ -89,6 +198,157 @@ function buildReflectionCheckpoint(events) {
89
198
  };
90
199
  }
91
200
 
201
+ // src/lib/background-jobs.ts
202
+ init_config();
203
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync, unlinkSync } from "fs";
204
+ import { execFileSync } from "child_process";
205
+ import os2 from "os";
206
+ import path2 from "path";
207
+ var JOB_DIR = path2.join(EXE_AI_DIR, "jobs");
208
+ var JOBS_FILE = path2.join(JOB_DIR, "jobs.json");
209
+ var LOCK_DIR = path2.join(JOB_DIR, "locks");
210
+ var DEFAULT_LOCK_TTL_MS = 6 * 60 * 60 * 1e3;
211
+ var MAX_HISTORY = 200;
212
+ function ensureDirs() {
213
+ mkdirSync2(LOCK_DIR, { recursive: true });
214
+ }
215
+ function now() {
216
+ return (/* @__PURE__ */ new Date()).toISOString();
217
+ }
218
+ function isAlive(pid) {
219
+ if (!pid || pid <= 0) return false;
220
+ try {
221
+ process.kill(pid, 0);
222
+ return true;
223
+ } catch {
224
+ return false;
225
+ }
226
+ }
227
+ function readJobsRaw() {
228
+ ensureDirs();
229
+ if (!existsSync3(JOBS_FILE)) return [];
230
+ try {
231
+ const parsed = JSON.parse(readFileSync2(JOBS_FILE, "utf8"));
232
+ return Array.isArray(parsed) ? parsed : [];
233
+ } catch {
234
+ return [];
235
+ }
236
+ }
237
+ function writeJobsRaw(jobs) {
238
+ ensureDirs();
239
+ const running = jobs.filter((j) => j.status === "running");
240
+ const rest = jobs.filter((j) => j.status !== "running").slice(-MAX_HISTORY);
241
+ writeFileSync(JOBS_FILE, JSON.stringify([...rest, ...running], null, 2) + "\n");
242
+ }
243
+ function lockPath(type) {
244
+ return path2.join(LOCK_DIR, `${type.replace(/[^a-zA-Z0-9_.-]/g, "_")}.lock`);
245
+ }
246
+ function acquireJobLock(type, ttlMs = DEFAULT_LOCK_TTL_MS) {
247
+ ensureDirs();
248
+ const file = lockPath(type);
249
+ if (existsSync3(file)) {
250
+ try {
251
+ const lock = JSON.parse(readFileSync2(file, "utf8"));
252
+ const age = Date.now() - Date.parse(lock.updatedAt ?? "");
253
+ if (lock.pid && isAlive(lock.pid) && Number.isFinite(age) && age < ttlMs) return false;
254
+ } catch {
255
+ }
256
+ try {
257
+ unlinkSync(file);
258
+ } catch {
259
+ }
260
+ }
261
+ try {
262
+ writeFileSync(file, JSON.stringify({ pid: process.pid, updatedAt: now() }, null, 2) + "\n", { flag: "wx" });
263
+ return true;
264
+ } catch {
265
+ return false;
266
+ }
267
+ }
268
+ function releaseJobLock(type) {
269
+ const file = lockPath(type);
270
+ try {
271
+ if (!existsSync3(file)) return;
272
+ const lock = JSON.parse(readFileSync2(file, "utf8"));
273
+ if (lock.pid === process.pid || !lock.pid || !isAlive(lock.pid)) unlinkSync(file);
274
+ } catch {
275
+ try {
276
+ unlinkSync(file);
277
+ } catch {
278
+ }
279
+ }
280
+ }
281
+ function startManagedJob(options) {
282
+ const lowPriority = options.lowPriority ?? true;
283
+ if (!acquireJobLock(options.type, options.lockTtlMs)) return null;
284
+ if (lowPriority) {
285
+ try {
286
+ os2.setPriority(process.pid, 10);
287
+ } catch {
288
+ }
289
+ }
290
+ const id = `${options.type}-${Date.now()}-${process.pid}`.replace(/[^a-zA-Z0-9_.-]/g, "_");
291
+ const record = {
292
+ id,
293
+ type: options.type,
294
+ name: options.name,
295
+ pid: process.pid,
296
+ command: options.command ?? process.argv.join(" "),
297
+ cwd: process.cwd(),
298
+ status: "running",
299
+ startedAt: now(),
300
+ updatedAt: now(),
301
+ lastHeartbeatAt: now(),
302
+ cancelCommand: `exe-os jobs cancel ${id}`,
303
+ lowPriority
304
+ };
305
+ const upsert = (patch) => {
306
+ const jobs = readJobsRaw().filter((j) => j.id !== id);
307
+ Object.assign(record, patch, { updatedAt: now() });
308
+ writeJobsRaw([...jobs, record]);
309
+ const file = lockPath(options.type);
310
+ try {
311
+ writeFileSync(file, JSON.stringify({ pid: process.pid, jobId: id, updatedAt: record.updatedAt }, null, 2) + "\n");
312
+ } catch {
313
+ }
314
+ };
315
+ upsert({});
316
+ const timer = setInterval(() => upsert({ lastHeartbeatAt: now() }), 3e4);
317
+ timer.unref?.();
318
+ const cleanup = (status, error) => {
319
+ clearInterval(timer);
320
+ upsert({ status, error, lastHeartbeatAt: now() });
321
+ releaseJobLock(options.type);
322
+ };
323
+ process.once("SIGTERM", () => {
324
+ cleanup("cancelled");
325
+ process.exit(0);
326
+ });
327
+ process.once("SIGINT", () => {
328
+ cleanup("cancelled");
329
+ process.exit(130);
330
+ });
331
+ process.once("exit", () => releaseJobLock(options.type));
332
+ return {
333
+ id,
334
+ update(progress) {
335
+ upsert({ progressCurrent: progress.current, progressTotal: progress.total, progressLabel: progress.label, lastHeartbeatAt: now() });
336
+ },
337
+ complete() {
338
+ cleanup("completed");
339
+ },
340
+ fail(err) {
341
+ cleanup("failed", err instanceof Error ? err.message : String(err));
342
+ },
343
+ cancel() {
344
+ cleanup("cancelled");
345
+ }
346
+ };
347
+ }
348
+ async function politeBatchPause(ms = 250) {
349
+ await new Promise((resolve) => setTimeout(resolve, ms));
350
+ }
351
+
92
352
  // src/bin/postgres-agentic-reflection-backfill.ts
93
353
  function arg(name) {
94
354
  const i = process.argv.indexOf(name);
@@ -117,6 +377,11 @@ async function ensureSchema(client) {
117
377
  await client.query(`CREATE INDEX IF NOT EXISTS idx_agent_reflection_session_time ON memory.agent_reflection_checkpoints (session_id, window_end_at)`);
118
378
  }
119
379
  async function main() {
380
+ const job = startManagedJob({ type: "postgres-agentic-reflection-backfill", name: "Postgres reflection checkpoint backfill", lowPriority: true });
381
+ if (!job) {
382
+ process.stderr.write("[postgres-agentic-reflection-backfill] Another Postgres reflection backfill is already running.\n");
383
+ return;
384
+ }
120
385
  const url = process.env.DATABASE_URL || process.env.EXED_DATABASE_URL;
121
386
  if (!url) throw new Error("DATABASE_URL or EXED_DATABASE_URL is required");
122
387
  const limit = Number(arg("--limit") ?? "20000");
@@ -172,10 +437,15 @@ async function main() {
172
437
  checkpoint.confidence
173
438
  ]);
174
439
  inserted++;
440
+ if (inserted % 250 === 0) {
441
+ job.update({ current: inserted, label: `Inserted/updated ${inserted} checkpoints` });
442
+ await politeBatchPause(500);
443
+ }
175
444
  }
176
445
  }
177
446
  process.stderr.write(`[postgres-agentic-reflection-backfill] Inserted/updated ${inserted} reflection checkpoints.
178
447
  `);
448
+ job.complete();
179
449
  } finally {
180
450
  await client.end();
181
451
  }