@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
@@ -0,0 +1,41 @@
1
+ import type { CodexLiveEvent } from '../codex/live-session.ts';
2
+ import type { PtyExit } from '../pty/pty_host.ts';
3
+ import type { TerminalBufferTail, TerminalSnapshotFrame } from '../terminal/snapshot-oracle.ts';
4
+
5
+ export interface SessionDataEvent {
6
+ cursor: number;
7
+ chunk: Buffer;
8
+ }
9
+
10
+ export interface SessionAttachHandlers {
11
+ onData: (event: SessionDataEvent) => void;
12
+ onExit: (exit: PtyExit) => void;
13
+ }
14
+
15
+ export interface LiveSessionLike {
16
+ attach(handlers: SessionAttachHandlers, sinceCursor?: number): string;
17
+ detach(attachmentId: string): void;
18
+ latestCursorValue(): number;
19
+ processId(): number | null;
20
+ write(data: string | Uint8Array): void;
21
+ resize(cols: number, rows: number): void;
22
+ snapshot(): TerminalSnapshotFrame;
23
+ bufferTail?(tailLines?: number): TerminalBufferTail;
24
+ close(): void;
25
+ onEvent(listener: (event: CodexLiveEvent) => void): () => void;
26
+ }
27
+
28
+ export interface StartSessionRuntimeInput {
29
+ readonly sessionId: string;
30
+ readonly args: readonly string[];
31
+ readonly initialCols: number;
32
+ readonly initialRows: number;
33
+ readonly env?: Record<string, string>;
34
+ readonly cwd?: string;
35
+ readonly tenantId?: string;
36
+ readonly userId?: string;
37
+ readonly workspaceId?: string;
38
+ readonly worktreeId?: string;
39
+ readonly terminalForegroundHex?: string;
40
+ readonly terminalBackgroundHex?: string;
41
+ }
@@ -1,8 +1,12 @@
1
- import type { StreamSessionController } from '../../control-plane/stream-protocol.ts';
2
- import type { StreamSessionStatusModel } from '../../control-plane/stream-protocol.ts';
3
- import type { ConversationRailSessionSummary } from '../conversation-rail.ts';
4
-
5
- interface ControlPlaneDirectoryRecord {
1
+ import {
2
+ isStreamSessionRuntimeStatus,
3
+ parseStreamSessionStatusModel,
4
+ type StreamSessionController,
5
+ type StreamSessionRuntimeStatus,
6
+ type StreamSessionStatusModel,
7
+ } from '../../control-plane/stream-protocol.ts';
8
+
9
+ export interface ControlPlaneDirectoryRecord {
6
10
  readonly directoryId: string;
7
11
  readonly tenantId: string;
8
12
  readonly userId: string;
@@ -12,7 +16,7 @@ interface ControlPlaneDirectoryRecord {
12
16
  readonly archivedAt: string | null;
13
17
  }
14
18
 
15
- interface ControlPlaneConversationRecord {
19
+ export interface ControlPlaneConversationRecord {
16
20
  readonly conversationId: string;
17
21
  readonly directoryId: string;
18
22
  readonly tenantId: string;
@@ -21,12 +25,12 @@ interface ControlPlaneConversationRecord {
21
25
  readonly title: string;
22
26
  readonly agentType: string;
23
27
  readonly adapterState: Record<string, unknown>;
24
- readonly runtimeStatus: ConversationRailSessionSummary['status'];
28
+ readonly runtimeStatus: StreamSessionRuntimeStatus;
25
29
  readonly runtimeStatusModel: StreamSessionStatusModel | null;
26
30
  readonly runtimeLive: boolean;
27
31
  }
28
32
 
29
- interface ControlPlaneRepositoryRecord {
33
+ export interface ControlPlaneRepositoryRecord {
30
34
  readonly repositoryId: string;
31
35
  readonly tenantId: string;
32
36
  readonly userId: string;
@@ -39,14 +43,14 @@ interface ControlPlaneRepositoryRecord {
39
43
  readonly archivedAt: string | null;
40
44
  }
41
45
 
42
- interface ControlPlaneGitSummaryRecord {
46
+ export interface ControlPlaneGitSummaryRecord {
43
47
  readonly branch: string;
44
48
  readonly changedFiles: number;
45
49
  readonly additions: number;
46
50
  readonly deletions: number;
47
51
  }
48
52
 
49
- interface ControlPlaneGitRepositorySnapshotRecord {
53
+ export interface ControlPlaneGitRepositorySnapshotRecord {
50
54
  readonly normalizedRemoteUrl: string | null;
51
55
  readonly commitCount: number | null;
52
56
  readonly lastCommitAt: string | null;
@@ -55,7 +59,7 @@ interface ControlPlaneGitRepositorySnapshotRecord {
55
59
  readonly defaultBranch: string | null;
56
60
  }
57
61
 
58
- interface ControlPlaneDirectoryGitStatusRecord {
62
+ export interface ControlPlaneDirectoryGitStatusRecord {
59
63
  readonly directoryId: string;
60
64
  readonly summary: ControlPlaneGitSummaryRecord;
61
65
  readonly repositorySnapshot: ControlPlaneGitRepositorySnapshotRecord;
@@ -64,10 +68,10 @@ interface ControlPlaneDirectoryGitStatusRecord {
64
68
  readonly observedAt: string;
65
69
  }
66
70
 
67
- type TaskStatus = 'draft' | 'ready' | 'in-progress' | 'completed';
68
- type TaskScopeKind = 'global' | 'repository' | 'project';
71
+ export type TaskStatus = 'draft' | 'ready' | 'in-progress' | 'completed';
72
+ export type TaskScopeKind = 'global' | 'repository' | 'project';
69
73
 
70
- interface ControlPlaneTaskRecord {
74
+ export interface ControlPlaneTaskRecord {
71
75
  readonly taskId: string;
72
76
  readonly tenantId: string;
73
77
  readonly userId: string;
@@ -76,7 +80,7 @@ interface ControlPlaneTaskRecord {
76
80
  readonly scopeKind: TaskScopeKind;
77
81
  readonly projectId: string | null;
78
82
  readonly title: string;
79
- readonly description: string;
83
+ readonly body: string;
80
84
  readonly status: TaskStatus;
81
85
  readonly orderIndex: number;
82
86
  readonly claimedByControllerId: string | null;
@@ -123,78 +127,6 @@ function asObjectRecord(value: unknown): Record<string, unknown> | null {
123
127
  return value as Record<string, unknown>;
124
128
  }
125
129
 
126
- function parseRuntimeStatusModel(value: unknown): StreamSessionStatusModel | null | undefined {
127
- if (value === null) {
128
- return null;
129
- }
130
- const model = asObjectRecord(value);
131
- if (model === null) {
132
- return undefined;
133
- }
134
- const runtimeStatus = model['runtimeStatus'];
135
- const phase = model['phase'];
136
- const glyph = model['glyph'];
137
- const badge = model['badge'];
138
- const detailText = asRequiredString(model['detailText']);
139
- const attentionReason = asNullableString(model['attentionReason']);
140
- const lastKnownWork = asNullableString(model['lastKnownWork']);
141
- const lastKnownWorkAt = asNullableString(model['lastKnownWorkAt']);
142
- const phaseHint = model['phaseHint'];
143
- const observedAt = asRequiredString(model['observedAt']);
144
- if (
145
- detailText === null ||
146
- attentionReason === undefined ||
147
- lastKnownWork === undefined ||
148
- lastKnownWorkAt === undefined ||
149
- observedAt === null
150
- ) {
151
- return undefined;
152
- }
153
- if (
154
- runtimeStatus !== 'running' &&
155
- runtimeStatus !== 'needs-input' &&
156
- runtimeStatus !== 'completed' &&
157
- runtimeStatus !== 'exited'
158
- ) {
159
- return undefined;
160
- }
161
- if (
162
- phase !== 'needs-action' &&
163
- phase !== 'starting' &&
164
- phase !== 'working' &&
165
- phase !== 'idle' &&
166
- phase !== 'exited'
167
- ) {
168
- return undefined;
169
- }
170
- if (glyph !== '▲' && glyph !== '◔' && glyph !== '◆' && glyph !== '○' && glyph !== '■') {
171
- return undefined;
172
- }
173
- if (badge !== 'NEED' && badge !== 'RUN ' && badge !== 'DONE' && badge !== 'EXIT') {
174
- return undefined;
175
- }
176
- if (
177
- phaseHint !== null &&
178
- phaseHint !== 'needs-action' &&
179
- phaseHint !== 'working' &&
180
- phaseHint !== 'idle'
181
- ) {
182
- return undefined;
183
- }
184
- return {
185
- runtimeStatus,
186
- phase,
187
- glyph,
188
- badge,
189
- detailText,
190
- attentionReason,
191
- lastKnownWork,
192
- lastKnownWorkAt,
193
- phaseHint,
194
- observedAt,
195
- };
196
- }
197
-
198
130
  export function parseDirectoryRecord(value: unknown): ControlPlaneDirectoryRecord | null {
199
131
  const record = asRecord(value);
200
132
  if (record === null) {
@@ -247,7 +179,7 @@ export function parseConversationRecord(value: unknown): ControlPlaneConversatio
247
179
  const agentType = asRequiredString(record['agentType']);
248
180
  const adapterState = asObjectRecord(record['adapterState']);
249
181
  const runtimeStatus = record['runtimeStatus'];
250
- const runtimeStatusModel = parseRuntimeStatusModel(record['runtimeStatusModel']);
182
+ const runtimeStatusModel = parseStreamSessionStatusModel(record['runtimeStatusModel']);
251
183
  const runtimeLive = record['runtimeLive'];
252
184
 
253
185
  if (
@@ -265,12 +197,7 @@ export function parseConversationRecord(value: unknown): ControlPlaneConversatio
265
197
  return null;
266
198
  }
267
199
 
268
- if (
269
- runtimeStatus !== 'running' &&
270
- runtimeStatus !== 'needs-input' &&
271
- runtimeStatus !== 'completed' &&
272
- runtimeStatus !== 'exited'
273
- ) {
200
+ if (!isStreamSessionRuntimeStatus(runtimeStatus)) {
274
201
  return null;
275
202
  }
276
203
 
@@ -457,7 +384,7 @@ export function parseTaskRecord(value: unknown): ControlPlaneTaskRecord | null {
457
384
  const repositoryId = asOptionalString(record['repositoryId']);
458
385
  const projectId = asOptionalString(record['projectId']);
459
386
  const title = asRequiredString(record['title']);
460
- const description = asRequiredString(record['description']);
387
+ const body = asRequiredString(record['body'] ?? record['description']);
461
388
  const status = parseTaskStatus(record['status']);
462
389
  const scopeKind = parseTaskScopeKind(record['scopeKind'], repositoryId, projectId);
463
390
  const orderIndex = record['orderIndex'];
@@ -479,7 +406,7 @@ export function parseTaskRecord(value: unknown): ControlPlaneTaskRecord | null {
479
406
  projectId === undefined ||
480
407
  scopeKind === null ||
481
408
  title === null ||
482
- description === null ||
409
+ body === null ||
483
410
  status === null ||
484
411
  typeof orderIndex !== 'number' ||
485
412
  claimedByControllerId === undefined ||
@@ -503,7 +430,7 @@ export function parseTaskRecord(value: unknown): ControlPlaneTaskRecord | null {
503
430
  scopeKind,
504
431
  projectId,
505
432
  title,
506
- description,
433
+ body,
507
434
  status,
508
435
  orderIndex,
509
436
  claimedByControllerId,
@@ -0,0 +1,43 @@
1
+ export interface ObservedStreamCursorState {
2
+ readonly lastCursorBySubscriptionId: ReadonlyMap<string, number>;
3
+ }
4
+
5
+ interface ObserveStreamCursorInput {
6
+ readonly subscriptionId: string;
7
+ readonly cursor: number;
8
+ }
9
+
10
+ interface ObserveStreamCursorResult {
11
+ readonly accepted: boolean;
12
+ readonly previousCursor: number | null;
13
+ readonly state: ObservedStreamCursorState;
14
+ }
15
+
16
+ export function createObservedStreamCursorState(): ObservedStreamCursorState {
17
+ return {
18
+ lastCursorBySubscriptionId: new Map<string, number>(),
19
+ };
20
+ }
21
+
22
+ export function observeStreamCursor(
23
+ state: ObservedStreamCursorState,
24
+ input: ObserveStreamCursorInput,
25
+ ): ObserveStreamCursorResult {
26
+ const previousCursor = state.lastCursorBySubscriptionId.get(input.subscriptionId) ?? null;
27
+ if (previousCursor !== null && input.cursor <= previousCursor) {
28
+ return {
29
+ accepted: false,
30
+ previousCursor,
31
+ state,
32
+ };
33
+ }
34
+ const next = new Map(state.lastCursorBySubscriptionId);
35
+ next.set(input.subscriptionId, input.cursor);
36
+ return {
37
+ accepted: true,
38
+ previousCursor,
39
+ state: {
40
+ lastCursorBySubscriptionId: next,
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,273 @@
1
+ import type { StreamObservedEvent } from '../../control-plane/stream-protocol.ts';
2
+ import {
3
+ parseConversationRecord,
4
+ parseDirectoryRecord,
5
+ parseRepositoryRecord,
6
+ parseTaskRecord,
7
+ type ControlPlaneConversationRecord,
8
+ type ControlPlaneDirectoryRecord,
9
+ type ControlPlaneRepositoryRecord,
10
+ type ControlPlaneTaskRecord,
11
+ } from '../contracts/records.ts';
12
+
13
+ export interface HarnessSyncedState {
14
+ readonly directoriesById: Readonly<Record<string, ControlPlaneDirectoryRecord>>;
15
+ readonly conversationsById: Readonly<Record<string, ControlPlaneConversationRecord>>;
16
+ readonly repositoriesById: Readonly<Record<string, ControlPlaneRepositoryRecord>>;
17
+ readonly tasksById: Readonly<Record<string, ControlPlaneTaskRecord>>;
18
+ }
19
+
20
+ export interface HarnessSyncedObservedReduction {
21
+ readonly state: HarnessSyncedState;
22
+ readonly changed: boolean;
23
+ readonly removedConversationIds: readonly string[];
24
+ readonly removedDirectoryIds: readonly string[];
25
+ readonly removedTaskIds: readonly string[];
26
+ readonly upsertedDirectoryIds: readonly string[];
27
+ readonly upsertedConversationIds: readonly string[];
28
+ readonly upsertedRepositoryIds: readonly string[];
29
+ readonly upsertedTaskIds: readonly string[];
30
+ }
31
+
32
+ const EMPTY_IDS: readonly string[] = [];
33
+
34
+ export function createHarnessSyncedState(): HarnessSyncedState {
35
+ return {
36
+ directoriesById: {},
37
+ conversationsById: {},
38
+ repositoriesById: {},
39
+ tasksById: {},
40
+ };
41
+ }
42
+
43
+ function unchanged(state: HarnessSyncedState): HarnessSyncedObservedReduction {
44
+ return {
45
+ state,
46
+ changed: false,
47
+ removedConversationIds: EMPTY_IDS,
48
+ removedDirectoryIds: EMPTY_IDS,
49
+ removedTaskIds: EMPTY_IDS,
50
+ upsertedDirectoryIds: EMPTY_IDS,
51
+ upsertedConversationIds: EMPTY_IDS,
52
+ upsertedRepositoryIds: EMPTY_IDS,
53
+ upsertedTaskIds: EMPTY_IDS,
54
+ };
55
+ }
56
+
57
+ export function applyObservedEventToSyncedState(
58
+ state: HarnessSyncedState,
59
+ event: StreamObservedEvent,
60
+ ): HarnessSyncedObservedReduction {
61
+ if (event.type === 'directory-upserted') {
62
+ const directory = parseDirectoryRecord(event.directory);
63
+ if (directory === null) {
64
+ return unchanged(state);
65
+ }
66
+ return {
67
+ ...unchanged(state),
68
+ state: {
69
+ ...state,
70
+ directoriesById: {
71
+ ...state.directoriesById,
72
+ [directory.directoryId]: directory,
73
+ },
74
+ },
75
+ changed: true,
76
+ upsertedDirectoryIds: [directory.directoryId],
77
+ };
78
+ }
79
+
80
+ if (event.type === 'directory-archived') {
81
+ const removedConversationIds: string[] = [];
82
+ const nextConversations = { ...state.conversationsById };
83
+ for (const [conversationId, conversation] of Object.entries(state.conversationsById)) {
84
+ if (conversation.directoryId !== event.directoryId) {
85
+ continue;
86
+ }
87
+ delete nextConversations[conversationId];
88
+ removedConversationIds.push(conversationId);
89
+ }
90
+ const directoryExisted = state.directoriesById[event.directoryId] !== undefined;
91
+ if (!directoryExisted && removedConversationIds.length === 0) {
92
+ return unchanged(state);
93
+ }
94
+ const nextDirectories = { ...state.directoriesById };
95
+ delete nextDirectories[event.directoryId];
96
+ return {
97
+ ...unchanged(state),
98
+ state: {
99
+ ...state,
100
+ directoriesById: nextDirectories,
101
+ conversationsById: nextConversations,
102
+ },
103
+ changed: true,
104
+ removedConversationIds,
105
+ removedDirectoryIds: directoryExisted ? [event.directoryId] : EMPTY_IDS,
106
+ };
107
+ }
108
+
109
+ if (event.type === 'conversation-created' || event.type === 'conversation-updated') {
110
+ const conversation = parseConversationRecord(event.conversation);
111
+ if (conversation === null) {
112
+ return unchanged(state);
113
+ }
114
+ return {
115
+ ...unchanged(state),
116
+ state: {
117
+ ...state,
118
+ conversationsById: {
119
+ ...state.conversationsById,
120
+ [conversation.conversationId]: conversation,
121
+ },
122
+ },
123
+ changed: true,
124
+ upsertedConversationIds: [conversation.conversationId],
125
+ };
126
+ }
127
+
128
+ if (event.type === 'conversation-archived' || event.type === 'conversation-deleted') {
129
+ if (state.conversationsById[event.conversationId] === undefined) {
130
+ return unchanged(state);
131
+ }
132
+ const nextConversations = { ...state.conversationsById };
133
+ delete nextConversations[event.conversationId];
134
+ return {
135
+ ...unchanged(state),
136
+ state: {
137
+ ...state,
138
+ conversationsById: nextConversations,
139
+ },
140
+ changed: true,
141
+ removedConversationIds: [event.conversationId],
142
+ };
143
+ }
144
+
145
+ if (event.type === 'repository-upserted' || event.type === 'repository-updated') {
146
+ const repository = parseRepositoryRecord(event.repository);
147
+ if (repository === null) {
148
+ return unchanged(state);
149
+ }
150
+ return {
151
+ ...unchanged(state),
152
+ state: {
153
+ ...state,
154
+ repositoriesById: {
155
+ ...state.repositoriesById,
156
+ [repository.repositoryId]: repository,
157
+ },
158
+ },
159
+ changed: true,
160
+ upsertedRepositoryIds: [repository.repositoryId],
161
+ };
162
+ }
163
+
164
+ if (event.type === 'repository-archived') {
165
+ const existing = state.repositoriesById[event.repositoryId];
166
+ if (existing === undefined) {
167
+ return unchanged(state);
168
+ }
169
+ return {
170
+ ...unchanged(state),
171
+ state: {
172
+ ...state,
173
+ repositoriesById: {
174
+ ...state.repositoriesById,
175
+ [event.repositoryId]: {
176
+ ...existing,
177
+ archivedAt: event.ts,
178
+ },
179
+ },
180
+ },
181
+ changed: true,
182
+ upsertedRepositoryIds: [event.repositoryId],
183
+ };
184
+ }
185
+
186
+ if (event.type === 'task-created' || event.type === 'task-updated') {
187
+ const task = parseTaskRecord(event.task);
188
+ if (task === null) {
189
+ return unchanged(state);
190
+ }
191
+ return {
192
+ ...unchanged(state),
193
+ state: {
194
+ ...state,
195
+ tasksById: {
196
+ ...state.tasksById,
197
+ [task.taskId]: task,
198
+ },
199
+ },
200
+ changed: true,
201
+ upsertedTaskIds: [task.taskId],
202
+ };
203
+ }
204
+
205
+ if (event.type === 'task-deleted') {
206
+ if (state.tasksById[event.taskId] === undefined) {
207
+ return unchanged(state);
208
+ }
209
+ const nextTasks = { ...state.tasksById };
210
+ delete nextTasks[event.taskId];
211
+ return {
212
+ ...unchanged(state),
213
+ state: {
214
+ ...state,
215
+ tasksById: nextTasks,
216
+ },
217
+ changed: true,
218
+ removedTaskIds: [event.taskId],
219
+ };
220
+ }
221
+
222
+ if (event.type === 'task-reordered') {
223
+ const nextTasks = { ...state.tasksById };
224
+ const upsertedTaskIds: string[] = [];
225
+ for (const value of event.tasks) {
226
+ const task = parseTaskRecord(value);
227
+ if (task === null) {
228
+ continue;
229
+ }
230
+ nextTasks[task.taskId] = task;
231
+ upsertedTaskIds.push(task.taskId);
232
+ }
233
+ if (upsertedTaskIds.length === 0) {
234
+ return unchanged(state);
235
+ }
236
+ return {
237
+ ...unchanged(state),
238
+ state: {
239
+ ...state,
240
+ tasksById: nextTasks,
241
+ },
242
+ changed: true,
243
+ upsertedTaskIds,
244
+ };
245
+ }
246
+
247
+ if (event.type === 'session-status') {
248
+ const conversationId = event.conversationId ?? event.sessionId;
249
+ const existing = state.conversationsById[conversationId];
250
+ if (existing === undefined) {
251
+ return unchanged(state);
252
+ }
253
+ return {
254
+ ...unchanged(state),
255
+ state: {
256
+ ...state,
257
+ conversationsById: {
258
+ ...state.conversationsById,
259
+ [conversationId]: {
260
+ ...existing,
261
+ runtimeStatus: event.status,
262
+ runtimeStatusModel: event.statusModel,
263
+ runtimeLive: event.live,
264
+ },
265
+ },
266
+ },
267
+ changed: true,
268
+ upsertedConversationIds: [conversationId],
269
+ };
270
+ }
271
+
272
+ return unchanged(state);
273
+ }
@@ -0,0 +1,81 @@
1
+ import { createStore, type StoreApi } from 'zustand/vanilla';
2
+ import type { StreamObservedEvent } from '../../control-plane/stream-protocol.ts';
3
+ import {
4
+ createObservedStreamCursorState,
5
+ observeStreamCursor,
6
+ type ObservedStreamCursorState,
7
+ } from '../state/observed-stream-cursor.ts';
8
+ import {
9
+ applyObservedEventToSyncedState,
10
+ createHarnessSyncedState,
11
+ type HarnessSyncedObservedReduction,
12
+ type HarnessSyncedState,
13
+ } from '../state/synced-observed-state.ts';
14
+
15
+ export interface HarnessSyncedStoreState {
16
+ readonly synced: HarnessSyncedState;
17
+ readonly observedStreamCursor: ObservedStreamCursorState;
18
+ }
19
+
20
+ export interface HarnessSyncedStoreApplyInput {
21
+ readonly subscriptionId: string;
22
+ readonly cursor: number;
23
+ readonly event: StreamObservedEvent;
24
+ }
25
+
26
+ export interface HarnessSyncedStoreApplyResult extends HarnessSyncedObservedReduction {
27
+ readonly cursorAccepted: boolean;
28
+ readonly previousCursor: number | null;
29
+ readonly previousState: HarnessSyncedState;
30
+ }
31
+
32
+ export type HarnessSyncedStore = StoreApi<HarnessSyncedStoreState>;
33
+
34
+ export function createHarnessSyncedStore(
35
+ initial: Partial<HarnessSyncedStoreState> = {},
36
+ ): HarnessSyncedStore {
37
+ return createStore<HarnessSyncedStoreState>(() => ({
38
+ synced: initial.synced ?? createHarnessSyncedState(),
39
+ observedStreamCursor: initial.observedStreamCursor ?? createObservedStreamCursorState(),
40
+ }));
41
+ }
42
+
43
+ export function applyObservedEventToHarnessSyncedStore(
44
+ store: HarnessSyncedStore,
45
+ input: HarnessSyncedStoreApplyInput,
46
+ ): HarnessSyncedStoreApplyResult {
47
+ const state = store.getState();
48
+ const observedCursor = observeStreamCursor(state.observedStreamCursor, {
49
+ subscriptionId: input.subscriptionId,
50
+ cursor: input.cursor,
51
+ });
52
+
53
+ if (!observedCursor.accepted) {
54
+ return {
55
+ state: state.synced,
56
+ changed: false,
57
+ cursorAccepted: false,
58
+ previousCursor: observedCursor.previousCursor,
59
+ previousState: state.synced,
60
+ removedConversationIds: [],
61
+ removedDirectoryIds: [],
62
+ removedTaskIds: [],
63
+ upsertedDirectoryIds: [],
64
+ upsertedConversationIds: [],
65
+ upsertedRepositoryIds: [],
66
+ upsertedTaskIds: [],
67
+ };
68
+ }
69
+
70
+ const reduced = applyObservedEventToSyncedState(state.synced, input.event);
71
+ store.setState({
72
+ observedStreamCursor: observedCursor.state,
73
+ synced: reduced.state,
74
+ });
75
+ return {
76
+ ...reduced,
77
+ cursorAccepted: true,
78
+ previousCursor: observedCursor.previousCursor,
79
+ previousState: state.synced,
80
+ };
81
+ }