@askexenow/exe-os 0.9.66 → 0.9.67

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 (99) hide show
  1. package/deploy/stack-manifests/v0.9.json +54 -5
  2. package/dist/bin/age-ontology-load.js +61 -0
  3. package/dist/bin/agentic-ontology-backfill.js +4708 -0
  4. package/dist/bin/agentic-reflection-backfill.js +4144 -0
  5. package/dist/bin/{exe-link.js → agentic-semantic-label.js} +1523 -2275
  6. package/dist/bin/backfill-conversations.js +506 -20
  7. package/dist/bin/backfill-responses.js +506 -20
  8. package/dist/bin/backfill-vectors.js +233 -20
  9. package/dist/bin/bulk-sync-postgres.js +4876 -0
  10. package/dist/bin/cleanup-stale-review-tasks.js +507 -21
  11. package/dist/bin/cli.js +2450 -1530
  12. package/dist/bin/exe-assign.js +506 -20
  13. package/dist/bin/exe-boot.js +410 -60
  14. package/dist/bin/exe-cloud.js +795 -105
  15. package/dist/bin/exe-dispatch.js +516 -22
  16. package/dist/bin/exe-doctor.js +587 -30
  17. package/dist/bin/exe-export-behaviors.js +518 -24
  18. package/dist/bin/exe-forget.js +507 -21
  19. package/dist/bin/exe-gateway.js +571 -25
  20. package/dist/bin/exe-heartbeat.js +518 -24
  21. package/dist/bin/exe-kill.js +507 -21
  22. package/dist/bin/exe-launch-agent.js +2312 -1069
  23. package/dist/bin/exe-new-employee.js +197 -165
  24. package/dist/bin/exe-pending-messages.js +507 -21
  25. package/dist/bin/exe-pending-notifications.js +507 -21
  26. package/dist/bin/exe-pending-reviews.js +507 -21
  27. package/dist/bin/exe-rename.js +507 -21
  28. package/dist/bin/exe-review.js +507 -21
  29. package/dist/bin/exe-search.js +518 -24
  30. package/dist/bin/exe-session-cleanup.js +516 -22
  31. package/dist/bin/exe-settings.js +4 -0
  32. package/dist/bin/exe-start-codex.js +682 -143
  33. package/dist/bin/exe-start-opencode.js +627 -79
  34. package/dist/bin/exe-status.js +507 -21
  35. package/dist/bin/exe-team.js +507 -21
  36. package/dist/bin/git-sweep.js +516 -22
  37. package/dist/bin/graph-backfill.js +558 -21
  38. package/dist/bin/graph-export.js +507 -21
  39. package/dist/bin/graph-layer-benchmark.js +109 -0
  40. package/dist/bin/install.js +305 -288
  41. package/dist/bin/intercom-check.js +516 -22
  42. package/dist/bin/postgres-agentic-reflection-backfill.js +187 -0
  43. package/dist/bin/postgres-agentic-semantic-backfill.js +237 -0
  44. package/dist/bin/scan-tasks.js +516 -22
  45. package/dist/bin/setup.js +412 -62
  46. package/dist/bin/shard-migrate.js +506 -20
  47. package/dist/gateway/index.js +569 -23
  48. package/dist/hooks/bug-report-worker.js +519 -25
  49. package/dist/hooks/codex-stop-task-finalizer.js +516 -22
  50. package/dist/hooks/commit-complete.js +516 -22
  51. package/dist/hooks/error-recall.js +518 -24
  52. package/dist/hooks/ingest.js +516 -22
  53. package/dist/hooks/instructions-loaded.js +507 -21
  54. package/dist/hooks/notification.js +507 -21
  55. package/dist/hooks/post-compact.js +507 -21
  56. package/dist/hooks/post-tool-combined.js +519 -25
  57. package/dist/hooks/pre-compact.js +516 -22
  58. package/dist/hooks/pre-tool-use.js +507 -21
  59. package/dist/hooks/prompt-submit.js +519 -25
  60. package/dist/hooks/session-end.js +516 -22
  61. package/dist/hooks/session-start.js +520 -26
  62. package/dist/hooks/stop.js +517 -23
  63. package/dist/hooks/subagent-stop.js +507 -21
  64. package/dist/hooks/summary-worker.js +411 -61
  65. package/dist/index.js +569 -23
  66. package/dist/lib/cloud-sync.js +391 -53
  67. package/dist/lib/config.js +13 -1
  68. package/dist/lib/consolidation.js +1 -1
  69. package/dist/lib/database.js +124 -0
  70. package/dist/lib/db.js +124 -0
  71. package/dist/lib/device-registry.js +124 -0
  72. package/dist/lib/embedder.js +13 -1
  73. package/dist/lib/exe-daemon.js +2184 -561
  74. package/dist/lib/hybrid-search.js +518 -24
  75. package/dist/lib/identity.js +3 -0
  76. package/dist/lib/keychain.js +178 -22
  77. package/dist/lib/messaging.js +3 -0
  78. package/dist/lib/reminders.js +3 -0
  79. package/dist/lib/schedules.js +233 -20
  80. package/dist/lib/skill-learning.js +16 -1
  81. package/dist/lib/store.js +506 -20
  82. package/dist/lib/tasks.js +16 -1
  83. package/dist/lib/tmux-routing.js +16 -1
  84. package/dist/lib/token-spend.js +3 -0
  85. package/dist/mcp/server.js +1757 -428
  86. package/dist/mcp/tools/complete-reminder.js +3 -0
  87. package/dist/mcp/tools/create-reminder.js +3 -0
  88. package/dist/mcp/tools/create-task.js +16 -1
  89. package/dist/mcp/tools/deactivate-behavior.js +3 -0
  90. package/dist/mcp/tools/list-reminders.js +3 -0
  91. package/dist/mcp/tools/list-tasks.js +3 -0
  92. package/dist/mcp/tools/send-message.js +3 -0
  93. package/dist/mcp/tools/update-task.js +16 -1
  94. package/dist/runtime/index.js +516 -22
  95. package/dist/tui/App.js +594 -29
  96. package/package.json +8 -5
  97. package/src/commands/exe/cloud.md +6 -10
  98. package/stack.release.json +3 -3
  99. package/src/commands/exe/link.md +0 -18
