@jmoyers/harness 0.1.11 → 0.1.20

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 (232) hide show
  1. package/README.md +31 -39
  2. package/package.json +31 -11
  3. package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
  4. package/packages/harness-ai/src/stream-text.ts +13 -91
  5. package/packages/harness-ui/src/frame-primitives.ts +158 -0
  6. package/packages/harness-ui/src/index.ts +18 -0
  7. package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
  8. package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
  9. package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
  10. package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
  11. package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
  12. package/packages/harness-ui/src/interaction/input.ts +420 -0
  13. package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
  14. package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
  15. package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
  16. package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
  17. package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
  18. package/packages/harness-ui/src/kit.ts +476 -0
  19. package/packages/harness-ui/src/layout.ts +238 -0
  20. package/packages/harness-ui/src/modal-manager.ts +222 -0
  21. package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
  22. package/packages/harness-ui/src/surface.ts +252 -0
  23. package/packages/harness-ui/src/text-layout.ts +210 -0
  24. package/packages/nim-core/src/contracts.ts +239 -0
  25. package/packages/nim-core/src/event-store.ts +299 -0
  26. package/packages/nim-core/src/events.ts +53 -0
  27. package/packages/nim-core/src/index.ts +9 -0
  28. package/packages/nim-core/src/provider-router.ts +129 -0
  29. package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
  30. package/packages/nim-core/src/runtime-factory.ts +49 -0
  31. package/packages/nim-core/src/runtime.ts +1797 -0
  32. package/packages/nim-core/src/session-store.ts +516 -0
  33. package/packages/nim-core/src/telemetry.ts +48 -0
  34. package/packages/nim-test-tui/src/index.ts +150 -0
  35. package/packages/nim-ui-core/src/index.ts +1 -0
  36. package/packages/nim-ui-core/src/projection.ts +87 -0
  37. package/scripts/codex-live-mux-runtime.ts +2 -3872
  38. package/scripts/control-plane-daemon.ts +11 -0
  39. package/scripts/harness-bin.js +5 -0
  40. package/scripts/harness-commands.ts +300 -0
  41. package/scripts/harness-runtime.ts +82 -0
  42. package/scripts/harness.ts +33 -3019
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/gateway/runtime.ts +1872 -0
  46. package/src/cli/parsing/flags.ts +23 -0
  47. package/src/cli/parsing/session.ts +42 -0
  48. package/src/cli/runtime/context.ts +193 -0
  49. package/src/cli/runtime-app/application.ts +392 -0
  50. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  51. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  52. package/src/cli/workflows/runtime.ts +965 -0
  53. package/src/clients/tui/left-rail-interactions.ts +519 -0
  54. package/src/clients/tui/main-pane-interactions.ts +509 -0
  55. package/src/clients/tui/modal-input-routing.ts +71 -0
  56. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  57. package/src/clients/web/synced-selectors.ts +132 -0
  58. package/src/codex/live-session.ts +82 -29
  59. package/src/config/config-core.ts +348 -8
  60. package/src/config/harness.config.template.jsonc +33 -0
  61. package/src/control-plane/agent-realtime-api.ts +82 -427
  62. package/src/control-plane/session-summary.ts +10 -81
  63. package/src/control-plane/status/reducer-base.ts +12 -12
  64. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  65. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  66. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  67. package/src/control-plane/stream-client.ts +12 -2
  68. package/src/control-plane/stream-command-parser.ts +83 -143
  69. package/src/control-plane/stream-protocol.ts +53 -37
  70. package/src/control-plane/stream-server-command.ts +376 -69
  71. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  72. package/src/control-plane/stream-server.ts +864 -70
  73. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  74. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  75. package/src/core/state/observed-stream-cursor.ts +43 -0
  76. package/src/core/state/synced-observed-state.ts +273 -0
  77. package/src/core/store/harness-synced-store.ts +81 -0
  78. package/src/diff/budget.ts +136 -0
  79. package/src/diff/build.ts +289 -0
  80. package/src/diff/chunker.ts +146 -0
  81. package/src/diff/git-invoke.ts +315 -0
  82. package/src/diff/git-parse.ts +472 -0
  83. package/src/diff/hash.ts +70 -0
  84. package/src/diff/index.ts +24 -0
  85. package/src/diff/normalize.ts +134 -0
  86. package/src/diff/types.ts +178 -0
  87. package/src/diff-ui/args.ts +346 -0
  88. package/src/diff-ui/commands.ts +123 -0
  89. package/src/diff-ui/finder.ts +94 -0
  90. package/src/diff-ui/highlight.ts +127 -0
  91. package/src/diff-ui/index.ts +2 -0
  92. package/src/diff-ui/model.ts +141 -0
  93. package/src/diff-ui/pager.ts +412 -0
  94. package/src/diff-ui/render.ts +337 -0
  95. package/src/diff-ui/runtime.ts +379 -0
  96. package/src/diff-ui/state.ts +224 -0
  97. package/src/diff-ui/types.ts +236 -0
  98. package/src/domain/workspace.ts +68 -5
  99. package/src/mux/control-plane-op-queue.ts +93 -7
  100. package/src/mux/conversation-rail.ts +28 -71
  101. package/src/mux/dual-pane-core.ts +13 -13
  102. package/src/mux/harness-core-ui.ts +313 -42
  103. package/src/mux/input-shortcuts.ts +13 -131
  104. package/src/mux/keybinding-catalog.ts +340 -0
  105. package/src/mux/keybinding-registry.ts +103 -0
  106. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  107. package/src/mux/live-mux/command-menu.ts +167 -4
  108. package/src/mux/live-mux/conversation-state.ts +13 -0
  109. package/src/mux/live-mux/directory-resolution.ts +1 -1
  110. package/src/mux/live-mux/git-snapshot.ts +33 -2
  111. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  112. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  113. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  114. package/src/mux/live-mux/input-forwarding.ts +59 -2
  115. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  116. package/src/mux/live-mux/left-nav.ts +35 -0
  117. package/src/mux/live-mux/link-click.ts +292 -0
  118. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  119. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  120. package/src/mux/live-mux/modal-input-reducers.ts +77 -12
  121. package/src/mux/live-mux/modal-overlays.ts +168 -34
  122. package/src/mux/live-mux/modal-pointer.ts +3 -7
  123. package/src/mux/live-mux/modal-prompt-handlers.ts +23 -2
  124. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  125. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  126. package/src/mux/live-mux/pointer-routing.ts +5 -2
  127. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  128. package/src/mux/live-mux/rail-layout.ts +33 -30
  129. package/src/mux/live-mux/release-notes.ts +383 -0
  130. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  131. package/src/mux/live-mux/repository-folding.ts +3 -0
  132. package/src/mux/live-mux/selection.ts +0 -4
  133. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  134. package/src/mux/project-pane-github-review.ts +271 -0
  135. package/src/mux/render-frame.ts +4 -0
  136. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  137. package/src/mux/task-composer.ts +21 -14
  138. package/src/mux/task-focused-pane.ts +118 -117
  139. package/src/mux/task-screen-keybindings.ts +10 -101
  140. package/src/mux/workspace-rail-model.ts +270 -104
  141. package/src/mux/workspace-rail.ts +45 -22
  142. package/src/pty/session-broker.ts +1 -1
  143. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  144. package/src/services/control-plane.ts +50 -32
  145. package/src/services/conversation-lifecycle.ts +118 -87
  146. package/src/services/conversation-startup-hydration.ts +20 -12
  147. package/src/services/directory-hydration.ts +21 -16
  148. package/src/services/event-persistence.ts +7 -0
  149. package/src/services/left-rail-pointer-handler.ts +329 -0
  150. package/src/services/mux-ui-state-persistence.ts +5 -1
  151. package/src/services/recording.ts +34 -26
  152. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  153. package/src/services/runtime-control-actions.ts +79 -61
  154. package/src/services/runtime-control-plane-ops.ts +122 -83
  155. package/src/services/runtime-conversation-actions.ts +40 -26
  156. package/src/services/runtime-conversation-activation.ts +73 -46
  157. package/src/services/runtime-conversation-starter.ts +53 -45
  158. package/src/services/runtime-conversation-title-edit.ts +91 -80
  159. package/src/services/runtime-envelope-handler.ts +107 -105
  160. package/src/services/runtime-git-state.ts +42 -29
  161. package/src/services/runtime-layout-resize.ts +3 -1
  162. package/src/services/runtime-left-rail-render.ts +99 -63
  163. package/src/services/runtime-nim-cli-session.ts +438 -0
  164. package/src/services/runtime-nim-session.ts +705 -0
  165. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  166. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  167. package/src/services/runtime-process-wiring.ts +29 -36
  168. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  169. package/src/services/runtime-render-flush.ts +63 -70
  170. package/src/services/runtime-render-lifecycle.ts +65 -64
  171. package/src/services/runtime-render-orchestrator.ts +55 -45
  172. package/src/services/runtime-render-pipeline.ts +106 -103
  173. package/src/services/runtime-render-state.ts +62 -49
  174. package/src/services/runtime-repository-actions.ts +97 -72
  175. package/src/services/runtime-right-pane-render.ts +80 -53
  176. package/src/services/runtime-shutdown.ts +38 -35
  177. package/src/services/runtime-stream-subscriptions.ts +35 -27
  178. package/src/services/runtime-task-composer-persistence.ts +71 -59
  179. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  180. package/src/services/runtime-task-editor-actions.ts +46 -29
  181. package/src/services/runtime-task-pane-actions.ts +220 -134
  182. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  183. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  184. package/src/services/runtime-workspace-observed-events.ts +33 -184
  185. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  186. package/src/services/session-diagnostics-store.ts +217 -0
  187. package/src/services/startup-background-resume.ts +26 -21
  188. package/src/services/startup-orchestrator.ts +16 -13
  189. package/src/services/startup-paint-tracker.ts +29 -21
  190. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  191. package/src/services/startup-settled-gate.ts +25 -15
  192. package/src/services/startup-shutdown.ts +18 -22
  193. package/src/services/startup-state-hydration.ts +44 -34
  194. package/src/services/startup-visibility.ts +12 -4
  195. package/src/services/task-pane-selection-actions.ts +89 -72
  196. package/src/services/task-planning-hydration.ts +24 -18
  197. package/src/services/task-planning-observed-events.ts +50 -52
  198. package/src/services/workspace-observed-events.ts +66 -63
  199. package/src/storage/storage-lifecycle-core.ts +438 -0
  200. package/src/store/control-plane-store-normalize.ts +33 -242
  201. package/src/store/control-plane-store-types.ts +1 -35
  202. package/src/store/control-plane-store.ts +360 -56
  203. package/src/store/event-store.ts +366 -8
  204. package/src/terminal/snapshot-oracle.ts +207 -94
  205. package/src/ui/mux-theme.ts +112 -8
  206. package/src/ui/panes/home-gridfire.ts +40 -31
  207. package/src/ui/panes/home.ts +10 -2
  208. package/src/ui/panes/nim.ts +315 -0
  209. package/src/mux/live-mux/actions-task.ts +0 -115
  210. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  211. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -85
  212. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  213. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  214. package/src/services/runtime-directory-actions.ts +0 -164
  215. package/src/services/runtime-input-pipeline.ts +0 -50
  216. package/src/services/runtime-input-router.ts +0 -195
  217. package/src/services/runtime-main-pane-input.ts +0 -230
  218. package/src/services/runtime-modal-input.ts +0 -137
  219. package/src/services/runtime-navigation-input.ts +0 -197
  220. package/src/services/runtime-rail-input.ts +0 -279
  221. package/src/services/runtime-task-pane.ts +0 -62
  222. package/src/services/runtime-workspace-actions.ts +0 -158
  223. package/src/ui/conversation-input-forwarder.ts +0 -114
  224. package/src/ui/conversation-selection-input.ts +0 -103
  225. package/src/ui/global-shortcut-input.ts +0 -89
  226. package/src/ui/input.ts +0 -269
  227. package/src/ui/kit.ts +0 -509
  228. package/src/ui/left-nav-input.ts +0 -80
  229. package/src/ui/left-rail-pointer-input.ts +0 -148
  230. package/src/ui/modals/manager.ts +0 -218
  231. package/src/ui/repository-fold-input.ts +0 -91
  232. package/src/ui/surface.ts +0 -224
