@myagentroam/node 0.11.36 → 0.11.38

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 (145) 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 -3
  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 +145 -253
  15. package/dist/control-outbound-scheduler.js +3 -0
  16. package/dist/database.js +34 -14
  17. package/dist/git-askpass.js +1 -1
  18. package/dist/git-execution-helper.js +1 -1
  19. package/dist/hosting/hosted-conversation-file-identity.js +27 -0
  20. package/dist/hosting/hosted-conversation-file-operations.js +44 -0
  21. package/dist/hosting/hosted-conversation-node-service.js +974 -0
  22. package/dist/hosting/hosted-conversation-runs.js +599 -0
  23. package/dist/hosting/hosted-conversation-templates.js +280 -0
  24. package/dist/hosting/hosted-conversation-uploads.js +344 -0
  25. package/dist/hosting/hosted-conversation-watch.js +194 -0
  26. package/dist/hosting/runtime-contracts.js +1 -0
  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/runtime-model-factory.js +0 -3
  47. package/dist/runner/mar-agent-runner.js +1 -1
  48. package/dist/runner/normalization.js +42 -0
  49. package/dist/runner/opencode/conversation-parser.js +1 -1
  50. package/dist/runner/opencode/managed-run-controller.js +6 -7
  51. package/dist/{opencode-server.js → runner/opencode/opencode-server.js} +2 -2
  52. package/dist/runner/opencode/tool-normalizer.js +1 -1
  53. package/dist/runner/opencode-runner.js +4 -5
  54. package/dist/runner/runner-registry.js +4 -1
  55. package/dist/runtime-state.js +48 -372
  56. package/dist/service/computer-use-enablement-service.js +1 -1
  57. package/dist/service/continuous-run-coordinator.js +52 -7
  58. package/dist/service/conversation-history-service.js +4 -4
  59. package/dist/service/conversation-hosting-capability-service.js +1 -1
  60. package/dist/service/conversation-hosting-enablement-service.js +1 -1
  61. package/dist/service/conversation-hosting-lifecycle-service.js +3 -3
  62. package/dist/service/conversation-hosting-maintenance-service.js +1 -1
  63. package/dist/service/conversation-observation.js +1 -1
  64. package/dist/service/conversation-segment-service.js +1 -1
  65. package/dist/service/external-session-resume-service.js +47 -27
  66. package/dist/service/fake-managed-run-service.js +3 -7
  67. package/dist/service/git-operation-service.js +1 -1
  68. package/dist/service/goal-continuation-coordinator.js +14 -7
  69. package/dist/service/mar-agent-model-transport-test-service.js +1 -1
  70. package/dist/service/native-session-watch-service.js +1 -1
  71. package/dist/service/node-connection-lifecycle-service.js +6 -3
  72. package/dist/service/node-control-channel.js +48 -11
  73. package/dist/service/node-request-service.js +1 -1
  74. package/dist/service/node-terminal-channel.js +2 -1
  75. package/dist/service/node-workspace-coordinator.js +3 -2
  76. package/dist/service/notification-mcp-bridge.js +6 -2
  77. package/dist/service/recent-run-projection-service.js +1 -1
  78. package/dist/service/restart-session-restorer.js +1 -1
  79. package/dist/service/run-event-service.js +49 -42
  80. package/dist/service/run-workbench-service.js +3 -3
  81. package/dist/service/runner-interaction-service.js +6 -5
  82. package/dist/service/runner-service.js +1 -1
  83. package/dist/service/session-recent-activity.js +2 -1
  84. package/dist/service/skill-directory-service.js +1 -1
  85. package/dist/service/skill-install-service.js +2 -2
  86. package/dist/service/terminal-service.js +1 -1
  87. package/dist/service/workbench-event-projection.js +3 -2
  88. package/dist/service/workbench-event-service.js +1 -1
  89. package/dist/service/workspace-service.js +3 -2
  90. package/dist/session/attachments.js +110 -0
  91. package/dist/session/contracts.js +1 -0
  92. package/dist/{util → session}/conversation-identity.js +1 -1
  93. package/dist/session/input-validation.js +15 -0
  94. package/dist/{service → session}/native-session-projection-service.js +40 -11
  95. package/dist/session/pagination.js +77 -0
  96. package/dist/{util → session}/runner-native-session-parsers.js +2 -1
  97. package/dist/session/runtime/approvals.js +90 -0
  98. package/dist/session/runtime/conversation-projection.js +310 -0
  99. package/dist/session/runtime/type-guards.js +19 -0
  100. package/dist/{service → session}/session-catalog-service.js +5 -4
  101. package/dist/{service → session}/session-command-service.js +3 -3
  102. package/dist/{service → session}/session-identity-service.js +1 -1
  103. package/dist/{service → session}/session-lifecycle-service.js +18 -7
  104. package/dist/{service → session}/session-message-service.js +6 -5
  105. package/dist/{service → session}/session-presentation-service.js +1 -1
  106. package/dist/{service → session}/session-query-service.js +2 -2
  107. package/dist/{service → session}/session-schedule-service.js +2 -2
  108. package/dist/util/records.js +3 -0
  109. package/dist/workspace/contracts.js +1 -0
  110. package/dist/workspace/file-index.js +534 -0
  111. package/dist/workspace/files.js +466 -0
  112. package/dist/workspace/git.js +991 -0
  113. package/dist/workspace/path-access.js +184 -0
  114. package/dist/workspace/utf8.js +12 -0
  115. package/dist/{service → workspace}/workspace-change-service.js +45 -21
  116. package/dist/{service → workspace}/workspace-code-navigation-service.js +6 -6
  117. package/dist/{service → workspace}/workspace-file-manager-service.js +1 -1
  118. package/dist/{service → workspace}/workspace-file-service.js +16 -16
  119. package/dist/{service → workspace}/workspace-git-exclude-service.js +1 -1
  120. package/dist/{service → workspace}/workspace-queue-workbench-service.js +3 -2
  121. package/dist/{service → workspace}/workspace-session-service.js +45 -40
  122. package/dist/{service → workspace}/workspace-upload-service.js +28 -28
  123. package/dist/{service → workspace}/workspace-watch-service.js +29 -28
  124. package/dist/{service → workspace}/workspace-workbench-service.js +4 -3
  125. package/package.json +4 -4
  126. package/dist/service/hosted-conversation-node-service.js +0 -2252
  127. package/dist/service/run-domain-state.js +0 -4
  128. package/dist/service/session-domain-state.js +0 -9
  129. package/dist/service/workspace-domain-state.js +0 -41
  130. package/dist/util/node-operation-parsers.js +0 -320
  131. package/dist/workspace.js +0 -2168
  132. /package/dist/{connector → application}/node-connector-options.js +0 -0
  133. /package/dist/{util → connection}/node-operation-data.js +0 -0
  134. /package/dist/{connector → connection}/node-operation-router.js +0 -0
  135. /package/dist/{service → hosting}/hosted-conversation-sandbox-profile.js +0 -0
  136. /package/dist/{service → hosting}/hosted-conversation-store.js +0 -0
  137. /package/dist/{util → mcp}/secret-environment.js +0 -0
  138. /package/dist/{claude-channel.js → runner/claude/claude-channel.js} +0 -0
  139. /package/dist/{util → runner}/personal-instructions.js +0 -0
  140. /package/dist/{util → runner}/runner-error.js +0 -0
  141. /package/dist/{service/conversation-work-process.js → session/runtime/work-process.js} +0 -0
  142. /package/dist/{service → session}/session-context-service.js +0 -0
  143. /package/dist/{util → workspace}/git-credential-client.js +0 -0
  144. /package/dist/{service → workspace}/workspace-content-search-service.js +0 -0
  145. /package/dist/{service → workspace}/workspace-watch-snapshot-service.js +0 -0
