@orion-agents/orion-code 0.1.0 → 0.1.2

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 (251) hide show
  1. package/README.md +107 -67
  2. package/README.zh-CN.md +192 -217
  3. package/dist/cli.js +23 -21
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/index.d.ts +1 -1
  6. package/dist/commands/index.d.ts.map +1 -1
  7. package/dist/commands/index.js +397 -212
  8. package/dist/commands/index.js.map +1 -1
  9. package/dist/commands/target-command.d.ts.map +1 -1
  10. package/dist/commands/target-command.js +43 -9
  11. package/dist/commands/target-command.js.map +1 -1
  12. package/dist/commands/types.d.ts +33 -0
  13. package/dist/commands/types.d.ts.map +1 -1
  14. package/dist/commands/types.js +5 -1
  15. package/dist/commands/types.js.map +1 -1
  16. package/dist/core/agent.d.ts.map +1 -1
  17. package/dist/core/agent.js +2 -2
  18. package/dist/core/agent.js.map +1 -1
  19. package/dist/framework/external-assertion.d.ts +65 -0
  20. package/dist/framework/external-assertion.d.ts.map +1 -0
  21. package/dist/framework/external-assertion.js +571 -0
  22. package/dist/framework/external-assertion.js.map +1 -0
  23. package/dist/framework/index.d.ts +6 -4
  24. package/dist/framework/index.d.ts.map +1 -1
  25. package/dist/framework/index.js +4 -1
  26. package/dist/framework/index.js.map +1 -1
  27. package/dist/framework/query.d.ts +18 -1
  28. package/dist/framework/query.d.ts.map +1 -1
  29. package/dist/framework/query.js +65 -28
  30. package/dist/framework/query.js.map +1 -1
  31. package/dist/framework/tool-scheduler.d.ts +2 -0
  32. package/dist/framework/tool-scheduler.d.ts.map +1 -1
  33. package/dist/framework/tool-scheduler.js +23 -9
  34. package/dist/framework/tool-scheduler.js.map +1 -1
  35. package/dist/framework/tool-serializer.d.ts.map +1 -1
  36. package/dist/framework/tool-serializer.js +20 -4
  37. package/dist/framework/tool-serializer.js.map +1 -1
  38. package/dist/framework/tool.d.ts +11 -5
  39. package/dist/framework/tool.d.ts.map +1 -1
  40. package/dist/framework/tool.js.map +1 -1
  41. package/dist/harness/context-harness.d.ts.map +1 -1
  42. package/dist/harness/context-harness.js +4 -3
  43. package/dist/harness/context-harness.js.map +1 -1
  44. package/dist/harness/drift-guard.d.ts.map +1 -1
  45. package/dist/harness/drift-guard.js +14 -5
  46. package/dist/harness/drift-guard.js.map +1 -1
  47. package/dist/harness/safety.d.ts.map +1 -1
  48. package/dist/harness/safety.js +19 -3
  49. package/dist/harness/safety.js.map +1 -1
  50. package/dist/init.d.ts +1 -1
  51. package/dist/init.d.ts.map +1 -1
  52. package/dist/init.js +115 -34
  53. package/dist/init.js.map +1 -1
  54. package/dist/ink-ui/screens/ReplScreen.d.ts.map +1 -1
  55. package/dist/ink-ui/screens/ReplScreen.js +4 -1
  56. package/dist/ink-ui/screens/ReplScreen.js.map +1 -1
  57. package/dist/memory/storage.d.ts.map +1 -1
  58. package/dist/memory/storage.js +1 -0
  59. package/dist/memory/storage.js.map +1 -1
  60. package/dist/memory/store.d.ts.map +1 -1
  61. package/dist/memory/store.js +11 -5
  62. package/dist/memory/store.js.map +1 -1
  63. package/dist/migration/command.d.ts.map +1 -1
  64. package/dist/migration/command.js +6 -1
  65. package/dist/migration/command.js.map +1 -1
  66. package/dist/migration/migrate.d.ts.map +1 -1
  67. package/dist/migration/migrate.js +4 -1
  68. package/dist/migration/migrate.js.map +1 -1
  69. package/dist/print-ui/launch.d.ts +4 -0
  70. package/dist/print-ui/launch.d.ts.map +1 -1
  71. package/dist/print-ui/launch.js +102 -9
  72. package/dist/print-ui/launch.js.map +1 -1
  73. package/dist/product/identity.d.ts +2 -2
  74. package/dist/product/identity.d.ts.map +1 -1
  75. package/dist/product/identity.js +2 -2
  76. package/dist/product/identity.js.map +1 -1
  77. package/dist/product/index.d.ts +2 -1
  78. package/dist/product/index.d.ts.map +1 -1
  79. package/dist/product/index.js +5 -1
  80. package/dist/product/index.js.map +1 -1
  81. package/dist/product/version.d.ts +12 -0
  82. package/dist/product/version.d.ts.map +1 -0
  83. package/dist/product/version.js +31 -0
  84. package/dist/product/version.js.map +1 -0
  85. package/dist/runtime/agent-runtime-controller.d.ts +49 -0
  86. package/dist/runtime/agent-runtime-controller.d.ts.map +1 -1
  87. package/dist/runtime/agent-runtime-controller.js +1048 -81
  88. package/dist/runtime/agent-runtime-controller.js.map +1 -1
  89. package/dist/runtime/agent-runtime-protocol.d.ts +14 -2
  90. package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
  91. package/dist/runtime/agent-runtime-protocol.js +18 -0
  92. package/dist/runtime/agent-runtime-protocol.js.map +1 -1
  93. package/dist/runtime/chat-controller.d.ts +6 -0
  94. package/dist/runtime/chat-controller.d.ts.map +1 -1
  95. package/dist/runtime/chat-controller.js +226 -20
  96. package/dist/runtime/chat-controller.js.map +1 -1
  97. package/dist/runtime/goals/accounting.d.ts +28 -0
  98. package/dist/runtime/goals/accounting.d.ts.map +1 -1
  99. package/dist/runtime/goals/accounting.js +69 -0
  100. package/dist/runtime/goals/accounting.js.map +1 -1
  101. package/dist/runtime/goals/completion-audit.d.ts +13 -8
  102. package/dist/runtime/goals/completion-audit.d.ts.map +1 -1
  103. package/dist/runtime/goals/completion-audit.js +598 -16
  104. package/dist/runtime/goals/completion-audit.js.map +1 -1
  105. package/dist/runtime/goals/coordinator.d.ts +44 -5
  106. package/dist/runtime/goals/coordinator.d.ts.map +1 -1
  107. package/dist/runtime/goals/coordinator.js +1124 -58
  108. package/dist/runtime/goals/coordinator.js.map +1 -1
  109. package/dist/runtime/goals/evidence.d.ts +37 -0
  110. package/dist/runtime/goals/evidence.d.ts.map +1 -0
  111. package/dist/runtime/goals/evidence.js +300 -0
  112. package/dist/runtime/goals/evidence.js.map +1 -0
  113. package/dist/runtime/goals/presentation.d.ts +4 -0
  114. package/dist/runtime/goals/presentation.d.ts.map +1 -0
  115. package/dist/runtime/goals/presentation.js +50 -0
  116. package/dist/runtime/goals/presentation.js.map +1 -0
  117. package/dist/runtime/goals/prompt.d.ts.map +1 -1
  118. package/dist/runtime/goals/prompt.js +38 -8
  119. package/dist/runtime/goals/prompt.js.map +1 -1
  120. package/dist/runtime/goals/tools.d.ts +18 -1
  121. package/dist/runtime/goals/tools.d.ts.map +1 -1
  122. package/dist/runtime/goals/tools.js +308 -28
  123. package/dist/runtime/goals/tools.js.map +1 -1
  124. package/dist/runtime/goals/types.d.ts +241 -7
  125. package/dist/runtime/goals/types.d.ts.map +1 -1
  126. package/dist/runtime/goals/types.js +8 -2
  127. package/dist/runtime/goals/types.js.map +1 -1
  128. package/dist/runtime/subagents/child-executor-guard.d.ts +12 -4
  129. package/dist/runtime/subagents/child-executor-guard.d.ts.map +1 -1
  130. package/dist/runtime/subagents/child-executor-guard.js +397 -61
  131. package/dist/runtime/subagents/child-executor-guard.js.map +1 -1
  132. package/dist/runtime/subagents/presets.d.ts +4 -5
  133. package/dist/runtime/subagents/presets.d.ts.map +1 -1
  134. package/dist/runtime/subagents/presets.js +18 -33
  135. package/dist/runtime/subagents/presets.js.map +1 -1
  136. package/dist/runtime/subagents/production.d.ts +3 -1
  137. package/dist/runtime/subagents/production.d.ts.map +1 -1
  138. package/dist/runtime/subagents/production.js +37 -7
  139. package/dist/runtime/subagents/production.js.map +1 -1
  140. package/dist/runtime/subagents/runner.d.ts.map +1 -1
  141. package/dist/runtime/subagents/runner.js +24 -3
  142. package/dist/runtime/subagents/runner.js.map +1 -1
  143. package/dist/runtime/subagents/runtime-integration.d.ts +3 -0
  144. package/dist/runtime/subagents/runtime-integration.d.ts.map +1 -1
  145. package/dist/runtime/subagents/runtime-integration.js +18 -7
  146. package/dist/runtime/subagents/runtime-integration.js.map +1 -1
  147. package/dist/runtime/subagents/supervisor.d.ts +4 -4
  148. package/dist/runtime/subagents/supervisor.d.ts.map +1 -1
  149. package/dist/runtime/subagents/supervisor.js +39 -19
  150. package/dist/runtime/subagents/supervisor.js.map +1 -1
  151. package/dist/runtime/subagents/types.d.ts +7 -0
  152. package/dist/runtime/subagents/types.d.ts.map +1 -1
  153. package/dist/runtime/subagents/types.js +12 -1
  154. package/dist/runtime/subagents/types.js.map +1 -1
  155. package/dist/runtime/ui-events.d.ts +10 -0
  156. package/dist/runtime/ui-events.d.ts.map +1 -1
  157. package/dist/runtime/ui-events.js +5 -5
  158. package/dist/runtime/ui-events.js.map +1 -1
  159. package/dist/runtime/ui-view-model.d.ts +3 -3
  160. package/dist/runtime/ui-view-model.d.ts.map +1 -1
  161. package/dist/runtime/ui-view-model.js +11 -4
  162. package/dist/runtime/ui-view-model.js.map +1 -1
  163. package/dist/services/config.d.ts +18 -7
  164. package/dist/services/config.d.ts.map +1 -1
  165. package/dist/services/config.js +79 -57
  166. package/dist/services/config.js.map +1 -1
  167. package/dist/services/doctor.d.ts.map +1 -1
  168. package/dist/services/doctor.js +164 -18
  169. package/dist/services/doctor.js.map +1 -1
  170. package/dist/services/global-config.d.ts +1 -1
  171. package/dist/services/global-config.d.ts.map +1 -1
  172. package/dist/services/goal-storage.d.ts +9 -4
  173. package/dist/services/goal-storage.d.ts.map +1 -1
  174. package/dist/services/goal-storage.js +1113 -75
  175. package/dist/services/goal-storage.js.map +1 -1
  176. package/dist/services/llm.d.ts +19 -0
  177. package/dist/services/llm.d.ts.map +1 -1
  178. package/dist/services/llm.js +153 -20
  179. package/dist/services/llm.js.map +1 -1
  180. package/dist/services/model-registry.d.ts +1 -1
  181. package/dist/services/model-registry.d.ts.map +1 -1
  182. package/dist/services/model-registry.js +69 -19
  183. package/dist/services/model-registry.js.map +1 -1
  184. package/dist/services/provider-resilience/coordinator.d.ts +2 -1
  185. package/dist/services/provider-resilience/coordinator.d.ts.map +1 -1
  186. package/dist/services/provider-resilience/coordinator.js +7 -6
  187. package/dist/services/provider-resilience/coordinator.js.map +1 -1
  188. package/dist/services/provider-resilience/error-classifier.d.ts.map +1 -1
  189. package/dist/services/provider-resilience/error-classifier.js +67 -17
  190. package/dist/services/provider-resilience/error-classifier.js.map +1 -1
  191. package/dist/services/provider-resilience/types.d.ts +1 -1
  192. package/dist/services/provider-resilience/types.d.ts.map +1 -1
  193. package/dist/services/provider-resilience/types.js.map +1 -1
  194. package/dist/services/redaction.d.ts.map +1 -1
  195. package/dist/services/redaction.js +9 -1
  196. package/dist/services/redaction.js.map +1 -1
  197. package/dist/services/session-storage.d.ts +12 -1
  198. package/dist/services/session-storage.d.ts.map +1 -1
  199. package/dist/services/session-storage.js +23 -7
  200. package/dist/services/session-storage.js.map +1 -1
  201. package/dist/services/storage-maintenance.d.ts +73 -3
  202. package/dist/services/storage-maintenance.d.ts.map +1 -1
  203. package/dist/services/storage-maintenance.js +771 -104
  204. package/dist/services/storage-maintenance.js.map +1 -1
  205. package/dist/services/task-manager.d.ts.map +1 -1
  206. package/dist/services/task-manager.js +2 -2
  207. package/dist/services/task-manager.js.map +1 -1
  208. package/dist/services/web-search-adapters.d.ts.map +1 -1
  209. package/dist/services/web-search-adapters.js +89 -49
  210. package/dist/services/web-search-adapters.js.map +1 -1
  211. package/dist/services/web-search-mcp.d.ts.map +1 -1
  212. package/dist/services/web-search-mcp.js +34 -37
  213. package/dist/services/web-search-mcp.js.map +1 -1
  214. package/dist/services/workspace-state.d.ts +4 -0
  215. package/dist/services/workspace-state.d.ts.map +1 -1
  216. package/dist/services/workspace-state.js +30 -9
  217. package/dist/services/workspace-state.js.map +1 -1
  218. package/dist/terminal-ui/completion.js +1 -1
  219. package/dist/terminal-ui/completion.js.map +1 -1
  220. package/dist/terminal-ui/launch.d.ts +3 -0
  221. package/dist/terminal-ui/launch.d.ts.map +1 -1
  222. package/dist/terminal-ui/launch.js +26 -98
  223. package/dist/terminal-ui/launch.js.map +1 -1
  224. package/dist/tools/git.d.ts.map +1 -1
  225. package/dist/tools/git.js +275 -27
  226. package/dist/tools/git.js.map +1 -1
  227. package/dist/tools/index.d.ts +2 -0
  228. package/dist/tools/index.d.ts.map +1 -1
  229. package/dist/tools/index.js +29 -10
  230. package/dist/tools/index.js.map +1 -1
  231. package/dist/tools/mcp.d.ts.map +1 -1
  232. package/dist/tools/mcp.js +20 -12
  233. package/dist/tools/mcp.js.map +1 -1
  234. package/dist/tools/web.d.ts.map +1 -1
  235. package/dist/tools/web.js +38 -14
  236. package/dist/tools/web.js.map +1 -1
  237. package/dist/tui-ui/layout.d.ts.map +1 -1
  238. package/dist/tui-ui/layout.js +36 -23
  239. package/dist/tui-ui/layout.js.map +1 -1
  240. package/dist/tui-ui/pickers.d.ts.map +1 -1
  241. package/dist/tui-ui/pickers.js +5 -4
  242. package/dist/tui-ui/pickers.js.map +1 -1
  243. package/dist/tui-ui/runner.d.ts +1 -0
  244. package/dist/tui-ui/runner.d.ts.map +1 -1
  245. package/dist/tui-ui/runner.js +15 -17
  246. package/dist/tui-ui/runner.js.map +1 -1
  247. package/dist/tui-ui/state.d.ts +6 -0
  248. package/dist/tui-ui/state.d.ts.map +1 -1
  249. package/dist/tui-ui/state.js +61 -30
  250. package/dist/tui-ui/state.js.map +1 -1
  251. package/package.json +14 -9
