@jmoyers/harness 0.1.10 → 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 (239) hide show
  1. package/README.md +31 -35
  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/{src/ui/modals/manager.ts → packages/harness-ui/src/modal-manager.ts} +94 -64
  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 -3721
  38. package/scripts/control-plane-daemon.ts +24 -2
  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 -3007
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/default-gateway-pointer.ts +193 -0
  46. package/src/cli/gateway/runtime.ts +1872 -0
  47. package/src/cli/parsing/flags.ts +23 -0
  48. package/src/cli/parsing/session.ts +42 -0
  49. package/src/cli/runtime/context.ts +193 -0
  50. package/src/cli/runtime-app/application.ts +392 -0
  51. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  52. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  53. package/src/cli/workflows/runtime.ts +965 -0
  54. package/src/clients/tui/left-rail-interactions.ts +519 -0
  55. package/src/clients/tui/main-pane-interactions.ts +509 -0
  56. package/src/clients/tui/modal-input-routing.ts +71 -0
  57. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  58. package/src/clients/web/synced-selectors.ts +132 -0
  59. package/src/codex/live-session.ts +82 -29
  60. package/src/config/config-core.ts +361 -10
  61. package/src/config/harness-paths.ts +4 -7
  62. package/src/config/harness-runtime-migration.ts +142 -19
  63. package/src/config/harness.config.template.jsonc +33 -0
  64. package/src/config/secrets-core.ts +92 -4
  65. package/src/control-plane/agent-realtime-api.ts +82 -427
  66. package/src/control-plane/prompt/thread-title-namer.ts +49 -23
  67. package/src/control-plane/session-summary.ts +10 -81
  68. package/src/control-plane/status/reducer-base.ts +12 -12
  69. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  70. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  71. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  72. package/src/control-plane/stream-client.ts +12 -2
  73. package/src/control-plane/stream-command-parser.ts +83 -143
  74. package/src/control-plane/stream-protocol.ts +53 -37
  75. package/src/control-plane/stream-server-background.ts +18 -2
  76. package/src/control-plane/stream-server-command.ts +376 -69
  77. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  78. package/src/control-plane/stream-server.ts +943 -80
  79. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  80. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  81. package/src/core/state/observed-stream-cursor.ts +43 -0
  82. package/src/core/state/synced-observed-state.ts +273 -0
  83. package/src/core/store/harness-synced-store.ts +81 -0
  84. package/src/diff/budget.ts +136 -0
  85. package/src/diff/build.ts +289 -0
  86. package/src/diff/chunker.ts +146 -0
  87. package/src/diff/git-invoke.ts +315 -0
  88. package/src/diff/git-parse.ts +472 -0
  89. package/src/diff/hash.ts +70 -0
  90. package/src/diff/index.ts +24 -0
  91. package/src/diff/normalize.ts +134 -0
  92. package/src/diff/types.ts +178 -0
  93. package/src/diff-ui/args.ts +346 -0
  94. package/src/diff-ui/commands.ts +123 -0
  95. package/src/diff-ui/finder.ts +94 -0
  96. package/src/diff-ui/highlight.ts +127 -0
  97. package/src/diff-ui/index.ts +2 -0
  98. package/src/diff-ui/model.ts +141 -0
  99. package/src/diff-ui/pager.ts +412 -0
  100. package/src/diff-ui/render.ts +337 -0
  101. package/src/diff-ui/runtime.ts +379 -0
  102. package/src/diff-ui/state.ts +224 -0
  103. package/src/diff-ui/types.ts +236 -0
  104. package/src/domain/conversations.ts +11 -7
  105. package/src/domain/workspace.ts +76 -4
  106. package/src/mux/control-plane-op-queue.ts +93 -7
  107. package/src/mux/conversation-rail.ts +28 -71
  108. package/src/mux/dual-pane-core.ts +13 -13
  109. package/src/mux/harness-core-ui.ts +313 -42
  110. package/src/mux/input-shortcuts.ts +22 -112
  111. package/src/mux/keybinding-catalog.ts +340 -0
  112. package/src/mux/keybinding-registry.ts +103 -0
  113. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  114. package/src/mux/live-mux/command-menu.ts +167 -4
  115. package/src/mux/live-mux/conversation-state.ts +13 -0
  116. package/src/mux/live-mux/directory-resolution.ts +1 -1
  117. package/src/mux/live-mux/git-parsing.ts +16 -0
  118. package/src/mux/live-mux/git-snapshot.ts +33 -2
  119. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  120. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  121. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  122. package/src/mux/live-mux/input-forwarding.ts +59 -2
  123. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  124. package/src/mux/live-mux/left-nav.ts +35 -0
  125. package/src/mux/live-mux/link-click.ts +292 -0
  126. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  127. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  128. package/src/mux/live-mux/modal-input-reducers.ts +106 -8
  129. package/src/mux/live-mux/modal-overlays.ts +210 -31
  130. package/src/mux/live-mux/modal-pointer.ts +3 -7
  131. package/src/mux/live-mux/modal-prompt-handlers.ts +107 -1
  132. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  133. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  134. package/src/mux/live-mux/pointer-routing.ts +5 -2
  135. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  136. package/src/mux/live-mux/rail-layout.ts +33 -30
  137. package/src/mux/live-mux/release-notes.ts +383 -0
  138. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  139. package/src/mux/live-mux/repository-folding.ts +3 -0
  140. package/src/mux/live-mux/selection.ts +0 -4
  141. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  142. package/src/mux/project-pane-github-review.ts +271 -0
  143. package/src/mux/render-frame.ts +4 -0
  144. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  145. package/src/mux/task-composer.ts +21 -14
  146. package/src/mux/task-focused-pane.ts +118 -117
  147. package/src/mux/task-screen-keybindings.ts +19 -82
  148. package/src/mux/workspace-rail-model.ts +270 -104
  149. package/src/mux/workspace-rail.ts +45 -22
  150. package/src/pty/session-broker.ts +1 -1
  151. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  152. package/src/services/control-plane.ts +50 -32
  153. package/src/services/conversation-lifecycle.ts +118 -87
  154. package/src/services/conversation-startup-hydration.ts +20 -12
  155. package/src/services/directory-hydration.ts +21 -16
  156. package/src/services/event-persistence.ts +7 -0
  157. package/src/services/left-rail-pointer-handler.ts +329 -0
  158. package/src/services/mux-ui-state-persistence.ts +5 -1
  159. package/src/services/recording.ts +34 -26
  160. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  161. package/src/services/runtime-control-actions.ts +79 -61
  162. package/src/services/runtime-control-plane-ops.ts +122 -83
  163. package/src/services/runtime-conversation-actions.ts +40 -26
  164. package/src/services/runtime-conversation-activation.ts +82 -30
  165. package/src/services/runtime-conversation-starter.ts +80 -48
  166. package/src/services/runtime-conversation-title-edit.ts +91 -80
  167. package/src/services/runtime-envelope-handler.ts +107 -105
  168. package/src/services/runtime-git-state.ts +42 -29
  169. package/src/services/runtime-layout-resize.ts +3 -1
  170. package/src/services/runtime-left-rail-render.ts +99 -63
  171. package/src/services/runtime-nim-cli-session.ts +438 -0
  172. package/src/services/runtime-nim-session.ts +705 -0
  173. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  174. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  175. package/src/services/runtime-process-wiring.ts +29 -36
  176. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  177. package/src/services/runtime-render-flush.ts +63 -70
  178. package/src/services/runtime-render-lifecycle.ts +65 -64
  179. package/src/services/runtime-render-orchestrator.ts +55 -45
  180. package/src/services/runtime-render-pipeline.ts +106 -103
  181. package/src/services/runtime-render-state.ts +62 -49
  182. package/src/services/runtime-repository-actions.ts +97 -70
  183. package/src/services/runtime-right-pane-render.ts +80 -53
  184. package/src/services/runtime-shutdown.ts +38 -35
  185. package/src/services/runtime-stream-subscriptions.ts +35 -27
  186. package/src/services/runtime-task-composer-persistence.ts +71 -59
  187. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  188. package/src/services/runtime-task-editor-actions.ts +46 -29
  189. package/src/services/runtime-task-pane-actions.ts +220 -134
  190. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  191. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  192. package/src/services/runtime-workspace-observed-events.ts +33 -184
  193. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  194. package/src/services/session-diagnostics-store.ts +217 -0
  195. package/src/services/startup-background-resume.ts +26 -21
  196. package/src/services/startup-orchestrator.ts +16 -13
  197. package/src/services/startup-paint-tracker.ts +29 -21
  198. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  199. package/src/services/startup-settled-gate.ts +25 -15
  200. package/src/services/startup-shutdown.ts +18 -22
  201. package/src/services/startup-state-hydration.ts +44 -34
  202. package/src/services/startup-visibility.ts +12 -4
  203. package/src/services/task-pane-selection-actions.ts +89 -72
  204. package/src/services/task-planning-hydration.ts +24 -18
  205. package/src/services/task-planning-observed-events.ts +50 -52
  206. package/src/services/workspace-observed-events.ts +66 -63
  207. package/src/storage/storage-lifecycle-core.ts +438 -0
  208. package/src/store/control-plane-store-normalize.ts +33 -242
  209. package/src/store/control-plane-store-types.ts +1 -35
  210. package/src/store/control-plane-store.ts +396 -56
  211. package/src/store/event-store.ts +397 -3
  212. package/src/terminal/snapshot-oracle.ts +207 -94
  213. package/src/ui/mux-theme.ts +112 -8
  214. package/src/ui/panes/home-gridfire.ts +40 -31
  215. package/src/ui/panes/home.ts +10 -2
  216. package/src/ui/panes/nim.ts +315 -0
  217. package/src/mux/live-mux/actions-task.ts +0 -115
  218. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  219. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -82
  220. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  221. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  222. package/src/services/runtime-directory-actions.ts +0 -164
  223. package/src/services/runtime-input-pipeline.ts +0 -50
  224. package/src/services/runtime-input-router.ts +0 -189
  225. package/src/services/runtime-main-pane-input.ts +0 -230
  226. package/src/services/runtime-modal-input.ts +0 -119
  227. package/src/services/runtime-navigation-input.ts +0 -197
  228. package/src/services/runtime-rail-input.ts +0 -278
  229. package/src/services/runtime-task-pane.ts +0 -62
  230. package/src/services/runtime-workspace-actions.ts +0 -158
  231. package/src/ui/conversation-input-forwarder.ts +0 -114
  232. package/src/ui/conversation-selection-input.ts +0 -103
  233. package/src/ui/global-shortcut-input.ts +0 -89
  234. package/src/ui/input.ts +0 -238
  235. package/src/ui/kit.ts +0 -509
  236. package/src/ui/left-nav-input.ts +0 -80
  237. package/src/ui/left-rail-pointer-input.ts +0 -148
  238. package/src/ui/repository-fold-input.ts +0 -91
  239. 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,6 +31,24 @@ 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
