@jmoyers/harness 0.1.10 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/README.md +31 -35
  2. package/package.json +31 -11
  3. package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
  4. package/packages/harness-ai/src/stream-text.ts +13 -91
  5. package/packages/harness-ui/src/frame-primitives.ts +158 -0
  6. package/packages/harness-ui/src/index.ts +18 -0
  7. package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
  8. package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
  9. package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
  10. package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
  11. package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
  12. package/packages/harness-ui/src/interaction/input.ts +420 -0
  13. package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
  14. package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
  15. package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
  16. package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
  17. package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
  18. package/packages/harness-ui/src/kit.ts +476 -0
  19. package/packages/harness-ui/src/layout.ts +238 -0
  20. package/{src/ui/modals/manager.ts → packages/harness-ui/src/modal-manager.ts} +94 -64
  21. package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
  22. package/packages/harness-ui/src/surface.ts +252 -0
  23. package/packages/harness-ui/src/text-layout.ts +210 -0
  24. package/packages/nim-core/src/contracts.ts +239 -0
  25. package/packages/nim-core/src/event-store.ts +299 -0
  26. package/packages/nim-core/src/events.ts +53 -0
  27. package/packages/nim-core/src/index.ts +9 -0
  28. package/packages/nim-core/src/provider-router.ts +129 -0
  29. package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
  30. package/packages/nim-core/src/runtime-factory.ts +49 -0
  31. package/packages/nim-core/src/runtime.ts +1797 -0
  32. package/packages/nim-core/src/session-store.ts +516 -0
  33. package/packages/nim-core/src/telemetry.ts +48 -0
  34. package/packages/nim-test-tui/src/index.ts +150 -0
  35. package/packages/nim-ui-core/src/index.ts +1 -0
  36. package/packages/nim-ui-core/src/projection.ts +87 -0
  37. package/scripts/codex-live-mux-runtime.ts +2 -3721
  38. package/scripts/control-plane-daemon.ts +24 -2
  39. package/scripts/harness-bin.js +5 -0
  40. package/scripts/harness-commands.ts +300 -0
  41. package/scripts/harness-runtime.ts +82 -0
  42. package/scripts/harness.ts +33 -3007
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/default-gateway-pointer.ts +193 -0
  46. package/src/cli/gateway/runtime.ts +1872 -0
  47. package/src/cli/parsing/flags.ts +23 -0
  48. package/src/cli/parsing/session.ts +42 -0
  49. package/src/cli/runtime/context.ts +193 -0
  50. package/src/cli/runtime-app/application.ts +392 -0
  51. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  52. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  53. package/src/cli/workflows/runtime.ts +965 -0
  54. package/src/clients/tui/left-rail-interactions.ts +519 -0
  55. package/src/clients/tui/main-pane-interactions.ts +509 -0
  56. package/src/clients/tui/modal-input-routing.ts +71 -0
  57. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  58. package/src/clients/web/synced-selectors.ts +132 -0
  59. package/src/codex/live-session.ts +82 -29
  60. package/src/config/config-core.ts +361 -10
  61. package/src/config/harness-paths.ts +4 -7
  62. package/src/config/harness-runtime-migration.ts +142 -19
  63. package/src/config/harness.config.template.jsonc +33 -0
  64. package/src/config/secrets-core.ts +92 -4
  65. package/src/control-plane/agent-realtime-api.ts +82 -427
  66. package/src/control-plane/prompt/thread-title-namer.ts +49 -23
  67. package/src/control-plane/session-summary.ts +10 -81
  68. package/src/control-plane/status/reducer-base.ts +12 -12
  69. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  70. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  71. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  72. package/src/control-plane/stream-client.ts +12 -2
  73. package/src/control-plane/stream-command-parser.ts +83 -143
  74. package/src/control-plane/stream-protocol.ts +53 -37
  75. package/src/control-plane/stream-server-background.ts +18 -2
  76. package/src/control-plane/stream-server-command.ts +376 -69
  77. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  78. package/src/control-plane/stream-server.ts +943 -80
  79. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  80. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  81. package/src/core/state/observed-stream-cursor.ts +43 -0
  82. package/src/core/state/synced-observed-state.ts +273 -0
  83. package/src/core/store/harness-synced-store.ts +81 -0
  84. package/src/diff/budget.ts +136 -0
  85. package/src/diff/build.ts +289 -0
  86. package/src/diff/chunker.ts +146 -0
  87. package/src/diff/git-invoke.ts +315 -0
  88. package/src/diff/git-parse.ts +472 -0
  89. package/src/diff/hash.ts +70 -0
  90. package/src/diff/index.ts +24 -0
  91. package/src/diff/normalize.ts +134 -0
  92. package/src/diff/types.ts +178 -0
  93. package/src/diff-ui/args.ts +346 -0
  94. package/src/diff-ui/commands.ts +123 -0
  95. package/src/diff-ui/finder.ts +94 -0
  96. package/src/diff-ui/highlight.ts +127 -0
  97. package/src/diff-ui/index.ts +2 -0
  98. package/src/diff-ui/model.ts +141 -0
  99. package/src/diff-ui/pager.ts +412 -0
  100. package/src/diff-ui/render.ts +337 -0
  101. package/src/diff-ui/runtime.ts +379 -0
  102. package/src/diff-ui/state.ts +224 -0
  103. package/src/diff-ui/types.ts +236 -0
  104. package/src/domain/conversations.ts +11 -7
  105. package/src/domain/workspace.ts +76 -4
  106. package/src/mux/control-plane-op-queue.ts +93 -7
  107. package/src/mux/conversation-rail.ts +28 -71
  108. package/src/mux/dual-pane-core.ts +13 -13
  109. package/src/mux/harness-core-ui.ts +313 -42
  110. package/src/mux/input-shortcuts.ts +22 -112
  111. package/src/mux/keybinding-catalog.ts +340 -0
  112. package/src/mux/keybinding-registry.ts +103 -0
  113. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  114. package/src/mux/live-mux/command-menu.ts +167 -4
  115. package/src/mux/live-mux/conversation-state.ts +13 -0
  116. package/src/mux/live-mux/directory-resolution.ts +1 -1
  117. package/src/mux/live-mux/git-parsing.ts +16 -0
  118. package/src/mux/live-mux/git-snapshot.ts +33 -2
  119. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  120. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  121. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  122. package/src/mux/live-mux/input-forwarding.ts +59 -2
  123. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  124. package/src/mux/live-mux/left-nav.ts +35 -0
  125. package/src/mux/live-mux/link-click.ts +292 -0
  126. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  127. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  128. package/src/mux/live-mux/modal-input-reducers.ts +106 -8
  129. package/src/mux/live-mux/modal-overlays.ts +210 -31
  130. package/src/mux/live-mux/modal-pointer.ts +3 -7
  131. package/src/mux/live-mux/modal-prompt-handlers.ts +107 -1
  132. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  133. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  134. package/src/mux/live-mux/pointer-routing.ts +5 -2
  135. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  136. package/src/mux/live-mux/rail-layout.ts +33 -30
  137. package/src/mux/live-mux/release-notes.ts +383 -0
  138. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  139. package/src/mux/live-mux/repository-folding.ts +3 -0
  140. package/src/mux/live-mux/selection.ts +0 -4
  141. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  142. package/src/mux/project-pane-github-review.ts +271 -0
  143. package/src/mux/render-frame.ts +4 -0
  144. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  145. package/src/mux/task-composer.ts +21 -14
  146. package/src/mux/task-focused-pane.ts +118 -117
  147. package/src/mux/task-screen-keybindings.ts +19 -82
  148. package/src/mux/workspace-rail-model.ts +270 -104
  149. package/src/mux/workspace-rail.ts +45 -22
  150. package/src/pty/session-broker.ts +1 -1
  151. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  152. package/src/services/control-plane.ts +50 -32
  153. package/src/services/conversation-lifecycle.ts +118 -87
  154. package/src/services/conversation-startup-hydration.ts +20 -12
  155. package/src/services/directory-hydration.ts +21 -16
  156. package/src/services/event-persistence.ts +7 -0
  157. package/src/services/left-rail-pointer-handler.ts +329 -0
  158. package/src/services/mux-ui-state-persistence.ts +5 -1
  159. package/src/services/recording.ts +34 -26
  160. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  161. package/src/services/runtime-control-actions.ts +79 -61
  162. package/src/services/runtime-control-plane-ops.ts +122 -83
  163. package/src/services/runtime-conversation-actions.ts +40 -26
  164. package/src/services/runtime-conversation-activation.ts +82 -30
  165. package/src/services/runtime-conversation-starter.ts +80 -48
  166. package/src/services/runtime-conversation-title-edit.ts +91 -80
  167. package/src/services/runtime-envelope-handler.ts +107 -105
  168. package/src/services/runtime-git-state.ts +42 -29
  169. package/src/services/runtime-layout-resize.ts +3 -1
  170. package/src/services/runtime-left-rail-render.ts +99 -63
  171. package/src/services/runtime-nim-cli-session.ts +438 -0
  172. package/src/services/runtime-nim-session.ts +705 -0
  173. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  174. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  175. package/src/services/runtime-process-wiring.ts +29 -36
  176. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  177. package/src/services/runtime-render-flush.ts +63 -70
  178. package/src/services/runtime-render-lifecycle.ts +65 -64
  179. package/src/services/runtime-render-orchestrator.ts +55 -45
  180. package/src/services/runtime-render-pipeline.ts +106 -103
  181. package/src/services/runtime-render-state.ts +62 -49
  182. package/src/services/runtime-repository-actions.ts +97 -70
  183. package/src/services/runtime-right-pane-render.ts +80 -53
  184. package/src/services/runtime-shutdown.ts +38 -35
  185. package/src/services/runtime-stream-subscriptions.ts +35 -27
  186. package/src/services/runtime-task-composer-persistence.ts +71 -59
  187. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  188. package/src/services/runtime-task-editor-actions.ts +46 -29
  189. package/src/services/runtime-task-pane-actions.ts +220 -134
  190. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  191. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  192. package/src/services/runtime-workspace-observed-events.ts +33 -184
  193. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  194. package/src/services/session-diagnostics-store.ts +217 -0
  195. package/src/services/startup-background-resume.ts +26 -21
  196. package/src/services/startup-orchestrator.ts +16 -13
  197. package/src/services/startup-paint-tracker.ts +29 -21
  198. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  199. package/src/services/startup-settled-gate.ts +25 -15
  200. package/src/services/startup-shutdown.ts +18 -22
  201. package/src/services/startup-state-hydration.ts +44 -34
  202. package/src/services/startup-visibility.ts +12 -4
  203. package/src/services/task-pane-selection-actions.ts +89 -72
  204. package/src/services/task-planning-hydration.ts +24 -18
  205. package/src/services/task-planning-observed-events.ts +50 -52
  206. package/src/services/workspace-observed-events.ts +66 -63
  207. package/src/storage/storage-lifecycle-core.ts +438 -0
  208. package/src/store/control-plane-store-normalize.ts +33 -242
  209. package/src/store/control-plane-store-types.ts +1 -35
  210. package/src/store/control-plane-store.ts +396 -56
  211. package/src/store/event-store.ts +397 -3
  212. package/src/terminal/snapshot-oracle.ts +207 -94
  213. package/src/ui/mux-theme.ts +112 -8
  214. package/src/ui/panes/home-gridfire.ts +40 -31
  215. package/src/ui/panes/home.ts +10 -2
  216. package/src/ui/panes/nim.ts +315 -0
  217. package/src/mux/live-mux/actions-task.ts +0 -115
  218. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  219. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -82
  220. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  221. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  222. package/src/services/runtime-directory-actions.ts +0 -164
  223. package/src/services/runtime-input-pipeline.ts +0 -50
  224. package/src/services/runtime-input-router.ts +0 -189
  225. package/src/services/runtime-main-pane-input.ts +0 -230
  226. package/src/services/runtime-modal-input.ts +0 -119
  227. package/src/services/runtime-navigation-input.ts +0 -197
  228. package/src/services/runtime-rail-input.ts +0 -278
  229. package/src/services/runtime-task-pane.ts +0 -62
  230. package/src/services/runtime-workspace-actions.ts +0 -158
  231. package/src/ui/conversation-input-forwarder.ts +0 -114
  232. package/src/ui/conversation-selection-input.ts +0 -103
  233. package/src/ui/global-shortcut-input.ts +0 -89
  234. package/src/ui/input.ts +0 -238
  235. package/src/ui/kit.ts +0 -509
  236. package/src/ui/left-nav-input.ts +0 -80
  237. package/src/ui/left-rail-pointer-input.ts +0 -148
  238. package/src/ui/repository-fold-input.ts +0 -91
  239. package/src/ui/surface.ts +0 -224
