@askexenow/exe-os 0.9.65 → 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 (113) 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} +1532 -2173
  6. package/dist/bin/backfill-conversations.js +528 -20
  7. package/dist/bin/backfill-responses.js +528 -20
  8. package/dist/bin/backfill-vectors.js +255 -20
  9. package/dist/bin/bulk-sync-postgres.js +4876 -0
  10. package/dist/bin/cleanup-stale-review-tasks.js +529 -21
  11. package/dist/bin/cli.js +3471 -1491
  12. package/dist/bin/exe-agent-config.js +4 -0
  13. package/dist/bin/exe-agent.js +16 -0
  14. package/dist/bin/exe-assign.js +528 -20
  15. package/dist/bin/exe-boot.js +492 -54
  16. package/dist/bin/exe-call.js +16 -0
  17. package/dist/bin/exe-cloud.js +7415 -518
  18. package/dist/bin/exe-dispatch.js +540 -22
  19. package/dist/bin/exe-doctor.js +3404 -1225
  20. package/dist/bin/exe-export-behaviors.js +542 -24
  21. package/dist/bin/exe-forget.js +529 -21
  22. package/dist/bin/exe-gateway.js +595 -25
  23. package/dist/bin/exe-heartbeat.js +541 -24
  24. package/dist/bin/exe-kill.js +529 -21
  25. package/dist/bin/exe-launch-agent.js +2334 -1067
  26. package/dist/bin/exe-new-employee.js +324 -166
  27. package/dist/bin/exe-pending-messages.js +529 -21
  28. package/dist/bin/exe-pending-notifications.js +529 -21
  29. package/dist/bin/exe-pending-reviews.js +529 -21
  30. package/dist/bin/exe-rename.js +529 -21
  31. package/dist/bin/exe-review.js +529 -21
  32. package/dist/bin/exe-search.js +542 -24
  33. package/dist/bin/exe-session-cleanup.js +540 -22
  34. package/dist/bin/exe-settings.js +14 -0
  35. package/dist/bin/exe-start-codex.js +817 -144
  36. package/dist/bin/exe-start-opencode.js +776 -80
  37. package/dist/bin/exe-status.js +529 -21
  38. package/dist/bin/exe-team.js +529 -21
  39. package/dist/bin/git-sweep.js +540 -22
  40. package/dist/bin/graph-backfill.js +580 -21
  41. package/dist/bin/graph-export.js +529 -21
  42. package/dist/bin/graph-layer-benchmark.js +109 -0
  43. package/dist/bin/install.js +420 -289
  44. package/dist/bin/intercom-check.js +540 -22
  45. package/dist/bin/postgres-agentic-reflection-backfill.js +187 -0
  46. package/dist/bin/postgres-agentic-semantic-backfill.js +237 -0
  47. package/dist/bin/scan-tasks.js +540 -22
  48. package/dist/bin/setup.js +790 -206
  49. package/dist/bin/shard-migrate.js +528 -20
  50. package/dist/bin/update.js +4 -0
  51. package/dist/gateway/index.js +593 -23
  52. package/dist/hooks/bug-report-worker.js +651 -64
  53. package/dist/hooks/codex-stop-task-finalizer.js +540 -22
  54. package/dist/hooks/commit-complete.js +540 -22
  55. package/dist/hooks/error-recall.js +542 -24
  56. package/dist/hooks/exe-heartbeat-hook.js +4 -0
  57. package/dist/hooks/ingest-worker.js +4 -0
  58. package/dist/hooks/ingest.js +539 -22
  59. package/dist/hooks/instructions-loaded.js +529 -21
  60. package/dist/hooks/notification.js +529 -21
  61. package/dist/hooks/post-compact.js +529 -21
  62. package/dist/hooks/post-tool-combined.js +543 -25
  63. package/dist/hooks/pre-compact.js +772 -127
  64. package/dist/hooks/pre-tool-use.js +529 -21
  65. package/dist/hooks/prompt-submit.js +543 -25
  66. package/dist/hooks/session-end.js +673 -140
  67. package/dist/hooks/session-start.js +662 -26
  68. package/dist/hooks/stop.js +540 -23
  69. package/dist/hooks/subagent-stop.js +529 -21
  70. package/dist/hooks/summary-worker.js +571 -126
  71. package/dist/index.js +593 -23
  72. package/dist/lib/agent-config.js +4 -0
  73. package/dist/lib/cloud-sync.js +408 -47
  74. package/dist/lib/config.js +25 -1
  75. package/dist/lib/consolidation.js +5 -1
  76. package/dist/lib/database.js +128 -0
  77. package/dist/lib/db-daemon-client.js +4 -0
  78. package/dist/lib/db.js +128 -0
  79. package/dist/lib/device-registry.js +128 -0
  80. package/dist/lib/embedder.js +25 -1
  81. package/dist/lib/employee-templates.js +16 -0
  82. package/dist/lib/employees.js +4 -0
  83. package/dist/lib/exe-daemon-client.js +4 -0
  84. package/dist/lib/exe-daemon.js +3158 -930
  85. package/dist/lib/hybrid-search.js +542 -24
  86. package/dist/lib/identity.js +7 -0
  87. package/dist/lib/keychain.js +178 -22
  88. package/dist/lib/license.js +4 -0
  89. package/dist/lib/messaging.js +7 -0
  90. package/dist/lib/reminders.js +7 -0
  91. package/dist/lib/schedules.js +255 -20
  92. package/dist/lib/skill-learning.js +28 -1
  93. package/dist/lib/status-brief.js +39 -0
  94. package/dist/lib/store.js +528 -20
  95. package/dist/lib/task-router.js +4 -0
  96. package/dist/lib/tasks.js +28 -1
  97. package/dist/lib/tmux-routing.js +28 -1
  98. package/dist/lib/token-spend.js +7 -0
  99. package/dist/mcp/server.js +2739 -813
  100. package/dist/mcp/tools/complete-reminder.js +7 -0
  101. package/dist/mcp/tools/create-reminder.js +7 -0
  102. package/dist/mcp/tools/create-task.js +28 -1
  103. package/dist/mcp/tools/deactivate-behavior.js +7 -0
  104. package/dist/mcp/tools/list-reminders.js +7 -0
  105. package/dist/mcp/tools/list-tasks.js +7 -0
  106. package/dist/mcp/tools/send-message.js +7 -0
  107. package/dist/mcp/tools/update-task.js +28 -1
  108. package/dist/runtime/index.js +540 -22
  109. package/dist/tui/App.js +618 -29
  110. package/package.json +9 -5
  111. package/src/commands/exe/cloud.md +11 -8
  112. package/stack.release.json +3 -3
  113. package/src/commands/exe/link.md +0 -17