@@ -1471,6 +1471,9 @@ function getClient() {
1471
1471
  if (_daemonClient && _daemonClient._isDaemonActive()) {
1472
1472
  return _daemonClient;
1473
1473
  }
1474
+ if (!_resilientClient) {
1475
+ return _adapterClient;
1476
+ }
1474
1477
  return _resilientClient;
1475
1478
  }
1476
1479
  async function initDaemonClient() {
@@ -2503,6 +2506,127 @@ async function ensureSchema() {
2503
2506
  VALUES (new.rowid, new.content, new.subject, new.predicate, new.object);
2504
2507
  END;
2505
2508
  `);
2509
+ await client.executeMultiple(`
2510
+ CREATE TABLE IF NOT EXISTS agent_sessions (
2511
+ id TEXT PRIMARY KEY,
2512
+ agent_id TEXT NOT NULL,
2513
+ project_name TEXT,
2514
+ started_at TEXT NOT NULL,
2515
+ last_event_at TEXT NOT NULL,
2516
+ event_count INTEGER NOT NULL DEFAULT 0,
2517
+ properties TEXT DEFAULT '{}'
2518
+ );
2519
+
2520
+ CREATE INDEX IF NOT EXISTS idx_agent_sessions_agent_time
2521
+ ON agent_sessions(agent_id, started_at);
2522
+
2523
+ CREATE TABLE IF NOT EXISTS agent_goals (
2524
+ id TEXT PRIMARY KEY,
2525
+ statement TEXT NOT NULL,
2526
+ owner_agent_id TEXT,
2527
+ project_name TEXT,
2528
+ status TEXT NOT NULL DEFAULT 'open',
2529
+ priority INTEGER NOT NULL DEFAULT 5,
2530
+ success_criteria TEXT,
2531
+ parent_goal_id TEXT,
2532
+ due_at TEXT,
2533
+ achieved_at TEXT,
2534
+ supersedes_id TEXT,
2535
+ created_at TEXT NOT NULL,
2536
+ updated_at TEXT NOT NULL,
2537
+ source_memory_id TEXT
2538
+ );
2539
+
2540
+ CREATE INDEX IF NOT EXISTS idx_agent_goals_project_status
2541
+ ON agent_goals(project_name, status, priority);
2542
+
2543
+ CREATE TABLE IF NOT EXISTS agent_events (
2544
+ id TEXT PRIMARY KEY,
2545
+ event_type TEXT NOT NULL,
2546
+ occurred_at TEXT NOT NULL,
2547
+ sequence_index INTEGER NOT NULL,
2548
+ actor_agent_id TEXT,
2549
+ agent_role TEXT,
2550
+ project_name TEXT,
2551
+ session_id TEXT,
2552
+ task_id TEXT,
2553
+ goal_id TEXT,
2554
+ parent_event_id TEXT,
2555
+ intention TEXT,
2556
+ outcome TEXT,
2557
+ evidence_memory_id TEXT,
2558
+ impact TEXT,
2559
+ payload TEXT DEFAULT '{}',
2560
+ created_at TEXT NOT NULL
2561
+ );
2562
+
2563
+ CREATE INDEX IF NOT EXISTS idx_agent_events_time
2564
+ ON agent_events(occurred_at, sequence_index);
2565
+
2566
+ CREATE INDEX IF NOT EXISTS idx_agent_events_session_seq
2567
+ ON agent_events(session_id, sequence_index);
2568
+
2569
+ CREATE INDEX IF NOT EXISTS idx_agent_events_goal_time
2570
+ ON agent_events(goal_id, occurred_at);
2571
+
2572
+ CREATE INDEX IF NOT EXISTS idx_agent_events_memory
2573
+ ON agent_events(evidence_memory_id);
2574
+
2575
+ CREATE TABLE IF NOT EXISTS agent_goal_links (
2576
+ id TEXT PRIMARY KEY,
2577
+ goal_id TEXT NOT NULL,
2578
+ link_type TEXT NOT NULL,
2579
+ target_id TEXT NOT NULL,
2580
+ target_type TEXT NOT NULL,
2581
+ created_at TEXT NOT NULL
2582
+ );
2583
+
2584
+ CREATE INDEX IF NOT EXISTS idx_agent_goal_links_goal
2585
+ ON agent_goal_links(goal_id, target_type);
2586
+
2587
+ CREATE TABLE IF NOT EXISTS agent_semantic_labels (
2588
+ id TEXT PRIMARY KEY,
2589
+ source_memory_id TEXT NOT NULL,
2590
+ event_id TEXT,
2591
+ labeler TEXT NOT NULL,
2592
+ schema_version INTEGER NOT NULL DEFAULT 1,
2593
+ confidence REAL NOT NULL DEFAULT 0,
2594
+ labels TEXT NOT NULL,
2595
+ created_at TEXT NOT NULL,
2596
+ updated_at TEXT NOT NULL
2597
+ );
2598
+
2599
+ CREATE INDEX IF NOT EXISTS idx_agent_semantic_labels_memory
2600
+ ON agent_semantic_labels(source_memory_id, labeler);
2601
+
2602
+ CREATE INDEX IF NOT EXISTS idx_agent_semantic_labels_event
2603
+ ON agent_semantic_labels(event_id);
2604
+
2605
+ CREATE TABLE IF NOT EXISTS agent_reflection_checkpoints (
2606
+ id TEXT PRIMARY KEY,
2607
+ project_name TEXT,
2608
+ session_id TEXT,
2609
+ window_start_at TEXT NOT NULL,
2610
+ window_end_at TEXT NOT NULL,
2611
+ event_count INTEGER NOT NULL DEFAULT 0,
2612
+ goal_count INTEGER NOT NULL DEFAULT 0,
2613
+ success_count INTEGER NOT NULL DEFAULT 0,
2614
+ failure_count INTEGER NOT NULL DEFAULT 0,
2615
+ risk_count INTEGER NOT NULL DEFAULT 0,
2616
+ summary TEXT NOT NULL,
2617
+ learnings TEXT NOT NULL DEFAULT '[]',
2618
+ next_actions TEXT NOT NULL DEFAULT '[]',
2619
+ evidence_event_ids TEXT NOT NULL DEFAULT '[]',
2620
+ confidence REAL NOT NULL DEFAULT 0,
2621
+ created_at TEXT NOT NULL
2622
+ );
2623
+
2624
+ CREATE INDEX IF NOT EXISTS idx_agent_reflection_project_time
2625
+ ON agent_reflection_checkpoints(project_name, window_end_at);
2626
+
2627
+ CREATE INDEX IF NOT EXISTS idx_agent_reflection_session_time
2628
+ ON agent_reflection_checkpoints(session_id, window_end_at);
2629
+ `);
2506
2630
  try {
2507
2631
  await client.execute({
2508
2632
  sql: `ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3`,
@@ -2881,12 +3005,13 @@ var keychain_exports = {};
2881
3005
  __export(keychain_exports, {
2882
3006
  deleteMasterKey: () => deleteMasterKey,
2883
3007
  exportMnemonic: () => exportMnemonic,
3008
+ getKeyStorageInfo: () => getKeyStorageInfo,
2884
3009
  getMasterKey: () => getMasterKey,
2885
3010
  importMnemonic: () => importMnemonic,
2886
3011
  setMasterKey: () => setMasterKey
2887
3012
  });
2888
3013
  import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
2889
- import { existsSync as existsSync8 } from "fs";
3014
+ import { existsSync as existsSync8, statSync as statSync2 } from "fs";
2890
3015
  import { execSync as execSync2 } from "child_process";
2891
3016
  import path8 from "path";
2892
3017
  import os6 from "os";
@@ -2896,29 +3021,65 @@ function getKeyDir() {
2896
3021
  function getKeyPath() {
2897
3022
  return path8.join(getKeyDir(), "master.key");
2898
3023
  }
2899
- function macKeychainGet() {
3024
+ function nativeKeychainAllowed() {
3025
+ return process.env.EXE_OS_DISABLE_NATIVE_KEYCHAIN !== "1";
3026
+ }
3027
+ function linuxSecretAvailable() {
3028
+ if (!nativeKeychainAllowed()) return false;
3029
+ if (process.platform !== "linux") return false;
3030
+ if (linuxSecretAvailability !== null) return linuxSecretAvailability;
3031
+ try {
3032
+ execSync2("command -v secret-tool >/dev/null 2>&1", { timeout: 1e3 });
3033
+ } catch {
3034
+ linuxSecretAvailability = false;
3035
+ return false;
3036
+ }
3037
+ try {
3038
+ execSync2("secret-tool search --all exe-os probe >/dev/null 2>&1", { timeout: 1e3 });
3039
+ linuxSecretAvailability = true;
3040
+ } catch {
3041
+ linuxSecretAvailability = false;
3042
+ }
3043
+ return linuxSecretAvailability;
3044
+ }
3045
+ function isRootOnlyTrustedServerKeyFile(keyPath) {
3046
+ if (process.platform !== "linux") return false;
3047
+ try {
3048
+ const uid = typeof os6.userInfo().uid === "number" ? os6.userInfo().uid : -1;
3049
+ const st = statSync2(keyPath);
3050
+ if (!st.isFile() || (st.mode & 63) !== 0) return false;
3051
+ if (uid === 0) return true;
3052
+ const exeOsDir = process.env.EXE_OS_DIR;
3053
+ return Boolean(exeOsDir && path8.resolve(keyPath).startsWith(path8.resolve(exeOsDir) + path8.sep));
3054
+ } catch {
3055
+ return false;
3056
+ }
3057
+ }
3058
+ function macKeychainGet(service = SERVICE) {
3059
+ if (!nativeKeychainAllowed()) return null;
2900
3060
  if (process.platform !== "darwin") return null;
2901
3061
  try {
2902
3062
  return execSync2(
2903
- `security find-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w 2>/dev/null`,
3063
+ `security find-generic-password -s "${service}" -a "${ACCOUNT}" -w 2>/dev/null`,
2904
3064
  { encoding: "utf-8", timeout: 5e3 }
2905
3065
  ).trim();
2906
3066
  } catch {
2907
3067
  return null;
2908
3068
  }
2909
3069
  }
2910
- function macKeychainSet(value) {
3070
+ function macKeychainSet(value, service = SERVICE) {
3071
+ if (!nativeKeychainAllowed()) return false;
2911
3072
  if (process.platform !== "darwin") return false;
2912
3073
  try {
2913
3074
  try {
2914
3075
  execSync2(
2915
- `security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`,
3076
+ `security delete-generic-password -s "${service}" -a "${ACCOUNT}" 2>/dev/null`,
2916
3077
  { timeout: 5e3 }
2917
3078
  );
2918
3079
  } catch {
2919
3080
  }
2920
3081
  execSync2(
2921
- `security add-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w "${value}"`,
3082
+ `security add-generic-password -s "${service}" -a "${ACCOUNT}" -w "${value}"`,
2922
3083
  { timeout: 5e3 }
2923
3084
  );
2924
3085
  return true;
@@ -2926,11 +3087,12 @@ function macKeychainSet(value) {
2926
3087
  return false;
2927
3088
  }
2928
3089
  }
2929
- function macKeychainDelete() {
3090
+ function macKeychainDelete(service = SERVICE) {
3091
+ if (!nativeKeychainAllowed()) return false;
2930
3092
  if (process.platform !== "darwin") return false;
2931
3093
  try {
2932
3094
  execSync2(
2933
- `security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`,
3095
+ `security delete-generic-password -s "${service}" -a "${ACCOUNT}" 2>/dev/null`,
2934
3096
  { timeout: 5e3 }
2935
3097
  );
2936
3098
  return true;
@@ -2938,22 +3100,22 @@ function macKeychainDelete() {
2938
3100
  return false;
2939
3101
  }
2940
3102
  }
2941
- function linuxSecretGet() {
2942
- if (process.platform !== "linux") return null;
3103
+ function linuxSecretGet(service = SERVICE) {
3104
+ if (!linuxSecretAvailable()) return null;
2943
3105
  try {
2944
3106
  return execSync2(
2945
- `secret-tool lookup service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
3107
+ `secret-tool lookup service "${service}" account "${ACCOUNT}" 2>/dev/null`,
2946
3108
  { encoding: "utf-8", timeout: 5e3 }
2947
3109
  ).trim();
2948
3110
  } catch {
2949
3111
  return null;
2950
3112
  }
