@myagentroam/node 0.11.35 → 0.11.37

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 (143) hide show
  1. package/dist/application/execution-lifecycle.js +54 -0
  2. package/dist/application/node-lifecycle.js +104 -0
  3. package/dist/application/node-operation-admission.js +51 -0
  4. package/dist/application/node-operation-contracts.js +1 -0
  5. package/dist/application/runner-composition.js +22 -0
  6. package/dist/application/session-projection-composition.js +26 -0
  7. package/dist/application/workspace-application.js +61 -0
  8. package/dist/application/workspace-composition.js +21 -0
  9. package/dist/capabilities.js +2 -2
  10. package/dist/codex-app-server.js +1 -471
  11. package/dist/connection/authorization.js +26 -0
  12. package/dist/{service → connection}/direct-transfer-service.js +1 -1
  13. package/dist/connection/endpoints.js +32 -0
  14. package/dist/connector.js +144 -252
  15. package/dist/database.js +34 -14
  16. package/dist/git-askpass.js +1 -1
  17. package/dist/git-execution-helper.js +1 -1
  18. package/dist/hosting/hosted-conversation-file-identity.js +27 -0
  19. package/dist/hosting/hosted-conversation-file-operations.js +44 -0
  20. package/dist/hosting/hosted-conversation-node-service.js +974 -0
  21. package/dist/hosting/hosted-conversation-runs.js +599 -0
  22. package/dist/hosting/hosted-conversation-templates.js +280 -0
  23. package/dist/hosting/hosted-conversation-uploads.js +344 -0
  24. package/dist/hosting/hosted-conversation-watch.js +194 -0
  25. package/dist/hosting/runtime-contracts.js +1 -0
  26. package/dist/marctl-guidance.js +1 -1
  27. package/dist/runner/abstract-runner.js +4 -13
  28. package/dist/{claude-agent-sdk.js → runner/claude/claude-agent-sdk.js} +2 -2
  29. package/dist/runner/claude/conversation-parser.js +2 -1
  30. package/dist/runner/claude/managed-run-controller.js +6 -7
  31. package/dist/runner/claude/message-parsers.js +2 -1
  32. package/dist/runner/claude/native-transcript.js +2 -1
  33. package/dist/runner/claude/tool-normalizer.js +1 -1
  34. package/dist/runner/claude-code-runner.js +2 -2
  35. package/dist/runner/codex/codex-app-server.js +471 -0
  36. package/dist/runner/codex/conversation-parser.js +4 -2
  37. package/dist/runner/codex/managed-run-controller.js +7 -9
  38. package/dist/runner/codex/native-transcript.js +1 -1
  39. package/dist/runner/codex/tool-normalizer.js +1 -1
  40. package/dist/runner/codex-runner.js +9 -27
  41. package/dist/runner/contracts.js +1 -0
  42. package/dist/runner/mar-agent/conversation-normalizer.js +2 -2
  43. package/dist/runner/mar-agent/history-projection.js +1 -1
  44. package/dist/runner/mar-agent/hosted-conversation-runtime.js +6 -10
  45. package/dist/runner/mar-agent/managed-run-controller.js +85 -61
  46. package/dist/runner/mar-agent-runner.js +1 -1
  47. package/dist/runner/normalization.js +42 -0
  48. package/dist/runner/opencode/conversation-parser.js +1 -1
  49. package/dist/runner/opencode/managed-run-controller.js +6 -7
  50. package/dist/{opencode-server.js → runner/opencode/opencode-server.js} +2 -2
  51. package/dist/runner/opencode/tool-normalizer.js +1 -1
  52. package/dist/runner/opencode-runner.js +4 -5
  53. package/dist/runner/runner-registry.js +4 -1
  54. package/dist/runtime-state.js +48 -372
  55. package/dist/service/computer-use-enablement-service.js +1 -1
  56. package/dist/service/continuous-run-coordinator.js +52 -7
  57. package/dist/service/conversation-history-service.js +4 -4
  58. package/dist/service/conversation-hosting-capability-service.js +1 -1
  59. package/dist/service/conversation-hosting-enablement-service.js +1 -1
  60. package/dist/service/conversation-hosting-lifecycle-service.js +3 -3
  61. package/dist/service/conversation-hosting-maintenance-service.js +1 -1
  62. package/dist/service/conversation-observation.js +1 -1
  63. package/dist/service/conversation-segment-service.js +1 -1
  64. package/dist/service/external-session-resume-service.js +47 -27
  65. package/dist/service/fake-managed-run-service.js +3 -7
  66. package/dist/service/git-operation-service.js +1 -1
  67. package/dist/service/goal-continuation-coordinator.js +14 -7
  68. package/dist/service/mar-agent-model-transport-test-service.js +1 -1
  69. package/dist/service/native-session-watch-service.js +1 -1
  70. package/dist/service/node-control-channel.js +1 -5
  71. package/dist/service/node-request-service.js +1 -1
  72. package/dist/service/node-terminal-channel.js +2 -1
  73. package/dist/service/node-workspace-coordinator.js +3 -2
  74. package/dist/service/notification-mcp-bridge.js +6 -2
  75. package/dist/service/recent-run-projection-service.js +1 -1
  76. package/dist/service/restart-session-restorer.js +1 -1
  77. package/dist/service/run-event-service.js +49 -42
  78. package/dist/service/run-workbench-service.js +3 -3
  79. package/dist/service/runner-interaction-service.js +6 -5
  80. package/dist/service/runner-service.js +1 -1
  81. package/dist/service/session-recent-activity.js +2 -1
  82. package/dist/service/skill-directory-service.js +1 -1
  83. package/dist/service/skill-install-service.js +2 -2
  84. package/dist/service/terminal-service.js +1 -1
  85. package/dist/service/workbench-event-projection.js +3 -2
  86. package/dist/service/workbench-event-service.js +1 -1
  87. package/dist/service/workspace-service.js +3 -2
  88. package/dist/session/attachments.js +110 -0
  89. package/dist/session/contracts.js +1 -0
  90. package/dist/{util → session}/conversation-identity.js +1 -1
  91. package/dist/session/input-validation.js +15 -0
  92. package/dist/{service → session}/native-session-projection-service.js +40 -11
  93. package/dist/session/pagination.js +77 -0
  94. package/dist/{util → session}/runner-native-session-parsers.js +2 -1
  95. package/dist/session/runtime/approvals.js +90 -0
  96. package/dist/session/runtime/conversation-projection.js +310 -0
  97. package/dist/session/runtime/type-guards.js +19 -0
  98. package/dist/{service → session}/session-catalog-service.js +5 -4
  99. package/dist/{service → session}/session-command-service.js +3 -3
  100. package/dist/{service → session}/session-identity-service.js +1 -1
  101. package/dist/{service → session}/session-lifecycle-service.js +18 -7
  102. package/dist/{service → session}/session-message-service.js +6 -5
  103. package/dist/{service → session}/session-presentation-service.js +1 -1
  104. package/dist/{service → session}/session-query-service.js +2 -2
  105. package/dist/{service → session}/session-schedule-service.js +2 -2
  106. package/dist/util/records.js +3 -0
  107. package/dist/workspace/contracts.js +1 -0
  108. package/dist/workspace/file-index.js +534 -0
  109. package/dist/workspace/files.js +466 -0
  110. package/dist/workspace/git.js +991 -0
  111. package/dist/workspace/path-access.js +184 -0
  112. package/dist/workspace/utf8.js +12 -0
  113. package/dist/{service → workspace}/workspace-change-service.js +45 -21
  114. package/dist/{service → workspace}/workspace-code-navigation-service.js +6 -6
  115. package/dist/{service → workspace}/workspace-file-manager-service.js +1 -1
  116. package/dist/{service → workspace}/workspace-file-service.js +16 -16
  117. package/dist/{service → workspace}/workspace-git-exclude-service.js +1 -1
  118. package/dist/{service → workspace}/workspace-queue-workbench-service.js +3 -2
  119. package/dist/{service → workspace}/workspace-session-service.js +45 -40
  120. package/dist/{service → workspace}/workspace-upload-service.js +28 -28
  121. package/dist/{service → workspace}/workspace-watch-service.js +29 -28
  122. package/dist/{service → workspace}/workspace-workbench-service.js +4 -3
  123. package/package.json +4 -4
  124. package/dist/service/hosted-conversation-node-service.js +0 -2252
  125. package/dist/service/run-domain-state.js +0 -4
  126. package/dist/service/session-domain-state.js +0 -9
  127. package/dist/service/workspace-domain-state.js +0 -41
  128. package/dist/util/node-operation-parsers.js +0 -320
  129. package/dist/workspace.js +0 -2168
  130. /package/dist/{connector → application}/node-connector-options.js +0 -0
  131. /package/dist/{util → connection}/node-operation-data.js +0 -0
  132. /package/dist/{connector → connection}/node-operation-router.js +0 -0
  133. /package/dist/{service → hosting}/hosted-conversation-sandbox-profile.js +0 -0
  134. /package/dist/{service → hosting}/hosted-conversation-store.js +0 -0
  135. /package/dist/{util → mcp}/secret-environment.js +0 -0
  136. /package/dist/{claude-channel.js → runner/claude/claude-channel.js} +0 -0
  137. /package/dist/{util → runner}/personal-instructions.js +0 -0
  138. /package/dist/{util → runner}/runner-error.js +0 -0
  139. /package/dist/{service/conversation-work-process.js → session/runtime/work-process.js} +0 -0
  140. /package/dist/{service → session}/session-context-service.js +0 -0
  141. /package/dist/{util → workspace}/git-credential-client.js +0 -0
  142. /package/dist/{service → workspace}/workspace-content-search-service.js +0 -0
  143. /package/dist/{service → workspace}/workspace-watch-snapshot-service.js +0 -0
