@gkorepanov/ccodex 0.3.0 → 0.4.0

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 (201) hide show
  1. package/README.md +141 -63
  2. package/assets/ccodex/goals/continuation.md +1 -0
  3. package/compatibility.json +5 -6
  4. package/config.example.toml +19 -4
  5. package/dist/claude/asyncQueue.d.ts +3 -0
  6. package/dist/claude/asyncQueue.js +13 -2
  7. package/dist/claude/asyncQueue.js.map +1 -1
  8. package/dist/claude/commandActions.d.ts +6 -0
  9. package/dist/claude/commandActions.js +57 -0
  10. package/dist/claude/commandActions.js.map +1 -0
  11. package/dist/claude/compactCommand.d.ts +2 -0
  12. package/dist/claude/compactCommand.js +7 -0
  13. package/dist/claude/compactCommand.js.map +1 -0
  14. package/dist/claude/goalTools.d.ts +15 -0
  15. package/dist/claude/goalTools.js +128 -0
  16. package/dist/claude/goalTools.js.map +1 -0
  17. package/dist/claude/hookMapper.d.ts +17 -4
  18. package/dist/claude/hookMapper.js +7 -5
  19. package/dist/claude/hookMapper.js.map +1 -1
  20. package/dist/claude/inputMapper.d.ts +1 -0
  21. package/dist/claude/inputMapper.js +1 -0
  22. package/dist/claude/inputMapper.js.map +1 -1
  23. package/dist/claude/rateLimits.d.ts +72 -0
  24. package/dist/claude/rateLimits.js +330 -0
  25. package/dist/claude/rateLimits.js.map +1 -0
  26. package/dist/claude/responseItemValidation.d.ts +2 -0
  27. package/dist/claude/responseItemValidation.js +40 -0
  28. package/dist/claude/responseItemValidation.js.map +1 -0
  29. package/dist/claude/resultClassifier.d.ts +8 -2
  30. package/dist/claude/resultClassifier.js +9 -1
  31. package/dist/claude/resultClassifier.js.map +1 -1
  32. package/dist/claude/service.d.ts +116 -29
  33. package/dist/claude/service.js +1337 -680
  34. package/dist/claude/service.js.map +1 -1
  35. package/dist/claude/session/backgroundOutput.d.ts +2 -0
  36. package/dist/claude/session/backgroundOutput.js +29 -0
  37. package/dist/claude/session/backgroundOutput.js.map +1 -0
  38. package/dist/claude/session/commands.d.ts +862 -0
  39. package/dist/claude/session/commands.js +2 -0
  40. package/dist/claude/session/commands.js.map +1 -0
  41. package/dist/claude/session/goalState.d.ts +49 -0
  42. package/dist/claude/session/goalState.js +407 -0
  43. package/dist/claude/session/goalState.js.map +1 -0
  44. package/dist/claude/session/mailbox.d.ts +52 -0
  45. package/dist/claude/session/mailbox.js +190 -0
  46. package/dist/claude/session/mailbox.js.map +1 -0
  47. package/dist/claude/session/outputAdapter.d.ts +11 -0
  48. package/dist/claude/session/outputAdapter.js +31 -0
  49. package/dist/claude/session/outputAdapter.js.map +1 -0
  50. package/dist/claude/session/permissionUpdates.d.ts +4 -0
  51. package/dist/claude/session/permissionUpdates.js +17 -0
  52. package/dist/claude/session/permissionUpdates.js.map +1 -0
  53. package/dist/claude/session/providerFacts.d.ts +47 -0
  54. package/dist/claude/session/providerFacts.js +84 -0
  55. package/dist/claude/session/providerFacts.js.map +1 -0
  56. package/dist/claude/session/providerRuntime.d.ts +1 -0
  57. package/dist/claude/session/providerRuntime.js +2 -0
  58. package/dist/claude/session/providerRuntime.js.map +1 -0
  59. package/dist/claude/session/providerRuntimeFactory.d.ts +49 -0
  60. package/dist/claude/session/providerRuntimeFactory.js +122 -0
  61. package/dist/claude/session/providerRuntimeFactory.js.map +1 -0
  62. package/dist/claude/session/repository.d.ts +57 -0
  63. package/dist/claude/session/repository.js +161 -0
  64. package/dist/claude/session/repository.js.map +1 -0
  65. package/dist/claude/session/runtime.d.ts +46 -0
  66. package/dist/claude/session/runtime.js +140 -0
  67. package/dist/claude/session/runtime.js.map +1 -0
  68. package/dist/claude/session/scopeState.d.ts +61 -0
  69. package/dist/claude/session/scopeState.js +74 -0
  70. package/dist/claude/session/scopeState.js.map +1 -0
  71. package/dist/claude/session/session.d.ts +285 -0
  72. package/dist/claude/session/session.js +6778 -0
  73. package/dist/claude/session/session.js.map +1 -0
  74. package/dist/claude/session/shellRunner.d.ts +13 -0
  75. package/dist/claude/session/shellRunner.js +103 -0
  76. package/dist/claude/session/shellRunner.js.map +1 -0
  77. package/dist/claude/session/usage.d.ts +2 -0
  78. package/dist/claude/session/usage.js +10 -0
  79. package/dist/claude/session/usage.js.map +1 -0
  80. package/dist/claude/sessionRegistry.d.ts +26 -0
  81. package/dist/claude/sessionRegistry.js +118 -0
  82. package/dist/claude/sessionRegistry.js.map +1 -0
  83. package/dist/claude/shellSupervisor.d.ts +1 -0
  84. package/dist/claude/shellSupervisor.js +69 -0
  85. package/dist/claude/shellSupervisor.js.map +1 -0
  86. package/dist/claude/statusCommand.d.ts +21 -0
  87. package/dist/claude/statusCommand.js +97 -0
  88. package/dist/claude/statusCommand.js.map +1 -0
  89. package/dist/claude/toolMapper.d.ts +9 -1
  90. package/dist/claude/toolMapper.js +98 -18
  91. package/dist/claude/toolMapper.js.map +1 -1
  92. package/dist/claude/transcriptBrancher.d.ts +37 -0
  93. package/dist/claude/transcriptBrancher.js +103 -0
  94. package/dist/claude/transcriptBrancher.js.map +1 -0
  95. package/dist/cli/delegate.js +23 -1
  96. package/dist/cli/delegate.js.map +1 -1
  97. package/dist/cli/main.js +28 -12
  98. package/dist/cli/main.js.map +1 -1
  99. package/dist/codex/generated/ResponseItemRuntimeSchema.d.ts +8 -0
  100. package/dist/codex/generated/ResponseItemRuntimeSchema.js +7 -0
  101. package/dist/codex/generated/ResponseItemRuntimeSchema.js.map +1 -0
  102. package/dist/compatibility/probe.js +11 -6
  103. package/dist/compatibility/probe.js.map +1 -1
  104. package/dist/config/config.d.ts +12 -0
  105. package/dist/config/config.js +103 -11
  106. package/dist/config/config.js.map +1 -1
  107. package/dist/daemon/daemon.js +91 -43
  108. package/dist/daemon/daemon.js.map +1 -1
  109. package/dist/daemon/ownership.d.ts +15 -0
  110. package/dist/daemon/ownership.js +185 -0
  111. package/dist/daemon/ownership.js.map +1 -0
  112. package/dist/daemon/probe.js +1 -1
  113. package/dist/daemon/supervisor.d.ts +3 -0
  114. package/dist/daemon/supervisor.js +37 -1
  115. package/dist/daemon/supervisor.js.map +1 -1
  116. package/dist/gateway/clientConnection.d.ts +7 -1
  117. package/dist/gateway/clientConnection.js +656 -60
  118. package/dist/gateway/clientConnection.js.map +1 -1
  119. package/dist/gateway/logicalThreadProjection.d.ts +8 -0
  120. package/dist/gateway/logicalThreadProjection.js +59 -0
  121. package/dist/gateway/logicalThreadProjection.js.map +1 -0
  122. package/dist/gateway/remoteRelay.js +0 -1
  123. package/dist/gateway/remoteRelay.js.map +1 -1
  124. package/dist/gateway/server.d.ts +1 -1
  125. package/dist/gateway/server.js +69 -11
  126. package/dist/gateway/server.js.map +1 -1
  127. package/dist/gateway/stockRpc.d.ts +5 -2
  128. package/dist/gateway/stockRpc.js +52 -6
  129. package/dist/gateway/stockRpc.js.map +1 -1
  130. package/dist/gateway/subscriptions.d.ts +19 -3
  131. package/dist/gateway/subscriptions.js +152 -13
  132. package/dist/gateway/subscriptions.js.map +1 -1
  133. package/dist/gateway/threadList.d.ts +7 -2
  134. package/dist/gateway/threadList.js +8 -4
  135. package/dist/gateway/threadList.js.map +1 -1
  136. package/dist/gateway/transientNotice.d.ts +15 -0
  137. package/dist/gateway/transientNotice.js +96 -1
  138. package/dist/gateway/transientNotice.js.map +1 -1
  139. package/dist/handoff/providerEpochs.d.ts +18 -0
  140. package/dist/handoff/providerEpochs.js +77 -0
  141. package/dist/handoff/providerEpochs.js.map +1 -0
  142. package/dist/handoff/service.d.ts +86 -6
  143. package/dist/handoff/service.js +1276 -168
  144. package/dist/handoff/service.js.map +1 -1
  145. package/dist/handoff/store.d.ts +189 -2
  146. package/dist/handoff/store.js +618 -7
  147. package/dist/handoff/store.js.map +1 -1
  148. package/dist/handoff/titleGeneration.d.ts +28 -0
  149. package/dist/handoff/titleGeneration.js +138 -0
  150. package/dist/handoff/titleGeneration.js.map +1 -0
  151. package/dist/management/doctor.d.ts +1 -1
  152. package/dist/management/doctor.js +32 -23
  153. package/dist/management/doctor.js.map +1 -1
  154. package/dist/management/lifecycle.js +14 -2
  155. package/dist/management/lifecycle.js.map +1 -1
  156. package/dist/management/remoteShim.d.ts +10 -0
  157. package/dist/management/remoteShim.js +65 -0
  158. package/dist/management/remoteShim.js.map +1 -0
  159. package/dist/management/setup.d.ts +5 -0
  160. package/dist/management/setup.js +120 -39
  161. package/dist/management/setup.js.map +1 -1
  162. package/dist/observability/metrics.d.ts +2 -0
  163. package/dist/observability/metrics.js +6 -0
  164. package/dist/observability/metrics.js.map +1 -1
  165. package/dist/protocol/errors.d.ts +1 -0
  166. package/dist/protocol/errors.js +4 -1
  167. package/dist/protocol/errors.js.map +1 -1
  168. package/dist/runtime/dependencies.js +3 -3
  169. package/dist/runtime/dependencies.js.map +1 -1
  170. package/dist/runtime/providerAvailability.d.ts +24 -0
  171. package/dist/runtime/providerAvailability.js +107 -0
  172. package/dist/runtime/providerAvailability.js.map +1 -0
  173. package/dist/state/stateCommand.d.ts +21 -0
  174. package/dist/state/stateCommand.js +198 -0
  175. package/dist/state/stateCommand.js.map +1 -0
  176. package/dist/state/stockStateTracker.d.ts +16 -0
  177. package/dist/state/stockStateTracker.js +137 -0
  178. package/dist/state/stockStateTracker.js.map +1 -0
  179. package/dist/store/HybridStore.d.ts +118 -2
  180. package/dist/store/HybridStore.js +23 -1
  181. package/dist/store/HybridStore.js.map +1 -1
  182. package/dist/store/memoryStore.d.ts +57 -7
  183. package/dist/store/memoryStore.js +337 -5
  184. package/dist/store/memoryStore.js.map +1 -1
  185. package/dist/store/sqliteStore.d.ts +30 -4
  186. package/dist/store/sqliteStore.js +395 -42
  187. package/dist/store/sqliteStore.js.map +1 -1
  188. package/install.sh +2 -2
  189. package/{LICENSES.md → legal/LICENSES.md} +2 -2
  190. package/{THIRD_PARTY_NOTICES.md → legal/THIRD_PARTY_NOTICES.md} +177 -148
  191. package/package.json +28 -18
  192. package/vendor/codex/continuation.md +51 -0
  193. package/BUILD_BENCHMARKS.md +0 -27
  194. package/RELEASING.md +0 -98
  195. package/SSH_VERIFY.md +0 -69
  196. package/dist/claude/interactions.d.ts +0 -23
  197. package/dist/claude/interactions.js +0 -77
  198. package/dist/claude/interactions.js.map +0 -1
  199. package/dist/claude/sessionRuntime.d.ts +0 -102
  200. package/dist/claude/sessionRuntime.js +0 -1361
  201. package/dist/claude/sessionRuntime.js.map +0 -1
