@askexenow/exe-os 0.8.33 → 0.8.37

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.
Files changed (89) hide show
  1. package/dist/bin/backfill-conversations.js +341 -349
  2. package/dist/bin/backfill-responses.js +81 -13
  3. package/dist/bin/backfill-vectors.js +72 -12
  4. package/dist/bin/cleanup-stale-review-tasks.js +63 -3
  5. package/dist/bin/cli.js +1737 -1117
  6. package/dist/bin/exe-assign.js +89 -19
  7. package/dist/bin/exe-boot.js +951 -101
  8. package/dist/bin/exe-call.js +61 -2
  9. package/dist/bin/exe-dispatch.js +61 -13
  10. package/dist/bin/exe-doctor.js +63 -3
  11. package/dist/bin/exe-export-behaviors.js +71 -3
  12. package/dist/bin/exe-forget.js +69 -4
  13. package/dist/bin/exe-gateway.js +178 -45
  14. package/dist/bin/exe-heartbeat.js +79 -14
  15. package/dist/bin/exe-kill.js +71 -3
  16. package/dist/bin/exe-launch-agent.js +148 -14
  17. package/dist/bin/exe-link.js +1437 -0
  18. package/dist/bin/exe-new-employee.js +98 -13
  19. package/dist/bin/exe-pending-messages.js +74 -8
  20. package/dist/bin/exe-pending-notifications.js +63 -3
  21. package/dist/bin/exe-pending-reviews.js +77 -11
  22. package/dist/bin/exe-rename.js +1287 -0
  23. package/dist/bin/exe-review.js +73 -5
  24. package/dist/bin/exe-search.js +88 -14
  25. package/dist/bin/exe-session-cleanup.js +102 -28
  26. package/dist/bin/exe-status.js +64 -4
  27. package/dist/bin/exe-team.js +64 -4
  28. package/dist/bin/git-sweep.js +80 -5
  29. package/dist/bin/graph-backfill.js +71 -3
  30. package/dist/bin/graph-export.js +71 -3
  31. package/dist/bin/install.js +38 -8
  32. package/dist/bin/scan-tasks.js +80 -5
  33. package/dist/bin/setup.js +128 -10
  34. package/dist/bin/shard-migrate.js +71 -3
  35. package/dist/bin/wiki-sync.js +71 -3
  36. package/dist/gateway/index.js +179 -46
  37. package/dist/hooks/bug-report-worker.js +254 -28
  38. package/dist/hooks/commit-complete.js +80 -5
  39. package/dist/hooks/error-recall.js +89 -15
  40. package/dist/hooks/exe-heartbeat-hook.js +1 -1
  41. package/dist/hooks/ingest-worker.js +185 -51
  42. package/dist/hooks/ingest.js +1 -1
  43. package/dist/hooks/instructions-loaded.js +81 -6
  44. package/dist/hooks/notification.js +81 -6
  45. package/dist/hooks/post-compact.js +81 -6
  46. package/dist/hooks/pre-compact.js +81 -6
  47. package/dist/hooks/pre-tool-use.js +423 -196
  48. package/dist/hooks/prompt-ingest-worker.js +91 -23
  49. package/dist/hooks/prompt-submit.js +159 -45
  50. package/dist/hooks/response-ingest-worker.js +96 -23
  51. package/dist/hooks/session-end.js +81 -6
  52. package/dist/hooks/session-start.js +89 -15
  53. package/dist/hooks/stop.js +81 -6
  54. package/dist/hooks/subagent-stop.js +81 -6
  55. package/dist/hooks/summary-worker.js +807 -55
  56. package/dist/index.js +198 -60
  57. package/dist/lib/cloud-sync.js +703 -18
  58. package/dist/lib/consolidation.js +4 -4
  59. package/dist/lib/database.js +64 -2
  60. package/dist/lib/device-registry.js +70 -3
  61. package/dist/lib/employee-templates.js +26 -0
  62. package/dist/lib/employees.js +34 -1
  63. package/dist/lib/exe-daemon.js +207 -74
  64. package/dist/lib/hybrid-search.js +88 -14
  65. package/dist/lib/identity-templates.js +51 -0
  66. package/dist/lib/identity.js +3 -3
  67. package/dist/lib/messaging.js +65 -17
  68. package/dist/lib/reminders.js +3 -3
  69. package/dist/lib/schedules.js +63 -3
  70. package/dist/lib/skill-learning.js +3 -3
  71. package/dist/lib/status-brief.js +63 -5
  72. package/dist/lib/store.js +73 -4
  73. package/dist/lib/task-router.js +4 -2
  74. package/dist/lib/tasks.js +95 -28
  75. package/dist/lib/tmux-routing.js +92 -23
  76. package/dist/mcp/server.js +800 -74
  77. package/dist/mcp/tools/complete-reminder.js +3 -3
  78. package/dist/mcp/tools/create-reminder.js +3 -3
  79. package/dist/mcp/tools/create-task.js +198 -31
  80. package/dist/mcp/tools/deactivate-behavior.js +4 -4
  81. package/dist/mcp/tools/list-reminders.js +3 -3
  82. package/dist/mcp/tools/list-tasks.js +19 -9
  83. package/dist/mcp/tools/send-message.js +69 -21
  84. package/dist/mcp/tools/update-task.js +28 -18
  85. package/dist/runtime/index.js +166 -28
  86. package/dist/tui/App.js +193 -40
  87. package/package.json +7 -3
  88. package/src/commands/exe/afk.md +116 -0
  89. package/src/commands/exe/rename.md +12 -0