@@ -0,0 +1,974 @@
1
+ import { HostedConversationTemplates, allowedModels, selectModel, networkModels, resolveMcpServers, runtimeModels, systemInstruction } from './hosted-conversation-templates.js';
2
+ import { openRegularFile, regularFileMetadata, assertRegularFileMetadata } from './hosted-conversation-file-identity.js';
3
+ import { HostedConversationUploads } from './hosted-conversation-uploads.js';
4
+ import { HostedConversationRuns } from './hosted-conversation-runs.js';
5
+ import { HostedConversationFileOperations } from './hosted-conversation-file-operations.js';
6
+ import { resolveSessionRollbackTarget } from '../service/session-rollback-target.js';
7
+ import { imagePreviews, MAX_IMAGE_PREVIEW_SOURCE_BYTES } from '../service/image-preview-service.js';
8
+ import { modelUsageGrantSchema } from '@myagentroam/protocol';
9
+ import { randomUUID } from 'node:crypto';
10
+ import { rm } from 'node:fs/promises';
11
+ import { toolResultResourceInputSchema, toolResultResourcePageSchema, conversationTurnSchema, conversationTemplateSummarySchema, hostedConversationConfigUpdateInputSchema, hostedConversationTemplateSwitchInputSchema, hostedConversationTemplateSwitchResultSchema, hostedConversationCreateInputSchema, hostedConversationDeleteResultSchema, hostedConversationFileDeleteInputSchema, hostedConversationFileDeleteResultSchema, hostedConversationFileReadInputSchema, hostedConversationFileReadResultSchema, hostedConversationFilesSchema, hostedConversationHistoryInputSchema, hostedConversationHistoryPageSchema, hostedConversationIdInputSchema, hostedConversationImageReadInputSchema, hostedConversationListInputSchema, hostedConversationListPageSchema, hostedConversationMessageInputSchema, hostedConversationMetadataUpdateInputSchema, hostedConversationModelOptionsInputSchema, hostedConversationModelOptionsSchema, hostedConversationRollbackInputSchema, hostedConversationSnapshotSchema, hostedConversationUnwatchInputSchema, hostedConversationUploadChunkInputSchema, hostedConversationUploadControlInputSchema, hostedConversationUploadCreateInputSchema, hostedConversationWatchInputSchema, resolveMarAgentDefaultReasoningEffort } from '@myagentroam/protocol';
12
+ import { hostedConversationImageRunId } from '../runner/mar-agent/generated-images.js';
13
+ import { SessionWatchSlotCoordinator } from '../service/session-watch-slot-coordinator.js';
14
+ import { HostedConversationWatch } from './hosted-conversation-watch.js';
15
+ const DEFAULT_HOSTED_CONVERSATION_TITLE = 'New conversation';
16
+ const MAX_FILE_READ_BYTES = 512 * 1024;
17
+ /** Implements the Node-owned hosted-conversation domain over an injected MAR Agent runtime. */
18
+ export class HostedConversationNodeService {
19
+ options;
20
+ now;
21
+ id;
22
+ pendingCreates = new Map();
23
+ runs;
24
+ templates;
25
+ fileOperations = new HostedConversationFileOperations();
26
+ hostedUploads;
27
+ closing;
28
+ cleanupLocks = new Set();
29
+ historyMutations = new Map();
30
+ historyVersions = new Map();
31
+ disablePrepared = false;
32
+ workbenchSequence = 0;
33
+ hostedWatch;
34
+ constructor(options) {
35
+ this.options = options;
36
+ this.now = options.now ?? Date.now;
37
+ this.id = options.id ?? randomUUID;
38
+ this.templates = new HostedConversationTemplates(options.database, options.store, options.nodeId, options.processExecutorFactory);
39
+ this.hostedUploads = new HostedConversationUploads({
40
+ store: options.store,
41
+ request: hostedRequest,
42
+ requireWritableConversation: (userId, id) => this.requireWritableConversation(userId, id),
43
+ loadTemplateVersion: (versionId, templateId) => this.templates.loadTemplateVersion(versionId, templateId),
44
+ publish: (id, payload) => this.hostedWatch.publishEvent(id, payload)
45
+ }, this.now, this.id);
46
+ this.hostedWatch = new HostedConversationWatch({
47
+ slots: options.sessionWatchSlots ?? new SessionWatchSlotCoordinator(),
48
+ nodeId: options.nodeId,
49
+ nodeGeneration: options.nodeGeneration,
50
+ emit: options.emitWatchEvent,
51
+ nextEvent: (payload) => this.makeWorkbenchEvent(payload),
52
+ initial: (lease) => this.get({
53
+ conversationId: lease.conversationId,
54
+ __requestUserId: lease.userId,
55
+ __sourceTemplateId: lease.templateId,
56
+ __marAgentModels: lease.models
57
+ }),
58
+ snapshot: (userId, conversationId, models) => this.snapshot(userId, conversationId, models),
59
+ mayRefresh: (conversationId, completedRunId) => {
60
+ const activeRunId = this.runs.current(conversationId)?.run.id;
61
+ return !((activeRunId !== undefined && activeRunId !== completedRunId) ||
62
+ (activeRunId === undefined && this.runs.activity(conversationId).status !== 'IDLE'));
63
+ }
64
+ });
65
+ this.runs = new HostedConversationRuns({
66
+ store: options.store,
67
+ hostedWatch: this.hostedWatch,
68
+ now: this.now,
69
+ nodeId: options.nodeId,
70
+ publishWorkbenchEvent: (payload) => this.publishWorkbenchEvent(payload),
71
+ uploads: this.hostedUploads,
72
+ runtime: options.runtime,
73
+ templates: this.templates,
74
+ secretEnvironment: options.secretEnvironment,
75
+ modelUsage: options.modelUsage
76
+ });
77
+ }
78
+ operations() {
79
+ return {
80
+ 'conversation.hosted.list': (raw) => this.list(raw),
81
+ 'conversation.hosted.model-options': (raw) => this.modelOptions(raw),
82
+ 'conversation.hosted.create': (raw) => this.create(raw),
83
+ 'conversation.hosted.get': (raw) => this.get(raw),
84
+ 'conversation.hosted.history': (raw) => this.history(raw),
85
+ 'conversation.hosted.watch': (raw) => this.watch(raw),
86
+ 'conversation.hosted.unwatch': (raw) => this.unwatch(raw),
87
+ 'conversation.hosted.message': (raw) => this.message(raw),
88
+ 'conversation.hosted.rollback': (raw) => this.rollback(raw),
89
+ 'conversation.hosted.interrupt': (raw) => this.interrupt(raw),
90
+ 'conversation.hosted.config.update': (raw) => this.updateConfiguration(raw),
91
+ 'conversation.hosted.template.switch': (raw) => this.switchTemplate(raw),
92
+ 'conversation.hosted.metadata.update': (raw) => this.updateMetadata(raw),
93
+ 'conversation.hosted.image.read': (raw) => this.readImage(raw),
94
+ 'conversation.hosted.tool-resource.read': (raw) => this.readToolResource(raw),
95
+ 'conversation.hosted.files.list': (raw) => this.listFiles(raw),
96
+ 'conversation.hosted.file.read': (raw) => this.readFile(raw),
97
+ 'conversation.hosted.file.delete': (raw) => this.fileWrite(raw, hostedConversationFileDeleteInputSchema, () => this.deleteFile(raw)),
98
+ 'conversation.hosted.files.upload.preflight': (raw) => this.hostedUploads.preflightUpload(raw),
99
+ 'conversation.hosted.files.upload.create': (raw) => this.fileWrite(raw, hostedConversationUploadCreateInputSchema, () => this.hostedUploads.createUpload(raw)),
100
+ 'conversation.hosted.files.upload.chunk': (raw) => this.fileWrite(raw, hostedConversationUploadChunkInputSchema, () => this.hostedUploads.writeUploadChunk(raw)),
101
+ 'conversation.hosted.files.upload.status': (raw) => this.hostedUploads.uploadStatus(raw),
102
+ 'conversation.hosted.files.upload.complete': (raw) => this.fileWrite(raw, hostedConversationUploadControlInputSchema, () => this.hostedUploads.completeUpload(raw)),
103
+ 'conversation.hosted.files.upload.cancel': (raw) => this.fileWrite(raw, hostedConversationUploadControlInputSchema, () => this.hostedUploads.cancelUpload(raw)),
104
+ 'conversation.hosted.delete': (raw) => this.delete(raw)
105
+ };
106
+ }
107
+ /**
108
+ * Closes Run admission while a management operation verifies that it can
109
+ * release the hosting runtime without interrupting an existing execution.
110
+ */
111
+ prepareForDisable() {
112
+ if (this.disablePrepared || this.runs.active || this.historyMutations.size > 0)
113
+ return undefined;
114
+ this.disablePrepared = true;
115
+ return () => {
116
+ this.disablePrepared = false;
117
+ };
118
+ }
119
+ list(raw) {
120
+ const payload = hostedRequest(raw);
121
+ const input = hostedConversationListInputSchema.parse(payload.data);
122
+ const page = this.options.store.listConversations({
123
+ userId: payload.userId,
124
+ limit: input.limit,
125
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor })
126
+ });
127
+ return hostedConversationListPageSchema.parse({
128
+ ...page,
129
+ items: page.items.map((conversation) => ({
130
+ ...conversation,
131
+ activity: this.runs.activity(conversation.id)
132
+ }))
133
+ });
134
+ }
135
+ async modelOptions(raw) {
136
+ if (this.disablePrepared)
137
+ throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
138
+ const payload = hostedRequest(raw);
139
+ const input = hostedConversationModelOptionsInputSchema.parse(payload.data);
140
+ const template = conversationTemplateSummarySchema.parse(payload.template);
141
+ if (template.id !== input.templateId ||
142
+ template.nodeId !== this.options.nodeId() ||
143
+ !template.available ||
144
+ template.status !== 'ACTIVE' ||
145
+ template.currentVersionId === null)
146
+ throw new Error('HOSTED_CONVERSATION_TEMPLATE_UNAVAILABLE');
147
+ const version = await this.templates.loadTemplateVersion(template.currentVersionId, template.id);
148
+ const models = allowedModels(version, runtimeModels(payload.models)).filter((model) => model.conversationModel !== false);
149
+ const selected = models.length === 0 ? undefined : selectModel(version, models, version.modelPolicy.defaultId);
150
+ return hostedConversationModelOptionsSchema.parse({
151
+ models: networkModels(models, version.draft.networkMode).map(modelSummary),
152
+ defaultModelId: selected?.id ?? null
153
+ });
154
+ }
155
+ create(raw) {
156
+ if (this.disablePrepared)
157
+ return Promise.reject(new Error('CONVERSATION_HOSTING_UNAVAILABLE'));
158
+ const payload = hostedRequest(raw);
159
+ const input = hostedConversationCreateInputSchema.parse(payload.data);
160
+ const template = conversationTemplateSummarySchema.parse(payload.template);
161
+ const models = runtimeModels(payload.models);
162
+ if (template.id !== input.templateId ||
163
+ template.nodeId !== this.options.nodeId() ||
164
+ !template.available ||
165
+ template.status !== 'ACTIVE' ||
166
+ template.currentVersionId === null)
167
+ throw new Error('HOSTED_CONVERSATION_TEMPLATE_UNAVAILABLE');
168
+ const key = `${payload.userId}\0${input.clientRequestId}`;
169
+ const pending = this.pendingCreates.get(key);
170
+ if (pending !== undefined)
171
+ return pending;
172
+ const existing = this.options.store.findConversationByClientRequest(payload.userId, input.clientRequestId);
173
+ if (existing !== undefined) {
174
+ if (existing.templateId !== input.templateId)
175
+ throw new Error('HOSTED_CONVERSATION_TEMPLATE_CONFLICT');
176
+ return this.snapshot(payload.userId, existing.id, models);
177
+ }
178
+ const operation = this.createConversation(payload.userId, input.clientRequestId, template, models, input.modelId, payload.runtimeScopeId);
179
+ this.pendingCreates.set(key, operation);
180
+ void operation.finally(() => this.pendingCreates.delete(key)).catch(() => undefined);
181
+ return operation;
182
+ }
183
+ async createConversation(userId, clientRequestId, template, models, requestedModelId, runtimeScopeId) {
184
+ await this.options.assertCanCreate?.();
185
+ const version = await this.templates.loadTemplateVersion(template.currentVersionId, template.id);
186
+ if (requestedModelId !== undefined &&
187
+ !allowedModels(version, models).some((model) => model.id === requestedModelId && model.conversationModel !== false))
188
+ throw new Error('HOSTED_CONVERSATION_MODEL_NOT_AVAILABLE');
189
+ const selected = selectModel(version, models, requestedModelId ?? version.modelPolicy.defaultId);
190
+ const conversationId = this.id();
191
+ const prepared = await this.options.store.prepareConversationEnvironment(userId, conversationId);
192
+ const pathMounts = this.templates.pathMounts(prepared.paths, version);
193
+ const processExecutor = this.templates.processExecutor(prepared.paths, version);
194
+ let sessionId;
195
+ try {
196
+ const session = await this.options.runtime.createSession({
197
+ conversationId,
198
+ homeDirectory: prepared.paths.homeDirectory,
199
+ workspaceDirectory: version.templateDirectory,
200
+ systemInstruction: systemInstruction(version),
201
+ defaultModelId: selected.id,
202
+ models: networkModels(allowedModels(version, models), version.draft.networkMode),
203
+ ...(runtimeScopeId === undefined ? {} : { runtimeScopeId }),
204
+ pathMounts,
205
+ processExecutor
206
+ });
207
+ sessionId = session.sessionId;
208
+ const createdAt = this.now();
209
+ const manifest = {
210
+ id: conversationId,
211
+ nodeId: this.options.nodeId(),
212
+ sessionId,
213
+ templateId: template.id,
214
+ templateName: template.name,
215
+ activeVersionId: version.id,
216
+ title: DEFAULT_HOSTED_CONVERSATION_TITLE,
217
+ titleSource: 'DEFAULT',
218
+ pinnedAt: null,
219
+ status: 'ACTIVE',
220
+ modelId: selected.id,
221
+ createdAt,
222
+ updatedAt: createdAt,
223
+ lastActivityAt: createdAt,
224
+ userStorageKey: prepared.userStorageKey,
225
+ conversationStorageKey: prepared.conversationStorageKey,
226
+ clientRequestId,
227
+ templateDeleted: false,
228
+ quota: {
229
+ usedBytes: 0,
230
+ maxBytes: version.draft.quotas.maxStorageBytes
231
+ }
232
+ };
233
+ const persisted = await this.options.store.createConversation(manifest);
234
+ if (persisted.id !== conversationId) {
235
+ await this.options.runtime
236
+ .releaseSession({
237
+ conversationId,
238
+ sessionId,
239
+ homeDirectory: prepared.paths.homeDirectory
240
+ })
241
+ .catch(() => undefined);
242
+ await rm(prepared.paths.conversationDirectory, { recursive: true, force: true });
243
+ }
244
+ return this.snapshot(userId, persisted.id, models);
245
+ }
246
+ catch (error) {
247
+ if (sessionId !== undefined)
248
+ await this.options.runtime
249
+ .releaseSession({
250
+ conversationId,
251
+ sessionId,
252
+ homeDirectory: prepared.paths.homeDirectory
253
+ })
254
+ .catch(() => undefined);
255
+ await rm(prepared.paths.conversationDirectory, { recursive: true, force: true }).catch(() => undefined);
256
+ throw error;
257
+ }
258
+ }
259
+ async get(raw) {
260
+ const payload = hostedRequest(raw);
261
+ const input = hostedConversationIdInputSchema.parse(payload.data);
262
+ this.assertTemplateContext(payload, this.options.store.requireConversation(payload.userId, input.conversationId));
263
+ return this.snapshot(payload.userId, input.conversationId, runtimeModels(payload.models), payload.templateDeleted, false, undefined, payload.sourceTemplateId);
264
+ }
265
+ async history(raw) {
266
+ const payload = hostedRequest(raw);
267
+ const input = hostedConversationHistoryInputSchema.parse(payload.data);
268
+ const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
269
+ const paths = this.options.store.pathsForConversation(payload.userId, conversation.id);
270
+ const page = await this.options.runtime.history({
271
+ conversationId: conversation.id,
272
+ sessionId: conversation.sessionId,
273
+ homeDirectory: paths.homeDirectory,
274
+ modelId: conversation.modelId,
275
+ limit: input.limit,
276
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor })
277
+ });
278
+ return hostedConversationHistoryPageSchema.parse(page);
279
+ }
280
+ async snapshot(userId, conversationId, availableModels, templateDeleted = false, includeActiveHistoryMutation = false, historyOverride, sourceTemplateId) {
281
+ if (includeActiveHistoryMutation)
282
+ return this.readSnapshot(userId, conversationId, availableModels, templateDeleted, historyOverride, sourceTemplateId);
283
+ while (true) {
284
+ const version = this.historyVersions.get(conversationId) ?? 0;
285
+ const mutation = this.historyMutations.get(conversationId);
286
+ if (mutation !== undefined) {
287
+ await mutation.catch(() => undefined);
288
+ continue;
289
+ }
290
+ const snapshot = await this.readSnapshot(userId, conversationId, availableModels, templateDeleted, undefined, sourceTemplateId);
291
+ if (!this.historyMutations.has(conversationId) &&
292
+ (this.historyVersions.get(conversationId) ?? 0) === version)
293
+ return snapshot;
294
+ }
295
+ }
296
+ async readSnapshot(userId, conversationId, availableModels, templateDeleted, historyOverride, sourceTemplateId) {
297
+ let conversation = this.options.store.requireConversation(userId, conversationId);
298
+ if (sourceTemplateId !== undefined && conversation.templateId !== sourceTemplateId)
299
+ throw new Error('HOSTED_CONVERSATION_TEMPLATE_CONFLICT');
300
+ const templateId = conversation.templateId;
301
+ const activeVersionId = conversation.activeVersionId;
302
+ if (templateDeleted)
303
+ conversation = await this.options.store.retainConversationTemplate(userId, conversationId, this.now(), sourceTemplateId ?? templateId);
304
+ const latestVersionId = conversation.templateDeleted
305
+ ? undefined
306
+ : this.templates.latestAvailableTemplateVersionId(conversation.templateId);
307
+ const displayVersionId = latestVersionId ?? conversation.activeVersionId;
308
+ const version = await this.templates.loadTemplateVersion(displayVersionId, conversation.templateId);
309
+ const models = networkModels(allowedModels(version, availableModels), version.draft.networkMode).filter((model) => model.conversationModel !== false);
310
+ if (models.length > 0 &&
311
+ availableModels.find((model) => model.id === conversation.modelId)?.conversationModel !==
312
+ false) {
313
+ const configuredModelId = conversation.modelId;
314
+ const selected = selectModel(version, availableModels, conversation.modelId);
315
+ if (selected.id !== conversation.modelId)
316
+ conversation = await this.options.store.updateConversation(conversation.id, (current) => current.templateId === templateId &&
317
+ current.activeVersionId === activeVersionId &&
318
+ current.modelId === configuredModelId
319
+ ? { ...current, modelId: selected.id }
320
+ : current);
321
+ }
322
+ const paths = this.options.store.pathsForConversation(userId, conversation.id);
323
+ const history = historyOverride ??
324
+ (await this.options.runtime.history({
325
+ conversationId: conversation.id,
326
+ sessionId: conversation.sessionId,
327
+ homeDirectory: paths.homeDirectory,
328
+ modelId: conversation.modelId,
329
+ limit: 10
330
+ }));
331
+ const current = this.options.store.requireConversation(userId, conversationId);
332
+ if (current.templateId !== templateId || current.activeVersionId !== activeVersionId)
333
+ throw new Error('HOSTED_CONVERSATION_TEMPLATE_CONFLICT');
334
+ return hostedConversationSnapshotSchema.parse({
335
+ nodeAppVersion: this.options.nodeAppVersion?.() ?? 'unknown',
336
+ conversation: this.runs.summary(conversation),
337
+ template: templateSummary(conversation, version.draft),
338
+ run: this.runs.current(conversation.id)?.run ?? null,
339
+ turns: history.turns,
340
+ historyNextCursor: history.nextCursor,
341
+ files: this.options.store.listFiles(userId, conversation.id),
342
+ models: models.map(modelSummary),
343
+ error: null,
344
+ snapshotSequence: 0
345
+ });
346
+ }
347
+ async watch(raw) {
348
+ if (this.closing)
349
+ throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
350
+ const payload = hostedRequest(raw);
351
+ const input = hostedConversationWatchInputSchema.parse(payload.data);
352
+ const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
353
+ this.assertTemplateContext(payload, conversation);
354
+ if (this.cleanupLocks.has(conversation.id))
355
+ throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
356
+ const sessionHash = requireWatchSessionHash(payload.sessionHash);
357
+ return this.hostedWatch.watch(input, sessionHash, payload.userId, conversation.templateId, () => runtimeModels(payload.models));
358
+ }
359
+ unwatch(raw) {
360
+ const payload = hostedRequest(raw);
361
+ const input = hostedConversationUnwatchInputSchema.parse(payload.data);
362
+ this.options.store.requireConversation(payload.userId, input.conversationId);
363
+ return this.hostedWatch.unwatch(input, requireWatchSessionHash(payload.sessionHash), payload.userId);
364
+ }
365
+ publishRuntimeEvent(conversationId, event) {
366
+ return this.hostedWatch.publishEvent(conversationId, {
367
+ conversationId,
368
+ kind: 'runtime',
369
+ runtimeEvent: event
370
+ });
371
+ }
372
+ isConversationInUse(conversationId) {
373
+ return (this.cleanupLocks.has(conversationId) ||
374
+ this.historyMutations.has(conversationId) ||
375
+ this.runs.busy(conversationId) ||
376
+ this.fileOperations.busy(conversationId) ||
377
+ this.hostedWatch.hasConversation(conversationId) ||
378
+ this.hostedUploads.busy(conversationId));
379
+ }
380
+ beginConversationCleanup(conversationId) {
381
+ if (this.isConversationInUse(conversationId))
382
+ return undefined;
383
+ this.cleanupLocks.add(conversationId);
384
+ return () => {
385
+ this.cleanupLocks.delete(conversationId);
386
+ };
387
+ }
388
+ async releaseConversationForCleanup(conversation) {
389
+ const paths = this.options.store.pathsForManifest(conversation);
390
+ await this.options.runtime
391
+ .releaseSession({
392
+ conversationId: conversation.id,
393
+ sessionId: conversation.sessionId,
394
+ homeDirectory: paths.homeDirectory
395
+ })
396
+ .catch(() => undefined);
397
+ }
398
+ dispose() {
399
+ if (this.closing)
400
+ return this.closing;
401
+ this.disablePrepared = true;
402
+ const runs = this.runs.close();
403
+ this.hostedWatch.close();
404
+ this.closing = (async () => {
405
+ const results = await Promise.allSettled([
406
+ runs,
407
+ (async () => {
408
+ await this.fileOperations.close();
409
+ await this.hostedUploads.close();
410
+ })()
411
+ ]);
412
+ const failure = results.find((result) => result.status === 'rejected');
413
+ if (failure)
414
+ throw failure.reason;
415
+ })();
416
+ return this.closing;
417
+ }
418
+ async fileWrite(raw, schema, operation) {
419
+ const payload = hostedRequest(raw);
420
+ const input = schema.parse(payload.data);
421
+ if (this.closing)
422
+ throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
423
+ this.requireWritableConversation(payload.userId, input.conversationId);
424
+ return this.fileOperations.write(input.conversationId, input.uploadId, operation);
425
+ }
426
+ async waitForFileWrites(conversationId) {
427
+ await this.fileOperations.wait(conversationId);
428
+ }
429
+ message(raw) {
430
+ if (this.disablePrepared)
431
+ return Promise.reject(new Error('CONVERSATION_HOSTING_UNAVAILABLE'));
432
+ const payload = hostedRequest(raw);
433
+ const input = hostedConversationMessageInputSchema.parse(payload.data);
434
+ this.assertTemplateContext(payload, this.options.store.requireConversation(payload.userId, input.conversationId));
435
+ if (this.historyMutations.has(input.conversationId))
436
+ return Promise.reject(new Error('HOSTED_CONVERSATION_STATE_INVALID'));
437
+ return this.runs.admitMessage(payload.userId, input.conversationId, input.clientMessageId, () => input.deliveryIntent === 'REPLACE_CURRENT'
438
+ ? this.replaceCurrentMessage(payload, input)
439
+ : this.startMessage(payload, input));
440
+ }
441
+ async replaceCurrentMessage(payload, input) {
442
+ const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
443
+ if (this.cleanupLocks.has(conversation.id) ||
444
+ this.historyMutations.has(conversation.id) ||
445
+ conversation.status !== 'ACTIVE')
446
+ throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
447
+ return this.runs.replaceMessage(payload, input, conversation, (steerPrompt) => this.startMessage(payload, input, steerPrompt));
448
+ }
449
+ async startMessage(payload, input, steerPrompt) {
450
+ if (this.disablePrepared)
451
+ throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
452
+ let conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
453
+ this.assertTemplateContext(payload, conversation);
454
+ if (this.cleanupLocks.has(conversation.id))
455
+ throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
456
+ if (this.historyMutations.has(conversation.id))
457
+ throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
458
+ if (conversation.status !== 'ACTIVE')
459
+ throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
460
+ if (this.runs.current(conversation.id) || this.runs.settlement(conversation.id))
461
+ throw new Error('HOSTED_CONVERSATION_RUN_ACTIVE');
462
+ return this.runs.startMessage(payload, input, conversation, steerPrompt);
463
+ }
464
+ rollback(raw) {
465
+ if (this.disablePrepared)
466
+ return Promise.reject(new Error('CONVERSATION_HOSTING_UNAVAILABLE'));
467
+ const payload = hostedRequest(raw);
468
+ const input = hostedConversationRollbackInputSchema.parse(payload.data);
469
+ const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
470
+ this.assertTemplateContext(payload, conversation);
471
+ if (conversation.status !== 'ACTIVE' ||
472
+ this.cleanupLocks.has(conversation.id) ||
473
+ this.historyMutations.has(conversation.id))
474
+ return Promise.reject(new Error('HOSTED_CONVERSATION_STATE_INVALID'));
475
+ if (this.runs.activity(conversation.id).status !== 'IDLE' || this.runs.busy(conversation.id))
476
+ return Promise.reject(new Error('HOSTED_CONVERSATION_RUN_ACTIVE'));
477
+ const operation = this.rollbackConversation(payload, conversation, input);
478
+ this.historyMutations.set(conversation.id, operation);
479
+ void operation
480
+ .finally(() => {
481
+ if (this.historyMutations.get(conversation.id) === operation)
482
+ this.historyMutations.delete(conversation.id);
483
+ })
484
+ .catch(() => undefined);
485
+ return operation;
486
+ }
487
+ async rollbackConversation(payload, conversation, input) {
488
+ const paths = this.options.store.pathsForConversation(payload.userId, conversation.id);
489
+ const turns = await this.readAllHistory(conversation, paths);
490
+ const targetIndex = turns.findIndex((turn) => turn.id === input.turnId);
491
+ const target = turns[targetIndex];
492
+ const selectedInput = target === undefined ? undefined : resolveSessionRollbackTarget(target, input.itemId);
493
+ if (target === undefined ||
494
+ !terminalHostedTurnStatus(target.status) ||
495
+ selectedInput === undefined ||
496
+ target.rewind?.before !== input.boundary)
497
+ throw new Error('HOSTED_CONVERSATION_ROLLBACK_TARGET_INVALID');
498
+ let rolledBack;
499
+ try {
500
+ rolledBack = await this.options.runtime.rollback({
501
+ conversationId: conversation.id,
502
+ sessionId: conversation.sessionId,
503
+ homeDirectory: paths.homeDirectory,
504
+ modelId: conversation.modelId,
505
+ boundaryTurnId: input.boundary,
506
+ ...(selectedInput.clientMessageId === undefined
507
+ ? {}
508
+ : {
509
+ beforeInput: {
510
+ turnId: target.rewind.after,
511
+ clientMessageId: selectedInput.clientMessageId
512
+ }
513
+ })
514
+ });
515
+ }
516
+ catch (error) {
517
+ if (error instanceof Error && error.message === 'MAR_AGENT_ROLLBACK_TARGET_INVALID')
518
+ throw new Error('HOSTED_CONVERSATION_ROLLBACK_TARGET_INVALID', { cause: error });
519
+ throw error;
520
+ }
521
+ this.historyVersions.set(conversation.id, (this.historyVersions.get(conversation.id) ?? 0) + 1);
522
+ await this.options.store.advanceLastActivity(conversation.id, Math.max(this.now(), rolledBack.lastActivityAt));
523
+ const snapshot = await this.snapshot(payload.userId, conversation.id, runtimeModels(payload.models), payload.templateDeleted, true);
524
+ this.hostedWatch.publishSnapshot(conversation.id, snapshot);
525
+ return snapshot;
526
+ }
527
+ async readAllHistory(conversation, paths) {
528
+ const turns = [];
529
+ let cursor;
530
+ do {
531
+ const page = await this.options.runtime.history({
532
+ conversationId: conversation.id,
533
+ sessionId: conversation.sessionId,
534
+ homeDirectory: paths.homeDirectory,
535
+ modelId: conversation.modelId,
536
+ limit: 100,
537
+ ...(cursor === undefined ? {} : { cursor })
538
+ });
539
+ turns.unshift(...page.turns.map((turn) => conversationTurnSchema.parse(turn)));
540
+ cursor = page.nextCursor ?? undefined;
541
+ } while (cursor !== undefined);
542
+ return turns;
543
+ }
544
+ interrupt(raw) {
545
+ const payload = hostedRequest(raw);
546
+ const input = hostedConversationIdInputSchema.parse(payload.data);
547
+ const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
548
+ const handle = this.runs.current(input.conversationId);
549
+ if (handle === undefined)
550
+ return { interrupted: false };
551
+ return { interrupted: this.runs.interruptActiveRun(payload.userId, conversation, handle) };
552
+ }
553
+ switchTemplate(raw) {
554
+ if (this.closing || this.disablePrepared)
555
+ throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
556
+ const payload = hostedRequest(raw);
557
+ const input = hostedConversationTemplateSwitchInputSchema.parse(payload.data);
558
+ const conversation = this.requireWritableConversation(payload.userId, input.conversationId);
559
+ if (this.fileOperations.busy(conversation.id) || this.hostedUploads.busy(conversation.id))
560
+ throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
561
+ if (this.runs.busy(conversation.id))
562
+ throw new Error('HOSTED_CONVERSATION_RUN_ACTIVE');
563
+ const template = conversationTemplateSummarySchema.parse(payload.template);
564
+ if (template.id !== input.templateId ||
565
+ template.nodeId !== conversation.nodeId ||
566
+ template.status !== 'ACTIVE' ||
567
+ !template.available ||
568
+ template.currentVersionId === null)
569
+ throw new Error('HOSTED_CONVERSATION_TEMPLATE_UNAVAILABLE');
570
+ return this.withConversationChange(conversation.id, async () => {
571
+ const current = this.options.store.requireConversation(payload.userId, conversation.id);
572
+ const unchanged = current.templateId === input.templateId;
573
+ if (!unchanged && current.templateId !== input.expectedTemplateId)
574
+ throw new Error('HOSTED_CONVERSATION_TEMPLATE_CONFLICT');
575
+ const version = await this.templates.loadTemplateVersion(unchanged ? current.activeVersionId : template.currentVersionId, template.id);
576
+ const models = networkModels(allowedModels(version, runtimeModels(payload.models)), version.draft.networkMode).filter((model) => model.conversationModel !== false);
577
+ let saved = current;
578
+ if (!unchanged) {
579
+ const selected = selectModel(version, models, current.modelId);
580
+ resolveMcpServers(version, this.options.secretEnvironment ?? process.env);
581
+ const paths = this.options.store.pathsForConversation(payload.userId, current.id);
582
+ this.templates.pathMounts(paths, version);
583
+ this.templates.processExecutor(paths, version);
584
+ if (current.quota.usedBytes > version.draft.quotas.maxStorageBytes)
585
+ throw new Error('HOSTED_CONVERSATION_QUOTA_EXCEEDED');
586
+ const updatedAt = this.now();
587
+ saved = await this.options.store.updateConversation(current.id, (latest) => ({
588
+ ...latest,
589
+ templateId: template.id,
590
+ templateName: template.name,
591
+ templateDeleted: false,
592
+ activeVersionId: version.id,
593
+ modelId: selected.id,
594
+ quota: { ...latest.quota, maxBytes: version.draft.quotas.maxStorageBytes },
595
+ updatedAt,
596
+ lastActivityAt: updatedAt
597
+ }));
598
+ try {
599
+ this.hostedWatch.publishEvent(saved.id, {
600
+ conversationId: saved.id,
601
+ kind: 'template-changed'
602
+ });
603
+ }
604
+ catch {
605
+ // Committed configuration does not depend on best-effort Watch delivery.
606
+ }
607
+ finally {
608
+ this.hostedWatch.removeConversation(saved.id);
609
+ }
610
+ }
611
+ return hostedConversationTemplateSwitchResultSchema.parse({
612
+ conversation: this.runs.summary(saved),
613
+ template: templateSummary(saved, version.draft),
614
+ models: models.map(modelSummary)
615
+ });
616
+ });
617
+ }
618
+ withConversationChange(conversationId, change) {
619
+ if (this.historyMutations.has(conversationId))
620
+ throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
621
+ const operation = Promise.resolve().then(change);
622
+ this.historyMutations.set(conversationId, operation);
623
+ void operation
624
+ .finally(() => {
625
+ if (this.historyMutations.get(conversationId) === operation)
626
+ this.historyMutations.delete(conversationId);
627
+ })
628
+ .catch(() => undefined);
629
+ return operation;
630
+ }
631
+ updateConfiguration(raw) {
632
+ const payload = hostedRequest(raw);
633
+ const input = hostedConversationConfigUpdateInputSchema.parse(payload.data);
634
+ const conversation = this.requireWritableConversation(payload.userId, input.conversationId);
635
+ this.assertTemplateContext(payload, conversation);
636
+ return this.withConversationChange(conversation.id, () => this.saveConfiguration(payload, input, conversation));
637
+ }
638
+ async saveConfiguration(payload, input, conversation) {
639
+ if (this.cleanupLocks.has(conversation.id))
640
+ throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
641
+ const latestVersionId = conversation.templateDeleted
642
+ ? undefined
643
+ : this.templates.latestAvailableTemplateVersionId(conversation.templateId);
644
+ const displayVersionId = latestVersionId ?? conversation.activeVersionId;
645
+ const version = await this.templates.loadTemplateVersion(displayVersionId, conversation.templateId);
646
+ const models = runtimeModels(payload.models);
647
+ const selected = selectModel(version, models, input.modelId ?? conversation.modelId);
648
+ const updatedAt = this.now();
649
+ const saved = await this.options.store.updateConversation(conversation.id, (current) => ({
650
+ ...current,
651
+ modelId: selected.id,
652
+ updatedAt,
653
+ lastActivityAt: updatedAt
654
+ }));
655
+ this.hostedWatch.publishEvent(conversation.id, {
656
+ conversationId: conversation.id,
657
+ kind: 'configuration',
658
+ conversation: this.runs.summary(saved)
659
+ });
660
+ return { conversation: this.runs.summary(saved) };
661
+ }
662
+ async updateMetadata(raw) {
663
+ const payload = hostedRequest(raw);
664
+ const input = hostedConversationMetadataUpdateInputSchema.parse(payload.data);
665
+ const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
666
+ if (this.cleanupLocks.has(conversation.id))
667
+ throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
668
+ const updatedAt = this.now();
669
+ const saved = await this.options.store.updateConversation(conversation.id, (current) => ({
670
+ ...current,
671
+ title: input.title ?? current.title,
672
+ titleSource: input.title === undefined ? current.titleSource : 'CUSTOM',
673
+ pinnedAt: input.pinned === undefined ? current.pinnedAt : input.pinned ? updatedAt : null,
674
+ updatedAt,
675
+ lastActivityAt: current.lastActivityAt
676
+ }));
677
+ this.hostedWatch.publishEvent(conversation.id, {
678
+ conversationId: conversation.id,
679
+ kind: 'configuration',
680
+ conversation: this.runs.summary(saved)
681
+ });
682
+ return { conversation: this.runs.summary(saved) };
683
+ }
684
+ async listFiles(raw) {
685
+ const payload = hostedRequest(raw);
686
+ const input = hostedConversationIdInputSchema.parse(payload.data);
687
+ return hostedConversationFilesSchema.parse({
688
+ files: this.options.store.listFiles(payload.userId, input.conversationId)
689
+ });
690
+ }
691
+ async readToolResource(raw) {
692
+ const payload = hostedRequest(raw);
693
+ const { conversationId } = hostedConversationIdInputSchema.parse(payload.data);
694
+ const conversation = this.options.store.requireConversation(payload.userId, conversationId);
695
+ const paths = this.options.store.pathsForConversation(payload.userId, conversation.id);
696
+ const data = payload.data;
697
+ const input = toolResultResourceInputSchema.parse({
698
+ resourceRef: data.resourceRef,
699
+ cursor: data.cursor,
700
+ maxBytes: data.maxBytes
701
+ });
702
+ if (!this.options.runtime.readToolResultResource)
703
+ throw new Error('TOOL_RESOURCE_UNSUPPORTED');
704
+ return toolResultResourcePageSchema.parse(await this.options.runtime.readToolResultResource({
705
+ homeDirectory: paths.homeDirectory,
706
+ sessionId: conversation.sessionId,
707
+ ...input
708
+ }));
709
+ }
710
+ async readImage(raw) {
711
+ const payload = hostedRequest(raw);
712
+ const input = hostedConversationImageReadInputSchema.parse(payload.data);
713
+ this.options.store.requireConversation(payload.userId, input.conversationId);
714
+ if (input.runId !== hostedConversationImageRunId(input.conversationId))
715
+ throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
716
+ const image = await this.options.readImage(input.runId, input.imageRef);
717
+ if (image === undefined)
718
+ throw new Error('RUN_IMAGE_NOT_FOUND');
719
+ return {
720
+ image: input.preview === undefined ? image : await imagePreviews.fromImage(image, input.preview)
721
+ };
722
+ }
723
+ async readFile(raw) {
724
+ const payload = hostedRequest(raw);
725
+ const input = hostedConversationFileReadInputSchema.parse(payload.data);
726
+ const file = this.options.store.requireFile(payload.userId, input.conversationId, input.fileId);
727
+ const path = this.options.store.filePath(payload.userId, file);
728
+ const offset = input.preview === undefined ? (input.offset ?? 0) : 0;
729
+ const limit = Math.min(input.limit ?? MAX_FILE_READ_BYTES, MAX_FILE_READ_BYTES);
730
+ const handle = await openRegularFile(path);
731
+ try {
732
+ const before = await handle.stat();
733
+ assertRegularFileMetadata(before);
734
+ const pathMetadata = await regularFileMetadata(path);
735
+ if (!sameFileIdentity(before, pathMetadata))
736
+ throw new Error('HOSTED_CONVERSATION_FILE_INVALID');
737
+ if (offset > before.size)
738
+ throw new Error('HOSTED_CONVERSATION_FILE_RANGE_INVALID');
739
+ if (input.preview !== undefined && before.size > MAX_IMAGE_PREVIEW_SOURCE_BYTES)
740
+ throw new Error('IMAGE_PREVIEW_TOO_LARGE');
741
+ const buffer = Buffer.alloc(input.preview === undefined
742
+ ? Math.min(limit, Math.max(0, before.size - offset))
743
+ : before.size);
744
+ const read = await handle.read(buffer, 0, buffer.length, offset);
745
+ if (input.preview !== undefined) {
746
+ while (read.bytesRead < buffer.length) {
747
+ const next = await handle.read(buffer, read.bytesRead, buffer.length - read.bytesRead, read.bytesRead);
748
+ if (next.bytesRead === 0)
749
+ throw new Error('HOSTED_CONVERSATION_FILE_INVALID');
750
+ read.bytesRead += next.bytesRead;
751
+ }
752
+ }
753
+ const after = await handle.stat();
754
+ const currentPathMetadata = await regularFileMetadata(path);
755
+ if (!sameFileSnapshot(before, after) || !sameFileSnapshot(after, currentPathMetadata))
756
+ throw new Error('HOSTED_CONVERSATION_FILE_INVALID');
757
+ if (input.preview !== undefined)
758
+ return { image: await imagePreviews.create(buffer, input.preview) };
759
+ const next = offset + read.bytesRead;
760
+ const eof = next >= after.size;
761
+ const currentFile = {
762
+ ...file,
763
+ size: after.size,
764
+ updatedAt: Math.max(file.createdAt, safeFileTimestamp(after.mtimeMs), safeFileTimestamp(after.ctimeMs))
765
+ };
766
+ return hostedConversationFileReadResultSchema.parse({
767
+ file: currentFile,
768
+ encoding: 'base64',
769
+ content: buffer.subarray(0, read.bytesRead).toString('base64'),
770
+ offset,
771
+ nextOffset: eof ? null : next,
772
+ eof
773
+ });
774
+ }
775
+ finally {
776
+ await handle.close();
777
+ }
778
+ }
779
+ async deleteFile(raw) {
780
+ const payload = hostedRequest(raw);
781
+ const input = hostedConversationFileDeleteInputSchema.parse(payload.data);
782
+ this.requireWritableConversation(payload.userId, input.conversationId);
783
+ const deleteKey = `${input.conversationId}\0${input.fileId}`;
784
+ this.fileOperations.beginDelete(deleteKey);
785
+ try {
786
+ const files = await this.options.store.deleteFile(payload.userId, input.conversationId, input.fileId, this.now());
787
+ const result = hostedConversationFileDeleteResultSchema.parse({
788
+ deletedFileId: input.fileId,
789
+ files
790
+ });
791
+ this.hostedWatch.publishEvent(input.conversationId, {
792
+ conversationId: input.conversationId,
793
+ kind: 'files',
794
+ files: result.files,
795
+ error: null
796
+ });
797
+ return result;
798
+ }
799
+ finally {
800
+ this.fileOperations.endDelete(deleteKey);
801
+ }
802
+ }
803
+ async delete(raw) {
804
+ if (this.closing)
805
+ throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
806
+ const payload = hostedRequest(raw);
807
+ const input = hostedConversationIdInputSchema.parse(payload.data);
808
+ const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
809
+ if (this.cleanupLocks.has(conversation.id) || this.historyMutations.has(conversation.id))
810
+ throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
811
+ this.cleanupLocks.add(conversation.id);
812
+ try {
813
+ await this.waitForPendingMessageStarts(conversation.id);
814
+ const handle = this.runs.current(conversation.id);
815
+ if (handle !== undefined) {
816
+ this.runs.markInterruption(conversation.id, handle.run.id);
817
+ if (!handle.cancel())
818
+ this.runs.clearInterruption(conversation.id, handle.run.id);
819
+ }
820
+ await this.waitForFileWrites(conversation.id);
821
+ await this.hostedUploads.cancelConversationUploads(payload.userId, conversation.id);
822
+ const paths = this.options.store.pathsForConversation(payload.userId, conversation.id);
823
+ await this.options.runtime
824
+ .releaseSession({
825
+ conversationId: conversation.id,
826
+ sessionId: conversation.sessionId,
827
+ homeDirectory: paths.homeDirectory
828
+ })
829
+ .catch(() => undefined);
830
+ await this.runs.settlement(conversation.id)?.catch(() => undefined);
831
+ this.hostedWatch.removeConversation(conversation.id);
832
+ await this.options.store.permanentDeleteConversation(conversation.id);
833
+ this.runs.forgetActivity(conversation.id);
834
+ this.historyVersions.delete(conversation.id);
835
+ return hostedConversationDeleteResultSchema.parse({
836
+ deletedConversationId: conversation.id
837
+ });
838
+ }
839
+ finally {
840
+ this.cleanupLocks.delete(conversation.id);
841
+ }
842
+ }
843
+ async waitForPendingMessageStarts(conversationId) {
844
+ await this.runs.waitForAdmissions(conversationId);
845
+ }
846
+ makeWorkbenchEvent(payload) {
847
+ return (this.options.nextWorkbenchEvent?.(payload) ?? {
848
+ sequence: ++this.workbenchSequence,
849
+ category: 'hosted-conversation',
850
+ payload
851
+ });
852
+ }
853
+ publishWorkbenchEvent(payload) {
854
+ const event = this.makeWorkbenchEvent(payload);
855
+ if (event !== undefined)
856
+ this.options.emitWorkbenchEvent?.(event);
857
+ }
858
+ requireWritableConversation(userId, conversationId) {
859
+ const conversation = this.options.store.requireConversation(userId, conversationId);
860
+ if (this.cleanupLocks.has(conversation.id) || this.historyMutations.has(conversation.id))
861
+ throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
862
+ if (conversation.status !== 'ACTIVE')
863
+ throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
864
+ return conversation;
865
+ }
866
+ assertTemplateContext(payload, conversation) {
867
+ if (payload.sourceTemplateId !== undefined &&
868
+ payload.sourceTemplateId !== conversation.templateId)
869
+ throw new Error('HOSTED_CONVERSATION_TEMPLATE_CONFLICT');
870
+ }
871
+ }
872
+ function hostedRequest(raw) {
873
+ if (!record(raw))
874
+ throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
875
+ const { __usageGrant, __requestUserId, __template, __marAgentModel, __marAgentModels, __marAgentCompact, __marAgentRuntimeScopeId, __workspaceWatchSessionHash, __templateDeleted, __sourceTemplateId, ...data } = raw;
876
+ if (typeof __requestUserId !== 'number' ||
877
+ !Number.isSafeInteger(__requestUserId) ||
878
+ __requestUserId <= 0)
879
+ throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
880
+ if (__templateDeleted !== undefined && __templateDeleted !== true)
881
+ throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
882
+ if (__sourceTemplateId !== undefined &&
883
+ (typeof __sourceTemplateId !== 'string' || __sourceTemplateId.length === 0))
884
+ throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
885
+ if (__marAgentRuntimeScopeId !== undefined &&
886
+ (typeof __marAgentRuntimeScopeId !== 'string' || __marAgentRuntimeScopeId.length === 0))
887
+ throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
888
+ return {
889
+ data,
890
+ ...(__sourceTemplateId === undefined ? {} : { sourceTemplateId: __sourceTemplateId }),
891
+ ...(__usageGrant === undefined
892
+ ? {}
893
+ : { usageGrant: modelUsageGrantSchema.parse(__usageGrant) }),
894
+ userId: __requestUserId,
895
+ ...(__template === undefined ? {} : { template: __template }),
896
+ ...(__marAgentModel === undefined ? {} : { model: __marAgentModel }),
897
+ ...(__marAgentModels === undefined ? {} : { models: __marAgentModels }),
898
+ ...(__marAgentCompact === undefined ? {} : { compact: __marAgentCompact }),
899
+ ...(__marAgentRuntimeScopeId === undefined ? {} : { runtimeScopeId: __marAgentRuntimeScopeId }),
900
+ ...(__workspaceWatchSessionHash === undefined
901
+ ? {}
902
+ : { sessionHash: __workspaceWatchSessionHash }),
903
+ ...(__templateDeleted === true ? { templateDeleted: true } : {})
904
+ };
905
+ }
906
+ function requireWatchSessionHash(value) {
907
+ if (typeof value !== 'string' || value.length === 0)
908
+ throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
909
+ return value;
910
+ }
911
+ function templateSummary(conversation, draft) {
912
+ return conversationTemplateSummarySchema.parse({
913
+ id: conversation.templateId,
914
+ nodeId: conversation.nodeId,
915
+ name: conversation.templateName,
916
+ description: draft.description,
917
+ status: conversation.templateDeleted ? 'DELETED' : 'ACTIVE',
918
+ currentVersionId: conversation.activeVersionId,
919
+ available: !conversation.templateDeleted,
920
+ createdAt: conversation.createdAt,
921
+ updatedAt: draft.updatedAt
922
+ });
923
+ }
924
+ function modelSummary(model, index) {
925
+ return {
926
+ id: model.id,
927
+ name: model.name,
928
+ protocol: model.protocol,
929
+ baseUrl: model.baseUrl,
930
+ modelId: model.modelId,
931
+ contextWindowTokens: model.contextWindowTokens,
932
+ maxOutputTokens: model.maxOutputTokens,
933
+ inputCapabilities: model.inputCapabilities,
934
+ outputCapabilities: model.outputCapabilities,
935
+ reasoningEfforts: model.reasoningEfforts,
936
+ defaultReasoningEffort: resolveMarAgentDefaultReasoningEffort(model.defaultReasoningEffort, model.reasoningEfforts),
937
+ titleGeneration: model.titleGeneration,
938
+ codeMode: model.codeMode,
939
+ ...(model.serviceTier === undefined ? {} : { serviceTier: model.serviceTier }),
940
+ hostedWebSearch: model.hostedWebSearch,
941
+ ...(model.responsesTransport === undefined
942
+ ? {}
943
+ : { responsesTransport: model.responsesTransport }),
944
+ responsesPreviousResponseId: model.responsesPreviousResponseId,
945
+ enabled: model.enabled,
946
+ sortOrder: index,
947
+ apiKeyConfigured: true
948
+ };
949
+ }
950
+ function terminalHostedTurnStatus(status) {
951
+ return (status === 'SUCCEEDED' ||
952
+ status === 'FAILED' ||
953
+ status === 'CANCELLED' ||
954
+ status === 'INTERRUPTED' ||
955
+ status === 'LOST');
956
+ }
957
+ function sameFileIdentity(left, right) {
958
+ return left.dev === right.dev && left.ino === right.ino;
959
+ }
960
+ function sameFileSnapshot(left, right) {
961
+ return (sameFileIdentity(left, right) &&
962
+ left.nlink === right.nlink &&
963
+ left.size === right.size &&
964
+ left.mtimeMs === right.mtimeMs &&
965
+ left.ctimeMs === right.ctimeMs);
966
+ }
967
+ function safeFileTimestamp(value) {
968
+ if (!Number.isFinite(value) || value < 0)
969
+ return 0;
970
+ return Math.min(Number.MAX_SAFE_INTEGER, Math.trunc(value));
971
+ }
972
+ function record(value) {
973
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
974
+ }