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