+
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
+ }
51
+
34
52
  function asObject(value: unknown): Record<string, unknown> {
35
53
  if (typeof value !== 'object' || value === null) {
36
54
  throw new Error('expected object row');
@@ -96,12 +114,27 @@ export function normalizeStoredRow(value: unknown): {
96
114
 
97
115
  export class SqliteEventStore {
98
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;
99
123
 
100
- constructor(filePath = ':memory:') {
124
+ constructor(filePath = ':memory:', options?: { busyTimeoutMs?: number }) {
101
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;
102
134
  this.db = new DatabaseSync(dbPath);
103
135
  this.configureConnection();
104
136
  this.initializeSchema();
137
+ this.ensureIncrementalAutoVacuumMode();
105
138
  }
106
139
 
107
140
  close(): void {
@@ -213,7 +246,212 @@ export class SqliteEventStore {
213
246
  });
214
247
  }
215
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
+
216
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
+
389
+ this.db.exec('BEGIN IMMEDIATE TRANSACTION');
390
+ try {
391
+ const currentVersion = this.readSchemaVersion();
392
+ this.assertSchemaVersionSupported(currentVersion);
393
+ this.applySchemaV1();
394
+ this.writeSchemaVersion(EVENT_STORE_SCHEMA_VERSION);
395
+ this.db.exec('COMMIT');
396
+ } catch (error) {
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
+ }
406
+ throw error;
407
+ }
408
+ }
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
+
453
+ private applySchemaV1(): void {
454
+ this.db.exec('PRAGMA auto_vacuum = INCREMENTAL;');
217
455
  this.db.exec(`
218
456
  CREATE TABLE IF NOT EXISTS events (
219
457
  row_id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -236,10 +474,166 @@ export class SqliteEventStore {
236
474
  `);
237
475
  }
238
476
 
477
+ private readSchemaVersion(): number {
478
+ const row = this.db.prepare('PRAGMA user_version;').get();
479
+ if (row === undefined) {
480
+ throw new Error('failed to read event store schema version');
481
+ }
482
+ const version = (row as Record<string, unknown>)['user_version'];
483
+ if (typeof version !== 'number' || !Number.isInteger(version) || version < 0) {
484
+ throw new Error(`invalid event store schema version value: ${String(version)}`);
485
+ }
486
+ return version;
487
+ }
488
+
489
+ private writeSchemaVersion(version: number): void {
490
+ this.db.exec(`PRAGMA user_version = ${String(version)};`);
491
+ }
492
+
239
493
  private configureConnection(): void {
494
+ this.db.exec(`PRAGMA busy_timeout = ${String(this.busyTimeoutMs)};`);
240
495
  this.db.exec('PRAGMA journal_mode = WAL;');
241
496
  this.db.exec('PRAGMA synchronous = NORMAL;');
242
- 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
+ }
243
637
  }
244
638
 
245
639
  private preparePath(filePath: string): string {