@@ -4,11 +4,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.collectStorageReport = collectStorageReport;
7
+ exports.collectStorageCleanupPlan = collectStorageCleanupPlan;
7
8
  exports.cleanupStorage = cleanupStorage;
9
+ exports.collectProjectMetadataRepairPlan = collectProjectMetadataRepairPlan;
8
10
  exports.repairProjectMetadata = repairProjectMetadata;
11
+ exports.serializeStorageMaintenancePlan = serializeStorageMaintenancePlan;
12
+ exports.deserializeStorageMaintenancePlan = deserializeStorageMaintenancePlan;
13
+ exports.consumeStorageMaintenancePlan = consumeStorageMaintenancePlan;
14
+ exports.resetStorageMaintenancePlanRegistry = resetStorageMaintenancePlanRegistry;
9
15
  exports.formatStorageReport = formatStorageReport;
10
16
  exports.formatStorageCleanupResult = formatStorageCleanupResult;
11
17
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
18
+ const crypto_1 = require("crypto");
12
19
  const fs_1 = require("fs");
13
20
  const path_1 = require("path");
14
21
  const session_storage_1 = require("./session-storage");
@@ -16,6 +23,8 @@ const config_dir_1 = require("./config-dir");
16
23
  const format_1 = require("./format");
17
24
  const HASH_PROJECT_RE = /^[a-f0-9]{16}$/;