@@ -10,13 +10,16 @@ export class HandoffStore {
10
10
  PRAGMA journal_mode=WAL;
11
11
  PRAGMA synchronous=FULL;
12
12
  PRAGMA busy_timeout=5000;
13
+ PRAGMA foreign_keys=ON;
13
14
  CREATE TABLE IF NOT EXISTS pending_provider_switches (
14
15
  thread_id TEXT PRIMARY KEY,
15
16
  source_provider TEXT NOT NULL,
16
17
  target_provider TEXT NOT NULL,
17
18
  target_model TEXT NOT NULL,
18
19
  settings_json TEXT NOT NULL,
19
- updated_at INTEGER NOT NULL
20
+ updated_at INTEGER NOT NULL,
21
+ revision INTEGER NOT NULL DEFAULT 1,
22
+ expected_epoch_id TEXT
20
23
  );
21
24
  CREATE TABLE IF NOT EXISTS stock_history_overlays (
22
25
  thread_id TEXT PRIMARY KEY,
@@ -25,21 +28,136 @@ export class HandoffStore {
25
28
  turns_json TEXT NOT NULL,
26
29
  created_at INTEGER NOT NULL
27
30
  );
31
+ CREATE TABLE IF NOT EXISTS handoff_jobs (
32
+ job_id TEXT PRIMARY KEY,
33
+ source_thread_id TEXT NOT NULL,
34
+ params_json TEXT NOT NULL,
35
+ pending_json TEXT,
36
+ status TEXT NOT NULL,
37
+ result_json TEXT,
38
+ target_json TEXT,
39
+ error TEXT,
40
+ error_notified INTEGER NOT NULL DEFAULT 0,
41
+ created_at INTEGER NOT NULL,
42
+ updated_at INTEGER NOT NULL
43
+ );
44
+ CREATE INDEX IF NOT EXISTS handoff_jobs_recovery
45
+ ON handoff_jobs(status, created_at);
46
+ CREATE INDEX IF NOT EXISTS handoff_jobs_failure
47
+ ON handoff_jobs(source_thread_id, status, error_notified, updated_at);
48
+ CREATE TABLE IF NOT EXISTS logical_threads (
49
+ public_thread_id TEXT PRIMARY KEY,
50
+ current_epoch_id TEXT NOT NULL,
51
+ thread_json TEXT NOT NULL,
52
+ revision INTEGER NOT NULL DEFAULT 1,
53
+ created_at INTEGER NOT NULL,
54
+ updated_at INTEGER NOT NULL
55
+ );
56
+ CREATE TABLE IF NOT EXISTS provider_epochs (
57
+ epoch_id TEXT PRIMARY KEY,
58
+ public_thread_id TEXT NOT NULL REFERENCES logical_threads(public_thread_id) ON DELETE CASCADE,
59
+ ordinal INTEGER NOT NULL,
60
+ provider TEXT NOT NULL CHECK(provider IN ('claude', 'stock')),
61
+ backend_thread_id TEXT NOT NULL,
62
+ model TEXT NOT NULL,
63
+ settings_json TEXT NOT NULL,
64
+ state TEXT NOT NULL CHECK(state IN ('current', 'sealed', 'provisional')),
65
+ created_at INTEGER NOT NULL,
66
+ sealed_at INTEGER,
67
+ UNIQUE(public_thread_id, ordinal),
68
+ UNIQUE(provider, backend_thread_id)
69
+ );
70
+ CREATE UNIQUE INDEX IF NOT EXISTS provider_epochs_one_current
71
+ ON provider_epochs(public_thread_id) WHERE state = 'current';
72
+ CREATE INDEX IF NOT EXISTS provider_epochs_public
73
+ ON provider_epochs(public_thread_id, ordinal);
74
+ CREATE TABLE IF NOT EXISTS logical_turns (
75
+ public_thread_id TEXT NOT NULL REFERENCES logical_threads(public_thread_id) ON DELETE CASCADE,
76
+ position INTEGER NOT NULL,
77
+ public_turn_id TEXT NOT NULL,
78
+ epoch_id TEXT REFERENCES provider_epochs(epoch_id),
79
+ provider_turn_id TEXT,
80
+ turn_json TEXT NOT NULL,
81
+ kind TEXT NOT NULL CHECK(kind IN ('provider', 'migrationCompact')),
82
+ PRIMARY KEY(public_thread_id, position),
83
+ UNIQUE(public_thread_id, public_turn_id),
84
+ CHECK((kind = 'provider' AND epoch_id IS NOT NULL AND provider_turn_id IS NOT NULL)
85
+ OR (kind = 'migrationCompact' AND epoch_id IS NULL AND provider_turn_id IS NULL))
86
+ );
87
+ CREATE UNIQUE INDEX IF NOT EXISTS logical_turns_provider
88
+ ON logical_turns(public_thread_id, epoch_id, provider_turn_id)
89
+ WHERE epoch_id IS NOT NULL AND provider_turn_id IS NOT NULL;
90
+ CREATE TABLE IF NOT EXISTS provider_switch_jobs (
91
+ job_id TEXT PRIMARY KEY,
92
+ public_thread_id TEXT NOT NULL REFERENCES logical_threads(public_thread_id) ON DELETE CASCADE,
93
+ expected_epoch_id TEXT NOT NULL REFERENCES provider_epochs(epoch_id),
94
+ expected_thread_revision INTEGER NOT NULL,
95
+ pending_revision INTEGER NOT NULL,
96
+ target_provider TEXT NOT NULL CHECK(target_provider IN ('claude', 'stock')),
97
+ target_model TEXT NOT NULL,
98
+ settings_json TEXT NOT NULL,
99
+ turn_params_json TEXT NOT NULL,
100
+ compaction_turn_json TEXT NOT NULL,
101
+ status TEXT NOT NULL CHECK(status IN ('queued', 'running', 'targetCreated', 'committed', 'failed')),
102
+ summary TEXT,
103
+ target_backend_thread_id TEXT,
104
+ target_provider_turn_id TEXT,
105
+ error TEXT,
106
+ created_at INTEGER NOT NULL,
107
+ updated_at INTEGER NOT NULL
108
+ );
109
+ CREATE UNIQUE INDEX IF NOT EXISTS provider_switch_jobs_active
110
+ ON provider_switch_jobs(public_thread_id)
111
+ WHERE status IN ('queued', 'running', 'targetCreated');
112
+ CREATE INDEX IF NOT EXISTS provider_switch_jobs_recovery
113
+ ON provider_switch_jobs(status, created_at);
114
+ CREATE TABLE IF NOT EXISTS fork_selections (
115
+ target_public_thread_id TEXT PRIMARY KEY REFERENCES logical_threads(public_thread_id) ON DELETE CASCADE,
116
+ source_public_thread_id TEXT NOT NULL REFERENCES logical_threads(public_thread_id) ON DELETE CASCADE,
117
+ provisional_epoch_id TEXT NOT NULL REFERENCES provider_epochs(epoch_id),
118
+ expected_target_revision INTEGER NOT NULL,
119
+ selected_epoch_id TEXT REFERENCES provider_epochs(epoch_id),
120
+ status TEXT NOT NULL CHECK(status IN ('pending', 'finalized')),
121
+ created_at INTEGER NOT NULL,
122
+ updated_at INTEGER NOT NULL
123
+ );
28
124
  `);
125
+ const pendingColumns = this.database.prepare("PRAGMA table_info(pending_provider_switches)")
126
+ .all();
127
+ if (!pendingColumns.some((column) => column.name === "revision")) {
128
+ this.database.exec("ALTER TABLE pending_provider_switches ADD COLUMN revision INTEGER NOT NULL DEFAULT 1");
129
+ }
130
+ if (!pendingColumns.some((column) => column.name === "expected_epoch_id")) {
131
+ this.database.exec("ALTER TABLE pending_provider_switches ADD COLUMN expected_epoch_id TEXT");
132
+ }
133
+ const jobColumns = this.database.prepare("PRAGMA table_info(handoff_jobs)")
134
+ .all();
135
+ if (!jobColumns.some((column) => column.name === "target_json")) {
136
+ this.database.exec("ALTER TABLE handoff_jobs ADD COLUMN target_json TEXT");
137
+ }
138
+ const switchColumns = this.database.prepare("PRAGMA table_info(provider_switch_jobs)")
139
+ .all();
140
+ if (!switchColumns.some((column) => column.name === "compaction_turn_json")) {
141
+ this.database.exec("ALTER TABLE provider_switch_jobs ADD COLUMN compaction_turn_json TEXT");
142
+ }
29
143
  chmodSync(path, 0o600);
30
144
  }
31
145
  setPending(pending) {
32
146
  this.database.prepare(`
33
147
  INSERT INTO pending_provider_switches (
34
- thread_id, source_provider, target_provider, target_model, settings_json, updated_at
35
- ) VALUES (?, ?, ?, ?, ?, ?)
148
+ thread_id, source_provider, target_provider, target_model, settings_json, updated_at, revision,
149
+ expected_epoch_id
150
+ ) VALUES (?, ?, ?, ?, ?, ?, 1, ?)
36
151
  ON CONFLICT(thread_id) DO UPDATE SET
37
152
  source_provider = excluded.source_provider,
38
153
  target_provider = excluded.target_provider,
39
154
  target_model = excluded.target_model,
40
155
  settings_json = excluded.settings_json,
41
- updated_at = excluded.updated_at
42
- `).run(pending.threadId, pending.sourceProvider, pending.targetProvider, pending.targetModel, JSON.stringify(pending.settings), Date.now());
156
+ updated_at = excluded.updated_at,
157
+ expected_epoch_id = excluded.expected_epoch_id,
158
+ revision = pending_provider_switches.revision + 1
159
+ `).run(pending.threadId, pending.sourceProvider, pending.targetProvider, pending.targetModel, JSON.stringify(pending.settings), Date.now(), pending.expectedEpochId ?? null);
160
+ return this.getPending(pending.threadId);
43
161
  }
