@bitkyc08/opencodex 2.52.0-preview.20260912 → 2.53.0-preview.20260913

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 (236) hide show
  1. package/gui/dist/assets/index-BBOZWGB6.css +1 -0
  2. package/gui/dist/assets/index-D7ynYo2K.js +128 -0
  3. package/gui/dist/index.html +2 -2
  4. package/native/remote-workspace-helper/Cargo.lock +130 -0
  5. package/native/remote-workspace-helper/Cargo.toml +24 -0
  6. package/native/remote-workspace-helper/src/main.rs +49 -0
  7. package/native/remote-workspace-helper/src/protocol.rs +246 -0
  8. package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
  9. package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
  10. package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
  11. package/package.json +6 -1
  12. package/src/adapters/anthropic-image-normalize.ts +30 -2
  13. package/src/adapters/anthropic.ts +1 -1
  14. package/src/adapters/base.ts +8 -2
  15. package/src/adapters/cursor/cursor-errors.ts +12 -0
  16. package/src/adapters/cursor/thread-continuity.ts +93 -0
  17. package/src/adapters/cursor.ts +104 -73
  18. package/src/adapters/devin/cloud-direct/chat.ts +312 -23
  19. package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
  20. package/src/adapters/devin/live-models.ts +70 -3
  21. package/src/adapters/devin.ts +281 -21
  22. package/src/adapters/google-wire-compiler.ts +14 -6
  23. package/src/adapters/google.ts +22 -8
  24. package/src/adapters/kiro/adapter.ts +316 -0
  25. package/src/adapters/kiro/conversation.ts +136 -0
  26. package/src/adapters/kiro/payload.ts +432 -0
  27. package/src/adapters/kiro/reasoning.ts +56 -0
  28. package/src/adapters/kiro/stream.ts +1153 -0
  29. package/src/adapters/kiro/usage.ts +223 -0
  30. package/src/adapters/kiro/wire.ts +76 -0
  31. package/src/adapters/kiro.ts +8 -2319
  32. package/src/adapters/mimo-free.ts +1 -1
  33. package/src/adapters/openai-chat-images.ts +101 -0
  34. package/src/adapters/openai-chat.ts +201 -181
  35. package/src/adapters/openai-responses.ts +92 -224
  36. package/src/adapters/registry.ts +0 -7
  37. package/src/adapters/run-turn-queue.ts +13 -6
  38. package/src/bridge.ts +14 -15
  39. package/src/chat/inbound.ts +29 -4
  40. package/src/chat/outbound.ts +145 -107
  41. package/src/claude/desktop-profile.ts +4 -6
  42. package/src/cli/account-api.ts +14 -0
  43. package/src/cli/account-extended.ts +1 -1
  44. package/src/cli/account-history.ts +60 -0
  45. package/src/cli/account-main.ts +80 -0
  46. package/src/cli/account.ts +11 -3
  47. package/src/cli/capabilities.ts +113 -0
  48. package/src/cli/catalog.ts +109 -0
  49. package/src/cli/dispatch.ts +9 -0
  50. package/src/cli/help.ts +2 -0
  51. package/src/cli/index.ts +2 -2
  52. package/src/cli/observe.ts +28 -1
  53. package/src/cli/opencode.ts +42 -8
  54. package/src/cli/provider-runtime.ts +11 -1
  55. package/src/cli/provider.ts +22 -2
  56. package/src/cli/registry.ts +21 -0
  57. package/src/cli/remote-workspace.ts +154 -0
  58. package/src/cli/status.ts +39 -7
  59. package/src/cli/usage-report.ts +14 -2
  60. package/src/client/hub-client.ts +34 -0
  61. package/src/client/hub-state.ts +9 -1
  62. package/src/codex/account-store.ts +78 -0
  63. package/src/codex/auth-api.ts +81 -54
  64. package/src/codex/auth-context.ts +45 -16
  65. package/src/codex/catalog/effort.ts +1 -1
  66. package/src/codex/catalog/metadata.ts +3 -6
  67. package/src/codex/catalog/native-models.ts +4 -4
  68. package/src/codex/catalog/parsing.ts +2 -20
  69. package/src/codex/catalog/provider-fetch.ts +10 -1
  70. package/src/codex/catalog/remote.ts +233 -0
  71. package/src/codex/catalog/sync.ts +403 -35
  72. package/src/codex/convergence.ts +1 -1
  73. package/src/codex/history-manifest.ts +36 -0
  74. package/src/codex/history-provider.ts +32 -5
  75. package/src/codex/inject.ts +9 -0
  76. package/src/codex/main-account.ts +113 -0
  77. package/src/codex/main-device-reauth-api.ts +89 -0
  78. package/src/codex/main-device-reauth.ts +217 -0
  79. package/src/codex/native-residue.ts +9 -2
  80. package/src/codex/quota-auto-refresh.ts +3 -2
  81. package/src/codex/quota-capacity.ts +98 -0
  82. package/src/codex/quota-history.ts +160 -0
  83. package/src/codex/quota-types.ts +8 -0
  84. package/src/codex/quota.ts +118 -91
  85. package/src/codex/refresh.ts +2 -1
  86. package/src/codex/routing.ts +90 -17
  87. package/src/codex/sync.ts +33 -4
  88. package/src/combos/request.ts +19 -1
  89. package/src/config/multi-agent-surface.ts +61 -0
  90. package/src/config/provider-validation.ts +176 -0
  91. package/src/config.ts +213 -11
  92. package/src/generated/compatibility-version.json +436 -168
  93. package/src/images/loop.ts +119 -36
  94. package/src/lib/admission.ts +12 -6
  95. package/src/lib/redact.ts +7 -0
  96. package/src/lib/translator-budget.ts +4 -3
  97. package/src/lib/windows-atomic-replace.ts +1 -0
  98. package/src/lib/windows-elevation.ts +1 -1
  99. package/src/oauth/chatgpt-device.ts +62 -5
  100. package/src/oauth/devin/cli-import.ts +130 -0
  101. package/src/oauth/devin.ts +63 -8
  102. package/src/oauth/index.ts +29 -14
  103. package/src/oauth/kiro.ts +18 -6
  104. package/src/oauth/login-cli.ts +9 -1
  105. package/src/oauth/meta-muse-device.ts +464 -0
  106. package/src/oauth/meta-muse.ts +123 -32
  107. package/src/oauth/pool-kernel.ts +9 -0
  108. package/src/oauth/pool-settings-capability.ts +2 -2
  109. package/src/oauth/store.ts +57 -0
  110. package/src/oauth/types.ts +31 -0
  111. package/src/providers/derive.ts +13 -3
  112. package/src/providers/devin-cli-authmode-migration.ts +57 -35
  113. package/src/providers/devin-provider-merge-migration.ts +240 -0
  114. package/src/providers/muse-key-quota.ts +117 -0
  115. package/src/providers/muse-subscription-usage.ts +14 -2
  116. package/src/providers/openai-sidecar.ts +25 -3
  117. package/src/providers/opencode-zen-rate-limit.ts +58 -0
  118. package/src/providers/provider-id-rewrite.ts +20 -5
  119. package/src/providers/quota-types.ts +12 -0
  120. package/src/providers/quota.ts +143 -102
  121. package/src/providers/reasoning-metadata.ts +543 -0
  122. package/src/providers/registry.ts +80 -49
  123. package/src/reasoning-effort.ts +26 -2
  124. package/src/remote/hub-usage.ts +32 -0
  125. package/src/remote-control/index.ts +192 -41
  126. package/src/remote-control/workspace-activation.ts +9 -0
  127. package/src/remote-control/workspace-agent-connection.ts +366 -0
  128. package/src/remote-control/workspace-claude-runtime.ts +243 -0
  129. package/src/remote-control/workspace-codex-runtime.ts +531 -0
  130. package/src/remote-control/workspace-codex-sandbox.ts +115 -0
  131. package/src/remote-control/workspace-command-runner.ts +748 -0
  132. package/src/remote-control/workspace-coordinator.ts +231 -0
  133. package/src/remote-control/workspace-device.ts +585 -0
  134. package/src/remote-control/workspace-executable.ts +43 -0
  135. package/src/remote-control/workspace-executor.ts +397 -0
  136. package/src/remote-control/workspace-hub.ts +519 -0
  137. package/src/remote-control/workspace-pi-runtime.ts +382 -0
  138. package/src/remote-control/workspace-process.ts +129 -0
  139. package/src/remote-control/workspace-rpc.ts +304 -0
  140. package/src/remote-control/workspace-runtime.ts +60 -0
  141. package/src/remote-control/workspace-secret-store.ts +39 -0
  142. package/src/remote-control/workspace-sessions.ts +799 -0
  143. package/src/remote-control/workspace-tool-bridge.ts +192 -0
  144. package/src/responses/code-mode-helper-compat.ts +22 -3
  145. package/src/responses/hosted-tool-policy.ts +0 -1
  146. package/src/responses/muse-tool-name-alias.ts +379 -0
  147. package/src/responses/plaintext-v2-agent-messages.ts +902 -0
  148. package/src/router.ts +7 -0
  149. package/src/routing/compatibility/behavior.ts +0 -1
  150. package/src/server/audio-client.ts +64 -0
  151. package/src/server/audio-dictation.ts +91 -0
  152. package/src/server/audio-live.ts +185 -0
  153. package/src/server/audio-transcriptions.ts +183 -0
  154. package/src/server/audio-upstream.ts +153 -0
  155. package/src/server/auth-cors.ts +61 -2
  156. package/src/server/chat-completions.ts +1 -1
  157. package/src/server/chat-native-sse.ts +92 -48
  158. package/src/server/chat-native.ts +37 -15
  159. package/src/server/hub-usage.ts +57 -0
  160. package/src/server/images.ts +4 -0
  161. package/src/server/index.ts +722 -57
  162. package/src/server/lifecycle.ts +5 -6
  163. package/src/server/live-call-bindings.ts +60 -0
  164. package/src/server/live.ts +12 -1
  165. package/src/server/management/agent-settings-routes.ts +25 -4
  166. package/src/server/management/api-access.ts +37 -0
  167. package/src/server/management/api-key-usage.ts +7 -2
  168. package/src/server/management/config-routes.ts +1 -18
  169. package/src/server/management/context.ts +15 -0
  170. package/src/server/management/logs-usage-routes.ts +2 -0
  171. package/src/server/management/oauth-account-routes.ts +39 -12
  172. package/src/server/management/provider-routes.ts +125 -2
  173. package/src/server/management/remote-workspace-routes.ts +140 -0
  174. package/src/server/management/route-registry.ts +15 -0
  175. package/src/server/management/usage-aggregate-cache.ts +14 -15
  176. package/src/server/management/usage-summary-cache.ts +2 -0
  177. package/src/server/management-api.ts +23 -0
  178. package/src/server/ports.ts +17 -0
  179. package/src/server/relay-eager.ts +4 -1
  180. package/src/server/relay.ts +70 -10
  181. package/src/server/request-decompress.ts +6 -3
  182. package/src/server/responses/agent-task-recovery.ts +25 -32
  183. package/src/server/responses/codex-auth-error.ts +11 -0
  184. package/src/server/responses/codex-ws-exchange.ts +52 -3
  185. package/src/server/responses/codex-ws-wire.ts +55 -0
  186. package/src/server/responses/compact.ts +9 -1
  187. package/src/server/responses/core.ts +337 -73
  188. package/src/server/responses/encrypted-payload.ts +45 -2
  189. package/src/server/responses/ws-upstream.ts +4 -1
  190. package/src/server/responses-self-named-namespace-scrub.ts +1 -3
  191. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  192. package/src/server/search.ts +3 -0
  193. package/src/server/sse-payload-rewrite.ts +136 -51
  194. package/src/server/ws-bridge.ts +35 -1
  195. package/src/service/cli.ts +372 -0
  196. package/src/service/diagnostics.ts +340 -0
  197. package/src/service/guards.ts +303 -0
  198. package/src/service/health.ts +222 -0
  199. package/src/service/launchd.ts +853 -0
  200. package/src/service/orchestration.ts +617 -0
  201. package/src/service/repair.ts +334 -0
  202. package/src/service/state.ts +363 -0
  203. package/src/service/systemd.ts +229 -0
  204. package/src/service/windows-ops.ts +690 -0
  205. package/src/service/windows-scheduler.ts +769 -0
  206. package/src/service/windows-taskxml.ts +613 -0
  207. package/src/service.ts +22 -5550
  208. package/src/storage/cleanup/db.ts +258 -0
  209. package/src/storage/cleanup/execute.ts +358 -0
  210. package/src/storage/cleanup/paths.ts +189 -0
  211. package/src/storage/cleanup/pending.ts +140 -0
  212. package/src/storage/cleanup/preview.ts +292 -0
  213. package/src/storage/cleanup/reconcile.ts +347 -0
  214. package/src/storage/cleanup/restore.ts +932 -0
  215. package/src/storage/cleanup/satellite.ts +474 -0
  216. package/src/storage/cleanup/staging.ts +129 -0
  217. package/src/storage/cleanup/types.ts +98 -0
  218. package/src/storage/cleanup.ts +49 -3127
  219. package/src/types/accounts.ts +2 -0
  220. package/src/types/config.ts +13 -12
  221. package/src/types/provider.ts +37 -0
  222. package/src/types/request.ts +2 -0
  223. package/src/types/tools.ts +17 -5
  224. package/src/types.ts +1 -0
  225. package/src/usage/expected-prices.ts +127 -0
  226. package/src/usage/log.ts +58 -1
  227. package/src/vision/eligibility.ts +13 -2
  228. package/src/web-search/loop.ts +56 -3
  229. package/gui/dist/assets/index-D_t6sCWs.js +0 -115
  230. package/gui/dist/assets/index-EdoPnm9_.css +0 -1
  231. package/src/adapters/devin-cli/acp.ts +0 -204
  232. package/src/adapters/devin-cli/adapter.ts +0 -345
  233. package/src/adapters/devin-cli/binary.ts +0 -69
  234. package/src/adapters/devin-cli/models.ts +0 -57
  235. package/src/oauth/devin-cli.ts +0 -149
  236. package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