@@ -1,5 +1,5 @@
1
1
  import { DatabaseSync } from './sqlite.ts';
2
- import { mkdirSync } from 'node:fs';
2
+ import { mkdirSync, statSync } from 'node:fs';
3
3
  import { dirname } from 'node:path';
4
4
  import type { NormalizedEventEnvelope } from '../events/normalized-events.ts';
5
5
 
@@ -31,7 +31,23 @@ interface PersistedEvent {
31
31
  event: NormalizedEventEnvelope;
32
32
  }
33
33
 
34
+ interface OnlineCopyForwardCompactionStepResult {
35
+ readonly state: 'idle' | 'copying' | 'finalized';
36
+ readonly copiedRows: number;
37
+ }
38
+
34
39
  const EVENT_STORE_SCHEMA_VERSION = 1;
40
+ const EVENT_COMPACTION_SHADOW_TABLE = 'events_compaction_shadow';
41
+ const EVENT_COMPACTION_OLD_TABLE = 'events_compaction_old';
42
+ const EVENT_STORE_AUTO_VACUUM_MIGRATION_MAX_FILE_BYTES = 64 * 1024 * 1024;
43
+
44
+ function sqliteStatementChanges(value: unknown): number {
45
+ if (typeof value !== 'object' || value === null) {
46
+ return 0;
47
+ }
48
+ const candidate = value as Record<string, unknown>;
49
+ return typeof candidate.changes === 'number' ? candidate.changes : 0;
50
+ }
35
51
 