@@ -262,7 +262,7 @@ function listShards() {
262
262
  }
263
263
  async function ensureShardSchema(client) {
264
264
  await client.execute("PRAGMA journal_mode = WAL");
265
- await client.execute("PRAGMA busy_timeout = 5000");
265
+ await client.execute("PRAGMA busy_timeout = 30000");
266
266
  try {
267
267
  await client.execute("PRAGMA libsql_vector_search_ef = 128");
268
268
  } catch {
@@ -446,124 +446,79 @@ var init_shard_manager = __esm({
446
446
  }
447
447
  });
448
448
 
449
- // src/lib/employees.ts
450
- var employees_exports = {};
451
- __export(employees_exports, {
452
- EMPLOYEES_PATH: () => EMPLOYEES_PATH,
453
- addEmployee: () => addEmployee,
454
- getEmployee: () => getEmployee,
455
- loadEmployees: () => loadEmployees,
456
- registerBinSymlinks: () => registerBinSymlinks,
457
- saveEmployees: () => saveEmployees,
458
- validateEmployeeName: () => validateEmployeeName
459
- });
460
- import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
461
- import { existsSync as existsSync5, symlinkSync, readlinkSync } from "fs";
462
- import { execSync } from "child_process";
463
- import path5 from "path";
464
- function validateEmployeeName(name) {
465
- if (!name) {
466
- return { valid: false, error: "Name is required" };
467
- }
468
- if (name.length > 32) {
469
- return { valid: false, error: "Name must be 32 characters or fewer" };
470
- }
471
- if (!/^[a-z][a-z0-9]*$/.test(name)) {
472
- return {
473
- valid: false,
474
- error: "Name must start with a letter and contain only lowercase alphanumeric characters"
475
- };
476
- }
477
- return { valid: true };
478
- }
479
- async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
480
- if (!existsSync5(employeesPath)) {
481
- return [];
482
- }
483
- const raw = await readFile3(employeesPath, "utf-8");
484
- try {
485
- return JSON.parse(raw);
486
- } catch {
487
- return [];
488
- }
489
- }
490
- async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
491
- await mkdir3(path5.dirname(employeesPath), { recursive: true });
492
- await writeFile3(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
493
- }
494
- function getEmployee(employees, name) {
495
- return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
496
- }
497
- function addEmployee(employees, employee) {
498
- const normalized = { ...employee, name: employee.name.toLowerCase() };
499
- if (employees.some((e) => e.name.toLowerCase() === normalized.name)) {
500
- throw new Error(`Employee '${normalized.name}' already exists`);
501
- }
502
- return [...employees, normalized];
503
- }
504
- function registerBinSymlinks(name) {
505
- const created = [];
506
- const skipped = [];
507
- const errors = [];
508
- let exeBinPath;
509
- try {
510
- exeBinPath = execSync("which exe", { encoding: "utf-8" }).trim();
511
- } catch {
512
- errors.push("Could not find 'exe' in PATH");
513
- return { created, skipped, errors };
514
- }
515
- const binDir = path5.dirname(exeBinPath);
516
- let target;
517
- try {
518
- target = readlinkSync(exeBinPath);
519
- } catch {
520
- errors.push("Could not read 'exe' symlink");
521
- return { created, skipped, errors };
522
- }
523
- for (const suffix of ["", "-opencode"]) {
524
- const linkName = `${name}${suffix}`;
525
- const linkPath = path5.join(binDir, linkName);
526
- if (existsSync5(linkPath)) {
527
- skipped.push(linkName);
528
- continue;
529
- }
530
- try {
531
- symlinkSync(target, linkPath);
532
- created.push(linkName);
533
- } catch (err) {
534
- errors.push(`${linkName}: ${err instanceof Error ? err.message : String(err)}`);
535
- }
536
- }
537
- return { created, skipped, errors };
538
- }
539
- var EMPLOYEES_PATH;
540
- var init_employees = __esm({
541
- "src/lib/employees.ts"() {
542
- "use strict";
543
- init_config();
544
- EMPLOYEES_PATH = path5.join(EXE_AI_DIR, "exe-employees.json");
545
- }
546
- });
547
-
548
449
  // src/bin/backfill-conversations.ts
549
450
  import crypto2 from "crypto";
550
451
  import { createReadStream } from "fs";
551
452
  import { readdir, stat } from "fs/promises";
552
- import path6 from "path";
453
+ import path5 from "path";
553
454
  import { createInterface } from "readline";
554
455
  import { homedir } from "os";
456
+ import { parseArgs } from "util";
555
457
 
556
458
  // src/types/memory.ts
557
459
  var EMBEDDING_DIM = 1024;
558
460
 
559
461
  // src/lib/database.ts
560
462
  import { createClient } from "@libsql/client";