@@ -0,0 +1,347 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { Database } from "bun:sqlite";
4
+ import { SQLITE_ID_CHUNK, chunkIds, columnExists, mapDbError, openDbWritable, tableExists } from "./db";
5
+ import type { ReconcileErr, RuntimeDbPaths } from "./db";
6
+ import { normalizeArchivedRolloutPath } from "./paths";
7
+ import { beginSatelliteWriteLocks, clearSatelliteBackup, commitSatelliteLock, deleteGoalsInTx, deleteLogsInTx, deleteMemoriesInTx, readConsolidateGlobalJob, restoreSatelliteBackup, rollbackAllSatelliteLocks, snapshotSatelliteBackupInLocks, snapshotStateDependents, writeSatelliteBackup } from "./satellite";
8
+ import type { SatelliteBackup, SatelliteWriteLocks } from "./satellite";
9
+ import type { ArchivedCandidate, CleanupErrorCode } from "./types";
10
+
11
+ export interface ThreadSnapshot {
12
+ id: string;
13
+ rollout_path: string;
14
+ archived: number | null;
15
+ history_mode?: string | null;
16
+ is_pinned?: number | null;
17
+ }
18
+
19
+ /**
20
+ * Load archived threads matching the candidate set.
21
+ * Optional columns are detected via PRAGMA; missing `threads` / query failures throw
22
+ * so callers map to `db_reconcile_failed` / `codex_busy` instead of treating them as empty.
23
+ */
24
+ function loadMatchingThreads(db: Database, candidates: ArchivedCandidate[], codexHome: string): ThreadSnapshot[] {
25
+ if (!tableExists(db, "threads")) {
26
+ throw new Error("missing_threads_table");
27
+ }
28
+ const logicalSet = new Set(candidates.map(c => c.relPath));
29
+ const hasArchived = columnExists(db, "threads", "archived");
30
+ const hasHistoryMode = columnExists(db, "threads", "history_mode");
31
+ const hasIsPinned = columnExists(db, "threads", "is_pinned");
32
+ const selectCols = ["id", "rollout_path"];
33
+ if (hasArchived) selectCols.push("archived");
34
+ if (hasHistoryMode) selectCols.push("history_mode");
35
+ if (hasIsPinned) selectCols.push("is_pinned");
36
+ const rows = db.query<
37
+ { id: string; rollout_path: string; archived?: number | null; history_mode?: string | null; is_pinned?: number | null },
38
+ []
39
+ >(`SELECT ${selectCols.join(", ")} FROM threads`).all();
40
+
41
+ return rows
42
+ .filter(row => {
43
+ // When the archived column is present, only archived=1 rows may be deleted.
44
+ if (hasArchived && Number(row.archived ?? 0) !== 1) {
45
+ return false;
46
+ }
47
+ const normalized = normalizeArchivedRolloutPath(row.rollout_path, codexHome);
48
+ return normalized !== null && logicalSet.has(normalized);
49
+ })
50
+ .map(row => ({
51
+ id: row.id,
52
+ rollout_path: row.rollout_path,
53
+ archived: hasArchived ? (row.archived ?? null) : null,
54
+ history_mode: hasHistoryMode ? (row.history_mode ?? null) : null,
55
+ is_pinned: hasIsPinned ? (row.is_pinned ?? null) : null,
56
+ }));
57
+ }
58
+
59
+ /**
60
+ * Partition matched threads into deletable and referenced snapshots. Linked spawn/fork
61
+ * history and paginated histories stay in the skipped set. Throws real DB errors.
62
+ */
63
+ function filterReferencedHistory(
64
+ db: Database,
65
+ threads: ThreadSnapshot[],
66
+ ): { safe: ThreadSnapshot[]; skipped: ThreadSnapshot[] } {
67
+ let safe = threads.filter(t => (t.history_mode ?? "").toLowerCase() !== "paginated");
68
+ const skipped = new Map(threads
69
+ .filter(t => (t.history_mode ?? "").toLowerCase() === "paginated")
70
+ .map(t => [t.id, t]));
71
+
72
+ while (safe.length > 0) {
73
+ const idSet = new Set(safe.map(t => t.id));
74
+ const unsafeIds = new Set<string>();
75
+
76
+ // Spawn edges that cross the delete boundary keep history reachable.
77
+ if (tableExists(db, "thread_spawn_edges")) {
78
+ for (const chunk of chunkIds([...idSet], SQLITE_ID_CHUNK)) {
79
+ const placeholders = chunk.map(() => "?").join(",");
80
+ const edges = db.query<{ parent_thread_id: string; child_thread_id: string }, string[]>(
81
+ `SELECT parent_thread_id, child_thread_id FROM thread_spawn_edges
82
+ WHERE parent_thread_id IN (${placeholders}) OR child_thread_id IN (${placeholders})`,
83
+ ).all(...chunk, ...chunk);
84
+ for (const edge of edges) {
85
+ if (!idSet.has(edge.parent_thread_id)) unsafeIds.add(edge.child_thread_id);
86
+ if (!idSet.has(edge.child_thread_id)) unsafeIds.add(edge.parent_thread_id);
87
+ }
88
+ }
89
+ }
90
+
91
+ // Other threads that list one of ours as forked_from / parent (when columns exist).
92
+ for (const column of ["forked_from_id", "parent_thread_id", "source_thread_id"] as const) {
93
+ if (!columnExists(db, "threads", column)) continue;
94
+ for (const chunk of chunkIds([...idSet], SQLITE_ID_CHUNK * 2)) {
95
+ const placeholders = chunk.map(() => "?").join(",");
96
+ const rows = db.query<{ id: string; ref: string }, string[]>(
97
+ `SELECT id, ${column} AS ref FROM threads WHERE ${column} IN (${placeholders})`,
98
+ ).all(...chunk);
99
+ for (const row of rows) {
100
+ if (!idSet.has(row.id)) unsafeIds.add(row.ref);
101
+ }
102
+ }
103
+ }
104
+
105
+ if (unsafeIds.size === 0) break;
106
+ for (const thread of safe) {
107
+ if (unsafeIds.has(thread.id)) skipped.set(thread.id, thread);
108
+ }
109
+ safe = safe.filter(thread => !unsafeIds.has(thread.id));
110
+ }
111
+
112
+ return { safe, skipped: [...skipped.values()] };
113
+ }
114
+
115
+ function deleteThreadsAndDependents(db: Database, threadIds: string[]): void {
116
+ if (threadIds.length === 0) return;
117
+
118
+ // Upstream deletes dynamic tools before spawn edges before threads.
119
+ if (tableExists(db, "thread_dynamic_tools")) {
120
+ for (const chunk of chunkIds(threadIds, SQLITE_ID_CHUNK * 2)) {
121
+ const placeholders = chunk.map(() => "?").join(",");
122
+ db.run(`DELETE FROM thread_dynamic_tools WHERE thread_id IN (${placeholders})`, chunk);
123
+ }
124
+ }
125
+
126
+ if (tableExists(db, "thread_spawn_edges")) {
127
+ for (const chunk of chunkIds(threadIds, SQLITE_ID_CHUNK)) {
128
+ const placeholders = chunk.map(() => "?").join(",");
129
+ db.run(
130
+ `DELETE FROM thread_spawn_edges WHERE parent_thread_id IN (${placeholders}) OR child_thread_id IN (${placeholders})`,
131
+ [...chunk, ...chunk],
132
+ );
133
+ }
134
+ }
135
+
136
+ for (const chunk of chunkIds(threadIds, SQLITE_ID_CHUNK * 2)) {
137
+ const placeholders = chunk.map(() => "?").join(",");
138
+ db.run(`DELETE FROM threads WHERE id IN (${placeholders})`, chunk);
139
+ }
140
+ }
141
+
142
+ interface ReconcileOk {
143
+ ok: true;
144
+ threads: ThreadSnapshot[];
145
+ skipped: ThreadSnapshot[];
146
+ }
147
+
148
+ interface ReconcileTestHooks {
149
+ /** Runs at the top of reconcileDeletedThreads, before the write lock is taken. */
150
+ beforeReconcileLock?: () => void;
151
+ failAfterLogsMutation?: boolean;
152
+ failAfterMemoriesMutation?: boolean;
153
+ failAfterGoalsMutation?: boolean;
154
+ failBeforeStateCommit?: boolean;
155
+ failSatelliteRestore?: boolean;
156
+ failSatelliteBackupWrite?: boolean;
157
+ /**
158
+ * Fail a satellite-backup.json *replacement* after the temp is durable but before
159
+ * rename — exercises crash-safety of the post-memories rewrite without truncating
160
+ * the last valid backup.
161
+ */
162
+ failSatelliteBackupReplace?: boolean;
163
+ /** Runs after satellite deletes are committed, before state thread deletion. */
164
+ afterSatelliteMutations?: () => void;
165
+ }
166
+
167
+ /** Delete snapshotted primary-key rows and commit each satellite write transaction. */
168
+ function deleteAndCommitSatellites(
169
+ locks: SatelliteWriteLocks,
170
+ backup: SatelliteBackup,
171
+ stageDir: string,
172
+ hooks?: ReconcileTestHooks,
173
+ ): void {
174
+ try {
175
+ if (locks.logs && backup.logs) {
176
+ deleteLogsInTx(locks.logs.db, backup.logs.rows);
177
+ commitSatelliteLock(locks.logs);
178
+ locks.logs = undefined;
179
+ if (hooks?.failAfterLogsMutation) throw new Error("test_fail_after_logs");
180
+ }
181
+ if (locks.memories && backup.memories) {
182
+ deleteMemoriesInTx(locks.memories.db, backup.memories);
183
+ if (backup.memories.consolidateTouched) {
184
+ // Capture under the write lock, but persist only after COMMIT+close.
185
+ // Holding BEGIN IMMEDIATE across a durable backup rewrite lets Windows CI
186
+ // disk/AV latency stall the lock long enough for concurrent reopen hooks
187
+ // (and bun's default 5s test timeout) to hang — see PR #558 windows-latest.
188
+ backup.memories.consolidatePostImage = readConsolidateGlobalJob(locks.memories.db);
189
+ }
190
+ commitSatelliteLock(locks.memories);
191
+ locks.memories = undefined;
192
+ if (backup.memories.consolidateTouched) {
193
+ writeSatelliteBackup(stageDir, backup, {
194
+ failReplaceBeforeRename: hooks?.failSatelliteBackupReplace,
195
+ });
196
+ }
197
+ if (hooks?.failAfterMemoriesMutation) throw new Error("test_fail_after_memories");
198
+ }
199
+ if (locks.goals && backup.goals) {
200
+ deleteGoalsInTx(locks.goals.db, backup.goals);
201
+ commitSatelliteLock(locks.goals);
202
+ locks.goals = undefined;
203
+ if (hooks?.failAfterGoalsMutation) throw new Error("test_fail_after_goals");
204
+ }
205
+ } catch (error) {
206
+ rollbackAllSatelliteLocks(locks);
207
+ throw error;
208
+ }
209
+ }
210
+
211
+ /** Load matching archived threads and retain referenced history — no deletes yet. */
212
+ export function loadThreadsForCleanup(
213
+ stateDbPath: string,
214
+ candidates: ArchivedCandidate[],
215
+ codexHome: string,
216
+ busyTimeoutMs: number,
217
+ ): ReconcileOk | ReconcileErr {
218
+ if (!stateDbPath || !existsSync(stateDbPath)) return { ok: true, threads: [], skipped: [] };
219
+ let db: Database | undefined;
220
+ try {
221
+ db = openDbWritable(stateDbPath, busyTimeoutMs);
222
+ const threads = loadMatchingThreads(db, candidates, codexHome);
223
+ if (threads.some(t => Number(t.is_pinned ?? 0) === 1)) {
224
+ return { ok: false, error: "pinned_thread" };
225
+ }
226
+ const filtered = filterReferencedHistory(db, threads);
227
+ return { ok: true, threads: filtered.safe, skipped: filtered.skipped };
228
+ } catch (error) {
229
+ return { ok: false, error: mapDbError(error) };
230
+ } finally {
231
+ try { db?.close(); } catch { /* */ }
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Reconcile all Codex per-thread stores for the matched archived candidates.
237
+ *
238
+ * Freezes the thread-ID set under the state write lock, persists a complete
239
+ * satellite backup, then mutates satellites (logs → memories → goals). Any later
240
+ * failure restores satellite rows before the caller restores staged files.
241
+ */
242
+ export function reconcileDeletedThreads(
243
+ paths: RuntimeDbPaths,
244
+ candidates: ArchivedCandidate[],
245
+ codexHome: string,
246
+ busyTimeoutMs: number,
247
+ stageDir: string,
248
+ hooks?: ReconcileTestHooks,
249
+ ): ReconcileOk | ReconcileErr {
250
+ if (!paths.state || !existsSync(paths.state)) return { ok: true, threads: [], skipped: [] };
251
+
252
+ if (hooks?.beforeReconcileLock) hooks.beforeReconcileLock();
253
+
254
+ let stateDb: Database | undefined;
255
+ let backup: SatelliteBackup | undefined;
256
+ let satellitesMutated = false;
257
+ let satelliteLocks: SatelliteWriteLocks | undefined;
258
+
259
+ const failWithRestore = (error: CleanupErrorCode, mapped?: CleanupErrorCode): ReconcileErr => {
260
+ const code = mapped ?? error;
261
+ let satelliteRestoreFailed = false;
262
+ if (satellitesMutated && backup) {
263
+ satelliteRestoreFailed = !restoreSatelliteBackup(
264
+ backup,
265
+ busyTimeoutMs,
266
+ Boolean(hooks?.failSatelliteRestore),
267
+ );
268
+ // Keep on-disk backup + manifest when restore cannot complete.
269
+ if (!satelliteRestoreFailed) clearSatelliteBackup(stageDir);
270
+ } else {
271
+ clearSatelliteBackup(stageDir);
272
+ }
273
+ return {
274
+ ok: false,
275
+ error: code,
276
+ ...(satelliteRestoreFailed ? { satelliteRestoreFailed: true } : {}),
277
+ };
278
+ };
279
+
280
+ try {
281
+ stateDb = openDbWritable(paths.state, busyTimeoutMs);
282
+ stateDb.exec("BEGIN IMMEDIATE");
283
+
284
+ // Freeze the exact delete set under the write lock before any satellite mutation.
285
+ const threads = loadMatchingThreads(stateDb, candidates, codexHome);
286
+ // A pin applied after selection must stop the delete, even though the
287
+ // staged files are already in trash staging — the caller restores them.
288
+ if (threads.some(t => Number(t.is_pinned ?? 0) === 1)) {
289
+ stateDb.exec("ROLLBACK");
290
+ return { ok: false, error: "pinned_thread" };
291
+ }
292
+ if (filterReferencedHistory(stateDb, threads).safe.length !== threads.length) {
293
+ stateDb.exec("ROLLBACK");
294
+ return { ok: false, error: "referenced_history" };
295
+ }
296
+ const threadIds = threads.map(t => t.id);
297
+
298
+ satelliteLocks = beginSatelliteWriteLocks(paths, busyTimeoutMs);
299
+ try {
300
+ backup = snapshotSatelliteBackupInLocks(satelliteLocks, threadIds);
301
+ const stateDeps = snapshotStateDependents(stateDb, threadIds);
302
+ backup.threads = stateDeps.threads;
303
+ backup.dynamicTools = stateDeps.dynamicTools;
304
+ backup.spawnEdges = stateDeps.spawnEdges;
305
+ try {
306
+ writeSatelliteBackup(stageDir, backup, {
307
+ failWrite: hooks?.failSatelliteBackupWrite,
308
+ });
309
+ } catch {
310
+ rollbackAllSatelliteLocks(satelliteLocks);
311
+ stateDb.exec("ROLLBACK");
312
+ clearSatelliteBackup(stageDir);
313
+ return { ok: false, error: "fs_failed" };
314
+ }
315
+
316
+ const hasSatelliteWork = Boolean(backup.logs || backup.memories || backup.goals);
317
+ if (hasSatelliteWork) {
318
+ satellitesMutated = true;
319
+ deleteAndCommitSatellites(satelliteLocks, backup, stageDir, hooks);
320
+ } else {
321
+ rollbackAllSatelliteLocks(satelliteLocks);
322
+ }
323
+ satelliteLocks = undefined;
324
+
325
+ if (hooks?.afterSatelliteMutations) hooks.afterSatelliteMutations();
326
+
327
+ // Re-check under the same lock before committing state deletes.
328
+ if (filterReferencedHistory(stateDb, threads).safe.length !== threads.length) {
329
+ stateDb.exec("ROLLBACK");
330
+ return failWithRestore("referenced_history");
331
+ }
332
+ deleteThreadsAndDependents(stateDb, threadIds);
333
+ if (hooks?.failBeforeStateCommit) throw new Error("test_fail_before_state_commit");
334
+ stateDb.exec("COMMIT");
335
+ // Keep satellite-backup.json for quarantine restore; permanent purge removes the stage.
336
+ return { ok: true, threads, skipped: [] };
337
+ } catch (error) {
338
+ if (satelliteLocks) rollbackAllSatelliteLocks(satelliteLocks);
339
+ throw error;
340
+ }
341
+ } catch (error) {
342
+ try { stateDb?.exec("ROLLBACK"); } catch { /* */ }
343
+ return failWithRestore("db_reconcile_failed", mapDbError(error));
344
+ } finally {
345
+ try { stateDb?.close(); } catch { /* */ }
346
+ }
347
+ }