44
162
  getPending(threadId) {
45
163
  const row = this.database.prepare("SELECT * FROM pending_provider_switches WHERE thread_id = ?")
@@ -50,10 +168,382 @@ export class HandoffStore {
50
168
  targetProvider: row.target_provider,
51
169
  targetModel: row.target_model,
52
170
  settings: JSON.parse(row.settings_json),
171
+ revision: row.revision,
172
+ ...(row.expected_epoch_id ? { expectedEpochId: row.expected_epoch_id } : {}),
53
173
  } : undefined;
54
174
  }
55
- clearPending(threadId) {
56
- this.database.prepare("DELETE FROM pending_provider_switches WHERE thread_id = ?").run(threadId);
175
+ clearPending(threadId, revision) {
176
+ const result = revision === undefined
177
+ ? this.database.prepare("DELETE FROM pending_provider_switches WHERE thread_id = ?").run(threadId)
178
+ : this.database.prepare("DELETE FROM pending_provider_switches WHERE thread_id = ? AND revision = ?")
179
+ .run(threadId, revision);
180
+ return Number(result.changes) > 0;
181
+ }
182
+ createLogicalThread(input) {
183
+ const now = input.epoch.createdAt ?? Date.now();
184
+ return this.transaction(() => {
185
+ this.database.prepare(`
186
+ INSERT INTO logical_threads (
187
+ public_thread_id, current_epoch_id, thread_json, revision, created_at, updated_at
188
+ ) VALUES (?, ?, ?, 1, ?, ?)
189
+ `).run(input.thread.id, input.epoch.id, JSON.stringify({ ...input.thread, turns: [] }), now, now);
190
+ this.insertEpoch(input.thread.id, 0, input.epoch, "current", now);
191
+ return this.getLogicalThread(input.thread.id);
192
+ });
193
+ }
194
+ getLogicalThread(publicThreadId) {
195
+ const row = this.database.prepare("SELECT * FROM logical_threads WHERE public_thread_id = ?")
196
+ .get(publicThreadId);
197
+ return row ? this.logicalThreadFromRow(row) : undefined;
198
+ }
199
+ updateLogicalThread(publicThreadId, expectedRevision, thread) {
200
+ const result = this.database.prepare(`
201
+ UPDATE logical_threads SET thread_json = ?, updated_at = ?
202
+ WHERE public_thread_id = ? AND revision = ?
203
+ `).run(JSON.stringify({ ...thread, id: publicThreadId, turns: [] }), Date.now(), publicThreadId, expectedRevision);
204
+ return Number(result.changes) === 1 ? this.getLogicalThread(publicThreadId) : undefined;
205
+ }
206
+ deleteLogicalThread(publicThreadId) {
207
+ return Number(this.database.prepare("DELETE FROM logical_threads WHERE public_thread_id = ?")
208
+ .run(publicThreadId).changes) === 1;
209
+ }
210
+ getEpoch(epochId) {
211
+ const row = this.database.prepare("SELECT * FROM provider_epochs WHERE epoch_id = ?")
212
+ .get(epochId);
213
+ return row ? this.epochFromRow(row) : undefined;
214
+ }
215
+ listEpochs(publicThreadId) {
216
+ return this.database.prepare(`
217
+ SELECT * FROM provider_epochs WHERE public_thread_id = ? ORDER BY ordinal ASC
218
+ `).all(publicThreadId).map((row) => this.epochFromRow(row));
219
+ }
220
+ updateCurrentEpoch(publicThreadId, expectedEpochId, patch) {
221
+ return this.transaction(() => {
222
+ const logical = this.getLogicalThread(publicThreadId);
223
+ const epoch = logical && this.getEpoch(expectedEpochId);
224
+ if (!logical || logical.currentEpochId !== expectedEpochId || epoch?.state !== "current")
225
+ return undefined;
226
+ const result = this.database.prepare(`
227
+ UPDATE provider_epochs SET model = ?, settings_json = ?
228
+ WHERE epoch_id = ? AND public_thread_id = ? AND state = 'current'
229
+ `).run(patch.model ?? epoch.model, JSON.stringify({ ...epoch.settings, ...(patch.settings ?? {}) }), expectedEpochId, publicThreadId);
230
+ return Number(result.changes) === 1 ? this.getEpoch(expectedEpochId) : undefined;
231
+ });
232
+ }
233
+ findEpochByBackend(provider, backendThreadId) {
234
+ const row = this.database.prepare(`
235
+ SELECT * FROM provider_epochs WHERE provider = ? AND backend_thread_id = ?
236
+ `).get(provider, backendThreadId);
237
+ return row ? this.epochFromRow(row) : undefined;
238
+ }
239
+ listBackendMappings() {
240
+ return this.database.prepare(`
241
+ SELECT public_thread_id, epoch_id, provider, backend_thread_id, state
242
+ FROM provider_epochs ORDER BY public_thread_id ASC, ordinal ASC
243
+ `).all().map((row) => ({
244
+ publicThreadId: row.public_thread_id,
245
+ epochId: row.epoch_id,
246
+ provider: row.provider,
247
+ backendThreadId: row.backend_thread_id,
248
+ state: row.state,
249
+ }));
250
+ }
251
+ listLogicalTurns(publicThreadId) {
252
+ return this.database.prepare(`
253
+ SELECT * FROM logical_turns WHERE public_thread_id = ? ORDER BY position ASC
254
+ `).all(publicThreadId).map((row) => this.logicalTurnFromRow(row));
255
+ }
256
+ findLogicalTurn(publicThreadId, publicTurnId) {
257
+ const row = this.database.prepare(`
258
+ SELECT * FROM logical_turns WHERE public_thread_id = ? AND public_turn_id = ?
259
+ `).get(publicThreadId, publicTurnId);
260
+ return row ? this.logicalTurnFromRow(row) : undefined;
261
+ }
262
+ replaceLogicalTurns(publicThreadId, expectedThreadRevision, turns) {
263
+ return this.transaction(() => {
264
+ const current = this.getLogicalThread(publicThreadId);
265
+ if (!current || current.revision !== expectedThreadRevision)
266
+ return false;
267
+ this.replaceTurns(publicThreadId, turns);
268
+ const result = this.database.prepare(`
269
+ UPDATE logical_threads SET revision = revision + 1, updated_at = ?
270
+ WHERE public_thread_id = ? AND revision = ?
271
+ `).run(Date.now(), publicThreadId, expectedThreadRevision);
272
+ return Number(result.changes) === 1;
273
+ });
274
+ }
275
+ stageProviderSwitch(input) {
276
+ return this.transaction(() => {
277
+ const logical = this.getLogicalThread(input.pending.threadId);
278
+ const epoch = logical && this.getEpoch(logical.currentEpochId);
279
+ if (!logical || logical.currentEpochId !== input.expectedEpochId
280
+ || epoch?.provider !== input.pending.sourceProvider)
281
+ return undefined;
282
+ return this.setPending({ ...input.pending, expectedEpochId: input.expectedEpochId });
283
+ });
284
+ }
285
+ createProviderSwitchJob(input) {
286
+ return this.transaction(() => {
287
+ const logical = this.getLogicalThread(input.publicThreadId);
288
+ const pending = this.getPending(input.publicThreadId);
289
+ const epoch = logical && this.getEpoch(logical.currentEpochId);
290
+ const active = this.database.prepare(`
291
+ SELECT 1 AS found FROM provider_switch_jobs
292
+ WHERE public_thread_id = ? AND status IN ('queued', 'running', 'targetCreated')
293
+ `).get(input.publicThreadId);
294
+ if (!logical || logical.currentEpochId !== input.expectedEpochId || active
295
+ || epoch?.provider !== pending?.sourceProvider
296
+ || pending?.revision !== input.pendingRevision || pending.expectedEpochId !== input.expectedEpochId
297
+ || pending.targetProvider !== input.targetProvider || pending.targetModel !== input.targetModel)
298
+ return undefined;
299
+ const now = input.createdAt ?? Date.now();
300
+ this.database.prepare(`
301
+ INSERT INTO provider_switch_jobs (
302
+ job_id, public_thread_id, expected_epoch_id, expected_thread_revision, pending_revision,
303
+ target_provider, target_model, settings_json, turn_params_json, status, created_at, updated_at
304
+ , compaction_turn_json
305
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?, ?)
306
+ `).run(input.id, input.publicThreadId, input.expectedEpochId, logical.revision, input.pendingRevision, input.targetProvider, input.targetModel, JSON.stringify(input.settings), JSON.stringify(input.turnParams), now, now, JSON.stringify(input.compactionTurn));
307
+ return this.getProviderSwitchJob(input.id);
308
+ });
309
+ }
310
+ getProviderSwitchJob(jobId) {
311
+ const row = this.database.prepare("SELECT * FROM provider_switch_jobs WHERE job_id = ?")
312
+ .get(jobId);
313
+ return row ? this.providerSwitchJobFromRow(row) : undefined;
314
+ }
315
+ recoverableProviderSwitchJobs() {
316
+ return this.database.prepare(`
317
+ SELECT * FROM provider_switch_jobs
318
+ WHERE status IN ('queued', 'running', 'targetCreated') ORDER BY created_at ASC
319
+ `).all().map((row) => this.providerSwitchJobFromRow(row));
320
+ }
321
+ claimProviderSwitchJob(jobId) {
322
+ const result = this.database.prepare(`
323
+ UPDATE provider_switch_jobs SET status = 'running', updated_at = ?
324
+ WHERE job_id = ? AND status = 'queued'
325
+ `).run(Date.now(), jobId);
326
+ return Number(result.changes) === 1 ? this.getProviderSwitchJob(jobId) : undefined;
327
+ }
328
+ requeueProviderSwitchJob(jobId) {
329
+ const result = this.database.prepare(`
330
+ UPDATE provider_switch_jobs SET
331
+ status = 'queued', summary = NULL, target_backend_thread_id = NULL,
332
+ target_provider_turn_id = NULL, error = NULL, updated_at = ?
333
+ WHERE job_id = ? AND status IN ('running', 'targetCreated')
334
+ `).run(Date.now(), jobId);
335
+ return Number(result.changes) === 1 ? this.getProviderSwitchJob(jobId) : this.getProviderSwitchJob(jobId);
336
+ }
337
+ checkpointProviderSwitchTarget(jobId, checkpoint) {
338
+ return this.transaction(() => {
339
+ const job = this.getProviderSwitchJob(jobId);
340
+ if (!job || !["running", "targetCreated"].includes(job.status)
341
+ || (job.targetBackendThreadId && job.targetBackendThreadId !== checkpoint.backendThreadId))
342
+ return false;
343
+ const result = this.database.prepare(`
344
+ UPDATE provider_switch_jobs SET
345
+ status = 'targetCreated', target_backend_thread_id = ?,
346
+ summary = COALESCE(?, summary), target_provider_turn_id = COALESCE(?, target_provider_turn_id),
347
+ updated_at = ?
348
+ WHERE job_id = ? AND status IN ('running', 'targetCreated')
349
+ `).run(checkpoint.backendThreadId, checkpoint.summary ?? null, checkpoint.providerTurnId ?? null, Date.now(), jobId);
350
+ return Number(result.changes) === 1;
351
+ });
352
+ }
353
+ commitProviderSwitch(input) {
354
+ return this.transaction(() => {
355
+ const job = this.getProviderSwitchJob(input.jobId);
356
+ const logical = job && this.getLogicalThread(job.publicThreadId);
357
+ const pending = job && this.getPending(job.publicThreadId);
358
+ if (!job || job.status !== "targetCreated" || !job.targetProviderTurnId || !logical
359
+ || logical.currentEpochId !== job.expectedEpochId || logical.revision !== job.expectedThreadRevision
360
+ || pending?.revision !== job.pendingRevision || pending.expectedEpochId !== job.expectedEpochId
361
+ || input.targetEpoch.provider !== job.targetProvider || input.targetEpoch.model !== job.targetModel
362
+ || input.targetEpoch.backendThreadId !== job.targetBackendThreadId)
363
+ return undefined;
364
+ const committedAt = input.committedAt ?? Date.now();
365
+ const ordinal = this.database.prepare(`
366
+ SELECT COALESCE(MAX(ordinal), -1) + 1 AS ordinal FROM provider_epochs WHERE public_thread_id = ?
367
+ `).get(job.publicThreadId).ordinal;
368
+ this.database.prepare(`
369
+ UPDATE provider_epochs SET state = 'sealed', sealed_at = ?
370
+ WHERE epoch_id = ? AND public_thread_id = ? AND state = 'current'
371
+ `).run(committedAt, job.expectedEpochId, job.publicThreadId);
372
+ this.insertEpoch(job.publicThreadId, ordinal, input.targetEpoch, "current", committedAt);
373
+ this.replaceTurns(job.publicThreadId, input.sourceTurns);
374
+ const update = this.database.prepare(`
375
+ UPDATE logical_threads SET current_epoch_id = ?, thread_json = ?, revision = revision + 1, updated_at = ?
376
+ WHERE public_thread_id = ? AND current_epoch_id = ? AND revision = ?
377
+ `).run(input.targetEpoch.id, JSON.stringify({ ...input.thread, turns: [] }), committedAt, job.publicThreadId, job.expectedEpochId, job.expectedThreadRevision);
378
+ if (Number(update.changes) !== 1)
379
+ throw new Error("Provider switch lost its logical-thread CAS during commit.");
380
+ this.database.prepare(`
381
+ DELETE FROM pending_provider_switches WHERE thread_id = ? AND revision = ? AND expected_epoch_id = ?
382
+ `).run(job.publicThreadId, job.pendingRevision, job.expectedEpochId);
383
+ this.database.prepare(`
384
+ UPDATE provider_switch_jobs SET status = 'committed', error = NULL, updated_at = ?
385
+ WHERE job_id = ? AND status = 'targetCreated'
386
+ `).run(committedAt, job.id);
387
+ return this.getLogicalThread(job.publicThreadId);
388
+ });
389
+ }
390
+ failProviderSwitch(jobId, error) {
391
+ return this.transaction(() => {
392
+ const job = this.getProviderSwitchJob(jobId);
393
+ if (!job || !["queued", "running", "targetCreated"].includes(job.status))
394
+ return false;
395
+ const result = this.database.prepare(`
396
+ UPDATE provider_switch_jobs SET status = 'failed', error = ?, updated_at = ?
397
+ WHERE job_id = ? AND status IN ('queued', 'running', 'targetCreated')
398
+ `).run(error, Date.now(), jobId);
399
+ if (Number(result.changes) !== 1)
400
+ return false;
401
+ const logical = this.getLogicalThread(job.publicThreadId);
402
+ if (logical?.currentEpochId === job.expectedEpochId) {
403
+ this.database.prepare(`
404
+ DELETE FROM pending_provider_switches WHERE thread_id = ? AND revision = ? AND expected_epoch_id = ?
405
+ `).run(job.publicThreadId, job.pendingRevision, job.expectedEpochId);
406
+ }
407
+ return true;
408
+ });
409
+ }
410
+ createForkSelection(selection) {
411
+ return this.transaction(() => {
412
+ const epoch = this.getEpoch(selection.provisionalEpochId);
413
+ const target = this.getLogicalThread(selection.targetPublicThreadId);
414
+ if (!epoch || epoch.publicThreadId !== selection.targetPublicThreadId
415
+ || target?.currentEpochId !== selection.provisionalEpochId
416
+ || !this.getLogicalThread(selection.sourcePublicThreadId))
417
+ return false;
418
+ const now = selection.createdAt ?? Date.now();
419
+ const result = this.database.prepare(`
420
+ INSERT OR IGNORE INTO fork_selections (
421
+ target_public_thread_id, source_public_thread_id, provisional_epoch_id, expected_target_revision,
422
+ status, created_at, updated_at
423
+ ) VALUES (?, ?, ?, ?, 'pending', ?, ?)
424
+ `).run(selection.targetPublicThreadId, selection.sourcePublicThreadId, selection.provisionalEpochId, target.revision, now, now);
425
+ return Number(result.changes) === 1;
426
+ });
427
+ }
428
+ getForkSelection(targetPublicThreadId) {
429
+ const row = this.database.prepare("SELECT * FROM fork_selections WHERE target_public_thread_id = ?")
430
+ .get(targetPublicThreadId);
431
+ return row ? this.forkSelectionFromRow(row) : undefined;
432
+ }
433
+ finalizeForkSelection(targetPublicThreadId, expectedProvisionalEpochId, selectedEpochId) {
434
+ return this.transaction(() => {
435
+ const selection = this.getForkSelection(targetPublicThreadId);
436
+ const selected = this.getEpoch(selectedEpochId);
437
+ if (!selection || selection.status !== "pending"
438
+ || selection.provisionalEpochId !== expectedProvisionalEpochId
439
+ || this.getLogicalThread(targetPublicThreadId)?.revision !== selection.expectedTargetRevision
440
+ || selected?.publicThreadId !== selection.sourcePublicThreadId)
441
+ return false;
442
+ const result = this.database.prepare(`
443
+ UPDATE fork_selections SET selected_epoch_id = ?, status = 'finalized', updated_at = ?
444
+ WHERE target_public_thread_id = ? AND provisional_epoch_id = ? AND status = 'pending'
445
+ `).run(selectedEpochId, Date.now(), targetPublicThreadId, expectedProvisionalEpochId);
446
+ return Number(result.changes) === 1;
447
+ });
448
+ }
449
+ commitForkSelection(input) {
450
+ return this.transaction(() => {
451
+ const selection = this.getForkSelection(input.targetPublicThreadId);
452
+ const target = selection && this.getLogicalThread(selection.targetPublicThreadId);
453
+ const selected = this.getEpoch(input.selectedSourceEpochId);
454
+ const provisional = this.getEpoch(input.expectedProvisionalEpochId);
455
+ if (!selection || selection.status !== "pending" || !target
456
+ || selection.provisionalEpochId !== input.expectedProvisionalEpochId
457
+ || selection.expectedTargetRevision !== target.revision
458
+ || target.currentEpochId !== input.expectedProvisionalEpochId
459
+ || selected?.publicThreadId !== selection.sourcePublicThreadId
460
+ || provisional?.publicThreadId !== target.publicThreadId || provisional.state !== "current"
461
+ || input.targetEpoch.provider !== selected.provider)
462
+ return undefined;
463
+ const committedAt = input.committedAt ?? Date.now();
464
+ const ordinal = this.database.prepare(`
465
+ SELECT COALESCE(MAX(ordinal), -1) + 1 AS ordinal FROM provider_epochs WHERE public_thread_id = ?
466
+ `).get(target.publicThreadId).ordinal;
467
+ this.database.prepare(`
468
+ UPDATE provider_epochs SET state = 'sealed', sealed_at = ?
469
+ WHERE epoch_id = ? AND public_thread_id = ? AND state = 'current'
470
+ `).run(committedAt, provisional.id, target.publicThreadId);
471
+ this.insertEpoch(target.publicThreadId, ordinal, input.targetEpoch, "current", committedAt);
472
+ this.replaceTurns(target.publicThreadId, input.turns);
473
+ const updated = this.database.prepare(`
474
+ UPDATE logical_threads SET current_epoch_id = ?, thread_json = ?, revision = revision + 1, updated_at = ?
475
+ WHERE public_thread_id = ? AND current_epoch_id = ? AND revision = ?
476
+ `).run(input.targetEpoch.id, JSON.stringify({ ...input.thread, turns: [] }), committedAt, target.publicThreadId, provisional.id, selection.expectedTargetRevision);
477
+ if (Number(updated.changes) !== 1)
478
+ throw new Error("Fork selection lost its logical-thread CAS during commit.");
479
+ const finalized = this.database.prepare(`
480
+ UPDATE fork_selections SET selected_epoch_id = ?, status = 'finalized', updated_at = ?
481
+ WHERE target_public_thread_id = ? AND provisional_epoch_id = ? AND status = 'pending'
482
+ `).run(selected.id, committedAt, target.publicThreadId, provisional.id);
483
+ if (Number(finalized.changes) !== 1)
484
+ throw new Error("Fork selection lost its finalize CAS during commit.");
485
+ return this.getLogicalThread(target.publicThreadId);
486
+ });
487
+ }
488
+ createJob(job) {
489
+ const now = Date.now();
490
+ this.database.prepare(`
491
+ INSERT INTO handoff_jobs (
492
+ job_id, source_thread_id, params_json, pending_json, status, created_at, updated_at
493
+ ) VALUES (?, ?, ?, ?, 'queued', ?, ?)
494
+ `).run(job.id, job.sourceThreadId, JSON.stringify(job.params), job.pending ? JSON.stringify(job.pending) : null, now, now);
495
+ return { ...job, status: "queued" };
496
+ }
497
+ recoverableJobs() {
498
+ const rows = this.database.prepare(`
499
+ SELECT * FROM handoff_jobs WHERE status IN ('queued', 'running') ORDER BY created_at ASC
500
+ `).all();
501
+ this.database.prepare(`
502
+ UPDATE handoff_jobs SET status = 'queued', updated_at = ? WHERE status = 'running'
503
+ `).run(Date.now());
504
+ return rows.map((row) => this.jobFromRow({ ...row, status: "queued" }));
505
+ }
506
+ markJobRunning(jobId) {
507
+ this.database.prepare(`
508
+ UPDATE handoff_jobs SET status = 'running', updated_at = ? WHERE job_id = ?
509
+ `).run(Date.now(), jobId);
510
+ }
511
+ completeJob(jobId, result) {
512
+ this.database.prepare(`
513
+ UPDATE handoff_jobs
514
+ SET status = 'completed', result_json = ?, error = NULL, updated_at = ?
515
+ WHERE job_id = ?
516
+ `).run(JSON.stringify(result), Date.now(), jobId);
517
+ }
518
+ checkpointJobTarget(jobId, target) {
519
+ this.database.prepare(`
520
+ UPDATE handoff_jobs SET target_json = ?, updated_at = ? WHERE job_id = ?
521
+ `).run(JSON.stringify(target), Date.now(), jobId);
522
+ }
523
+ clearJobTarget(jobId) {
524
+ this.database.prepare(`
525
+ UPDATE handoff_jobs SET target_json = NULL, updated_at = ? WHERE job_id = ?
526
+ `).run(Date.now(), jobId);
527
+ }
528
+ failJob(jobId, error) {
529
+ this.database.prepare(`
530
+ UPDATE handoff_jobs
531
+ SET status = 'failed', error = ?, updated_at = ?
532
+ WHERE job_id = ?
533
+ `).run(error, Date.now(), jobId);
534
+ }
535
+ claimFailedJob(sourceThreadId) {
536
+ const row = this.database.prepare(`
537
+ SELECT * FROM handoff_jobs
538
+ WHERE source_thread_id = ? AND status = 'failed' AND error_notified = 0
539
+ ORDER BY updated_at ASC LIMIT 1
540
+ `).get(sourceThreadId);
541
+ if (!row?.error)
542
+ return undefined;
543
+ this.database.prepare(`
544
+ UPDATE handoff_jobs SET error_notified = 1, updated_at = ? WHERE job_id = ?
545
+ `).run(Date.now(), row.job_id);
546
+ return row.error;
57
547
  }
58
548
  setOverlay(overlay) {
59
549
  this.database.prepare(`
@@ -82,5 +572,126 @@ export class HandoffStore {
82
572
  close() {
83
573
  this.database.close();
84
574
  }
575
+ jobFromRow(row) {
576
+ return {
577
+ id: row.job_id,
578
+ sourceThreadId: row.source_thread_id,
579
+ params: JSON.parse(row.params_json),
580
+ ...(row.pending_json
581
+ ? { pending: JSON.parse(row.pending_json) }
582
+ : {}),
583
+ status: row.status,
584
+ ...(row.result_json ? { result: JSON.parse(row.result_json) } : {}),
585
+ ...(row.target_json ? { target: JSON.parse(row.target_json) } : {}),
586
+ ...(row.error ? { error: row.error } : {}),
587
+ };
588
+ }
589
+ insertEpoch(publicThreadId, ordinal, epoch, state, defaultCreatedAt) {
590
+ this.database.prepare(`
591
+ INSERT INTO provider_epochs (
592
+ epoch_id, public_thread_id, ordinal, provider, backend_thread_id,
593
+ model, settings_json, state, created_at, sealed_at
594
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
595
+ `).run(epoch.id, publicThreadId, ordinal, epoch.provider, epoch.backendThreadId, epoch.model, JSON.stringify(epoch.settings), state, epoch.createdAt ?? defaultCreatedAt);
596
+ }
597
+ replaceTurns(publicThreadId, turns) {
598
+ this.database.prepare("DELETE FROM logical_turns WHERE public_thread_id = ?").run(publicThreadId);
599
+ const insert = this.database.prepare(`
600
+ INSERT INTO logical_turns (
601
+ public_thread_id, position, public_turn_id, epoch_id, provider_turn_id, turn_json, kind
602
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
603
+ `);
604
+ turns.forEach((turn, position) => {
605
+ if (turn.publicTurnId !== turn.turn.id) {
606
+ throw new Error(`Logical turn id '${turn.publicTurnId}' does not match payload '${turn.turn.id}'.`);
607
+ }
608
+ insert.run(publicThreadId, position, turn.publicTurnId, turn.epochId ?? null, turn.providerTurnId ?? null, JSON.stringify(turn.turn), turn.kind);
609
+ });
610
+ }
611
+ logicalThreadFromRow(row) {
612
+ return {
613
+ publicThreadId: row.public_thread_id,
614
+ currentEpochId: row.current_epoch_id,
615
+ thread: JSON.parse(row.thread_json),
616
+ revision: row.revision,
617
+ createdAt: row.created_at,
618
+ updatedAt: row.updated_at,
619
+ };
620
+ }
621
+ epochFromRow(row) {
622
+ return {
623
+ id: row.epoch_id,
624
+ publicThreadId: row.public_thread_id,
625
+ ordinal: row.ordinal,
626
+ provider: row.provider,
627
+ backendThreadId: row.backend_thread_id,
628
+ model: row.model,
629
+ settings: JSON.parse(row.settings_json),
630
+ state: row.state,
631
+ createdAt: row.created_at,
632
+ ...(row.sealed_at === null ? {} : { sealedAt: row.sealed_at }),
633
+ };
634
+ }
635
+ logicalTurnFromRow(row) {
636
+ return {
637
+ publicThreadId: row.public_thread_id,
638
+ position: row.position,
639
+ publicTurnId: row.public_turn_id,
640
+ ...(row.epoch_id ? { epochId: row.epoch_id } : {}),
641
+ ...(row.provider_turn_id ? { providerTurnId: row.provider_turn_id } : {}),
642
+ turn: JSON.parse(row.turn_json),
643
+ kind: row.kind,
644
+ };
645
+ }
646
+ providerSwitchJobFromRow(row) {
647
+ return {
648
+ id: row.job_id,
649
+ publicThreadId: row.public_thread_id,
650
+ expectedEpochId: row.expected_epoch_id,
651
+ expectedThreadRevision: row.expected_thread_revision,
652
+ pendingRevision: row.pending_revision,
653
+ targetProvider: row.target_provider,
654
+ targetModel: row.target_model,
655
+ settings: JSON.parse(row.settings_json),
656
+ turnParams: JSON.parse(row.turn_params_json),
657
+ compactionTurn: row.compaction_turn_json
658
+ ? JSON.parse(row.compaction_turn_json)
659
+ : {
660
+ id: row.job_id, items: [], itemsView: "full", status: "inProgress", error: null,
661
+ startedAt: null, completedAt: null, durationMs: null,
662
+ },
663
+ status: row.status,
664
+ ...(row.summary === null ? {} : { summary: row.summary }),
665
+ ...(row.target_backend_thread_id === null ? {} : { targetBackendThreadId: row.target_backend_thread_id }),
666
+ ...(row.target_provider_turn_id === null ? {} : { targetProviderTurnId: row.target_provider_turn_id }),
667
+ ...(row.error === null ? {} : { error: row.error }),
668
+ createdAt: row.created_at,
669
+ updatedAt: row.updated_at,
670
+ };
671
+ }
672
+ forkSelectionFromRow(row) {
673
+ return {
674
+ targetPublicThreadId: row.target_public_thread_id,
675
+ sourcePublicThreadId: row.source_public_thread_id,
676
+ provisionalEpochId: row.provisional_epoch_id,
677
+ expectedTargetRevision: row.expected_target_revision,
678
+ ...(row.selected_epoch_id === null ? {} : { selectedEpochId: row.selected_epoch_id }),
679
+ status: row.status,
680
+ createdAt: row.created_at,
681
+ updatedAt: row.updated_at,
682
+ };
683
+ }
684
+ transaction(action) {
685
+ this.database.exec("BEGIN IMMEDIATE");
686
+ try {
687
+ const result = action();
688
+ this.database.exec("COMMIT");
689
+ return result;
690
+ }
691
+ catch (error) {
692
+ this.database.exec("ROLLBACK");
693
+ throw error;
694
+ }
695
+ }
85
696
  }
86
697
  //# sourceMappingURL=store.js.map