463
+
464
+ // src/lib/db-retry.ts
465
+ var MAX_RETRIES = 3;
466
+ var BASE_DELAY_MS = 200;
467
+ var MAX_JITTER_MS = 300;
468
+ function isBusyError(err) {
469
+ if (err instanceof Error) {
470
+ const msg = err.message.toLowerCase();
471
+ return msg.includes("sqlite_busy") || msg.includes("database is locked");
472
+ }
473
+ return false;
474
+ }
475
+ function delay(ms) {
476
+ return new Promise((resolve) => setTimeout(resolve, ms));
477
+ }
478
+ async function retryOnBusy(fn, label) {
479
+ let lastError;
480
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
481
+ try {
482
+ return await fn();
483
+ } catch (err) {
484
+ lastError = err;
485
+ if (!isBusyError(err) || attempt === MAX_RETRIES) {
486
+ throw err;
487
+ }
488
+ const backoff = BASE_DELAY_MS * Math.pow(2, attempt);
489
+ const jitter = Math.floor(Math.random() * MAX_JITTER_MS);
490
+ process.stderr.write(
491
+ `[exe-os] SQLITE_BUSY ${label} retry ${attempt + 1}/${MAX_RETRIES} \u2014 waiting ${backoff + jitter}ms
492
+ `
493
+ );
494
+ await delay(backoff + jitter);
495
+ }
496
+ }
497
+ throw lastError;
498
+ }
499
+ function wrapWithRetry(client) {
500
+ return new Proxy(client, {
501
+ get(target, prop, receiver) {
502
+ if (prop === "execute") {
503
+ return (sql) => retryOnBusy(() => target.execute(sql), "execute");
504
+ }
505
+ if (prop === "batch") {
506
+ return (stmts) => retryOnBusy(() => target.batch(stmts), "batch");
507
+ }
508
+ return Reflect.get(target, prop, receiver);
509
+ }
510
+ });
511
+ }
512
+
513
+ // src/lib/database.ts
561
514
  var _client = null;
515
+ var _resilientClient = null;
562
516
  var initTurso = initDatabase;
