@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
@@ -8,13 +8,18 @@ import {
8
8
  parseSessionSummaryRecord,
9
9
  } from '../control-plane/session-summary.ts';
10
10
  import {
11
+ type ControlPlaneConversationRecord,
12
+ type ControlPlaneDirectoryGitStatusRecord,
13
+ type ControlPlaneDirectoryRecord,
14
+ type ControlPlaneRepositoryRecord,
15
+ type ControlPlaneTaskRecord,
11
16
  parseConversationRecord,
12
17
  parseDirectoryGitStatusRecord,
13
18
  parseDirectoryRecord,
14
19
  parseRepositoryRecord,
15
20
  parseSessionControllerRecord,
16
21
  parseTaskRecord,
17
- } from '../mux/live-mux/control-plane-records.ts';
22
+ } from '../core/contracts/records.ts';
18
23
 
19
24
  interface ControlPlaneScope {
20
25
  readonly tenantId: string;
@@ -26,13 +31,6 @@ interface ControlPlaneCommandClient {
26
31
  sendCommand(command: StreamCommand): Promise<Record<string, unknown>>;
27
32
  }
28
33
 
29
- type ControlPlaneRepositoryRecord = NonNullable<ReturnType<typeof parseRepositoryRecord>>;
30
- type ControlPlaneTaskRecord = NonNullable<ReturnType<typeof parseTaskRecord>>;
31
- type ControlPlaneDirectoryRecord = NonNullable<ReturnType<typeof parseDirectoryRecord>>;
32
- type ControlPlaneConversationRecord = NonNullable<ReturnType<typeof parseConversationRecord>>;
33
- type ControlPlaneDirectoryGitStatusRecord = NonNullable<
34
- ReturnType<typeof parseDirectoryGitStatusRecord>
35
- >;
36
34
  type ControlPlaneSessionControllerRecord = NonNullable<
37
35
  ReturnType<typeof parseSessionControllerRecord>
38
36
  >;
@@ -267,19 +265,19 @@ export class ControlPlaneService {
267
265
  };
268
266
  }
269
267
 
270
- async archiveConversation(conversationId: string): Promise<void> {
268
+ archiveConversation = async (conversationId: string): Promise<void> => {
271
269
  await this.client.sendCommand({
272
270
  type: 'conversation.archive',
273
271
  conversationId,
274
272
  });
275
- }
273
+ };
276
274
 
277
- async archiveDirectory(directoryId: string): Promise<void> {
275
+ archiveDirectory = async (directoryId: string): Promise<void> => {
278
276
  await this.client.sendCommand({
279
277
  type: 'directory.archive',
280
278
  directoryId,
281
279
  });
282
- }
280
+ };
283
281
 