@@ -181,6 +181,10 @@ var init_config = __esm({
181
181
  checkOnBoot: true,
182
182
  autoInstall: false,
183
183
  checkIntervalMs: 24 * 60 * 60 * 1e3
184
+ },
185
+ orchestration: {
186
+ phase: "phase_1_coo",
187
+ phaseSetBy: "default"
184
188
  }
185
189
  };
186
190
  }
@@ -1467,6 +1471,9 @@ function getClient() {
1467
1471
  if (_daemonClient && _daemonClient._isDaemonActive()) {
1468
1472
  return _daemonClient;
1469
1473
  }
1474
+ if (!_resilientClient) {
1475
+ return _adapterClient;
1476
+ }
1470
1477
  return _resilientClient;
1471
1478
  }
1472
1479
  async function initDaemonClient() {
@@ -2499,6 +2506,127 @@ async function ensureSchema() {
2499
2506
  VALUES (new.rowid, new.content, new.subject, new.predicate, new.object);
2500
2507
  END;
2501
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
+ `);
2502
2630
  try {
2503
2631
  await client.execute({
2504
2632
  sql: `ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3`,
@@ -2877,12 +3005,13 @@ var keychain_exports = {};
2877
3005
  __export(keychain_exports, {
2878
3006
  deleteMasterKey: () => deleteMasterKey,
2879
3007
  exportMnemonic: () => exportMnemonic,
3008
+ getKeyStorageInfo: () => getKeyStorageInfo,
2880
3009
  getMasterKey: () => getMasterKey,
2881
3010
  importMnemonic: () => importMnemonic,
2882
3011
  setMasterKey: () => setMasterKey
2883
3012
  });
2884
3013
  import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
2885
- import { existsSync as existsSync8 } from "fs";
3014
+ import { existsSync as existsSync8, statSync as statSync2 } from "fs";
2886
3015
  import { execSync as execSync2 } from "child_process";
2887
3016
  import path8 from "path";
2888
3017
  import os6 from "os";
@@ -2892,29 +3021,65 @@ function getKeyDir() {
2892
3021
  function getKeyPath() {
2893
3022
  return path8.join(getKeyDir(), "master.key");
2894
3023
  }
2895
- 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;
2896
3060
  if (process.platform !== "darwin") return null;
2897
3061
  try {
2898
3062
  return execSync2(
2899
- `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`,
2900
3064
  { encoding: "utf-8", timeout: 5e3 }
2901
3065
  ).trim();
2902
3066
  } catch {
2903
3067
  return null;
2904
3068
  }
2905
3069
  }
2906
- function macKeychainSet(value) {
3070
+ function macKeychainSet(value, service = SERVICE) {
3071
+ if (!nativeKeychainAllowed()) return false;
2907
3072
  if (process.platform !== "darwin") return false;
2908
3073
  try {
2909
3074
  try {
2910
3075
  execSync2(
2911
- `security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`,
3076
+ `security delete-generic-password -s "${service}" -a "${ACCOUNT}" 2>/dev/null`,
2912
3077
  { timeout: 5e3 }
2913
3078
  );
2914
3079
  } catch {
2915
3080
  }
2916
3081
  execSync2(
2917
- `security add-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w "${value}"`,
3082
+ `security add-generic-password -s "${service}" -a "${ACCOUNT}" -w "${value}"`,
2918
3083
  { timeout: 5e3 }
2919
3084
  );
2920
3085
  return true;
@@ -2922,11 +3087,12 @@ function macKeychainSet(value) {
2922
3087
  return false;
2923
3088
  }
2924
3089
  }
2925
- function macKeychainDelete() {
3090
+ function macKeychainDelete(service = SERVICE) {
3091
+ if (!nativeKeychainAllowed()) return false;
2926
3092
  if (process.platform !== "darwin") return false;
2927
3093
  try {
2928
3094
  execSync2(
2929
- `security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`,
3095
+ `security delete-generic-password -s "${service}" -a "${ACCOUNT}" 2>/dev/null`,
2930
3096
  { timeout: 5e3 }
2931
3097
  );
2932
3098
  return true;
@@ -2934,22 +3100,22 @@ function macKeychainDelete() {
2934
3100
  return false;
2935
3101
  }
2936
3102
  }
2937
- function linuxSecretGet() {
2938
- if (process.platform !== "linux") return null;
3103
+ function linuxSecretGet(service = SERVICE) {
3104
+ if (!linuxSecretAvailable()) return null;
2939
3105
  try {
2940
3106
  return execSync2(
2941
- `secret-tool lookup service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
3107
+ `secret-tool lookup service "${service}" account "${ACCOUNT}" 2>/dev/null`,
2942
3108
  { encoding: "utf-8", timeout: 5e3 }
2943
3109
  ).trim();
2944
3110
  } catch {
2945
3111
  return null;
2946
3112
  }