@@ -0,0 +1,599 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { marAgentGeneratedImagesDirectory, hostedConversationGeneratedImagesDisplayDirectory } from '../runner/mar-agent/generated-images.js';
3
+ import { allowedModels, selectModel, networkModels, resolveMcpServers, runtimeModels, systemInstruction } from './hosted-conversation-templates.js';
4
+ import { steerComposerMessage } from '@myagentroam/protocol';
5
+ import { hostedConversationSummarySchema } from '@myagentroam/protocol';
6
+ import { sessionTitleFromFirstMessage } from '../session/input-validation.js';
7
+ import { safeErrorCode } from '../util/safe-error.js';
8
+ class HostedRunEvents {
9
+ conversationId;
10
+ watch;
11
+ accepted = false;
12
+ pending = [];
13
+ constructor(conversationId, watch) {
14
+ this.conversationId = conversationId;
15
+ this.watch = watch;
16
+ }
17
+ emit = (event) => {
18
+ if (this.accepted)
19
+ this.publish(event);
20
+ else
21
+ this.pending.push(event);
22
+ };
23
+ accept() {
24
+ this.accepted = true;
25
+ for (const event of this.pending)
26
+ this.publish(event);
27
+ this.pending.length = 0;
28
+ }
29
+ publish(event) {
30
+ this.watch.publishEvent(this.conversationId, {
31
+ conversationId: this.conversationId,
32
+ kind: 'runtime',
33
+ runtimeEvent: event
34
+ });
35
+ }
36
+ }
37
+ /** Owns accepted Hosted Runs, message admissions, settlements and interruption intent. */
38
+ export class HostedConversationRuns {
39
+ options;
40
+ activities = new Map();
41
+ constructor(options) {
42
+ this.options = options;
43
+ }
44
+ forgetActivity(id) {
45
+ this.activities.delete(id);
46
+ }
47
+ runtimeEvents(conversationId) {
48
+ return new HostedRunEvents(conversationId, this.options.hostedWatch);
49
+ }
50
+ adopt(input) {
51
+ const { conversation, handle, userId } = input;
52
+ this.accept(conversation.id, handle);
53
+ this.publishActivity(userId, conversation, 'RUNNING', handle.run.startedAt);
54
+ if (input.titleChanged)
55
+ this.options.hostedWatch.publishEvent(conversation.id, {
56
+ conversationId: conversation.id,
57
+ kind: 'conversation',
58
+ conversation: this.summary(conversation)
59
+ });
60
+ this.options.hostedWatch.publishEvent(conversation.id, {
61
+ conversationId: conversation.id,
62
+ kind: 'run',
63
+ run: handle.run
64
+ });
65
+ input.events.accept();
66
+ const settlement = Promise.resolve().then(async () => {
67
+ try {
68
+ const completion = await handle.completion.catch(() => ({
69
+ status: 'FAILED',
70
+ completedAt: this.options.now()
71
+ }));
72
+ await this.waitForNativeAdmission(handle);
73
+ await this.finishRun(userId, conversation.id, input.clientMessageId, handle.run, completion);
74
+ }
75
+ catch {
76
+ // Settlement failures must not leave the conversation permanently busy.
77
+ }
78
+ finally {
79
+ this.release(conversation.id, handle, settlement);
80
+ }
81
+ });
82
+ this.trackSettlement(conversation.id, settlement);
83
+ void settlement;
84
+ }
85
+ async startMessage(payload, input, conversation, steerPrompt) {
86
+ const temporaryTitle = sessionTitleFromFirstMessage(input.prompt);
87
+ const firstNonEmptyMessage = temporaryTitle.length > 0 &&
88
+ !this.options.store.hasNonEmptyUserMessage(payload.userId, conversation.id);
89
+ const models = runtimeModels(payload.models);
90
+ const paths = this.options.store.pathsForConversation(payload.userId, conversation.id);
91
+ let version;
92
+ let selected;
93
+ let mcpServers;
94
+ let upgrade;
95
+ const latestVersionId = conversation.templateDeleted
96
+ ? undefined
97
+ : this.options.templates.latestAvailableTemplateVersionId(conversation.templateId);
98
+ if (latestVersionId !== undefined && latestVersionId !== conversation.activeVersionId) {
99
+ const previousVersionId = conversation.activeVersionId;
100
+ const targetVersionId = latestVersionId;
101
+ try {
102
+ version = await this.options.templates.loadTemplateVersion(targetVersionId, conversation.templateId);
103
+ selected = selectModel(version, models, input.modelId ?? version.modelPolicy.defaultId);
104
+ mcpServers = resolveMcpServers(version, this.options.secretEnvironment ?? process.env);
105
+ this.options.templates.pathMounts(paths, version);
106
+ this.options.templates.processExecutor(paths, version);
107
+ upgrade = { previousVersionId, targetVersionId };
108
+ }
109
+ catch (error) {
110
+ this.publishUpgradeFailure(conversation, previousVersionId, targetVersionId, error);
111
+ throw new Error('HOSTED_CONVERSATION_UPGRADE_FAILED');
112
+ }
113
+ }
114
+ else {
115
+ version = await this.options.templates.loadTemplateVersion(conversation.activeVersionId, conversation.templateId);
116
+ selected = selectModel(version, models, input.modelId ?? conversation.modelId);
117
+ }
118
+ const attachments = await Promise.all(input.attachmentIds.map((fileId) => this.options.uploads.requireAttachment(payload.userId, conversation.id, fileId)));
119
+ const prompt = attachmentPrompt(steerPrompt ?? input.prompt, attachments);
120
+ const generatedImagesDirectory = marAgentGeneratedImagesDirectory(paths.homeDirectory, conversation.sessionId);
121
+ const generatedImagesLogicalPath = hostedConversationGeneratedImagesDisplayDirectory(conversation.sessionId);
122
+ if (generatedImagesDirectory === undefined || generatedImagesLogicalPath === undefined)
123
+ throw new Error('HOSTED_CONVERSATION_SESSION_INVALID');
124
+ await mkdir(generatedImagesDirectory, { recursive: true, mode: 0o700 });
125
+ const temporaryDirectory = await this.options.store.prepareConversationTemporaryDirectory(payload.userId, conversation.id);
126
+ const pathMounts = this.options.templates.pathMounts(paths, version, generatedImagesDirectory, generatedImagesLogicalPath, temporaryDirectory);
127
+ this.publishActivity(payload.userId, conversation, 'STARTING', this.options.now());
128
+ const runtimeEvents = this.runtimeEvents(conversation.id);
129
+ const handle = await this.options.runtime
130
+ .startRun({
131
+ ...(payload.usageGrant === undefined || this.options.modelUsage === undefined
132
+ ? {}
133
+ : { onModelUsage: (usage) => this.options.modelUsage(payload.usageGrant, usage) }),
134
+ conversationId: conversation.id,
135
+ sessionId: conversation.sessionId,
136
+ clientMessageId: input.clientMessageId,
137
+ userPrompt: input.prompt,
138
+ prompt,
139
+ attachmentIds: input.attachmentIds,
140
+ access: 'full-access',
141
+ modelId: selected.id,
142
+ models: networkModels(allowedModels(version, models), version.draft.networkMode),
143
+ ...(payload.compact === undefined ? {} : { compact: payload.compact }),
144
+ ...(payload.runtimeScopeId === undefined ? {} : { runtimeScopeId: payload.runtimeScopeId }),
145
+ systemInstruction: systemInstruction(version, generatedImagesLogicalPath),
146
+ homeDirectory: paths.homeDirectory,
147
+ workspaceDirectory: version.templateDirectory,
148
+ temporaryDirectory,
149
+ pathMounts,
150
+ processExecutor: this.options.templates.processExecutor(paths, version, temporaryDirectory, generatedImagesDirectory, generatedImagesLogicalPath),
151
+ mcpServers: (mcpServers ?? resolveMcpServers(version, this.options.secretEnvironment ?? process.env)).map((server) => ({ ...server, authorizationIdentity: `user:${payload.userId}` })),
152
+ webAccess: version.draft.networkMode !== 'NONE',
153
+ onEvent: runtimeEvents.emit
154
+ })
155
+ .catch((error) => {
156
+ this.publishActivity(payload.userId, conversation, 'IDLE', this.options.now());
157
+ if (upgrade !== undefined) {
158
+ this.publishUpgradeFailure(conversation, upgrade.previousVersionId, upgrade.targetVersionId, error);
159
+ throw new Error('HOSTED_CONVERSATION_UPGRADE_FAILED');
160
+ }
161
+ throw error;
162
+ });
163
+ const startedAt = handle.run.startedAt;
164
+ let messageCreated = false;
165
+ let accepted;
166
+ try {
167
+ this.options.store.createMessage({
168
+ conversationId: conversation.id,
169
+ clientMessageId: input.clientMessageId,
170
+ runId: handle.run.id,
171
+ userTextPresent: temporaryTitle.length > 0,
172
+ status: 'RUNNING',
173
+ createdAt: startedAt,
174
+ updatedAt: startedAt
175
+ });
176
+ messageCreated = true;
177
+ const configuredAt = this.options.now();
178
+ accepted = await this.options.store.saveMessageAcceptance({
179
+ conversationId: conversation.id,
180
+ modelId: selected.id,
181
+ activityAt: configuredAt,
182
+ ...(upgrade === undefined
183
+ ? {}
184
+ : {
185
+ templateVersion: {
186
+ expectedActiveVersionId: upgrade.previousVersionId,
187
+ activeVersionId: upgrade.targetVersionId,
188
+ maxStorageBytes: version.draft.quotas.maxStorageBytes
189
+ }
190
+ }),
191
+ ...(firstNonEmptyMessage ? { automaticTitle: temporaryTitle } : {})
192
+ });
193
+ }
194
+ catch (error) {
195
+ try {
196
+ handle.cancel();
197
+ }
198
+ catch {
199
+ // Preserve the original persistence failure.
200
+ }
201
+ const completion = await handle.completion.catch(() => ({
202
+ status: 'FAILED',
203
+ completedAt: this.options.now()
204
+ }));
205
+ if (messageCreated)
206
+ try {
207
+ this.options.store.finishMessage(conversation.id, input.clientMessageId, completion.status, completion.completedAt);
208
+ }
209
+ catch {
210
+ // Preserve the original persistence failure.
211
+ }
212
+ this.publishActivity(payload.userId, conversation, 'IDLE', completion.completedAt);
213
+ if (upgrade !== undefined)
214
+ this.publishUpgradeFailure(conversation, upgrade.previousVersionId, upgrade.targetVersionId, error);
215
+ throw error;
216
+ }
217
+ conversation = accepted.conversation;
218
+ if (upgrade !== undefined)
219
+ this.options.hostedWatch.publishEvent(conversation.id, {
220
+ conversationId: conversation.id,
221
+ kind: 'upgrade',
222
+ upgrade: {
223
+ status: 'SUCCEEDED',
224
+ previousVersionId: upgrade.previousVersionId,
225
+ targetVersionId: upgrade.targetVersionId,
226
+ activeVersionId: conversation.activeVersionId,
227
+ conversation: this.summary(conversation),
228
+ error: null
229
+ }
230
+ });
231
+ this.adopt({
232
+ userId: payload.userId,
233
+ conversation,
234
+ clientMessageId: input.clientMessageId,
235
+ handle,
236
+ titleChanged: accepted.titleChanged,
237
+ events: runtimeEvents
238
+ });
239
+ return {
240
+ conversation: this.summary(conversation),
241
+ run: handle.run
242
+ };
243
+ }
244
+ publishUpgradeFailure(conversation, previousVersionId, targetVersionId, error) {
245
+ this.options.hostedWatch.publishEvent(conversation.id, {
246
+ conversationId: conversation.id,
247
+ kind: 'upgrade',
248
+ upgrade: {
249
+ status: 'FAILED',
250
+ previousVersionId,
251
+ targetVersionId,
252
+ activeVersionId: conversation.activeVersionId,
253
+ conversation: this.summary(conversation),
254
+ error: {
255
+ code: safeErrorCode(error, 'HOSTED_CONVERSATION_UPGRADE_FAILED'),
256
+ message: 'The conversation template upgrade failed.'
257
+ }
258
+ }
259
+ });
260
+ }
261
+ pendingMessages = new Map();
262
+ activeRuns = new Map();
263
+ settlements = new Map();
264
+ nativeAdmissions = new Map();
265
+ expectedInterruptions = new Set();
266
+ async replaceMessage(payload, input, conversation, start) {
267
+ const handle = this.current(conversation.id);
268
+ if (handle === undefined) {
269
+ if (this.settlement(conversation.id))
270
+ throw new Error('HOSTED_CONVERSATION_RUN_ACTIVE');
271
+ return start();
272
+ }
273
+ const steerPrompt = steerComposerMessage(input.prompt);
274
+ if (handle.steer) {
275
+ const barrier = Promise.withResolvers();
276
+ this.beginNativeAdmission(handle, barrier.promise);
277
+ let result;
278
+ try {
279
+ const attachments = await Promise.all(input.attachmentIds.map((fileId) => this.options.uploads.requireAttachment(payload.userId, conversation.id, fileId)));
280
+ result = await handle.steer({
281
+ prompt: input.prompt,
282
+ modelText: attachmentPrompt(steerPrompt, attachments),
283
+ clientMessageId: input.clientMessageId,
284
+ ...(input.modelId === undefined ? {} : { modelId: input.modelId }),
285
+ ...(payload.runtimeScopeId === undefined
286
+ ? {}
287
+ : { runtimeScopeId: payload.runtimeScopeId })
288
+ });
289
+ if (result.status !== 'notAccepted') {
290
+ const now = this.options.now();
291
+ this.options.store.createMessage({
292
+ conversationId: conversation.id,
293
+ clientMessageId: input.clientMessageId,
294
+ runId: handle.run.id,
295
+ userTextPresent: result.status === 'accepted' && sessionTitleFromFirstMessage(input.prompt).length > 0,
296
+ status: 'RUNNING',
297
+ createdAt: handle.run.startedAt,
298
+ updatedAt: now
299
+ });
300
+ const accepted = await this.options.store.saveMessageAcceptance({
301
+ conversationId: conversation.id,
302
+ modelId: conversation.modelId,
303
+ activityAt: now
304
+ });
305
+ return { conversation: this.summary(accepted.conversation), run: handle.run };
306
+ }
307
+ }
308
+ finally {
309
+ barrier.resolve();
310
+ this.endNativeAdmission(handle);
311
+ }
312
+ }
313
+ if (!this.interruptActiveRun(payload.userId, conversation, handle))
314
+ throw new Error('HOSTED_CONVERSATION_RUN_ACTIVE');
315
+ await this.settlement(conversation.id)?.catch(() => undefined);
316
+ return start(steerPrompt);
317
+ }
318
+ admitMessage(userId, conversationId, clientMessageId, begin) {
319
+ const key = `${conversationId}\0${clientMessageId}`;
320
+ const pending = this.pending(key);
321
+ if (pending !== undefined)
322
+ return pending;
323
+ const existing = this.options.store.findMessage(userId, conversationId, clientMessageId);
324
+ if (existing !== undefined) {
325
+ const conversation = this.options.store.requireConversation(userId, conversationId);
326
+ return Promise.resolve({
327
+ conversation: this.summary(conversation),
328
+ run: {
329
+ id: existing.runId,
330
+ status: existing.status,
331
+ startedAt: existing.createdAt,
332
+ completedAt: existing.status === 'RUNNING' ? null : existing.updatedAt
333
+ }
334
+ });
335
+ }
336
+ if (this.hasPending(conversationId))
337
+ return Promise.reject(new Error('HOSTED_CONVERSATION_RUN_ACTIVE'));
338
+ const operation = begin();
339
+ this.acceptMessage(key, operation);
340
+ return operation;
341
+ }
342
+ get active() {
343
+ return this.pendingMessages.size > 0 || this.activeRuns.size > 0 || this.settlements.size > 0;
344
+ }
345
+ current(conversationId) {
346
+ return this.activeRuns.get(conversationId);
347
+ }
348
+ settlement(conversationId) {
349
+ return this.settlements.get(conversationId);
350
+ }
351
+ pending(key) {
352
+ return this.pendingMessages.get(key);
353
+ }
354
+ hasPending(conversationId) {
355
+ const prefix = `${conversationId}\0`;
356
+ return [...this.pendingMessages.keys()].some((key) => key.startsWith(prefix));
357
+ }
358
+ busy(conversationId) {
359
+ return (this.activeRuns.has(conversationId) ||
360
+ this.settlements.has(conversationId) ||
361
+ this.hasPending(conversationId));
362
+ }
363
+ acceptMessage(key, operation) {
364
+ this.pendingMessages.set(key, operation);
365
+ void operation.finally(() => this.pendingMessages.delete(key)).catch(() => undefined);
366
+ }
367
+ async waitForAdmissions(conversationId) {
368
+ const prefix = `${conversationId}\0`;
369
+ const pending = [...this.pendingMessages.entries()]
370
+ .filter(([key]) => key.startsWith(prefix))
371
+ .map(([, operation]) => operation);
372
+ await Promise.allSettled(pending);
373
+ }
374
+ accept(conversationId, handle) {
375
+ this.activeRuns.set(conversationId, handle);
376
+ }
377
+ trackSettlement(conversationId, settlement) {
378
+ this.settlements.set(conversationId, settlement);
379
+ }
380
+ beginNativeAdmission(handle, barrier) {
381
+ this.nativeAdmissions.set(handle, barrier);
382
+ }
383
+ endNativeAdmission(handle) {
384
+ this.nativeAdmissions.delete(handle);
385
+ }
386
+ waitForNativeAdmission(handle) {
387
+ return this.nativeAdmissions.get(handle);
388
+ }
389
+ release(conversationId, handle, settlement) {
390
+ this.clearInterruption(conversationId, handle.run.id);
391
+ if (this.settlements.get(conversationId) === settlement)
392
+ this.settlements.delete(conversationId);
393
+ if (this.activeRuns.get(conversationId) === handle)
394
+ this.activeRuns.delete(conversationId);
395
+ }
396
+ markInterruption(conversationId, runId) {
397
+ this.expectedInterruptions.add(`${conversationId}\0${runId}`);
398
+ }
399
+ clearInterruption(conversationId, runId) {
400
+ this.expectedInterruptions.delete(`${conversationId}\0${runId}`);
401
+ }
402
+ expectsInterruption(conversationId, runId) {
403
+ return this.expectedInterruptions.has(`${conversationId}\0${runId}`);
404
+ }
405
+ interruptActiveRun(userId, conversation, handle) {
406
+ this.markInterruption(conversation.id, handle.run.id);
407
+ try {
408
+ const interrupted = handle.cancel();
409
+ if (!interrupted) {
410
+ this.clearInterruption(conversation.id, handle.run.id);
411
+ return false;
412
+ }
413
+ this.publishActivity(userId, conversation, 'CANCELLING', this.options.now());
414
+ return true;
415
+ }
416
+ catch (error) {
417
+ this.clearInterruption(conversation.id, handle.run.id);
418
+ throw error;
419
+ }
420
+ }
421
+ cancelAll(cancelled, errors) {
422
+ for (const [conversationId, handle] of this.activeRuns) {
423
+ if (cancelled.has(handle))
424
+ continue;
425
+ cancelled.add(handle);
426
+ this.markInterruption(conversationId, handle.run.id);
427
+ try {
428
+ if (!handle.cancel('shutdown'))
429
+ this.clearInterruption(conversationId, handle.run.id);
430
+ }
431
+ catch (error) {
432
+ this.clearInterruption(conversationId, handle.run.id);
433
+ errors.push(error);
434
+ }
435
+ }
436
+ }
437
+ async close() {
438
+ const cancelled = new Set();
439
+ const errors = [];
440
+ this.cancelAll(cancelled, errors);
441
+ await Promise.allSettled(this.pendingMessages.values());
442
+ this.cancelAll(cancelled, errors);
443
+ await Promise.allSettled(this.settlements.values());
444
+ if (errors.length > 0)
445
+ throw errors[0];
446
+ }
447
+ async finishRun(userId, conversationId, clientMessageId, run, completion) {
448
+ const expectedInterruption = this.expectsInterruption(conversationId, run.id);
449
+ let conversation;
450
+ try {
451
+ this.options.store.finishRunMessages(conversationId, run.id, completion.status, completion.completedAt);
452
+ try {
453
+ const files = await this.options.store.reconcileFiles(userId, conversationId);
454
+ this.options.hostedWatch.publishEvent(conversationId, {
455
+ conversationId,
456
+ kind: 'files',
457
+ files,
458
+ error: null
459
+ });
460
+ }
461
+ catch (error) {
462
+ this.options.hostedWatch.publishEvent(conversationId, {
463
+ conversationId,
464
+ kind: 'files',
465
+ files: this.options.store.listFiles(userId, conversationId),
466
+ error: {
467
+ code: safeErrorCode(error, 'HOSTED_CONVERSATION_FILE_RECONCILE_FAILED'),
468
+ message: 'Hosted conversation file projection failed.'
469
+ }
470
+ });
471
+ }
472
+ this.options.hostedWatch.publishEvent(conversationId, {
473
+ conversationId,
474
+ kind: 'run',
475
+ run: {
476
+ id: run.id,
477
+ status: completion.status,
478
+ startedAt: run.startedAt,
479
+ completedAt: completion.completedAt
480
+ }
481
+ });
482
+ void this.options.hostedWatch.refreshReadySnapshots(conversationId, run.id);
483
+ await this.options.store.advanceLastActivity(conversationId, completion.completedAt);
484
+ await this.applyAutomaticTitle(conversationId, completion.title, completion.completedAt).catch(() => undefined);
485
+ conversation = this.options.store.requireConversation(userId, conversationId);
486
+ }
487
+ finally {
488
+ this.clearInterruption(conversationId, run.id);
489
+ if (conversation === undefined)
490
+ try {
491
+ conversation = this.options.store.requireConversation(userId, conversationId);
492
+ }
493
+ catch {
494
+ // The conversation may have been deleted while the Run was settling.
495
+ }
496
+ if (conversation !== undefined)
497
+ this.publishActivity(userId, conversation, 'IDLE', completion.completedAt);
498
+ }
499
+ if (conversation !== undefined &&
500
+ (completion.status !== 'INTERRUPTED' || !expectedInterruption))
501
+ this.publishTerminalNotification(userId, conversation, run.id, completion);
502
+ }
503
+ async applyAutomaticTitle(conversationId, value, updatedAt) {
504
+ const title = automaticHostedConversationTitle(value);
505
+ if (title === undefined)
506
+ return;
507
+ const saved = await this.options.store.saveAutomaticTitle(conversationId, title, updatedAt);
508
+ if (saved === undefined)
509
+ return;
510
+ this.options.hostedWatch.publishEvent(conversationId, {
511
+ conversationId,
512
+ kind: 'conversation',
513
+ conversation: this.summary(saved)
514
+ });
515
+ }
516
+ activity(conversationId) {
517
+ return (this.activities.get(conversationId) ?? {
518
+ status: 'IDLE',
519
+ version: 0,
520
+ updatedAt: 0
521
+ });
522
+ }
523
+ summary(conversation) {
524
+ return hostedConversationSummary(conversation, this.activity(conversation.id));
525
+ }
526
+ publishActivity(userId, conversation, status, updatedAt) {
527
+ const current = this.activity(conversation.id);
528
+ const activity = {
529
+ status,
530
+ version: current.version + 1,
531
+ updatedAt
532
+ };
533
+ this.activities.set(conversation.id, activity);
534
+ this.options.publishWorkbenchEvent({
535
+ kind: 'activity',
536
+ userId,
537
+ templateId: conversation.templateId,
538
+ conversationId: conversation.id,
539
+ activity
540
+ });
541
+ }
542
+ publishTerminalNotification(userId, conversation, runId, completion) {
543
+ this.options.publishWorkbenchEvent({
544
+ kind: 'notification',
545
+ event: {
546
+ runId,
547
+ sequence: 1,
548
+ eventType: 'notification',
549
+ payload: {
550
+ targetType: 'HOSTED_CONVERSATION',
551
+ userId,
552
+ templateId: conversation.templateId,
553
+ conversationId: conversation.id,
554
+ conversationTitle: conversation.title,
555
+ runId,
556
+ notificationId: `run:${this.options.nodeId()}:${conversation.id}:${runId}`,
557
+ kind: 'RUN_TERMINAL',
558
+ outcome: completion.status,
559
+ occurredAt: completion.completedAt
560
+ },
561
+ createdAt: completion.completedAt
562
+ }
563
+ });
564
+ }
565
+ }
566
+ function hostedConversationSummary(manifest, activity) {
567
+ return hostedConversationSummarySchema.parse({
568
+ id: manifest.id,
569
+ nodeId: manifest.nodeId,
570
+ sessionId: manifest.sessionId,
571
+ templateId: manifest.templateId,
572
+ templateName: manifest.templateName,
573
+ activeVersionId: manifest.activeVersionId,
574
+ title: manifest.title,
575
+ titleSource: manifest.titleSource,
576
+ pinnedAt: manifest.pinnedAt,
577
+ status: manifest.status,
578
+ modelId: manifest.modelId,
579
+ createdAt: manifest.createdAt,
580
+ updatedAt: manifest.updatedAt,
581
+ lastActivityAt: manifest.lastActivityAt,
582
+ ...(activity === undefined ? {} : { activity })
583
+ });
584
+ }
585
+ function automaticHostedConversationTitle(value) {
586
+ if (value === undefined)
587
+ return undefined;
588
+ const title = sessionTitleFromFirstMessage(value);
589
+ if (title.length === 0 ||
590
+ /^(?:new|untitled) session(?:\s*[-–—:]\s*\d{4}-\d{2}-\d{2}.*)?$/iu.test(title))
591
+ return undefined;
592
+ return title;
593
+ }
594
+ export function attachmentPrompt(prompt, attachments) {
595
+ if (attachments.length === 0)
596
+ return prompt;
597
+ const suffix = attachments.map((file) => `- /workspace/inbox/${file.relativePath}`).join('\n');
598
+ return `${prompt}${prompt.trim().length === 0 ? '' : '\n\n'}Attachments:\n${suffix}`;
599
+ }