@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.
- package/README.md +31 -39
- package/package.json +31 -11
- package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
- package/packages/harness-ai/src/stream-text.ts +13 -91
- package/packages/harness-ui/src/frame-primitives.ts +158 -0
- package/packages/harness-ui/src/index.ts +18 -0
- package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
- package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
- package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
- package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
- package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
- package/packages/harness-ui/src/interaction/input.ts +420 -0
- package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
- package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
- package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
- package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
- package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
- package/packages/harness-ui/src/kit.ts +476 -0
- package/packages/harness-ui/src/layout.ts +238 -0
- package/packages/harness-ui/src/modal-manager.ts +222 -0
- package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
- package/packages/harness-ui/src/surface.ts +252 -0
- package/packages/harness-ui/src/text-layout.ts +210 -0
- package/packages/nim-core/src/contracts.ts +239 -0
- package/packages/nim-core/src/event-store.ts +299 -0
- package/packages/nim-core/src/events.ts +53 -0
- package/packages/nim-core/src/index.ts +9 -0
- package/packages/nim-core/src/provider-router.ts +129 -0
- package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
- package/packages/nim-core/src/runtime-factory.ts +49 -0
- package/packages/nim-core/src/runtime.ts +1797 -0
- package/packages/nim-core/src/session-store.ts +516 -0
- package/packages/nim-core/src/telemetry.ts +48 -0
- package/packages/nim-test-tui/src/index.ts +150 -0
- package/packages/nim-ui-core/src/index.ts +1 -0
- package/packages/nim-ui-core/src/projection.ts +87 -0
- package/scripts/codex-live-mux-runtime.ts +2 -3872
- package/scripts/control-plane-daemon.ts +11 -0
- package/scripts/harness-bin.js +5 -0
- package/scripts/harness-commands.ts +300 -0
- package/scripts/harness-runtime.ts +82 -0
- package/scripts/harness.ts +33 -3019
- package/scripts/nim-tui-smoke.ts +748 -0
- package/src/cli/auth/runtime.ts +948 -0
- package/src/cli/gateway/runtime.ts +1872 -0
- package/src/cli/parsing/flags.ts +23 -0
- package/src/cli/parsing/session.ts +42 -0
- package/src/cli/runtime/context.ts +193 -0
- package/src/cli/runtime-app/application.ts +392 -0
- package/src/cli/runtime-infra/gateway-control.ts +729 -0
- package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
- package/src/cli/workflows/runtime.ts +965 -0
- package/src/clients/tui/left-rail-interactions.ts +519 -0
- package/src/clients/tui/main-pane-interactions.ts +509 -0
- package/src/clients/tui/modal-input-routing.ts +71 -0
- package/src/clients/tui/render-snapshot-adapter.ts +88 -0
- package/src/clients/web/synced-selectors.ts +132 -0
- package/src/codex/live-session.ts +82 -29
- package/src/config/config-core.ts +348 -8
- package/src/config/harness.config.template.jsonc +33 -0
- package/src/control-plane/agent-realtime-api.ts +82 -427
- package/src/control-plane/session-summary.ts +10 -81
- package/src/control-plane/status/reducer-base.ts +12 -12
- package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
- package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
- package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
- package/src/control-plane/stream-client.ts +12 -2
- package/src/control-plane/stream-command-parser.ts +83 -143
- package/src/control-plane/stream-protocol.ts +53 -37
- package/src/control-plane/stream-server-command.ts +376 -69
- package/src/control-plane/stream-server-session-runtime.ts +3 -2
- package/src/control-plane/stream-server.ts +864 -70
- package/src/control-plane/stream-session-runtime-types.ts +41 -0
- package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
- package/src/core/state/observed-stream-cursor.ts +43 -0
- package/src/core/state/synced-observed-state.ts +273 -0
- package/src/core/store/harness-synced-store.ts +81 -0
- package/src/diff/budget.ts +136 -0
- package/src/diff/build.ts +289 -0
- package/src/diff/chunker.ts +146 -0
- package/src/diff/git-invoke.ts +315 -0
- package/src/diff/git-parse.ts +472 -0
- package/src/diff/hash.ts +70 -0
- package/src/diff/index.ts +24 -0
- package/src/diff/normalize.ts +134 -0
- package/src/diff/types.ts +178 -0
- package/src/diff-ui/args.ts +346 -0
- package/src/diff-ui/commands.ts +123 -0
- package/src/diff-ui/finder.ts +94 -0
- package/src/diff-ui/highlight.ts +127 -0
- package/src/diff-ui/index.ts +2 -0
- package/src/diff-ui/model.ts +141 -0
- package/src/diff-ui/pager.ts +412 -0
- package/src/diff-ui/render.ts +337 -0
- package/src/diff-ui/runtime.ts +379 -0
- package/src/diff-ui/state.ts +224 -0
- package/src/diff-ui/types.ts +236 -0
- package/src/domain/workspace.ts +68 -5
- package/src/mux/control-plane-op-queue.ts +93 -7
- package/src/mux/conversation-rail.ts +28 -71
- package/src/mux/dual-pane-core.ts +13 -13
- package/src/mux/harness-core-ui.ts +313 -42
- package/src/mux/input-shortcuts.ts +13 -131
- package/src/mux/keybinding-catalog.ts +340 -0
- package/src/mux/keybinding-registry.ts +103 -0
- package/src/mux/live-mux/command-menu-open-in.ts +280 -0
- package/src/mux/live-mux/command-menu.ts +167 -4
- package/src/mux/live-mux/conversation-state.ts +13 -0
- package/src/mux/live-mux/directory-resolution.ts +1 -1
- package/src/mux/live-mux/git-snapshot.ts +33 -2
- package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
- package/src/mux/live-mux/home-pane-drop.ts +1 -1
- package/src/mux/live-mux/home-pane-pointer.ts +10 -0
- package/src/mux/live-mux/input-forwarding.ts +59 -2
- package/src/mux/live-mux/left-nav-activation.ts +124 -7
- package/src/mux/live-mux/left-nav.ts +35 -0
- package/src/mux/live-mux/link-click.ts +292 -0
- package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
- package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
- package/src/mux/live-mux/modal-input-reducers.ts +77 -12
- package/src/mux/live-mux/modal-overlays.ts +168 -34
- package/src/mux/live-mux/modal-pointer.ts +3 -7
- package/src/mux/live-mux/modal-prompt-handlers.ts +23 -2
- package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
- package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
- package/src/mux/live-mux/pointer-routing.ts +5 -2
- package/src/mux/live-mux/project-pane-pointer.ts +8 -0
- package/src/mux/live-mux/rail-layout.ts +33 -30
- package/src/mux/live-mux/release-notes.ts +383 -0
- package/src/mux/live-mux/render-trace-analysis.ts +52 -7
- package/src/mux/live-mux/repository-folding.ts +3 -0
- package/src/mux/live-mux/selection.ts +0 -4
- package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
- package/src/mux/project-pane-github-review.ts +271 -0
- package/src/mux/render-frame.ts +4 -0
- package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
- package/src/mux/task-composer.ts +21 -14
- package/src/mux/task-focused-pane.ts +118 -117
- package/src/mux/task-screen-keybindings.ts +10 -101
- package/src/mux/workspace-rail-model.ts +270 -104
- package/src/mux/workspace-rail.ts +45 -22
- package/src/pty/session-broker.ts +1 -1
- package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
- package/src/services/control-plane.ts +50 -32
- package/src/services/conversation-lifecycle.ts +118 -87
- package/src/services/conversation-startup-hydration.ts +20 -12
- package/src/services/directory-hydration.ts +21 -16
- package/src/services/event-persistence.ts +7 -0
- package/src/services/left-rail-pointer-handler.ts +329 -0
- package/src/services/mux-ui-state-persistence.ts +5 -1
- package/src/services/recording.ts +34 -26
- package/src/services/runtime-command-menu-agent-tools.ts +1 -1
- package/src/services/runtime-control-actions.ts +79 -61
- package/src/services/runtime-control-plane-ops.ts +122 -83
- package/src/services/runtime-conversation-actions.ts +40 -26
- package/src/services/runtime-conversation-activation.ts +73 -46
- package/src/services/runtime-conversation-starter.ts +53 -45
- package/src/services/runtime-conversation-title-edit.ts +91 -80
- package/src/services/runtime-envelope-handler.ts +107 -105
- package/src/services/runtime-git-state.ts +42 -29
- package/src/services/runtime-layout-resize.ts +3 -1
- package/src/services/runtime-left-rail-render.ts +99 -63
- package/src/services/runtime-nim-cli-session.ts +438 -0
- package/src/services/runtime-nim-session.ts +705 -0
- package/src/services/runtime-nim-tool-bridge.ts +141 -0
- package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
- package/src/services/runtime-process-wiring.ts +29 -36
- package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
- package/src/services/runtime-render-flush.ts +63 -70
- package/src/services/runtime-render-lifecycle.ts +65 -64
- package/src/services/runtime-render-orchestrator.ts +55 -45
- package/src/services/runtime-render-pipeline.ts +106 -103
- package/src/services/runtime-render-state.ts +62 -49
- package/src/services/runtime-repository-actions.ts +97 -72
- package/src/services/runtime-right-pane-render.ts +80 -53
- package/src/services/runtime-shutdown.ts +38 -35
- package/src/services/runtime-stream-subscriptions.ts +35 -27
- package/src/services/runtime-task-composer-persistence.ts +71 -59
- package/src/services/runtime-task-composer-snapshot.ts +14 -0
- package/src/services/runtime-task-editor-actions.ts +46 -29
- package/src/services/runtime-task-pane-actions.ts +220 -134
- package/src/services/runtime-task-pane-shortcuts.ts +323 -123
- package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
- package/src/services/runtime-workspace-observed-events.ts +33 -184
- package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
- package/src/services/session-diagnostics-store.ts +217 -0
- package/src/services/startup-background-resume.ts +26 -21
- package/src/services/startup-orchestrator.ts +16 -13
- package/src/services/startup-paint-tracker.ts +29 -21
- package/src/services/startup-persisted-conversation-queue.ts +19 -13
- package/src/services/startup-settled-gate.ts +25 -15
- package/src/services/startup-shutdown.ts +18 -22
- package/src/services/startup-state-hydration.ts +44 -34
- package/src/services/startup-visibility.ts +12 -4
- package/src/services/task-pane-selection-actions.ts +89 -72
- package/src/services/task-planning-hydration.ts +24 -18
- package/src/services/task-planning-observed-events.ts +50 -52
- package/src/services/workspace-observed-events.ts +66 -63
- package/src/storage/storage-lifecycle-core.ts +438 -0
- package/src/store/control-plane-store-normalize.ts +33 -242
- package/src/store/control-plane-store-types.ts +1 -35
- package/src/store/control-plane-store.ts +360 -56
- package/src/store/event-store.ts +366 -8
- package/src/terminal/snapshot-oracle.ts +207 -94
- package/src/ui/mux-theme.ts +112 -8
- package/src/ui/panes/home-gridfire.ts +40 -31
- package/src/ui/panes/home.ts +10 -2
- package/src/ui/panes/nim.ts +315 -0
- package/src/mux/live-mux/actions-task.ts +0 -115
- package/src/mux/live-mux/left-rail-actions.ts +0 -118
- package/src/mux/live-mux/left-rail-conversation-click.ts +0 -85
- package/src/mux/live-mux/left-rail-pointer.ts +0 -74
- package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
- package/src/services/runtime-directory-actions.ts +0 -164
- package/src/services/runtime-input-pipeline.ts +0 -50
- package/src/services/runtime-input-router.ts +0 -195
- package/src/services/runtime-main-pane-input.ts +0 -230
- package/src/services/runtime-modal-input.ts +0 -137
- package/src/services/runtime-navigation-input.ts +0 -197
- package/src/services/runtime-rail-input.ts +0 -279
- package/src/services/runtime-task-pane.ts +0 -62
- package/src/services/runtime-workspace-actions.ts +0 -158
- package/src/ui/conversation-input-forwarder.ts +0 -114
- package/src/ui/conversation-selection-input.ts +0 -103
- package/src/ui/global-shortcut-input.ts +0 -89
- package/src/ui/input.ts +0 -269
- package/src/ui/kit.ts +0 -509
- package/src/ui/left-nav-input.ts +0 -80
- package/src/ui/left-rail-pointer-input.ts +0 -148
- package/src/ui/modals/manager.ts +0 -218
- package/src/ui/repository-fold-input.ts +0 -91
- package/src/ui/surface.ts +0 -224
package/src/store/event-store.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|