@@ -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
- phaseHint: null,
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
- phaseHint: null,
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: string;
208
- description?: string;
209
- linear?: TaskLinearInput;
218
+ title?: string | null;
219
+ body?: string;
210
220
  }
211
221
 
212
222
  interface UpdateTaskInput {
213
- title?: string;
214
- description?: string;
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 = normalizeNonEmptyLabel(input.title, 'title');
1268
- const description = input.description ?? '';
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
- description,
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'draft', ?, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?)
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
- description,
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
- description,
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
- description,
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
- update.title === undefined ? existing.title : normalizeNonEmptyLabel(update.title, 'title');
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
- const linear =
1459
- update.linear === undefined
1460
- ? existing.linear
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
- description = ?,
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
- description TEXT NOT NULL DEFAULT '',
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('PRAGMA busy_timeout = 2000;');
3126
+ this.db.exec(`PRAGMA busy_timeout = ${String(this.busyTimeoutMs)};`);
2930
3127
  }
2931
3128
 
2932
- private ensureColumnExists(table: string, column: string, definition: string): void {
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
- const exists = rows.some((row) => {
3271
+ return rows.some((row) => {
2935
3272
  const asRow = row as Record<string, unknown>;
2936
3273
  return asRow['name'] === column;
2937
3274
  });
2938
- if (exists) {
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};`);