@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
|
@@ -3,7 +3,6 @@ import { mkdirSync } from 'node:fs';
|
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import {
|
|
6
|
-
applyTaskLinearInput,
|
|
7
6
|
normalizeGitHubPrJobRow,
|
|
8
7
|
normalizeGitHubPullRequestRow,
|
|
9
8
|
normalizeGitHubSyncStateRow,
|
|
@@ -13,7 +12,6 @@ import {
|
|
|
13
12
|
normalizeProjectSettingsRow,
|
|
14
13
|
asString,
|
|
15
14
|
asStringOrNull,
|
|
16
|
-
defaultTaskLinearRecord,
|
|
17
15
|
normalizeNonEmptyLabel,
|
|
18
16
|
normalizeRepositoryRow,
|
|
19
17
|
normalizeStoredConversationRow,
|
|
@@ -23,7 +21,6 @@ import {
|
|
|
23
21
|
normalizeTaskRow,
|
|
24
22
|
normalizeTelemetryRow,
|
|
25
23
|
normalizeTelemetrySource,
|
|
26
|
-
serializeTaskLinear,
|
|
27
24
|
sqliteStatementChanges,
|
|
28
25
|
uniqueValues,
|
|
29
26
|
} from './control-plane-store-normalize.ts';
|
|
@@ -40,13 +37,11 @@ import type {
|
|
|
40
37
|
ControlPlaneProjectTaskFocusMode,
|
|
41
38
|
ControlPlaneProjectThreadSpawnMode,
|
|
42
39
|
ControlPlaneRepositoryRecord,
|
|
43
|
-
ControlPlaneTaskLinearRecord,
|
|
44
40
|
ControlPlaneTaskRecord,
|
|
45
41
|
ControlPlaneTaskScopeKind,
|
|
46
42
|
ControlPlaneTaskStatus,
|
|
47
43
|
ControlPlaneTelemetryRecord,
|
|
48
44
|
ControlPlaneTelemetrySummary,
|
|
49
|
-
TaskLinearInput,
|
|
50
45
|
} from './control-plane-store-types.ts';
|
|
51
46
|
import type { PtyExit } from '../pty/pty_host.ts';
|
|
52
47
|
import type { CodexTelemetrySource } from '../control-plane/codex-telemetry.ts';
|
|
@@ -64,7 +59,7 @@ const DEFAULT_RUNTIME_STATUS_MODEL_JSON = JSON.stringify({
|
|
|
64
59
|
attentionReason: null,
|
|
65
60
|
lastKnownWork: null,
|
|
66
61
|
lastKnownWorkAt: null,
|
|
67
|
-
|
|
62
|
+
activityHint: null,
|
|
68
63
|
observedAt: new Date(0).toISOString(),
|
|
69
64
|
} satisfies StreamSessionStatusModel | null);
|
|
70
65
|
|
|
@@ -89,11 +84,28 @@ function initialRuntimeStatusModel(
|
|
|
89
84
|
attentionReason: null,
|
|
90
85
|
lastKnownWork: null,
|
|
91
86
|
lastKnownWorkAt: null,
|
|
92
|
-
|
|
87
|
+
activityHint: null,
|
|
93
88
|
observedAt,
|
|
94
89
|
};
|
|
95
90
|
}
|
|
96
91
|
|
|
92
|
+
function normalizeTaskTitle(value: string | null | undefined): string {
|
|
93
|
+
if (value === undefined || value === null) {
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
return value.trim();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizeTaskBody(value: string, field: string): string {
|
|
100
|
+
if (typeof value !== 'string') {
|
|
101
|
+
throw new Error(`expected string for ${field}`);
|
|
102
|
+
}
|
|
103
|
+
if (value.trim().length === 0) {
|
|
104
|
+
throw new Error(`${field} must be non-empty`);
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
|
|
97
109
|
export type {
|
|
98
110
|
ControlPlaneAutomationPolicyRecord,
|
|
99
111
|
ControlPlaneAutomationPolicyScope,
|
|
@@ -107,7 +119,6 @@ export type {
|
|
|
107
119
|
ControlPlaneProjectTaskFocusMode,
|
|
108
120
|
ControlPlaneProjectThreadSpawnMode,
|
|
109
121
|
ControlPlaneRepositoryRecord,
|
|
110
|
-
ControlPlaneTaskLinearRecord,
|
|
111
122
|
ControlPlaneTaskRecord,
|
|
112
123
|
ControlPlaneTaskScopeKind,
|
|
113
124
|
ControlPlaneTelemetryRecord,
|
|
@@ -204,17 +215,15 @@ interface CreateTaskInput {
|
|
|
204
215
|
workspaceId: string;
|
|
205
216
|
repositoryId?: string;
|
|
206
217
|
projectId?: string;
|
|
207
|
-
title
|
|
208
|
-
|
|
209
|
-
linear?: TaskLinearInput;
|
|
218
|
+
title?: string | null;
|
|
219
|
+
body?: string;
|
|
210
220
|
}
|
|
211
221
|
|
|
212
222
|
interface UpdateTaskInput {
|
|
213
|
-
title?: string;
|
|
214
|
-
|
|
223
|
+
title?: string | null;
|
|
224
|
+
body?: string;
|
|
215
225
|
repositoryId?: string | null;
|
|
216
226
|
projectId?: string | null;
|
|
217
|
-
linear?: TaskLinearInput | null;
|
|
218
227
|
}
|
|
219
228
|
|
|
220
229
|
interface ListTaskQuery {
|
|
@@ -356,15 +365,35 @@ interface ListGitHubSyncStateQuery {
|
|
|
356
365
|
}
|
|
357
366
|
|
|
358
367
|
const CONTROL_PLANE_SCHEMA_VERSION = 1;
|
|
368
|
+
const TELEMETRY_COMPACTION_SHADOW_TABLE = 'session_telemetry_compaction_shadow';
|
|
369
|
+
const TELEMETRY_COMPACTION_OLD_TABLE = 'session_telemetry_compaction_old';
|
|
370
|
+
|
|
371
|
+
interface OnlineCopyForwardCompactionStepResult {
|
|
372
|
+
readonly state: 'idle' | 'copying' | 'finalized';
|
|
373
|
+
readonly copiedRows: number;
|
|
374
|
+
}
|
|
359
375
|
|
|
360
376
|
export class SqliteControlPlaneStore {
|
|
361
377
|
private readonly db: DatabaseSync;
|
|
378
|
+
private readonly inMemory: boolean;
|
|
379
|
+
private telemetryCopyForwardRequested = false;
|
|
380
|
+
private telemetryCopyForwardActive = false;
|
|
381
|
+
private telemetryCopyForwardCursorRowId = 0;
|
|
382
|
+
private readonly busyTimeoutMs: number;
|
|
362
383
|
|
|
363
|
-
constructor(filePath = ':memory:') {
|
|
384
|
+
constructor(filePath = ':memory:', options?: { busyTimeoutMs?: number }) {
|
|
364
385
|
const resolvedPath = this.preparePath(filePath);
|
|
386
|
+
this.inMemory = resolvedPath === ':memory:';
|
|
387
|
+
this.busyTimeoutMs =
|
|
388
|
+
typeof options?.busyTimeoutMs === 'number' &&
|
|
389
|
+
Number.isFinite(options.busyTimeoutMs) &&
|
|
390
|
+
options.busyTimeoutMs > 0
|
|
391
|
+
? Math.floor(options.busyTimeoutMs)
|
|
392
|
+
: 5000;
|
|
365
393
|
this.db = new DatabaseSync(resolvedPath);
|
|
366
394
|
this.configureConnection();
|
|
367
395
|
this.initializeSchema();
|
|
396
|
+
this.ensureIncrementalAutoVacuumMode();
|
|
368
397
|
}
|
|
369
398
|
|
|
370
399
|
close(): void {
|
|
@@ -884,6 +913,144 @@ export class SqliteControlPlaneStore {
|
|
|
884
913
|
return sqliteStatementChanges(result) > 0;
|
|
885
914
|
}
|
|
886
915
|
|
|
916
|
+
pruneTelemetryOlderThan(cutoffIngestedAt: string, limit = 1000): number {
|
|
917
|
+
const safeLimit = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 1000;
|
|
918
|
+
const result = this.db
|
|
919
|
+
.prepare(
|
|
920
|
+
`
|
|
921
|
+
DELETE FROM session_telemetry
|
|
922
|
+
WHERE telemetry_id IN (
|
|
923
|
+
SELECT telemetry_id
|
|
924
|
+
FROM session_telemetry
|
|
925
|
+
WHERE ingested_at < ?
|
|
926
|
+
ORDER BY telemetry_id ASC
|
|
927
|
+
LIMIT ?
|
|
928
|
+
)
|
|
929
|
+
`,
|
|
930
|
+
)
|
|
931
|
+
.run(cutoffIngestedAt, safeLimit);
|
|
932
|
+
const changes = sqliteStatementChanges(result);
|
|
933
|
+
if (changes > 0) {
|
|
934
|
+
this.telemetryCopyForwardRequested = true;
|
|
935
|
+
}
|
|
936
|
+
return changes;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
countTelemetryOlderThan(cutoffIngestedAt: string): number {
|
|
940
|
+
const row = this.db
|
|
941
|
+
.prepare(
|
|
942
|
+
`
|
|
943
|
+
SELECT COUNT(*) AS count
|
|
944
|
+
FROM session_telemetry
|
|
945
|
+
WHERE ingested_at < ?
|
|
946
|
+
`,
|
|
947
|
+
)
|
|
948
|
+
.get(cutoffIngestedAt);
|
|
949
|
+
if (row === undefined) {
|
|
950
|
+
return 0;
|
|
951
|
+
}
|
|
952
|
+
const asRow = asRecord(row);
|
|
953
|
+
const count = asNumberOrNull(asRow.count, 'count');
|
|
954
|
+
return count ?? 0;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
checkpointWal(mode: 'PASSIVE' | 'TRUNCATE' = 'PASSIVE'): void {
|
|
958
|
+
this.db.exec(`PRAGMA wal_checkpoint(${mode});`);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
compactFreelistPages(maxPages: number): void {
|
|
962
|
+
const safeMaxPages = Number.isFinite(maxPages) ? Math.max(1, Math.floor(maxPages)) : 1;
|
|
963
|
+
this.db.exec(`PRAGMA incremental_vacuum(${String(safeMaxPages)});`);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
runOnlineCopyForwardCompactionStep(
|
|
967
|
+
batchSize = 5000,
|
|
968
|
+
finalizeTailRows = 1200,
|
|
969
|
+
): OnlineCopyForwardCompactionStepResult {
|
|
970
|
+
if (this.inMemory) {
|
|
971
|
+
return {
|
|
972
|
+
state: 'idle',
|
|
973
|
+
copiedRows: 0,
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const safeBatchSize = Number.isFinite(batchSize) ? Math.max(1, Math.floor(batchSize)) : 5000;
|
|
978
|
+
const safeFinalizeTailRows = Number.isFinite(finalizeTailRows)
|
|
979
|
+
? Math.max(1, Math.floor(finalizeTailRows))
|
|
980
|
+
: 1200;
|
|
981
|
+
|
|
982
|
+
if (!this.telemetryCopyForwardActive) {
|
|
983
|
+
if (!this.telemetryCopyForwardRequested) {
|
|
984
|
+
return { state: 'idle', copiedRows: 0 };
|
|
985
|
+
}
|
|
986
|
+
if (this.countTotalTelemetryRows() === 0) {
|
|
987
|
+
this.telemetryCopyForwardRequested = false;
|
|
988
|
+
return { state: 'idle', copiedRows: 0 };
|
|
989
|
+
}
|
|
990
|
+
this.db.exec('BEGIN IMMEDIATE TRANSACTION');
|
|
991
|
+
try {
|
|
992
|
+
this.resetTelemetryCompactionShadowTable();
|
|
993
|
+
this.db.exec('COMMIT');
|
|
994
|
+
} catch (error) {
|
|
995
|
+
this.db.exec('ROLLBACK');
|
|
996
|
+
throw error;
|
|
997
|
+
}
|
|
998
|
+
this.telemetryCopyForwardActive = true;
|
|
999
|
+
this.telemetryCopyForwardCursorRowId = 0;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
this.db.exec('BEGIN IMMEDIATE TRANSACTION');
|
|
1003
|
+
let copiedRows: number;
|
|
1004
|
+
let remainingRows: number;
|
|
1005
|
+
try {
|
|
1006
|
+
copiedRows = this.copyTelemetryCompactionBatch(
|
|
1007
|
+
this.telemetryCopyForwardCursorRowId,
|
|
1008
|
+
safeBatchSize,
|
|
1009
|
+
);
|
|
1010
|
+
if (copiedRows > 0) {
|
|
1011
|
+
this.telemetryCopyForwardCursorRowId = this.readTelemetryCompactionShadowCursorRowId();
|
|
1012
|
+
}
|
|
1013
|
+
remainingRows = this.countTelemetryRowsAfterId(this.telemetryCopyForwardCursorRowId);
|
|
1014
|
+
this.db.exec('COMMIT');
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
this.db.exec('ROLLBACK');
|
|
1017
|
+
this.resetTelemetryCompactionStateAfterFailure();
|
|
1018
|
+
throw error;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (remainingRows > safeFinalizeTailRows) {
|
|
1022
|
+
return { state: 'copying', copiedRows };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
this.db.exec('BEGIN IMMEDIATE TRANSACTION');
|
|
1026
|
+
try {
|
|
1027
|
+
const tailCopied = this.copyTelemetryCompactionBatch(
|
|
1028
|
+
this.telemetryCopyForwardCursorRowId,
|
|
1029
|
+
safeFinalizeTailRows,
|
|
1030
|
+
);
|
|
1031
|
+
if (tailCopied > 0) {
|
|
1032
|
+
this.telemetryCopyForwardCursorRowId = this.readTelemetryCompactionShadowCursorRowId();
|
|
1033
|
+
}
|
|
1034
|
+
const postTailRemaining = this.countTelemetryRowsAfterId(
|
|
1035
|
+
this.telemetryCopyForwardCursorRowId,
|
|
1036
|
+
);
|
|
1037
|
+
if (postTailRemaining > 0) {
|
|
1038
|
+
this.db.exec('COMMIT');
|
|
1039
|
+
return { state: 'copying', copiedRows: copiedRows + tailCopied };
|
|
1040
|
+
}
|
|
1041
|
+
this.swapInTelemetryCompactionShadowTable();
|
|
1042
|
+
this.telemetryCopyForwardRequested = false;
|
|
1043
|
+
this.telemetryCopyForwardActive = false;
|
|
1044
|
+
this.telemetryCopyForwardCursorRowId = 0;
|
|
1045
|
+
this.db.exec('COMMIT');
|
|
1046
|
+
return { state: 'finalized', copiedRows: copiedRows + tailCopied };
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
this.db.exec('ROLLBACK');
|
|
1049
|
+
this.resetTelemetryCompactionStateAfterFailure();
|
|
1050
|
+
throw error;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
887
1054
|
latestTelemetrySummary(sessionId: string): ControlPlaneTelemetrySummary | null {
|
|
888
1055
|
const row = this.db
|
|
889
1056
|
.prepare(
|
|
@@ -1266,9 +1433,8 @@ export class SqliteControlPlaneStore {
|
|
|
1266
1433
|
}
|
|
1267
1434
|
|
|
1268
1435
|
createTask(input: CreateTaskInput): ControlPlaneTaskRecord {
|
|
1269
|
-
const title =
|
|
1270
|
-
const
|
|
1271
|
-
const linear = applyTaskLinearInput(defaultTaskLinearRecord(), input.linear ?? {});
|
|
1436
|
+
const title = normalizeTaskTitle(input.title);
|
|
1437
|
+
const body = normalizeTaskBody(input.body ?? title, 'body');
|
|
1272
1438
|
this.db.exec('BEGIN IMMEDIATE TRANSACTION');
|
|
1273
1439
|
try {
|
|
1274
1440
|
const existing = this.getTask(input.taskId);
|
|
@@ -1277,6 +1443,9 @@ export class SqliteControlPlaneStore {
|
|
|
1277
1443
|
}
|
|
1278
1444
|
const repositoryId = input.repositoryId ?? null;
|
|
1279
1445
|
const projectId = input.projectId ?? null;
|
|
1446
|
+
if (repositoryId === null && projectId === null) {
|
|
1447
|
+
throw new Error('task scope required: repositoryId or projectId');
|
|
1448
|
+
}
|
|
1280
1449
|
if (repositoryId !== null) {
|
|
1281
1450
|
const repository = this.getActiveRepository(repositoryId);
|
|
1282
1451
|
this.assertScopeMatch(input, repository, 'task');
|
|
@@ -1300,8 +1469,7 @@ export class SqliteControlPlaneStore {
|
|
|
1300
1469
|
scope_kind,
|
|
1301
1470
|
project_id,
|
|
1302
1471
|
title,
|
|
1303
|
-
|
|
1304
|
-
linear_json,
|
|
1472
|
+
body,
|
|
1305
1473
|
status,
|
|
1306
1474
|
order_index,
|
|
1307
1475
|
claimed_by_controller_id,
|
|
@@ -1312,7 +1480,7 @@ export class SqliteControlPlaneStore {
|
|
|
1312
1480
|
completed_at,
|
|
1313
1481
|
created_at,
|
|
1314
1482
|
updated_at
|
|
1315
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
1483
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'draft', ?, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?)
|
|
1316
1484
|
`,
|
|
1317
1485
|
)
|
|
1318
1486
|
.run(
|
|
@@ -1324,8 +1492,7 @@ export class SqliteControlPlaneStore {
|
|
|
1324
1492
|
scopeKind,
|
|
1325
1493
|
projectId,
|
|
1326
1494
|
title,
|
|
1327
|
-
|
|
1328
|
-
serializeTaskLinear(linear),
|
|
1495
|
+
body,
|
|
1329
1496
|
orderIndex,
|
|
1330
1497
|
createdAt,
|
|
1331
1498
|
createdAt,
|
|
@@ -1355,8 +1522,7 @@ export class SqliteControlPlaneStore {
|
|
|
1355
1522
|
scope_kind,
|
|
1356
1523
|
project_id,
|
|
1357
1524
|
title,
|
|
1358
|
-
|
|
1359
|
-
linear_json,
|
|
1525
|
+
body,
|
|
1360
1526
|
status,
|
|
1361
1527
|
order_index,
|
|
1362
1528
|
claimed_by_controller_id,
|
|
@@ -1423,8 +1589,7 @@ export class SqliteControlPlaneStore {
|
|
|
1423
1589
|
scope_kind,
|
|
1424
1590
|
project_id,
|
|
1425
1591
|
title,
|
|
1426
|
-
|
|
1427
|
-
linear_json,
|
|
1592
|
+
body,
|
|
1428
1593
|
status,
|
|
1429
1594
|
order_index,
|
|
1430
1595
|
claimed_by_controller_id,
|
|
@@ -1450,19 +1615,14 @@ export class SqliteControlPlaneStore {
|
|
|
1450
1615
|
if (existing === null) {
|
|
1451
1616
|
return null;
|
|
1452
1617
|
}
|
|
1453
|
-
const title =
|
|
1454
|
-
|
|
1455
|
-
const description =
|
|
1456
|
-
update.description === undefined ? existing.description : update.description;
|
|
1618
|
+
const title = update.title === undefined ? existing.title : normalizeTaskTitle(update.title);
|
|
1619
|
+
const body = update.body === undefined ? existing.body : normalizeTaskBody(update.body, 'body');
|
|
1457
1620
|
const repositoryId =
|
|
1458
1621
|
update.repositoryId === undefined ? existing.repositoryId : update.repositoryId;
|
|
1459
1622
|
const projectId = update.projectId === undefined ? existing.projectId : update.projectId;
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
: update.linear === null
|
|
1464
|
-
? defaultTaskLinearRecord()
|
|
1465
|
-
: applyTaskLinearInput(existing.linear, update.linear);
|
|
1623
|
+
if (repositoryId === null && projectId === null) {
|
|
1624
|
+
throw new Error('task scope required: repositoryId or projectId');
|
|
1625
|
+
}
|
|
1466
1626
|
if (repositoryId !== null) {
|
|
1467
1627
|
const repository = this.getActiveRepository(repositoryId);
|
|
1468
1628
|
this.assertScopeMatch(existing, repository, 'task');
|
|
@@ -1482,22 +1642,12 @@ export class SqliteControlPlaneStore {
|
|
|
1482
1642
|
scope_kind = ?,
|
|
1483
1643
|
project_id = ?,
|
|
1484
1644
|
title = ?,
|
|
1485
|
-
|
|
1486
|
-
linear_json = ?,
|
|
1645
|
+
body = ?,
|
|
1487
1646
|
updated_at = ?
|
|
1488
1647
|
WHERE task_id = ?
|
|
1489
1648
|
`,
|
|
1490
1649
|
)
|
|
1491
|
-
.run(
|
|
1492
|
-
repositoryId,
|
|
1493
|
-
scopeKind,
|
|
1494
|
-
projectId,
|
|
1495
|
-
title,
|
|
1496
|
-
description,
|
|
1497
|
-
serializeTaskLinear(linear),
|
|
1498
|
-
updatedAt,
|
|
1499
|
-
taskId,
|
|
1500
|
-
);
|
|
1650
|
+
.run(repositoryId, scopeKind, projectId, title, body, updatedAt, taskId);
|
|
1501
1651
|
return this.getTask(taskId);
|
|
1502
1652
|
}
|
|
1503
1653
|
|
|
@@ -2756,8 +2906,7 @@ export class SqliteControlPlaneStore {
|
|
|
2756
2906
|
scope_kind TEXT NOT NULL DEFAULT 'global',
|
|
2757
2907
|
project_id TEXT REFERENCES directories(directory_id),
|
|
2758
2908
|
title TEXT NOT NULL,
|
|
2759
|
-
|
|
2760
|
-
linear_json TEXT NOT NULL DEFAULT '{}',
|
|
2909
|
+
body TEXT NOT NULL DEFAULT '',
|
|
2761
2910
|
status TEXT NOT NULL,
|
|
2762
2911
|
order_index INTEGER NOT NULL,
|
|
2763
2912
|
claimed_by_controller_id TEXT,
|
|
@@ -2774,13 +2923,20 @@ export class SqliteControlPlaneStore {
|
|
|
2774
2923
|
CREATE INDEX IF NOT EXISTS idx_tasks_scope
|
|
2775
2924
|
ON tasks (tenant_id, user_id, workspace_id, order_index, created_at, task_id);
|
|
2776
2925
|
`);
|
|
2777
|
-
this.ensureColumnExists('tasks', 'linear_json', `linear_json TEXT NOT NULL DEFAULT '{}'`);
|
|
2778
2926
|
this.ensureColumnExists('tasks', 'scope_kind', `scope_kind TEXT NOT NULL DEFAULT 'global'`);
|
|
2779
2927
|
this.ensureColumnExists(
|
|
2780
2928
|
'tasks',
|
|
2781
2929
|
'project_id',
|
|
2782
2930
|
`project_id TEXT REFERENCES directories(directory_id)`,
|
|
2783
2931
|
);
|
|
2932
|
+
this.ensureColumnExists('tasks', 'body', `body TEXT NOT NULL DEFAULT ''`);
|
|
2933
|
+
if (this.columnExists('tasks', 'description')) {
|
|
2934
|
+
this.db.exec(`
|
|
2935
|
+
UPDATE tasks
|
|
2936
|
+
SET body = description
|
|
2937
|
+
WHERE (body IS NULL OR TRIM(body) = '') AND description IS NOT NULL
|
|
2938
|
+
`);
|
|
2939
|
+
}
|
|
2784
2940
|
this.db.exec(`
|
|
2785
2941
|
CREATE INDEX IF NOT EXISTS idx_tasks_scope_kind
|
|
2786
2942
|
ON tasks (tenant_id, user_id, workspace_id, scope_kind, repository_id, project_id, order_index);
|
|
@@ -2803,6 +2959,10 @@ export class SqliteControlPlaneStore {
|
|
|
2803
2959
|
SET scope_kind = 'repository'
|
|
2804
2960
|
WHERE scope_kind = 'global' AND repository_id IS NOT NULL AND project_id IS NULL;
|
|
2805
2961
|
`);
|
|
2962
|
+
this.db.exec(`
|
|
2963
|
+
DELETE FROM tasks
|
|
2964
|
+
WHERE repository_id IS NULL AND project_id IS NULL;
|
|
2965
|
+
`);
|
|
2806
2966
|
|
|
2807
2967
|
this.db.exec(`
|
|
2808
2968
|
CREATE TABLE IF NOT EXISTS project_settings (
|
|
@@ -2960,18 +3120,162 @@ export class SqliteControlPlaneStore {
|
|
|
2960
3120
|
}
|
|
2961
3121
|
|
|
2962
3122
|
private configureConnection(): void {
|
|
3123
|
+
this.db.exec('PRAGMA auto_vacuum = INCREMENTAL;');
|
|
2963
3124
|
this.db.exec('PRAGMA journal_mode = WAL;');
|
|
2964
3125
|
this.db.exec('PRAGMA synchronous = NORMAL;');
|
|
2965
|
-
this.db.exec(
|
|
3126
|
+
this.db.exec(`PRAGMA busy_timeout = ${String(this.busyTimeoutMs)};`);
|
|
2966
3127
|
}
|
|
2967
3128
|
|
|
2968
|
-
private
|
|
3129
|
+
private ensureIncrementalAutoVacuumMode(): void {
|
|
3130
|
+
if (this.inMemory) {
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
const modeRow = this.db.prepare('PRAGMA auto_vacuum;').get();
|
|
3134
|
+
if (modeRow === undefined) {
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
const mode = asNumberOrNull(asRecord(modeRow).auto_vacuum, 'auto_vacuum');
|
|
3138
|
+
if (mode === 2) {
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
try {
|
|
3142
|
+
this.db.exec('PRAGMA auto_vacuum = INCREMENTAL;');
|
|
3143
|
+
this.db.exec('VACUUM;');
|
|
3144
|
+
} catch {
|
|
3145
|
+
// Best-effort migration only; maintenance can still run without mode flip.
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
private countTotalTelemetryRows(): number {
|
|
3150
|
+
const row = this.db.prepare('SELECT COUNT(*) AS count FROM session_telemetry;').get();
|
|
3151
|
+
if (row === undefined) {
|
|
3152
|
+
return 0;
|
|
3153
|
+
}
|
|
3154
|
+
return asNumberOrNull(asRecord(row).count, 'count') ?? 0;
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
private countTelemetryRowsAfterId(telemetryId: number): number {
|
|
3158
|
+
const row = this.db
|
|
3159
|
+
.prepare('SELECT COUNT(*) AS count FROM session_telemetry WHERE telemetry_id > ?;')
|
|
3160
|
+
.get(telemetryId);
|
|
3161
|
+
if (row === undefined) {
|
|
3162
|
+
return 0;
|
|
3163
|
+
}
|
|
3164
|
+
return asNumberOrNull(asRecord(row).count, 'count') ?? 0;
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
private copyTelemetryCompactionBatch(afterTelemetryId: number, limit: number): number {
|
|
3168
|
+
const safeLimit = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 1;
|
|
3169
|
+
const result = this.db
|
|
3170
|
+
.prepare(
|
|
3171
|
+
`
|
|
3172
|
+
INSERT INTO ${TELEMETRY_COMPACTION_SHADOW_TABLE} (
|
|
3173
|
+
telemetry_id,
|
|
3174
|
+
source,
|
|
3175
|
+
session_id,
|
|
3176
|
+
provider_thread_id,
|
|
3177
|
+
event_name,
|
|
3178
|
+
severity,
|
|
3179
|
+
summary,
|
|
3180
|
+
observed_at,
|
|
3181
|
+
ingested_at,
|
|
3182
|
+
payload_json,
|
|
3183
|
+
fingerprint
|
|
3184
|
+
)
|
|
3185
|
+
SELECT
|
|
3186
|
+
telemetry_id,
|
|
3187
|
+
source,
|
|
3188
|
+
session_id,
|
|
3189
|
+
provider_thread_id,
|
|
3190
|
+
event_name,
|
|
3191
|
+
severity,
|
|
3192
|
+
summary,
|
|
3193
|
+
observed_at,
|
|
3194
|
+
ingested_at,
|
|
3195
|
+
payload_json,
|
|
3196
|
+
fingerprint
|
|
3197
|
+
FROM session_telemetry
|
|
3198
|
+
WHERE telemetry_id > ?
|
|
3199
|
+
ORDER BY telemetry_id ASC
|
|
3200
|
+
LIMIT ?
|
|
3201
|
+
`,
|
|
3202
|
+
)
|
|
3203
|
+
.run(afterTelemetryId, safeLimit);
|
|
3204
|
+
return sqliteStatementChanges(result);
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
private readTelemetryCompactionShadowCursorRowId(): number {
|
|
3208
|
+
const row = this.db
|
|
3209
|
+
.prepare(
|
|
3210
|
+
`
|
|
3211
|
+
SELECT telemetry_id
|
|
3212
|
+
FROM ${TELEMETRY_COMPACTION_SHADOW_TABLE}
|
|
3213
|
+
ORDER BY telemetry_id DESC
|
|
3214
|
+
LIMIT 1
|
|
3215
|
+
`,
|
|
3216
|
+
)
|
|
3217
|
+
.get();
|
|
3218
|
+
if (row === undefined) {
|
|
3219
|
+
return 0;
|
|
3220
|
+
}
|
|
3221
|
+
return asNumberOrNull(asRecord(row).telemetry_id, 'telemetry_id') ?? 0;
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
private resetTelemetryCompactionShadowTable(): void {
|
|
3225
|
+
this.db.exec(`DROP TABLE IF EXISTS ${TELEMETRY_COMPACTION_SHADOW_TABLE};`);
|
|
3226
|
+
this.db.exec(`
|
|
3227
|
+
CREATE TABLE ${TELEMETRY_COMPACTION_SHADOW_TABLE} (
|
|
3228
|
+
telemetry_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3229
|
+
source TEXT NOT NULL,
|
|
3230
|
+
session_id TEXT,
|
|
3231
|
+
provider_thread_id TEXT,
|
|
3232
|
+
event_name TEXT,
|
|
3233
|
+
severity TEXT,
|
|
3234
|
+
summary TEXT,
|
|
3235
|
+
observed_at TEXT NOT NULL,
|
|
3236
|
+
ingested_at TEXT NOT NULL,
|
|
3237
|
+
payload_json TEXT NOT NULL,
|
|
3238
|
+
fingerprint TEXT NOT NULL UNIQUE
|
|
3239
|
+
);
|
|
3240
|
+
`);
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
private swapInTelemetryCompactionShadowTable(): void {
|
|
3244
|
+
this.db.exec('DROP INDEX IF EXISTS idx_session_telemetry_session;');
|
|
3245
|
+
this.db.exec('DROP INDEX IF EXISTS idx_session_telemetry_thread;');
|
|
3246
|
+
this.db.exec(`ALTER TABLE session_telemetry RENAME TO ${TELEMETRY_COMPACTION_OLD_TABLE};`);
|
|
3247
|
+
this.db.exec(`ALTER TABLE ${TELEMETRY_COMPACTION_SHADOW_TABLE} RENAME TO session_telemetry;`);
|
|
3248
|
+
this.db.exec(`
|
|
3249
|
+
CREATE INDEX IF NOT EXISTS idx_session_telemetry_session
|
|
3250
|
+
ON session_telemetry (session_id, observed_at DESC, telemetry_id DESC);
|
|
3251
|
+
`);
|
|
3252
|
+
this.db.exec(`
|
|
3253
|
+
CREATE INDEX IF NOT EXISTS idx_session_telemetry_thread
|
|
3254
|
+
ON session_telemetry (provider_thread_id, observed_at DESC, telemetry_id DESC);
|
|
3255
|
+
`);
|
|
3256
|
+
this.db.exec(`DROP TABLE ${TELEMETRY_COMPACTION_OLD_TABLE};`);
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
private resetTelemetryCompactionStateAfterFailure(): void {
|
|
3260
|
+
this.telemetryCopyForwardActive = false;
|
|
3261
|
+
this.telemetryCopyForwardCursorRowId = 0;
|
|
3262
|
+
try {
|
|
3263
|
+
this.db.exec(`DROP TABLE IF EXISTS ${TELEMETRY_COMPACTION_SHADOW_TABLE};`);
|
|
3264
|
+
} catch {
|
|
3265
|
+
// Best-effort cleanup only.
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
private columnExists(table: string, column: string): boolean {
|
|
2969
3270
|
const rows = this.db.prepare(`PRAGMA table_info(${table})`).all();
|
|
2970
|
-
|
|
3271
|
+
return rows.some((row) => {
|
|
2971
3272
|
const asRow = row as Record<string, unknown>;
|
|
2972
3273
|
return asRow['name'] === column;
|
|
2973
3274
|
});
|
|
2974
|
-
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
private ensureColumnExists(table: string, column: string, definition: string): void {
|
|
3278
|
+
if (this.columnExists(table, column)) {
|
|
2975
3279
|
return;
|
|
2976
3280
|
}
|
|
2977
3281
|
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${definition};`);
|