18
25
  const TEMP_PROJECT_RE = /^(private-tmp-|private-var-folders-.*orion-code-|tmp-orion-code-|test-project$)/;
26
+ const STORAGE_PLAN_TOKEN_TTL_MS = 10 * 60 * 1000;
27
+ const issuedStorageMaintenancePlans = new Map();
19
28
  function collectStorageReport() {
20
29
  const configHome = (0, config_dir_1.getConfigHome)();
21
30
  const projectsDir = (0, config_dir_1.getProjectsDir)();
@@ -24,10 +33,11 @@ function collectStorageReport() {
24
33
  .filter(project => !HASH_PROJECT_RE.test(project.projectKey) && !TEMP_PROJECT_RE.test(project.projectKey))
25
34
  .map(project => project.projectKey));
26
35
  const vectorProjects = inspectVectorProjects(configHome, validProjectKeys);
36
+ const metadataRepairPlan = collectProjectMetadataRepairPlan();
27
37
  const issues = [];
28
38
  const legacySessions = (0, path_1.join)(configHome, 'sessions');
29
39
  const legacyMemory = (0, path_1.join)(configHome, 'memory');
30
- if ((0, fs_1.existsSync)(legacySessions)) {
40
+ if (safeIsDirectory(legacySessions)) {
31
41
  issues.push({
32
42
  kind: 'legacy-global-sessions',
33
43
  path: legacySessions,
@@ -36,7 +46,7 @@ function collectStorageReport() {
36
46
  canCleanup: true,
37
47
  });
38
48
  }
39
- if ((0, fs_1.existsSync)(legacyMemory)) {
49
+ if (safeIsDirectory(legacyMemory)) {
40
50
  issues.push({
41
51
  kind: 'legacy-global-memory',
42
52
  path: legacyMemory,
@@ -79,26 +89,19 @@ function collectStorageReport() {
79
89
  });
80
90
  continue;
81
91
  }
82
- if (!project.hasMetadata) {
83
- issues.push({
84
- kind: 'missing-project-metadata',
85
- path: (0, config_dir_1.getProjectMetadataPath)(project.projectPath || project.path),
86
- projectKey: project.projectKey,
87
- summary: 'Project metadata file is missing',
88
- bytes: project.bytes,
89
- canCleanup: false,
90
- });
91
- }
92
- else if (project.projectPath && (0, config_dir_1.getCanonicalProjectKey)(project.projectPath) !== project.projectKey) {
93
- issues.push({
94
- kind: 'invalid-project-metadata',
95
- path: (0, config_dir_1.getProjectMetadataPath)(project.projectPath),
96
- projectKey: project.projectKey,
97
- summary: 'Project metadata does not match the canonical project key',
98
- bytes: project.bytes,
99
- canCleanup: false,
100
- });
101
- }
92
+ }
93
+ const projectsByKey = new Map(projects.map(project => [project.projectKey, project]));
94
+ for (const action of metadataRepairPlan.actions) {
95
+ issues.push({
96
+ kind: action.kind,
97
+ path: action.metadataPath,
98
+ projectKey: action.projectKey,
99
+ summary: action.kind === 'missing-project-metadata'
100
+ ? 'Project metadata file is missing'
101
+ : 'Project metadata is invalid',
102
+ bytes: projectsByKey.get(action.projectKey)?.bytes,
103
+ canCleanup: false,
104
+ });
102
105
  }
103
106
  for (const vectorProject of vectorProjects) {
104
107
  if (!vectorProject.orphan)
@@ -119,74 +122,187 @@ function collectStorageReport() {
119
122
  totalBytes: dirSize(configHome),
120
123
  };
121
124
  }
122
- function cleanupStorage(options = {}) {
125
+ /** Collect an immutable, serializable cleanup plan without modifying storage. */
126
+ function collectStorageCleanupPlan(options = {}) {
123
127
  const opts = {
124
- dryRun: options.dryRun ?? false,
125
128
  includeLegacy: options.includeLegacy ?? true,
126
129
  includeTemp: options.includeTemp ?? true,
127
130
  includeEmpty: options.includeEmpty ?? true,
128
131
  includeVectorOrphans: options.includeVectorOrphans ?? true,
129
132
  };
130
133
  const report = collectStorageReport();
131
- const deletedPaths = [];
132
- const skippedPaths = [];
133
- let vectorDeletedRows = 0;
134
+ const actions = [];
135
+ const skipped = [];
134
136
  for (const issue of report.issues) {
135
- const shouldDeletePath = (opts.includeLegacy && (issue.kind === 'legacy-global-sessions' || issue.kind === 'legacy-global-memory' || issue.kind === 'legacy-hash-project')) ||
137
+ const shouldDeletePath = (opts.includeLegacy &&
138
+ (issue.kind === 'legacy-global-sessions' ||
139
+ issue.kind === 'legacy-global-memory' ||
140
+ issue.kind === 'legacy-hash-project')) ||
136
141
  (opts.includeTemp && issue.kind === 'temp-project') ||
137
142
  (opts.includeEmpty && issue.kind === 'empty-project');
138
- if (shouldDeletePath && issue.path) {
139
- if (!opts.dryRun) {
140
- (0, fs_1.rmSync)(issue.path, { recursive: true, force: true });
141
- }
142
- deletedPaths.push(issue.path);
143
+ if (shouldDeletePath && issue.path && isPathCleanupKind(issue.kind)) {
144
+ const action = inspectCleanupPathAction(report, issue.kind, issue.path, issue.projectKey);
145
+ if (action)
146
+ actions.push(action);
147
+ else
148
+ skipped.push(issue.path);
143
149
  continue;
144
150
  }
145
151
  if (opts.includeVectorOrphans && issue.kind === 'vector-orphan-project' && issue.projectKey) {
146
- const deleted = deleteVectorProjectRows(report.configHome, issue.projectKey, opts.dryRun);
147
- vectorDeletedRows += deleted;
152
+ const action = inspectVectorCleanupAction(report, issue.projectKey);
153
+ if (action)
154
+ actions.push(action);
155
+ else
156
+ skipped.push(issue.projectKey);
148
157
  continue;
149
158
  }
150
- if (issue.canCleanup) {
151
- skippedPaths.push(issue.path || issue.projectKey || issue.kind);
152
- }
159
+ if (issue.canCleanup)
160
+ skipped.push(issue.path || issue.projectKey || issue.kind);
153
161
  }
154
- if (!opts.dryRun) {
155
- repairProjectMetadata();
162
+ actions.sort((a, b) => cleanupActionKey(a).localeCompare(cleanupActionKey(b)));
163
+ return {
164
+ version: 1,
165
+ configHome: report.configHome,
166
+ projectsDir: report.projectsDir,
167
+ actions,
168
+ skipped,
169
+ issues: report.issues,
170
+ };
171
+ }
172
+ /** Apply only actions captured in a cleanup plan, after exact state revalidation. */
173
+ function cleanupStorage(options = {}, plan = collectStorageCleanupPlan(options)) {
174
+ const dryRun = options.dryRun ?? false;
175
+ const deletedPaths = [];
176
+ const skippedPaths = [...plan.skipped];
177
+ const quarantinedPaths = [];
178
+ let vectorDeletedRows = 0;
179
+ if (!sameStorageRoots(plan.configHome, plan.projectsDir)) {
180
+ return {
181
+ dryRun,
182
+ deletedPaths,
183
+ skippedPaths: [...skippedPaths, ...plan.actions.map(cleanupActionKey)],
184
+ quarantinedPaths,
185
+ vectorDeletedRows,
186
+ issues: plan.issues,
187
+ };
188
+ }
189
+ for (const action of plan.actions) {
190
+ if (action.type === 'path') {
191
+ const current = inspectCleanupPathAction({
192
+ configHome: plan.configHome,
193
+ projectsDir: plan.projectsDir,
194
+ }, action.kind, action.path, action.projectKey);
195
+ if (!current || !sameCleanupPathAction(current, action)) {
196
+ skippedPaths.push(action.path);
197
+ continue;
198
+ }
199
+ if (dryRun) {
200
+ quarantinedPaths.push(action.path);
201
+ continue;
202
+ }
203
+ const quarantine = quarantinePlannedPath(action);
204
+ if (quarantine.status === 'restored' || quarantine.status === 'skipped') {
205
+ skippedPaths.push(action.path);
206
+ }
207
+ if (quarantine.quarantinePath)
208
+ quarantinedPaths.push(quarantine.quarantinePath);
209
+ continue;
210
+ }
211
+ const current = inspectVectorCleanupAction({ configHome: plan.configHome, projectsDir: plan.projectsDir }, action.projectKey);
212
+ if (!current || !sameVectorCleanupAction(current, action)) {
213
+ skippedPaths.push(action.projectKey);
214
+ continue;
215
+ }
216
+ if (dryRun) {
217
+ vectorDeletedRows += action.rowCount;
218
+ continue;
219
+ }
220
+ const deletedRows = deletePlannedVectorRows(plan.configHome, action);
221
+ if (deletedRows !== action.rowCount) {
222
+ skippedPaths.push(action.projectKey);
223
+ continue;
224
+ }
225
+ vectorDeletedRows += deletedRows;
156
226
  }
157
227
  return {
158
- dryRun: opts.dryRun,
228
+ dryRun,
159
229
  deletedPaths,
160
230
  skippedPaths,
231
+ quarantinedPaths,
161
232
  vectorDeletedRows,
162
- issues: report.issues,
233
+ issues: plan.issues,
163
234
  };
164
235
  }
165
- function repairProjectMetadata() {
236
+ /** Collect a read-only, revalidatable plan for safe project metadata repairs. */
237
+ function collectProjectMetadataRepairPlan() {
166
238
  const projectsDir = (0, config_dir_1.getProjectsDir)();
167
- const repaired = [];
239
+ const actions = [];
168
240
  const skipped = [];
169
- if (!(0, fs_1.existsSync)(projectsDir)) {
170
- (0, fs_1.mkdirSync)(projectsDir, { recursive: true, mode: 0o700 });
171
- return { repaired, skipped };
172
- }
241
+ if (!safeIsDirectory(projectsDir))
242
+ return { version: 1, projectsDir, actions, skipped };
173
243
  for (const entry of (0, fs_1.readdirSync)(projectsDir)) {
174
244
  if (HASH_PROJECT_RE.test(entry) || TEMP_PROJECT_RE.test(entry)) {
175
245
  skipped.push(entry);
176
246
  continue;
177
247
  }
178
- const projectDir = (0, path_1.join)(projectsDir, entry);
179
- if (!safeIsDirectory(projectDir))
180
- continue;
181
- const projectPath = inferProjectPath(projectDir);
182
- if (!projectPath) {
248
+ const observation = inspectProjectMetadataRepair(projectsDir, entry);
249
+ if (observation.action) {
250
+ actions.push(observation.action);
251
+ }
252
+ else if (observation.skipped) {
183
253
  skipped.push(entry);
184
- continue;
185
254
  }
186
- (0, config_dir_1.updateProjectMetadata)(projectPath);
187
- repaired.push(entry);
188
255
  }
189
- return { repaired, skipped };
256
+ actions.sort((a, b) => a.projectKey.localeCompare(b.projectKey));
257
+ return { version: 1, projectsDir, actions, skipped };
258
+ }
259
+ /** Apply only the targets in a repair plan, after revalidating every target. */
260
+ function repairProjectMetadata(plan = collectProjectMetadataRepairPlan()) {
261
+ const projectsDir = (0, config_dir_1.getProjectsDir)();
262
+ const repaired = [];
263
+ const skipped = [...plan.skipped];
264
+ const blockedReason = 'Writable project metadata repair is disabled because Node.js cannot provide race-safe directory-relative writes on this platform.';
265
+ if (plan.version !== 1 || plan.projectsDir !== projectsDir) {
266
+ return {
267
+ repaired,
268
+ skipped: [...skipped, ...plan.actions.map(action => action.projectKey)],
269
+ writeDisabled: true,
270
+ blockedReason,
271
+ };
272
+ }
273
+ skipped.push(...plan.actions.map(action => action.projectKey));
274
+ return { repaired, skipped, writeDisabled: true, blockedReason };
275
+ }
276
+ /** Issue an opaque, process-local, expiring token for a previewed maintenance plan. */
277
+ function serializeStorageMaintenancePlan(plan) {
278
+ if ((plan.kind === 'cleanup' && !isStorageCleanupPlan(plan.plan)) ||
279
+ (plan.kind === 'repair' && !isProjectMetadataRepairPlan(plan.plan))) {
280
+ throw new Error('Cannot issue an invalid storage maintenance plan');
281
+ }
282
+ pruneExpiredStorageMaintenancePlans();
283
+ let nonce;
284
+ do {
285
+ nonce = (0, crypto_1.randomBytes)(32).toString('base64url');
286
+ } while (issuedStorageMaintenancePlans.has(nonce));
287
+ issuedStorageMaintenancePlans.set(nonce, {
288
+ expiresAt: Date.now() + STORAGE_PLAN_TOKEN_TTL_MS,
289
+ plan: cloneStorageMaintenancePlan(plan),
290
+ });
291
+ return `v2.${nonce}`;
292
+ }
293
+ /** Peek at an active plan token without consuming it (used only for preview/dry-run). */
294
+ function deserializeStorageMaintenancePlan(token) {
295
+ const issued = readIssuedStorageMaintenancePlan(token, false);
296
+ return issued ? cloneStorageMaintenancePlan(issued) : undefined;
297
+ }
298
+ /** Consume a plan token exactly once before a confirmed mutation attempt. */
299
+ function consumeStorageMaintenancePlan(token) {
300
+ const issued = readIssuedStorageMaintenancePlan(token, true);
301
+ return issued ? cloneStorageMaintenancePlan(issued) : undefined;
302
+ }
303
+ /** Clear process-local plan capabilities, primarily for process-boundary tests. */
304
+ function resetStorageMaintenancePlanRegistry() {
305
+ issuedStorageMaintenancePlans.clear();
190
306
  }
191
307
  function formatStorageReport(report) {
192
308
  const lines = [
@@ -223,7 +339,8 @@ function formatStorageCleanupResult(result) {
223
339
  const lines = [
224
340
  result.dryRun ? 'Orion Code Storage Cleanup (dry run)' : 'Orion Code Storage Cleanup',
225
341
  '─'.repeat(40),
226
- `Paths ${result.deletedPaths.length}`,
342
+ `Deleted ${result.deletedPaths.length}`,
343
+ `Quarantined ${result.quarantinedPaths.length}`,
227
344
  `Vector rows ${result.vectorDeletedRows}`,
228
345
  ];
229
346
  for (const path of result.deletedPaths.slice(0, 20)) {
@@ -235,65 +352,512 @@ function formatStorageCleanupResult(result) {
235
352
  if (result.skippedPaths.length > 0) {
236
353
  lines.push(`Skipped ${result.skippedPaths.length}`);
237
354
  }
355
+ if (result.quarantinedPaths.length > 0) {
356
+ for (const path of result.quarantinedPaths.slice(0, 20)) {
357
+ lines.push(` ${result.dryRun ? 'would quarantine' : 'retained'} ${path}`);
358
+ }
359
+ }
238
360
  return lines.join('\n');
239
361
  }
240
362
  function listProjectSummaries(projectsDir) {
241
- if (!(0, fs_1.existsSync)(projectsDir))
363
+ if (!safeIsDirectory(projectsDir))
242
364
  return [];
243
365
  const summaries = [];
244
366
  for (const projectKey of (0, fs_1.readdirSync)(projectsDir)) {
245
367
  const projectDir = (0, path_1.join)(projectsDir, projectKey);
246
368
  if (!safeIsDirectory(projectDir))
247
369
  continue;
248
- const projectPath = inferProjectPath(projectDir);
249
- const metadata = projectPath ? (0, config_dir_1.readProjectMetadata)(projectPath) : null;
370
+ const metadata = inspectProjectMetadata(projectDir, projectKey);
371
+ const repair = inspectProjectMetadataRepair(projectsDir, projectKey).action;
372
+ const projectPath = metadata.valid ? metadata.projectPath : repair?.projectPath;
250
373
  const sessionsDir = (0, path_1.join)(projectDir, 'sessions');
251
- const sessions = (0, fs_1.existsSync)(sessionsDir)
252
- ? (0, fs_1.readdirSync)(sessionsDir).filter(file => file.endsWith('.json') && !session_storage_1.SESSION_SIDECAR_SUFFIXES.some(s => file.endsWith(s))).length
374
+ const sessions = safeIsDirectory(sessionsDir)
375
+ ? (0, fs_1.readdirSync)(sessionsDir).filter(file => file.endsWith('.json') &&
376
+ !session_storage_1.SESSION_SIDECAR_SUFFIXES.some(s => file.endsWith(s)) &&
377
+ safeIsFile((0, path_1.join)(sessionsDir, file))).length
253
378
  : 0;
254
379
  summaries.push({
255
380
  projectKey,
256
381
  path: projectDir,
257
382
  bytes: dirSize(projectDir),
258
- hasMetadata: !!metadata,
383
+ hasMetadata: metadata.valid,
259
384
  projectPath,
260
385
  sessions,
261
386
  });
262
387
  }
263
388
  return summaries.sort((a, b) => b.bytes - a.bytes || a.projectKey.localeCompare(b.projectKey));
264
389
  }
265
- function inferProjectPath(projectDir) {
390
+ function inspectProjectMetadata(projectDir, expectedProjectKey) {
266
391
  const metadataPath = (0, path_1.join)(projectDir, 'project.json');
392
+ const inspected = readTextFileNoFollow(metadataPath);
393
+ if (inspected.status === 'missing') {
394
+ return { exists: false, valid: false, repairable: true, fingerprint: 'missing' };
395
+ }
396
+ if (inspected.status !== 'ok') {
397
+ return {
398
+ exists: true,
399
+ valid: false,
400
+ repairable: false,
401
+ fingerprint: inspected.fingerprint,
402
+ };
403
+ }
404
+ const fingerprint = fingerprintText(inspected.raw);
267
405
  try {
268
- const parsed = JSON.parse((0, fs_1.readFileSync)(metadataPath, 'utf8'));
269
- if (typeof parsed.projectPath === 'string' && parsed.projectPath) {
270
- return parsed.projectPath;
271
- }
406
+ const parsed = JSON.parse(inspected.raw);
407
+ const projectPath = normalizeProjectPath(parsed.projectPath);
408
+ const valid = parsed.schemaVersion === config_dir_1.PROJECT_METADATA_SCHEMA_VERSION &&
409
+ parsed.projectKey === expectedProjectKey &&
410
+ !!projectPath &&
411
+ (0, config_dir_1.getCanonicalProjectKey)(projectPath) === expectedProjectKey;
412
+ return { exists: true, valid, repairable: true, fingerprint, projectPath };
272
413
  }
273
414
  catch {
274
- // Fall through to session metadata.
415
+ return { exists: true, valid: false, repairable: true, fingerprint };
275
416
  }
417
+ }
418
+ function inspectProjectSessions(projectDir) {
276
419
  const sessionsDir = (0, path_1.join)(projectDir, 'sessions');
277
- if (!(0, fs_1.existsSync)(sessionsDir))
278
- return undefined;
279
- for (const file of (0, fs_1.readdirSync)(sessionsDir)) {
420
+ if (!pathExistsNoFollow(sessionsDir)) {
421
+ return { safe: true, fingerprint: fingerprintText('missing'), projectPaths: [] };
422
+ }
423
+ if (!safeIsDirectory(sessionsDir)) {
424
+ return { safe: false, fingerprint: fingerprintText('unsafe-directory'), projectPaths: [] };
425
+ }
426
+ const evidence = [];
427
+ const projectPaths = [];
428
+ let files;
429
+ try {
430
+ files = (0, fs_1.readdirSync)(sessionsDir).sort();
431
+ }
432
+ catch {
433
+ return { safe: false, fingerprint: fingerprintText('unreadable-directory'), projectPaths: [] };
434
+ }
435
+ for (const file of files) {
280
436
  if (!file.endsWith('.json') || session_storage_1.SESSION_SIDECAR_SUFFIXES.some(s => file.endsWith(s)))
281
437
  continue;
438
+ const inspected = readTextFileNoFollow((0, path_1.join)(sessionsDir, file));
439
+ if (inspected.status !== 'ok') {
440
+ return {
441
+ safe: false,
442
+ fingerprint: fingerprintText(`${file}:${inspected.fingerprint}`),
443
+ projectPaths: [],
444
+ };
445
+ }
446
+ evidence.push(`${file}:${fingerprintText(inspected.raw)}`);
282
447
  try {
283
- const parsed = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(sessionsDir, file), 'utf8'));
284
- if (typeof parsed.projectPath === 'string' && parsed.projectPath) {
285
- return parsed.projectPath;
448
+ const parsed = JSON.parse(inspected.raw);
449
+ const projectPath = normalizeProjectPath(parsed.projectPath);
450
+ if (projectPath)
451
+ projectPaths.push(projectPath);
452
+ }
453
+ catch { }
454
+ }
455
+ return {
456
+ safe: true,
457
+ fingerprint: fingerprintText(evidence.join('\0')),
458
+ projectPaths,
459
+ };
460
+ }
461
+ function inspectProjectMetadataRepair(projectsDir, projectKey) {
462
+ if (HASH_PROJECT_RE.test(projectKey) || TEMP_PROJECT_RE.test(projectKey)) {
463
+ return { skipped: true };
464
+ }
465
+ const projectDir = (0, path_1.join)(projectsDir, projectKey);
466
+ if (!safeIsDirectory(projectDir))
467
+ return { skipped: true };
468
+ const metadata = inspectProjectMetadata(projectDir, projectKey);
469
+ if (metadata.valid)
470
+ return { skipped: false };
471
+ if (!metadata.repairable)
472
+ return { skipped: true };
473
+ const sessions = inspectProjectSessions(projectDir);
474
+ if (!sessions.safe)
475
+ return { skipped: true };
476
+ const inferredPaths = new Set(sessions.projectPaths);
477
+ if (metadata.projectPath)
478
+ inferredPaths.add(metadata.projectPath);
479
+ if (inferredPaths.size !== 1)
480
+ return { skipped: true };
481
+ const projectPath = [...inferredPaths][0];
482
+ if ((0, config_dir_1.getCanonicalProjectKey)(projectPath) !== projectKey)
483
+ return { skipped: true };
484
+ return {
485
+ skipped: false,
486
+ action: {
487
+ kind: metadata.exists ? 'invalid-project-metadata' : 'missing-project-metadata',
488
+ projectKey,
489
+ projectDir,
490
+ projectPath,
491
+ metadataPath: (0, path_1.join)(projectDir, 'project.json'),
492
+ stateFingerprint: fingerprintText(`${pathIdentityFingerprint(projectDir)}\0${metadata.fingerprint}\0${sessions.fingerprint}`),
493
+ },
494
+ };
495
+ }
496
+ function normalizeProjectPath(value) {
497
+ if (typeof value !== 'string' || !value.trim())
498
+ return undefined;
499
+ return (0, config_dir_1.resolveProjectStoragePath)(value);
500
+ }
501
+ function isPathCleanupKind(kind) {
502
+ return (kind === 'legacy-global-sessions' ||
503
+ kind === 'legacy-global-memory' ||
504
+ kind === 'legacy-hash-project' ||
505
+ kind === 'temp-project' ||
506
+ kind === 'empty-project');
507
+ }
508
+ function quarantinePlannedPath(action) {
509
+ const parent = (0, path_1.dirname)(action.path);
510
+ const quarantinePath = (0, path_1.join)(parent, `.${(0, path_1.basename)(action.path)}.orion-quarantine-${(0, crypto_1.randomBytes)(24).toString('hex')}`);
511
+ try {
512
+ if (pathExistsNoFollow(quarantinePath))
513
+ return { status: 'skipped' };
514
+ (0, fs_1.renameSync)(action.path, quarantinePath);
515
+ }
516
+ catch {
517
+ return { status: 'skipped' };
518
+ }
519
+ const quarantined = inspectPathTreeNoFollow(quarantinePath);
520
+ if (!quarantined || quarantined.fingerprint !== action.stateFingerprint) {
521
+ if (!pathExistsNoFollow(action.path)) {
522
+ try {
523
+ (0, fs_1.renameSync)(quarantinePath, action.path);
524
+ return { status: 'restored' };
286
525
  }
526
+ catch { }
287
527
  }
288
- catch {
289
- // Try the next session file.
528
+ return {
529
+ status: 'skipped',
530
+ quarantinePath: pathExistsNoFollow(quarantinePath) ? quarantinePath : undefined,
531
+ };
532
+ }
533
+ return { status: 'quarantined', quarantinePath };
534
+ }
535
+ function inspectCleanupPathAction(roots, kind, path, projectKey) {
536
+ if (!isExpectedCleanupPath(roots, kind, path, projectKey))
537
+ return undefined;
538
+ const state = inspectPathTreeNoFollow(path);
539
+ if (!state || state.type !== 'directory')
540
+ return undefined;
541
+ if (kind === 'empty-project' && state.bytes !== 0)
542
+ return undefined;
543
+ return { type: 'path', kind, path, projectKey, stateFingerprint: state.fingerprint };
544
+ }
545
+ function isExpectedCleanupPath(roots, kind, path, projectKey) {
546
+ if (kind === 'legacy-global-sessions')
547
+ return path === (0, path_1.join)(roots.configHome, 'sessions');
548
+ if (kind === 'legacy-global-memory')
549
+ return path === (0, path_1.join)(roots.configHome, 'memory');
550
+ if (!projectKey ||
551
+ !isSafeProjectKey(projectKey) ||
552
+ path !== (0, path_1.join)(roots.projectsDir, projectKey)) {
553
+ return false;
554
+ }
555
+ if (kind === 'legacy-hash-project')
556
+ return HASH_PROJECT_RE.test(projectKey);
557
+ if (kind === 'temp-project')
558
+ return TEMP_PROJECT_RE.test(projectKey);
559
+ return !HASH_PROJECT_RE.test(projectKey) && !TEMP_PROJECT_RE.test(projectKey);
560
+ }
561
+ function sameCleanupPathAction(current, planned) {
562
+ return (current.kind === planned.kind &&
563
+ current.path === planned.path &&
564
+ current.projectKey === planned.projectKey &&
565
+ current.stateFingerprint === planned.stateFingerprint);
566
+ }
567
+ function inspectVectorCleanupAction(roots, projectKey) {
568
+ if (!isSafeProjectKey(projectKey) || safeIsDirectory((0, path_1.join)(roots.projectsDir, projectKey))) {
569
+ return undefined;
570
+ }
571
+ const snapshot = readVectorProjectSnapshot(roots.configHome, projectKey);
572
+ if (!snapshot || snapshot.rows.length === 0)
573
+ return undefined;
574
+ return {
575
+ type: 'vector',
576
+ kind: 'vector-orphan-project',
577
+ projectKey,
578
+ rowCount: snapshot.rows.length,
579
+ databaseIdentity: snapshot.databaseIdentity,
580
+ rows: snapshot.rows,
581
+ stateFingerprint: fingerprintText(canonicalJson({ databaseIdentity: snapshot.databaseIdentity, rows: snapshot.rows })),
582
+ };
583
+ }
584
+ function sameVectorCleanupAction(current, planned) {
585
+ return (current.projectKey === planned.projectKey &&
586
+ current.databaseIdentity === planned.databaseIdentity &&
587
+ canonicalJson(current.rows) === canonicalJson(planned.rows) &&
588
+ current.stateFingerprint === planned.stateFingerprint &&
589
+ current.rowCount === planned.rowCount);
590
+ }
591
+ function cleanupActionKey(action) {
592
+ return action.type === 'path' ? action.path : `vector:${action.projectKey}`;
593
+ }
594
+ function sameStorageRoots(configHome, projectsDir) {
595
+ return configHome === (0, config_dir_1.getConfigHome)() && projectsDir === (0, config_dir_1.getProjectsDir)();
596
+ }
597
+ function isSafeProjectKey(projectKey) {
598
+ return (!!projectKey &&
599
+ projectKey !== '.' &&
600
+ projectKey !== '..' &&
601
+ !projectKey.includes('/') &&
602
+ !projectKey.includes('\\'));
603
+ }
604
+ function inspectPathTreeNoFollow(target) {
605
+ let before;
606
+ try {
607
+ before = (0, fs_1.lstatSync)(target);
608
+ }
609
+ catch {
610
+ return undefined;
611
+ }
612
+ if (before.isSymbolicLink())
613
+ return undefined;
614
+ if (before.isFile()) {
615
+ const inspected = readFileNoFollow(target);
616
+ if (inspected.status !== 'ok' || !inspected.raw)
617
+ return undefined;
618
+ return {
619
+ type: 'file',
620
+ bytes: inspected.raw.length,
621
+ fingerprint: fingerprintText(`file:${stableObjectIdentity(before)}:${inspected.fingerprint}`),
622
+ };
623
+ }
624
+ if (!before.isDirectory())
625
+ return undefined;
626
+ let entries;
627
+ try {
628
+ entries = (0, fs_1.readdirSync)(target).sort();
629
+ }
630
+ catch {
631
+ return undefined;
632
+ }
633
+ const children = [];
634
+ let bytes = 0;
635
+ for (const entry of entries) {
636
+ const state = inspectPathTreeNoFollow((0, path_1.join)(target, entry));
637
+ if (!state)
638
+ return undefined;
639
+ bytes += state.bytes;
640
+ children.push(`${entry}:${state.fingerprint}`);
641
+ }
642
+ let after;
643
+ try {
644
+ after = (0, fs_1.lstatSync)(target);
645
+ }
646
+ catch {
647
+ return undefined;
648
+ }
649
+ if (!after.isDirectory() || !sameStatIdentity(before, after))
650
+ return undefined;
651
+ return {
652
+ type: 'directory',
653
+ bytes,
654
+ fingerprint: fingerprintText(`directory:${stableObjectIdentity(after)}\0${children.join('\0')}`),
655
+ };
656
+ }
657
+ function readTextFileNoFollow(path) {
658
+ const inspected = readFileNoFollow(path);
659
+ if (inspected.status !== 'ok')
660
+ return inspected;
661
+ return {
662
+ status: 'ok',
663
+ fingerprint: inspected.fingerprint,
664
+ raw: inspected.raw.toString('utf8'),
665
+ };
666
+ }
667
+ function readFileNoFollow(path) {
668
+ let before;
669
+ try {
670
+ before = (0, fs_1.lstatSync)(path);
671
+ }
672
+ catch (error) {
673
+ const code = error.code;
674
+ return {
675
+ status: code === 'ENOENT' ? 'missing' : 'unreadable',
676
+ fingerprint: fingerprintText(code || 'lstat-error'),
677
+ };
678
+ }
679
+ if (before.isSymbolicLink() || !before.isFile()) {
680
+ return { status: 'unsafe', fingerprint: fingerprintText(`unsafe:${statIdentity(before)}`) };
681
+ }
682
+ let fd;
683
+ try {
684
+ fd = (0, fs_1.openSync)(path, fs_1.constants.O_RDONLY | fs_1.constants.O_NOFOLLOW);
685
+ const opened = (0, fs_1.fstatSync)(fd);
686
+ if (!opened.isFile() || !sameStatIdentity(before, opened)) {
687
+ return { status: 'unsafe', fingerprint: fingerprintText('identity-drift') };
290
688
  }
689
+ const raw = (0, fs_1.readFileSync)(fd);
690
+ const after = (0, fs_1.fstatSync)(fd);
691
+ if (!sameStatIdentity(opened, after)) {
692
+ return { status: 'unreadable', fingerprint: fingerprintText('read-drift') };
693
+ }
694
+ return { status: 'ok', fingerprint: fingerprintText(raw.toString('base64')), raw };
695
+ }
696
+ catch (error) {
697
+ const code = error.code || 'read-error';
698
+ return {
699
+ status: code === 'ELOOP' ? 'unsafe' : 'unreadable',
700
+ fingerprint: fingerprintText(`${code}:${statIdentity(before)}`),
701
+ };
702
+ }
703
+ finally {
704
+ if (fd !== undefined)
705
+ (0, fs_1.closeSync)(fd);
706
+ }
707
+ }
708
+ function statIdentity(stat) {
709
+ return [stat.dev, stat.ino, stat.mode, stat.size, stat.mtimeMs, stat.ctimeMs].join(':');
710
+ }
711
+ function stableObjectIdentity(stat) {
712
+ return [stat.dev, stat.ino, stat.mode].join(':');
713
+ }
714
+ function sameStatIdentity(a, b) {
715
+ return statIdentity(a) === statIdentity(b);
716
+ }
717
+ function pathIdentityFingerprint(path) {
718
+ try {
719
+ const stat = (0, fs_1.lstatSync)(path);
720
+ if (stat.isSymbolicLink())
721
+ return fingerprintText('unsafe-symlink');
722
+ return fingerprintText(stableObjectIdentity(stat));
723
+ }
724
+ catch (error) {
725
+ return fingerprintText(error.code || 'lstat-error');
726
+ }
727
+ }
728
+ function pathExistsNoFollow(path) {
729
+ try {
730
+ (0, fs_1.lstatSync)(path);
731
+ return true;
732
+ }
733
+ catch {
734
+ return false;
291
735
  }
292
- return undefined;
736
+ }
737
+ function safeIsFile(path) {
738
+ try {
739
+ return (0, fs_1.lstatSync)(path).isFile();
740
+ }
741
+ catch {
742
+ return false;
743
+ }
744
+ }
745
+ function isRecord(value) {
746
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
747
+ }
748
+ function cloneStorageMaintenancePlan(plan) {
749
+ return JSON.parse(JSON.stringify(plan));
750
+ }
751
+ function readIssuedStorageMaintenancePlan(token, consume) {
752
+ pruneExpiredStorageMaintenancePlans();
753
+ const match = token.match(/^v2\.([A-Za-z0-9_-]{43})$/);
754
+ if (!match)
755
+ return undefined;
756
+ const issued = issuedStorageMaintenancePlans.get(match[1]);
757
+ if (!issued || issued.expiresAt <= Date.now()) {
758
+ if (issued)
759
+ issuedStorageMaintenancePlans.delete(match[1]);
760
+ return undefined;
761
+ }
762
+ if (consume)
763
+ issuedStorageMaintenancePlans.delete(match[1]);
764
+ return issued.plan;
765
+ }
766
+ function pruneExpiredStorageMaintenancePlans() {
767
+ const now = Date.now();
768
+ for (const [nonce, issued] of issuedStorageMaintenancePlans) {
769
+ if (issued.expiresAt <= now)
770
+ issuedStorageMaintenancePlans.delete(nonce);
771
+ }
772
+ }
773
+ function isSha256(value) {
774
+ return typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
775
+ }
776
+ function isStringArray(value) {
777
+ return Array.isArray(value) && value.every(item => typeof item === 'string');
778
+ }
779
+ function isStorageIssue(value) {
780
+ if (!isRecord(value) || !isStorageIssueKind(value.kind))
781
+ return false;
782
+ return (typeof value.summary === 'string' &&
783
+ typeof value.canCleanup === 'boolean' &&
784
+ (value.path === undefined || typeof value.path === 'string') &&
785
+ (value.projectKey === undefined || typeof value.projectKey === 'string') &&
786
+ (value.bytes === undefined || typeof value.bytes === 'number'));
787
+ }
788
+ function isStorageIssueKind(value) {
789
+ return (value === 'legacy-global-sessions' ||
790
+ value === 'legacy-global-memory' ||
791
+ value === 'legacy-hash-project' ||
792
+ value === 'temp-project' ||
793
+ value === 'empty-project' ||
794
+ value === 'missing-project-metadata' ||
795
+ value === 'invalid-project-metadata' ||
796
+ value === 'vector-orphan-project');
797
+ }
798
+ function isStorageCleanupAction(value) {
799
+ if (!isRecord(value) || !isSha256(value.stateFingerprint))
800
+ return false;
801
+ if (value.type === 'path') {
802
+ return (isPathCleanupKind(value.kind) &&
803
+ typeof value.path === 'string' &&
804
+ (value.projectKey === undefined || typeof value.projectKey === 'string'));
805
+ }
806
+ return (value.type === 'vector' &&
807
+ value.kind === 'vector-orphan-project' &&
808
+ typeof value.projectKey === 'string' &&
809
+ isSafeProjectKey(value.projectKey) &&
810
+ Number.isSafeInteger(value.rowCount) &&
811
+ value.rowCount > 0 &&
812
+ isSha256(value.databaseIdentity) &&
813
+ Array.isArray(value.rows) &&
814
+ value.rows.length === value.rowCount &&
815
+ value.rows.every(isStorageVectorRowSnapshot) &&
816
+ new Set(value.rows.map(row => row.id)).size === value.rows.length &&
817
+ fingerprintText(canonicalJson({ databaseIdentity: value.databaseIdentity, rows: value.rows })) === value.stateFingerprint);
818
+ }
819
+ function isStorageVectorRowSnapshot(value) {
820
+ return (isRecord(value) &&
821
+ typeof value.id === 'string' &&
822
+ typeof value.canonicalRow === 'string' &&
823
+ isSha256(value.rowFingerprint) &&
824
+ fingerprintText(value.canonicalRow) === value.rowFingerprint);
825
+ }
826
+ function isStorageCleanupPlan(value) {
827
+ return (isRecord(value) &&
828
+ value.version === 1 &&
829
+ typeof value.configHome === 'string' &&
830
+ typeof value.projectsDir === 'string' &&
831
+ Array.isArray(value.actions) &&
832
+ value.actions.every(isStorageCleanupAction) &&
833
+ isStringArray(value.skipped) &&
834
+ Array.isArray(value.issues) &&
835
+ value.issues.every(isStorageIssue));
836
+ }
837
+ function isProjectMetadataRepairAction(value) {
838
+ return (isRecord(value) &&
839
+ (value.kind === 'missing-project-metadata' || value.kind === 'invalid-project-metadata') &&
840
+ typeof value.projectKey === 'string' &&
841
+ isSafeProjectKey(value.projectKey) &&
842
+ typeof value.projectDir === 'string' &&
843
+ typeof value.projectPath === 'string' &&
844
+ typeof value.metadataPath === 'string' &&
845
+ isSha256(value.stateFingerprint));
846
+ }
847
+ function isProjectMetadataRepairPlan(value) {
848
+ return (isRecord(value) &&
849
+ value.version === 1 &&
850
+ typeof value.projectsDir === 'string' &&
851
+ Array.isArray(value.actions) &&
852
+ value.actions.every(isProjectMetadataRepairAction) &&
853
+ isStringArray(value.skipped));
854
+ }
855
+ function fingerprintText(value) {
856
+ return (0, crypto_1.createHash)('sha256').update(value).digest('hex');
293
857
  }
294
858
  function inspectVectorProjects(configHome, validProjectKeys) {
295
859
  const dbPath = (0, path_1.join)(configHome, 'vector.db');
296
- if (!(0, fs_1.existsSync)(dbPath))
860
+ if (!safeIsFile(dbPath))
297
861
  return [];
298
862
  let db = null;
299
863
  try {
@@ -301,12 +865,14 @@ function inspectVectorProjects(configHome, validProjectKeys) {
301
865
  const hasMemories = db.prepare("SELECT name FROM sqlite_master WHERE name = 'memories'").get();
302
866
  if (!hasMemories)
303
867
  return [];
304
- const rows = db.prepare(`
868
+ const rows = db
869
+ .prepare(`
305
870
  SELECT COALESCE(project, 'global') as project, COUNT(*) as rows
306
871
  FROM memories
307
872
  GROUP BY COALESCE(project, 'global')
308
873
  ORDER BY rows DESC, project ASC
309
- `).all();
874
+ `)
875
+ .all();
310
876
  return rows.map(row => ({
311
877
  project: row.project,
312
878
  rows: row.rows,
@@ -320,39 +886,140 @@ function inspectVectorProjects(configHome, validProjectKeys) {
320
886
  db?.close();
321
887
  }
322
888
  }
323
- function deleteVectorProjectRows(configHome, project, dryRun) {
889
+ function readVectorProjectSnapshot(configHome, project) {
324
890
  const dbPath = (0, path_1.join)(configHome, 'vector.db');
325
- if (!(0, fs_1.existsSync)(dbPath))
326
- return 0;
327
- const db = new better_sqlite3_1.default(dbPath);
891
+ const databaseIdentity = databaseFileIdentity(dbPath);
892
+ if (!databaseIdentity)
893
+ return undefined;
894
+ let db;
328
895
  try {
896
+ db = new better_sqlite3_1.default(dbPath, { readonly: true });
897
+ db.defaultSafeIntegers(true);
329
898
  const hasMemories = db.prepare("SELECT name FROM sqlite_master WHERE name = 'memories'").get();
330
899
  if (!hasMemories)
331
- return 0;
332
- const ids = db.prepare('SELECT id FROM memories WHERE project = ?').all(project).map(row => row.id);
333
- if (dryRun || ids.length === 0)
334
- return ids.length;
335
- const placeholders = ids.map(() => '?').join(', ');
336
- if (db.prepare("SELECT name FROM sqlite_master WHERE name = 'vec_memories'").get() && db.prepare("SELECT name FROM sqlite_master WHERE name = 'memory_vectors'").get()) {
337
- db.prepare(`DELETE FROM vec_memories WHERE rowid IN (SELECT vector_rowid FROM memory_vectors WHERE memory_id IN (${placeholders}))`).run(...ids);
338
- db.prepare(`DELETE FROM memory_vectors WHERE memory_id IN (${placeholders})`).run(...ids);
339
- }
340
- db.prepare(`DELETE FROM memories WHERE id IN (${placeholders})`).run(...ids);
341
- return ids.length;
900
+ return undefined;
901
+ const rows = readVectorRowsFromDatabase(db, project);
902
+ if (!rows || databaseFileIdentity(dbPath) !== databaseIdentity)
903
+ return undefined;
904
+ return { databaseIdentity, rows };
905
+ }
906
+ catch {
907
+ return undefined;
908
+ }
909
+ finally {
910
+ db?.close();
911
+ }
912
+ }
913
+ function deletePlannedVectorRows(configHome, action) {
914
+ const dbPath = (0, path_1.join)(configHome, 'vector.db');
915
+ if (databaseFileIdentity(dbPath) !== action.databaseIdentity)
916
+ return 0;
917
+ const db = new better_sqlite3_1.default(dbPath);
918
+ db.defaultSafeIntegers(true);
919
+ try {
920
+ const apply = db.transaction(() => {
921
+ const hasMemories = db
922
+ .prepare("SELECT name FROM sqlite_master WHERE name = 'memories'")
923
+ .get();
924
+ if (!hasMemories)
925
+ return 0;
926
+ if (databaseFileIdentity(dbPath) !== action.databaseIdentity)
927
+ return 0;
928
+ const currentRows = readVectorRowsFromDatabase(db, action.projectKey);
929
+ if (!currentRows)
930
+ return 0;
931
+ const currentStateFingerprint = fingerprintText(canonicalJson({ databaseIdentity: action.databaseIdentity, rows: currentRows }));
932
+ if (currentRows.length !== action.rowCount ||
933
+ canonicalJson(currentRows) !== canonicalJson(action.rows) ||
934
+ currentStateFingerprint !== action.stateFingerprint ||
935
+ databaseFileIdentity(dbPath) !== action.databaseIdentity) {
936
+ return 0;
937
+ }
938
+ const currentIds = action.rows.map(row => row.id);
939
+ const placeholders = currentIds.map(() => '?').join(', ');
940
+ if (db.prepare("SELECT name FROM sqlite_master WHERE name = 'vec_memories'").get() &&
941
+ db.prepare("SELECT name FROM sqlite_master WHERE name = 'memory_vectors'").get()) {
942
+ db.prepare(`DELETE FROM vec_memories WHERE rowid IN (SELECT vector_rowid FROM memory_vectors WHERE memory_id IN (${placeholders}))`).run(...currentIds);
943
+ db.prepare(`DELETE FROM memory_vectors WHERE memory_id IN (${placeholders})`).run(...currentIds);
944
+ }
945
+ db.prepare(`DELETE FROM memories WHERE id IN (${placeholders})`).run(...currentIds);
946
+ return currentIds.length;
947
+ });
948
+ return apply.immediate();
342
949
  }
343
950
  finally {
344
951
  db.close();
345
952
  }
346
953
  }
954
+ function readVectorRowsFromDatabase(db, project) {
955
+ const rows = db
956
+ .prepare('SELECT * FROM memories WHERE project = ? ORDER BY id')
957
+ .all(project);
958
+ const snapshots = [];
959
+ for (const row of rows) {
960
+ if (typeof row.id !== 'string')
961
+ return undefined;
962
+ const canonicalRow = canonicalJson(row);
963
+ snapshots.push({
964
+ id: row.id,
965
+ canonicalRow,
966
+ rowFingerprint: fingerprintText(canonicalRow),
967
+ });
968
+ }
969
+ return snapshots;
970
+ }
971
+ function databaseFileIdentity(path) {
972
+ try {
973
+ const stat = (0, fs_1.lstatSync)(path);
974
+ if (!stat.isFile() || stat.isSymbolicLink())
975
+ return undefined;
976
+ return fingerprintText(stableObjectIdentity(stat));
977
+ }
978
+ catch {
979
+ return undefined;
980
+ }
981
+ }
982
+ function canonicalJson(value) {
983
+ return JSON.stringify(toCanonicalJsonValue(value));
984
+ }
985
+ function toCanonicalJsonValue(value) {
986
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
987
+ return value;
988
+ if (typeof value === 'number') {
989
+ if (Number.isFinite(value))
990
+ return Object.is(value, -0) ? 0 : value;
991
+ return { $number: String(value) };
992
+ }
993
+ if (typeof value === 'bigint')
994
+ return { $bigint: value.toString() };
995
+ if (Buffer.isBuffer(value))
996
+ return { $buffer: value.toString('base64') };
997
+ if (Array.isArray(value))
998
+ return value.map(toCanonicalJsonValue);
999
+ if (isRecord(value)) {
1000
+ const result = {};
1001
+ for (const key of Object.keys(value).sort()) {
1002
+ result[key] = toCanonicalJsonValue(value[key]);
1003
+ }
1004
+ return result;
1005
+ }
1006
+ return { $unsupported: String(value) };
1007
+ }
347
1008
  function dirSize(target) {
348
- if (!(0, fs_1.existsSync)(target))
1009
+ let stat;
1010
+ try {
1011
+ stat = (0, fs_1.lstatSync)(target);
1012
+ }
1013
+ catch {
1014
+ return 0;
1015
+ }
1016
+ if (stat.isSymbolicLink())
1017
+ return 0;
1018
+ if (stat.isFile())
1019
+ return stat.size;
1020
+ if (!stat.isDirectory())
349
1021
  return 0;
350
1022
  try {
351
- const stat = (0, fs_1.statSync)(target);
352
- if (stat.isFile())
353
- return stat.size;
354
- if (!stat.isDirectory())
355
- return 0;
356
1023
  return (0, fs_1.readdirSync)(target).reduce((sum, entry) => sum + dirSize((0, path_1.join)(target, entry)), 0);
357
1024
  }
358
1025
  catch {
@@ -361,7 +1028,7 @@ function dirSize(target) {
361
1028
  }
362
1029
  function safeIsDirectory(target) {
363
1030
  try {
364
- return (0, fs_1.statSync)(target).isDirectory();
1031
+ return (0, fs_1.lstatSync)(target).isDirectory();
365
1032
  }
366
1033
  catch {
367
1034
  return false;