@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.
- package/README.md +31 -35
- 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/{src/ui/modals/manager.ts → packages/harness-ui/src/modal-manager.ts} +94 -64
- 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 -3721
- package/scripts/control-plane-daemon.ts +24 -2
- 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 -3007
- package/scripts/nim-tui-smoke.ts +748 -0
- package/src/cli/auth/runtime.ts +948 -0
- package/src/cli/default-gateway-pointer.ts +193 -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 +361 -10
- package/src/config/harness-paths.ts +4 -7
- package/src/config/harness-runtime-migration.ts +142 -19
- package/src/config/harness.config.template.jsonc +33 -0
- package/src/config/secrets-core.ts +92 -4
- package/src/control-plane/agent-realtime-api.ts +82 -427
- package/src/control-plane/prompt/thread-title-namer.ts +49 -23
- 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-background.ts +18 -2
- 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 +943 -80
- 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/conversations.ts +11 -7
- package/src/domain/workspace.ts +76 -4
- 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 +22 -112
- 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-parsing.ts +16 -0
- 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 +106 -8
- package/src/mux/live-mux/modal-overlays.ts +210 -31
- package/src/mux/live-mux/modal-pointer.ts +3 -7
- package/src/mux/live-mux/modal-prompt-handlers.ts +107 -1
- 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 +19 -82
- 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 +82 -30
- package/src/services/runtime-conversation-starter.ts +80 -48
- 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 -70
- 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 +396 -56
- package/src/store/event-store.ts +397 -3
- 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 -82
- 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 -189
- package/src/services/runtime-main-pane-input.ts +0 -230
- package/src/services/runtime-modal-input.ts +0 -119
- package/src/services/runtime-navigation-input.ts +0 -197
- package/src/services/runtime-rail-input.ts +0 -278
- 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 -238
- 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/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,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
|
-
|
|
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 {
|