@myagentroam/node 0.11.36 → 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.
- package/dist/application/execution-lifecycle.js +54 -0
- package/dist/application/node-lifecycle.js +104 -0
- package/dist/application/node-operation-admission.js +51 -0
- package/dist/application/node-operation-contracts.js +1 -0
- package/dist/application/runner-composition.js +22 -0
- package/dist/application/session-projection-composition.js +26 -0
- package/dist/application/workspace-application.js +61 -0
- package/dist/application/workspace-composition.js +21 -0
- package/dist/capabilities.js +2 -2
- package/dist/codex-app-server.js +1 -471
- package/dist/connection/authorization.js +26 -0
- package/dist/{service → connection}/direct-transfer-service.js +1 -1
- package/dist/connection/endpoints.js +32 -0
- package/dist/connector.js +144 -252
- package/dist/database.js +34 -14
- package/dist/git-askpass.js +1 -1
- package/dist/git-execution-helper.js +1 -1
- package/dist/hosting/hosted-conversation-file-identity.js +27 -0
- package/dist/hosting/hosted-conversation-file-operations.js +44 -0
- package/dist/hosting/hosted-conversation-node-service.js +974 -0
- package/dist/hosting/hosted-conversation-runs.js +599 -0
- package/dist/hosting/hosted-conversation-templates.js +280 -0
- package/dist/hosting/hosted-conversation-uploads.js +344 -0
- package/dist/hosting/hosted-conversation-watch.js +194 -0
- package/dist/hosting/runtime-contracts.js +1 -0
- package/dist/runner/abstract-runner.js +4 -13
- package/dist/{claude-agent-sdk.js → runner/claude/claude-agent-sdk.js} +2 -2
- package/dist/runner/claude/conversation-parser.js +2 -1
- package/dist/runner/claude/managed-run-controller.js +6 -7
- package/dist/runner/claude/message-parsers.js +2 -1
- package/dist/runner/claude/native-transcript.js +2 -1
- package/dist/runner/claude/tool-normalizer.js +1 -1
- package/dist/runner/claude-code-runner.js +2 -2
- package/dist/runner/codex/codex-app-server.js +471 -0
- package/dist/runner/codex/conversation-parser.js +4 -2
- package/dist/runner/codex/managed-run-controller.js +7 -9
- package/dist/runner/codex/native-transcript.js +1 -1
- package/dist/runner/codex/tool-normalizer.js +1 -1
- package/dist/runner/codex-runner.js +9 -27
- package/dist/runner/contracts.js +1 -0
- package/dist/runner/mar-agent/conversation-normalizer.js +2 -2
- package/dist/runner/mar-agent/history-projection.js +1 -1
- package/dist/runner/mar-agent/hosted-conversation-runtime.js +6 -10
- package/dist/runner/mar-agent/managed-run-controller.js +85 -61
- package/dist/runner/mar-agent-runner.js +1 -1
- package/dist/runner/normalization.js +42 -0
- package/dist/runner/opencode/conversation-parser.js +1 -1
- package/dist/runner/opencode/managed-run-controller.js +6 -7
- package/dist/{opencode-server.js → runner/opencode/opencode-server.js} +2 -2
- package/dist/runner/opencode/tool-normalizer.js +1 -1
- package/dist/runner/opencode-runner.js +4 -5
- package/dist/runner/runner-registry.js +4 -1
- package/dist/runtime-state.js +48 -372
- package/dist/service/computer-use-enablement-service.js +1 -1
- package/dist/service/continuous-run-coordinator.js +52 -7
- package/dist/service/conversation-history-service.js +4 -4
- package/dist/service/conversation-hosting-capability-service.js +1 -1
- package/dist/service/conversation-hosting-enablement-service.js +1 -1
- package/dist/service/conversation-hosting-lifecycle-service.js +3 -3
- package/dist/service/conversation-hosting-maintenance-service.js +1 -1
- package/dist/service/conversation-observation.js +1 -1
- package/dist/service/conversation-segment-service.js +1 -1
- package/dist/service/external-session-resume-service.js +47 -27
- package/dist/service/fake-managed-run-service.js +3 -7
- package/dist/service/git-operation-service.js +1 -1
- package/dist/service/goal-continuation-coordinator.js +14 -7
- package/dist/service/mar-agent-model-transport-test-service.js +1 -1
- package/dist/service/native-session-watch-service.js +1 -1
- package/dist/service/node-control-channel.js +1 -5
- package/dist/service/node-request-service.js +1 -1
- package/dist/service/node-terminal-channel.js +2 -1
- package/dist/service/node-workspace-coordinator.js +3 -2
- package/dist/service/notification-mcp-bridge.js +6 -2
- package/dist/service/recent-run-projection-service.js +1 -1
- package/dist/service/restart-session-restorer.js +1 -1
- package/dist/service/run-event-service.js +49 -42
- package/dist/service/run-workbench-service.js +3 -3
- package/dist/service/runner-interaction-service.js +6 -5
- package/dist/service/runner-service.js +1 -1
- package/dist/service/session-recent-activity.js +2 -1
- package/dist/service/skill-directory-service.js +1 -1
- package/dist/service/skill-install-service.js +2 -2
- package/dist/service/terminal-service.js +1 -1
- package/dist/service/workbench-event-projection.js +3 -2
- package/dist/service/workbench-event-service.js +1 -1
- package/dist/service/workspace-service.js +3 -2
- package/dist/session/attachments.js +110 -0
- package/dist/session/contracts.js +1 -0
- package/dist/{util → session}/conversation-identity.js +1 -1
- package/dist/session/input-validation.js +15 -0
- package/dist/{service → session}/native-session-projection-service.js +40 -11
- package/dist/session/pagination.js +77 -0
- package/dist/{util → session}/runner-native-session-parsers.js +2 -1
- package/dist/session/runtime/approvals.js +90 -0
- package/dist/session/runtime/conversation-projection.js +310 -0
- package/dist/session/runtime/type-guards.js +19 -0
- package/dist/{service → session}/session-catalog-service.js +5 -4
- package/dist/{service → session}/session-command-service.js +3 -3
- package/dist/{service → session}/session-identity-service.js +1 -1
- package/dist/{service → session}/session-lifecycle-service.js +18 -7
- package/dist/{service → session}/session-message-service.js +6 -5
- package/dist/{service → session}/session-presentation-service.js +1 -1
- package/dist/{service → session}/session-query-service.js +2 -2
- package/dist/{service → session}/session-schedule-service.js +2 -2
- package/dist/util/records.js +3 -0
- package/dist/workspace/contracts.js +1 -0
- package/dist/workspace/file-index.js +534 -0
- package/dist/workspace/files.js +466 -0
- package/dist/workspace/git.js +991 -0
- package/dist/workspace/path-access.js +184 -0
- package/dist/workspace/utf8.js +12 -0
- package/dist/{service → workspace}/workspace-change-service.js +45 -21
- package/dist/{service → workspace}/workspace-code-navigation-service.js +6 -6
- package/dist/{service → workspace}/workspace-file-manager-service.js +1 -1
- package/dist/{service → workspace}/workspace-file-service.js +16 -16
- package/dist/{service → workspace}/workspace-git-exclude-service.js +1 -1
- package/dist/{service → workspace}/workspace-queue-workbench-service.js +3 -2
- package/dist/{service → workspace}/workspace-session-service.js +45 -40
- package/dist/{service → workspace}/workspace-upload-service.js +28 -28
- package/dist/{service → workspace}/workspace-watch-service.js +29 -28
- package/dist/{service → workspace}/workspace-workbench-service.js +4 -3
- package/package.json +5 -5
- package/dist/service/hosted-conversation-node-service.js +0 -2252
- package/dist/service/run-domain-state.js +0 -4
- package/dist/service/session-domain-state.js +0 -9
- package/dist/service/workspace-domain-state.js +0 -41
- package/dist/util/node-operation-parsers.js +0 -320
- package/dist/workspace.js +0 -2168
- /package/dist/{connector → application}/node-connector-options.js +0 -0
- /package/dist/{util → connection}/node-operation-data.js +0 -0
- /package/dist/{connector → connection}/node-operation-router.js +0 -0
- /package/dist/{service → hosting}/hosted-conversation-sandbox-profile.js +0 -0
- /package/dist/{service → hosting}/hosted-conversation-store.js +0 -0
- /package/dist/{util → mcp}/secret-environment.js +0 -0
- /package/dist/{claude-channel.js → runner/claude/claude-channel.js} +0 -0
- /package/dist/{util → runner}/personal-instructions.js +0 -0
- /package/dist/{util → runner}/runner-error.js +0 -0
- /package/dist/{service/conversation-work-process.js → session/runtime/work-process.js} +0 -0
- /package/dist/{service → session}/session-context-service.js +0 -0
- /package/dist/{util → workspace}/git-credential-client.js +0 -0
- /package/dist/{service → workspace}/workspace-content-search-service.js +0 -0
- /package/dist/{service → workspace}/workspace-watch-snapshot-service.js +0 -0
|
@@ -1,2252 +0,0 @@
|
|
|
1
|
-
import { resolveSessionRollbackTarget } from './session-rollback-target.js';
|
|
2
|
-
import { assertMcpServerNames } from '../mcp/naming.js';
|
|
3
|
-
import { imagePreviews, MAX_IMAGE_PREVIEW_SOURCE_BYTES } from './image-preview-service.js';
|
|
4
|
-
import { modelUsageGrantSchema, steerComposerMessage } from '@myagentroam/protocol';
|
|
5
|
-
import { randomUUID } from 'node:crypto';
|
|
6
|
-
import { constants } from 'node:fs';
|
|
7
|
-
import { lstat, mkdir, open, readFile, rename, rm, utimes } from 'node:fs/promises';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
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, hostedConversationSummarySchema, hostedConversationUnwatchInputSchema, hostedConversationUploadChunkInputSchema, hostedConversationUploadControlInputSchema, hostedConversationUploadCreateInputSchema, hostedConversationUploadPreflightInputSchema, hostedConversationUploadPreflightResultSchema, hostedConversationUploadSchema, hostedConversationWatchInputSchema, marAgentRuntimeModelConfigurationSchema, resolveMarAgentDefaultReasoningEffort } from '@myagentroam/protocol';
|
|
10
|
-
import { hostedConversationGeneratedImagesDisplayDirectory, hostedConversationImageRunId, marAgentGeneratedImagesDirectory } from '../runner/mar-agent/generated-images.js';
|
|
11
|
-
import { sessionTitleFromFirstMessage } from '../util/node-operation-parsers.js';
|
|
12
|
-
import { safeErrorCode } from '../util/safe-error.js';
|
|
13
|
-
import { SessionWatchSlotCoordinator } from './session-watch-slot-coordinator.js';
|
|
14
|
-
import { parseTemplateVersionManifest, templateContentHash, templateMcpConfigHash, templateManifestHash } from './conversation-template-manifest.js';
|
|
15
|
-
const DEFAULT_HOSTED_CONVERSATION_TITLE = 'New conversation';
|
|
16
|
-
const HOST_POLICY_VERSION = 1;
|
|
17
|
-
const MAX_FILE_READ_BYTES = 512 * 1024;
|
|
18
|
-
const WATCH_TTL_MS = 45_000;
|
|
19
|
-
/** Implements the Node-owned hosted-conversation domain over an injected MAR Agent runtime. */
|
|
20
|
-
export class HostedConversationNodeService {
|
|
21
|
-
options;
|
|
22
|
-
now;
|
|
23
|
-
id;
|
|
24
|
-
pendingCreates = new Map();
|
|
25
|
-
pendingMessages = new Map();
|
|
26
|
-
activeRuns = new Map();
|
|
27
|
-
runSettlements = new Map();
|
|
28
|
-
nativeAdmissions = new Map();
|
|
29
|
-
activities = new Map();
|
|
30
|
-
expectedInterruptions = new Set();
|
|
31
|
-
uploads = new Map();
|
|
32
|
-
uploadsByRequest = new Map();
|
|
33
|
-
pendingUploadCreates = new Map();
|
|
34
|
-
uploadNameReservations = new Map();
|
|
35
|
-
fileDeletes = new Set();
|
|
36
|
-
fileWrites = new Map();
|
|
37
|
-
closing;
|
|
38
|
-
watchLeases = new Map();
|
|
39
|
-
cleanupLocks = new Set();
|
|
40
|
-
historyMutations = new Map();
|
|
41
|
-
historyVersions = new Map();
|
|
42
|
-
sessionWatchSlots;
|
|
43
|
-
disablePrepared = false;
|
|
44
|
-
snapshotSequence = 0;
|
|
45
|
-
workbenchSequence = 0;
|
|
46
|
-
constructor(options) {
|
|
47
|
-
this.options = options;
|
|
48
|
-
this.now = options.now ?? Date.now;
|
|
49
|
-
this.id = options.id ?? randomUUID;
|
|
50
|
-
this.sessionWatchSlots = options.sessionWatchSlots ?? new SessionWatchSlotCoordinator();
|
|
51
|
-
}
|
|
52
|
-
operations() {
|
|
53
|
-
return {
|
|
54
|
-
'conversation.hosted.list': (raw) => this.list(raw),
|
|
55
|
-
'conversation.hosted.model-options': (raw) => this.modelOptions(raw),
|
|
56
|
-
'conversation.hosted.create': (raw) => this.create(raw),
|
|
57
|
-
'conversation.hosted.get': (raw) => this.get(raw),
|
|
58
|
-
'conversation.hosted.history': (raw) => this.history(raw),
|
|
59
|
-
'conversation.hosted.watch': (raw) => this.watch(raw),
|
|
60
|
-
'conversation.hosted.unwatch': (raw) => this.unwatch(raw),
|
|
61
|
-
'conversation.hosted.message': (raw) => this.message(raw),
|
|
62
|
-
'conversation.hosted.rollback': (raw) => this.rollback(raw),
|
|
63
|
-
'conversation.hosted.interrupt': (raw) => this.interrupt(raw),
|
|
64
|
-
'conversation.hosted.config.update': (raw) => this.updateConfiguration(raw),
|
|
65
|
-
'conversation.hosted.template.switch': (raw) => this.switchTemplate(raw),
|
|
66
|
-
'conversation.hosted.metadata.update': (raw) => this.updateMetadata(raw),
|
|
67
|
-
'conversation.hosted.image.read': (raw) => this.readImage(raw),
|
|
68
|
-
'conversation.hosted.tool-resource.read': (raw) => this.readToolResource(raw),
|
|
69
|
-
'conversation.hosted.files.list': (raw) => this.listFiles(raw),
|
|
70
|
-
'conversation.hosted.file.read': (raw) => this.readFile(raw),
|
|
71
|
-
'conversation.hosted.file.delete': (raw) => this.fileWrite(raw, hostedConversationFileDeleteInputSchema, () => this.deleteFile(raw)),
|
|
72
|
-
'conversation.hosted.files.upload.preflight': (raw) => this.preflightUpload(raw),
|
|
73
|
-
'conversation.hosted.files.upload.create': (raw) => this.fileWrite(raw, hostedConversationUploadCreateInputSchema, () => this.createUpload(raw)),
|
|
74
|
-
'conversation.hosted.files.upload.chunk': (raw) => this.fileWrite(raw, hostedConversationUploadChunkInputSchema, () => this.writeUploadChunk(raw)),
|
|
75
|
-
'conversation.hosted.files.upload.status': (raw) => this.uploadStatus(raw),
|
|
76
|
-
'conversation.hosted.files.upload.complete': (raw) => this.fileWrite(raw, hostedConversationUploadControlInputSchema, () => this.completeUpload(raw)),
|
|
77
|
-
'conversation.hosted.files.upload.cancel': (raw) => this.fileWrite(raw, hostedConversationUploadControlInputSchema, () => this.cancelUpload(raw)),
|
|
78
|
-
'conversation.hosted.delete': (raw) => this.delete(raw)
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Closes Run admission while a management operation verifies that it can
|
|
83
|
-
* release the hosting runtime without interrupting an existing execution.
|
|
84
|
-
*/
|
|
85
|
-
prepareForDisable() {
|
|
86
|
-
if (this.disablePrepared ||
|
|
87
|
-
this.pendingMessages.size > 0 ||
|
|
88
|
-
this.activeRuns.size > 0 ||
|
|
89
|
-
this.runSettlements.size > 0 ||
|
|
90
|
-
this.historyMutations.size > 0)
|
|
91
|
-
return undefined;
|
|
92
|
-
this.disablePrepared = true;
|
|
93
|
-
return () => {
|
|
94
|
-
this.disablePrepared = false;
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
list(raw) {
|
|
98
|
-
const payload = hostedRequest(raw);
|
|
99
|
-
const input = hostedConversationListInputSchema.parse(payload.data);
|
|
100
|
-
const page = this.options.store.listConversations({
|
|
101
|
-
userId: payload.userId,
|
|
102
|
-
limit: input.limit,
|
|
103
|
-
...(input.cursor === undefined ? {} : { cursor: input.cursor })
|
|
104
|
-
});
|
|
105
|
-
return hostedConversationListPageSchema.parse({
|
|
106
|
-
...page,
|
|
107
|
-
items: page.items.map((conversation) => ({
|
|
108
|
-
...conversation,
|
|
109
|
-
activity: this.activity(conversation.id)
|
|
110
|
-
}))
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
async modelOptions(raw) {
|
|
114
|
-
if (this.disablePrepared)
|
|
115
|
-
throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
|
|
116
|
-
const payload = hostedRequest(raw);
|
|
117
|
-
const input = hostedConversationModelOptionsInputSchema.parse(payload.data);
|
|
118
|
-
const template = conversationTemplateSummarySchema.parse(payload.template);
|
|
119
|
-
if (template.id !== input.templateId ||
|
|
120
|
-
template.nodeId !== this.options.nodeId() ||
|
|
121
|
-
!template.available ||
|
|
122
|
-
template.status !== 'ACTIVE' ||
|
|
123
|
-
template.currentVersionId === null)
|
|
124
|
-
throw new Error('HOSTED_CONVERSATION_TEMPLATE_UNAVAILABLE');
|
|
125
|
-
const version = await this.loadTemplateVersion(template.currentVersionId, template.id);
|
|
126
|
-
const models = allowedModels(version, runtimeModels(payload.models)).filter((model) => model.conversationModel !== false);
|
|
127
|
-
const selected = models.length === 0 ? undefined : selectModel(version, models, version.modelPolicy.defaultId);
|
|
128
|
-
return hostedConversationModelOptionsSchema.parse({
|
|
129
|
-
models: networkModels(models, version.draft.networkMode).map(modelSummary),
|
|
130
|
-
defaultModelId: selected?.id ?? null
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
create(raw) {
|
|
134
|
-
if (this.disablePrepared)
|
|
135
|
-
return Promise.reject(new Error('CONVERSATION_HOSTING_UNAVAILABLE'));
|
|
136
|
-
const payload = hostedRequest(raw);
|
|
137
|
-
const input = hostedConversationCreateInputSchema.parse(payload.data);
|
|
138
|
-
const template = conversationTemplateSummarySchema.parse(payload.template);
|
|
139
|
-
const models = runtimeModels(payload.models);
|
|
140
|
-
if (template.id !== input.templateId ||
|
|
141
|
-
template.nodeId !== this.options.nodeId() ||
|
|
142
|
-
!template.available ||
|
|
143
|
-
template.status !== 'ACTIVE' ||
|
|
144
|
-
template.currentVersionId === null)
|
|
145
|
-
throw new Error('HOSTED_CONVERSATION_TEMPLATE_UNAVAILABLE');
|
|
146
|
-
const key = `${payload.userId}\0${input.clientRequestId}`;
|
|
147
|
-
const pending = this.pendingCreates.get(key);
|
|
148
|
-
if (pending !== undefined)
|
|
149
|
-
return pending;
|
|
150
|
-
const existing = this.options.store.findConversationByClientRequest(payload.userId, input.clientRequestId);
|
|
151
|
-
if (existing !== undefined) {
|
|
152
|
-
if (existing.templateId !== input.templateId)
|
|
153
|
-
throw new Error('HOSTED_CONVERSATION_TEMPLATE_CONFLICT');
|
|
154
|
-
return this.snapshot(payload.userId, existing.id, models);
|
|
155
|
-
}
|
|
156
|
-
const operation = this.createConversation(payload.userId, input.clientRequestId, template, models, input.modelId, payload.runtimeScopeId);
|
|
157
|
-
this.pendingCreates.set(key, operation);
|
|
158
|
-
void operation.finally(() => this.pendingCreates.delete(key)).catch(() => undefined);
|
|
159
|
-
return operation;
|
|
160
|
-
}
|
|
161
|
-
async createConversation(userId, clientRequestId, template, models, requestedModelId, runtimeScopeId) {
|
|
162
|
-
await this.options.assertCanCreate?.();
|
|
163
|
-
const version = await this.loadTemplateVersion(template.currentVersionId, template.id);
|
|
164
|
-
if (requestedModelId !== undefined &&
|
|
165
|
-
!allowedModels(version, models).some((model) => model.id === requestedModelId && model.conversationModel !== false))
|
|
166
|
-
throw new Error('HOSTED_CONVERSATION_MODEL_NOT_AVAILABLE');
|
|
167
|
-
const selected = selectModel(version, models, requestedModelId ?? version.modelPolicy.defaultId);
|
|
168
|
-
const conversationId = this.id();
|
|
169
|
-
const prepared = await this.options.store.prepareConversationEnvironment(userId, conversationId);
|
|
170
|
-
const pathMounts = this.pathMounts(prepared.paths, version);
|
|
171
|
-
const processExecutor = this.processExecutor(prepared.paths, version);
|
|
172
|
-
let sessionId;
|
|
173
|
-
try {
|
|
174
|
-
const session = await this.options.runtime.createSession({
|
|
175
|
-
conversationId,
|
|
176
|
-
homeDirectory: prepared.paths.homeDirectory,
|
|
177
|
-
workspaceDirectory: version.templateDirectory,
|
|
178
|
-
systemInstruction: systemInstruction(version),
|
|
179
|
-
defaultModelId: selected.id,
|
|
180
|
-
models: networkModels(allowedModels(version, models), version.draft.networkMode),
|
|
181
|
-
...(runtimeScopeId === undefined ? {} : { runtimeScopeId }),
|
|
182
|
-
pathMounts,
|
|
183
|
-
processExecutor
|
|
184
|
-
});
|
|
185
|
-
sessionId = session.sessionId;
|
|
186
|
-
const createdAt = this.now();
|
|
187
|
-
const manifest = {
|
|
188
|
-
id: conversationId,
|
|
189
|
-
nodeId: this.options.nodeId(),
|
|
190
|
-
sessionId,
|
|
191
|
-
templateId: template.id,
|
|
192
|
-
templateName: template.name,
|
|
193
|
-
activeVersionId: version.id,
|
|
194
|
-
title: DEFAULT_HOSTED_CONVERSATION_TITLE,
|
|
195
|
-
titleSource: 'DEFAULT',
|
|
196
|
-
pinnedAt: null,
|
|
197
|
-
status: 'ACTIVE',
|
|
198
|
-
modelId: selected.id,
|
|
199
|
-
createdAt,
|
|
200
|
-
updatedAt: createdAt,
|
|
201
|
-
lastActivityAt: createdAt,
|
|
202
|
-
userStorageKey: prepared.userStorageKey,
|
|
203
|
-
conversationStorageKey: prepared.conversationStorageKey,
|
|
204
|
-
clientRequestId,
|
|
205
|
-
templateDeleted: false,
|
|
206
|
-
quota: {
|
|
207
|
-
usedBytes: 0,
|
|
208
|
-
maxBytes: version.draft.quotas.maxStorageBytes
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
const persisted = await this.options.store.createConversation(manifest);
|
|
212
|
-
if (persisted.id !== conversationId) {
|
|
213
|
-
await this.options.runtime
|
|
214
|
-
.releaseSession({
|
|
215
|
-
conversationId,
|
|
216
|
-
sessionId,
|
|
217
|
-
homeDirectory: prepared.paths.homeDirectory
|
|
218
|
-
})
|
|
219
|
-
.catch(() => undefined);
|
|
220
|
-
await rm(prepared.paths.conversationDirectory, { recursive: true, force: true });
|
|
221
|
-
}
|
|
222
|
-
return this.snapshot(userId, persisted.id, models);
|
|
223
|
-
}
|
|
224
|
-
catch (error) {
|
|
225
|
-
if (sessionId !== undefined)
|
|
226
|
-
await this.options.runtime
|
|
227
|
-
.releaseSession({
|
|
228
|
-
conversationId,
|
|
229
|
-
sessionId,
|
|
230
|
-
homeDirectory: prepared.paths.homeDirectory
|
|
231
|
-
})
|
|
232
|
-
.catch(() => undefined);
|
|
233
|
-
await rm(prepared.paths.conversationDirectory, { recursive: true, force: true }).catch(() => undefined);
|
|
234
|
-
throw error;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
async get(raw) {
|
|
238
|
-
const payload = hostedRequest(raw);
|
|
239
|
-
const input = hostedConversationIdInputSchema.parse(payload.data);
|
|
240
|
-
this.assertTemplateContext(payload, this.options.store.requireConversation(payload.userId, input.conversationId));
|
|
241
|
-
return this.snapshot(payload.userId, input.conversationId, runtimeModels(payload.models), payload.templateDeleted, false, undefined, payload.sourceTemplateId);
|
|
242
|
-
}
|
|
243
|
-
async history(raw) {
|
|
244
|
-
const payload = hostedRequest(raw);
|
|
245
|
-
const input = hostedConversationHistoryInputSchema.parse(payload.data);
|
|
246
|
-
const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
|
|
247
|
-
const paths = this.options.store.pathsForConversation(payload.userId, conversation.id);
|
|
248
|
-
const page = await this.options.runtime.history({
|
|
249
|
-
conversationId: conversation.id,
|
|
250
|
-
sessionId: conversation.sessionId,
|
|
251
|
-
homeDirectory: paths.homeDirectory,
|
|
252
|
-
modelId: conversation.modelId,
|
|
253
|
-
limit: input.limit,
|
|
254
|
-
...(input.cursor === undefined ? {} : { cursor: input.cursor })
|
|
255
|
-
});
|
|
256
|
-
return hostedConversationHistoryPageSchema.parse(page);
|
|
257
|
-
}
|
|
258
|
-
async snapshot(userId, conversationId, availableModels, templateDeleted = false, includeActiveHistoryMutation = false, historyOverride, sourceTemplateId) {
|
|
259
|
-
if (includeActiveHistoryMutation)
|
|
260
|
-
return this.readSnapshot(userId, conversationId, availableModels, templateDeleted, historyOverride, sourceTemplateId);
|
|
261
|
-
while (true) {
|
|
262
|
-
const version = this.historyVersions.get(conversationId) ?? 0;
|
|
263
|
-
const mutation = this.historyMutations.get(conversationId);
|
|
264
|
-
if (mutation !== undefined) {
|
|
265
|
-
await mutation.catch(() => undefined);
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
const snapshot = await this.readSnapshot(userId, conversationId, availableModels, templateDeleted, undefined, sourceTemplateId);
|
|
269
|
-
if (!this.historyMutations.has(conversationId) &&
|
|
270
|
-
(this.historyVersions.get(conversationId) ?? 0) === version)
|
|
271
|
-
return snapshot;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
async readSnapshot(userId, conversationId, availableModels, templateDeleted, historyOverride, sourceTemplateId) {
|
|
275
|
-
let conversation = this.options.store.requireConversation(userId, conversationId);
|
|
276
|
-
if (sourceTemplateId !== undefined && conversation.templateId !== sourceTemplateId)
|
|
277
|
-
throw new Error('HOSTED_CONVERSATION_TEMPLATE_CONFLICT');
|
|
278
|
-
const templateId = conversation.templateId;
|
|
279
|
-
const activeVersionId = conversation.activeVersionId;
|
|
280
|
-
if (templateDeleted)
|
|
281
|
-
conversation = await this.options.store.retainConversationTemplate(userId, conversationId, this.now(), sourceTemplateId ?? templateId);
|
|
282
|
-
const latestVersionId = conversation.templateDeleted
|
|
283
|
-
? undefined
|
|
284
|
-
: this.latestAvailableTemplateVersionId(conversation.templateId);
|
|
285
|
-
const displayVersionId = latestVersionId ?? conversation.activeVersionId;
|
|
286
|
-
const version = await this.loadTemplateVersion(displayVersionId, conversation.templateId);
|
|
287
|
-
const models = networkModels(allowedModels(version, availableModels), version.draft.networkMode).filter((model) => model.conversationModel !== false);
|
|
288
|
-
if (models.length > 0 &&
|
|
289
|
-
availableModels.find((model) => model.id === conversation.modelId)?.conversationModel !==
|
|
290
|
-
false) {
|
|
291
|
-
const configuredModelId = conversation.modelId;
|
|
292
|
-
const selected = selectModel(version, availableModels, conversation.modelId);
|
|
293
|
-
if (selected.id !== conversation.modelId)
|
|
294
|
-
conversation = await this.options.store.updateConversation(conversation.id, (current) => current.templateId === templateId &&
|
|
295
|
-
current.activeVersionId === activeVersionId &&
|
|
296
|
-
current.modelId === configuredModelId
|
|
297
|
-
? { ...current, modelId: selected.id }
|
|
298
|
-
: current);
|
|
299
|
-
}
|
|
300
|
-
const paths = this.options.store.pathsForConversation(userId, conversation.id);
|
|
301
|
-
const history = historyOverride ??
|
|
302
|
-
(await this.options.runtime.history({
|
|
303
|
-
conversationId: conversation.id,
|
|
304
|
-
sessionId: conversation.sessionId,
|
|
305
|
-
homeDirectory: paths.homeDirectory,
|
|
306
|
-
modelId: conversation.modelId,
|
|
307
|
-
limit: 10
|
|
308
|
-
}));
|
|
309
|
-
const current = this.options.store.requireConversation(userId, conversationId);
|
|
310
|
-
if (current.templateId !== templateId || current.activeVersionId !== activeVersionId)
|
|
311
|
-
throw new Error('HOSTED_CONVERSATION_TEMPLATE_CONFLICT');
|
|
312
|
-
return hostedConversationSnapshotSchema.parse({
|
|
313
|
-
nodeAppVersion: this.options.nodeAppVersion?.() ?? 'unknown',
|
|
314
|
-
conversation: this.summary(conversation),
|
|
315
|
-
template: templateSummary(conversation, version.draft),
|
|
316
|
-
run: this.activeRuns.get(conversation.id)?.run ?? null,
|
|
317
|
-
turns: history.turns,
|
|
318
|
-
historyNextCursor: history.nextCursor,
|
|
319
|
-
files: this.options.store.listFiles(userId, conversation.id),
|
|
320
|
-
models: models.map(modelSummary),
|
|
321
|
-
error: null,
|
|
322
|
-
snapshotSequence: 0
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
async watch(raw) {
|
|
326
|
-
if (this.closing)
|
|
327
|
-
throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
|
|
328
|
-
const payload = hostedRequest(raw);
|
|
329
|
-
const input = hostedConversationWatchInputSchema.parse(payload.data);
|
|
330
|
-
const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
|
|
331
|
-
this.assertTemplateContext(payload, conversation);
|
|
332
|
-
if (this.cleanupLocks.has(conversation.id))
|
|
333
|
-
throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
|
|
334
|
-
const sessionHash = requireWatchSessionHash(payload.sessionHash);
|
|
335
|
-
const ownerKey = watchOwnerKey(sessionHash, input.workbenchConnectionId);
|
|
336
|
-
if (input.mode === 'renew') {
|
|
337
|
-
const lease = this.watchLeases.get(ownerKey);
|
|
338
|
-
if (lease === undefined ||
|
|
339
|
-
!this.sessionWatchSlots.owns(ownerKey, lease.slotToken) ||
|
|
340
|
-
lease.watchId !== input.watchId ||
|
|
341
|
-
lease.conversationId !== input.conversationId ||
|
|
342
|
-
lease.userId !== payload.userId) {
|
|
343
|
-
throw new Error('SESSION_WATCH_NOT_FOUND');
|
|
344
|
-
}
|
|
345
|
-
if (lease.expiresAt <= Date.now()) {
|
|
346
|
-
this.removeWatchLease(ownerKey);
|
|
347
|
-
throw new Error('SESSION_WATCH_NOT_FOUND');
|
|
348
|
-
}
|
|
349
|
-
lease.expiresAt = Date.now() + WATCH_TTL_MS;
|
|
350
|
-
clearTimeout(lease.timer);
|
|
351
|
-
lease.timer = this.watchLeaseTimer(ownerKey);
|
|
352
|
-
return this.acceptedWatch(lease);
|
|
353
|
-
}
|
|
354
|
-
const models = runtimeModels(payload.models);
|
|
355
|
-
const existing = this.watchLeases.get(ownerKey);
|
|
356
|
-
if (existing !== undefined &&
|
|
357
|
-
this.sessionWatchSlots.owns(ownerKey, existing.slotToken) &&
|
|
358
|
-
existing.watchId === input.watchId &&
|
|
359
|
-
existing.conversationId === input.conversationId &&
|
|
360
|
-
existing.userId === payload.userId &&
|
|
361
|
-
existing.expiresAt > Date.now()) {
|
|
362
|
-
existing.expiresAt = Date.now() + WATCH_TTL_MS;
|
|
363
|
-
clearTimeout(existing.timer);
|
|
364
|
-
existing.timer = this.watchLeaseTimer(ownerKey);
|
|
365
|
-
return this.acceptedWatch(existing);
|
|
366
|
-
}
|
|
367
|
-
this.removeWatchLease(ownerKey);
|
|
368
|
-
const slotToken = this.sessionWatchSlots.replace(ownerKey, 'hosted-conversation', () => this.removeWatchLease(ownerKey));
|
|
369
|
-
const lease = {
|
|
370
|
-
ownerKey,
|
|
371
|
-
slotToken,
|
|
372
|
-
sessionHash,
|
|
373
|
-
workbenchConnectionId: input.workbenchConnectionId,
|
|
374
|
-
watchId: input.watchId,
|
|
375
|
-
conversationId: input.conversationId,
|
|
376
|
-
templateId: conversation.templateId,
|
|
377
|
-
userId: payload.userId,
|
|
378
|
-
models,
|
|
379
|
-
nodeGeneration: this.options.nodeGeneration?.() ?? 'local-test-generation',
|
|
380
|
-
pendingEvents: [],
|
|
381
|
-
expiresAt: Date.now() + WATCH_TTL_MS,
|
|
382
|
-
ready: false,
|
|
383
|
-
timer: undefined
|
|
384
|
-
};
|
|
385
|
-
lease.timer = this.watchLeaseTimer(ownerKey);
|
|
386
|
-
this.watchLeases.set(ownerKey, lease);
|
|
387
|
-
void this.initializeWatchLease(lease);
|
|
388
|
-
return this.acceptedWatch(lease);
|
|
389
|
-
}
|
|
390
|
-
unwatch(raw) {
|
|
391
|
-
const payload = hostedRequest(raw);
|
|
392
|
-
const input = hostedConversationUnwatchInputSchema.parse(payload.data);
|
|
393
|
-
this.options.store.requireConversation(payload.userId, input.conversationId);
|
|
394
|
-
const ownerKey = watchOwnerKey(requireWatchSessionHash(payload.sessionHash), input.workbenchConnectionId);
|
|
395
|
-
const lease = this.watchLeases.get(ownerKey);
|
|
396
|
-
if (lease?.watchId === input.watchId &&
|
|
397
|
-
lease.conversationId === input.conversationId &&
|
|
398
|
-
lease.userId === payload.userId)
|
|
399
|
-
this.removeWatchLease(ownerKey);
|
|
400
|
-
return { unwatched: true };
|
|
401
|
-
}
|
|
402
|
-
publishRuntimeEvent(conversationId, event) {
|
|
403
|
-
return this.publishHostedEvent(conversationId, {
|
|
404
|
-
conversationId,
|
|
405
|
-
kind: 'runtime',
|
|
406
|
-
runtimeEvent: event
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
isConversationInUse(conversationId) {
|
|
410
|
-
return (this.cleanupLocks.has(conversationId) ||
|
|
411
|
-
this.historyMutations.has(conversationId) ||
|
|
412
|
-
this.activeRuns.has(conversationId) ||
|
|
413
|
-
this.runSettlements.has(conversationId) ||
|
|
414
|
-
this.fileWrites.has(conversationId) ||
|
|
415
|
-
[...this.watchLeases.values()].some((lease) => lease.conversationId === conversationId) ||
|
|
416
|
-
[...this.uploads.values()].some((upload) => upload.conversationId === conversationId && upload.status === 'UPLOADING') ||
|
|
417
|
-
[...this.pendingMessages.keys()].some((key) => key.startsWith(`${conversationId}\0`)));
|
|
418
|
-
}
|
|
419
|
-
beginConversationCleanup(conversationId) {
|
|
420
|
-
if (this.isConversationInUse(conversationId))
|
|
421
|
-
return undefined;
|
|
422
|
-
this.cleanupLocks.add(conversationId);
|
|
423
|
-
return () => {
|
|
424
|
-
this.cleanupLocks.delete(conversationId);
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
async releaseConversationForCleanup(conversation) {
|
|
428
|
-
const paths = this.options.store.pathsForManifest(conversation);
|
|
429
|
-
await this.options.runtime
|
|
430
|
-
.releaseSession({
|
|
431
|
-
conversationId: conversation.id,
|
|
432
|
-
sessionId: conversation.sessionId,
|
|
433
|
-
homeDirectory: paths.homeDirectory
|
|
434
|
-
})
|
|
435
|
-
.catch(() => undefined);
|
|
436
|
-
}
|
|
437
|
-
dispose() {
|
|
438
|
-
if (this.closing)
|
|
439
|
-
return this.closing;
|
|
440
|
-
this.disablePrepared = true;
|
|
441
|
-
for (const [conversationId, handle] of this.activeRuns) {
|
|
442
|
-
const interruptionKey = runInterruptionKey(conversationId, handle.run.id);
|
|
443
|
-
this.expectedInterruptions.add(interruptionKey);
|
|
444
|
-
if (!handle.cancel())
|
|
445
|
-
this.expectedInterruptions.delete(interruptionKey);
|
|
446
|
-
}
|
|
447
|
-
for (const ownerKey of [...this.watchLeases.keys()])
|
|
448
|
-
this.removeWatchLease(ownerKey);
|
|
449
|
-
this.closing = (async () => {
|
|
450
|
-
await Promise.all([...this.fileWrites.keys()].map((id) => this.waitForFileWrites(id)));
|
|
451
|
-
await Promise.allSettled([...this.uploads.values()].map(async (upload) => {
|
|
452
|
-
if (upload.status === 'UPLOADING') {
|
|
453
|
-
upload.status = 'CANCELLED';
|
|
454
|
-
await rm(upload.temporaryPath, { force: true });
|
|
455
|
-
}
|
|
456
|
-
}));
|
|
457
|
-
this.uploads.clear();
|
|
458
|
-
this.uploadsByRequest.clear();
|
|
459
|
-
this.uploadNameReservations.clear();
|
|
460
|
-
})();
|
|
461
|
-
return this.closing;
|
|
462
|
-
}
|
|
463
|
-
async fileWrite(raw, schema, operation) {
|
|
464
|
-
const payload = hostedRequest(raw);
|
|
465
|
-
const input = schema.parse(payload.data);
|
|
466
|
-
if (this.closing)
|
|
467
|
-
throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
|
|
468
|
-
this.requireWritableConversation(payload.userId, input.conversationId);
|
|
469
|
-
const writes = this.fileWrites.get(input.conversationId) ?? new Set();
|
|
470
|
-
if (input.uploadId !== undefined &&
|
|
471
|
-
[...writes].some((write) => write.uploadId === input.uploadId))
|
|
472
|
-
throw new Error('HOSTED_CONVERSATION_FILE_CONFLICT');
|
|
473
|
-
// Register before the operation can enter its first asynchronous preparation.
|
|
474
|
-
const write = {
|
|
475
|
-
...(input.uploadId === undefined ? {} : { uploadId: input.uploadId }),
|
|
476
|
-
completion: Promise.resolve().then(operation)
|
|
477
|
-
};
|
|
478
|
-
writes.add(write);
|
|
479
|
-
this.fileWrites.set(input.conversationId, writes);
|
|
480
|
-
try {
|
|
481
|
-
return await write.completion;
|
|
482
|
-
}
|
|
483
|
-
finally {
|
|
484
|
-
writes.delete(write);
|
|
485
|
-
if (writes.size === 0)
|
|
486
|
-
this.fileWrites.delete(input.conversationId);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
async waitForFileWrites(conversationId) {
|
|
490
|
-
const writes = this.fileWrites.get(conversationId);
|
|
491
|
-
if (writes)
|
|
492
|
-
await Promise.allSettled([...writes].map((write) => write.completion));
|
|
493
|
-
}
|
|
494
|
-
message(raw) {
|
|
495
|
-
if (this.disablePrepared)
|
|
496
|
-
return Promise.reject(new Error('CONVERSATION_HOSTING_UNAVAILABLE'));
|
|
497
|
-
const payload = hostedRequest(raw);
|
|
498
|
-
const input = hostedConversationMessageInputSchema.parse(payload.data);
|
|
499
|
-
this.assertTemplateContext(payload, this.options.store.requireConversation(payload.userId, input.conversationId));
|
|
500
|
-
if (this.historyMutations.has(input.conversationId))
|
|
501
|
-
return Promise.reject(new Error('HOSTED_CONVERSATION_STATE_INVALID'));
|
|
502
|
-
const key = `${input.conversationId}\0${input.clientMessageId}`;
|
|
503
|
-
const pending = this.pendingMessages.get(key);
|
|
504
|
-
if (pending !== undefined)
|
|
505
|
-
return pending;
|
|
506
|
-
const existing = this.options.store.findMessage(payload.userId, input.conversationId, input.clientMessageId);
|
|
507
|
-
if (existing !== undefined) {
|
|
508
|
-
const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
|
|
509
|
-
return Promise.resolve({
|
|
510
|
-
conversation: this.summary(conversation),
|
|
511
|
-
run: runFromMessage(existing)
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
if ([...this.pendingMessages.keys()].some((candidate) => candidate.startsWith(`${input.conversationId}\0`)))
|
|
515
|
-
return Promise.reject(new Error('HOSTED_CONVERSATION_RUN_ACTIVE'));
|
|
516
|
-
const operation = input.deliveryIntent === 'REPLACE_CURRENT'
|
|
517
|
-
? this.replaceCurrentMessage(payload, input)
|
|
518
|
-
: this.startMessage(payload, input);
|
|
519
|
-
this.pendingMessages.set(key, operation);
|
|
520
|
-
void operation.finally(() => this.pendingMessages.delete(key)).catch(() => undefined);
|
|
521
|
-
return operation;
|
|
522
|
-
}
|
|
523
|
-
async replaceCurrentMessage(payload, input) {
|
|
524
|
-
const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
|
|
525
|
-
if (this.cleanupLocks.has(conversation.id) ||
|
|
526
|
-
this.historyMutations.has(conversation.id) ||
|
|
527
|
-
conversation.status !== 'ACTIVE')
|
|
528
|
-
throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
|
|
529
|
-
const handle = this.activeRuns.get(conversation.id);
|
|
530
|
-
if (handle === undefined) {
|
|
531
|
-
if (this.runSettlements.has(conversation.id))
|
|
532
|
-
throw new Error('HOSTED_CONVERSATION_RUN_ACTIVE');
|
|
533
|
-
return this.startMessage(payload, input);
|
|
534
|
-
}
|
|
535
|
-
const steerPrompt = steerComposerMessage(input.prompt);
|
|
536
|
-
if (handle.steer) {
|
|
537
|
-
const barrier = Promise.withResolvers();
|
|
538
|
-
this.nativeAdmissions.set(handle, barrier.promise);
|
|
539
|
-
let result;
|
|
540
|
-
try {
|
|
541
|
-
const attachments = await Promise.all(input.attachmentIds.map((fileId) => this.requireAttachment(payload.userId, conversation.id, fileId)));
|
|
542
|
-
result = await handle.steer({
|
|
543
|
-
prompt: input.prompt,
|
|
544
|
-
modelText: attachmentPrompt(steerPrompt, attachments),
|
|
545
|
-
clientMessageId: input.clientMessageId,
|
|
546
|
-
...(input.modelId === undefined ? {} : { modelId: input.modelId }),
|
|
547
|
-
...(payload.runtimeScopeId === undefined
|
|
548
|
-
? {}
|
|
549
|
-
: { runtimeScopeId: payload.runtimeScopeId })
|
|
550
|
-
});
|
|
551
|
-
if (result.status !== 'notAccepted') {
|
|
552
|
-
const now = this.now();
|
|
553
|
-
this.options.store.createMessage({
|
|
554
|
-
conversationId: conversation.id,
|
|
555
|
-
clientMessageId: input.clientMessageId,
|
|
556
|
-
runId: handle.run.id,
|
|
557
|
-
userTextPresent: result.status === 'accepted' && sessionTitleFromFirstMessage(input.prompt).length > 0,
|
|
558
|
-
status: 'RUNNING',
|
|
559
|
-
createdAt: handle.run.startedAt,
|
|
560
|
-
updatedAt: now
|
|
561
|
-
});
|
|
562
|
-
const accepted = await this.options.store.saveMessageAcceptance({
|
|
563
|
-
conversationId: conversation.id,
|
|
564
|
-
modelId: conversation.modelId,
|
|
565
|
-
activityAt: now
|
|
566
|
-
});
|
|
567
|
-
return { conversation: this.summary(accepted.conversation), run: handle.run };
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
finally {
|
|
571
|
-
barrier.resolve();
|
|
572
|
-
this.nativeAdmissions.delete(handle);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
if (!this.interruptActiveRun(payload.userId, conversation, handle))
|
|
576
|
-
throw new Error('HOSTED_CONVERSATION_RUN_ACTIVE');
|
|
577
|
-
await this.runSettlements.get(conversation.id)?.catch(() => undefined);
|
|
578
|
-
return this.startMessage(payload, input, steerPrompt);
|
|
579
|
-
}
|
|
580
|
-
async startMessage(payload, input, steerPrompt) {
|
|
581
|
-
let conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
|
|
582
|
-
this.assertTemplateContext(payload, conversation);
|
|
583
|
-
if (this.cleanupLocks.has(conversation.id))
|
|
584
|
-
throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
|
|
585
|
-
if (this.historyMutations.has(conversation.id))
|
|
586
|
-
throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
|
|
587
|
-
if (conversation.status !== 'ACTIVE')
|
|
588
|
-
throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
|
|
589
|
-
if (this.activeRuns.has(conversation.id) || this.runSettlements.has(conversation.id))
|
|
590
|
-
throw new Error('HOSTED_CONVERSATION_RUN_ACTIVE');
|
|
591
|
-
const temporaryTitle = sessionTitleFromFirstMessage(input.prompt);
|
|
592
|
-
const firstNonEmptyMessage = temporaryTitle.length > 0 &&
|
|
593
|
-
!this.options.store.hasNonEmptyUserMessage(payload.userId, conversation.id);
|
|
594
|
-
const models = runtimeModels(payload.models);
|
|
595
|
-
const paths = this.options.store.pathsForConversation(payload.userId, conversation.id);
|
|
596
|
-
let version;
|
|
597
|
-
let selected;
|
|
598
|
-
let mcpServers;
|
|
599
|
-
let upgrade;
|
|
600
|
-
const latestVersionId = conversation.templateDeleted
|
|
601
|
-
? undefined
|
|
602
|
-
: this.latestAvailableTemplateVersionId(conversation.templateId);
|
|
603
|
-
if (latestVersionId !== undefined && latestVersionId !== conversation.activeVersionId) {
|
|
604
|
-
const previousVersionId = conversation.activeVersionId;
|
|
605
|
-
const targetVersionId = latestVersionId;
|
|
606
|
-
try {
|
|
607
|
-
version = await this.loadTemplateVersion(targetVersionId, conversation.templateId);
|
|
608
|
-
selected = selectModel(version, models, input.modelId ?? version.modelPolicy.defaultId);
|
|
609
|
-
mcpServers = resolveMcpServers(version, this.options.secretEnvironment ?? process.env);
|
|
610
|
-
this.pathMounts(paths, version);
|
|
611
|
-
this.processExecutor(paths, version);
|
|
612
|
-
upgrade = { previousVersionId, targetVersionId };
|
|
613
|
-
}
|
|
614
|
-
catch (error) {
|
|
615
|
-
this.publishUpgradeFailure(conversation, previousVersionId, targetVersionId, error);
|
|
616
|
-
throw new Error('HOSTED_CONVERSATION_UPGRADE_FAILED');
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
else {
|
|
620
|
-
version = await this.loadTemplateVersion(conversation.activeVersionId, conversation.templateId);
|
|
621
|
-
selected = selectModel(version, models, input.modelId ?? conversation.modelId);
|
|
622
|
-
}
|
|
623
|
-
const attachments = await Promise.all(input.attachmentIds.map((fileId) => this.requireAttachment(payload.userId, conversation.id, fileId)));
|
|
624
|
-
const prompt = attachmentPrompt(steerPrompt ?? input.prompt, attachments);
|
|
625
|
-
const generatedImagesDirectory = marAgentGeneratedImagesDirectory(paths.homeDirectory, conversation.sessionId);
|
|
626
|
-
const generatedImagesLogicalPath = hostedConversationGeneratedImagesDisplayDirectory(conversation.sessionId);
|
|
627
|
-
if (generatedImagesDirectory === undefined || generatedImagesLogicalPath === undefined)
|
|
628
|
-
throw new Error('HOSTED_CONVERSATION_SESSION_INVALID');
|
|
629
|
-
await mkdir(generatedImagesDirectory, { recursive: true, mode: 0o700 });
|
|
630
|
-
const temporaryDirectory = await this.options.store.prepareConversationTemporaryDirectory(payload.userId, conversation.id);
|
|
631
|
-
const pathMounts = this.pathMounts(paths, version, generatedImagesDirectory, generatedImagesLogicalPath, temporaryDirectory);
|
|
632
|
-
this.publishActivity(payload.userId, conversation, 'STARTING', this.now());
|
|
633
|
-
const pendingRuntimeEvents = [];
|
|
634
|
-
let runtimeEventsAccepted = false;
|
|
635
|
-
const handle = await this.options.runtime
|
|
636
|
-
.startRun({
|
|
637
|
-
...(payload.usageGrant === undefined || this.options.modelUsage === undefined
|
|
638
|
-
? {}
|
|
639
|
-
: { onModelUsage: (usage) => this.options.modelUsage(payload.usageGrant, usage) }),
|
|
640
|
-
conversationId: conversation.id,
|
|
641
|
-
sessionId: conversation.sessionId,
|
|
642
|
-
clientMessageId: input.clientMessageId,
|
|
643
|
-
userPrompt: input.prompt,
|
|
644
|
-
prompt,
|
|
645
|
-
attachmentIds: input.attachmentIds,
|
|
646
|
-
access: 'full-access',
|
|
647
|
-
modelId: selected.id,
|
|
648
|
-
models: networkModels(allowedModels(version, models), version.draft.networkMode),
|
|
649
|
-
...(payload.compact === undefined ? {} : { compact: payload.compact }),
|
|
650
|
-
...(payload.runtimeScopeId === undefined ? {} : { runtimeScopeId: payload.runtimeScopeId }),
|
|
651
|
-
systemInstruction: systemInstruction(version, generatedImagesLogicalPath),
|
|
652
|
-
homeDirectory: paths.homeDirectory,
|
|
653
|
-
workspaceDirectory: version.templateDirectory,
|
|
654
|
-
temporaryDirectory,
|
|
655
|
-
pathMounts,
|
|
656
|
-
processExecutor: this.processExecutor(paths, version, temporaryDirectory, generatedImagesDirectory, generatedImagesLogicalPath),
|
|
657
|
-
mcpServers: (mcpServers ?? resolveMcpServers(version, this.options.secretEnvironment ?? process.env)).map((server) => ({ ...server, authorizationIdentity: `user:${payload.userId}` })),
|
|
658
|
-
webAccess: version.draft.networkMode !== 'NONE',
|
|
659
|
-
onEvent: (event) => {
|
|
660
|
-
if (runtimeEventsAccepted)
|
|
661
|
-
this.publishRuntimeEvent(conversation.id, event);
|
|
662
|
-
else
|
|
663
|
-
pendingRuntimeEvents.push(event);
|
|
664
|
-
}
|
|
665
|
-
})
|
|
666
|
-
.catch((error) => {
|
|
667
|
-
this.publishActivity(payload.userId, conversation, 'IDLE', this.now());
|
|
668
|
-
if (upgrade !== undefined) {
|
|
669
|
-
this.publishUpgradeFailure(conversation, upgrade.previousVersionId, upgrade.targetVersionId, error);
|
|
670
|
-
throw new Error('HOSTED_CONVERSATION_UPGRADE_FAILED');
|
|
671
|
-
}
|
|
672
|
-
throw error;
|
|
673
|
-
});
|
|
674
|
-
const startedAt = handle.run.startedAt;
|
|
675
|
-
let messageCreated = false;
|
|
676
|
-
let accepted;
|
|
677
|
-
try {
|
|
678
|
-
this.options.store.createMessage({
|
|
679
|
-
conversationId: conversation.id,
|
|
680
|
-
clientMessageId: input.clientMessageId,
|
|
681
|
-
runId: handle.run.id,
|
|
682
|
-
userTextPresent: temporaryTitle.length > 0,
|
|
683
|
-
status: 'RUNNING',
|
|
684
|
-
createdAt: startedAt,
|
|
685
|
-
updatedAt: startedAt
|
|
686
|
-
});
|
|
687
|
-
messageCreated = true;
|
|
688
|
-
const configuredAt = this.now();
|
|
689
|
-
accepted = await this.options.store.saveMessageAcceptance({
|
|
690
|
-
conversationId: conversation.id,
|
|
691
|
-
modelId: selected.id,
|
|
692
|
-
activityAt: configuredAt,
|
|
693
|
-
...(upgrade === undefined
|
|
694
|
-
? {}
|
|
695
|
-
: {
|
|
696
|
-
templateVersion: {
|
|
697
|
-
expectedActiveVersionId: upgrade.previousVersionId,
|
|
698
|
-
activeVersionId: upgrade.targetVersionId,
|
|
699
|
-
maxStorageBytes: version.draft.quotas.maxStorageBytes
|
|
700
|
-
}
|
|
701
|
-
}),
|
|
702
|
-
...(firstNonEmptyMessage ? { automaticTitle: temporaryTitle } : {})
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
catch (error) {
|
|
706
|
-
try {
|
|
707
|
-
handle.cancel();
|
|
708
|
-
}
|
|
709
|
-
catch {
|
|
710
|
-
// Preserve the original persistence failure.
|
|
711
|
-
}
|
|
712
|
-
const completion = await handle.completion.catch(() => ({
|
|
713
|
-
status: 'FAILED',
|
|
714
|
-
completedAt: this.now()
|
|
715
|
-
}));
|
|
716
|
-
if (messageCreated)
|
|
717
|
-
try {
|
|
718
|
-
this.options.store.finishMessage(conversation.id, input.clientMessageId, completion.status, completion.completedAt);
|
|
719
|
-
}
|
|
720
|
-
catch {
|
|
721
|
-
// Preserve the original persistence failure.
|
|
722
|
-
}
|
|
723
|
-
this.publishActivity(payload.userId, conversation, 'IDLE', completion.completedAt);
|
|
724
|
-
if (upgrade !== undefined)
|
|
725
|
-
this.publishUpgradeFailure(conversation, upgrade.previousVersionId, upgrade.targetVersionId, error);
|
|
726
|
-
throw error;
|
|
727
|
-
}
|
|
728
|
-
conversation = accepted.conversation;
|
|
729
|
-
if (upgrade !== undefined)
|
|
730
|
-
this.publishHostedEvent(conversation.id, {
|
|
731
|
-
conversationId: conversation.id,
|
|
732
|
-
kind: 'upgrade',
|
|
733
|
-
upgrade: {
|
|
734
|
-
status: 'SUCCEEDED',
|
|
735
|
-
previousVersionId: upgrade.previousVersionId,
|
|
736
|
-
targetVersionId: upgrade.targetVersionId,
|
|
737
|
-
activeVersionId: conversation.activeVersionId,
|
|
738
|
-
conversation: this.summary(conversation),
|
|
739
|
-
error: null
|
|
740
|
-
}
|
|
741
|
-
});
|
|
742
|
-
this.activeRuns.set(conversation.id, handle);
|
|
743
|
-
this.publishActivity(payload.userId, conversation, 'RUNNING', handle.run.startedAt);
|
|
744
|
-
if (accepted.titleChanged)
|
|
745
|
-
this.publishHostedEvent(conversation.id, {
|
|
746
|
-
conversationId: conversation.id,
|
|
747
|
-
kind: 'conversation',
|
|
748
|
-
conversation: this.summary(conversation)
|
|
749
|
-
});
|
|
750
|
-
this.publishHostedEvent(conversation.id, {
|
|
751
|
-
conversationId: conversation.id,
|
|
752
|
-
kind: 'run',
|
|
753
|
-
run: handle.run
|
|
754
|
-
});
|
|
755
|
-
runtimeEventsAccepted = true;
|
|
756
|
-
for (const event of pendingRuntimeEvents)
|
|
757
|
-
this.publishRuntimeEvent(conversation.id, event);
|
|
758
|
-
pendingRuntimeEvents.length = 0;
|
|
759
|
-
const settlement = Promise.resolve().then(async () => {
|
|
760
|
-
try {
|
|
761
|
-
const completion = await handle.completion.catch(() => ({
|
|
762
|
-
status: 'FAILED',
|
|
763
|
-
completedAt: this.now()
|
|
764
|
-
}));
|
|
765
|
-
await this.nativeAdmissions.get(handle);
|
|
766
|
-
await this.finishRun(payload.userId, conversation.id, input.clientMessageId, handle.run, completion);
|
|
767
|
-
}
|
|
768
|
-
catch {
|
|
769
|
-
// Run settlement failures must not leave the conversation permanently busy.
|
|
770
|
-
}
|
|
771
|
-
finally {
|
|
772
|
-
this.expectedInterruptions.delete(runInterruptionKey(conversation.id, handle.run.id));
|
|
773
|
-
if (this.runSettlements.get(conversation.id) === settlement)
|
|
774
|
-
this.runSettlements.delete(conversation.id);
|
|
775
|
-
if (this.activeRuns.get(conversation.id) === handle)
|
|
776
|
-
this.activeRuns.delete(conversation.id);
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
|
-
this.runSettlements.set(conversation.id, settlement);
|
|
780
|
-
void settlement;
|
|
781
|
-
return {
|
|
782
|
-
conversation: this.summary(conversation),
|
|
783
|
-
run: handle.run
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
rollback(raw) {
|
|
787
|
-
if (this.disablePrepared)
|
|
788
|
-
return Promise.reject(new Error('CONVERSATION_HOSTING_UNAVAILABLE'));
|
|
789
|
-
const payload = hostedRequest(raw);
|
|
790
|
-
const input = hostedConversationRollbackInputSchema.parse(payload.data);
|
|
791
|
-
const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
|
|
792
|
-
this.assertTemplateContext(payload, conversation);
|
|
793
|
-
if (conversation.status !== 'ACTIVE' ||
|
|
794
|
-
this.cleanupLocks.has(conversation.id) ||
|
|
795
|
-
this.historyMutations.has(conversation.id))
|
|
796
|
-
return Promise.reject(new Error('HOSTED_CONVERSATION_STATE_INVALID'));
|
|
797
|
-
if (this.activity(conversation.id).status !== 'IDLE' ||
|
|
798
|
-
this.activeRuns.has(conversation.id) ||
|
|
799
|
-
this.runSettlements.has(conversation.id) ||
|
|
800
|
-
[...this.pendingMessages.keys()].some((key) => key.startsWith(`${conversation.id}\0`)))
|
|
801
|
-
return Promise.reject(new Error('HOSTED_CONVERSATION_RUN_ACTIVE'));
|
|
802
|
-
const operation = this.rollbackConversation(payload, conversation, input);
|
|
803
|
-
this.historyMutations.set(conversation.id, operation);
|
|
804
|
-
void operation
|
|
805
|
-
.finally(() => {
|
|
806
|
-
if (this.historyMutations.get(conversation.id) === operation)
|
|
807
|
-
this.historyMutations.delete(conversation.id);
|
|
808
|
-
})
|
|
809
|
-
.catch(() => undefined);
|
|
810
|
-
return operation;
|
|
811
|
-
}
|
|
812
|
-
async rollbackConversation(payload, conversation, input) {
|
|
813
|
-
const paths = this.options.store.pathsForConversation(payload.userId, conversation.id);
|
|
814
|
-
const turns = await this.readAllHistory(conversation, paths);
|
|
815
|
-
const targetIndex = turns.findIndex((turn) => turn.id === input.turnId);
|
|
816
|
-
const target = turns[targetIndex];
|
|
817
|
-
const selectedInput = target === undefined ? undefined : resolveSessionRollbackTarget(target, input.itemId);
|
|
818
|
-
if (target === undefined ||
|
|
819
|
-
!terminalHostedTurnStatus(target.status) ||
|
|
820
|
-
selectedInput === undefined ||
|
|
821
|
-
target.rewind?.before !== input.boundary)
|
|
822
|
-
throw new Error('HOSTED_CONVERSATION_ROLLBACK_TARGET_INVALID');
|
|
823
|
-
let rolledBack;
|
|
824
|
-
try {
|
|
825
|
-
rolledBack = await this.options.runtime.rollback({
|
|
826
|
-
conversationId: conversation.id,
|
|
827
|
-
sessionId: conversation.sessionId,
|
|
828
|
-
homeDirectory: paths.homeDirectory,
|
|
829
|
-
modelId: conversation.modelId,
|
|
830
|
-
boundaryTurnId: input.boundary,
|
|
831
|
-
...(selectedInput.clientMessageId === undefined
|
|
832
|
-
? {}
|
|
833
|
-
: {
|
|
834
|
-
beforeInput: {
|
|
835
|
-
turnId: target.rewind.after,
|
|
836
|
-
clientMessageId: selectedInput.clientMessageId
|
|
837
|
-
}
|
|
838
|
-
})
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
catch (error) {
|
|
842
|
-
if (error instanceof Error && error.message === 'MAR_AGENT_ROLLBACK_TARGET_INVALID')
|
|
843
|
-
throw new Error('HOSTED_CONVERSATION_ROLLBACK_TARGET_INVALID', { cause: error });
|
|
844
|
-
throw error;
|
|
845
|
-
}
|
|
846
|
-
this.historyVersions.set(conversation.id, (this.historyVersions.get(conversation.id) ?? 0) + 1);
|
|
847
|
-
await this.options.store.advanceLastActivity(conversation.id, Math.max(this.now(), rolledBack.lastActivityAt));
|
|
848
|
-
const snapshot = await this.snapshot(payload.userId, conversation.id, runtimeModels(payload.models), payload.templateDeleted, true);
|
|
849
|
-
this.publishHostedSnapshot(conversation.id, snapshot);
|
|
850
|
-
return snapshot;
|
|
851
|
-
}
|
|
852
|
-
async readAllHistory(conversation, paths) {
|
|
853
|
-
const turns = [];
|
|
854
|
-
let cursor;
|
|
855
|
-
do {
|
|
856
|
-
const page = await this.options.runtime.history({
|
|
857
|
-
conversationId: conversation.id,
|
|
858
|
-
sessionId: conversation.sessionId,
|
|
859
|
-
homeDirectory: paths.homeDirectory,
|
|
860
|
-
modelId: conversation.modelId,
|
|
861
|
-
limit: 100,
|
|
862
|
-
...(cursor === undefined ? {} : { cursor })
|
|
863
|
-
});
|
|
864
|
-
turns.unshift(...page.turns.map((turn) => conversationTurnSchema.parse(turn)));
|
|
865
|
-
cursor = page.nextCursor ?? undefined;
|
|
866
|
-
} while (cursor !== undefined);
|
|
867
|
-
return turns;
|
|
868
|
-
}
|
|
869
|
-
async finishRun(userId, conversationId, clientMessageId, run, completion) {
|
|
870
|
-
const interruptionKey = runInterruptionKey(conversationId, run.id);
|
|
871
|
-
const expectedInterruption = this.expectedInterruptions.has(interruptionKey);
|
|
872
|
-
let conversation;
|
|
873
|
-
try {
|
|
874
|
-
this.options.store.finishRunMessages(conversationId, run.id, completion.status, completion.completedAt);
|
|
875
|
-
try {
|
|
876
|
-
const files = await this.options.store.reconcileFiles(userId, conversationId);
|
|
877
|
-
this.publishHostedEvent(conversationId, {
|
|
878
|
-
conversationId,
|
|
879
|
-
kind: 'files',
|
|
880
|
-
files,
|
|
881
|
-
error: null
|
|
882
|
-
});
|
|
883
|
-
}
|
|
884
|
-
catch (error) {
|
|
885
|
-
this.publishHostedEvent(conversationId, {
|
|
886
|
-
conversationId,
|
|
887
|
-
kind: 'files',
|
|
888
|
-
files: this.options.store.listFiles(userId, conversationId),
|
|
889
|
-
error: {
|
|
890
|
-
code: safeErrorCode(error, 'HOSTED_CONVERSATION_FILE_RECONCILE_FAILED'),
|
|
891
|
-
message: 'Hosted conversation file projection failed.'
|
|
892
|
-
}
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
this.publishHostedEvent(conversationId, {
|
|
896
|
-
conversationId,
|
|
897
|
-
kind: 'run',
|
|
898
|
-
run: {
|
|
899
|
-
id: run.id,
|
|
900
|
-
status: completion.status,
|
|
901
|
-
startedAt: run.startedAt,
|
|
902
|
-
completedAt: completion.completedAt
|
|
903
|
-
}
|
|
904
|
-
});
|
|
905
|
-
void this.refreshReadyWatchSnapshots(conversationId, run.id);
|
|
906
|
-
await this.options.store.advanceLastActivity(conversationId, completion.completedAt);
|
|
907
|
-
await this.applyAutomaticTitle(conversationId, completion.title, completion.completedAt).catch(() => undefined);
|
|
908
|
-
conversation = this.options.store.requireConversation(userId, conversationId);
|
|
909
|
-
}
|
|
910
|
-
finally {
|
|
911
|
-
this.expectedInterruptions.delete(interruptionKey);
|
|
912
|
-
if (conversation === undefined)
|
|
913
|
-
try {
|
|
914
|
-
conversation = this.options.store.requireConversation(userId, conversationId);
|
|
915
|
-
}
|
|
916
|
-
catch {
|
|
917
|
-
// The conversation may have been deleted while the Run was settling.
|
|
918
|
-
}
|
|
919
|
-
if (conversation !== undefined)
|
|
920
|
-
this.publishActivity(userId, conversation, 'IDLE', completion.completedAt);
|
|
921
|
-
}
|
|
922
|
-
if (conversation !== undefined &&
|
|
923
|
-
(completion.status !== 'INTERRUPTED' || !expectedInterruption))
|
|
924
|
-
this.publishTerminalNotification(userId, conversation, run.id, completion);
|
|
925
|
-
}
|
|
926
|
-
async applyAutomaticTitle(conversationId, value, updatedAt) {
|
|
927
|
-
const title = automaticHostedConversationTitle(value);
|
|
928
|
-
if (title === undefined)
|
|
929
|
-
return;
|
|
930
|
-
const saved = await this.options.store.saveAutomaticTitle(conversationId, title, updatedAt);
|
|
931
|
-
if (saved === undefined)
|
|
932
|
-
return;
|
|
933
|
-
this.publishHostedEvent(conversationId, {
|
|
934
|
-
conversationId,
|
|
935
|
-
kind: 'conversation',
|
|
936
|
-
conversation: this.summary(saved)
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
interrupt(raw) {
|
|
940
|
-
const payload = hostedRequest(raw);
|
|
941
|
-
const input = hostedConversationIdInputSchema.parse(payload.data);
|
|
942
|
-
const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
|
|
943
|
-
const handle = this.activeRuns.get(input.conversationId);
|
|
944
|
-
if (handle === undefined)
|
|
945
|
-
return { interrupted: false };
|
|
946
|
-
return { interrupted: this.interruptActiveRun(payload.userId, conversation, handle) };
|
|
947
|
-
}
|
|
948
|
-
interruptActiveRun(userId, conversation, handle) {
|
|
949
|
-
const interruptionKey = runInterruptionKey(conversation.id, handle.run.id);
|
|
950
|
-
this.expectedInterruptions.add(interruptionKey);
|
|
951
|
-
try {
|
|
952
|
-
const interrupted = handle.cancel();
|
|
953
|
-
if (!interrupted) {
|
|
954
|
-
this.expectedInterruptions.delete(interruptionKey);
|
|
955
|
-
return false;
|
|
956
|
-
}
|
|
957
|
-
this.publishActivity(userId, conversation, 'CANCELLING', this.now());
|
|
958
|
-
return true;
|
|
959
|
-
}
|
|
960
|
-
catch (error) {
|
|
961
|
-
this.expectedInterruptions.delete(interruptionKey);
|
|
962
|
-
throw error;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
switchTemplate(raw) {
|
|
966
|
-
if (this.closing || this.disablePrepared)
|
|
967
|
-
throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
|
|
968
|
-
const payload = hostedRequest(raw);
|
|
969
|
-
const input = hostedConversationTemplateSwitchInputSchema.parse(payload.data);
|
|
970
|
-
const conversation = this.requireWritableConversation(payload.userId, input.conversationId);
|
|
971
|
-
if (this.fileWrites.has(conversation.id) ||
|
|
972
|
-
[...this.uploads.values()].some((upload) => upload.conversationId === conversation.id && upload.status === 'UPLOADING'))
|
|
973
|
-
throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
|
|
974
|
-
if (this.activeRuns.has(conversation.id) ||
|
|
975
|
-
this.runSettlements.has(conversation.id) ||
|
|
976
|
-
[...this.pendingMessages.keys()].some((key) => key.startsWith(`${conversation.id}\0`)))
|
|
977
|
-
throw new Error('HOSTED_CONVERSATION_RUN_ACTIVE');
|
|
978
|
-
const template = conversationTemplateSummarySchema.parse(payload.template);
|
|
979
|
-
if (template.id !== input.templateId ||
|
|
980
|
-
template.nodeId !== conversation.nodeId ||
|
|
981
|
-
template.status !== 'ACTIVE' ||
|
|
982
|
-
!template.available ||
|
|
983
|
-
template.currentVersionId === null)
|
|
984
|
-
throw new Error('HOSTED_CONVERSATION_TEMPLATE_UNAVAILABLE');
|
|
985
|
-
return this.withConversationChange(conversation.id, async () => {
|
|
986
|
-
const current = this.options.store.requireConversation(payload.userId, conversation.id);
|
|
987
|
-
const unchanged = current.templateId === input.templateId;
|
|
988
|
-
if (!unchanged && current.templateId !== input.expectedTemplateId)
|
|
989
|
-
throw new Error('HOSTED_CONVERSATION_TEMPLATE_CONFLICT');
|
|
990
|
-
const version = await this.loadTemplateVersion(unchanged ? current.activeVersionId : template.currentVersionId, template.id);
|
|
991
|
-
const models = networkModels(allowedModels(version, runtimeModels(payload.models)), version.draft.networkMode).filter((model) => model.conversationModel !== false);
|
|
992
|
-
let saved = current;
|
|
993
|
-
if (!unchanged) {
|
|
994
|
-
const selected = selectModel(version, models, current.modelId);
|
|
995
|
-
resolveMcpServers(version, this.options.secretEnvironment ?? process.env);
|
|
996
|
-
const paths = this.options.store.pathsForConversation(payload.userId, current.id);
|
|
997
|
-
this.pathMounts(paths, version);
|
|
998
|
-
this.processExecutor(paths, version);
|
|
999
|
-
if (current.quota.usedBytes > version.draft.quotas.maxStorageBytes)
|
|
1000
|
-
throw new Error('HOSTED_CONVERSATION_QUOTA_EXCEEDED');
|
|
1001
|
-
const updatedAt = this.now();
|
|
1002
|
-
saved = await this.options.store.updateConversation(current.id, (latest) => ({
|
|
1003
|
-
...latest,
|
|
1004
|
-
templateId: template.id,
|
|
1005
|
-
templateName: template.name,
|
|
1006
|
-
templateDeleted: false,
|
|
1007
|
-
activeVersionId: version.id,
|
|
1008
|
-
modelId: selected.id,
|
|
1009
|
-
quota: { ...latest.quota, maxBytes: version.draft.quotas.maxStorageBytes },
|
|
1010
|
-
updatedAt,
|
|
1011
|
-
lastActivityAt: updatedAt
|
|
1012
|
-
}));
|
|
1013
|
-
try {
|
|
1014
|
-
this.publishHostedEvent(saved.id, { conversationId: saved.id, kind: 'template-changed' });
|
|
1015
|
-
}
|
|
1016
|
-
catch {
|
|
1017
|
-
// Committed configuration does not depend on best-effort Watch delivery.
|
|
1018
|
-
}
|
|
1019
|
-
finally {
|
|
1020
|
-
for (const [key, lease] of this.watchLeases)
|
|
1021
|
-
if (lease.conversationId === saved.id)
|
|
1022
|
-
this.removeWatchLease(key);
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
return hostedConversationTemplateSwitchResultSchema.parse({
|
|
1026
|
-
conversation: this.summary(saved),
|
|
1027
|
-
template: templateSummary(saved, version.draft),
|
|
1028
|
-
models: models.map(modelSummary)
|
|
1029
|
-
});
|
|
1030
|
-
});
|
|
1031
|
-
}
|
|
1032
|
-
withConversationChange(conversationId, change) {
|
|
1033
|
-
if (this.historyMutations.has(conversationId))
|
|
1034
|
-
throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
|
|
1035
|
-
const operation = Promise.resolve().then(change);
|
|
1036
|
-
this.historyMutations.set(conversationId, operation);
|
|
1037
|
-
void operation
|
|
1038
|
-
.finally(() => {
|
|
1039
|
-
if (this.historyMutations.get(conversationId) === operation)
|
|
1040
|
-
this.historyMutations.delete(conversationId);
|
|
1041
|
-
})
|
|
1042
|
-
.catch(() => undefined);
|
|
1043
|
-
return operation;
|
|
1044
|
-
}
|
|
1045
|
-
updateConfiguration(raw) {
|
|
1046
|
-
const payload = hostedRequest(raw);
|
|
1047
|
-
const input = hostedConversationConfigUpdateInputSchema.parse(payload.data);
|
|
1048
|
-
const conversation = this.requireWritableConversation(payload.userId, input.conversationId);
|
|
1049
|
-
this.assertTemplateContext(payload, conversation);
|
|
1050
|
-
return this.withConversationChange(conversation.id, () => this.saveConfiguration(payload, input, conversation));
|
|
1051
|
-
}
|
|
1052
|
-
async saveConfiguration(payload, input, conversation) {
|
|
1053
|
-
if (this.cleanupLocks.has(conversation.id))
|
|
1054
|
-
throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
|
|
1055
|
-
const latestVersionId = conversation.templateDeleted
|
|
1056
|
-
? undefined
|
|
1057
|
-
: this.latestAvailableTemplateVersionId(conversation.templateId);
|
|
1058
|
-
const displayVersionId = latestVersionId ?? conversation.activeVersionId;
|
|
1059
|
-
const version = await this.loadTemplateVersion(displayVersionId, conversation.templateId);
|
|
1060
|
-
const models = runtimeModels(payload.models);
|
|
1061
|
-
const selected = selectModel(version, models, input.modelId ?? conversation.modelId);
|
|
1062
|
-
const updatedAt = this.now();
|
|
1063
|
-
const saved = await this.options.store.updateConversation(conversation.id, (current) => ({
|
|
1064
|
-
...current,
|
|
1065
|
-
modelId: selected.id,
|
|
1066
|
-
updatedAt,
|
|
1067
|
-
lastActivityAt: updatedAt
|
|
1068
|
-
}));
|
|
1069
|
-
this.publishHostedEvent(conversation.id, {
|
|
1070
|
-
conversationId: conversation.id,
|
|
1071
|
-
kind: 'configuration',
|
|
1072
|
-
conversation: this.summary(saved)
|
|
1073
|
-
});
|
|
1074
|
-
return { conversation: this.summary(saved) };
|
|
1075
|
-
}
|
|
1076
|
-
async updateMetadata(raw) {
|
|
1077
|
-
const payload = hostedRequest(raw);
|
|
1078
|
-
const input = hostedConversationMetadataUpdateInputSchema.parse(payload.data);
|
|
1079
|
-
const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
|
|
1080
|
-
if (this.cleanupLocks.has(conversation.id))
|
|
1081
|
-
throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
|
|
1082
|
-
const updatedAt = this.now();
|
|
1083
|
-
const saved = await this.options.store.updateConversation(conversation.id, (current) => ({
|
|
1084
|
-
...current,
|
|
1085
|
-
title: input.title ?? current.title,
|
|
1086
|
-
titleSource: input.title === undefined ? current.titleSource : 'CUSTOM',
|
|
1087
|
-
pinnedAt: input.pinned === undefined ? current.pinnedAt : input.pinned ? updatedAt : null,
|
|
1088
|
-
updatedAt,
|
|
1089
|
-
lastActivityAt: current.lastActivityAt
|
|
1090
|
-
}));
|
|
1091
|
-
this.publishHostedEvent(conversation.id, {
|
|
1092
|
-
conversationId: conversation.id,
|
|
1093
|
-
kind: 'configuration',
|
|
1094
|
-
conversation: this.summary(saved)
|
|
1095
|
-
});
|
|
1096
|
-
return { conversation: this.summary(saved) };
|
|
1097
|
-
}
|
|
1098
|
-
async listFiles(raw) {
|
|
1099
|
-
const payload = hostedRequest(raw);
|
|
1100
|
-
const input = hostedConversationIdInputSchema.parse(payload.data);
|
|
1101
|
-
return hostedConversationFilesSchema.parse({
|
|
1102
|
-
files: this.options.store.listFiles(payload.userId, input.conversationId)
|
|
1103
|
-
});
|
|
1104
|
-
}
|
|
1105
|
-
async readToolResource(raw) {
|
|
1106
|
-
const payload = hostedRequest(raw);
|
|
1107
|
-
const { conversationId } = hostedConversationIdInputSchema.parse(payload.data);
|
|
1108
|
-
const conversation = this.options.store.requireConversation(payload.userId, conversationId);
|
|
1109
|
-
const paths = this.options.store.pathsForConversation(payload.userId, conversation.id);
|
|
1110
|
-
const data = payload.data;
|
|
1111
|
-
const input = toolResultResourceInputSchema.parse({
|
|
1112
|
-
resourceRef: data.resourceRef,
|
|
1113
|
-
cursor: data.cursor,
|
|
1114
|
-
maxBytes: data.maxBytes
|
|
1115
|
-
});
|
|
1116
|
-
if (!this.options.runtime.readToolResultResource)
|
|
1117
|
-
throw new Error('TOOL_RESOURCE_UNSUPPORTED');
|
|
1118
|
-
return toolResultResourcePageSchema.parse(await this.options.runtime.readToolResultResource({
|
|
1119
|
-
homeDirectory: paths.homeDirectory,
|
|
1120
|
-
sessionId: conversation.sessionId,
|
|
1121
|
-
...input
|
|
1122
|
-
}));
|
|
1123
|
-
}
|
|
1124
|
-
async readImage(raw) {
|
|
1125
|
-
const payload = hostedRequest(raw);
|
|
1126
|
-
const input = hostedConversationImageReadInputSchema.parse(payload.data);
|
|
1127
|
-
this.options.store.requireConversation(payload.userId, input.conversationId);
|
|
1128
|
-
if (input.runId !== hostedConversationImageRunId(input.conversationId))
|
|
1129
|
-
throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
|
|
1130
|
-
const image = await this.options.readImage(input.runId, input.imageRef);
|
|
1131
|
-
if (image === undefined)
|
|
1132
|
-
throw new Error('RUN_IMAGE_NOT_FOUND');
|
|
1133
|
-
return {
|
|
1134
|
-
image: input.preview === undefined ? image : await imagePreviews.fromImage(image, input.preview)
|
|
1135
|
-
};
|
|
1136
|
-
}
|
|
1137
|
-
async readFile(raw) {
|
|
1138
|
-
const payload = hostedRequest(raw);
|
|
1139
|
-
const input = hostedConversationFileReadInputSchema.parse(payload.data);
|
|
1140
|
-
const file = this.options.store.requireFile(payload.userId, input.conversationId, input.fileId);
|
|
1141
|
-
const path = this.options.store.filePath(payload.userId, file);
|
|
1142
|
-
const offset = input.preview === undefined ? (input.offset ?? 0) : 0;
|
|
1143
|
-
const limit = Math.min(input.limit ?? MAX_FILE_READ_BYTES, MAX_FILE_READ_BYTES);
|
|
1144
|
-
const handle = await openRegularFile(path);
|
|
1145
|
-
try {
|
|
1146
|
-
const before = await handle.stat();
|
|
1147
|
-
assertRegularFileMetadata(before);
|
|
1148
|
-
const pathMetadata = await regularFileMetadata(path);
|
|
1149
|
-
if (!sameFileIdentity(before, pathMetadata))
|
|
1150
|
-
throw new Error('HOSTED_CONVERSATION_FILE_INVALID');
|
|
1151
|
-
if (offset > before.size)
|
|
1152
|
-
throw new Error('HOSTED_CONVERSATION_FILE_RANGE_INVALID');
|
|
1153
|
-
if (input.preview !== undefined && before.size > MAX_IMAGE_PREVIEW_SOURCE_BYTES)
|
|
1154
|
-
throw new Error('IMAGE_PREVIEW_TOO_LARGE');
|
|
1155
|
-
const buffer = Buffer.alloc(input.preview === undefined
|
|
1156
|
-
? Math.min(limit, Math.max(0, before.size - offset))
|
|
1157
|
-
: before.size);
|
|
1158
|
-
const read = await handle.read(buffer, 0, buffer.length, offset);
|
|
1159
|
-
if (input.preview !== undefined) {
|
|
1160
|
-
while (read.bytesRead < buffer.length) {
|
|
1161
|
-
const next = await handle.read(buffer, read.bytesRead, buffer.length - read.bytesRead, read.bytesRead);
|
|
1162
|
-
if (next.bytesRead === 0)
|
|
1163
|
-
throw new Error('HOSTED_CONVERSATION_FILE_INVALID');
|
|
1164
|
-
read.bytesRead += next.bytesRead;
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
const after = await handle.stat();
|
|
1168
|
-
const currentPathMetadata = await regularFileMetadata(path);
|
|
1169
|
-
if (!sameFileSnapshot(before, after) || !sameFileSnapshot(after, currentPathMetadata))
|
|
1170
|
-
throw new Error('HOSTED_CONVERSATION_FILE_INVALID');
|
|
1171
|
-
if (input.preview !== undefined)
|
|
1172
|
-
return { image: await imagePreviews.create(buffer, input.preview) };
|
|
1173
|
-
const next = offset + read.bytesRead;
|
|
1174
|
-
const eof = next >= after.size;
|
|
1175
|
-
const currentFile = {
|
|
1176
|
-
...file,
|
|
1177
|
-
size: after.size,
|
|
1178
|
-
updatedAt: Math.max(file.createdAt, safeFileTimestamp(after.mtimeMs), safeFileTimestamp(after.ctimeMs))
|
|
1179
|
-
};
|
|
1180
|
-
return hostedConversationFileReadResultSchema.parse({
|
|
1181
|
-
file: currentFile,
|
|
1182
|
-
encoding: 'base64',
|
|
1183
|
-
content: buffer.subarray(0, read.bytesRead).toString('base64'),
|
|
1184
|
-
offset,
|
|
1185
|
-
nextOffset: eof ? null : next,
|
|
1186
|
-
eof
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
|
-
finally {
|
|
1190
|
-
await handle.close();
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
async deleteFile(raw) {
|
|
1194
|
-
const payload = hostedRequest(raw);
|
|
1195
|
-
const input = hostedConversationFileDeleteInputSchema.parse(payload.data);
|
|
1196
|
-
this.requireWritableConversation(payload.userId, input.conversationId);
|
|
1197
|
-
const deleteKey = `${input.conversationId}\0${input.fileId}`;
|
|
1198
|
-
if (this.fileDeletes.has(deleteKey))
|
|
1199
|
-
throw new Error('HOSTED_CONVERSATION_FILE_CONFLICT');
|
|
1200
|
-
this.fileDeletes.add(deleteKey);
|
|
1201
|
-
try {
|
|
1202
|
-
const files = await this.options.store.deleteFile(payload.userId, input.conversationId, input.fileId, this.now());
|
|
1203
|
-
const result = hostedConversationFileDeleteResultSchema.parse({
|
|
1204
|
-
deletedFileId: input.fileId,
|
|
1205
|
-
files
|
|
1206
|
-
});
|
|
1207
|
-
this.publishHostedEvent(input.conversationId, {
|
|
1208
|
-
conversationId: input.conversationId,
|
|
1209
|
-
kind: 'files',
|
|
1210
|
-
files: result.files,
|
|
1211
|
-
error: null
|
|
1212
|
-
});
|
|
1213
|
-
return result;
|
|
1214
|
-
}
|
|
1215
|
-
finally {
|
|
1216
|
-
this.fileDeletes.delete(deleteKey);
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
async preflightUpload(raw) {
|
|
1220
|
-
const payload = hostedRequest(raw);
|
|
1221
|
-
const input = hostedConversationUploadPreflightInputSchema.parse(payload.data);
|
|
1222
|
-
const conversation = this.requireWritableConversation(payload.userId, input.conversationId);
|
|
1223
|
-
const version = await this.loadTemplateVersion(conversation.activeVersionId, conversation.templateId);
|
|
1224
|
-
const files = await Promise.all(input.files.map(async (file) => ({
|
|
1225
|
-
name: file.name,
|
|
1226
|
-
status: await this.uploadPreflightStatus(conversation, version.draft, file.name, file.size)
|
|
1227
|
-
})));
|
|
1228
|
-
return hostedConversationUploadPreflightResultSchema.parse({ files });
|
|
1229
|
-
}
|
|
1230
|
-
async createUpload(raw) {
|
|
1231
|
-
const payload = hostedRequest(raw);
|
|
1232
|
-
const input = hostedConversationUploadCreateInputSchema.parse(payload.data);
|
|
1233
|
-
const conversation = this.requireWritableConversation(payload.userId, input.conversationId);
|
|
1234
|
-
const requestKey = `${payload.userId}\0${conversation.id}\0${input.clientRequestId}`;
|
|
1235
|
-
const existingId = this.uploadsByRequest.get(requestKey);
|
|
1236
|
-
if (existingId !== undefined) {
|
|
1237
|
-
const existing = this.uploads.get(existingId);
|
|
1238
|
-
if (existing !== undefined)
|
|
1239
|
-
return { upload: hostedConversationUploadSchema.parse(uploadProjection(existing)) };
|
|
1240
|
-
}
|
|
1241
|
-
const pending = this.pendingUploadCreates.get(requestKey);
|
|
1242
|
-
if (pending !== undefined)
|
|
1243
|
-
return pending;
|
|
1244
|
-
const operation = this.createUploadForRequest(payload, input, conversation, requestKey);
|
|
1245
|
-
this.pendingUploadCreates.set(requestKey, operation);
|
|
1246
|
-
void operation
|
|
1247
|
-
.finally(() => {
|
|
1248
|
-
if (this.pendingUploadCreates.get(requestKey) === operation)
|
|
1249
|
-
this.pendingUploadCreates.delete(requestKey);
|
|
1250
|
-
})
|
|
1251
|
-
.catch(() => undefined);
|
|
1252
|
-
return operation;
|
|
1253
|
-
}
|
|
1254
|
-
async createUploadForRequest(payload, input, conversation, requestKey) {
|
|
1255
|
-
const version = await this.loadTemplateVersion(conversation.activeVersionId, conversation.templateId);
|
|
1256
|
-
const paths = this.options.store.pathsForConversation(payload.userId, conversation.id);
|
|
1257
|
-
const status = await this.uploadPreflightStatus(conversation, version.draft, input.name, input.size);
|
|
1258
|
-
if (status === 'QUOTA_EXCEEDED' || status === 'TOO_LARGE')
|
|
1259
|
-
throw new Error('HOSTED_CONVERSATION_QUOTA_EXCEEDED');
|
|
1260
|
-
if (status !== 'READY')
|
|
1261
|
-
throw new Error('HOSTED_CONVERSATION_FILE_INVALID');
|
|
1262
|
-
const uploadId = this.id();
|
|
1263
|
-
const temporaryPath = join(paths.uploadsDirectory, `.${uploadId}.upload`);
|
|
1264
|
-
const name = await this.allocateUploadName(paths, payload.userId, conversation.id, input.name);
|
|
1265
|
-
const targetPath = join(paths.uploadsDirectory, name);
|
|
1266
|
-
try {
|
|
1267
|
-
const handle = await open(temporaryPath, 'wx', 0o600);
|
|
1268
|
-
await handle.close();
|
|
1269
|
-
}
|
|
1270
|
-
catch (error) {
|
|
1271
|
-
this.releaseUploadName(conversation.id, name);
|
|
1272
|
-
throw error;
|
|
1273
|
-
}
|
|
1274
|
-
const upload = {
|
|
1275
|
-
uploadId,
|
|
1276
|
-
conversationId: conversation.id,
|
|
1277
|
-
userId: payload.userId,
|
|
1278
|
-
clientRequestId: input.clientRequestId,
|
|
1279
|
-
name,
|
|
1280
|
-
mimeType: input.mimeType,
|
|
1281
|
-
size: input.size,
|
|
1282
|
-
temporaryPath,
|
|
1283
|
-
targetPath,
|
|
1284
|
-
receivedBytes: 0,
|
|
1285
|
-
status: 'UPLOADING'
|
|
1286
|
-
};
|
|
1287
|
-
this.uploads.set(uploadId, upload);
|
|
1288
|
-
this.uploadsByRequest.set(requestKey, uploadId);
|
|
1289
|
-
return { upload: hostedConversationUploadSchema.parse(uploadProjection(upload)) };
|
|
1290
|
-
}
|
|
1291
|
-
async writeUploadChunk(raw) {
|
|
1292
|
-
const payload = hostedRequest(raw);
|
|
1293
|
-
const input = hostedConversationUploadChunkInputSchema.parse(payload.data);
|
|
1294
|
-
const upload = this.requireUpload(payload.userId, input.conversationId, input.uploadId);
|
|
1295
|
-
if (upload.status !== 'UPLOADING')
|
|
1296
|
-
throw new Error('HOSTED_CONVERSATION_UPLOAD_STATE_INVALID');
|
|
1297
|
-
if (input.offset !== upload.receivedBytes)
|
|
1298
|
-
throw new Error('HOSTED_CONVERSATION_UPLOAD_OFFSET_INVALID');
|
|
1299
|
-
const chunk = Buffer.from(input.dataBase64, 'base64');
|
|
1300
|
-
if (upload.receivedBytes + chunk.byteLength > upload.size)
|
|
1301
|
-
throw new Error('HOSTED_CONVERSATION_UPLOAD_SIZE_INVALID');
|
|
1302
|
-
await assertUploadTemporaryFile(upload.temporaryPath, upload.receivedBytes);
|
|
1303
|
-
const handle = await open(upload.temporaryPath, 'r+');
|
|
1304
|
-
try {
|
|
1305
|
-
await handle.write(chunk, 0, chunk.byteLength, upload.receivedBytes);
|
|
1306
|
-
}
|
|
1307
|
-
finally {
|
|
1308
|
-
await handle.close();
|
|
1309
|
-
}
|
|
1310
|
-
upload.receivedBytes += chunk.byteLength;
|
|
1311
|
-
return { upload: hostedConversationUploadSchema.parse(uploadProjection(upload)) };
|
|
1312
|
-
}
|
|
1313
|
-
uploadStatus(raw) {
|
|
1314
|
-
const payload = hostedRequest(raw);
|
|
1315
|
-
const input = hostedConversationUploadControlInputSchema.parse(payload.data);
|
|
1316
|
-
const upload = this.requireUpload(payload.userId, input.conversationId, input.uploadId);
|
|
1317
|
-
return { upload: hostedConversationUploadSchema.parse(uploadProjection(upload)) };
|
|
1318
|
-
}
|
|
1319
|
-
async completeUpload(raw) {
|
|
1320
|
-
const payload = hostedRequest(raw);
|
|
1321
|
-
const input = hostedConversationUploadControlInputSchema.parse(payload.data);
|
|
1322
|
-
const upload = this.requireUpload(payload.userId, input.conversationId, input.uploadId);
|
|
1323
|
-
if (upload.status === 'COMPLETED')
|
|
1324
|
-
return { upload: hostedConversationUploadSchema.parse(uploadProjection(upload)) };
|
|
1325
|
-
if (upload.status !== 'UPLOADING' || upload.receivedBytes !== upload.size)
|
|
1326
|
-
throw new Error('HOSTED_CONVERSATION_UPLOAD_INCOMPLETE');
|
|
1327
|
-
const conversation = this.requireWritableConversation(payload.userId, upload.conversationId);
|
|
1328
|
-
const version = await this.loadTemplateVersion(conversation.activeVersionId, conversation.templateId);
|
|
1329
|
-
if (upload.size > version.draft.quotas.maxUploadBytes ||
|
|
1330
|
-
conversation.quota.usedBytes + upload.size > conversation.quota.maxBytes)
|
|
1331
|
-
throw new Error('HOSTED_CONVERSATION_QUOTA_EXCEEDED');
|
|
1332
|
-
await assertUploadTemporaryFile(upload.temporaryPath, upload.size);
|
|
1333
|
-
const target = await lstat(upload.targetPath).catch((error) => {
|
|
1334
|
-
if (error.code === 'ENOENT')
|
|
1335
|
-
return undefined;
|
|
1336
|
-
throw error;
|
|
1337
|
-
});
|
|
1338
|
-
if (target !== undefined)
|
|
1339
|
-
throw new Error('HOSTED_CONVERSATION_FILE_INVALID');
|
|
1340
|
-
const handle = await open(upload.temporaryPath, 'r+');
|
|
1341
|
-
try {
|
|
1342
|
-
await handle.sync();
|
|
1343
|
-
}
|
|
1344
|
-
finally {
|
|
1345
|
-
await handle.close();
|
|
1346
|
-
}
|
|
1347
|
-
await rename(upload.temporaryPath, upload.targetPath);
|
|
1348
|
-
const createdAt = this.now();
|
|
1349
|
-
try {
|
|
1350
|
-
await utimes(upload.targetPath, new Date(createdAt), new Date(createdAt));
|
|
1351
|
-
const file = {
|
|
1352
|
-
id: this.options.store.fileProjectionId(upload.conversationId, 'UPLOAD', upload.name),
|
|
1353
|
-
conversationId: upload.conversationId,
|
|
1354
|
-
category: 'UPLOAD',
|
|
1355
|
-
relativePath: upload.name,
|
|
1356
|
-
displayName: upload.name,
|
|
1357
|
-
mimeType: upload.mimeType,
|
|
1358
|
-
size: upload.size,
|
|
1359
|
-
source: 'USER',
|
|
1360
|
-
createdAt,
|
|
1361
|
-
updatedAt: createdAt
|
|
1362
|
-
};
|
|
1363
|
-
upload.file = await this.options.store.registerFile(payload.userId, file);
|
|
1364
|
-
upload.status = 'COMPLETED';
|
|
1365
|
-
this.releaseUploadName(upload.conversationId, upload.name);
|
|
1366
|
-
this.publishHostedEvent(upload.conversationId, {
|
|
1367
|
-
conversationId: upload.conversationId,
|
|
1368
|
-
kind: 'files',
|
|
1369
|
-
files: this.options.store.listFiles(payload.userId, upload.conversationId),
|
|
1370
|
-
error: null
|
|
1371
|
-
});
|
|
1372
|
-
return { upload: hostedConversationUploadSchema.parse(uploadProjection(upload)) };
|
|
1373
|
-
}
|
|
1374
|
-
catch (error) {
|
|
1375
|
-
await rename(upload.targetPath, upload.temporaryPath).catch(() => undefined);
|
|
1376
|
-
this.releaseUploadName(upload.conversationId, upload.name);
|
|
1377
|
-
throw error;
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
async cancelUpload(raw) {
|
|
1381
|
-
const payload = hostedRequest(raw);
|
|
1382
|
-
const input = hostedConversationUploadControlInputSchema.parse(payload.data);
|
|
1383
|
-
const upload = this.requireUpload(payload.userId, input.conversationId, input.uploadId);
|
|
1384
|
-
if (upload.status === 'COMPLETED')
|
|
1385
|
-
throw new Error('HOSTED_CONVERSATION_UPLOAD_STATE_INVALID');
|
|
1386
|
-
upload.status = 'CANCELLED';
|
|
1387
|
-
await rm(upload.temporaryPath, { force: true });
|
|
1388
|
-
this.releaseUploadName(upload.conversationId, upload.name);
|
|
1389
|
-
return { upload: hostedConversationUploadSchema.parse(uploadProjection(upload)) };
|
|
1390
|
-
}
|
|
1391
|
-
async delete(raw) {
|
|
1392
|
-
if (this.closing)
|
|
1393
|
-
throw new Error('CONVERSATION_HOSTING_UNAVAILABLE');
|
|
1394
|
-
const payload = hostedRequest(raw);
|
|
1395
|
-
const input = hostedConversationIdInputSchema.parse(payload.data);
|
|
1396
|
-
const conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
|
|
1397
|
-
if (this.cleanupLocks.has(conversation.id) || this.historyMutations.has(conversation.id))
|
|
1398
|
-
throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
|
|
1399
|
-
this.cleanupLocks.add(conversation.id);
|
|
1400
|
-
try {
|
|
1401
|
-
await this.waitForPendingMessageStarts(conversation.id);
|
|
1402
|
-
const handle = this.activeRuns.get(conversation.id);
|
|
1403
|
-
if (handle !== undefined) {
|
|
1404
|
-
const interruptionKey = runInterruptionKey(conversation.id, handle.run.id);
|
|
1405
|
-
this.expectedInterruptions.add(interruptionKey);
|
|
1406
|
-
if (!handle.cancel())
|
|
1407
|
-
this.expectedInterruptions.delete(interruptionKey);
|
|
1408
|
-
}
|
|
1409
|
-
await this.waitForFileWrites(conversation.id);
|
|
1410
|
-
await this.cancelConversationUploads(payload.userId, conversation.id);
|
|
1411
|
-
const paths = this.options.store.pathsForConversation(payload.userId, conversation.id);
|
|
1412
|
-
await this.options.runtime
|
|
1413
|
-
.releaseSession({
|
|
1414
|
-
conversationId: conversation.id,
|
|
1415
|
-
sessionId: conversation.sessionId,
|
|
1416
|
-
homeDirectory: paths.homeDirectory
|
|
1417
|
-
})
|
|
1418
|
-
.catch(() => undefined);
|
|
1419
|
-
await this.runSettlements.get(conversation.id)?.catch(() => undefined);
|
|
1420
|
-
this.removeConversationWatchLeases(conversation.id);
|
|
1421
|
-
await this.options.store.permanentDeleteConversation(conversation.id);
|
|
1422
|
-
this.activities.delete(conversation.id);
|
|
1423
|
-
this.historyVersions.delete(conversation.id);
|
|
1424
|
-
return hostedConversationDeleteResultSchema.parse({
|
|
1425
|
-
deletedConversationId: conversation.id
|
|
1426
|
-
});
|
|
1427
|
-
}
|
|
1428
|
-
finally {
|
|
1429
|
-
this.cleanupLocks.delete(conversation.id);
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
async waitForPendingMessageStarts(conversationId) {
|
|
1433
|
-
const prefix = `${conversationId}\0`;
|
|
1434
|
-
const pending = [...this.pendingMessages.entries()]
|
|
1435
|
-
.filter(([key]) => key.startsWith(prefix))
|
|
1436
|
-
.map(([, operation]) => operation);
|
|
1437
|
-
await Promise.allSettled(pending);
|
|
1438
|
-
}
|
|
1439
|
-
async initializeWatchLease(lease) {
|
|
1440
|
-
try {
|
|
1441
|
-
const snapshot = await this.get({
|
|
1442
|
-
conversationId: lease.conversationId,
|
|
1443
|
-
__requestUserId: lease.userId,
|
|
1444
|
-
__sourceTemplateId: lease.templateId,
|
|
1445
|
-
__marAgentModels: lease.models
|
|
1446
|
-
});
|
|
1447
|
-
if (this.watchLeases.get(lease.ownerKey) !== lease)
|
|
1448
|
-
return;
|
|
1449
|
-
this.options.emitWatchEvent?.({
|
|
1450
|
-
type: 'watch.hosted-conversation.snapshot',
|
|
1451
|
-
workbenchConnectionId: lease.workbenchConnectionId,
|
|
1452
|
-
watchId: lease.watchId,
|
|
1453
|
-
nodeGeneration: lease.nodeGeneration,
|
|
1454
|
-
nodeId: this.options.nodeId(),
|
|
1455
|
-
templateId: lease.templateId,
|
|
1456
|
-
conversationId: lease.conversationId,
|
|
1457
|
-
revision: 1,
|
|
1458
|
-
snapshotSequence: ++this.snapshotSequence,
|
|
1459
|
-
expiresAt: lease.expiresAt,
|
|
1460
|
-
payload: snapshot
|
|
1461
|
-
});
|
|
1462
|
-
lease.ready = true;
|
|
1463
|
-
for (const event of lease.pendingEvents)
|
|
1464
|
-
this.emitWatchLeaseEvent(lease, event);
|
|
1465
|
-
lease.pendingEvents.length = 0;
|
|
1466
|
-
}
|
|
1467
|
-
catch (error) {
|
|
1468
|
-
if (this.watchLeases.get(lease.ownerKey) !== lease)
|
|
1469
|
-
return;
|
|
1470
|
-
this.options.emitWatchEvent?.({
|
|
1471
|
-
type: 'watch.failed',
|
|
1472
|
-
workbenchConnectionId: lease.workbenchConnectionId,
|
|
1473
|
-
watchType: 'hosted-conversation',
|
|
1474
|
-
watchId: lease.watchId,
|
|
1475
|
-
nodeGeneration: lease.nodeGeneration,
|
|
1476
|
-
nodeId: this.options.nodeId(),
|
|
1477
|
-
templateId: lease.templateId,
|
|
1478
|
-
conversationId: lease.conversationId,
|
|
1479
|
-
code: safeErrorCode(error, 'HOSTED_CONVERSATION_WATCH_FAILED'),
|
|
1480
|
-
message: 'Hosted conversation watch snapshot failed.'
|
|
1481
|
-
});
|
|
1482
|
-
this.removeWatchLease(lease.ownerKey);
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
async refreshReadyWatchSnapshots(conversationId, completedRunId) {
|
|
1486
|
-
const leases = [...this.watchLeases.values()].filter((lease) => lease.conversationId === conversationId && lease.ready);
|
|
1487
|
-
await Promise.allSettled(leases.map(async (lease) => {
|
|
1488
|
-
const snapshot = await this.snapshot(lease.userId, conversationId, lease.models);
|
|
1489
|
-
if (this.watchLeases.get(lease.ownerKey) !== lease || !lease.ready)
|
|
1490
|
-
return;
|
|
1491
|
-
const activeRunId = this.activeRuns.get(conversationId)?.run.id;
|
|
1492
|
-
if ((activeRunId !== undefined && activeRunId !== completedRunId) ||
|
|
1493
|
-
(activeRunId === undefined && this.activity(conversationId).status !== 'IDLE'))
|
|
1494
|
-
return;
|
|
1495
|
-
this.options.emitWatchEvent?.({
|
|
1496
|
-
type: 'watch.hosted-conversation.snapshot',
|
|
1497
|
-
workbenchConnectionId: lease.workbenchConnectionId,
|
|
1498
|
-
watchId: lease.watchId,
|
|
1499
|
-
nodeGeneration: lease.nodeGeneration,
|
|
1500
|
-
nodeId: this.options.nodeId(),
|
|
1501
|
-
templateId: lease.templateId,
|
|
1502
|
-
conversationId: lease.conversationId,
|
|
1503
|
-
revision: 1,
|
|
1504
|
-
snapshotSequence: ++this.snapshotSequence,
|
|
1505
|
-
expiresAt: lease.expiresAt,
|
|
1506
|
-
payload: {
|
|
1507
|
-
...snapshot,
|
|
1508
|
-
run: null
|
|
1509
|
-
}
|
|
1510
|
-
});
|
|
1511
|
-
}));
|
|
1512
|
-
}
|
|
1513
|
-
publishHostedSnapshot(conversationId, snapshot) {
|
|
1514
|
-
for (const lease of this.watchLeases.values()) {
|
|
1515
|
-
if (lease.conversationId !== conversationId || !lease.ready)
|
|
1516
|
-
continue;
|
|
1517
|
-
this.options.emitWatchEvent?.({
|
|
1518
|
-
type: 'watch.hosted-conversation.snapshot',
|
|
1519
|
-
workbenchConnectionId: lease.workbenchConnectionId,
|
|
1520
|
-
watchId: lease.watchId,
|
|
1521
|
-
nodeGeneration: lease.nodeGeneration,
|
|
1522
|
-
nodeId: this.options.nodeId(),
|
|
1523
|
-
templateId: lease.templateId,
|
|
1524
|
-
conversationId: lease.conversationId,
|
|
1525
|
-
revision: 1,
|
|
1526
|
-
snapshotSequence: ++this.snapshotSequence,
|
|
1527
|
-
expiresAt: lease.expiresAt,
|
|
1528
|
-
payload: snapshot
|
|
1529
|
-
});
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
activity(conversationId) {
|
|
1533
|
-
return (this.activities.get(conversationId) ?? {
|
|
1534
|
-
status: 'IDLE',
|
|
1535
|
-
version: 0,
|
|
1536
|
-
updatedAt: 0
|
|
1537
|
-
});
|
|
1538
|
-
}
|
|
1539
|
-
summary(conversation) {
|
|
1540
|
-
return hostedConversationSummary(conversation, this.activity(conversation.id));
|
|
1541
|
-
}
|
|
1542
|
-
publishActivity(userId, conversation, status, updatedAt) {
|
|
1543
|
-
const current = this.activity(conversation.id);
|
|
1544
|
-
const activity = {
|
|
1545
|
-
status,
|
|
1546
|
-
version: current.version + 1,
|
|
1547
|
-
updatedAt
|
|
1548
|
-
};
|
|
1549
|
-
this.activities.set(conversation.id, activity);
|
|
1550
|
-
this.publishWorkbenchEvent({
|
|
1551
|
-
kind: 'activity',
|
|
1552
|
-
userId,
|
|
1553
|
-
templateId: conversation.templateId,
|
|
1554
|
-
conversationId: conversation.id,
|
|
1555
|
-
activity
|
|
1556
|
-
});
|
|
1557
|
-
}
|
|
1558
|
-
publishTerminalNotification(userId, conversation, runId, completion) {
|
|
1559
|
-
this.publishWorkbenchEvent({
|
|
1560
|
-
kind: 'notification',
|
|
1561
|
-
event: {
|
|
1562
|
-
runId,
|
|
1563
|
-
sequence: 1,
|
|
1564
|
-
eventType: 'notification',
|
|
1565
|
-
payload: {
|
|
1566
|
-
targetType: 'HOSTED_CONVERSATION',
|
|
1567
|
-
userId,
|
|
1568
|
-
templateId: conversation.templateId,
|
|
1569
|
-
conversationId: conversation.id,
|
|
1570
|
-
conversationTitle: conversation.title,
|
|
1571
|
-
runId,
|
|
1572
|
-
notificationId: `run:${this.options.nodeId()}:${conversation.id}:${runId}`,
|
|
1573
|
-
kind: 'RUN_TERMINAL',
|
|
1574
|
-
outcome: completion.status,
|
|
1575
|
-
occurredAt: completion.completedAt
|
|
1576
|
-
},
|
|
1577
|
-
createdAt: completion.completedAt
|
|
1578
|
-
}
|
|
1579
|
-
});
|
|
1580
|
-
}
|
|
1581
|
-
publishWorkbenchEvent(payload) {
|
|
1582
|
-
const event = this.options.nextWorkbenchEvent?.(payload) ?? {
|
|
1583
|
-
sequence: ++this.workbenchSequence,
|
|
1584
|
-
category: 'hosted-conversation',
|
|
1585
|
-
payload
|
|
1586
|
-
};
|
|
1587
|
-
if (event !== undefined)
|
|
1588
|
-
this.options.emitWorkbenchEvent?.(event);
|
|
1589
|
-
}
|
|
1590
|
-
publishUpgradeFailure(conversation, previousVersionId, targetVersionId, error) {
|
|
1591
|
-
this.publishHostedEvent(conversation.id, {
|
|
1592
|
-
conversationId: conversation.id,
|
|
1593
|
-
kind: 'upgrade',
|
|
1594
|
-
upgrade: {
|
|
1595
|
-
status: 'FAILED',
|
|
1596
|
-
previousVersionId,
|
|
1597
|
-
targetVersionId,
|
|
1598
|
-
activeVersionId: conversation.activeVersionId,
|
|
1599
|
-
conversation: this.summary(conversation),
|
|
1600
|
-
error: {
|
|
1601
|
-
code: safeErrorCode(error, 'HOSTED_CONVERSATION_UPGRADE_FAILED'),
|
|
1602
|
-
message: 'The conversation template upgrade failed.'
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
});
|
|
1606
|
-
}
|
|
1607
|
-
publishHostedEvent(conversationId, payload) {
|
|
1608
|
-
const leases = [...this.watchLeases.values()].filter((lease) => lease.conversationId === conversationId);
|
|
1609
|
-
if (leases.length === 0)
|
|
1610
|
-
return false;
|
|
1611
|
-
const event = this.options.nextWorkbenchEvent?.(payload) ?? {
|
|
1612
|
-
sequence: ++this.workbenchSequence,
|
|
1613
|
-
category: 'hosted-conversation',
|
|
1614
|
-
payload
|
|
1615
|
-
};
|
|
1616
|
-
if (event === undefined)
|
|
1617
|
-
return false;
|
|
1618
|
-
for (const lease of leases) {
|
|
1619
|
-
if (!lease.ready)
|
|
1620
|
-
lease.pendingEvents.push(event);
|
|
1621
|
-
else
|
|
1622
|
-
this.emitWatchLeaseEvent(lease, event);
|
|
1623
|
-
}
|
|
1624
|
-
return true;
|
|
1625
|
-
}
|
|
1626
|
-
emitWatchLeaseEvent(lease, event) {
|
|
1627
|
-
this.options.emitWatchEvent?.({
|
|
1628
|
-
type: 'watch.hosted-conversation.event',
|
|
1629
|
-
workbenchConnectionId: lease.workbenchConnectionId,
|
|
1630
|
-
watchId: lease.watchId,
|
|
1631
|
-
nodeGeneration: lease.nodeGeneration,
|
|
1632
|
-
nodeId: this.options.nodeId(),
|
|
1633
|
-
templateId: lease.templateId,
|
|
1634
|
-
conversationId: lease.conversationId,
|
|
1635
|
-
expiresAt: lease.expiresAt,
|
|
1636
|
-
event
|
|
1637
|
-
});
|
|
1638
|
-
}
|
|
1639
|
-
acceptedWatch(lease) {
|
|
1640
|
-
return {
|
|
1641
|
-
accepted: true,
|
|
1642
|
-
watchId: lease.watchId,
|
|
1643
|
-
nodeGeneration: lease.nodeGeneration,
|
|
1644
|
-
expiresAt: lease.expiresAt
|
|
1645
|
-
};
|
|
1646
|
-
}
|
|
1647
|
-
removeConversationWatchLeases(conversationId) {
|
|
1648
|
-
for (const lease of [...this.watchLeases.values()])
|
|
1649
|
-
if (lease.conversationId === conversationId)
|
|
1650
|
-
this.removeWatchLease(lease.ownerKey);
|
|
1651
|
-
}
|
|
1652
|
-
removeWatchLease(ownerKey) {
|
|
1653
|
-
const lease = this.watchLeases.get(ownerKey);
|
|
1654
|
-
if (lease === undefined)
|
|
1655
|
-
return;
|
|
1656
|
-
clearTimeout(lease.timer);
|
|
1657
|
-
this.watchLeases.delete(ownerKey);
|
|
1658
|
-
this.sessionWatchSlots.release(ownerKey, lease.slotToken);
|
|
1659
|
-
}
|
|
1660
|
-
watchLeaseTimer(ownerKey) {
|
|
1661
|
-
const timer = setTimeout(() => this.removeWatchLease(ownerKey), WATCH_TTL_MS);
|
|
1662
|
-
timer.unref();
|
|
1663
|
-
return timer;
|
|
1664
|
-
}
|
|
1665
|
-
requireWritableConversation(userId, conversationId) {
|
|
1666
|
-
const conversation = this.options.store.requireConversation(userId, conversationId);
|
|
1667
|
-
if (this.cleanupLocks.has(conversation.id) || this.historyMutations.has(conversation.id))
|
|
1668
|
-
throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
|
|
1669
|
-
if (conversation.status !== 'ACTIVE')
|
|
1670
|
-
throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
|
|
1671
|
-
return conversation;
|
|
1672
|
-
}
|
|
1673
|
-
assertTemplateContext(payload, conversation) {
|
|
1674
|
-
if (payload.sourceTemplateId !== undefined &&
|
|
1675
|
-
payload.sourceTemplateId !== conversation.templateId)
|
|
1676
|
-
throw new Error('HOSTED_CONVERSATION_TEMPLATE_CONFLICT');
|
|
1677
|
-
}
|
|
1678
|
-
requireUpload(userId, conversationId, uploadId) {
|
|
1679
|
-
this.options.store.requireConversation(userId, conversationId);
|
|
1680
|
-
const upload = this.uploads.get(uploadId);
|
|
1681
|
-
if (upload === undefined ||
|
|
1682
|
-
upload.userId !== userId ||
|
|
1683
|
-
upload.conversationId !== conversationId)
|
|
1684
|
-
throw new Error('HOSTED_CONVERSATION_UPLOAD_NOT_FOUND');
|
|
1685
|
-
return upload;
|
|
1686
|
-
}
|
|
1687
|
-
async cancelConversationUploads(userId, conversationId) {
|
|
1688
|
-
for (const upload of this.uploads.values()) {
|
|
1689
|
-
if (upload.userId !== userId || upload.conversationId !== conversationId)
|
|
1690
|
-
continue;
|
|
1691
|
-
if (upload.status === 'UPLOADING') {
|
|
1692
|
-
upload.status = 'CANCELLED';
|
|
1693
|
-
await rm(upload.temporaryPath, { force: true }).catch(() => undefined);
|
|
1694
|
-
this.releaseUploadName(upload.conversationId, upload.name);
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
async requireAttachment(userId, conversationId, fileId) {
|
|
1699
|
-
const file = this.options.store.requireFile(userId, conversationId, fileId);
|
|
1700
|
-
if (file.category !== 'UPLOAD')
|
|
1701
|
-
throw new Error('HOSTED_CONVERSATION_ATTACHMENT_INVALID');
|
|
1702
|
-
await assertRegularFile(this.options.store.filePath(userId, file), file.size);
|
|
1703
|
-
return file;
|
|
1704
|
-
}
|
|
1705
|
-
async uploadPreflightStatus(conversation, draft, name, size) {
|
|
1706
|
-
if (Buffer.byteLength(name, 'utf8') > 255)
|
|
1707
|
-
return 'INVALID_NAME';
|
|
1708
|
-
if (size > draft.quotas.maxUploadBytes)
|
|
1709
|
-
return 'TOO_LARGE';
|
|
1710
|
-
if (conversation.quota.usedBytes + size > conversation.quota.maxBytes)
|
|
1711
|
-
return 'QUOTA_EXCEEDED';
|
|
1712
|
-
return 'READY';
|
|
1713
|
-
}
|
|
1714
|
-
async allocateUploadName(paths, userId, conversationId, requestedName) {
|
|
1715
|
-
const existingNames = new Set(this.options.store
|
|
1716
|
-
.listFiles(userId, conversationId)
|
|
1717
|
-
.filter((file) => file.category === 'UPLOAD')
|
|
1718
|
-
.map((file) => canonicalUploadName(file.relativePath)));
|
|
1719
|
-
const parts = uploadNameParts(requestedName);
|
|
1720
|
-
let index = parts.index;
|
|
1721
|
-
for (;;) {
|
|
1722
|
-
const candidate = uploadNameCandidate(parts, index);
|
|
1723
|
-
const canonical = canonicalUploadName(candidate);
|
|
1724
|
-
if (existingNames.has(canonical) || this.isUploadNameReserved(conversationId, canonical)) {
|
|
1725
|
-
index = Math.max(1, index + 1);
|
|
1726
|
-
continue;
|
|
1727
|
-
}
|
|
1728
|
-
this.reserveUploadName(conversationId, canonical);
|
|
1729
|
-
let target;
|
|
1730
|
-
try {
|
|
1731
|
-
target = await lstat(join(paths.uploadsDirectory, candidate));
|
|
1732
|
-
}
|
|
1733
|
-
catch (error) {
|
|
1734
|
-
if (error.code === 'ENOENT')
|
|
1735
|
-
return candidate;
|
|
1736
|
-
this.releaseUploadName(conversationId, candidate);
|
|
1737
|
-
throw error;
|
|
1738
|
-
}
|
|
1739
|
-
if (target === undefined)
|
|
1740
|
-
return candidate;
|
|
1741
|
-
this.releaseUploadName(conversationId, candidate);
|
|
1742
|
-
if (!target.isFile())
|
|
1743
|
-
throw new Error('HOSTED_CONVERSATION_FILE_INVALID');
|
|
1744
|
-
existingNames.add(canonical);
|
|
1745
|
-
index = Math.max(1, index + 1);
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
isUploadNameReserved(conversationId, canonicalName) {
|
|
1749
|
-
return this.uploadNameReservations.get(conversationId)?.has(canonicalName) ?? false;
|
|
1750
|
-
}
|
|
1751
|
-
reserveUploadName(conversationId, canonicalName) {
|
|
1752
|
-
const reserved = this.uploadNameReservations.get(conversationId) ?? new Set();
|
|
1753
|
-
reserved.add(canonicalName);
|
|
1754
|
-
this.uploadNameReservations.set(conversationId, reserved);
|
|
1755
|
-
}
|
|
1756
|
-
releaseUploadName(conversationId, name) {
|
|
1757
|
-
const reserved = this.uploadNameReservations.get(conversationId);
|
|
1758
|
-
if (reserved === undefined)
|
|
1759
|
-
return;
|
|
1760
|
-
reserved.delete(canonicalUploadName(name));
|
|
1761
|
-
if (reserved.size === 0)
|
|
1762
|
-
this.uploadNameReservations.delete(conversationId);
|
|
1763
|
-
}
|
|
1764
|
-
pathMounts(paths, version, generatedImagesDirectory, generatedImagesLogicalPath, temporaryDirectory) {
|
|
1765
|
-
const mounts = [
|
|
1766
|
-
{
|
|
1767
|
-
logicalPath: '/workspace',
|
|
1768
|
-
physicalPath: version.templateDirectory,
|
|
1769
|
-
writable: false
|
|
1770
|
-
},
|
|
1771
|
-
{
|
|
1772
|
-
logicalPath: '/workspace/inbox',
|
|
1773
|
-
physicalPath: paths.uploadsDirectory,
|
|
1774
|
-
writable: true
|
|
1775
|
-
},
|
|
1776
|
-
{
|
|
1777
|
-
logicalPath: '/workspace/output',
|
|
1778
|
-
physicalPath: paths.outputDirectory,
|
|
1779
|
-
writable: true
|
|
1780
|
-
}
|
|
1781
|
-
];
|
|
1782
|
-
if (temporaryDirectory !== undefined)
|
|
1783
|
-
mounts.push({
|
|
1784
|
-
logicalPath: '/tmp',
|
|
1785
|
-
physicalPath: temporaryDirectory,
|
|
1786
|
-
writable: true
|
|
1787
|
-
});
|
|
1788
|
-
if (generatedImagesDirectory !== undefined && generatedImagesLogicalPath !== undefined)
|
|
1789
|
-
mounts.push({
|
|
1790
|
-
logicalPath: generatedImagesLogicalPath,
|
|
1791
|
-
physicalPath: generatedImagesDirectory,
|
|
1792
|
-
writable: false
|
|
1793
|
-
});
|
|
1794
|
-
if (version.sourceWorkspacePath !== undefined)
|
|
1795
|
-
mounts.push({
|
|
1796
|
-
logicalPath: version.sourceWorkspacePath,
|
|
1797
|
-
physicalPath: version.templateDirectory,
|
|
1798
|
-
writable: false
|
|
1799
|
-
}, {
|
|
1800
|
-
logicalPath: join(version.sourceWorkspacePath, 'inbox'),
|
|
1801
|
-
physicalPath: paths.uploadsDirectory,
|
|
1802
|
-
writable: true
|
|
1803
|
-
}, {
|
|
1804
|
-
logicalPath: join(version.sourceWorkspacePath, 'output'),
|
|
1805
|
-
physicalPath: paths.outputDirectory,
|
|
1806
|
-
writable: true
|
|
1807
|
-
});
|
|
1808
|
-
return mounts;
|
|
1809
|
-
}
|
|
1810
|
-
processExecutor(paths, version, temporaryDirectory = paths.temporaryRootDirectory, generatedImagesDirectory, generatedImagesLogicalPath) {
|
|
1811
|
-
return this.options.processExecutorFactory({
|
|
1812
|
-
workspaceDirectory: version.templateDirectory,
|
|
1813
|
-
sourceWorkspacePath: version.sourceWorkspacePath,
|
|
1814
|
-
inboxDirectory: paths.uploadsDirectory,
|
|
1815
|
-
outputDirectory: paths.outputDirectory,
|
|
1816
|
-
...(generatedImagesDirectory === undefined || generatedImagesLogicalPath === undefined
|
|
1817
|
-
? {}
|
|
1818
|
-
: { generatedImagesDirectory, generatedImagesLogicalPath }),
|
|
1819
|
-
temporaryDirectory,
|
|
1820
|
-
networkMode: version.draft.networkMode
|
|
1821
|
-
});
|
|
1822
|
-
}
|
|
1823
|
-
latestAvailableTemplateVersionId(templateId) {
|
|
1824
|
-
const row = this.options.database.raw
|
|
1825
|
-
.prepare(`SELECT id FROM conversation_template_versions
|
|
1826
|
-
WHERE template_id=? AND status='AVAILABLE'
|
|
1827
|
-
ORDER BY version_number DESC,published_at DESC,id DESC
|
|
1828
|
-
LIMIT 1`)
|
|
1829
|
-
.get(templateId);
|
|
1830
|
-
if (row === undefined)
|
|
1831
|
-
return undefined;
|
|
1832
|
-
if (typeof row.id !== 'string')
|
|
1833
|
-
throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
|
|
1834
|
-
return row.id;
|
|
1835
|
-
}
|
|
1836
|
-
async loadTemplateVersion(versionId, templateId) {
|
|
1837
|
-
const row = this.options.database.raw
|
|
1838
|
-
.prepare(`SELECT id,template_id,status,storage_key,manifest,content_hash,manifest_hash,mcp_config_hash,
|
|
1839
|
-
file_count,total_bytes,host_policy_version
|
|
1840
|
-
FROM conversation_template_versions WHERE id=? AND template_id=?`)
|
|
1841
|
-
.get(versionId, templateId);
|
|
1842
|
-
if (row === undefined ||
|
|
1843
|
-
(row.status !== 'AVAILABLE' && row.status !== 'RETAINED') ||
|
|
1844
|
-
row.host_policy_version !== HOST_POLICY_VERSION ||
|
|
1845
|
-
typeof row.storage_key !== 'string' ||
|
|
1846
|
-
typeof row.manifest !== 'string' ||
|
|
1847
|
-
typeof row.content_hash !== 'string' ||
|
|
1848
|
-
typeof row.manifest_hash !== 'string' ||
|
|
1849
|
-
typeof row.mcp_config_hash !== 'string')
|
|
1850
|
-
throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
|
|
1851
|
-
const versionDirectory = this.options.store.templateVersionDirectory(row.storage_key);
|
|
1852
|
-
const manifestPath = join(versionDirectory, 'manifest.json');
|
|
1853
|
-
const contentDirectory = join(versionDirectory, 'content');
|
|
1854
|
-
const manifestInfo = await lstat(manifestPath).catch(() => undefined);
|
|
1855
|
-
const contentInfo = await lstat(contentDirectory).catch(() => undefined);
|
|
1856
|
-
if (manifestInfo === undefined ||
|
|
1857
|
-
!manifestInfo.isFile() ||
|
|
1858
|
-
manifestInfo.isSymbolicLink() ||
|
|
1859
|
-
contentInfo === undefined ||
|
|
1860
|
-
!contentInfo.isDirectory() ||
|
|
1861
|
-
contentInfo.isSymbolicLink() ||
|
|
1862
|
-
(contentInfo.mode & 0o777) !== 0o555)
|
|
1863
|
-
throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
|
|
1864
|
-
let persisted;
|
|
1865
|
-
try {
|
|
1866
|
-
const manifestText = await readFile(manifestPath, 'utf8');
|
|
1867
|
-
const manifestJson = manifestText.endsWith('\n') ? manifestText.slice(0, -1) : manifestText;
|
|
1868
|
-
if (manifestJson !== row.manifest || templateManifestHash(manifestText) !== row.manifest_hash)
|
|
1869
|
-
throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
|
|
1870
|
-
persisted = JSON.parse(manifestJson);
|
|
1871
|
-
}
|
|
1872
|
-
catch {
|
|
1873
|
-
throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
|
|
1874
|
-
}
|
|
1875
|
-
if (!record(persisted))
|
|
1876
|
-
throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
|
|
1877
|
-
const snapshot = parseTemplateVersionManifest(persisted);
|
|
1878
|
-
const { allowedModels, defaultModelId, ...draft } = snapshot.draft;
|
|
1879
|
-
const mcpServers = snapshot.mcpServers;
|
|
1880
|
-
const contentHash = templateContentHash(snapshot.directories, snapshot.entries);
|
|
1881
|
-
const totalBytes = snapshot.entries.reduce((total, entry) => total + entry.size, 0);
|
|
1882
|
-
if (snapshot.mcpConfigHash !== row.mcp_config_hash ||
|
|
1883
|
-
templateMcpConfigHash(mcpServers) !== row.mcp_config_hash ||
|
|
1884
|
-
snapshot.contentHash !== row.content_hash ||
|
|
1885
|
-
contentHash !== row.content_hash ||
|
|
1886
|
-
row.file_count !== snapshot.entries.length ||
|
|
1887
|
-
row.total_bytes !== totalBytes)
|
|
1888
|
-
throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
|
|
1889
|
-
if (persisted.versionId !== versionId ||
|
|
1890
|
-
persisted.templateId !== templateId ||
|
|
1891
|
-
persisted.hostPolicyVersion !== HOST_POLICY_VERSION ||
|
|
1892
|
-
draft.templateId !== templateId ||
|
|
1893
|
-
draft.nodeId !== this.options.nodeId())
|
|
1894
|
-
throw new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED');
|
|
1895
|
-
return {
|
|
1896
|
-
id: versionId,
|
|
1897
|
-
templateId,
|
|
1898
|
-
storageKey: row.storage_key,
|
|
1899
|
-
templateDirectory: contentDirectory,
|
|
1900
|
-
sourceWorkspacePath: snapshot.sourceWorkspacePath,
|
|
1901
|
-
draft,
|
|
1902
|
-
modelPolicy: {
|
|
1903
|
-
allowedIds: allowedModels.map((model) => model.modelId),
|
|
1904
|
-
defaultId: defaultModelId
|
|
1905
|
-
},
|
|
1906
|
-
mcpServers
|
|
1907
|
-
};
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
function hostedRequest(raw) {
|
|
1911
|
-
if (!record(raw))
|
|
1912
|
-
throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
|
|
1913
|
-
const { __usageGrant, __requestUserId, __template, __marAgentModel, __marAgentModels, __marAgentCompact, __marAgentRuntimeScopeId, __workspaceWatchSessionHash, __templateDeleted, __sourceTemplateId, ...data } = raw;
|
|
1914
|
-
if (typeof __requestUserId !== 'number' ||
|
|
1915
|
-
!Number.isSafeInteger(__requestUserId) ||
|
|
1916
|
-
__requestUserId <= 0)
|
|
1917
|
-
throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
|
|
1918
|
-
if (__templateDeleted !== undefined && __templateDeleted !== true)
|
|
1919
|
-
throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
|
|
1920
|
-
if (__sourceTemplateId !== undefined &&
|
|
1921
|
-
(typeof __sourceTemplateId !== 'string' || __sourceTemplateId.length === 0))
|
|
1922
|
-
throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
|
|
1923
|
-
if (__marAgentRuntimeScopeId !== undefined &&
|
|
1924
|
-
(typeof __marAgentRuntimeScopeId !== 'string' || __marAgentRuntimeScopeId.length === 0))
|
|
1925
|
-
throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
|
|
1926
|
-
return {
|
|
1927
|
-
data,
|
|
1928
|
-
...(__sourceTemplateId === undefined ? {} : { sourceTemplateId: __sourceTemplateId }),
|
|
1929
|
-
...(__usageGrant === undefined
|
|
1930
|
-
? {}
|
|
1931
|
-
: { usageGrant: modelUsageGrantSchema.parse(__usageGrant) }),
|
|
1932
|
-
userId: __requestUserId,
|
|
1933
|
-
...(__template === undefined ? {} : { template: __template }),
|
|
1934
|
-
...(__marAgentModel === undefined ? {} : { model: __marAgentModel }),
|
|
1935
|
-
...(__marAgentModels === undefined ? {} : { models: __marAgentModels }),
|
|
1936
|
-
...(__marAgentCompact === undefined ? {} : { compact: __marAgentCompact }),
|
|
1937
|
-
...(__marAgentRuntimeScopeId === undefined ? {} : { runtimeScopeId: __marAgentRuntimeScopeId }),
|
|
1938
|
-
...(__workspaceWatchSessionHash === undefined
|
|
1939
|
-
? {}
|
|
1940
|
-
: { sessionHash: __workspaceWatchSessionHash }),
|
|
1941
|
-
...(__templateDeleted === true ? { templateDeleted: true } : {})
|
|
1942
|
-
};
|
|
1943
|
-
}
|
|
1944
|
-
function requireWatchSessionHash(value) {
|
|
1945
|
-
if (typeof value !== 'string' || value.length === 0)
|
|
1946
|
-
throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
|
|
1947
|
-
return value;
|
|
1948
|
-
}
|
|
1949
|
-
function watchOwnerKey(sessionHash, workbenchConnectionId) {
|
|
1950
|
-
return `${sessionHash}:${workbenchConnectionId}`;
|
|
1951
|
-
}
|
|
1952
|
-
function runtimeModels(value) {
|
|
1953
|
-
return marAgentRuntimeModelConfigurationSchema
|
|
1954
|
-
.array()
|
|
1955
|
-
.parse(value);
|
|
1956
|
-
}
|
|
1957
|
-
function allowedModels(version, models) {
|
|
1958
|
-
const allowed = new Set(version.modelPolicy.allowedIds);
|
|
1959
|
-
return models.filter((model) => model.enabled && allowed.has(model.modelId));
|
|
1960
|
-
}
|
|
1961
|
-
function selectModel(version, models, requestedModelId) {
|
|
1962
|
-
const available = allowedModels(version, models).filter((model) => model.conversationModel !== false);
|
|
1963
|
-
const requested = available.find((candidate) => candidate.id === requestedModelId);
|
|
1964
|
-
if (requested !== undefined)
|
|
1965
|
-
return requested;
|
|
1966
|
-
if (models.some((candidate) => candidate.id === requestedModelId && candidate.conversationModel === false))
|
|
1967
|
-
throw new Error('HOSTED_CONVERSATION_MODEL_NOT_AVAILABLE');
|
|
1968
|
-
const defaultModel = available.find((candidate) => candidate.modelId === version.modelPolicy.defaultId);
|
|
1969
|
-
if (defaultModel !== undefined)
|
|
1970
|
-
return defaultModel;
|
|
1971
|
-
const model = version.modelPolicy.allowedIds
|
|
1972
|
-
.map((allowedId) => available.find((candidate) => candidate.modelId === allowedId))
|
|
1973
|
-
.find((candidate) => candidate !== undefined);
|
|
1974
|
-
if (model === undefined)
|
|
1975
|
-
throw new Error('HOSTED_CONVERSATION_MODEL_NOT_AVAILABLE');
|
|
1976
|
-
return model;
|
|
1977
|
-
}
|
|
1978
|
-
function networkModels(models, networkMode) {
|
|
1979
|
-
if (networkMode !== 'NONE')
|
|
1980
|
-
return models;
|
|
1981
|
-
return models.map((model) => model.hostedWebSearch ? { ...model, hostedWebSearch: false } : model);
|
|
1982
|
-
}
|
|
1983
|
-
function resolveMcpServers(version, environment) {
|
|
1984
|
-
const resolveReferences = (references) => {
|
|
1985
|
-
const resolved = {};
|
|
1986
|
-
for (const [name, reference] of Object.entries(references ?? {})) {
|
|
1987
|
-
const value = environment[reference.secretName];
|
|
1988
|
-
if (value === undefined)
|
|
1989
|
-
throw new Error('CONVERSATION_TEMPLATE_MCP_INVALID');
|
|
1990
|
-
resolved[name] = value;
|
|
1991
|
-
}
|
|
1992
|
-
return resolved;
|
|
1993
|
-
};
|
|
1994
|
-
assertMcpServerNames(version.mcpServers.map((server) => ({ name: server.serverName })));
|
|
1995
|
-
return version.mcpServers.flatMap((server) => {
|
|
1996
|
-
if (server.runtime.transport === 'HTTP') {
|
|
1997
|
-
if (version.draft.networkMode !== 'UNRESTRICTED')
|
|
1998
|
-
return [];
|
|
1999
|
-
return [
|
|
2000
|
-
{
|
|
2001
|
-
name: server.serverName,
|
|
2002
|
-
lifecycle: 'EXECUTION',
|
|
2003
|
-
transport: 'HTTP',
|
|
2004
|
-
url: server.runtime.url,
|
|
2005
|
-
headers: {
|
|
2006
|
-
...resolveReferences(server.runtime.headers),
|
|
2007
|
-
...resolveReferences(server.configuration.headers)
|
|
2008
|
-
}
|
|
2009
|
-
}
|
|
2010
|
-
];
|
|
2011
|
-
}
|
|
2012
|
-
return [
|
|
2013
|
-
{
|
|
2014
|
-
name: server.serverName,
|
|
2015
|
-
lifecycle: 'EXECUTION',
|
|
2016
|
-
transport: 'STDIO',
|
|
2017
|
-
command: server.runtime.command,
|
|
2018
|
-
args: [...server.runtime.args, ...(server.configuration.arguments ?? [])],
|
|
2019
|
-
environment: {
|
|
2020
|
-
...resolveReferences(server.runtime.environment),
|
|
2021
|
-
...resolveReferences(server.configuration.environment)
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
|
-
];
|
|
2025
|
-
});
|
|
2026
|
-
}
|
|
2027
|
-
function hostedConversationSummary(manifest, activity) {
|
|
2028
|
-
return hostedConversationSummarySchema.parse({
|
|
2029
|
-
id: manifest.id,
|
|
2030
|
-
nodeId: manifest.nodeId,
|
|
2031
|
-
sessionId: manifest.sessionId,
|
|
2032
|
-
templateId: manifest.templateId,
|
|
2033
|
-
templateName: manifest.templateName,
|
|
2034
|
-
activeVersionId: manifest.activeVersionId,
|
|
2035
|
-
title: manifest.title,
|
|
2036
|
-
titleSource: manifest.titleSource,
|
|
2037
|
-
pinnedAt: manifest.pinnedAt,
|
|
2038
|
-
status: manifest.status,
|
|
2039
|
-
modelId: manifest.modelId,
|
|
2040
|
-
createdAt: manifest.createdAt,
|
|
2041
|
-
updatedAt: manifest.updatedAt,
|
|
2042
|
-
lastActivityAt: manifest.lastActivityAt,
|
|
2043
|
-
...(activity === undefined ? {} : { activity })
|
|
2044
|
-
});
|
|
2045
|
-
}
|
|
2046
|
-
function templateSummary(conversation, draft) {
|
|
2047
|
-
return conversationTemplateSummarySchema.parse({
|
|
2048
|
-
id: conversation.templateId,
|
|
2049
|
-
nodeId: conversation.nodeId,
|
|
2050
|
-
name: conversation.templateName,
|
|
2051
|
-
description: draft.description,
|
|
2052
|
-
status: conversation.templateDeleted ? 'DELETED' : 'ACTIVE',
|
|
2053
|
-
currentVersionId: conversation.activeVersionId,
|
|
2054
|
-
available: !conversation.templateDeleted,
|
|
2055
|
-
createdAt: conversation.createdAt,
|
|
2056
|
-
updatedAt: draft.updatedAt
|
|
2057
|
-
});
|
|
2058
|
-
}
|
|
2059
|
-
function modelSummary(model, index) {
|
|
2060
|
-
return {
|
|
2061
|
-
id: model.id,
|
|
2062
|
-
name: model.name,
|
|
2063
|
-
protocol: model.protocol,
|
|
2064
|
-
baseUrl: model.baseUrl,
|
|
2065
|
-
modelId: model.modelId,
|
|
2066
|
-
contextWindowTokens: model.contextWindowTokens,
|
|
2067
|
-
maxOutputTokens: model.maxOutputTokens,
|
|
2068
|
-
inputCapabilities: model.inputCapabilities,
|
|
2069
|
-
outputCapabilities: model.outputCapabilities,
|
|
2070
|
-
reasoningEfforts: model.reasoningEfforts,
|
|
2071
|
-
defaultReasoningEffort: resolveMarAgentDefaultReasoningEffort(model.defaultReasoningEffort, model.reasoningEfforts),
|
|
2072
|
-
titleGeneration: model.titleGeneration,
|
|
2073
|
-
codeMode: model.codeMode,
|
|
2074
|
-
...(model.fastMode === true ? { fastMode: true } : {}),
|
|
2075
|
-
hostedWebSearch: model.hostedWebSearch,
|
|
2076
|
-
...(model.responsesTransport === undefined
|
|
2077
|
-
? {}
|
|
2078
|
-
: { responsesTransport: model.responsesTransport }),
|
|
2079
|
-
responsesPreviousResponseId: model.responsesPreviousResponseId,
|
|
2080
|
-
enabled: model.enabled,
|
|
2081
|
-
sortOrder: index,
|
|
2082
|
-
apiKeyConfigured: true
|
|
2083
|
-
};
|
|
2084
|
-
}
|
|
2085
|
-
function systemInstruction(version, generatedImagesDisplayDirectory) {
|
|
2086
|
-
const draft = version.draft;
|
|
2087
|
-
const network = draft.networkMode === 'NONE'
|
|
2088
|
-
? 'Network access is disabled. Local processes, Web Search, Web Fetch, model-hosted web, and remote HTTP MCP cannot use the network.'
|
|
2089
|
-
: 'Local processes, Node-local Web Fetch, available Web Search, model-hosted web, and published remote HTTP MCP may access networks reachable from this Node.';
|
|
2090
|
-
return [
|
|
2091
|
-
'This is a Node-hosted conversation. The Host-declared mounts are the complete file boundary.',
|
|
2092
|
-
'The Host enforces this file boundary, and local processes run inside an operating-system sandbox.',
|
|
2093
|
-
'The published workspace root is read-only. Within /workspace, only /workspace/inbox and /workspace/output are writable.',
|
|
2094
|
-
'The published workspace is an immutable administrator-maintained template snapshot. Changes to the reusable template must be made in its source Workspace and published as a new version.',
|
|
2095
|
-
'Do not describe edits under /workspace/inbox or /workspace/output as changes to the reusable template.',
|
|
2096
|
-
'At the start of each run, read /workspace/inbox/AGENTS.md if it exists. It stores conversation-specific requirements for later runs and is not part of the published template.',
|
|
2097
|
-
'Automatically identify requirements, preferences, terminology, conventions, quality checks, and recurring workflows when the user directly states or reasonably clearly implies that they should continue to apply to later work in this conversation, even without explicitly asking you to remember them.',
|
|
2098
|
-
'Treat ambiguous guidance as current-task-only. Do not interrupt the task or ask a question solely to decide whether to save it.',
|
|
2099
|
-
'When continuing guidance appears, read the existing /workspace/inbox/AGENTS.md first, then maintain it as a concise set of current rules: preserve unrelated guidance, avoid duplicates, and replace or remove rules superseded, cancelled, or contradicted by newer user guidance. Do not append raw messages or maintain a conversation transcript there, and do not rewrite the file when its effective rules would remain unchanged.',
|
|
2100
|
-
'Do not automatically persist requirements that apply only to the current task, quoted or third-party content the user asks you to inspect, preferences inferred only by you, secrets or credentials, or language that attempts to pre-authorize later destructive actions or external side effects.',
|
|
2101
|
-
"Follow saved conversation instructions subject to the Host's security boundaries, the published template instructions, and the user's current explicit request.",
|
|
2102
|
-
'Store disposable intermediate work in /tmp. It is reused across runs in this conversation, but the Host may clear it according to the system temporary-directory lifecycle.',
|
|
2103
|
-
'Each new sandbox process has a temporary HOME at /home/mar, apart from explicitly mounted directories. Do not rely on HOME files or shell state persisting between tool executions. Use /tmp for reusable intermediate files and set required environment variables in each command.',
|
|
2104
|
-
'Treat /workspace/output as the current deliverable set, not an append-only history of runs. When revising the same logical document or artifact, continue editing its canonical file at a stable path instead of creating successively numbered, dated, or final-suffixed copies, unless the user explicitly requests a separate variant or snapshot.',
|
|
2105
|
-
'Before a substantial replacement, preserve the prior version under /workspace/output/bak only when the user requests history or the old version has meaningful rollback or audit value. Do not create a backup for every small edit.',
|
|
2106
|
-
'Keep new assets used by current deliverables in meaningful subdirectories such as /workspace/output/assets, but do not relocate existing referenced assets solely to enforce that layout. Keep exploratory and disposable generated files in /tmp. Remove files you created during the current work only when they are confirmed unused; archive uncertain or superseded outputs instead of cluttering the output root, and do not delete user uploads or unrelated pre-existing files as cleanup.',
|
|
2107
|
-
'Place final deliverables in appropriate paths under /workspace/output so they become user-visible outputs.',
|
|
2108
|
-
'When reporting an output file to the user, use a Markdown link in the exact form [relative/path](/workspace/output/relative/path) so the user can open its controlled preview or download.',
|
|
2109
|
-
...(generatedImagesDisplayDirectory === undefined
|
|
2110
|
-
? []
|
|
2111
|
-
: [
|
|
2112
|
-
`Generated images from this conversation remain available across executions in the read-only ${generatedImagesDisplayDirectory} directory.`,
|
|
2113
|
-
`When the user asks to place a previously generated image in output, inspect ${generatedImagesDisplayDirectory} and copy the existing file from ${generatedImagesDisplayDirectory} to /workspace/output when available. If it is missing, report that fact; regenerate only when requested or authorized by the user.`
|
|
2114
|
-
]),
|
|
2115
|
-
...(version.sourceWorkspacePath === undefined
|
|
2116
|
-
? []
|
|
2117
|
-
: [
|
|
2118
|
-
`The published workspace is also mounted read-only at ${version.sourceWorkspacePath} as a compatibility alias; it is the same immutable snapshot, not the live source Workspace. ${join(version.sourceWorkspacePath, 'inbox')} and ${join(version.sourceWorkspacePath, 'output')} are aliases of the same writable overlays.`
|
|
2119
|
-
]),
|
|
2120
|
-
'Read user uploads from /workspace/inbox.',
|
|
2121
|
-
network
|
|
2122
|
-
].join('\n');
|
|
2123
|
-
}
|
|
2124
|
-
function attachmentPrompt(prompt, attachments) {
|
|
2125
|
-
if (attachments.length === 0)
|
|
2126
|
-
return prompt;
|
|
2127
|
-
const suffix = attachments.map((file) => `- /workspace/inbox/${file.relativePath}`).join('\n');
|
|
2128
|
-
return `${prompt}${prompt.trim().length === 0 ? '' : '\n\n'}Attachments:\n${suffix}`;
|
|
2129
|
-
}
|
|
2130
|
-
function automaticHostedConversationTitle(value) {
|
|
2131
|
-
if (value === undefined)
|
|
2132
|
-
return undefined;
|
|
2133
|
-
const title = sessionTitleFromFirstMessage(value);
|
|
2134
|
-
if (title.length === 0 ||
|
|
2135
|
-
/^(?:new|untitled) session(?:\s*[-–—:]\s*\d{4}-\d{2}-\d{2}.*)?$/iu.test(title))
|
|
2136
|
-
return undefined;
|
|
2137
|
-
return title;
|
|
2138
|
-
}
|
|
2139
|
-
function terminalHostedTurnStatus(status) {
|
|
2140
|
-
return (status === 'SUCCEEDED' ||
|
|
2141
|
-
status === 'FAILED' ||
|
|
2142
|
-
status === 'CANCELLED' ||
|
|
2143
|
-
status === 'INTERRUPTED' ||
|
|
2144
|
-
status === 'LOST');
|
|
2145
|
-
}
|
|
2146
|
-
function runFromMessage(record) {
|
|
2147
|
-
return {
|
|
2148
|
-
id: record.runId,
|
|
2149
|
-
status: record.status,
|
|
2150
|
-
startedAt: record.createdAt,
|
|
2151
|
-
completedAt: record.status === 'RUNNING' ? null : record.updatedAt
|
|
2152
|
-
};
|
|
2153
|
-
}
|
|
2154
|
-
function runInterruptionKey(conversationId, runId) {
|
|
2155
|
-
return `${conversationId}\0${runId}`;
|
|
2156
|
-
}
|
|
2157
|
-
function canonicalUploadName(name) {
|
|
2158
|
-
return name.normalize('NFKC').toLocaleLowerCase();
|
|
2159
|
-
}
|
|
2160
|
-
function uploadNameParts(name) {
|
|
2161
|
-
const dot = name.lastIndexOf('.');
|
|
2162
|
-
const extension = dot > 0 ? name.slice(dot) : '';
|
|
2163
|
-
const stem = dot > 0 ? name.slice(0, dot) : name;
|
|
2164
|
-
const suffix = /^(.*) \((\d+)\)$/u.exec(stem);
|
|
2165
|
-
if (suffix === null || suffix[1] === undefined || suffix[2] === undefined)
|
|
2166
|
-
return { stem, extension, index: 0 };
|
|
2167
|
-
const index = Number.parseInt(suffix[2], 10);
|
|
2168
|
-
return {
|
|
2169
|
-
stem: suffix[1],
|
|
2170
|
-
extension,
|
|
2171
|
-
index: Number.isSafeInteger(index) && index > 0 ? index : 0
|
|
2172
|
-
};
|
|
2173
|
-
}
|
|
2174
|
-
function uploadNameCandidate(parts, index) {
|
|
2175
|
-
if (index === 0)
|
|
2176
|
-
return `${parts.stem}${parts.extension}`;
|
|
2177
|
-
const suffix = ` (${index})`;
|
|
2178
|
-
const maximumStemLength = 512 - parts.extension.length - suffix.length;
|
|
2179
|
-
return `${truncateUploadStem(parts.stem, maximumStemLength)}${suffix}${parts.extension}`;
|
|
2180
|
-
}
|
|
2181
|
-
function truncateUploadStem(stem, maximumLength) {
|
|
2182
|
-
if (stem.length <= maximumLength)
|
|
2183
|
-
return stem;
|
|
2184
|
-
const truncated = stem.slice(0, Math.max(0, maximumLength));
|
|
2185
|
-
const last = truncated.charCodeAt(truncated.length - 1);
|
|
2186
|
-
return last >= 0xd800 && last <= 0xdbff ? truncated.slice(0, -1) : truncated;
|
|
2187
|
-
}
|
|
2188
|
-
function uploadProjection(upload) {
|
|
2189
|
-
return {
|
|
2190
|
-
uploadId: upload.uploadId,
|
|
2191
|
-
conversationId: upload.conversationId,
|
|
2192
|
-
name: upload.name,
|
|
2193
|
-
mimeType: upload.mimeType,
|
|
2194
|
-
receivedBytes: upload.receivedBytes,
|
|
2195
|
-
size: upload.size,
|
|
2196
|
-
status: upload.status,
|
|
2197
|
-
...(upload.file === undefined ? {} : { file: upload.file })
|
|
2198
|
-
};
|
|
2199
|
-
}
|
|
2200
|
-
async function assertRegularFile(path, expectedSize) {
|
|
2201
|
-
const metadata = await regularFileMetadata(path);
|
|
2202
|
-
if (metadata.size !== expectedSize)
|
|
2203
|
-
throw new Error('HOSTED_CONVERSATION_FILE_INVALID');
|
|
2204
|
-
}
|
|
2205
|
-
async function openRegularFile(path) {
|
|
2206
|
-
return open(path, constants.O_RDONLY | constants.O_NOFOLLOW).catch(() => {
|
|
2207
|
-
throw new Error('HOSTED_CONVERSATION_FILE_INVALID');
|
|
2208
|
-
});
|
|
2209
|
-
}
|
|
2210
|
-
async function regularFileMetadata(path) {
|
|
2211
|
-
const metadata = await lstat(path).catch(() => {
|
|
2212
|
-
throw new Error('HOSTED_CONVERSATION_FILE_INVALID');
|
|
2213
|
-
});
|
|
2214
|
-
assertRegularFileMetadata(metadata);
|
|
2215
|
-
return metadata;
|
|
2216
|
-
}
|
|
2217
|
-
function assertRegularFileMetadata(metadata) {
|
|
2218
|
-
if (!metadata.isFile() ||
|
|
2219
|
-
metadata.isSymbolicLink() ||
|
|
2220
|
-
metadata.nlink !== 1 ||
|
|
2221
|
-
!Number.isSafeInteger(metadata.size) ||
|
|
2222
|
-
metadata.size < 0)
|
|
2223
|
-
throw new Error('HOSTED_CONVERSATION_FILE_INVALID');
|
|
2224
|
-
}
|
|
2225
|
-
function sameFileIdentity(left, right) {
|
|
2226
|
-
return left.dev === right.dev && left.ino === right.ino;
|
|
2227
|
-
}
|
|
2228
|
-
function sameFileSnapshot(left, right) {
|
|
2229
|
-
return (sameFileIdentity(left, right) &&
|
|
2230
|
-
left.nlink === right.nlink &&
|
|
2231
|
-
left.size === right.size &&
|
|
2232
|
-
left.mtimeMs === right.mtimeMs &&
|
|
2233
|
-
left.ctimeMs === right.ctimeMs);
|
|
2234
|
-
}
|
|
2235
|
-
function safeFileTimestamp(value) {
|
|
2236
|
-
if (!Number.isFinite(value) || value < 0)
|
|
2237
|
-
return 0;
|
|
2238
|
-
return Math.min(Number.MAX_SAFE_INTEGER, Math.trunc(value));
|
|
2239
|
-
}
|
|
2240
|
-
async function assertUploadTemporaryFile(path, expectedSize) {
|
|
2241
|
-
const metadata = await lstat(path).catch(() => {
|
|
2242
|
-
throw new Error('HOSTED_CONVERSATION_UPLOAD_STATE_INVALID');
|
|
2243
|
-
});
|
|
2244
|
-
if (!metadata.isFile() ||
|
|
2245
|
-
metadata.isSymbolicLink() ||
|
|
2246
|
-
metadata.nlink !== 1 ||
|
|
2247
|
-
metadata.size !== expectedSize)
|
|
2248
|
-
throw new Error('HOSTED_CONVERSATION_UPLOAD_STATE_INVALID');
|
|
2249
|
-
}
|
|
2250
|
-
function record(value) {
|
|
2251
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
2252
|
-
}
|