@askexenow/exe-os 0.9.21 → 0.9.22

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 (60) hide show
  1. package/dist/bin/backfill-conversations.js +17 -4
  2. package/dist/bin/backfill-responses.js +17 -4
  3. package/dist/bin/backfill-vectors.js +2 -2
  4. package/dist/bin/cleanup-stale-review-tasks.js +17 -4
  5. package/dist/bin/cli.js +378 -171
  6. package/dist/bin/exe-assign.js +17 -4
  7. package/dist/bin/exe-boot.js +2 -2
  8. package/dist/bin/exe-dispatch.js +17 -4
  9. package/dist/bin/exe-doctor.js +2 -2
  10. package/dist/bin/exe-export-behaviors.js +17 -4
  11. package/dist/bin/exe-forget.js +17 -4
  12. package/dist/bin/exe-gateway.js +17 -4
  13. package/dist/bin/exe-heartbeat.js +17 -4
  14. package/dist/bin/exe-kill.js +17 -4
  15. package/dist/bin/exe-launch-agent.js +17 -4
  16. package/dist/bin/exe-pending-messages.js +17 -4
  17. package/dist/bin/exe-pending-notifications.js +17 -4
  18. package/dist/bin/exe-pending-reviews.js +17 -4
  19. package/dist/bin/exe-review.js +17 -4
  20. package/dist/bin/exe-search.js +23 -8
  21. package/dist/bin/exe-session-cleanup.js +17 -4
  22. package/dist/bin/exe-start-codex.js +209 -32
  23. package/dist/bin/exe-start-opencode.js +17 -4
  24. package/dist/bin/exe-status.js +17 -4
  25. package/dist/bin/exe-team.js +17 -4
  26. package/dist/bin/git-sweep.js +17 -4
  27. package/dist/bin/graph-backfill.js +17 -4
  28. package/dist/bin/graph-export.js +17 -4
  29. package/dist/bin/install.js +42 -0
  30. package/dist/bin/intercom-check.js +17 -4
  31. package/dist/bin/scan-tasks.js +17 -4
  32. package/dist/bin/shard-migrate.js +17 -4
  33. package/dist/bin/update.js +187 -42
  34. package/dist/gateway/index.js +17 -4
  35. package/dist/hooks/bug-report-worker.js +793 -150
  36. package/dist/hooks/codex-stop-task-finalizer.js +3020 -2375
  37. package/dist/hooks/commit-complete.js +156 -6
  38. package/dist/hooks/error-recall.js +23 -8
  39. package/dist/hooks/ingest.js +17 -4
  40. package/dist/hooks/instructions-loaded.js +17 -4
  41. package/dist/hooks/notification.js +17 -4
  42. package/dist/hooks/post-compact.js +17 -4
  43. package/dist/hooks/post-tool-combined.js +23 -8
  44. package/dist/hooks/pre-compact.js +156 -8
  45. package/dist/hooks/pre-tool-use.js +21 -12
  46. package/dist/hooks/prompt-submit.js +23 -8
  47. package/dist/hooks/session-end.js +156 -8
  48. package/dist/hooks/session-start.js +23 -8
  49. package/dist/hooks/stop.js +306 -9
  50. package/dist/hooks/subagent-stop.js +306 -9
  51. package/dist/hooks/summary-worker.js +2 -2
  52. package/dist/index.js +17 -4
  53. package/dist/lib/exe-daemon.js +17 -4
  54. package/dist/lib/hybrid-search.js +23 -8
  55. package/dist/lib/schedules.js +2 -2
  56. package/dist/lib/store.js +17 -4
  57. package/dist/mcp/server.js +36 -10
  58. package/dist/runtime/index.js +17 -4
  59. package/dist/tui/App.js +17 -4
  60. package/package.json +1 -1
@@ -16,8 +16,8 @@ var __export = (target, all) => {
16
16
  };
17
17
 
18
18
  // src/lib/secure-files.ts
