@jmoyers/harness 0.1.0

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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/native/ptyd/Cargo.lock +16 -0
  4. package/native/ptyd/Cargo.toml +7 -0
  5. package/native/ptyd/src/main.rs +257 -0
  6. package/package.json +90 -0
  7. package/scripts/build-ptyd.sh +73 -0
  8. package/scripts/control-plane-daemon.ts +277 -0
  9. package/scripts/cursor-hook-relay.ts +82 -0
  10. package/scripts/harness-animate.ts +469 -0
  11. package/scripts/harness-bin.js +77 -0
  12. package/scripts/harness-core.ts +1 -0
  13. package/scripts/harness-inspector.ts +439 -0
  14. package/scripts/harness.ts +2493 -0
  15. package/src/adapters/agent-session-state.ts +390 -0
  16. package/src/cli/gateway-record.ts +173 -0
  17. package/src/codex/live-session.ts +872 -0
  18. package/src/config/config-core.ts +1359 -0
  19. package/src/config/secrets-core.ts +170 -0
  20. package/src/control-plane/agent-realtime-api.ts +2441 -0
  21. package/src/control-plane/codex-session-stream.ts +392 -0
  22. package/src/control-plane/codex-telemetry.ts +1325 -0
  23. package/src/control-plane/lifecycle-hooks.ts +706 -0
  24. package/src/control-plane/session-summary.ts +380 -0
  25. package/src/control-plane/status/agent-status-reducer.ts +21 -0
  26. package/src/control-plane/status/reducer-base.ts +170 -0
  27. package/src/control-plane/status/reducers/claude-status-reducer.ts +37 -0
  28. package/src/control-plane/status/reducers/codex-status-reducer.ts +48 -0
  29. package/src/control-plane/status/reducers/critique-status-reducer.ts +15 -0
  30. package/src/control-plane/status/reducers/cursor-status-reducer.ts +37 -0
  31. package/src/control-plane/status/reducers/terminal-status-reducer.ts +15 -0
  32. package/src/control-plane/status/session-status-engine.ts +76 -0
  33. package/src/control-plane/stream-client.ts +396 -0
  34. package/src/control-plane/stream-command-parser.ts +1673 -0
  35. package/src/control-plane/stream-protocol.ts +1808 -0
  36. package/src/control-plane/stream-server-background.ts +486 -0
  37. package/src/control-plane/stream-server-command.ts +2557 -0
  38. package/src/control-plane/stream-server-connection.ts +234 -0
  39. package/src/control-plane/stream-server-observed-filter.ts +112 -0
  40. package/src/control-plane/stream-server-session-runtime.ts +566 -0
  41. package/src/control-plane/stream-server-state-store.ts +15 -0
  42. package/src/control-plane/stream-server.ts +3192 -0
  43. package/src/cursor/managed-hooks.ts +282 -0
  44. package/src/domain/conversations.ts +414 -0
  45. package/src/domain/directories.ts +78 -0
  46. package/src/domain/repositories.ts +123 -0
  47. package/src/domain/tasks.ts +148 -0
  48. package/src/domain/workspace.ts +156 -0
  49. package/src/events/normalized-events.ts +124 -0
  50. package/src/mux/ansi-integrity.ts +103 -0
  51. package/src/mux/control-plane-op-queue.ts +212 -0
  52. package/src/mux/conversation-rail.ts +339 -0
  53. package/src/mux/double-click.ts +78 -0
  54. package/src/mux/dual-pane-core.ts +435 -0
  55. package/src/mux/harness-core-ui.ts +817 -0
  56. package/src/mux/input-shortcuts.ts +667 -0
  57. package/src/mux/live-mux/actions-conversation.ts +344 -0
  58. package/src/mux/live-mux/actions-repository.ts +246 -0
  59. package/src/mux/live-mux/actions-task.ts +115 -0
  60. package/src/mux/live-mux/args.ts +142 -0
  61. package/src/mux/live-mux/command-menu.ts +298 -0
  62. package/src/mux/live-mux/control-plane-records.ts +546 -0
  63. package/src/mux/live-mux/conversation-state.ts +188 -0
  64. package/src/mux/live-mux/directory-resolution.ts +34 -0
  65. package/src/mux/live-mux/event-mapping.ts +96 -0
  66. package/src/mux/live-mux/gateway-profiler.ts +152 -0
  67. package/src/mux/live-mux/gateway-render-trace.ts +177 -0
  68. package/src/mux/live-mux/gateway-status-timeline.ts +166 -0
  69. package/src/mux/live-mux/git-parsing.ts +131 -0
  70. package/src/mux/live-mux/git-snapshot.ts +263 -0
  71. package/src/mux/live-mux/git-state.ts +136 -0
  72. package/src/mux/live-mux/global-shortcut-handlers.ts +143 -0
  73. package/src/mux/live-mux/home-pane-actions.ts +58 -0
  74. package/src/mux/live-mux/home-pane-drop.ts +44 -0
  75. package/src/mux/live-mux/home-pane-entity-click.ts +96 -0
  76. package/src/mux/live-mux/home-pane-pointer.ts +96 -0
  77. package/src/mux/live-mux/input-forwarding.ts +112 -0
  78. package/src/mux/live-mux/layout.ts +30 -0
  79. package/src/mux/live-mux/left-nav-activation.ts +103 -0
  80. package/src/mux/live-mux/left-nav.ts +85 -0
  81. package/src/mux/live-mux/left-rail-actions.ts +118 -0
  82. package/src/mux/live-mux/left-rail-conversation-click.ts +82 -0
  83. package/src/mux/live-mux/left-rail-pointer.ts +74 -0
  84. package/src/mux/live-mux/modal-command-menu-handler.ts +101 -0
  85. package/src/mux/live-mux/modal-conversation-handlers.ts +217 -0
  86. package/src/mux/live-mux/modal-input-reducers.ts +94 -0
  87. package/src/mux/live-mux/modal-overlays.ts +287 -0
  88. package/src/mux/live-mux/modal-pointer.ts +70 -0
  89. package/src/mux/live-mux/modal-prompt-handlers.ts +187 -0
  90. package/src/mux/live-mux/modal-task-editor-handler.ts +156 -0
  91. package/src/mux/live-mux/observed-stream.ts +87 -0
  92. package/src/mux/live-mux/palette-parsing.ts +128 -0
  93. package/src/mux/live-mux/pointer-routing.ts +108 -0
  94. package/src/mux/live-mux/process-usage.ts +53 -0
  95. package/src/mux/live-mux/project-pane-pointer.ts +44 -0
  96. package/src/mux/live-mux/rail-layout.ts +244 -0
  97. package/src/mux/live-mux/render-trace-analysis.ts +213 -0
  98. package/src/mux/live-mux/render-trace-state.ts +84 -0
  99. package/src/mux/live-mux/repository-folding.ts +207 -0
  100. package/src/mux/live-mux/runtime-shutdown.ts +51 -0
  101. package/src/mux/live-mux/selection.ts +411 -0
  102. package/src/mux/live-mux/startup-utils.ts +187 -0
  103. package/src/mux/live-mux/status-timeline-state.ts +82 -0
  104. package/src/mux/live-mux/task-pane-shortcuts.ts +206 -0
  105. package/src/mux/live-mux/terminal-palette.ts +79 -0
  106. package/src/mux/new-thread-prompt.ts +165 -0
  107. package/src/mux/project-tree.ts +295 -0
  108. package/src/mux/render-frame.ts +113 -0
  109. package/src/mux/runtime-wiring.ts +185 -0
  110. package/src/mux/selector-index.ts +160 -0
  111. package/src/mux/startup-sequencer.ts +238 -0
  112. package/src/mux/task-composer.ts +289 -0
  113. package/src/mux/task-focused-pane.ts +417 -0
  114. package/src/mux/task-screen-keybindings.ts +539 -0
  115. package/src/mux/terminal-input-modes.ts +35 -0
  116. package/src/mux/workspace-path.ts +55 -0
  117. package/src/mux/workspace-rail-model.ts +701 -0
  118. package/src/mux/workspace-rail.ts +247 -0
  119. package/src/perf/perf-core.ts +307 -0
  120. package/src/pty/pty_host.ts +217 -0
  121. package/src/pty/session-broker.ts +158 -0
  122. package/src/recording/terminal-recording.ts +383 -0
  123. package/src/services/control-plane.ts +567 -0
  124. package/src/services/conversation-lifecycle.ts +176 -0
  125. package/src/services/conversation-startup-hydration.ts +47 -0
  126. package/src/services/directory-hydration.ts +49 -0
  127. package/src/services/event-persistence.ts +104 -0
  128. package/src/services/mux-ui-state-persistence.ts +82 -0
  129. package/src/services/output-load-sampler.ts +231 -0
  130. package/src/services/process-usage-refresh.ts +88 -0
  131. package/src/services/recording.ts +75 -0
  132. package/src/services/render-trace-recorder.ts +177 -0
  133. package/src/services/runtime-control-actions.ts +123 -0
  134. package/src/services/runtime-control-plane-ops.ts +131 -0
  135. package/src/services/runtime-conversation-actions.ts +113 -0
  136. package/src/services/runtime-conversation-activation.ts +78 -0
  137. package/src/services/runtime-conversation-starter.ts +171 -0
  138. package/src/services/runtime-conversation-title-edit.ts +149 -0
  139. package/src/services/runtime-directory-actions.ts +164 -0
  140. package/src/services/runtime-envelope-handler.ts +198 -0
  141. package/src/services/runtime-git-state.ts +92 -0
  142. package/src/services/runtime-input-pipeline.ts +50 -0
  143. package/src/services/runtime-input-router.ts +202 -0
  144. package/src/services/runtime-layout-resize.ts +236 -0
  145. package/src/services/runtime-left-rail-render.ts +159 -0
  146. package/src/services/runtime-main-pane-input.ts +230 -0
  147. package/src/services/runtime-modal-input.ts +119 -0
  148. package/src/services/runtime-navigation-input.ts +207 -0
  149. package/src/services/runtime-process-wiring.ts +68 -0
  150. package/src/services/runtime-rail-input.ts +287 -0
  151. package/src/services/runtime-render-flush.ts +146 -0
  152. package/src/services/runtime-render-lifecycle.ts +104 -0
  153. package/src/services/runtime-render-orchestrator.ts +108 -0
  154. package/src/services/runtime-render-pipeline.ts +167 -0
  155. package/src/services/runtime-render-state.ts +72 -0
  156. package/src/services/runtime-repository-actions.ts +197 -0
  157. package/src/services/runtime-right-pane-render.ts +132 -0
  158. package/src/services/runtime-shutdown.ts +79 -0
  159. package/src/services/runtime-stream-subscriptions.ts +56 -0
  160. package/src/services/runtime-task-composer-persistence.ts +139 -0
  161. package/src/services/runtime-task-editor-actions.ts +83 -0
  162. package/src/services/runtime-task-pane-actions.ts +198 -0
  163. package/src/services/runtime-task-pane-shortcuts.ts +189 -0
  164. package/src/services/runtime-task-pane.ts +62 -0
  165. package/src/services/runtime-workspace-actions.ts +153 -0
  166. package/src/services/runtime-workspace-observed-events.ts +190 -0
  167. package/src/services/session-projection-instrumentation.ts +190 -0
  168. package/src/services/startup-background-probe.ts +91 -0
  169. package/src/services/startup-background-resume.ts +65 -0
  170. package/src/services/startup-orchestrator.ts +166 -0
  171. package/src/services/startup-output-tracker.ts +54 -0
  172. package/src/services/startup-paint-tracker.ts +115 -0
  173. package/src/services/startup-persisted-conversation-queue.ts +45 -0
  174. package/src/services/startup-settled-gate.ts +67 -0
  175. package/src/services/startup-shutdown.ts +53 -0
  176. package/src/services/startup-span-tracker.ts +77 -0
  177. package/src/services/startup-state-hydration.ts +94 -0
  178. package/src/services/startup-visibility.ts +35 -0
  179. package/src/services/status-timeline-recorder.ts +144 -0
  180. package/src/services/task-pane-selection-actions.ts +153 -0
  181. package/src/services/task-planning-hydration.ts +58 -0
  182. package/src/services/task-planning-observed-events.ts +89 -0
  183. package/src/services/workspace-observed-events.ts +113 -0
  184. package/src/store/control-plane-store-normalize.ts +760 -0
  185. package/src/store/control-plane-store-types.ts +224 -0
  186. package/src/store/control-plane-store.ts +2951 -0
  187. package/src/store/event-store.ts +253 -0
  188. package/src/store/sqlite.ts +81 -0
  189. package/src/terminal/compat-matrix.ts +345 -0
  190. package/src/terminal/differential-checkpoints.ts +132 -0
  191. package/src/terminal/parity-suite.ts +441 -0
  192. package/src/terminal/snapshot-oracle.ts +1840 -0
  193. package/src/ui/conversation-input-forwarder.ts +114 -0
  194. package/src/ui/conversation-selection-input.ts +103 -0
  195. package/src/ui/debug-footer-notice.ts +39 -0
  196. package/src/ui/global-shortcut-input.ts +126 -0
  197. package/src/ui/input-preflight.ts +68 -0
  198. package/src/ui/input-token-router.ts +312 -0
  199. package/src/ui/input.ts +238 -0
  200. package/src/ui/kit.ts +509 -0
  201. package/src/ui/left-nav-input.ts +80 -0
  202. package/src/ui/left-rail-pointer-input.ts +148 -0
  203. package/src/ui/main-pane-pointer-input.ts +150 -0
  204. package/src/ui/modals/manager.ts +192 -0
  205. package/src/ui/mux-theme.ts +529 -0
  206. package/src/ui/panes/conversation.ts +19 -0
  207. package/src/ui/panes/home-gridfire.ts +302 -0
  208. package/src/ui/panes/home.ts +109 -0
  209. package/src/ui/panes/left-rail.ts +12 -0
  210. package/src/ui/panes/project.ts +44 -0
  211. package/src/ui/pointer-routing-input.ts +158 -0
  212. package/src/ui/repository-fold-input.ts +91 -0
  213. package/src/ui/screen.ts +210 -0
  214. package/src/ui/surface.ts +224 -0
