@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
|
@@ -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 {
|
|
@@ -355,14 +364,36 @@ interface ListGitHubSyncStateQuery {
|
|
|
355
364
|
limit?: number;
|
|
356
365
|
}
|
|
357
366
|
|
|
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
|
+
}
|
|
375
|
+
|
|
358
376
|
export class SqliteControlPlaneStore {
|
|
359
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;
|
|
360
383
|
|
|
361
|
-
constructor(filePath = ':memory:') {
|
|
384
|
+
constructor(filePath = ':memory:', options?: { busyTimeoutMs?: number }) {
|
|
362
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;
|
|
363
393
|
this.db = new DatabaseSync(resolvedPath);
|
|
364
394
|
this.configureConnection();
|
|
365
395
|
this.initializeSchema();
|
|
396
|
+
this.ensureIncrementalAutoVacuumMode();
|
|
366
397
|
}
|
|
367
398
|
|
|
368
399
|
close(): void {
|
|
@@ -882,6 +913,144 @@ export class SqliteControlPlaneStore {
|
|
|
882
913
|
return sqliteStatementChanges(result) > 0;
|
|
883
914
|
}
|
|
884
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
|
+
|
|
885
1054
|
latestTelemetrySummary(sessionId: string): ControlPlaneTelemetrySummary | null {
|
|
886
1055
|
const row = this.db
|
|
887
1056
|
.prepare(
|
|
@@ -1264,9 +1433,8 @@ export class SqliteControlPlaneStore {
|
|
|
1264
1433
|
}
|
|
1265
1434
|
|
|
1266
1435
|
createTask(input: CreateTaskInput): ControlPlaneTaskRecord {
|
|
1267
|
-
const title =
|
|
1268
|
-
const
|
|
1269
|
-
const linear = applyTaskLinearInput(defaultTaskLinearRecord(), input.linear ?? {});
|
|
1436
|
+
const title = normalizeTaskTitle(input.title);
|
|
1437
|
+
const body = normalizeTaskBody(input.body ?? title, 'body');
|
|
1270
1438
|
this.db.exec('BEGIN IMMEDIATE TRANSACTION');
|
|
1271
1439
|
try {
|
|
1272
1440
|
const existing = this.getTask(input.taskId);
|
|
@@ -1275,6 +1443,9 @@ export class SqliteControlPlaneStore {
|
|
|
1275
1443
|
}
|
|
1276
1444
|
const repositoryId = input.repositoryId ?? null;
|
|
1277
1445
|
const projectId = input.projectId ?? null;
|
|
1446
|
+
if (repositoryId === null && projectId === null) {
|
|
1447
|
+
throw new Error('task scope required: repositoryId or projectId');
|
|
1448
|
+
}
|
|
1278
1449
|
if (repositoryId !== null) {
|
|
1279
1450
|
const repository = this.getActiveRepository(repositoryId);
|
|
1280
1451
|
this.assertScopeMatch(input, repository, 'task');
|
|
@@ -1298,8 +1469,7 @@ export class SqliteControlPlaneStore {
|
|
|
1298
1469
|
scope_kind,
|
|
1299
1470
|
project_id,
|
|
1300
1471
|
title,
|
|
1301
|
-
|
|
1302
|
-
linear_json,
|
|
1472
|
+
body,
|
|
1303
1473
|
status,
|
|
1304
1474
|
order_index,
|
|
1305
1475
|
claimed_by_controller_id,
|
|
@@ -1310,7 +1480,7 @@ export class SqliteControlPlaneStore {
|
|
|
1310
1480
|
completed_at,
|
|
1311
1481
|
created_at,
|
|
1312
1482
|
updated_at
|
|
1313
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
1483
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'draft', ?, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?)
|
|
1314
1484
|
`,
|
|
1315
1485
|
)
|
|
1316
1486
|
.run(
|
|
@@ -1322,8 +1492,7 @@ export class SqliteControlPlaneStore {
|
|
|
1322
1492
|
scopeKind,
|
|
1323
1493
|
projectId,
|
|
1324
1494
|
title,
|
|
1325
|
-
|
|
1326
|
-
serializeTaskLinear(linear),
|
|
1495
|
+
body,
|
|
1327
1496
|
orderIndex,
|
|
1328
1497
|
createdAt,
|
|
1329
1498
|
createdAt,
|
|
@@ -1353,8 +1522,7 @@ export class SqliteControlPlaneStore {
|
|
|
1353
1522
|
scope_kind,
|
|
1354
1523
|
project_id,
|
|
1355
1524
|
title,
|
|
1356
|
-
|
|
1357
|
-
linear_json,
|
|
1525
|
+
body,
|
|
1358
1526
|
status,
|
|
1359
1527
|
order_index,
|
|
1360
1528
|
claimed_by_controller_id,
|
|
@@ -1421,8 +1589,7 @@ export class SqliteControlPlaneStore {
|
|
|
1421
1589
|
scope_kind,
|
|
1422
1590
|
project_id,
|
|
1423
1591
|
title,
|
|
1424
|
-
|
|
1425
|
-
linear_json,
|
|
1592
|
+
body,
|
|
1426
1593
|
status,
|
|
1427
1594
|
order_index,
|
|
1428
1595
|
claimed_by_controller_id,
|
|
@@ -1448,19 +1615,14 @@ export class SqliteControlPlaneStore {
|
|
|
1448
1615
|
if (existing === null) {
|
|
1449
1616
|
return null;
|
|
1450
1617
|
}
|
|
1451
|
-
const title =
|
|
1452
|
-
|
|
1453
|
-
const description =
|
|
1454
|
-
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');
|
|
1455
1620
|
const repositoryId =
|
|
1456
1621
|
update.repositoryId === undefined ? existing.repositoryId : update.repositoryId;
|
|
1457
1622
|
const projectId = update.projectId === undefined ? existing.projectId : update.projectId;
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
: update.linear === null
|
|
1462
|
-
? defaultTaskLinearRecord()
|
|
1463
|
-
: applyTaskLinearInput(existing.linear, update.linear);
|
|
1623
|
+
if (repositoryId === null && projectId === null) {
|
|
1624
|
+
throw new Error('task scope required: repositoryId or projectId');
|
|
1625
|
+
}
|
|
1464
1626
|
if (repositoryId !== null) {
|
|
1465
1627
|
const repository = this.getActiveRepository(repositoryId);
|
|
1466
1628
|
this.assertScopeMatch(existing, repository, 'task');
|
|
@@ -1480,22 +1642,12 @@ export class SqliteControlPlaneStore {
|
|
|
1480
1642
|
scope_kind = ?,
|
|
1481
1643
|
project_id = ?,
|
|
1482
1644
|
title = ?,
|
|
1483
|
-
|
|
1484
|
-
linear_json = ?,
|
|
1645
|
+
body = ?,
|
|
1485
1646
|
updated_at = ?
|
|
1486
1647
|
WHERE task_id = ?
|
|
1487
1648
|
`,
|
|
1488
1649
|
)
|
|
1489
|
-
.run(
|
|
1490
|
-
repositoryId,
|
|
1491
|
-
scopeKind,
|
|
1492
|
-
projectId,
|
|
1493
|
-
title,
|
|
1494
|
-
description,
|
|
1495
|
-
serializeTaskLinear(linear),
|
|
1496
|
-
updatedAt,
|
|
1497
|
-
taskId,
|
|
1498
|
-
);
|
|
1650
|
+
.run(repositoryId, scopeKind, projectId, title, body, updatedAt, taskId);
|
|
1499
1651
|
return this.getTask(taskId);
|
|
1500
1652
|
}
|
|
1501
1653
|
|
|
@@ -2628,6 +2780,24 @@ export class SqliteControlPlaneStore {
|
|
|
2628
2780
|
}
|
|
2629
2781
|
|
|
2630
2782
|
private initializeSchema(): void {
|
|
2783
|
+
this.db.exec('BEGIN IMMEDIATE TRANSACTION');
|
|
2784
|
+
try {
|
|
2785
|
+
const currentVersion = this.readSchemaVersion();
|
|
2786
|
+
if (currentVersion > CONTROL_PLANE_SCHEMA_VERSION) {
|
|
2787
|
+
throw new Error(
|
|
2788
|
+
`control-plane schema version ${String(currentVersion)} is newer than supported version ${String(CONTROL_PLANE_SCHEMA_VERSION)}`,
|
|
2789
|
+
);
|
|
2790
|
+
}
|
|
2791
|
+
this.applySchemaV1();
|
|
2792
|
+
this.writeSchemaVersion(CONTROL_PLANE_SCHEMA_VERSION);
|
|
2793
|
+
this.db.exec('COMMIT');
|
|
2794
|
+
} catch (error) {
|
|
2795
|
+
this.db.exec('ROLLBACK');
|
|
2796
|
+
throw error;
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
private applySchemaV1(): void {
|
|
2631
2801
|
this.db.exec(`
|
|
2632
2802
|
CREATE TABLE IF NOT EXISTS directories (
|
|
2633
2803
|
directory_id TEXT PRIMARY KEY,
|
|
@@ -2736,8 +2906,7 @@ export class SqliteControlPlaneStore {
|
|
|
2736
2906
|
scope_kind TEXT NOT NULL DEFAULT 'global',
|
|
2737
2907
|
project_id TEXT REFERENCES directories(directory_id),
|
|
2738
2908
|
title TEXT NOT NULL,
|
|
2739
|
-
|
|
2740
|
-
linear_json TEXT NOT NULL DEFAULT '{}',
|
|
2909
|
+
body TEXT NOT NULL DEFAULT '',
|
|
2741
2910
|
status TEXT NOT NULL,
|
|
2742
2911
|
order_index INTEGER NOT NULL,
|
|
2743
2912
|
claimed_by_controller_id TEXT,
|
|
@@ -2754,13 +2923,20 @@ export class SqliteControlPlaneStore {
|
|
|
2754
2923
|
CREATE INDEX IF NOT EXISTS idx_tasks_scope
|
|
2755
2924
|
ON tasks (tenant_id, user_id, workspace_id, order_index, created_at, task_id);
|
|
2756
2925
|
`);
|
|
2757
|
-
this.ensureColumnExists('tasks', 'linear_json', `linear_json TEXT NOT NULL DEFAULT '{}'`);
|
|
2758
2926
|
this.ensureColumnExists('tasks', 'scope_kind', `scope_kind TEXT NOT NULL DEFAULT 'global'`);
|
|
2759
2927
|
this.ensureColumnExists(
|
|
2760
2928
|
'tasks',
|
|
2761
2929
|
'project_id',
|
|
2762
2930
|
`project_id TEXT REFERENCES directories(directory_id)`,
|
|
2763
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
|
+
}
|
|
2764
2940
|
this.db.exec(`
|
|
2765
2941
|
CREATE INDEX IF NOT EXISTS idx_tasks_scope_kind
|
|
2766
2942
|
ON tasks (tenant_id, user_id, workspace_id, scope_kind, repository_id, project_id, order_index);
|
|
@@ -2783,6 +2959,10 @@ export class SqliteControlPlaneStore {
|
|
|
2783
2959
|
SET scope_kind = 'repository'
|
|
2784
2960
|
WHERE scope_kind = 'global' AND repository_id IS NOT NULL AND project_id IS NULL;
|
|
2785
2961
|
`);
|
|
2962
|
+
this.db.exec(`
|
|
2963
|
+
DELETE FROM tasks
|
|
2964
|
+
WHERE repository_id IS NULL AND project_id IS NULL;
|
|
2965
|
+
`);
|
|
2786
2966
|
|
|
2787
2967
|
this.db.exec(`
|
|
2788
2968
|
CREATE TABLE IF NOT EXISTS project_settings (
|
|
@@ -2923,19 +3103,179 @@ export class SqliteControlPlaneStore {
|
|
|
2923
3103
|
`);
|
|
2924
3104
|
}
|
|
2925
3105
|
|
|
3106
|
+
private readSchemaVersion(): number {
|
|
3107
|
+
const row = this.db.prepare('PRAGMA user_version;').get();
|
|
3108
|
+
if (row === undefined) {
|
|
3109
|
+
throw new Error('failed to read control-plane schema version');
|
|
3110
|
+
}
|
|
3111
|
+
const version = (row as Record<string, unknown>)['user_version'];
|
|
3112
|
+
if (typeof version !== 'number' || !Number.isInteger(version) || version < 0) {
|
|
3113
|
+
throw new Error(`invalid control-plane schema version value: ${String(version)}`);
|
|
3114
|
+
}
|
|
3115
|
+
return version;
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
private writeSchemaVersion(version: number): void {
|
|
3119
|
+
this.db.exec(`PRAGMA user_version = ${String(version)};`);
|
|
3120
|
+
}
|
|
3121
|
+
|
|
2926
3122
|
private configureConnection(): void {
|
|
3123
|
+
this.db.exec('PRAGMA auto_vacuum = INCREMENTAL;');
|
|
2927
3124
|
this.db.exec('PRAGMA journal_mode = WAL;');
|
|
2928
3125
|
this.db.exec('PRAGMA synchronous = NORMAL;');
|
|
2929
|
-
this.db.exec(
|
|
3126
|
+
this.db.exec(`PRAGMA busy_timeout = ${String(this.busyTimeoutMs)};`);
|
|
2930
3127
|
}
|
|
2931
3128
|
|
|
2932
|
-
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 {
|
|
2933
3270
|
const rows = this.db.prepare(`PRAGMA table_info(${table})`).all();
|
|
2934
|
-
|
|
3271
|
+
return rows.some((row) => {
|
|
2935
3272
|
const asRow = row as Record<string, unknown>;
|
|
2936
3273
|
return asRow['name'] === column;
|
|
2937
3274
|
});
|
|
2938
|
-
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
private ensureColumnExists(table: string, column: string, definition: string): void {
|
|
3278
|
+
if (this.columnExists(table, column)) {
|
|
2939
3279
|
return;
|
|
2940
3280
|
}
|
|
2941
3281
|
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${definition};`);
|