19
- import { chmodSync, existsSync, mkdirSync } from "fs";
20
- import { chmod, mkdir } from "fs/promises";
19
+ import { chmodSync, existsSync as existsSync2, mkdirSync } from "fs";
20
+ import { chmod, mkdir as mkdir2 } from "fs/promises";
21
21
  var init_secure_files = __esm({
22
22
  "src/lib/secure-files.ts"() {
23
23
  "use strict";
@@ -25,16 +25,16 @@ var init_secure_files = __esm({
25
25
  });
26
26
 
27
27
  // src/lib/config.ts
28
- import { readFile, writeFile } from "fs/promises";
29
- import { readFileSync as readFileSync2, existsSync as existsSync2, renameSync } from "fs";
30
- import path2 from "path";
31
- import os from "os";
32
- function resolveDataDir() {
28
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
29
+ import { readFileSync as readFileSync2, existsSync as existsSync3, renameSync } from "fs";
30
+ import path3 from "path";
31
+ import os2 from "os";
32
+ function resolveDataDir2() {
33
33
  if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
34
34
  if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
35
- const newDir = path2.join(os.homedir(), ".exe-os");
36
- const legacyDir = path2.join(os.homedir(), ".exe-mem");
37
- if (!existsSync2(newDir) && existsSync2(legacyDir)) {
35
+ const newDir = path3.join(os2.homedir(), ".exe-os");
36
+ const legacyDir = path3.join(os2.homedir(), ".exe-mem");
37
+ if (!existsSync3(newDir) && existsSync3(legacyDir)) {
38
38
  try {
39
39
  renameSync(legacyDir, newDir);
40
40
  process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
@@ -50,11 +50,11 @@ var init_config = __esm({
50
50
  "src/lib/config.ts"() {
51
51
  "use strict";
52
52
  init_secure_files();
53
- EXE_AI_DIR = resolveDataDir();
54
- DB_PATH = path2.join(EXE_AI_DIR, "memories.db");
55
- MODELS_DIR = path2.join(EXE_AI_DIR, "models");
56
- CONFIG_PATH = path2.join(EXE_AI_DIR, "config.json");
57
- LEGACY_LANCE_PATH = path2.join(EXE_AI_DIR, "local.lance");
53
+ EXE_AI_DIR = resolveDataDir2();
54
+ DB_PATH = path3.join(EXE_AI_DIR, "memories.db");
55
+ MODELS_DIR = path3.join(EXE_AI_DIR, "models");
56
+ CONFIG_PATH = path3.join(EXE_AI_DIR, "config.json");
57
+ LEGACY_LANCE_PATH = path3.join(EXE_AI_DIR, "local.lance");
58
58
  CURRENT_CONFIG_VERSION = 1;
59
59
  DEFAULT_CONFIG = {
60
60
  config_version: CURRENT_CONFIG_VERSION,
@@ -133,12 +133,12 @@ __export(license_exports, {
133
133
  stopLicenseRevalidation: () => stopLicenseRevalidation,
134
134
  validateLicense: () => validateLicense
135
135
  });
136
- import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
136
+ import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
137
137
  import { randomUUID } from "crypto";
138
138
  import { createRequire } from "module";
139
139
  import { pathToFileURL } from "url";
140
- import os2 from "os";
141
- import path3 from "path";
140
+ import os3 from "os";
141
+ import path4 from "path";
142
142
  import { jwtVerify, importSPKI } from "jose";
143
143
  async function fetchRetry(url, init) {
144
144
  try {
@@ -149,16 +149,16 @@ async function fetchRetry(url, init) {
149
149
  }
150
150
  }
151
151
  function loadDeviceId() {
152
- const deviceJsonPath = path3.join(EXE_AI_DIR, "device.json");
152
+ const deviceJsonPath = path4.join(EXE_AI_DIR, "device.json");
153
153
  try {
154
- if (existsSync3(deviceJsonPath)) {
154
+ if (existsSync4(deviceJsonPath)) {
155
155
  const data = JSON.parse(readFileSync3(deviceJsonPath, "utf8"));
156
156
  if (data.deviceId) return data.deviceId;
157
157
  }
158
158
  } catch {
159
159
  }
160
160
  try {
161
- if (existsSync3(DEVICE_ID_PATH)) {
161
+ if (existsSync4(DEVICE_ID_PATH)) {
162
162
  const id2 = readFileSync3(DEVICE_ID_PATH, "utf8").trim();
163
163
  if (id2) return id2;
164
164
  }
@@ -171,7 +171,7 @@ function loadDeviceId() {
171
171
  }
172
172
  function loadLicense() {
173
173
  try {
174
- if (!existsSync3(LICENSE_PATH)) return null;
174
+ if (!existsSync4(LICENSE_PATH)) return null;
175
175
  return readFileSync3(LICENSE_PATH, "utf8").trim();
176
176
  } catch {
177
177
  return null;
@@ -205,7 +205,7 @@ async function verifyLicenseJwt(token) {
205
205
  }
206
206
  async function getCachedLicense() {
207
207
  try {
208
- if (!existsSync3(CACHE_PATH)) return null;
208
+ if (!existsSync4(CACHE_PATH)) return null;
209
209
  const raw = JSON.parse(readFileSync3(CACHE_PATH, "utf8"));
210
210
  if (!raw.token || typeof raw.token !== "string") return null;
211
211
  return await verifyLicenseJwt(raw.token);
@@ -215,7 +215,7 @@ async function getCachedLicense() {
215
215
  }
216
216
  function readCachedToken() {
217
217
  try {
218
- if (!existsSync3(CACHE_PATH)) return null;
218
+ if (!existsSync4(CACHE_PATH)) return null;
219
219
  const raw = JSON.parse(readFileSync3(CACHE_PATH, "utf8"));
220
220
  return typeof raw.token === "string" ? raw.token : null;
221
221
  } catch {
@@ -258,8 +258,8 @@ function loadPrismaForLicense() {
258
258
  if (_prismaFailed) return null;
259
259
  const dbUrl = process.env.DATABASE_URL;
260
260
  if (!dbUrl) {
261
- const exeDbRoot = process.env.EXE_DB_ROOT ?? path3.join(os2.homedir(), "exe-db");
262
- if (!existsSync3(path3.join(exeDbRoot, "package.json"))) {
261
+ const exeDbRoot = process.env.EXE_DB_ROOT ?? path4.join(os3.homedir(), "exe-db");
262
+ if (!existsSync4(path4.join(exeDbRoot, "package.json"))) {
263
263
  _prismaFailed = true;
264
264
  return null;
265
265
  }
@@ -273,8 +273,8 @@ function loadPrismaForLicense() {
273
273
  if (!Ctor2) throw new Error(`No PrismaClient at ${explicitPath}`);
274
274
  return new Ctor2();
275
275
  }
276
- const exeDbRoot = process.env.EXE_DB_ROOT ?? path3.join(os2.homedir(), "exe-db");
277
- const req = createRequire(path3.join(exeDbRoot, "package.json"));
276
+ const exeDbRoot = process.env.EXE_DB_ROOT ?? path4.join(os3.homedir(), "exe-db");
277
+ const req = createRequire(path4.join(exeDbRoot, "package.json"));
278
278
  const entry = req.resolve("@prisma/client");
279
279
  const mod = await import(pathToFileURL(entry).href);
280
280
  const Ctor = mod.PrismaClient ?? mod.default?.PrismaClient;
@@ -363,7 +363,7 @@ async function validateLicense(apiKey, deviceId) {
363
363
  const cached = await getCachedLicense();
364
364
  if (cached) return cached;
365
365
  try {
366
- if (existsSync3(CACHE_PATH)) {
366
+ if (existsSync4(CACHE_PATH)) {
367
367
  const raw = JSON.parse(readFileSync3(CACHE_PATH, "utf8"));
368
368
  if (raw.pgLicense && raw.ts && Date.now() - raw.ts < 7 * 24 * 60 * 60 * 1e3) {
369
369
  return raw.pgLicense;
@@ -388,8 +388,8 @@ async function checkLicense() {
388
388
  let key = loadLicense();
389
389
  if (!key) {
390
390
  try {
391
- const configPath = path3.join(EXE_AI_DIR, "config.json");
392
- if (existsSync3(configPath)) {
391
+ const configPath = path4.join(EXE_AI_DIR, "config.json");
392
+ if (existsSync4(configPath)) {
393
393
  const raw = JSON.parse(readFileSync3(configPath, "utf8"));
394
394
  const cloud = raw.cloud;
395
395
  if (cloud?.apiKey) {
@@ -549,9 +549,9 @@ var init_license = __esm({
549
549
  "src/lib/license.ts"() {
550
550
  "use strict";
551
551
  init_config();
552
- LICENSE_PATH = path3.join(EXE_AI_DIR, "license.key");
553
- CACHE_PATH = path3.join(EXE_AI_DIR, "license-cache.json");
554
- DEVICE_ID_PATH = path3.join(EXE_AI_DIR, "device-id");
552
+ LICENSE_PATH = path4.join(EXE_AI_DIR, "license.key");
553
+ CACHE_PATH = path4.join(EXE_AI_DIR, "license-cache.json");
554
+ DEVICE_ID_PATH = path4.join(EXE_AI_DIR, "device-id");
555
555
  API_BASE = "https://askexe.com/cloud";
556
556
  RETRY_DELAY_MS = 500;
557
557
  LICENSE_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
@@ -601,12 +601,102 @@ function isMainModule(importMetaUrl) {
601
601
  }
602
602
  }
603
603
 
604
+ // src/lib/update-backup.ts
605
+ import { copyFile, readFile, readdir, writeFile, rm, mkdir, cp } from "fs/promises";
606
+ import { existsSync } from "fs";
607
+ import path from "path";
608
+ import os from "os";
609
+ var BACKUP_DIR_NAME = ".update-backup";
610
+ function resolveDataDir() {
611
+ if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
612
+ if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
613
+ return path.join(os.homedir(), ".exe-os");
614
+ }
615
+ var BACKUP_TARGETS = [
616
+ { name: "config.json", type: "file" },
617
+ { name: "exe-employees.json", type: "file" },
618
+ { name: "master.key", type: "file" },
619
+ { name: "identity", type: "dir" }
620
+ // All .db files (SQLCipher databases) — matched dynamically
621
+ ];
622
+ async function createUpdateBackup(currentVersion, dataDir) {
623
+ const dir = dataDir ?? resolveDataDir();
624
+ const backupDir = path.join(dir, BACKUP_DIR_NAME);
625
+ if (existsSync(backupDir)) {
626
+ await rm(backupDir, { recursive: true, force: true });
627
+ }
628
+ await mkdir(backupDir, { recursive: true });
629
+ const backedUpFiles = [];
630
+ for (const target of BACKUP_TARGETS) {
631
+ const src = path.join(dir, target.name);
632
+ if (!existsSync(src)) continue;
633
+ const dest = path.join(backupDir, target.name);
634
+ if (target.type === "file") {
635
+ await copyFile(src, dest);
636
+ } else {
637
+ await cp(src, dest, { recursive: true });
638
+ }
639
+ backedUpFiles.push(target.name);
640
+ }
641
+ const entries = await readdir(dir, { withFileTypes: true });
642
+ for (const entry of entries) {
643
+ if (entry.isFile() && entry.name.endsWith(".db") && entry.name !== BACKUP_DIR_NAME) {
644
+ const src = path.join(dir, entry.name);
645
+ const dest = path.join(backupDir, entry.name);
646
+ await copyFile(src, dest);
647
+ backedUpFiles.push(entry.name);
648
+ }
649
+ }
650
+ const manifest = {
651
+ version: currentVersion,
652
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
653
+ files: backedUpFiles
654
+ };
655
+ await writeFile(
656
+ path.join(backupDir, "manifest.json"),
657
+ JSON.stringify(manifest, null, 2) + "\n"
658
+ );
659
+ return manifest;
660
+ }
661
+ async function restoreFromBackup(dataDir) {
662
+ const dir = dataDir ?? resolveDataDir();
663
+ const backupDir = path.join(dir, BACKUP_DIR_NAME);
664
+ const manifestPath = path.join(backupDir, "manifest.json");
665
+ if (!existsSync(manifestPath)) {
666
+ throw new Error(
667
+ `No backup found at ${backupDir}. Nothing to restore.`
668
+ );
669
+ }
670
+ const manifest = JSON.parse(
671
+ await readFile(manifestPath, "utf-8")
672
+ );
673
+ for (const fileName of manifest.files) {
674
+ const src = path.join(backupDir, fileName);
675
+ const dest = path.join(dir, fileName);
676
+ if (!existsSync(src)) continue;
677
+ const stat = await import("fs/promises").then((m) => m.stat(src));
678
+ if (stat.isDirectory()) {
679
+ await cp(src, dest, { recursive: true, force: true });
680
+ } else {
681
+ await copyFile(src, dest);
682
+ }
683
+ }
684
+ return manifest;
685
+ }
686
+ async function deleteBackup(dataDir) {
687
+ const dir = dataDir ?? resolveDataDir();
688
+ const backupDir = path.join(dir, BACKUP_DIR_NAME);
689
+ if (existsSync(backupDir)) {
690
+ await rm(backupDir, { recursive: true, force: true });
691
+ }
692
+ }
693
+
604
694
  // src/lib/update-check.ts
605
695
  import { execSync } from "child_process";
606
696
  import { readFileSync } from "fs";
607
- import path from "path";
697
+ import path2 from "path";
608
698
  function getLocalVersion(packageRoot) {
609
- const pkgPath = path.join(packageRoot, "package.json");
699
+ const pkgPath = path2.join(packageRoot, "package.json");
610
700
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
611
701
  return pkg.version;
612
702
  }
@@ -639,10 +729,47 @@ function checkForUpdate(packageRoot) {
639
729
  }
640
730
 
641
731
  // src/bin/update.ts
732
+ async function runRestore() {
733
+ console.log("\n\u{1F504} Restoring from update backup...");
734
+ try {
735
+ const manifest = await restoreFromBackup();
736
+ console.log(` Restored ${manifest.files.length} file(s) from v${manifest.version} backup.`);
737
+ console.log(` Backup was created at ${manifest.timestamp}`);
738
+ console.log(`
739
+ \u{1F4E5} Reinstalling @askexenow/exe-os@${manifest.version}...`);
740
+ try {
741
+ execSync2(`npm install -g @askexenow/exe-os@${manifest.version}`, {
742
+ stdio: ["pipe", "pipe", "inherit"],
743
+ timeout: 3e5
744
+ });
745
+ console.log(`
746
+ \u2705 Restored to v${manifest.version}`);
747
+ } catch {
748
+ console.error(`
749
+ \u26A0\uFE0F Could not reinstall v${manifest.version} via npm.`);
750
+ console.error(` Your data files have been restored. Try manually:`);
751
+ console.error(` npm install -g @askexenow/exe-os@${manifest.version}`);
752
+ }
753
+ try {
754
+ await deleteBackup();
755
+ } catch {
756
+ }
757
+ console.log("\n\u{1F680} Restore complete. Restart your sessions.\n");
758
+ } catch (err) {
759
+ console.error("\u274C Restore failed.");
760
+ if (err instanceof Error) console.error(` ${err.message}`);
761
+ process.exit(1);
762
+ }
763
+ }
642
764
  async function runUpdate(cliArgs) {
643
765
  const args = cliArgs ?? process.argv.slice(2);
644
766
  const autoMode = args.includes("--auto") || args.includes("-y");
645
767
  const checkOnly = args.includes("--check");
768
+ const restoreMode = args.includes("--restore");
769
+ if (restoreMode) {
770
+ await runRestore();
771
+ return;
772
+ }
646
773
  const packageRoot = new URL("../..", import.meta.url).pathname;
647
774
  const result = checkForUpdate(packageRoot);
648
775
  if (result.error) {
@@ -677,7 +804,16 @@ async function runUpdate(cliArgs) {
677
804
  console.log("Update skipped.");
678
805
  process.exit(0);
679
806
  }
680
- console.log("\n\u{1F9F9} Clearing npm cache...");
807
+ console.log("\n\u{1F4BE} Backing up customer data...");
808
+ try {
809
+ const backupResult = await createUpdateBackup(result.localVersion);
810
+ console.log(` Backed up ${backupResult.files.length} file(s) to ${BACKUP_DIR_NAME}/`);
811
+ } catch (err) {
812
+ console.error("\u274C Backup failed \u2014 aborting update to protect your data.");
813
+ if (err instanceof Error) console.error(` ${err.message}`);
814
+ process.exit(1);
815
+ }
816
+ console.log("\u{1F9F9} Clearing npm cache...");
681
817
  try {
682
818
  execSync2("npm cache clean --force", { stdio: "pipe" });
683
819
  console.log(" Done");
@@ -692,8 +828,9 @@ async function runUpdate(cliArgs) {
692
828
  timeout: 3e5
693
829
  });
694
830
  } catch (err) {
695
- console.error("\n\u274C Update failed.");
696
- console.error(" Try manually: npm install -g @askexenow/exe-os@latest");
831
+ console.error("\n\u274C Update failed. Your backup is preserved.");
832
+ console.error(` Restore with: exe-os update --restore`);
833
+ console.error(" Or try manually: npm install -g @askexenow/exe-os@latest");
697
834
  if (err instanceof Error && err.message) {
698
835
  console.error(` Error: ${err.message.split("\n")[0]}`);
699
836
  }
@@ -712,30 +849,38 @@ async function runUpdate(cliArgs) {
712
849
  }
713
850
  }
714
851
  const remoteVersion = result.remoteVersion;
852
+ const updateSucceeded = newVersion !== result.localVersion;
715
853
  if (newVersion === remoteVersion) {
716
854
  console.log(`
717
855
  \u2705 Updated to v${newVersion}`);
718
- } else if (newVersion !== result.localVersion) {
856
+ } else if (updateSucceeded) {
719
857
  console.log(`
720
858
  \u2705 Updated to v${newVersion} (latest: v${remoteVersion})`);
721
859
  } else {
722
860
  console.log(`
723
861
  \u26A0\uFE0F Version unchanged (v${newVersion}). npm cache may be stale.`);
862
+ console.log(" Backup preserved. Restore with: exe-os update --restore");
724
863
  console.log(" Try: npm cache clean --force && npm install -g @askexenow/exe-os@latest");
725
864
  }
865
+ if (updateSucceeded) {
866
+ try {
867
+ await deleteBackup();
868
+ } catch {
869
+ }
870
+ }
726
871
  console.log(" Hooks re-wired, daemon restarted automatically.");
727
872
  console.log("");
728
873
  console.log(" \x1B[33m\u26A1 Run /mcp in each active Claude Code session to pick up new tools.\x1B[0m");
729
874
  console.log(" \x1B[2m(MCP servers can't hot-reload \u2014 Claude Code needs to reconnect them.)\x1B[0m");
730
875
  try {
731
- const { existsSync: exists, readFileSync: readFile2 } = await import("fs");
876
+ const { existsSync: exists, readFileSync: readFile3 } = await import("fs");
732
877
  const p = await import("path");
733
878
  const { homedir: home } = await import("os");
734
879
  const exeDir = p.default.join(home(), ".exe-os");
735
880
  const licKeyPath = p.default.join(exeDir, "license.key");
736
881
  const configPath = p.default.join(exeDir, "config.json");
737
882
  if (!exists(licKeyPath) && exists(configPath)) {
738
- const cfg = JSON.parse(readFile2(configPath, "utf8"));
883
+ const cfg = JSON.parse(readFile3(configPath, "utf8"));
739
884
  const cloud = cfg.cloud;
740
885
  if (cloud?.apiKey) {
741
886
  const { mirrorLicenseKey: mirrorLicenseKey2 } = await Promise.resolve().then(() => (init_license(), license_exports));
@@ -3580,8 +3580,8 @@ function getShardClient(projectName) {
3580
3580
  throw new Error("Shard manager not initialized. Call initShardManager() first.");
3581
3581
  }
3582
3582
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
3583
- if (!safeName) {
3584
- throw new Error(`Invalid project name for shard: "${projectName}"`);
3583
+ if (!safeName || safeName === "unknown") {
3584
+ throw new Error(`Invalid project name for shard: "${projectName}" (resolved to "${safeName}")`);
3585
3585
  }
3586
3586
  const cached = _shards.get(safeName);
3587
3587
  if (cached) {
@@ -4450,19 +4450,32 @@ async function flushBatch() {
4450
4450
  const { isShardingEnabled: isShardingEnabled2, getReadyShardClient: getReadyShardClient2 } = await Promise.resolve().then(() => (init_shard_manager(), shard_manager_exports));
4451
4451
  if (isShardingEnabled2()) {
4452
4452
  const byProject = /* @__PURE__ */ new Map();
4453
+ let skippedUnknown = 0;
4453
4454
  for (const row of batch) {
4454
- const proj = row.project_name || "unknown";
4455
+ const proj = row.project_name?.trim();
4456
+ if (!proj) {
4457
+ skippedUnknown++;
4458
+ continue;
4459
+ }
4455
4460
  if (!byProject.has(proj)) byProject.set(proj, []);
4456
4461
  byProject.get(proj).push(row);
4457
4462
  }
4463
+ if (skippedUnknown > 0) {
4464
+ process.stderr.write(
4465
+ `[store] Shard skip: ${skippedUnknown} record(s) with empty project_name (kept in main DB only)
4466
+ `
4467
+ );
4468
+ }
4458
4469
  for (const [project, rows] of byProject) {
4459
4470
  try {
4460
4471
  const shardClient = await getReadyShardClient2(project);
4461
4472
  const shardStmts = rows.map(buildStmt);
4462
4473
  await shardClient.batch(shardStmts, "write");
4463
4474
  } catch (err) {
4475
+ const fullError = err instanceof Error ? `${err.name}: ${err.message}${err.stack ? `
4476
+ ${err.stack.split("\n").slice(1, 3).join("\n")}` : ""}` : String(err);
4464
4477
  process.stderr.write(
4465
- `[store] Shard write failed for ${project}: ${err instanceof Error ? err.message : String(err)}
4478
+ `[store] Shard write failed for ${project} (${rows.length} records): ${fullError}
4466
4479
  `
4467
4480
  );
4468
4481
  }