36
52
  function asObject(value: unknown): Record<string, unknown> {
37
53
  if (typeof value !== 'object' || value === null) {
@@ -98,12 +114,27 @@ export function normalizeStoredRow(value: unknown): {
98
114
 
99
115
  export class SqliteEventStore {
100
116
  private readonly db: DatabaseSync;
117
+ private readonly inMemory: boolean;
118
+ private readonly dbPath: string;
119
+ private copyForwardRequested = false;
120
+ private copyForwardActive = false;
121
+ private copyForwardCursorRowId = 0;
122
+ private readonly busyTimeoutMs: number;
101
123
 
102
- constructor(filePath = ':memory:') {
124
+ constructor(filePath = ':memory:', options?: { busyTimeoutMs?: number }) {
103
125
  const dbPath = this.preparePath(filePath);
126
+ this.dbPath = dbPath;
127
+ this.inMemory = dbPath === ':memory:';
128
+ this.busyTimeoutMs =
129
+ typeof options?.busyTimeoutMs === 'number' &&
130
+ Number.isFinite(options.busyTimeoutMs) &&
131
+ options.busyTimeoutMs > 0
132
+ ? Math.floor(options.busyTimeoutMs)
133
+ : 5000;
104
134
  this.db = new DatabaseSync(dbPath);
105
135
  this.configureConnection();
106
136
  this.initializeSchema();
137
+ this.ensureIncrementalAutoVacuumMode();
107
138
  }
108
139
 
109
140
  close(): void {
@@ -215,25 +246,212 @@ export class SqliteEventStore {
215
246
  });
216
247
  }
217
248
 
249
+ pruneEventsOlderThan(cutoffTs: string, limit = 1000): number {
250
+ const safeLimit = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 1000;
251
+ const result = this.db
252
+ .prepare(
253
+ `
254
+ DELETE FROM events
255
+ WHERE row_id IN (
256
+ SELECT row_id
257
+ FROM events
258
+ WHERE ts < ?
259
+ ORDER BY row_id ASC
260
+ LIMIT ?
261
+ )
262
+ `,
263
+ )
264
+ .run(cutoffTs, safeLimit);
265
+ const changes = sqliteStatementChanges(result);
266
+ if (changes > 0) {
267
+ this.copyForwardRequested = true;
268
+ }
269
+ return changes;
270
+ }
271
+
272
+ countEventsOlderThan(cutoffTs: string): number {
273
+ const row = this.db
274
+ .prepare(
275
+ `
276
+ SELECT COUNT(*) AS count
277
+ FROM events
278
+ WHERE ts < ?
279
+ `,
280
+ )
281
+ .get(cutoffTs);
282
+ const asRow = asObject(row);
283
+ return asNumber(asRow.count, 'count');
284
+ }
285
+
286
+ checkpointWal(mode: 'PASSIVE' | 'TRUNCATE' = 'PASSIVE'): void {
287
+ this.db.exec(`PRAGMA wal_checkpoint(${mode});`);
288
+ }
289
+
290
+ compactFreelistPages(maxPages: number): void {
291
+ const safeMaxPages = Number.isFinite(maxPages) ? Math.max(1, Math.floor(maxPages)) : 1;
292
+ this.db.exec(`PRAGMA incremental_vacuum(${String(safeMaxPages)});`);
293
+ }
294
+
295
+ runOnlineCopyForwardCompactionStep(
296
+ batchSize = 5000,
297
+ finalizeTailRows = 1200,
298
+ ): OnlineCopyForwardCompactionStepResult {
299
+ if (this.inMemory) {
300
+ return {
301
+ state: 'idle',
302
+ copiedRows: 0,
303
+ };
304
+ }
305
+
306
+ const safeBatchSize = Number.isFinite(batchSize) ? Math.max(1, Math.floor(batchSize)) : 5000;
307
+ const safeFinalizeTailRows = Number.isFinite(finalizeTailRows)
308
+ ? Math.max(1, Math.floor(finalizeTailRows))
309
+ : 1200;
310
+
311
+ if (!this.copyForwardActive) {
312
+ if (!this.copyForwardRequested) {
313
+ return { state: 'idle', copiedRows: 0 };
314
+ }
315
+ if (this.countTotalEventRows() === 0) {
316
+ this.copyForwardRequested = false;
317
+ return { state: 'idle', copiedRows: 0 };
318
+ }
319
+ this.db.exec('BEGIN IMMEDIATE TRANSACTION');
320
+ try {
321
+ this.resetCompactionShadowTable();
322
+ this.db.exec('COMMIT');
323
+ } catch (error) {
324
+ this.db.exec('ROLLBACK');
325
+ throw error;
326
+ }
327
+ this.copyForwardActive = true;
328
+ this.copyForwardCursorRowId = 0;
329
+ }
330
+
331
+ this.db.exec('BEGIN IMMEDIATE TRANSACTION');
332
+ let copiedRows: number;
333
+ let remainingRows: number;
334
+ try {
335
+ copiedRows = this.copyCompactionBatch(this.copyForwardCursorRowId, safeBatchSize);
336
+ if (copiedRows > 0) {
337
+ this.copyForwardCursorRowId = this.readCompactionShadowCursorRowId();
338
+ }
339
+ remainingRows = this.countEventsAfterRowId(this.copyForwardCursorRowId);
340
+ this.db.exec('COMMIT');
341
+ } catch (error) {
342
+ this.db.exec('ROLLBACK');
343
+ this.resetCompactionStateAfterFailure();
344
+ throw error;
345
+ }
346
+
347
+ if (remainingRows > safeFinalizeTailRows) {
348
+ return { state: 'copying', copiedRows };
349
+ }
350
+
351
+ this.db.exec('BEGIN IMMEDIATE TRANSACTION');
352
+ try {
353
+ const tailCopied = this.copyCompactionBatch(
354
+ this.copyForwardCursorRowId,
355
+ safeFinalizeTailRows,
356
+ );
357
+ if (tailCopied > 0) {
358
+ this.copyForwardCursorRowId = this.readCompactionShadowCursorRowId();
359
+ }
360
+ const postTailRemaining = this.countEventsAfterRowId(this.copyForwardCursorRowId);
361
+ if (postTailRemaining > 0) {
362
+ this.db.exec('COMMIT');
363
+ return { state: 'copying', copiedRows: copiedRows + tailCopied };
364
+ }
365
+ this.swapInCompactionShadowTable();
366
+ this.copyForwardRequested = false;
367
+ this.copyForwardActive = false;
368
+ this.copyForwardCursorRowId = 0;
369
+ this.db.exec('COMMIT');
370
+ return { state: 'finalized', copiedRows: copiedRows + tailCopied };
371
+ } catch (error) {
372
+ this.db.exec('ROLLBACK');
373
+ this.resetCompactionStateAfterFailure();
374
+ throw error;
375
+ }
376
+ }
377
+
218
378
  private initializeSchema(): void {
379
+ const initialVersion = this.readSchemaVersion();
380
+ this.assertSchemaVersionSupported(initialVersion);
381
+ if (
382
+ initialVersion === EVENT_STORE_SCHEMA_VERSION &&
383
+ this.hasSchemaV1Table() &&
384
+ this.hasSchemaV1Index()
385
+ ) {
386
+ return;
387
+ }
388
+
219
389
  this.db.exec('BEGIN IMMEDIATE TRANSACTION');
220
390
  try {
221
391
  const currentVersion = this.readSchemaVersion();
222
- if (currentVersion > EVENT_STORE_SCHEMA_VERSION) {
223
- throw new Error(
224
- `event store schema version ${String(currentVersion)} is newer than supported version ${String(EVENT_STORE_SCHEMA_VERSION)}`,
225
- );
226
- }
392
+ this.assertSchemaVersionSupported(currentVersion);
227
393
  this.applySchemaV1();
228
394
  this.writeSchemaVersion(EVENT_STORE_SCHEMA_VERSION);
229
395
  this.db.exec('COMMIT');
230
396
  } catch (error) {
231
397
  this.db.exec('ROLLBACK');
398
+ if (
399
+ this.isBusyLockError(error) &&
400
+ this.readSchemaVersion() === EVENT_STORE_SCHEMA_VERSION &&
401
+ this.hasSchemaV1Table() &&
402
+ this.hasSchemaV1Index()
403
+ ) {
404
+ return;
405
+ }
232
406
  throw error;
233
407
  }
234
408
  }
235
409
 
410
+ private assertSchemaVersionSupported(currentVersion: number): void {
411
+ if (currentVersion > EVENT_STORE_SCHEMA_VERSION) {
412
+ throw new Error(
413
+ `event store schema version ${String(currentVersion)} is newer than supported version ${String(EVENT_STORE_SCHEMA_VERSION)}`,
414
+ );
415
+ }
416
+ }
417
+
418
+ private hasSchemaV1Table(): boolean {
419
+ const row = this.db
420
+ .prepare(
421
+ `
422
+ SELECT 1 AS present
423
+ FROM sqlite_master
424
+ WHERE type = 'table' AND name = 'events'
425
+ LIMIT 1
426
+ `,
427
+ )
428
+ .get();
429
+ return row !== undefined;
430
+ }
431
+
432
+ private hasSchemaV1Index(): boolean {
433
+ const row = this.db
434
+ .prepare(
435
+ `
436
+ SELECT 1 AS present
437
+ FROM sqlite_master
438
+ WHERE type = 'index' AND name = 'idx_events_scope_cursor'
439
+ LIMIT 1
440
+ `,
441
+ )
442
+ .get();
443
+ return row !== undefined;
444
+ }
445
+
446
+ private isBusyLockError(error: unknown): boolean {
447
+ if (!(error instanceof Error)) {
448
+ return false;
449
+ }
450
+ return error.message.toLowerCase().includes('database is locked');
451
+ }
452
+
236
453
  private applySchemaV1(): void {
454
+ this.db.exec('PRAGMA auto_vacuum = INCREMENTAL;');
237
455
  this.db.exec(`
238
456
  CREATE TABLE IF NOT EXISTS events (
239
457
  row_id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -273,9 +491,149 @@ export class SqliteEventStore {
273
491
  }
274
492
 
275
493
  private configureConnection(): void {
494
+ this.db.exec(`PRAGMA busy_timeout = ${String(this.busyTimeoutMs)};`);
276
495
  this.db.exec('PRAGMA journal_mode = WAL;');
277
496
  this.db.exec('PRAGMA synchronous = NORMAL;');
278
- this.db.exec('PRAGMA busy_timeout = 2000;');
497
+ }
498
+
499
+ private ensureIncrementalAutoVacuumMode(): void {
500
+ if (this.inMemory) {
501
+ return;
502
+ }
503
+ if (!this.shouldAttemptAutoVacuumModeMigration()) {
504
+ return;
505
+ }
506
+ const modeRow = this.db.prepare('PRAGMA auto_vacuum;').get();
507
+ const mode = asNumber(asObject(modeRow).auto_vacuum, 'auto_vacuum');
508
+ if (mode === 2) {
509
+ return;
510
+ }
511
+ try {
512
+ this.db.exec('PRAGMA auto_vacuum = INCREMENTAL;');
513
+ this.db.exec('VACUUM;');
514
+ } catch {
515
+ // Best-effort migration only; maintenance can still run without mode flip.
516
+ }
517
+ }
518
+
519
+ private shouldAttemptAutoVacuumModeMigration(): boolean {
520
+ try {
521
+ return statSync(this.dbPath).size <= EVENT_STORE_AUTO_VACUUM_MIGRATION_MAX_FILE_BYTES;
522
+ } catch {
523
+ return true;
524
+ }
525
+ }
526
+
527
+ private countTotalEventRows(): number {
528
+ const row = this.db.prepare('SELECT COUNT(*) AS count FROM events;').get();
529
+ return asNumber(asObject(row).count, 'count');
530
+ }
531
+
532
+ private countEventsAfterRowId(rowId: number): number {
533
+ const row = this.db
534
+ .prepare('SELECT COUNT(*) AS count FROM events WHERE row_id > ?;')
535
+ .get(rowId);
536
+ return asNumber(asObject(row).count, 'count');
537
+ }
538
+
539
+ private copyCompactionBatch(afterRowId: number, limit: number): number {
540
+ const safeLimit = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 1;
541
+ const result = this.db
542
+ .prepare(
543
+ `
544
+ INSERT INTO ${EVENT_COMPACTION_SHADOW_TABLE} (
545
+ row_id,
546
+ tenant_id,
547
+ user_id,
548
+ workspace_id,
549
+ worktree_id,
550
+ conversation_id,
551
+ turn_id,
552
+ event_id,
553
+ source,
554
+ event_type,
555
+ ts,
556
+ payload_json
557
+ )
558
+ SELECT
559
+ row_id,
560
+ tenant_id,
561
+ user_id,
562
+ workspace_id,
563
+ worktree_id,
564
+ conversation_id,
565
+ turn_id,
566
+ event_id,
567
+ source,
568
+ event_type,
569
+ ts,
570
+ payload_json
571
+ FROM events
572
+ WHERE row_id > ?
573
+ ORDER BY row_id ASC
574
+ LIMIT ?
575
+ `,
576
+ )
577
+ .run(afterRowId, safeLimit);
578
+ return sqliteStatementChanges(result);
579
+ }
580
+
581
+ private readCompactionShadowCursorRowId(): number {
582
+ const row = this.db
583
+ .prepare(
584
+ `
585
+ SELECT row_id
586
+ FROM ${EVENT_COMPACTION_SHADOW_TABLE}
587
+ ORDER BY row_id DESC
588
+ LIMIT 1
589
+ `,
590
+ )
591
+ .get();
592
+ if (row === undefined) {
593
+ return 0;
594
+ }
595
+ return asNumber(asObject(row).row_id, 'row_id');
596
+ }
597
+
598
+ private resetCompactionShadowTable(): void {
599
+ this.db.exec(`DROP TABLE IF EXISTS ${EVENT_COMPACTION_SHADOW_TABLE};`);
600
+ this.db.exec(`
601
+ CREATE TABLE ${EVENT_COMPACTION_SHADOW_TABLE} (
602
+ row_id INTEGER PRIMARY KEY AUTOINCREMENT,
603
+ tenant_id TEXT NOT NULL,
604
+ user_id TEXT NOT NULL,
605
+ workspace_id TEXT NOT NULL,
606
+ worktree_id TEXT NOT NULL,
607
+ conversation_id TEXT NOT NULL,
608
+ turn_id TEXT,
609
+ event_id TEXT NOT NULL UNIQUE,
610
+ source TEXT NOT NULL,
611
+ event_type TEXT NOT NULL,
612
+ ts TEXT NOT NULL,
613
+ payload_json TEXT NOT NULL
614
+ );
615
+ `);
616
+ }
617
+
618
+ private swapInCompactionShadowTable(): void {
619
+ this.db.exec('DROP INDEX IF EXISTS idx_events_scope_cursor;');
620
+ this.db.exec(`ALTER TABLE events RENAME TO ${EVENT_COMPACTION_OLD_TABLE};`);
621
+ this.db.exec(`ALTER TABLE ${EVENT_COMPACTION_SHADOW_TABLE} RENAME TO events;`);
622
+ this.db.exec(`
623
+ CREATE INDEX IF NOT EXISTS idx_events_scope_cursor
624
+ ON events (tenant_id, user_id, conversation_id, row_id);
625
+ `);
626
+ this.db.exec(`DROP TABLE ${EVENT_COMPACTION_OLD_TABLE};`);
627
+ }
628
+
629
+ private resetCompactionStateAfterFailure(): void {
630
+ this.copyForwardActive = false;
631
+ this.copyForwardCursorRowId = 0;
632
+ try {
633
+ this.db.exec(`DROP TABLE IF EXISTS ${EVENT_COMPACTION_SHADOW_TABLE};`);
634
+ } catch {
635
+ // Best-effort cleanup only.
636
+ }
279
637
  }
280
638
 
281
639
  private preparePath(filePath: string): string {