2947
3113
  }
2948
- function linuxSecretSet(value) {
2949
- if (process.platform !== "linux") return false;
3114
+ function linuxSecretSet(value, service = SERVICE) {
3115
+ if (!linuxSecretAvailable()) return false;
2950
3116
  try {
2951
3117
  execSync2(
2952
- `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`,
2953
3119
  { timeout: 5e3 }
2954
3120
  );
2955
3121
  return true;
@@ -2957,11 +3123,12 @@ function linuxSecretSet(value) {
2957
3123
  return false;
2958
3124
  }
2959
3125
  }
2960
- function linuxSecretDelete() {
3126
+ function linuxSecretDelete(service = SERVICE) {
3127
+ if (!nativeKeychainAllowed()) return false;
2961
3128
  if (process.platform !== "linux") return false;
2962
3129
  try {
2963
3130
  execSync2(
2964
- `secret-tool clear service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
3131
+ `secret-tool clear service "${service}" account "${ACCOUNT}" 2>/dev/null`,
2965
3132
  { timeout: 5e3 }
2966
3133
  );
2967
3134
  return true;
@@ -2970,6 +3137,7 @@ function linuxSecretDelete() {
2970
3137
  }
2971
3138
  }
2972
3139
  async function tryKeytar() {
3140
+ if (!nativeKeychainAllowed()) return null;
2973
3141
  try {
2974
3142
  return await import("keytar");
2975
3143
  } catch {
@@ -3043,7 +3211,19 @@ async function writeMachineBoundFileFallback(b64) {
3043
3211
  return "plaintext";
3044
3212
  }
3045
3213
  async function getMasterKey() {
3046
- 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
+ }
3047
3227
  if (nativeValue) {
3048
3228
  return Buffer.from(nativeValue, "base64");
3049
3229
  }
@@ -3051,12 +3231,17 @@ async function getMasterKey() {
3051
3231
  if (keytar) {
3052
3232
  try {
3053
3233
  const keytarValue = await keytar.getPassword(SERVICE, ACCOUNT);
3054
- if (keytarValue) {
3055
- 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);
3056
3237
  if (migrated) {
3057
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
+ }
3058
3243
  }
3059
- return Buffer.from(keytarValue, "base64");
3244
+ return Buffer.from(legacyKeytarValue, "base64");
3060
3245
  }
3061
3246
  } catch {
3062
3247
  }
@@ -3081,7 +3266,7 @@ async function getMasterKey() {
3081
3266
  const decrypted = decryptWithMachineKey(content, machineKey);
3082
3267
  if (!decrypted) {
3083
3268
  process.stderr.write(
3084
- "[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"
3085
3270
  );
3086
3271
  return null;
3087
3272
  }
@@ -3090,6 +3275,9 @@ async function getMasterKey() {
3090
3275
  b64Value = content;
3091
3276
  }
3092
3277
  const key = Buffer.from(b64Value, "base64");
3278
+ if (!content.startsWith(ENCRYPTED_PREFIX) && isRootOnlyTrustedServerKeyFile(keyPath)) {
3279
+ return key;
3280
+ }
3093
3281
  const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
3094
3282
  if (migrated) {
3095
3283
  process.stderr.write("[keychain] Migrated key from file to native keychain.\n");
@@ -3117,6 +3305,97 @@ async function getMasterKey() {
3117
3305
  return null;
3118
3306
  }
3119
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
+ }
3120
3399
  async function setMasterKey(key) {
3121
3400
  const b64 = key.toString("base64");
3122
3401
  if (macKeychainSet(b64) || linuxSecretSet(b64)) {
@@ -3142,10 +3421,13 @@ async function setMasterKey(key) {
3142
3421
  async function deleteMasterKey() {
3143
3422
  macKeychainDelete();
3144
3423
  linuxSecretDelete();
3424
+ macKeychainDelete(LEGACY_SERVICE);
3425
+ linuxSecretDelete(LEGACY_SERVICE);
3145
3426
  const keytar = await tryKeytar();
3146
3427
  if (keytar) {
3147
3428
  try {
3148
3429
  await keytar.deletePassword(SERVICE, ACCOUNT);
3430
+ await keytar.deletePassword(LEGACY_SERVICE, ACCOUNT);
3149
3431
  } catch {
3150
3432
  }
3151
3433
  }
@@ -3183,12 +3465,14 @@ async function importMnemonic(mnemonic) {
3183
3465
  const entropy = mnemonicToEntropy(trimmed);
3184
3466
  return Buffer.from(entropy, "hex");
3185
3467
  }
3186
- var SERVICE, ACCOUNT, ENCRYPTED_PREFIX;
3468
+ var SERVICE, LEGACY_SERVICE, ACCOUNT, linuxSecretAvailability, ENCRYPTED_PREFIX;
3187
3469
  var init_keychain = __esm({
3188
3470
  "src/lib/keychain.ts"() {
3189
3471
  "use strict";
3190
- SERVICE = "exe-mem";
3472
+ SERVICE = "exe-os";
3473
+ LEGACY_SERVICE = "exe-mem";
3191
3474
  ACCOUNT = "master-key";
3475
+ linuxSecretAvailability = null;
3192
3476
  ENCRYPTED_PREFIX = "enc:";
3193
3477
  }
3194
3478
  });
@@ -3204,7 +3488,7 @@ __export(db_backup_exports, {
3204
3488
  listBackups: () => listBackups,
3205
3489
  rotateBackups: () => rotateBackups
3206
3490
  });
3207
- 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";
3208
3492
  import path9 from "path";
3209
3493
  function findActiveDb() {
3210
3494
  for (const name of DB_NAMES) {
@@ -3248,7 +3532,7 @@ function rotateBackups(keepDays = DEFAULT_KEEP_DAYS) {
3248
3532
  if (!file.endsWith(".db") && !file.endsWith(".db-wal") && !file.endsWith(".db-shm")) continue;
3249
3533
  const filePath = path9.join(BACKUP_DIR, file);
3250
3534
  try {
3251
- const stat = statSync2(filePath);
3535
+ const stat = statSync3(filePath);
3252
3536
  if (stat.mtimeMs < cutoff) {
3253
3537
  unlinkSync4(filePath);
3254
3538
  deleted++;
@@ -3266,7 +3550,7 @@ function listBackups() {
3266
3550
  const files = readdirSync(BACKUP_DIR).filter((f) => f.endsWith(".db") && !f.endsWith("-wal") && !f.endsWith("-shm"));
3267
3551
  return files.map((name) => {
3268
3552
  const p = path9.join(BACKUP_DIR, name);
3269
- const stat = statSync2(p);
3553
+ const stat = statSync3(p);
3270
3554
  return { path: p, name, size: stat.size, date: stat.mtime };
3271
3555
  }).sort((a, b) => b.date.getTime() - a.date.getTime());
3272
3556
  } catch {
@@ -3298,7 +3582,7 @@ var init_db_backup = __esm({
3298
3582
 
3299
3583
  // src/lib/cloud-sync.ts
3300
3584
  init_database();
3301
- 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";
3302
3586
  import crypto3 from "crypto";
3303
3587
  import path10 from "path";
3304
3588
  import { homedir as homedir2 } from "os";
@@ -3453,18 +3737,36 @@ function loadPgClient() {
3453
3737
  const { pathToFileURL: pathToFileURL3 } = await import("url");
3454
3738
  const explicitPath = process.env.EXE_OS_PRISMA_CLIENT_PATH;
3455
3739
  if (explicitPath) {
3456
- const mod2 = await import(pathToFileURL3(explicitPath).href);
3457
- const Ctor2 = mod2.PrismaClient ?? mod2.default?.PrismaClient;
3458
- if (!Ctor2) throw new Error(`No PrismaClient at ${explicitPath}`);
3459
- 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();
3460
3744
  }
3461
3745
  const exeDbRoot = process.env.EXE_DB_ROOT ?? path10.join(homedir2(), "exe-db");
3462
- const req = createRequire3(path10.join(exeDbRoot, "package.json"));
3463
- const entry = req.resolve("@prisma/client");
3464
- const mod = await import(pathToFileURL3(entry).href);
3465
- const Ctor = mod.PrismaClient ?? mod.default?.PrismaClient;
3466
- if (!Ctor) throw new Error("No PrismaClient");
3467
- 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
+ };
3468
3770
  })().catch(() => {
3469
3771
  _pgFailed = true;
3470
3772
  _pgPromise = null;
@@ -3485,7 +3787,7 @@ async function pushToPostgres(records) {
3485
3787
  let inserted = 0;
3486
3788
  for (const rec of records) {
3487
3789
  try {
3488
- await prisma.$executeRawUnsafe(
3790
+ const changed = await prisma.$executeRawUnsafe(
3489
3791
  `INSERT INTO raw.raw_events (id, source, source_id, event_type, payload, metadata, timestamp)
3490
3792
  VALUES (gen_random_uuid(), 'cloud_sync', $1, 'memory', $2::jsonb, $3::jsonb, $4)
3491
3793
  ON CONFLICT (source, source_id, event_type) DO NOTHING`,
@@ -3494,7 +3796,7 @@ async function pushToPostgres(records) {
3494
3796
  JSON.stringify({ agent_id: rec.agent_id, project_name: rec.project_name, tool_name: rec.tool_name }),
3495
3797
  rec.timestamp ? new Date(String(rec.timestamp)) : /* @__PURE__ */ new Date()
3496
3798
  );
3497
- inserted++;
3799
+ inserted += Number(changed ?? 0);
3498
3800
  } catch {
3499
3801
  }
3500
3802
  }
@@ -3599,6 +3901,23 @@ async function cloudPush(records, maxVersion, config) {
3599
3901
  return false;
3600
3902
  }
3601
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
+ }
3602
3921
  async function cloudPull(sinceVersion, config) {
3603
3922
  assertSecureEndpoint(config.endpoint);
3604
3923
  try {
@@ -3618,22 +3937,62 @@ async function cloudPull(sinceVersion, config) {
3618
3937
  if (!response.ok) return { records: [], maxVersion: sinceVersion };
3619
3938
  const data = await response.json();
3620
3939
  const allRecords = [];
3621
- for (const { blob } of data.blobs ?? []) {
3940
+ let maxReadableVersion = sinceVersion;
3941
+ let skippedBlobs = 0;
3942
+ for (const { version, blob } of data.blobs ?? []) {
3622
3943
  try {
3623
3944
  const compressed = decryptSyncBlob(blob);
3624
3945
  const json = decompress(compressed).toString("utf8");
3625
3946
  const records = JSON.parse(json);
3626
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
+ );
3627
3958
  } catch {
3959
+ skippedBlobs++;
3628
3960
  continue;
3629
3961
  }
3630
3962
  }
3631
- 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 };
3632
3967
  } catch (err) {
3633
3968
  logError(`[cloud-sync] PULL FAILED: ${err instanceof Error ? err.message : String(err)}`);
3634
3969
  return { records: [], maxVersion: sinceVersion };
3635
3970
  }
3636
3971
  }
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()) {
3974
+ try {
3975
+ await client.execute("CREATE TABLE IF NOT EXISTS sync_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
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");
3978
+ } catch {
3979
+ return false;
3980
+ }
3981
+ }
3982
+ async function clearCloudReuploadRequired(client = getClient()) {
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')");
3985
+ await client.execute("INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('cloud_relink_required', '0')");
3986
+ await client.execute({
3987
+ sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('cloud_reuploaded_at', ?)",
3988
+ args: [(/* @__PURE__ */ new Date()).toISOString()]
3989
+ });
3990
+ await client.execute("DELETE FROM sync_meta WHERE key IN ('last_cloud_pull_version', 'last_cloud_push_version')");
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
+ }
3637
3996
  async function cloudSync(config) {
3638
3997
  if (!isSyncCryptoInitialized()) {
3639
3998
  try {
@@ -3655,13 +4014,10 @@ async function cloudSync(config) {
3655
4014
  throw new Error("[cloud-sync] Database not initialized. Call initStore() before cloudSync().");
3656
4015
  }
3657
4016
  try {
3658
- const relink = await client.execute("SELECT value FROM sync_meta WHERE key = 'cloud_relink_required' LIMIT 1");
3659
- if (String(relink.rows[0]?.value ?? "") === "1") {
3660
- throw new Error("[cloud-sync] Paused after key rotation. Re-link/reupload cloud sync with the new recovery phrase before syncing.");
3661
- }
4017
+ if (await getCloudReuploadRequired(client)) throw new Error(CLOUD_REUPLOAD_REQUIRED_MESSAGE);
3662
4018
  } catch (err) {
3663
4019
  const msg = err instanceof Error ? err.message : String(err);
3664
- if (msg.includes("Paused after key rotation")) throw err;
4020
+ if (msg === CLOUD_REUPLOAD_REQUIRED_MESSAGE || msg.includes("key rotation")) throw err;
3665
4021
  }
3666
4022
  try {
3667
4023
  const { getRawClient: getRawClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
@@ -3919,7 +4275,7 @@ async function cloudSync(config) {
3919
4275
  const { getLatestBackup: getLatestBackup2 } = await Promise.resolve().then(() => (init_db_backup(), db_backup_exports));
3920
4276
  const latestBackup = getLatestBackup2();
3921
4277
  if (latestBackup) {
3922
- const backupSize = statSync3(latestBackup).size;
4278
+ const backupSize = statSync4(latestBackup).size;
3923
4279
  const MAX_CLOUD_BACKUP_BYTES = 50 * 1024 * 1024;
3924
4280
  if (backupSize <= MAX_CLOUD_BACKUP_BYTES) {
3925
4281
  const backupData = readFileSync7(latestBackup);
@@ -4592,8 +4948,10 @@ async function cloudPullDocuments(config) {
4592
4948
  return { pulled };
4593
4949
  }
4594
4950
  export {
4951
+ CLOUD_REUPLOAD_REQUIRED_MESSAGE,
4595
4952
  assertSecureEndpoint,
4596
4953
  buildRosterBlob,
4954
+ clearCloudReuploadRequired,
4597
4955
  cloudPull,
4598
4956
  cloudPullBehaviors,
4599
4957
  cloudPullBlob,
@@ -4612,7 +4970,10 @@ export {
4612
4970
  cloudPushGraphRAG,
4613
4971
  cloudPushRoster,
4614
4972
  cloudPushTasks,
4973
+ cloudResetMemoryBlobs,
4615
4974
  cloudSync,
4975
+ getCloudReuploadRequired,
4976
+ markCloudReuploadRequired,
4616
4977
  mergeConfig,
4617
4978
  mergeRosterFromRemote,
4618
4979
  pushToPostgres,