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