2951
3113
  }
2952
- function linuxSecretSet(value) {
2953
- if (process.platform !== "linux") return false;
3114
+ function linuxSecretSet(value, service = SERVICE) {
3115
+ if (!linuxSecretAvailable()) return false;
2954
3116
  try {
2955
3117
  execSync2(
2956
- `echo -n "${value}" | secret-tool store --label="exe-os master key" service "${SERVICE}" account "${ACCOUNT}"`,
3118
+ `echo -n "${value}" | secret-tool store --label="exe-os master key" service "${service}" account "${ACCOUNT}" 2>/dev/null`,
2957
3119
  { timeout: 5e3 }
2958
3120
  );
2959
3121
  return true;
@@ -2961,11 +3123,12 @@ function linuxSecretSet(value) {
2961
3123
  return false;
2962
3124
  }
2963
3125
  }
2964
- function linuxSecretDelete() {
3126
+ function linuxSecretDelete(service = SERVICE) {
3127
+ if (!nativeKeychainAllowed()) return false;
2965
3128
  if (process.platform !== "linux") return false;
2966
3129
  try {
2967
3130
  execSync2(
2968
- `secret-tool clear service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
3131
+ `secret-tool clear service "${service}" account "${ACCOUNT}" 2>/dev/null`,
2969
3132
  { timeout: 5e3 }
2970
3133
  );
2971
3134
  return true;
@@ -2974,6 +3137,7 @@ function linuxSecretDelete() {
2974
3137
  }
2975
3138
  }
2976
3139
  async function tryKeytar() {
3140
+ if (!nativeKeychainAllowed()) return null;
2977
3141
  try {
2978
3142
  return await import("keytar");
2979
3143
  } catch {
@@ -3047,7 +3211,19 @@ async function writeMachineBoundFileFallback(b64) {
3047
3211
  return "plaintext";
3048
3212
  }
3049
3213
  async function getMasterKey() {
3050
- const nativeValue = macKeychainGet() ?? linuxSecretGet();
3214
+ let nativeValue = macKeychainGet() ?? linuxSecretGet();
3215
+ if (!nativeValue) {
3216
+ const legacyValue = macKeychainGet(LEGACY_SERVICE) ?? linuxSecretGet(LEGACY_SERVICE);
3217
+ if (legacyValue) {
3218
+ const migrated = macKeychainSet(legacyValue) || linuxSecretSet(legacyValue);
3219
+ if (migrated) {
3220
+ macKeychainDelete(LEGACY_SERVICE);
3221
+ linuxSecretDelete(LEGACY_SERVICE);
3222
+ process.stderr.write("[keychain] Migrated keychain service from exe-mem to exe-os.\n");
3223
+ }
3224
+ nativeValue = legacyValue;
3225
+ }
3226
+ }
3051
3227
  if (nativeValue) {
3052
3228
  return Buffer.from(nativeValue, "base64");
3053
3229
  }
@@ -3055,12 +3231,17 @@ async function getMasterKey() {
3055
3231
  if (keytar) {
3056
3232
  try {
3057
3233
  const keytarValue = await keytar.getPassword(SERVICE, ACCOUNT);
3058
- if (keytarValue) {
3059
- const migrated = macKeychainSet(keytarValue) || linuxSecretSet(keytarValue);
3234
+ const legacyKeytarValue = keytarValue ?? await keytar.getPassword(LEGACY_SERVICE, ACCOUNT);
3235
+ if (legacyKeytarValue) {
3236
+ const migrated = macKeychainSet(legacyKeytarValue) || linuxSecretSet(legacyKeytarValue);
3060
3237
  if (migrated) {
3061
3238
  process.stderr.write("[keychain] Migrated key from keytar to native keychain.\n");
3239
+ try {
3240
+ await keytar.deletePassword(LEGACY_SERVICE, ACCOUNT);
3241
+ } catch {
3242
+ }
3062
3243
  }
3063
- return Buffer.from(keytarValue, "base64");
3244
+ return Buffer.from(legacyKeytarValue, "base64");
3064
3245
  }
3065
3246
  } catch {
3066
3247
  }
@@ -3085,7 +3266,7 @@ async function getMasterKey() {
3085
3266
  const decrypted = decryptWithMachineKey(content, machineKey);
3086
3267
  if (!decrypted) {
3087
3268
  process.stderr.write(
3088
- "[keychain] Key decryption failed \u2014 machine may have changed.\n Use your 24-word recovery phrase: exe-os link import\n"
3269
+ "[keychain] Key decryption failed \u2014 machine may have changed.\n Use your 24-word recovery phrase during setup: exe-os setup\n"
3089
3270
  );
3090
3271
  return null;
3091
3272
  }
@@ -3094,6 +3275,9 @@ async function getMasterKey() {
3094
3275
  b64Value = content;
3095
3276
  }
3096
3277
  const key = Buffer.from(b64Value, "base64");
3278
+ if (!content.startsWith(ENCRYPTED_PREFIX) && isRootOnlyTrustedServerKeyFile(keyPath)) {
3279
+ return key;
3280
+ }
3097
3281
  const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
3098
3282
  if (migrated) {
3099
3283
  process.stderr.write("[keychain] Migrated key from file to native keychain.\n");
@@ -3121,6 +3305,97 @@ async function getMasterKey() {
3121
3305
  return null;
3122
3306
  }
3123
3307
  }
3308
+ async function getKeyStorageInfo() {
3309
+ if (macKeychainGet()) {
3310
+ return {
3311
+ kind: "macos-keychain",
3312
+ secure: true,
3313
+ note: "stored in macOS Keychain via built-in security CLI"
3314
+ };
3315
+ }
3316
+ if (macKeychainGet(LEGACY_SERVICE)) {
3317
+ return {
3318
+ kind: "macos-keychain",
3319
+ secure: true,
3320
+ note: "stored in legacy macOS Keychain service exe-mem; next key read migrates it to exe-os"
3321
+ };
3322
+ }
3323
+ if (linuxSecretGet()) {
3324
+ return {
3325
+ kind: "linux-secret-service",
3326
+ secure: true,
3327
+ note: "stored in Linux Secret Service via secret-tool"
3328
+ };
3329
+ }
3330
+ if (linuxSecretGet(LEGACY_SERVICE)) {
3331
+ return {
3332
+ kind: "linux-secret-service",
3333
+ secure: true,
3334
+ note: "stored in legacy Linux Secret Service service exe-mem; next key read migrates it to exe-os"
3335
+ };
3336
+ }
3337
+ const keytar = await tryKeytar();
3338
+ if (keytar) {
3339
+ try {
3340
+ if (await keytar.getPassword(SERVICE, ACCOUNT)) {
3341
+ return {
3342
+ kind: "legacy-keytar",
3343
+ secure: true,
3344
+ note: "stored in legacy keytar backend; will migrate to native keychain when possible"
3345
+ };
3346
+ }
3347
+ if (await keytar.getPassword(LEGACY_SERVICE, ACCOUNT)) {
3348
+ return {
3349
+ kind: "legacy-keytar",
3350
+ secure: true,
3351
+ note: "stored in legacy keytar service exe-mem; will migrate to native exe-os keychain when possible"
3352
+ };
3353
+ }
3354
+ } catch {
3355
+ }
3356
+ }
3357
+ const keyPath = getKeyPath();
3358
+ if (!existsSync8(keyPath)) {
3359
+ return {
3360
+ kind: "missing",
3361
+ secure: false,
3362
+ path: keyPath,
3363
+ note: "no key found in OS keychain, legacy keytar, or file fallback"
3364
+ };
3365
+ }
3366
+ try {
3367
+ const content = (await readFile3(keyPath, "utf-8")).trim();
3368
+ if (content.startsWith(ENCRYPTED_PREFIX)) {
3369
+ return {
3370
+ kind: "encrypted-file",
3371
+ secure: true,
3372
+ path: keyPath,
3373
+ note: "stored in machine-bound encrypted file fallback"
3374
+ };
3375
+ }
3376
+ if (isRootOnlyTrustedServerKeyFile(keyPath)) {
3377
+ return {
3378
+ kind: "server-secret-file",
3379
+ secure: true,
3380
+ path: keyPath,
3381
+ note: "stored as root-only trusted server secret file"
3382
+ };
3383
+ }
3384
+ return {
3385
+ kind: "plaintext-file",
3386
+ secure: false,
3387
+ path: keyPath,
3388
+ note: "stored in legacy plaintext file; reading it will migrate or encrypt it"
3389
+ };
3390
+ } catch {
3391
+ return {
3392
+ kind: "missing",
3393
+ secure: false,
3394
+ path: keyPath,
3395
+ note: "key file exists but could not be read"
3396
+ };
3397
+ }
3398
+ }
3124
3399
  async function setMasterKey(key) {
3125
3400
  const b64 = key.toString("base64");
3126
3401
  if (macKeychainSet(b64) || linuxSecretSet(b64)) {
@@ -3146,10 +3421,13 @@ async function setMasterKey(key) {
3146
3421
  async function deleteMasterKey() {
3147
3422
  macKeychainDelete();
3148
3423
  linuxSecretDelete();
3424
+ macKeychainDelete(LEGACY_SERVICE);
3425
+ linuxSecretDelete(LEGACY_SERVICE);
3149
3426
  const keytar = await tryKeytar();
3150
3427
  if (keytar) {
3151
3428
  try {
3152
3429
  await keytar.deletePassword(SERVICE, ACCOUNT);
3430
+ await keytar.deletePassword(LEGACY_SERVICE, ACCOUNT);
3153
3431
  } catch {
3154
3432
  }
3155
3433
  }
@@ -3187,12 +3465,14 @@ async function importMnemonic(mnemonic) {
3187
3465
  const entropy = mnemonicToEntropy(trimmed);
3188
3466
  return Buffer.from(entropy, "hex");
3189
3467
  }
3190
- var SERVICE, ACCOUNT, ENCRYPTED_PREFIX;
3468
+ var SERVICE, LEGACY_SERVICE, ACCOUNT, linuxSecretAvailability, ENCRYPTED_PREFIX;
3191
3469
  var init_keychain = __esm({
3192
3470
  "src/lib/keychain.ts"() {
3193
3471
  "use strict";
3194
- SERVICE = "exe-mem";
3472
+ SERVICE = "exe-os";
3473
+ LEGACY_SERVICE = "exe-mem";
3195
3474
  ACCOUNT = "master-key";
3475
+ linuxSecretAvailability = null;
3196
3476
  ENCRYPTED_PREFIX = "enc:";
3197
3477
  }
3198
3478
  });
@@ -3208,7 +3488,7 @@ __export(db_backup_exports, {
3208
3488
  listBackups: () => listBackups,
3209
3489
  rotateBackups: () => rotateBackups
3210
3490
  });
3211
- import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync, unlinkSync as unlinkSync4, statSync as statSync2 } from "fs";
3491
+ import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync, unlinkSync as unlinkSync4, statSync as statSync3 } from "fs";
3212
3492
  import path9 from "path";
3213
3493
  function findActiveDb() {
3214
3494
  for (const name of DB_NAMES) {
@@ -3252,7 +3532,7 @@ function rotateBackups(keepDays = DEFAULT_KEEP_DAYS) {
3252
3532
  if (!file.endsWith(".db") && !file.endsWith(".db-wal") && !file.endsWith(".db-shm")) continue;
3253
3533
  const filePath = path9.join(BACKUP_DIR, file);
3254
3534
  try {
3255
- const stat = statSync2(filePath);
3535
+ const stat = statSync3(filePath);
3256
3536
  if (stat.mtimeMs < cutoff) {
3257
3537
  unlinkSync4(filePath);
3258
3538
  deleted++;
@@ -3270,7 +3550,7 @@ function listBackups() {
3270
3550
  const files = readdirSync(BACKUP_DIR).filter((f) => f.endsWith(".db") && !f.endsWith("-wal") && !f.endsWith("-shm"));
3271
3551
  return files.map((name) => {
3272
3552
  const p = path9.join(BACKUP_DIR, name);
3273
- const stat = statSync2(p);
3553
+ const stat = statSync3(p);
3274
3554
  return { path: p, name, size: stat.size, date: stat.mtime };
3275
3555
  }).sort((a, b) => b.date.getTime() - a.date.getTime());
3276
3556
  } catch {
@@ -3302,7 +3582,7 @@ var init_db_backup = __esm({
3302
3582
 
3303
3583
  // src/lib/cloud-sync.ts
3304
3584
  init_database();
3305
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync10, readdirSync as readdirSync2, mkdirSync as mkdirSync5, appendFileSync, unlinkSync as unlinkSync5, openSync as openSync2, closeSync as closeSync2, statSync as statSync3 } from "fs";
3585
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync10, readdirSync as readdirSync2, mkdirSync as mkdirSync5, appendFileSync, unlinkSync as unlinkSync5, openSync as openSync2, closeSync as closeSync2, statSync as statSync4 } from "fs";
3306
3586
  import crypto3 from "crypto";
3307
3587
  import path10 from "path";
3308
3588
  import { homedir as homedir2 } from "os";
@@ -3457,18 +3737,36 @@ function loadPgClient() {
3457
3737
  const { pathToFileURL: pathToFileURL3 } = await import("url");
3458
3738
  const explicitPath = process.env.EXE_OS_PRISMA_CLIENT_PATH;
3459
3739
  if (explicitPath) {
3460
- const mod2 = await import(pathToFileURL3(explicitPath).href);
3461
- const Ctor2 = mod2.PrismaClient ?? mod2.default?.PrismaClient;
3462
- if (!Ctor2) throw new Error(`No PrismaClient at ${explicitPath}`);
3463
- return new Ctor2();
3740
+ const mod = await import(pathToFileURL3(explicitPath).href);
3741
+ const Ctor = mod.PrismaClient ?? mod.default?.PrismaClient;
3742
+ if (!Ctor) throw new Error(`No PrismaClient at ${explicitPath}`);
3743
+ return new Ctor();
3464
3744
  }
3465
3745
  const exeDbRoot = process.env.EXE_DB_ROOT ?? path10.join(homedir2(), "exe-db");
3466
- const req = createRequire3(path10.join(exeDbRoot, "package.json"));
3467
- const entry = req.resolve("@prisma/client");
3468
- const mod = await import(pathToFileURL3(entry).href);
3469
- const Ctor = mod.PrismaClient ?? mod.default?.PrismaClient;
3470
- if (!Ctor) throw new Error("No PrismaClient");
3471
- return new Ctor();
3746
+ const packagePath = path10.join(exeDbRoot, "package.json");
3747
+ if (existsSync10(packagePath)) {
3748
+ const req = createRequire3(packagePath);
3749
+ const entry = req.resolve("@prisma/client");
3750
+ const mod = await import(pathToFileURL3(entry).href);
3751
+ const Ctor = mod.PrismaClient ?? mod.default?.PrismaClient;
3752
+ if (!Ctor) throw new Error("No PrismaClient");
3753
+ return new Ctor();
3754
+ }
3755
+ const { Pool } = await import("pg");
3756
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
3757
+ return {
3758
+ async $queryRawUnsafe(query, ...values) {
3759
+ const result = await pool.query(query, values);
3760
+ return result.rows;
3761
+ },
3762
+ async $executeRawUnsafe(query, ...values) {
3763
+ const result = await pool.query(query, values);
3764
+ return result.rowCount ?? 0;
3765
+ },
3766
+ async $disconnect() {
3767
+ await pool.end();
3768
+ }
3769
+ };
3472
3770
  })().catch(() => {
3473
3771
  _pgFailed = true;
3474
3772
  _pgPromise = null;
@@ -3489,7 +3787,7 @@ async function pushToPostgres(records) {
3489
3787
  let inserted = 0;
3490
3788
  for (const rec of records) {
3491
3789
  try {
3492
- await prisma.$executeRawUnsafe(
3790
+ const changed = await prisma.$executeRawUnsafe(
3493
3791
  `INSERT INTO raw.raw_events (id, source, source_id, event_type, payload, metadata, timestamp)
3494
3792
  VALUES (gen_random_uuid(), 'cloud_sync', $1, 'memory', $2::jsonb, $3::jsonb, $4)
3495
3793
  ON CONFLICT (source, source_id, event_type) DO NOTHING`,
@@ -3498,7 +3796,7 @@ async function pushToPostgres(records) {
3498
3796
  JSON.stringify({ agent_id: rec.agent_id, project_name: rec.project_name, tool_name: rec.tool_name }),
3499
3797
  rec.timestamp ? new Date(String(rec.timestamp)) : /* @__PURE__ */ new Date()
3500
3798
  );
3501
- inserted++;
3799
+ inserted += Number(changed ?? 0);
3502
3800
  } catch {
3503
3801
  }
3504
3802
  }
@@ -3603,6 +3901,23 @@ async function cloudPush(records, maxVersion, config) {
3603
3901
  return false;
3604
3902
  }
3605
3903
  }
3904
+ async function cloudResetMemoryBlobs(config) {
3905
+ assertSecureEndpoint(config.endpoint);
3906
+ const resp = await fetchWithRetry(`${config.endpoint}/sync/reset-memory`, {
3907
+ method: "POST",
3908
+ headers: {
3909
+ Authorization: `Bearer ${config.apiKey}`,
3910
+ "Content-Type": "application/json",
3911
+ "X-Device-Id": loadDeviceId()
3912
+ },
3913
+ body: JSON.stringify({ confirm: "LOCAL DB IS SOURCE OF TRUTH" })
3914
+ });
3915
+ if (!resp.ok) {
3916
+ throw new Error(`cloud reset failed: HTTP ${resp.status}`);
3917
+ }
3918
+ const data = await resp.json();
3919
+ return { deleted: Number(data.deleted ?? 0), freedBytes: Number(data.freed_bytes ?? 0) };
3920
+ }
3606
3921
  async function cloudPull(sinceVersion, config) {
3607
3922
  assertSecureEndpoint(config.endpoint);
3608
3923
  try {
@@ -3622,41 +3937,62 @@ async function cloudPull(sinceVersion, config) {
3622
3937
  if (!response.ok) return { records: [], maxVersion: sinceVersion };
3623
3938
  const data = await response.json();
3624
3939
  const allRecords = [];
3625
- for (const { blob } of data.blobs ?? []) {
3940
+ let maxReadableVersion = sinceVersion;
3941
+ let skippedBlobs = 0;
3942
+ for (const { version, blob } of data.blobs ?? []) {
3626
3943
  try {
3627
3944
  const compressed = decryptSyncBlob(blob);
3628
3945
  const json = decompress(compressed).toString("utf8");
3629
3946
  const records = JSON.parse(json);
3630
3947
  allRecords.push(...records);
3948
+ const recordMax = records.reduce((max, rec) => {
3949
+ const v = Number(rec.version ?? 0);
3950
+ return Number.isFinite(v) ? Math.max(max, v) : max;
3951
+ }, 0);
3952
+ const blobVersion = Number(version ?? 0);
3953
+ maxReadableVersion = Math.max(
3954
+ maxReadableVersion,
3955
+ Number.isFinite(blobVersion) ? blobVersion : 0,
3956
+ recordMax
3957
+ );
3631
3958
  } catch {
3959
+ skippedBlobs++;
3632
3960
  continue;
3633
3961
  }
3634
3962
  }
3635
- return { records: allRecords, maxVersion: data.max_version ?? sinceVersion };
3963
+ if (skippedBlobs > 0) {
3964
+ logError(`[cloud-sync] PULL skipped ${skippedBlobs} undecryptable blob(s); pull cursor advanced only to last readable version ${maxReadableVersion}`);
3965
+ }
3966
+ return { records: allRecords, maxVersion: maxReadableVersion };
3636
3967
  } catch (err) {
3637
3968
  logError(`[cloud-sync] PULL FAILED: ${err instanceof Error ? err.message : String(err)}`);
3638
3969
  return { records: [], maxVersion: sinceVersion };
3639
3970
  }
3640
3971
  }
3641
- var CLOUD_RELINK_REQUIRED_MESSAGE = "[cloud-sync] Paused after key rotation. Run `exe-os cloud relink --dry-run` for the safe relink checklist.";
3642
- async function getCloudRelinkRequired(client = getClient()) {
3972
+ var CLOUD_REUPLOAD_REQUIRED_MESSAGE = "Cloud sync is blocked because this device rotated its memory encryption key. Run `exe-os cloud reupload` first to re-upload the cloud backup with the new key.";
3973
+ async function getCloudReuploadRequired(client = getClient()) {
3643
3974
  try {
3644
3975
  await client.execute("CREATE TABLE IF NOT EXISTS sync_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
3645
- const relink = await client.execute("SELECT value FROM sync_meta WHERE key = 'cloud_relink_required' LIMIT 1");
3646
- return String(relink.rows[0]?.value ?? "") === "1";
3976
+ const result = await client.execute("SELECT key, value FROM sync_meta WHERE key IN ('cloud_reupload_required', 'cloud_relink_required')");
3977
+ return result.rows.some((row) => String(row.value ?? "") === "1");
3647
3978
  } catch {
3648
3979
  return false;
3649
3980
  }
3650
3981
  }
3651
- async function clearCloudRelinkRequired(client = getClient()) {
3982
+ async function clearCloudReuploadRequired(client = getClient()) {
3652
3983
  await client.execute("CREATE TABLE IF NOT EXISTS sync_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
3984
+ await client.execute("INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('cloud_reupload_required', '0')");
3653
3985
  await client.execute("INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('cloud_relink_required', '0')");
3654
3986
  await client.execute({
3655
- sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('cloud_relinked_at', ?)",
3987
+ sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('cloud_reuploaded_at', ?)",
3656
3988
  args: [(/* @__PURE__ */ new Date()).toISOString()]
3657
3989
  });
3658
3990
  await client.execute("DELETE FROM sync_meta WHERE key IN ('last_cloud_pull_version', 'last_cloud_push_version')");
3659
3991
  }
3992
+ async function markCloudReuploadRequired(client = getClient()) {
3993
+ await client.execute("CREATE TABLE IF NOT EXISTS sync_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
3994
+ await client.execute("INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('cloud_reupload_required', '1')");
3995
+ }
3660
3996
  async function cloudSync(config) {
3661
3997
  if (!isSyncCryptoInitialized()) {
3662
3998
  try {
@@ -3678,10 +4014,10 @@ async function cloudSync(config) {
3678
4014
  throw new Error("[cloud-sync] Database not initialized. Call initStore() before cloudSync().");
3679
4015
  }
3680
4016
  try {
3681
- if (await getCloudRelinkRequired(client)) throw new Error(CLOUD_RELINK_REQUIRED_MESSAGE);
4017
+ if (await getCloudReuploadRequired(client)) throw new Error(CLOUD_REUPLOAD_REQUIRED_MESSAGE);
3682
4018
  } catch (err) {
3683
4019
  const msg = err instanceof Error ? err.message : String(err);
3684
- if (msg.includes("Paused after key rotation")) throw err;
4020
+ if (msg === CLOUD_REUPLOAD_REQUIRED_MESSAGE || msg.includes("key rotation")) throw err;
3685
4021
  }
3686
4022
  try {
3687
4023
  const { getRawClient: getRawClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
@@ -3939,7 +4275,7 @@ async function cloudSync(config) {
3939
4275
  const { getLatestBackup: getLatestBackup2 } = await Promise.resolve().then(() => (init_db_backup(), db_backup_exports));
3940
4276
  const latestBackup = getLatestBackup2();
3941
4277
  if (latestBackup) {
3942
- const backupSize = statSync3(latestBackup).size;
4278
+ const backupSize = statSync4(latestBackup).size;
3943
4279
  const MAX_CLOUD_BACKUP_BYTES = 50 * 1024 * 1024;
3944
4280
  if (backupSize <= MAX_CLOUD_BACKUP_BYTES) {
3945
4281
  const backupData = readFileSync7(latestBackup);
@@ -4612,10 +4948,10 @@ async function cloudPullDocuments(config) {
4612
4948
  return { pulled };
4613
4949
  }
4614
4950
  export {
4615
- CLOUD_RELINK_REQUIRED_MESSAGE,
4951
+ CLOUD_REUPLOAD_REQUIRED_MESSAGE,
4616
4952
  assertSecureEndpoint,
4617
4953
  buildRosterBlob,
4618
- clearCloudRelinkRequired,
4954
+ clearCloudReuploadRequired,
4619
4955
  cloudPull,
4620
4956
  cloudPullBehaviors,
4621
4957
  cloudPullBlob,
@@ -4634,8 +4970,10 @@ export {
4634
4970
  cloudPushGraphRAG,
4635
4971
  cloudPushRoster,
4636
4972
  cloudPushTasks,
4973
+ cloudResetMemoryBlobs,
4637
4974
  cloudSync,
4638
- getCloudRelinkRequired,
4975
+ getCloudReuploadRequired,
4976
+ markCloudReuploadRequired,
4639
4977
  mergeConfig,
4640
4978
  mergeRosterFromRemote,
4641
4979
  pushToPostgres,