284
282
  async attachPty(input: { sessionId: string; sinceCursor: number }): Promise<void> {
285
283
  await this.client.sendCommand({
@@ -349,12 +347,12 @@ export class ControlPlaneService {
349
347
  await this.client.sendCommand(command);
350
348
  }
351
349
 
352
- async closePtySession(sessionId: string): Promise<void> {
350
+ closePtySession = async (sessionId: string): Promise<void> => {
353
351
  await this.client.sendCommand({
354
352
  type: 'pty.close',
355
353
  sessionId,
356
354
  });
357
- }
355
+ };
358
356
 
359
357
  async getSessionStatus(sessionId: string): Promise<ControlPlaneSessionSummary | null> {
360
358
  const result = await this.client.sendCommand({
@@ -384,12 +382,12 @@ export class ControlPlaneService {
384
382
  return parseSessionSummaryList(result['sessions']);
385
383
  }
386
384
 
387
- async removeSession(sessionId: string): Promise<void> {
385
+ removeSession = async (sessionId: string): Promise<void> => {
388
386
  await this.client.sendCommand({
389
387
  type: 'session.remove',
390
388
  sessionId,
391
389
  });
392
- }
390
+ };
393
391
 
394
392
  async claimSession(input: {
395
393
  sessionId: string;
@@ -461,19 +459,28 @@ export class ControlPlaneService {
461
459
  }
462
460
 
463
461
  async createTask(input: {
464
- repositoryId: string;
465
- title: string;
466
- description: string;
462
+ repositoryId?: string;
463
+ projectId?: string;
464
+ title?: string | null;
465
+ body: string;
467
466
  }): Promise<ControlPlaneTaskRecord> {
468
- const result = await this.client.sendCommand({
467
+ const command: StreamCommand = {
469
468
  type: 'task.create',
470
469
  tenantId: this.scope.tenantId,
471
470
  userId: this.scope.userId,
472
471
  workspaceId: this.scope.workspaceId,
473
- repositoryId: input.repositoryId,
474
- title: input.title,
475
- description: input.description,
476
- });
472
+ body: input.body,
473
+ };
474
+ if (input.repositoryId !== undefined) {
475
+ command.repositoryId = input.repositoryId;
476
+ }
477
+ if (input.projectId !== undefined) {
478
+ command.projectId = input.projectId;
479
+ }
480
+ if (input.title !== undefined) {
481
+ command.title = input.title;
482
+ }
483
+ const result = await this.client.sendCommand(command);
477
484
  return this.parseTaskFromResult(
478
485
  result,
479
486
  'control-plane task.create returned malformed task record',
@@ -482,17 +489,28 @@ export class ControlPlaneService {
482
489
 
483
490
  async updateTask(input: {
484
491
  taskId: string;
485
- repositoryId: string | null;
486
- title: string;
487
- description: string;
492
+ repositoryId?: string | null;
493
+ projectId?: string | null;
494
+ title?: string | null;
495
+ body?: string;
488
496
  }): Promise<ControlPlaneTaskRecord> {
489
- const result = await this.client.sendCommand({
497
+ const command: StreamCommand = {
490
498
  type: 'task.update',
491
499
  taskId: input.taskId,
492
- repositoryId: input.repositoryId,
493
- title: input.title,
494
- description: input.description,
495
- });
500
+ };
501
+ if (input.repositoryId !== undefined) {
502
+ command.repositoryId = input.repositoryId;
503
+ }
504
+ if (input.projectId !== undefined) {
505
+ command.projectId = input.projectId;
506
+ }
507
+ if (input.title !== undefined) {
508
+ command.title = input.title;
509
+ }
510
+ if (input.body !== undefined) {
511
+ command.body = input.body;
512
+ }
513
+ const result = await this.client.sendCommand(command);
496
514
  return this.parseTaskFromResult(
497
515
  result,
498
516
  'control-plane task.update returned malformed task record',
@@ -1,36 +1,41 @@
1
1
  import {
2
- ConversationStartupHydrationService,
2
+ createConversationStartupHydrationService,
3
+ type ConversationStartupHydrationService,
3
4
  type ConversationStartupHydrationServiceOptions,
4
5
  type SessionSummaryLike,
5
6
  } from './conversation-startup-hydration.ts';
6
7
  import {
7
- RuntimeConversationStarter,
8
+ createRuntimeConversationStarter,
9
+ type RuntimeConversationStarter,
8
10
  type RuntimeConversationStarterConversationRecord,
9
11
  type RuntimeConversationStarterOptions,
10
12
  } from './runtime-conversation-starter.ts';
11
13
  import {
12
- RuntimeConversationActivation,
14
+ createRuntimeConversationActivation,
15
+ type RuntimeConversationActivation,
13
16
  type RuntimeConversationActivationOptions,
14
17
  } from './runtime-conversation-activation.ts';
15
18
  import {
16
- RuntimeConversationActions,
19
+ createRuntimeConversationActions,
20
+ type RuntimeConversationActions,
17
21
  type RuntimeConversationActionsOptions,
18
22
  } from './runtime-conversation-actions.ts';
19
23
  import {
20
- RuntimeConversationTitleEditService,
24
+ createRuntimeConversationTitleEditService,
21
25
  type RuntimeConversationTitleEditServiceOptions,
22
26
  } from './runtime-conversation-title-edit.ts';
23
27
  import {
24
- RuntimeStreamSubscriptions,
28
+ createRuntimeStreamSubscriptions,
25
29
  type RuntimeStreamSubscriptionsOptions,
26
30
  } from './runtime-stream-subscriptions.ts';
27
31
  import {
28
- StartupPersistedConversationQueueService,
32
+ createStartupPersistedConversationQueueService,
33
+ type StartupPersistedConversationQueueService,
29
34
  type StartupPersistedConversationQueueServiceOptions,
30
35
  type StartupQueueConversationRecord,
31
36
  } from './startup-persisted-conversation-queue.ts';
32
37
 
33
- interface ConversationLifecycleOptions<
38
+ export interface ConversationLifecycleOptions<
34
39
  TConversation extends RuntimeConversationStarterConversationRecord &
35
40
  StartupQueueConversationRecord & { title: string },
36
41
  TSessionSummary extends SessionSummaryLike,
@@ -57,120 +62,146 @@ interface ConversationLifecycleOptions<
57
62
  readonly titleEdit: RuntimeConversationTitleEditServiceOptions<TConversation>;
58
63
  }
59
64
 
60
- export class ConversationLifecycle<
65
+ export interface ConversationLifecycle<TConversation> {
66
+ subscribeConversationEvents(sessionId: string): Promise<void>;
67
+ unsubscribeConversationEvents(sessionId: string): Promise<void>;
68
+ subscribeTaskPlanningEvents(afterCursor: number | null): Promise<void>;
69
+ unsubscribeTaskPlanningEvents(): Promise<void>;
70
+ startConversation(sessionId: string): Promise<TConversation>;
71
+ activateConversation(
72
+ sessionId: string,
73
+ options?: { readonly signal?: AbortSignal },
74
+ ): Promise<void>;
75
+ createAndActivateConversationInDirectory(directoryId: string, agentType: string): Promise<void>;
76
+ openOrCreateCritiqueConversationInDirectory(directoryId: string): Promise<void>;
77
+ takeoverConversation(sessionId: string): Promise<void>;
78
+ scheduleConversationTitlePersist(): void;
79
+ stopConversationTitleEdit(persistPending: boolean): void;
80
+ beginConversationTitleEdit(conversationId: string): void;
81
+ clearConversationTitleEditTimer(): void;
82
+ hydrateConversationList(): Promise<void>;
83
+ queuePersistedConversationsInBackground(activeSessionId: string | null): number;
84
+ }
85
+
86
+ export function createConversationLifecycle<
61
87
  TConversation extends RuntimeConversationStarterConversationRecord &
62
88
  StartupQueueConversationRecord & { title: string },
63
89
  TSessionSummary extends SessionSummaryLike,
64
90
  TControllerRecord,
65
- > {
66
- private readonly streamSubscriptions: RuntimeStreamSubscriptions;
67
- private readonly starter: RuntimeConversationStarter<TConversation, TSessionSummary>;
68
- private readonly startupHydration: ConversationStartupHydrationService<TSessionSummary>;
69
- private readonly startupQueue: StartupPersistedConversationQueueService<TConversation>;
70
- private readonly activation: RuntimeConversationActivation;
71
- private readonly actions: RuntimeConversationActions<TControllerRecord>;
72
- private readonly titleEdit: RuntimeConversationTitleEditService<TConversation>;
73
-
74
- constructor(
75
- options: ConversationLifecycleOptions<TConversation, TSessionSummary, TControllerRecord>,
76
- ) {
77
- this.streamSubscriptions = new RuntimeStreamSubscriptions(options.streamSubscriptions);
78
- this.starter = new RuntimeConversationStarter({
79
- ...options.starter,
80
- subscribeConversationEvents: async (sessionId) => {
81
- await this.subscribeConversationEvents(sessionId);
82
- },
83
- });
84
- this.startupHydration = new ConversationStartupHydrationService({
85
- ...options.startupHydration,
86
- subscribeConversationEvents: async (sessionId) => {
87
- await this.subscribeConversationEvents(sessionId);
88
- },
89
- });
90
- this.startupQueue = new StartupPersistedConversationQueueService({
91
- ...options.startupQueue,
92
- startConversation: async (sessionId) => {
93
- await this.startConversation(sessionId);
94
- },
95
- });
96
- this.activation = new RuntimeConversationActivation({
97
- ...options.activation,
98
- startConversation: async (sessionId) => {
99
- await this.startConversation(sessionId);
100
- },
101
- });
102
- this.actions = new RuntimeConversationActions({
103
- ...options.actions,
104
- startConversation: async (sessionId) => {
105
- await this.startConversation(sessionId);
106
- },
107
- activateConversation: async (sessionId) => {
108
- await this.activateConversation(sessionId);
109
- },
110
- });
111
- this.titleEdit = new RuntimeConversationTitleEditService(options.titleEdit);
112
- }
91
+ >(
92
+ options: ConversationLifecycleOptions<TConversation, TSessionSummary, TControllerRecord>,
93
+ ): ConversationLifecycle<TConversation> {
94
+ const streamSubscriptions = createRuntimeStreamSubscriptions(options.streamSubscriptions);
95
+ let starter: RuntimeConversationStarter<TConversation>;
96
+ let activation: RuntimeConversationActivation;
97
+ let startupHydration: ConversationStartupHydrationService;
98
+ let startupQueue: StartupPersistedConversationQueueService;
99
+ let actions: RuntimeConversationActions;
100
+ const titleEdit = createRuntimeConversationTitleEditService(options.titleEdit);
113
101
 
114
- async subscribeConversationEvents(sessionId: string): Promise<void> {
115
- await this.streamSubscriptions.subscribeConversationEvents(sessionId);
102
+ async function subscribeConversationEvents(sessionId: string): Promise<void> {
103
+ await streamSubscriptions.subscribeConversationEvents(sessionId);
116
104
  }
117
105
 
118
- async unsubscribeConversationEvents(sessionId: string): Promise<void> {
119
- await this.streamSubscriptions.unsubscribeConversationEvents(sessionId);
106
+ async function unsubscribeConversationEvents(sessionId: string): Promise<void> {
107
+ await streamSubscriptions.unsubscribeConversationEvents(sessionId);
120
108
  }
121
109
 
122
- async subscribeTaskPlanningEvents(afterCursor: number | null): Promise<void> {
123
- await this.streamSubscriptions.subscribeTaskPlanningEvents(afterCursor);
110
+ async function subscribeTaskPlanningEvents(afterCursor: number | null): Promise<void> {
111
+ await streamSubscriptions.subscribeTaskPlanningEvents(afterCursor);
124
112
  }
125
113
 
126
- async unsubscribeTaskPlanningEvents(): Promise<void> {
127
- await this.streamSubscriptions.unsubscribeTaskPlanningEvents();
114
+ async function unsubscribeTaskPlanningEvents(): Promise<void> {
115
+ await streamSubscriptions.unsubscribeTaskPlanningEvents();
128
116
  }
129
117
 
130
- async startConversation(sessionId: string): Promise<TConversation> {
131
- return await this.starter.startConversation(sessionId);
118
+ async function startConversation(sessionId: string): Promise<TConversation> {
119
+ return await starter.startConversation(sessionId);
132
120
  }
133
121
 
134
- async activateConversation(sessionId: string): Promise<void> {
135
- await this.activation.activateConversation(sessionId);
122
+ async function activateConversation(
123
+ sessionId: string,
124
+ activateOptions: { readonly signal?: AbortSignal } = {},
125
+ ): Promise<void> {
126
+ await activation.activateConversation(sessionId, activateOptions);
136
127
  }
137
128
 
138
- async createAndActivateConversationInDirectory(
129
+ async function createAndActivateConversationInDirectory(
139
130
  directoryId: string,
140
131
  agentType: string,
141
132
  ): Promise<void> {
142
- await this.actions.createAndActivateConversationInDirectory(directoryId, agentType);
133
+ await actions.createAndActivateConversationInDirectory(directoryId, agentType);
143
134
  }
144
135
 
145
- async openOrCreateCritiqueConversationInDirectory(directoryId: string): Promise<void> {
146
- await this.actions.openOrCreateCritiqueConversationInDirectory(directoryId);
136
+ async function openOrCreateCritiqueConversationInDirectory(directoryId: string): Promise<void> {
137
+ await actions.openOrCreateCritiqueConversationInDirectory(directoryId);
147
138
  }
148
139
 
149
- async takeoverConversation(sessionId: string): Promise<void> {
150
- await this.actions.takeoverConversation(sessionId);
140
+ async function takeoverConversation(sessionId: string): Promise<void> {
141
+ await actions.takeoverConversation(sessionId);
151
142
  }
152
143
 
153
- scheduleConversationTitlePersist(): void {
154
- this.titleEdit.schedulePersist();
144
+ function scheduleConversationTitlePersist(): void {
145
+ titleEdit.schedulePersist();
155
146
  }
156
147
 
157
- stopConversationTitleEdit(persistPending: boolean): void {
158
- this.titleEdit.stop(persistPending);
148
+ function stopConversationTitleEdit(persistPending: boolean): void {
149
+ titleEdit.stop(persistPending);
159
150
  }
160
151
 
161
- beginConversationTitleEdit(conversationId: string): void {
162
- this.titleEdit.begin(conversationId);
152
+ function beginConversationTitleEdit(conversationId: string): void {
153
+ titleEdit.begin(conversationId);
163
154
  }
164
155
 
165
- clearConversationTitleEditTimer(): void {
166
- this.titleEdit.clearCurrentTimer();
156
+ function clearConversationTitleEditTimer(): void {
157
+ titleEdit.clearCurrentTimer();
167
158
  }
168
159
 
169
- async hydrateConversationList(): Promise<void> {
170
- await this.startupHydration.hydrateConversationList();
160
+ async function hydrateConversationList(): Promise<void> {
161
+ await startupHydration.hydrateConversationList();
171
162
  }
172
163
 
173
- queuePersistedConversationsInBackground(activeSessionId: string | null): number {
174
- return this.startupQueue.queuePersistedConversationsInBackground(activeSessionId);
164
+ function queuePersistedConversationsInBackground(activeSessionId: string | null): number {
165
+ return startupQueue.queuePersistedConversationsInBackground(activeSessionId);
175
166
  }
167
+
168
+ starter = createRuntimeConversationStarter({
169
+ ...options.starter,
170
+ subscribeConversationEvents,
171
+ });
172
+ startupHydration = createConversationStartupHydrationService({
173
+ ...options.startupHydration,
174
+ subscribeConversationEvents,
175
+ });
176
+ startupQueue = createStartupPersistedConversationQueueService({
177
+ ...options.startupQueue,
178
+ startConversation,
179
+ });
180
+ activation = createRuntimeConversationActivation({
181
+ ...options.activation,
182
+ startConversation,
183
+ });
184
+ actions = createRuntimeConversationActions({
185
+ ...options.actions,
186
+ startConversation,
187
+ activateConversation,
188
+ });
189
+
190
+ return {
191
+ subscribeConversationEvents,
192
+ unsubscribeConversationEvents,
193
+ subscribeTaskPlanningEvents,
194
+ unsubscribeTaskPlanningEvents,
195
+ startConversation,
196
+ activateConversation,
197
+ createAndActivateConversationInDirectory,
198
+ openOrCreateCritiqueConversationInDirectory,
199
+ takeoverConversation,
200
+ scheduleConversationTitlePersist,
201
+ stopConversationTitleEdit,
202
+ beginConversationTitleEdit,
203
+ clearConversationTitleEditTimer,
204
+ hydrateConversationList,
205
+ queuePersistedConversationsInBackground,
206
+ };
176
207
  }
@@ -19,24 +19,28 @@ export interface ConversationStartupHydrationServiceOptions<
19
19
  readonly subscribeConversationEvents: (sessionId: string) => Promise<void>;
20
20
  }
21
21
 
22
- export class ConversationStartupHydrationService<TSessionSummary extends SessionSummaryLike> {
23
- constructor(
24
- private readonly options: ConversationStartupHydrationServiceOptions<TSessionSummary>,
25
- ) {}
22
+ export interface ConversationStartupHydrationService {
23
+ hydrateConversationList(): Promise<void>;
24
+ }
26
25
 
27
- async hydrateConversationList(): Promise<void> {
28
- const hydrateSpan = this.options.startHydrationSpan();
29
- await this.options.hydrateDirectoryList();
26
+ export function createConversationStartupHydrationService<
27
+ TSessionSummary extends SessionSummaryLike,
28
+ >(
29
+ options: ConversationStartupHydrationServiceOptions<TSessionSummary>,
30
+ ): ConversationStartupHydrationService {
31
+ async function hydrateConversationList(): Promise<void> {
32
+ const hydrateSpan = options.startHydrationSpan();
33
+ await options.hydrateDirectoryList();
30
34
  let persistedCount = 0;
31
- for (const directoryId of this.options.directoryIds()) {
32
- persistedCount += await this.options.hydratePersistedConversationsForDirectory(directoryId);
35
+ for (const directoryId of options.directoryIds()) {
36
+ persistedCount += await options.hydratePersistedConversationsForDirectory(directoryId);
33
37
  }
34
38
 
35
- const summaries = await this.options.listSessions();
39
+ const summaries = await options.listSessions();
36
40
  for (const summary of summaries) {
37
- this.options.upsertFromSessionSummary(summary);
41
+ options.upsertFromSessionSummary(summary);
38
42
  if (summary.live) {
39
- await this.options.subscribeConversationEvents(summary.sessionId);
43
+ await options.subscribeConversationEvents(summary.sessionId);
40
44
  }
41
45
  }
42
46
  hydrateSpan.end({
@@ -44,4 +48,8 @@ export class ConversationStartupHydrationService<TSessionSummary extends Session
44
48
  live: summaries.length,
45
49
  });
46
50
  }
51
+
52
+ return {
53
+ hydrateConversationList,
54
+ };
47
55
  }
@@ -8,7 +8,7 @@ interface DirectoryHydrationControlPlane<TDirectoryRecord extends DirectoryRecor
8
8
  upsertDirectory(input: { directoryId: string; path: string }): Promise<TDirectoryRecord>;
9
9
  }
10
10
 
11
- interface DirectoryHydrationServiceOptions<TDirectoryRecord extends DirectoryRecordLike> {
11
+ export interface DirectoryHydrationServiceOptions<TDirectoryRecord extends DirectoryRecordLike> {
12
12
  readonly controlPlaneService: DirectoryHydrationControlPlane<TDirectoryRecord>;
13
13
  readonly resolveWorkspacePathForMux: (rawPath: string) => string;
14
14
  readonly clearDirectories: () => void;
@@ -18,32 +18,37 @@ interface DirectoryHydrationServiceOptions<TDirectoryRecord extends DirectoryRec
18
18
  readonly resolveActiveDirectoryId: () => string | null;
19
19
  }
20
20
 
21
- export class DirectoryHydrationService<TDirectoryRecord extends DirectoryRecordLike> {
22
- constructor(private readonly options: DirectoryHydrationServiceOptions<TDirectoryRecord>) {}
21
+ export interface DirectoryHydrationService {
22
+ hydrate(): Promise<void>;
23
+ }
23
24
 
24
- async hydrate(): Promise<void> {
25
- const rows = await this.options.controlPlaneService.listDirectories();
26
- this.options.clearDirectories();
25
+ export function createDirectoryHydrationService<TDirectoryRecord extends DirectoryRecordLike>(
26
+ options: DirectoryHydrationServiceOptions<TDirectoryRecord>,
27
+ ): DirectoryHydrationService {
28
+ async function hydrate(): Promise<void> {
29
+ const rows = await options.controlPlaneService.listDirectories();
30
+ options.clearDirectories();
27
31
  for (const row of rows) {
28
- const normalizedPath = this.options.resolveWorkspacePathForMux(row.path);
32
+ const normalizedPath = options.resolveWorkspacePathForMux(row.path);
29
33
  if (normalizedPath !== row.path) {
30
- const repairedRecord = await this.options.controlPlaneService.upsertDirectory({
34
+ const repairedRecord = await options.controlPlaneService.upsertDirectory({
31
35
  directoryId: row.directoryId,
32
36
  path: normalizedPath,
33
37
  });
34
- this.options.setDirectory(row.directoryId, repairedRecord);
38
+ options.setDirectory(row.directoryId, repairedRecord);
35
39
  continue;
36
40
  }
37
- this.options.setDirectory(row.directoryId, row);
41
+ options.setDirectory(row.directoryId, row);
38
42
  }
39
- if (!this.options.hasDirectory(this.options.persistedDirectory.directoryId)) {
40
- this.options.setDirectory(
41
- this.options.persistedDirectory.directoryId,
42
- this.options.persistedDirectory,
43
- );
43
+ if (!options.hasDirectory(options.persistedDirectory.directoryId)) {
44
+ options.setDirectory(options.persistedDirectory.directoryId, options.persistedDirectory);
44
45
  }
45
- if (this.options.resolveActiveDirectoryId() === null) {
46
+ if (options.resolveActiveDirectoryId() === null) {
46
47
  throw new Error('no active directory available after hydrate');
47
48
  }
48
49
  }
50
+
51
+ return {
52
+ hydrate,
53
+ };
49
54
  }
@@ -23,6 +23,10 @@ interface EventPersistenceOptions {
23
23
  const DEFAULT_FLUSH_DELAY_MS = 12;
24
24
  const DEFAULT_FLUSH_MAX_BATCH = 64;
25
25
 
26
+ function shouldPersistEvent(event: NormalizedEventEnvelope): boolean {
27
+ return !(event.source === 'provider' && event.type === 'provider-text-delta');
28
+ }
29
+
26
30
  export class EventPersistence {
27
31
  private readonly flushDelayMs: number;
28
32
  private readonly flushMaxBatch: number;
@@ -46,6 +50,9 @@ export class EventPersistence {
46
50
  }
47
51
 
48
52
  enqueue(event: NormalizedEventEnvelope): void {
53
+ if (!shouldPersistEvent(event)) {
54
+ return;
55
+ }
49
56
  this.pendingEvents.push(event);
50
57
  if (this.pendingEvents.length >= this.flushMaxBatch) {
51
58
  this.flush('immediate');