@@ -0,0 +1,566 @@
1
+ import { mergeAdapterStateFromSessionEvent } from '../adapters/agent-session-state.ts';
2
+ import type { CodexLiveEvent } from '../codex/live-session.ts';
3
+ import type {
4
+ StreamObservedEvent,
5
+ StreamSessionController,
6
+ StreamSessionEvent,
7
+ StreamSessionKeyEventRecord,
8
+ StreamSessionRuntimeStatus,
9
+ StreamSessionStatusModel,
10
+ StreamSignal,
11
+ } from './stream-protocol.ts';
12
+
13
+ const CLAUDE_NEEDS_INPUT_NOTIFICATION_TYPES = new Set([
14
+ 'permissionrequest',
15
+ 'approvalrequest',
16
+ 'approvalrequired',
17
+ 'inputrequired',
18
+ ]);
19
+ const CLAUDE_RUNNING_NOTIFICATION_TYPES = new Set([
20
+ 'permissionapproved',
21
+ 'permissiongranted',
22
+ 'approvalapproved',
23
+ 'approvalgranted',
24
+ ]);
25
+
26
+ interface RuntimeSession {
27
+ id: string;
28
+ directoryId: string | null;
29
+ tenantId: string;
30
+ userId: string;
31
+ workspaceId: string;
32
+ agentType: string;
33
+ adapterState: Record<string, unknown>;
34
+ eventSubscriberConnectionIds: Set<string>;
35
+ status: StreamSessionRuntimeStatus;
36
+ statusModel: StreamSessionStatusModel | null;
37
+ attentionReason: string | null;
38
+ lastEventAt: string | null;
39
+ lastExit: { code: number | null; signal: NodeJS.Signals | null } | null;
40
+ exitedAt: string | null;
41
+ latestTelemetry: {
42
+ source: StreamSessionKeyEventRecord['source'];
43
+ eventName: string | null;
44
+ severity: string | null;
45
+ summary: string | null;
46
+ observedAt: string;
47
+ } | null;
48
+ session: {
49
+ write(data: string | Uint8Array): void;
50
+ resize(cols: number, rows: number): void;
51
+ processId(): number | null;
52
+ } | null;
53
+ }
54
+
55
+ interface StreamRuntimeContext {
56
+ readonly sessions: Map<string, RuntimeSession>;
57
+ connectionCanMutateSession(connectionId: string, state: RuntimeSession): boolean;
58
+ destroySession(sessionId: string, closeSession: boolean): void;
59
+ deactivateSession(sessionId: string, closeSession: boolean): void;
60
+ sendToConnection(
61
+ connectionId: string,
62
+ envelope: Record<string, unknown>,
63
+ diagnosticSessionId?: string | null,
64
+ ): void;
65
+ sessionScope(state: RuntimeSession): {
66
+ tenantId: string;
67
+ userId: string;
68
+ workspaceId: string;
69
+ directoryId: string | null;
70
+ conversationId: string | null;
71
+ };
72
+ publishObservedEvent(
73
+ scope: {
74
+ tenantId: string;
75
+ userId: string;
76
+ workspaceId: string;
77
+ directoryId: string | null;
78
+ conversationId: string | null;
79
+ },
80
+ event: StreamObservedEvent,
81
+ ): void;
82
+ publishSessionKeyObservedEvent(
83
+ state: RuntimeSession,
84
+ keyEvent: StreamSessionKeyEventRecord,
85
+ ): void;
86
+ refreshSessionStatusModel(state: RuntimeSession, observedAt: string): void;
87
+ toPublicSessionController(
88
+ controller: StreamSessionController | null,
89
+ ): StreamSessionController | null;
90
+ readonly stateStore: {
91
+ updateConversationAdapterState(
92
+ conversationId: string,
93
+ adapterState: Record<string, unknown>,
94
+ ): void;
95
+ updateConversationRuntime(
96
+ conversationId: string,
97
+ input: {
98
+ status: StreamSessionRuntimeStatus;
99
+ statusModel: StreamSessionStatusModel | null;
100
+ live: boolean;
101
+ attentionReason: string | null;
102
+ processId: number | null;
103
+ lastEventAt: string | null;
104
+ lastExit: { code: number | null; signal: NodeJS.Signals | null } | null;
105
+ },
106
+ ): void;
107
+ };
108
+ }
109
+
110
+ interface ApplySessionKeyEventOptions {
111
+ readonly applyStatusHint: boolean;
112
+ }
113
+
114
+ function readTrimmedString(value: unknown): string | null {
115
+ if (typeof value !== 'string') {
116
+ return null;
117
+ }
118
+ const trimmed = value.trim();
119
+ return trimmed.length > 0 ? trimmed : null;
120
+ }
121
+
122
+ function normalizeEventToken(value: string): string {
123
+ return value
124
+ .trim()
125
+ .toLowerCase()
126
+ .replace(/[^a-z0-9]+/g, '');
127
+ }
128
+
129
+ function claudeStatusHintFromNotificationType(
130
+ notificationType: string,
131
+ ): 'running' | 'needs-input' | 'completed' | null {
132
+ const token = normalizeEventToken(notificationType);
133
+ if (token.length === 0) {
134
+ return null;
135
+ }
136
+ if (CLAUDE_NEEDS_INPUT_NOTIFICATION_TYPES.has(token)) {
137
+ return 'needs-input';
138
+ }
139
+ if (CLAUDE_RUNNING_NOTIFICATION_TYPES.has(token)) {
140
+ return 'running';
141
+ }
142
+ if (
143
+ token.includes('abort') ||
144
+ token.includes('interrupt') ||
145
+ token.includes('cancel') ||
146
+ token === 'stop' ||
147
+ token === 'completed' ||
148
+ token === 'complete' ||
149
+ token.includes('turncomplete')
150
+ ) {
151
+ return 'completed';
152
+ }
153
+ return null;
154
+ }
155
+
156
+ function mapSessionEvent(event: CodexLiveEvent): StreamSessionEvent | null {
157
+ if (event.type === 'notify') {
158
+ return {
159
+ type: 'notify',
160
+ record: {
161
+ ts: event.record.ts,
162
+ payload: event.record.payload,
163
+ },
164
+ };
165
+ }
166
+
167
+ if (event.type === 'session-exit') {
168
+ return {
169
+ type: 'session-exit',
170
+ exit: event.exit,
171
+ };
172
+ }
173
+
174
+ return null;
175
+ }
176
+
177
+ export function persistConversationRuntime(ctx: StreamRuntimeContext, state: RuntimeSession): void {
178
+ ctx.stateStore.updateConversationRuntime(state.id, {
179
+ status: state.status,
180
+ statusModel: state.statusModel,
181
+ live: state.session !== null,
182
+ attentionReason: state.attentionReason,
183
+ processId: state.session?.processId() ?? null,
184
+ lastEventAt: state.lastEventAt,
185
+ lastExit: state.lastExit,
186
+ });
187
+ }
188
+
189
+ export function publishStatusObservedEvent(ctx: StreamRuntimeContext, state: RuntimeSession): void {
190
+ ctx.publishObservedEvent(ctx.sessionScope(state), {
191
+ type: 'session-status',
192
+ sessionId: state.id,
193
+ status: state.status,
194
+ attentionReason: state.attentionReason,
195
+ statusModel: state.statusModel,
196
+ live: state.session !== null,
197
+ ts: new Date().toISOString(),
198
+ directoryId: state.directoryId,
199
+ conversationId: state.id,
200
+ telemetry: state.latestTelemetry,
201
+ controller: ctx.toPublicSessionController(
202
+ (state as RuntimeSession & { controller?: StreamSessionController | null }).controller ??
203
+ null,
204
+ ),
205
+ });
206
+ }
207
+
208
+ export function setSessionStatus(
209
+ ctx: StreamRuntimeContext,
210
+ state: RuntimeSession,
211
+ status: StreamSessionRuntimeStatus,
212
+ attentionReason: string | null,
213
+ lastEventAt: string | null,
214
+ ): void {
215
+ state.status = status;
216
+ state.attentionReason = attentionReason;
217
+ if (lastEventAt !== null) {
218
+ state.lastEventAt = lastEventAt;
219
+ }
220
+ const observedAt = lastEventAt ?? state.lastEventAt ?? new Date().toISOString();
221
+ ctx.refreshSessionStatusModel(state, observedAt);
222
+ persistConversationRuntime(ctx, state);
223
+ publishStatusObservedEvent(ctx, state);
224
+ }
225
+
226
+ export function handleInput(
227
+ ctx: StreamRuntimeContext,
228
+ connectionId: string,
229
+ sessionId: string,
230
+ dataBase64: string,
231
+ ): void {
232
+ const state = ctx.sessions.get(sessionId);
233
+ if (state === undefined) {
234
+ return;
235
+ }
236
+ if (!ctx.connectionCanMutateSession(connectionId, state)) {
237
+ return;
238
+ }
239
+ if (state.status === 'exited' || state.session === null) {
240
+ return;
241
+ }
242
+
243
+ const data = Buffer.from(dataBase64, 'base64');
244
+ if (data.length === 0 && dataBase64.length > 0) {
245
+ return;
246
+ }
247
+ state.session.write(data);
248
+ }
249
+
250
+ export function handleResize(
251
+ ctx: StreamRuntimeContext,
252
+ connectionId: string,
253
+ sessionId: string,
254
+ cols: number,
255
+ rows: number,
256
+ ): void {
257
+ const state = ctx.sessions.get(sessionId);
258
+ if (state === undefined) {
259
+ return;
260
+ }
261
+ if (!ctx.connectionCanMutateSession(connectionId, state)) {
262
+ return;
263
+ }
264
+ if (state.status === 'exited' || state.session === null) {
265
+ return;
266
+ }
267
+ state.session.resize(cols, rows);
268
+ }
269
+
270
+ export function handleSignal(
271
+ ctx: StreamRuntimeContext,
272
+ connectionId: string,
273
+ sessionId: string,
274
+ signal: StreamSignal,
275
+ ): void {
276
+ const state = ctx.sessions.get(sessionId);
277
+ if (state === undefined) {
278
+ return;
279
+ }
280
+ if (!ctx.connectionCanMutateSession(connectionId, state)) {
281
+ return;
282
+ }
283
+ if (state.status === 'exited' || state.session === null) {
284
+ return;
285
+ }
286
+
287
+ if (signal === 'interrupt') {
288
+ state.session.write('\u0003');
289
+ setSessionStatus(ctx, state, 'completed', null, new Date().toISOString());
290
+ return;
291
+ }
292
+
293
+ if (signal === 'eof') {
294
+ state.session.write('\u0004');
295
+ return;
296
+ }
297
+
298
+ ctx.destroySession(sessionId, true);
299
+ }
300
+
301
+ export function notifyKeyEventFromPayload(
302
+ agentType: string,
303
+ payload: Record<string, unknown>,
304
+ observedAt: string,
305
+ ): StreamSessionKeyEventRecord | null {
306
+ if (agentType === 'codex') {
307
+ const notifyPayloadType = readTrimmedString(payload['type']);
308
+ if (notifyPayloadType !== 'agent-turn-complete') {
309
+ return null;
310
+ }
311
+ return {
312
+ source: 'otlp-metric',
313
+ eventName: 'codex.turn.e2e_duration_ms',
314
+ severity: null,
315
+ summary: 'turn complete (notify)',
316
+ observedAt,
317
+ statusHint: 'completed',
318
+ };
319
+ }
320
+ if (agentType !== 'claude') {
321
+ if (agentType !== 'cursor') {
322
+ return null;
323
+ }
324
+ const hookEventNameRaw =
325
+ readTrimmedString(payload['hook_event_name']) ??
326
+ readTrimmedString(payload['hookEventName']) ??
327
+ readTrimmedString(payload['event_name']) ??
328
+ readTrimmedString(payload['eventName']) ??
329
+ readTrimmedString(payload['event']);
330
+ if (hookEventNameRaw === null) {
331
+ return null;
332
+ }
333
+ const hookEventToken = normalizeEventToken(hookEventNameRaw);
334
+ if (hookEventToken.length === 0) {
335
+ return null;
336
+ }
337
+ const eventName = `cursor.${hookEventToken}`;
338
+ const summary =
339
+ readTrimmedString(payload['summary']) ??
340
+ readTrimmedString(payload['message']) ??
341
+ readTrimmedString(payload['reason']) ??
342
+ readTrimmedString(payload['output']);
343
+ const finalStatusRaw =
344
+ readTrimmedString(payload['final_status']) ?? readTrimmedString(payload['finalStatus']) ?? '';
345
+ const finalStatus = normalizeEventToken(finalStatusRaw);
346
+ const reasonToken = normalizeEventToken(readTrimmedString(payload['reason']) ?? '');
347
+
348
+ let statusHint: StreamSessionKeyEventRecord['statusHint'] = null;
349
+ let normalizedSummary = summary;
350
+ if (hookEventToken === 'beforesubmitprompt') {
351
+ statusHint = 'running';
352
+ normalizedSummary ??= 'prompt submitted';
353
+ } else if (
354
+ hookEventToken.startsWith('before') &&
355
+ (hookEventToken.includes('shell') ||
356
+ hookEventToken.includes('mcp') ||
357
+ hookEventToken.includes('tool'))
358
+ ) {
359
+ statusHint = 'running';
360
+ normalizedSummary ??= 'tool started (hook)';
361
+ } else if (
362
+ hookEventToken === 'stop' ||
363
+ hookEventToken === 'sessionend' ||
364
+ hookEventToken.includes('abort') ||
365
+ reasonToken.includes('abort') ||
366
+ finalStatus === 'aborted' ||
367
+ finalStatus === 'cancelled' ||
368
+ finalStatus === 'canceled' ||
369
+ finalStatus === 'completed'
370
+ ) {
371
+ statusHint = 'completed';
372
+ normalizedSummary ??=
373
+ finalStatus === 'aborted' ? 'turn complete (aborted)' : 'turn complete (hook)';
374
+ } else if (
375
+ hookEventToken.startsWith('after') &&
376
+ (hookEventToken.includes('shell') ||
377
+ hookEventToken.includes('mcp') ||
378
+ hookEventToken.includes('tool'))
379
+ ) {
380
+ normalizedSummary ??= 'tool finished (hook)';
381
+ }
382
+
383
+ return {
384
+ source: 'otlp-log',
385
+ eventName,
386
+ severity: null,
387
+ summary: normalizedSummary,
388
+ observedAt,
389
+ statusHint,
390
+ };
391
+ }
392
+
393
+ const hookEventNameRaw =
394
+ readTrimmedString(payload['hook_event_name']) ?? readTrimmedString(payload['hookEventName']);
395
+ if (hookEventNameRaw === null) {
396
+ return null;
397
+ }
398
+ const hookEventToken = normalizeEventToken(hookEventNameRaw);
399
+ if (hookEventToken.length === 0) {
400
+ return null;
401
+ }
402
+ const eventName = `claude.${hookEventToken}`;
403
+ const summary = readTrimmedString(payload['message']) ?? readTrimmedString(payload['reason']);
404
+ const notificationType = readTrimmedString(payload['notification_type'])?.toLowerCase() ?? '';
405
+
406
+ let statusHint: StreamSessionKeyEventRecord['statusHint'] = null;
407
+ let normalizedSummary = summary;
408
+ if (hookEventToken === 'userpromptsubmit') {
409
+ statusHint = 'running';
410
+ normalizedSummary ??= 'prompt submitted';
411
+ } else if (hookEventToken === 'pretooluse') {
412
+ statusHint = 'running';
413
+ normalizedSummary ??= 'tool started (hook)';
414
+ } else if (
415
+ hookEventToken === 'stop' ||
416
+ hookEventToken === 'subagentstop' ||
417
+ hookEventToken === 'sessionend'
418
+ ) {
419
+ statusHint = 'completed';
420
+ normalizedSummary ??= 'turn complete (hook)';
421
+ } else if (hookEventToken === 'notification') {
422
+ statusHint = claudeStatusHintFromNotificationType(notificationType);
423
+ if (normalizedSummary === null) {
424
+ normalizedSummary = notificationType.length > 0 ? notificationType : hookEventNameRaw;
425
+ }
426
+ }
427
+
428
+ return {
429
+ source: 'otlp-log',
430
+ eventName,
431
+ severity: null,
432
+ summary: normalizedSummary,
433
+ observedAt,
434
+ statusHint,
435
+ };
436
+ }
437
+
438
+ function summarizeUnmappedNotifyPayload(payload: Record<string, unknown>): string {
439
+ const keys = Object.keys(payload).slice(0, 6);
440
+ if (keys.length === 0) {
441
+ return 'notify payload unmapped (no keys)';
442
+ }
443
+ return `notify payload unmapped keys=${keys.join(',')}`;
444
+ }
445
+
446
+ function normalizedAgentTypeForUnmappedEvent(
447
+ agentType: string,
448
+ ): 'codex' | 'claude' | 'cursor' | 'terminal' | 'critique' | 'agent' {
449
+ if (
450
+ agentType === 'codex' ||
451
+ agentType === 'claude' ||
452
+ agentType === 'cursor' ||
453
+ agentType === 'terminal' ||
454
+ agentType === 'critique'
455
+ ) {
456
+ return agentType;
457
+ }
458
+ return 'agent';
459
+ }
460
+
461
+ export function unmappedNotifyKeyEventFromPayload(
462
+ agentType: string,
463
+ payload: Record<string, unknown>,
464
+ observedAt: string,
465
+ ): StreamSessionKeyEventRecord {
466
+ const normalizedAgentType = normalizedAgentTypeForUnmappedEvent(agentType);
467
+ return {
468
+ source: 'otlp-log',
469
+ eventName: `${normalizedAgentType}.notify.unmapped`,
470
+ severity: null,
471
+ summary: summarizeUnmappedNotifyPayload(payload),
472
+ observedAt,
473
+ statusHint: null,
474
+ };
475
+ }
476
+
477
+ export function applySessionKeyEvent(
478
+ ctx: StreamRuntimeContext,
479
+ state: RuntimeSession,
480
+ keyEvent: StreamSessionKeyEventRecord,
481
+ options: ApplySessionKeyEventOptions,
482
+ ): void {
483
+ state.latestTelemetry = {
484
+ source: keyEvent.source,
485
+ eventName: keyEvent.eventName,
486
+ severity: keyEvent.severity,
487
+ summary: keyEvent.summary,
488
+ observedAt: keyEvent.observedAt,
489
+ };
490
+ ctx.publishSessionKeyObservedEvent(state, keyEvent);
491
+ if (options.applyStatusHint && keyEvent.statusHint === 'needs-input') {
492
+ const nextAttentionReason = keyEvent.summary ?? state.attentionReason ?? 'input required';
493
+ setSessionStatus(ctx, state, 'needs-input', nextAttentionReason, keyEvent.observedAt);
494
+ return;
495
+ }
496
+ if (options.applyStatusHint && keyEvent.statusHint !== null) {
497
+ setSessionStatus(ctx, state, keyEvent.statusHint, null, keyEvent.observedAt);
498
+ return;
499
+ }
500
+ setSessionStatus(ctx, state, state.status, state.attentionReason, keyEvent.observedAt);
501
+ }
502
+
503
+ export function handleSessionEvent(
504
+ ctx: StreamRuntimeContext,
505
+ sessionId: string,
506
+ event: CodexLiveEvent,
507
+ ): void {
508
+ const sessionState = ctx.sessions.get(sessionId);
509
+ if (sessionState === undefined) {
510
+ return;
511
+ }
512
+
513
+ const mapped = mapSessionEvent(event);
514
+ if (mapped !== null && event.type !== 'terminal-output') {
515
+ const observedAt = mapped.type === 'session-exit' ? new Date().toISOString() : mapped.record.ts;
516
+ for (const connectionId of sessionState.eventSubscriberConnectionIds) {
517
+ ctx.sendToConnection(
518
+ connectionId,
519
+ {
520
+ kind: 'pty.event',
521
+ sessionId,
522
+ event: mapped,
523
+ },
524
+ sessionId,
525
+ );
526
+ }
527
+ ctx.publishObservedEvent(ctx.sessionScope(sessionState), {
528
+ type: 'session-event',
529
+ sessionId,
530
+ event: mapped,
531
+ ts: new Date().toISOString(),
532
+ directoryId: sessionState.directoryId,
533
+ conversationId: sessionState.id,
534
+ });
535
+ const mergedAdapterState = mergeAdapterStateFromSessionEvent(
536
+ sessionState.agentType,
537
+ sessionState.adapterState,
538
+ mapped,
539
+ observedAt,
540
+ );
541
+ if (mergedAdapterState !== null) {
542
+ sessionState.adapterState = mergedAdapterState;
543
+ ctx.stateStore.updateConversationAdapterState(sessionState.id, mergedAdapterState);
544
+ }
545
+ if (mapped.type === 'notify') {
546
+ const keyEvent =
547
+ notifyKeyEventFromPayload(sessionState.agentType, mapped.record.payload, observedAt) ??
548
+ unmappedNotifyKeyEventFromPayload(
549
+ sessionState.agentType,
550
+ mapped.record.payload,
551
+ observedAt,
552
+ );
553
+ applySessionKeyEvent(ctx, sessionState, keyEvent, {
554
+ applyStatusHint: true,
555
+ });
556
+ }
557
+ }
558
+
559
+ if (event.type === 'session-exit') {
560
+ sessionState.lastExit = event.exit;
561
+ const exitedAt = new Date().toISOString();
562
+ sessionState.exitedAt = exitedAt;
563
+ setSessionStatus(ctx, sessionState, 'exited', null, exitedAt);
564
+ ctx.deactivateSession(sessionState.id, true);
565
+ }
566
+ }
@@ -0,0 +1,15 @@
1
+ interface StateStoreContext {
2
+ readonly ownsStateStore: boolean;
3
+ stateStoreClosed: boolean;
4
+ readonly stateStore: {
5
+ close(): void;
6
+ };
7
+ }
8
+
9
+ export function closeOwnedStateStore(ctx: StateStoreContext): void {
10
+ if (!ctx.ownsStateStore || ctx.stateStoreClosed) {
11
+ return;
12
+ }
13
+ ctx.stateStore.close();
14
+ ctx.stateStoreClosed = true;
15
+ }