563
517
  async function initDatabase(config) {
564
518
  if (_client) {
565
519
  _client.close();
566
520
  _client = null;
521
+ _resilientClient = null;
567
522
  }
568
523
  const opts = {
569
524
  url: `file:${config.dbPath}`
@@ -572,17 +527,24 @@ async function initDatabase(config) {
572
527
  opts.encryptionKey = config.encryptionKey;
573
528
  }
574
529
  _client = createClient(opts);
530
+ _resilientClient = wrapWithRetry(_client);
575
531
  }
576
532
  function getClient() {
533
+ if (!_resilientClient) {
534
+ throw new Error("Database client not initialized. Call initDatabase() first.");
535
+ }
536
+ return _resilientClient;
537
+ }
538
+ function getRawClient() {
577
539
  if (!_client) {
578
540
  throw new Error("Database client not initialized. Call initDatabase() first.");
579
541
  }
580
542
  return _client;
581
543
  }
582
544
  async function ensureSchema() {
583
- const client = getClient();
545
+ const client = getRawClient();
584
546
  await client.execute("PRAGMA journal_mode = WAL");
585
- await client.execute("PRAGMA busy_timeout = 5000");
547
+ await client.execute("PRAGMA busy_timeout = 30000");
586
548
  try {
587
549
  await client.execute("PRAGMA libsql_vector_search_ef = 128");
588
550
  } catch {
@@ -1483,7 +1445,8 @@ async function writeMemory(record) {
1483
1445
  has_error: record.has_error ? 1 : 0,
1484
1446
  raw_text: record.raw_text,
1485
1447
  vector: record.vector,
1486
- version: _nextVersion++,
1448
+ version: 0,
1449
+ // Placeholder — assigned atomically at flush time
1487
1450
  task_id: record.task_id ?? null,
1488
1451
  importance: record.importance ?? 5,
1489
1452
  status: record.status ?? "active",
@@ -1517,6 +1480,13 @@ async function flushBatch() {
1517
1480
  _flushing = true;
1518
1481
  try {
1519
1482
  const batch = _pendingRecords.slice(0);
1483
+ const client = getClient();
1484
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1485
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1486
+ for (const row of batch) {
1487
+ row.version = baseVersion++;
1488
+ }
1489
+ _nextVersion = baseVersion;
1520
1490
  const buildStmt = (row) => {
1521
1491
  const hasVector = row.vector !== null;
1522
1492
  const taskId = row.task_id ?? null;
@@ -1831,11 +1801,11 @@ async function connectEmbedDaemon() {
1831
1801
  }
1832
1802
  }
1833
1803
  const start = Date.now();
1834
- let delay = 100;
1804
+ let delay2 = 100;
1835
1805
  while (Date.now() - start < CONNECT_TIMEOUT_MS) {
1836
- await new Promise((r) => setTimeout(r, delay));
1806
+ await new Promise((r) => setTimeout(r, delay2));
1837
1807
  if (await connectToSocket()) return true;
1838
- delay = Math.min(delay * 2, 3e3);
1808
+ delay2 = Math.min(delay2 * 2, 3e3);
1839
1809
  }
1840
1810
  return false;
1841
1811
  }
@@ -1927,11 +1897,11 @@ async function embedViaClient(text, priority = "high") {
1927
1897
  `);
1928
1898
  killAndRespawnDaemon();
1929
1899
  const start = Date.now();
1930
- let delay = 200;
1900
+ let delay2 = 200;
1931
1901
  while (Date.now() - start < CONNECT_TIMEOUT_MS) {
1932
- await new Promise((r) => setTimeout(r, delay));
1902
+ await new Promise((r) => setTimeout(r, delay2));
1933
1903
  if (await connectToSocket()) break;
1934
- delay = Math.min(delay * 2, 3e3);
1904
+ delay2 = Math.min(delay2 * 2, 3e3);
1935
1905
  }
1936
1906
  if (!_connected) return null;
1937
1907
  }
@@ -1943,11 +1913,11 @@ async function embedViaClient(text, priority = "high") {
1943
1913
  `);
1944
1914
  killAndRespawnDaemon();
1945
1915
  const start = Date.now();
1946
- let delay = 200;
1916
+ let delay2 = 200;
1947
1917
  while (Date.now() - start < CONNECT_TIMEOUT_MS) {
1948
- await new Promise((r) => setTimeout(r, delay));
1918
+ await new Promise((r) => setTimeout(r, delay2));
1949
1919
  if (await connectToSocket()) break;
1950
- delay = Math.min(delay * 2, 3e3);
1920
+ delay2 = Math.min(delay2 * 2, 3e3);
1951
1921
  }
1952
1922
  if (!_connected) return null;
1953
1923
  const retry = await sendRequest([text], priority);
@@ -1973,9 +1943,11 @@ function isMainModule(importMetaUrl) {
1973
1943
  }
1974
1944
 
1975
1945
  // src/bin/backfill-conversations.ts
1976
- process.env.EXE_EMBED_PRIORITY = "low";
1977
- async function findJsonlFiles(cutoffMs) {
1978
- const projectsDir = path6.join(homedir(), ".claude", "projects");
1946
+ var TOOL_NAME = "backfill-conversation";
1947
+ var MIN_MESSAGES = 3;
1948
+ var MAX_SUMMARY_LENGTH = 4e3;
1949
+ async function findJsonlFiles(sinceDate, projectFilter) {
1950
+ const projectsDir = path5.join(homedir(), ".claude", "projects");
1979
1951
  const files = [];
1980
1952
  async function walk(dir) {
1981
1953
  let entries;
@@ -1985,61 +1957,67 @@ async function findJsonlFiles(cutoffMs) {
1985
1957
  return;
1986
1958
  }
1987
1959
  for (const entry of entries) {
1988
- const full = path6.join(dir, entry.name);
1960
+ const full = path5.join(dir, entry.name);
1989
1961
  if (entry.isDirectory()) {
1962
+ if (entry.name === "subagents" || entry.name === "tool-results") continue;
1990
1963
  await walk(full);
1991
1964
  } else if (entry.name.endsWith(".jsonl")) {
1992
1965
  try {
1993
1966
  const s = await stat(full);
1994
- if (s.mtimeMs >= cutoffMs) files.push(full);
1967
+ if (sinceDate && s.mtimeMs < sinceDate.getTime()) continue;
1968
+ files.push(full);
1995
1969
  } catch {
1996
1970
  }
1997
1971
  }
1998
1972
  }
1999
1973
  }
2000
- await walk(projectsDir);
1974
+ if (projectFilter) {
1975
+ let projectDirs;
1976
+ try {
1977
+ projectDirs = await readdir(projectsDir, { withFileTypes: true });
1978
+ } catch {
1979
+ return files;
1980
+ }
1981
+ for (const entry of projectDirs) {
1982
+ if (!entry.isDirectory()) continue;
1983
+ const decoded = decodeProjectDir(entry.name);
1984
+ if (decoded.toLowerCase().includes(projectFilter.toLowerCase())) {
1985
+ await walk(path5.join(projectsDir, entry.name));
1986
+ }
1987
+ }
1988
+ } else {
1989
+ await walk(projectsDir);
1990
+ }
2001
1991
  return files;
2002
1992
  }
2003
- function projectNameFromPath(filePath) {
2004
- const projectsDir = path6.join(homedir(), ".claude", "projects");
2005
- const relative = path6.relative(projectsDir, filePath);
2006
- const projectDir = relative.split(path6.sep)[0] ?? relative;
1993
+ function decodeProjectDir(dirName) {
2007
1994
  const homeEncoded = homedir().replaceAll("/", "-");
2008
- if (projectDir.startsWith(homeEncoded + "-")) {
2009
- return projectDir.slice(homeEncoded.length + 1);
1995
+ if (dirName.startsWith(homeEncoded + "-")) {
1996
+ return dirName.slice(homeEncoded.length + 1);
2010
1997
  }
2011
- if (projectDir === homeEncoded) return "home";
2012
- return projectDir;
1998
+ if (dirName === homeEncoded) return "home";
1999
+ return dirName;
2013
2000
  }
2014
- function extractAssistantText(content) {
2015
- if (typeof content === "string") return content;
2016
- const texts = [];
2017
- for (const block of content) {
2018
- if (block.type === "text" && block.text) {
2019
- texts.push(block.text);
2020
- }
2021
- }
2022
- return texts.join("\n");
2023
- }
2024
- function extractUserText(content) {
2025
- if (typeof content === "string") return content;
2026
- const texts = [];
2027
- for (const block of content) {
2028
- if (block.type === "text" && block.text) {
2029
- texts.push(block.text);
2030
- }
2031
- }
2032
- return texts.join("\n");
2001
+ function projectNameFromPath(filePath) {
2002
+ const projectsDir = path5.join(homedir(), ".claude", "projects");
2003
+ const relative = path5.relative(projectsDir, filePath);
2004
+ const projectDir = relative.split(path5.sep)[0] ?? "unknown";
2005
+ return decodeProjectDir(projectDir);
2033
2006
  }
2034
- async function extractConversationPairs(filePath, projectFilter) {
2035
- const fallbackProject = projectNameFromPath(filePath);
2036
- if (projectFilter && !fallbackProject.toLowerCase().includes(projectFilter.toLowerCase())) {
2037
- return [];
2038
- }
2039
- const pairs = [];
2040
- let fileCwd = "";
2041
- let fileSessionId = "";
2042
- let pendingUser = null;
2007
+ async function parseConversation(filePath) {
2008
+ const conv = {
2009
+ sessionId: path5.basename(filePath, ".jsonl"),
2010
+ projectName: projectNameFromPath(filePath),
2011
+ cwd: void 0,
2012
+ startTime: void 0,
2013
+ endTime: void 0,
2014
+ userMessages: [],
2015
+ toolCounts: {},
2016
+ filesTouched: /* @__PURE__ */ new Set(),
2017
+ errorCount: 0,
2018
+ totalMessages: 0,
2019
+ agentId: "default"
2020
+ };
2043
2021
  const rl = createInterface({
2044
2022
  input: createReadStream(filePath, { encoding: "utf8" }),
2045
2023
  crlfDelay: Infinity
@@ -2052,251 +2030,265 @@ async function extractConversationPairs(filePath, projectFilter) {
2052
2030
  } catch {
2053
2031
  continue;
2054
2032
  }
2055
- if (entry.cwd) fileCwd = entry.cwd;
2056
- if (entry.sessionId) fileSessionId = entry.sessionId;
2057
- if (entry.type === "user" && entry.message?.content) {
2058
- const text = extractUserText(entry.message.content);
2059
- if (text.trim()) {
2060
- pendingUser = {
2061
- text,
2062
- timestamp: entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
2063
- uuid: entry.uuid ?? ""
2064
- };
2033
+ if (entry.cwd && typeof entry.cwd === "string") {
2034
+ conv.cwd = entry.cwd;
2035
+ }
2036
+ const ts = entry.timestamp;
2037
+ if (ts) {
2038
+ if (!conv.startTime || ts < conv.startTime) conv.startTime = ts;
2039
+ if (!conv.endTime || ts > conv.endTime) conv.endTime = ts;
2040
+ }
2041
+ const entryType = entry.type;
2042
+ if (entryType === "user") {
2043
+ conv.totalMessages++;
2044
+ const message = entry.message;
2045
+ if (message?.content) {
2046
+ const text = extractUserText(message.content);
2047
+ if (text && text.length > 10) {
2048
+ conv.userMessages.push(text);
2049
+ }
2050
+ }
2051
+ } else if (entryType === "assistant") {
2052
+ conv.totalMessages++;
2053
+ const message = entry.message;
2054
+ if (message?.content && Array.isArray(message.content)) {
2055
+ for (const block of message.content) {
2056
+ if (typeof block !== "object" || block === null) continue;
2057
+ const b = block;
2058
+ if (b.type === "tool_use") {
2059
+ const toolName = b.name;
2060
+ conv.toolCounts[toolName] = (conv.toolCounts[toolName] || 0) + 1;
2061
+ const input = b.input;
2062
+ if (input?.file_path && typeof input.file_path === "string") {
2063
+ if (toolName === "Write" || toolName === "Edit") {
2064
+ conv.filesTouched.add(input.file_path);
2065
+ }
2066
+ }
2067
+ }
2068
+ }
2065
2069
  }
2066
- } else if (entry.type === "assistant" && entry.message?.content) {
2067
- const assistantText = extractAssistantText(entry.message.content);
2068
- if (!assistantText.trim()) continue;
2069
- const resolvedProject = fileCwd ? path6.basename(fileCwd) : fallbackProject;
2070
- const userText = pendingUser?.text ?? "";
2071
- const timestamp = entry.timestamp ?? pendingUser?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
2072
- pairs.push({
2073
- userText,
2074
- assistantText,
2075
- timestamp,
2076
- sessionId: fileSessionId || path6.basename(filePath, ".jsonl"),
2077
- project: resolvedProject,
2078
- cwd: fileCwd || fallbackProject
2079
- });
2080
- pendingUser = null;
2081
2070
  }
2082
2071
  }
2083
- return pairs;
2072
+ if (conv.cwd) {
2073
+ conv.projectName = path5.basename(conv.cwd);
2074
+ const worktreeMatch = conv.cwd.match(/\.worktrees\/([^/]+)/);
2075
+ if (worktreeMatch?.[1]) {
2076
+ conv.agentId = worktreeMatch[1];
2077
+ }
2078
+ }
2079
+ return conv;
2084
2080
  }
2085
- function hashPair(userText, assistantText) {
2086
- return crypto2.createHash("sha256").update(userText.slice(0, 500) + "||" + assistantText.slice(0, 500)).digest("hex");
2081
+ function extractUserText(content) {
2082
+ if (typeof content === "string") return content;
2083
+ if (Array.isArray(content)) {
2084
+ const parts = [];
2085
+ for (const block of content) {
2086
+ if (typeof block === "string") {
2087
+ parts.push(block);
2088
+ } else if (typeof block === "object" && block !== null) {
2089
+ const b = block;
2090
+ if (b.type === "text" && typeof b.text === "string") {
2091
+ parts.push(b.text);
2092
+ }
2093
+ }
2094
+ }
2095
+ return parts.join("\n");
2096
+ }
2097
+ return "";
2087
2098
  }
2088
- async function loadExistingHashes(cutoffIso) {
2099
+ function buildSummary(conv) {
2100
+ const parts = [];
2101
+ parts.push(`Session: ${conv.sessionId}`);
2102
+ parts.push(`Project: ${conv.projectName}`);
2103
+ if (conv.startTime) {
2104
+ parts.push(`Time: ${conv.startTime}${conv.endTime ? ` \u2192 ${conv.endTime}` : ""}`);
2105
+ }
2106
+ parts.push(`Messages: ${conv.totalMessages}`);
2107
+ if (conv.agentId !== "default") {
2108
+ parts.push(`Agent: ${conv.agentId}`);
2109
+ }
2110
+ parts.push("");
2111
+ if (conv.userMessages.length > 0) {
2112
+ parts.push("## What was asked");
2113
+ const prompts = conv.userMessages.slice(0, 5);
2114
+ for (const prompt of prompts) {
2115
+ const truncated = prompt.length > 300 ? prompt.slice(0, 300) + "..." : prompt;
2116
+ const cleaned = truncated.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
2117
+ if (cleaned) parts.push(`- ${cleaned}`);
2118
+ }
2119
+ if (conv.userMessages.length > 5) {
2120
+ parts.push(`- ... and ${conv.userMessages.length - 5} more prompts`);
2121
+ }
2122
+ parts.push("");
2123
+ }
2124
+ const toolEntries = Object.entries(conv.toolCounts).sort((a, b) => b[1] - a[1]);
2125
+ if (toolEntries.length > 0) {
2126
+ parts.push("## Tools used");
2127
+ for (const [tool, count] of toolEntries) {
2128
+ parts.push(`- ${tool}: ${count}`);
2129
+ }
2130
+ parts.push("");
2131
+ }
2132
+ if (conv.filesTouched.size > 0) {
2133
+ parts.push("## Files modified");
2134
+ const fileList = [...conv.filesTouched].sort();
2135
+ const shown = fileList.slice(0, 20);
2136
+ for (const f of shown) {
2137
+ parts.push(`- ${f}`);
2138
+ }
2139
+ if (fileList.length > 20) {
2140
+ parts.push(`- ... and ${fileList.length - 20} more files`);
2141
+ }
2142
+ parts.push("");
2143
+ }
2144
+ if (conv.errorCount > 0) {
2145
+ parts.push(`Errors: ${conv.errorCount}`);
2146
+ }
2147
+ let summary = parts.join("\n");
2148
+ if (summary.length > MAX_SUMMARY_LENGTH) {
2149
+ summary = summary.slice(0, MAX_SUMMARY_LENGTH);
2150
+ }
2151
+ return summary;
2152
+ }
2153
+ async function loadExistingSourcePaths() {
2089
2154
  const client = getClient();
2090
- const hashes = /* @__PURE__ */ new Set();
2155
+ const paths = /* @__PURE__ */ new Set();
2091
2156
  let offset = 0;
2092
2157
  const batchSize = 500;
2093
2158
  while (true) {
2094
2159
  const result = await client.execute({
2095
- sql: `SELECT content_text, agent_response FROM conversations
2096
- WHERE platform = 'claude-code' AND timestamp > ?
2160
+ sql: `SELECT source_path FROM memories
2161
+ WHERE tool_name = ? AND source_path IS NOT NULL
2097
2162
  ORDER BY id LIMIT ? OFFSET ?`,
2098
- args: [cutoffIso, batchSize, offset]
2163
+ args: [TOOL_NAME, batchSize, offset]
2099
2164
  });
2100
2165
  if (result.rows.length === 0) break;
2101
2166
  for (const row of result.rows) {
2102
- hashes.add(
2103
- hashPair(
2104
- row.content_text ?? "",
2105
- row.agent_response ?? ""
2106
- )
2107
- );
2167
+ paths.add(row.source_path);
2108
2168
  }
2109
2169
  offset += batchSize;
2110
2170
  }
2111
- return hashes;
2112
- }
2113
- var MAX_CONTENT_LENGTH = 8e3;
2114
- var MIN_ASSISTANT_LENGTH = 50;
2115
- async function storePair(pair, daemonConnected, stats, agentId) {
2116
- const client = getClient();
2117
- const id = crypto2.randomUUID();
2118
- const userTrunc = pair.userText.length > MAX_CONTENT_LENGTH ? pair.userText.slice(0, MAX_CONTENT_LENGTH) : pair.userText;
2119
- const assistTrunc = pair.assistantText.length > MAX_CONTENT_LENGTH ? pair.assistantText.slice(0, MAX_CONTENT_LENGTH) : pair.assistantText;
2120
- await client.execute({
2121
- sql: `INSERT INTO conversations
2122
- (id, platform, external_id, sender_id, sender_name, sender_phone, sender_email,
2123
- recipient_id, channel_id, thread_id, reply_to_id,
2124
- content_text, content_media, agent_response, agent_name,
2125
- timestamp, ingested_at)
2126
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2127
- args: [
2128
- id,
2129
- "claude-code",
2130
- null,
2131
- "user",
2132
- "user",
2133
- null,
2134
- null,
2135
- null,
2136
- pair.project,
2137
- pair.sessionId,
2138
- null,
2139
- userTrunc,
2140
- null,
2141
- assistTrunc,
2142
- "claude",
2143
- pair.timestamp,
2144
- (/* @__PURE__ */ new Date()).toISOString()
2145
- ]
2146
- });
2147
- stats.conversationsStored++;
2148
- const rawText = [
2149
- `[claude-code] Conversation in ${pair.project}`,
2150
- userTrunc ? `User: ${userTrunc}` : null,
2151
- `Assistant: ${assistTrunc}`
2152
- ].filter(Boolean).join("\n");
2153
- let vector = null;
2154
- if (daemonConnected) {
2155
- try {
2156
- vector = await embedViaClient(rawText, "low");
2157
- if (!vector) stats.embedFailed++;
2158
- } catch {
2159
- stats.embedFailed++;
2160
- }
2161
- }
2162
- await writeMemory({
2163
- id: crypto2.randomUUID(),
2164
- agent_id: agentId,
2165
- agent_role: "coo",
2166
- session_id: pair.sessionId,
2167
- timestamp: pair.timestamp,
2168
- tool_name: "ConversationBackfill",
2169
- project_name: pair.project,
2170
- has_error: false,
2171
- raw_text: rawText,
2172
- vector,
2173
- importance: 3
2174
- });
2175
- stats.memoriesStored++;
2171
+ return paths;
2176
2172
  }
2177
2173
  async function backfillConversations(options) {
2178
2174
  const stats = {
2179
2175
  filesScanned: 0,
2180
2176
  conversationsStored: 0,
2181
- memoriesStored: 0,
2182
2177
  skippedDedup: 0,
2183
- skippedShort: 0,
2178
+ skippedTooShort: 0,
2184
2179
  embedFailed: 0
2185
2180
  };
2186
- const cutoffMs = Date.now() - options.days * 24 * 60 * 60 * 1e3;
2187
- const cutoffIso = new Date(cutoffMs).toISOString();
2188
- let cooAgentId = "exe";
2189
- try {
2190
- const { loadEmployees: loadEmployees2 } = await Promise.resolve().then(() => (init_employees(), employees_exports));
2191
- const employees = await loadEmployees2();
2192
- const coo = employees.find((e) => e.role === "COO");
2193
- if (coo) cooAgentId = coo.name.toLowerCase();
2194
- } catch {
2181
+ const sinceDate = options.since ? new Date(options.since) : void 0;
2182
+ if (sinceDate && isNaN(sinceDate.getTime())) {
2183
+ throw new Error(`Invalid --since date: ${options.since}`);
2195
2184
  }
2196
- process.stderr.write(`[backfill-conversations] Scanning last ${options.days} days (agent: ${cooAgentId})...
2197
- `);
2185
+ process.stderr.write("[backfill-conversations] Initializing store...\n");
2186
+ await initStore();
2187
+ let daemonConnected = false;
2198
2188
  if (!options.dryRun) {
2199
- process.stderr.write("[backfill-conversations] Initializing store...\n");
2200
- await initStore();
2201
- try {
2202
- const client = getClient();
2203
- const old = await client.execute({
2204
- sql: "SELECT COUNT(*) as cnt FROM memories WHERE agent_id = 'backfill' OR (tool_name = 'ConversationBackfill' AND agent_id != ?)",
2205
- args: [cooAgentId]
2206
- });
2207
- const count = Number(old.rows[0]?.cnt ?? 0);
2208
- if (count > 0) {
2209
- await client.execute({
2210
- sql: "UPDATE memories SET agent_id = ?, agent_role = 'coo' WHERE agent_id = 'backfill' OR (tool_name = 'ConversationBackfill' AND agent_id != ?)",
2211
- args: [cooAgentId, cooAgentId]
2212
- });
2213
- process.stderr.write(`[backfill-conversations] Migrated ${count} records \u2192 agent_id='${cooAgentId}'
2214
- `);
2215
- }
2216
- } catch {
2189
+ daemonConnected = await connectEmbedDaemon();
2190
+ if (!daemonConnected) {
2191
+ process.stderr.write(
2192
+ "[backfill-conversations] WARNING: Daemon unavailable \u2014 vectors will be NULL (backfill-vectors can fix later)\n"
2193
+ );
2217
2194
  }
2218
2195
  }
2219
- const daemonConnected = options.dryRun ? false : await connectEmbedDaemon();
2220
- if (!daemonConnected && !options.dryRun) {
2221
- process.stderr.write(
2222
- "[backfill-conversations] WARNING: Daemon unavailable \u2014 vectors will be NULL (backfill-vectors can fix later)\n"
2223
- );
2224
- }
2225
- let seenHashes = /* @__PURE__ */ new Set();
2226
- if (!options.dryRun) {
2227
- process.stderr.write("[backfill-conversations] Loading existing hashes for dedup...\n");
2228
- seenHashes = await loadExistingHashes(cutoffIso);
2229
- process.stderr.write(`[backfill-conversations] ${seenHashes.size} existing conversations loaded
2196
+ process.stderr.write("[backfill-conversations] Loading already-ingested conversations...\n");
2197
+ const existingPaths = options.dryRun ? /* @__PURE__ */ new Set() : await loadExistingSourcePaths();
2198
+ process.stderr.write(`[backfill-conversations] ${existingPaths.size} conversations already ingested
2230
2199
  `);
2231
- }
2232
- const files = await findJsonlFiles(cutoffMs);
2233
- process.stderr.write(`[backfill-conversations] Found ${files.length} JSONL files
2200
+ const files = await findJsonlFiles(sinceDate, options.project);
2201
+ process.stderr.write(`[backfill-conversations] Found ${files.length} JSONL files to process
2234
2202
  `);
2203
+ process.env.EXE_EMBED_PRIORITY = "low";
2235
2204
  for (const file of files) {
2236
- const pairs = await extractConversationPairs(file, options.projectFilter);
2237
2205
  stats.filesScanned++;
2238
- for (const pair of pairs) {
2239
- if (pair.assistantText.length < MIN_ASSISTANT_LENGTH) {
2240
- stats.skippedShort++;
2241
- continue;
2242
- }
2243
- const hash = hashPair(pair.userText, pair.assistantText);
2244
- if (seenHashes.has(hash)) {
2245
- stats.skippedDedup++;
2246
- continue;
2206
+ if (existingPaths.has(file)) {
2207
+ stats.skippedDedup++;
2208
+ continue;
2209
+ }
2210
+ const conv = await parseConversation(file);
2211
+ if (conv.totalMessages < MIN_MESSAGES) {
2212
+ stats.skippedTooShort++;
2213
+ continue;
2214
+ }
2215
+ const summary = buildSummary(conv);
2216
+ if (options.dryRun) {
2217
+ process.stdout.write(`
2218
+ \u2500\u2500\u2500 ${file} \u2500\u2500\u2500
2219
+ `);
2220
+ process.stdout.write(`Project: ${conv.projectName} | Messages: ${conv.totalMessages}`);
2221
+ process.stdout.write(` | Tools: ${Object.keys(conv.toolCounts).length}`);
2222
+ process.stdout.write(` | Files: ${conv.filesTouched.size}
2223
+ `);
2224
+ const firstPrompt = conv.userMessages[0];
2225
+ if (firstPrompt) {
2226
+ process.stdout.write(`First prompt: ${firstPrompt.slice(0, 120)}
2227
+ `);
2247
2228
  }
2248
- seenHashes.add(hash);
2249
- if (!options.dryRun) {
2250
- await storePair(pair, daemonConnected, stats, cooAgentId);
2251
- } else {
2252
- stats.conversationsStored++;
2253
- stats.memoriesStored++;
2229
+ stats.conversationsStored++;
2230
+ continue;
2231
+ }
2232
+ let vector = null;
2233
+ if (daemonConnected) {
2234
+ try {
2235
+ vector = await embedViaClient(summary, "low");
2236
+ if (!vector) stats.embedFailed++;
2237
+ } catch {
2238
+ stats.embedFailed++;
2254
2239
  }
2255
2240
  }
2256
- if (stats.filesScanned % 20 === 0) {
2241
+ await writeMemory({
2242
+ id: crypto2.randomUUID(),
2243
+ agent_id: conv.agentId,
2244
+ agent_role: conv.agentId === "exe" ? "COO" : "specialist",
2245
+ session_id: conv.sessionId,
2246
+ timestamp: conv.startTime ?? (/* @__PURE__ */ new Date()).toISOString(),
2247
+ tool_name: TOOL_NAME,
2248
+ project_name: conv.projectName,
2249
+ has_error: conv.errorCount > 0,
2250
+ raw_text: summary,
2251
+ vector,
2252
+ source_path: file,
2253
+ source_type: "conversation"
2254
+ });
2255
+ existingPaths.add(file);
2256
+ stats.conversationsStored++;
2257
+ if (stats.filesScanned % 50 === 0) {
2257
2258
  process.stderr.write(
2258
2259
  `[backfill-conversations] Progress: ${stats.filesScanned}/${files.length} files, ${stats.conversationsStored} stored
2259
2260
  `
2260
2261
  );
2261
- if (!options.dryRun) await flushBatch();
2262
+ await flushBatch();
2262
2263
  }
2263
2264
  }
2264
- if (!options.dryRun) await flushBatch();
2265
+ if (!options.dryRun) {
2266
+ await flushBatch();
2267
+ }
2265
2268
  process.stderr.write(
2266
- `[backfill-conversations] Done. Files: ${stats.filesScanned}, Conversations: ${stats.conversationsStored}, Memories: ${stats.memoriesStored}, Dedup: ${stats.skippedDedup}, Short: ${stats.skippedShort}, EmbedFail: ${stats.embedFailed}
2269
+ `[backfill-conversations] Done. Scanned: ${stats.filesScanned}, Stored: ${stats.conversationsStored}, Dedup: ${stats.skippedDedup}, TooShort: ${stats.skippedTooShort}, EmbedFail: ${stats.embedFailed}
2267
2270
  `
2268
2271
  );
2269
2272
  return stats;
2270
2273
  }
2271
- function parseArgs(argv) {
2272
- let days = 30;
2273
- let projectFilter;
2274
- let dryRun = false;
2275
- for (let i = 0; i < argv.length; i++) {
2276
- switch (argv[i]) {
2277
- case "--days":
2278
- days = parseInt(argv[++i] ?? "30", 10) || 30;
2279
- break;
2280
- case "--project":
2281
- projectFilter = argv[++i] ?? void 0;
2282
- break;
2283
- case "--dry-run":
2284
- dryRun = true;
2285
- break;
2286
- }
2287
- }
2288
- return { days, projectFilter, dryRun };
2289
- }
2290
2274
  if (isMainModule(import.meta.url)) {
2291
- const options = parseArgs(process.argv.slice(2));
2292
- backfillConversations(options).then((result) => {
2275
+ const { values } = parseArgs({
2276
+ options: {
2277
+ since: { type: "string" },
2278
+ project: { type: "string" },
2279
+ "dry-run": { type: "boolean", default: false }
2280
+ },
2281
+ strict: true
2282
+ });
2283
+ backfillConversations({
2284
+ since: values.since,
2285
+ project: values.project,
2286
+ dryRun: values["dry-run"]
2287
+ }).then((result) => {
2293
2288
  console.log(JSON.stringify(result, null, 2));
2294
2289
  process.exit(0);
2295
2290
  }).catch((err) => {
2296
- console.error(
2297
- "Backfill failed:",
2298
- err instanceof Error ? err.message : String(err)
2299
- );
2291
+ console.error("Backfill failed:", err instanceof Error ? err.message : String(err));
2300
2292
  process.exit(1);
2301
2293
  });
2302
2294
  }