@frockbot/cloudflare 0.0.0 → 0.7.292
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/build-artifact.ts +130 -0
- package/build-flutter-web.ts +508 -0
- package/deployment-config/README.md +414 -0
- package/deployment-config/cli.ts +228 -0
- package/deployment-config/generate.ts +689 -0
- package/deployment-config/jsonc.ts +18 -0
- package/deployment-config/profile-schema.generated.ts +391 -0
- package/deployment-config/profile.schema.json +358 -0
- package/deployment-config/profile.ts +124 -0
- package/migrations/0001_better_auth.sql +15 -0
- package/migrations/0002_drop_account_issuer.sql +8 -0
- package/package.json +61 -5
- package/release-version.ts +20 -0
- package/src/account-admission.ts +179 -0
- package/src/account-deletion.ts +274 -0
- package/src/admin-entrypoint.ts +194 -0
- package/src/admin-identities.ts +26 -0
- package/src/audit.ts +103 -0
- package/src/auth-package.access.ts +24 -0
- package/src/auth-package.ts +36 -0
- package/src/avatar-state-cleanup.ts +107 -0
- package/src/billing-computer.ts +267 -0
- package/src/billing-readiness.ts +21 -0
- package/src/billing.ts +461 -0
- package/src/bot-capabilities.ts +480 -0
- package/src/bot-recovery.ts +1 -0
- package/src/bot-state-channel.ts +702 -0
- package/src/bot-state.ts +3995 -0
- package/src/bot-template-cleanup.ts +98 -0
- package/src/bot-title-cleanup.ts +104 -0
- package/src/brand-icon.png +0 -0
- package/src/brand-logo.ts +2 -0
- package/src/brand.ts +36 -0
- package/src/client-compatibility.ts +51 -0
- package/src/compaction-announcement-cleanup.ts +113 -0
- package/src/computer-egress.ts +110 -0
- package/src/computer-host.ts +92 -0
- package/src/computer-screenshot-cleanup.ts +78 -0
- package/src/contracts.ts +832 -0
- package/src/debug.ts +272 -0
- package/src/default-packages-marker-cleanup.ts +39 -0
- package/src/deployment-policy-admin-host.ts +121 -0
- package/src/deployment-policy.ts +426 -0
- package/src/directory-profile-cleanup.ts +101 -0
- package/src/durable-rpc.ts +753 -0
- package/src/durable-session.ts +21 -0
- package/src/entry-boundary.ts +103 -0
- package/src/frock-ai.ts +338 -0
- package/src/gateway.ts +1392 -0
- package/src/group-chat.ts +736 -0
- package/src/hidden-bot-notifications-cleanup.ts +32 -0
- package/src/index.ts +2798 -0
- package/src/insights.ts +15 -0
- package/src/machine-messages-cleanup.ts +121 -0
- package/src/machine-socket.ts +164 -0
- package/src/memory-records.ts +181 -0
- package/src/memory.ts +145 -0
- package/src/model-rates.ts +252 -0
- package/src/native-auth.ts +1006 -0
- package/src/native-sessions.ts +121 -0
- package/src/notification-state-cleanup.ts +18 -0
- package/src/ollama-web-search-cleanup.ts +69 -0
- package/src/package-page-shapes-cleanup.ts +190 -0
- package/src/plugin-egress.ts +31 -0
- package/src/plugin-page-route.ts +72 -0
- package/src/plugin-panels-cleanup.ts +101 -0
- package/src/prepared-input-cleanup.ts +105 -0
- package/src/production-secrets.ts +484 -0
- package/src/project-cleanup.ts +123 -0
- package/src/project-events-cleanup.ts +152 -0
- package/src/publication-state-cleanup.ts +92 -0
- package/src/push.ts +498 -0
- package/src/request-body.ts +165 -0
- package/src/routine-state-cleanup.ts +57 -0
- package/src/search.ts +88 -0
- package/src/sidebar-label-cleanup.ts +165 -0
- package/src/skill-index-cleanup.ts +53 -0
- package/src/supersede-cleanup.ts +162 -0
- package/src/test-chat-cleanup.ts +93 -0
- package/src/uploads.ts +486 -0
- package/src/user-application.ts +1068 -0
- package/src/user-configuration.ts +4491 -0
- package/src/voice-assistant.ts +4999 -0
- package/src/voice-dictation.ts +743 -0
- package/src/working-context-cleanup.ts +96 -0
- package/src/workspace.ts +201 -0
- package/wrangler.jsonc +385 -0
- package/README.md +0 -3
package/src/bot-state.ts
ADDED
|
@@ -0,0 +1,3995 @@
|
|
|
1
|
+
import { prepaidComputerHost } from "./billing-computer.js";
|
|
2
|
+
import type { UsageAttributionV1 } from "@frockbot/app/billing/ledger";
|
|
3
|
+
import { runCauseV1 } from "@frockbot/app/billing/run-cause";
|
|
4
|
+
import { runCauseReadersV1 } from "@frockbot/app/shell/run-cause";
|
|
5
|
+
import {
|
|
6
|
+
createHostedModelRatesReaderV1,
|
|
7
|
+
type HostedModelRatesAuthorityV1,
|
|
8
|
+
} from "./model-rates.js";
|
|
9
|
+
import { DEPLOYMENT_POLICY_SINGLETON_NAME } from "./deployment-policy.js";
|
|
10
|
+
import type { BillingAccountRpc } from "./billing.js";
|
|
11
|
+
import {
|
|
12
|
+
hostedBillingEnabledV1,
|
|
13
|
+
type BillingSwitchEnv,
|
|
14
|
+
} from "./billing-readiness.js";
|
|
15
|
+
import { cleanNotificationTestState } from "./notification-state-cleanup.js";
|
|
16
|
+
import { cleanHiddenBotNotifications } from "./hidden-bot-notifications-cleanup.js";
|
|
17
|
+
import { cleanRetiredRoutineStateV1 } from "./routine-state-cleanup.js";
|
|
18
|
+
import { cleanUnpreparedRunsV1 } from "./prepared-input-cleanup.js";
|
|
19
|
+
import { cleanSupersedeStateV1 } from "./supersede-cleanup.js";
|
|
20
|
+
import { cleanBotLabelV1 } from "./sidebar-label-cleanup.js";
|
|
21
|
+
import { cleanBotTitleV1 } from "./bot-title-cleanup.js";
|
|
22
|
+
import { cleanProjectEventsV1 } from "./project-events-cleanup.js";
|
|
23
|
+
import { cleanUndecodableSkillIndexesV1 } from "./skill-index-cleanup.js";
|
|
24
|
+
import {
|
|
25
|
+
messageIdV1,
|
|
26
|
+
visibleMessageRecordsV1,
|
|
27
|
+
type MessageNotice,
|
|
28
|
+
} from "@frockbot/app/notifications/messages";
|
|
29
|
+
import {
|
|
30
|
+
PUSH_OUTBOX_DRAIN_LIMIT,
|
|
31
|
+
PUSH_OUTBOX_PREFIX,
|
|
32
|
+
PUSH_READ_KEY,
|
|
33
|
+
} from "@frockbot/app/notifications/storage-keys";
|
|
34
|
+
import {
|
|
35
|
+
UNREAD_STATE_KEY,
|
|
36
|
+
optionalUnreadStateV1,
|
|
37
|
+
} from "@frockbot/app/shell/unread";
|
|
38
|
+
import type { PushUpdate } from "./push.js";
|
|
39
|
+
import { cleanIncidentTestChatsV1 } from "./test-chat-cleanup.js";
|
|
40
|
+
import { cleanBotAvatarTestState } from "./avatar-state-cleanup.js";
|
|
41
|
+
import { cleanBotProfileMirrorTestState } from "./directory-profile-cleanup.js";
|
|
42
|
+
import { cleanRetiredPublicationStateV1 } from "./publication-state-cleanup.js";
|
|
43
|
+
import { cleanCompactionAnnouncementsV1 } from "./compaction-announcement-cleanup.js";
|
|
44
|
+
import { projectUnprojectedSessionsV1 } from "./working-context-cleanup.js";
|
|
45
|
+
import { cleanRetiredComputerScreenshotsV1 } from "./computer-screenshot-cleanup.js";
|
|
46
|
+
import { createComputerLoginVaultV1 } from "@frockbot/app/shell/computer-logins";
|
|
47
|
+
import { computerBotPathKeyV1 } from "@frockbot/computer/core/bot-path";
|
|
48
|
+
import {
|
|
49
|
+
decodeStoredComputerFrameV1,
|
|
50
|
+
type StoredComputerFrameV1,
|
|
51
|
+
} from "@frockbot/computer/frame";
|
|
52
|
+
import {
|
|
53
|
+
deliverProfileMirrorV1,
|
|
54
|
+
PROFILE_MIRROR_KEY_V1,
|
|
55
|
+
} from "@frockbot/app/flock/profile-mirror";
|
|
56
|
+
import { DurableObject } from "cloudflare:workers";
|
|
57
|
+
import {
|
|
58
|
+
BotStateChannel,
|
|
59
|
+
BOT_STATE_CHANNEL_INTERNAL_PATH,
|
|
60
|
+
} from "./bot-state-channel.js";
|
|
61
|
+
import { foundationShellApplicationV1 } from "@frockbot/app/runtime";
|
|
62
|
+
import { BRAND_V1 } from "#brand";
|
|
63
|
+
import {
|
|
64
|
+
computerBotContribution,
|
|
65
|
+
createFoundationBackendContributions,
|
|
66
|
+
createFoundationMountedContributionsV1,
|
|
67
|
+
flockBotContribution,
|
|
68
|
+
backendDescriptorsV1,
|
|
69
|
+
shellBotContribution,
|
|
70
|
+
} from "@frockbot/app/contributions";
|
|
71
|
+
import { ComputerRegistry } from "@frockbot/computer/core/host";
|
|
72
|
+
import {
|
|
73
|
+
mountRuntimeFeaturesV1,
|
|
74
|
+
type TurnSupervisor,
|
|
75
|
+
} from "@frockbot/core/contracts";
|
|
76
|
+
import {
|
|
77
|
+
computerHostBindingV1,
|
|
78
|
+
createComputerHostV1,
|
|
79
|
+
} from "./computer-host.js";
|
|
80
|
+
import type { ShellComputerHostOptionsV1 } from "@frockbot/app/shell/backend-runtime";
|
|
81
|
+
import {
|
|
82
|
+
decodeBotConfigurationExecuteRpcV1,
|
|
83
|
+
decodeBotConfigurationReadRpcV1,
|
|
84
|
+
decodeUserSettingsViewV1,
|
|
85
|
+
decodeCompositionGenerationIdV1,
|
|
86
|
+
decodeRevertCompositionCommandV1,
|
|
87
|
+
MAX_COMPOSITION_GENERATION_PAGE_V1,
|
|
88
|
+
userTimezoneV1,
|
|
89
|
+
type BotSettingsViewV1,
|
|
90
|
+
type RevertCompositionCommandV1,
|
|
91
|
+
} from "@frockbot/core/configuration";
|
|
92
|
+
import {
|
|
93
|
+
BotDurableAuthority,
|
|
94
|
+
IDENTITY_KEY,
|
|
95
|
+
type BotIdentity,
|
|
96
|
+
type BotTurnCommand,
|
|
97
|
+
} from "@frockbot/core/durable";
|
|
98
|
+
import {
|
|
99
|
+
keepBotFilesV1,
|
|
100
|
+
recordUploadV1,
|
|
101
|
+
removeBotFilesV1,
|
|
102
|
+
resolveUploadRefsV1,
|
|
103
|
+
} from "@frockbot/app/uploads/bot";
|
|
104
|
+
import {
|
|
105
|
+
decodeStoredUploadV1,
|
|
106
|
+
type StoredUploadV1,
|
|
107
|
+
} from "@frockbot/app/uploads/shared";
|
|
108
|
+
import {
|
|
109
|
+
botFileStoreV1,
|
|
110
|
+
deleteBotUploadsV1,
|
|
111
|
+
releaseBotUploadQuotaRpcV1,
|
|
112
|
+
} from "./uploads.js";
|
|
113
|
+
import type {
|
|
114
|
+
OwnedBotTurnCommand,
|
|
115
|
+
ShellBotBackendContribution,
|
|
116
|
+
} from "@frockbot/app/shell/backend";
|
|
117
|
+
import type { BotStateEnv } from "@frockbot/app/shell/backend-state";
|
|
118
|
+
import type { BotStateRpcTargetV1 } from "@frockbot/app/shell/durable-rpc-targets";
|
|
119
|
+
import { createBindingEmailSenderV1 } from "@frockbot/app/email/sender";
|
|
120
|
+
import {
|
|
121
|
+
acceptSubagentTask,
|
|
122
|
+
claimTaskMessages,
|
|
123
|
+
listTasks,
|
|
124
|
+
readSubagentTaskContext,
|
|
125
|
+
readTask,
|
|
126
|
+
settleTask,
|
|
127
|
+
stopSubagentTask,
|
|
128
|
+
stopTaskForUser,
|
|
129
|
+
} from "@frockbot/app/subagents/bot";
|
|
130
|
+
import {
|
|
131
|
+
isolateConnection,
|
|
132
|
+
isolateModelTransport,
|
|
133
|
+
isolateAuthority,
|
|
134
|
+
isolateInvokeModel,
|
|
135
|
+
isolateSettings,
|
|
136
|
+
isolateStorageDelete,
|
|
137
|
+
isolateStorageGet,
|
|
138
|
+
isolateStorageList,
|
|
139
|
+
isolateStoragePut,
|
|
140
|
+
type IsolateCallScopeV1,
|
|
141
|
+
isolateMemoryForget,
|
|
142
|
+
isolateJevDecide,
|
|
143
|
+
isolateMemoryRead,
|
|
144
|
+
isolateMemoryWrite,
|
|
145
|
+
isolateEmail,
|
|
146
|
+
isolateSchedule,
|
|
147
|
+
isolateWorkspaceDelete,
|
|
148
|
+
isolateWorkspaceList,
|
|
149
|
+
isolateWorkspaceRead,
|
|
150
|
+
isolateWorkspaceStat,
|
|
151
|
+
isolateWorkspaceWrite,
|
|
152
|
+
} from "@frockbot/app/isolates/bot";
|
|
153
|
+
import { isolateDeviceCall } from "@frockbot/app/isolates/device";
|
|
154
|
+
import { deliverMachineResult } from "@frockbot/app/machine/bot";
|
|
155
|
+
import { answerComputerEgressV1 } from "@frockbot/computer/egress";
|
|
156
|
+
import { decodeComputerEgressAnswerInputV1 } from "./computer-egress.js";
|
|
157
|
+
import { listSkills } from "@frockbot/app/skills/bot";
|
|
158
|
+
import {
|
|
159
|
+
archiveEligible,
|
|
160
|
+
refreshScheduledWork,
|
|
161
|
+
} from "@frockbot/app/shell/identity";
|
|
162
|
+
import { stopRun } from "@frockbot/app/shell/turn";
|
|
163
|
+
import {
|
|
164
|
+
isVoiceReplyOutboxEntryV1,
|
|
165
|
+
VOICE_REPLY_OUTBOX_PREFIX_V1,
|
|
166
|
+
} from "@frockbot/app/shell/voice-reply";
|
|
167
|
+
import {
|
|
168
|
+
acknowledgeNotification,
|
|
169
|
+
listNotifications,
|
|
170
|
+
} from "@frockbot/app/notifications/bot";
|
|
171
|
+
import {
|
|
172
|
+
connectionTriggersFromUserV1,
|
|
173
|
+
pluginTriggerIndexFromUserV1,
|
|
174
|
+
deliverConnectEvent,
|
|
175
|
+
deliverRoutineModuleEvent,
|
|
176
|
+
listPluginTriggerRoutines,
|
|
177
|
+
deliverRoutineHook,
|
|
178
|
+
executeRoutineCommand,
|
|
179
|
+
executeRoutineInboxCommand,
|
|
180
|
+
listRoutineInbox,
|
|
181
|
+
listRoutineRuns,
|
|
182
|
+
listRoutines,
|
|
183
|
+
projectRoutineAccountTimezoneV1,
|
|
184
|
+
readRoutinesFrame,
|
|
185
|
+
readRoutineRun,
|
|
186
|
+
} from "@frockbot/app/routines/bot";
|
|
187
|
+
import {
|
|
188
|
+
BOT_CONFIGURATION_KEY,
|
|
189
|
+
executeConfiguration,
|
|
190
|
+
readBotSettingsV1,
|
|
191
|
+
readConfiguration,
|
|
192
|
+
resolveConfiguration,
|
|
193
|
+
userConfigurationV1,
|
|
194
|
+
} from "@frockbot/app/settings/bot";
|
|
195
|
+
import { decideApproval, listApprovals } from "@frockbot/app/approvals/bot";
|
|
196
|
+
import { cardAction, listCards, readCardView } from "@frockbot/app/cards/bot";
|
|
197
|
+
import { submitSecretV1 } from "@frockbot/app/secrets/bot";
|
|
198
|
+
import {
|
|
199
|
+
decodeSecretSubmitCommandV1,
|
|
200
|
+
type SecretSubmitCommandV1,
|
|
201
|
+
} from "@frockbot/app/secrets/shared";
|
|
202
|
+
import {
|
|
203
|
+
getCompositionGeneration,
|
|
204
|
+
listCompositionGenerations,
|
|
205
|
+
revertComposition,
|
|
206
|
+
} from "@frockbot/app/shell/composition-views";
|
|
207
|
+
import { executeUnreadCommand, readUnread } from "@frockbot/app/shell/unread";
|
|
208
|
+
import { appendAnnouncement } from "@frockbot/app/shell/reads";
|
|
209
|
+
import {
|
|
210
|
+
isBotTombstoneV1,
|
|
211
|
+
type FlockBotBackendContribution,
|
|
212
|
+
} from "@frockbot/app/flock/bot";
|
|
213
|
+
import type { ComputerBotBackendContribution } from "@frockbot/computer/bot";
|
|
214
|
+
import { decodeComputerCommandV1 } from "@frockbot/computer/protocol";
|
|
215
|
+
import {
|
|
216
|
+
decodeBotLifecycleCommandV1,
|
|
217
|
+
decodeBotRegistrationV1,
|
|
218
|
+
decodeUpdateAvatarCommandV1,
|
|
219
|
+
decodeUpdateVoiceCommandV1,
|
|
220
|
+
decodeUpdateLookCommandV1,
|
|
221
|
+
decodeLookIdentityViewV1,
|
|
222
|
+
type BotLifecycleCommandV1,
|
|
223
|
+
type BotRegistrationV1,
|
|
224
|
+
} from "@frockbot/app/flock/shared";
|
|
225
|
+
import { decodeBotDebugQueryV1 } from "@frockbot/app/shell/debug-protocol";
|
|
226
|
+
import {
|
|
227
|
+
decodeClientRunListQueryV1,
|
|
228
|
+
decodeClientRunLookupQueryV1,
|
|
229
|
+
decodeClientRunStopCommandV1,
|
|
230
|
+
type ClientRunListQueryV1,
|
|
231
|
+
type ClientRunLookupQueryV1,
|
|
232
|
+
type ClientRunStopCommandV1,
|
|
233
|
+
} from "@frockbot/app/shell/run-protocol";
|
|
234
|
+
import {
|
|
235
|
+
decodeBotUnreadCommandV1,
|
|
236
|
+
type BotUnreadCommandV1,
|
|
237
|
+
} from "@frockbot/app/shell/unread";
|
|
238
|
+
import {
|
|
239
|
+
decodeApprovalDecisionCommandV1,
|
|
240
|
+
type ApprovalDecisionCommandV1,
|
|
241
|
+
} from "@frockbot/app/shell/approvals";
|
|
242
|
+
import {
|
|
243
|
+
decodeCardActionCommandV1,
|
|
244
|
+
decodeCardSurfaceIdV1,
|
|
245
|
+
type CardActionCommandV1,
|
|
246
|
+
} from "@frockbot/app/shell/cards";
|
|
247
|
+
import {
|
|
248
|
+
decodeIsolateJevRequestV1,
|
|
249
|
+
decodeIsolateMemoryReadRequestV1,
|
|
250
|
+
decodeIsolateStorageDeleteRequestV1,
|
|
251
|
+
decodeIsolateStorageGetRequestV1,
|
|
252
|
+
decodeIsolateStorageListRequestV1,
|
|
253
|
+
decodeIsolateStoragePutRequestV1,
|
|
254
|
+
decodeIsolateMemoryWriteRequestV1,
|
|
255
|
+
decodeIsolateDeviceCallRequestV1,
|
|
256
|
+
decodeIsolateEmailRequestV1,
|
|
257
|
+
decodeIsolateScheduleRequestV1,
|
|
258
|
+
decodeIsolateWorkspaceDeleteRequestV1,
|
|
259
|
+
decodeIsolateWorkspaceListRequestV1,
|
|
260
|
+
decodeIsolateWorkspacePathV1,
|
|
261
|
+
decodeIsolateWorkspaceWriteRequestV1,
|
|
262
|
+
decodeNormalizedModelRequestV1,
|
|
263
|
+
} from "@frockbot/core/contracts";
|
|
264
|
+
import type {
|
|
265
|
+
NormalizedModelRequest,
|
|
266
|
+
WorkspaceFilesV1,
|
|
267
|
+
WorkspaceGenerationsV1,
|
|
268
|
+
WorkspacePathV1,
|
|
269
|
+
WorkspaceRootV1,
|
|
270
|
+
WorkspaceSyncEffectsV1,
|
|
271
|
+
} from "@frockbot/core/contracts";
|
|
272
|
+
import { decodeWorkspacePathV1 } from "@frockbot/core/contracts";
|
|
273
|
+
|
|
274
|
+
/** Base64 without a Node Buffer: this object runs in workerd. */
|
|
275
|
+
function bytesToBase64(bytes: Uint8Array): string {
|
|
276
|
+
let binary = "";
|
|
277
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
278
|
+
return btoa(binary);
|
|
279
|
+
}
|
|
280
|
+
import {
|
|
281
|
+
decodeRoutineCommandV1,
|
|
282
|
+
decodeRoutineInboxCommandV1,
|
|
283
|
+
type RoutineCommandV1,
|
|
284
|
+
type RoutineInboxCommandV1,
|
|
285
|
+
} from "@frockbot/app/routines/shared";
|
|
286
|
+
import {
|
|
287
|
+
decodeRoutineHookDeliveryV1,
|
|
288
|
+
ROUTINE_HOOK_BODY_MAX_BYTES,
|
|
289
|
+
type RoutineHookDeliveryV1,
|
|
290
|
+
} from "@frockbot/app/routines/hook";
|
|
291
|
+
import type { RoutineModuleEventDeliveryV1 } from "@frockbot/app/routines/store";
|
|
292
|
+
import {
|
|
293
|
+
decodeSubagentRunTaskRequestV1,
|
|
294
|
+
type SubagentRunTaskRequestV1,
|
|
295
|
+
} from "@frockbot/app/subagents/durable-binding";
|
|
296
|
+
import {
|
|
297
|
+
decodeTaskOutcomeV1,
|
|
298
|
+
type TaskOutcomeV1,
|
|
299
|
+
} from "@frockbot/app/subagents/records";
|
|
300
|
+
import {
|
|
301
|
+
decodeMachineResultDeliveryV1,
|
|
302
|
+
type MachineResultDeliveryV1,
|
|
303
|
+
} from "@frockbot/app/machine/delivery";
|
|
304
|
+
import {
|
|
305
|
+
createDurableWorkspaceFilesV1,
|
|
306
|
+
createR2ObjectBucketV1,
|
|
307
|
+
deleteBotWorkspaceRootsV1,
|
|
308
|
+
} from "./workspace.js";
|
|
309
|
+
import {
|
|
310
|
+
publishWorkspaceSkillGenerationV1,
|
|
311
|
+
readDurableSkillIndexV1,
|
|
312
|
+
} from "@frockbot/app/skills/index-store";
|
|
313
|
+
import { decodeSkillMetadataIndexV1 } from "@frockbot/app/skills/metadata-index";
|
|
314
|
+
import { reseedInstructionRootV1 } from "@frockbot/app/skills/reseed";
|
|
315
|
+
import {
|
|
316
|
+
workspaceObjectPrefixV1,
|
|
317
|
+
type WorkspaceGenerationPublicationV1,
|
|
318
|
+
} from "@frockbot/core/workspace-store";
|
|
319
|
+
import { cleanRetiredMemoryFactObjectsV1 } from "@frockbot/app/memory/cleanup";
|
|
320
|
+
import type { ClientWorkspaceFileV1 } from "./contracts.js";
|
|
321
|
+
import {
|
|
322
|
+
DurableWorkspaceGenerations,
|
|
323
|
+
DurableWorkspaceSyncEffects,
|
|
324
|
+
} from "@frockbot/core/durable";
|
|
325
|
+
import type { MemoryGroupsV1 } from "@frockbot/app/memory/groups";
|
|
326
|
+
import { memoryMembershipRevisionV1 } from "@frockbot/app/memory/engine-tools";
|
|
327
|
+
import {
|
|
328
|
+
decodeMemoryChunkIndexEntryV1,
|
|
329
|
+
memoryChunkIndexEntriesV1,
|
|
330
|
+
MEMORY_CHUNK_INDEX_PREFIX_V1,
|
|
331
|
+
type MemoryChunkIndexWriterV1,
|
|
332
|
+
} from "@frockbot/app/memory/chunk-index";
|
|
333
|
+
import {
|
|
334
|
+
searchRowsFromClientRunV1,
|
|
335
|
+
type SearchSinkV1,
|
|
336
|
+
} from "@frockbot/app/search";
|
|
337
|
+
import {
|
|
338
|
+
createBotSearchRowPageV1,
|
|
339
|
+
createUserSearchSinkV1,
|
|
340
|
+
type UserSearchRpc,
|
|
341
|
+
} from "./search.js";
|
|
342
|
+
import {
|
|
343
|
+
AuditOutboxV1,
|
|
344
|
+
auditEntriesFromStoredRunV1,
|
|
345
|
+
auditEntryForDeviceUseV1,
|
|
346
|
+
decodeDeviceUseV1,
|
|
347
|
+
DEVICE_USE_PAGE_DONE_V1,
|
|
348
|
+
DeviceUseLogV1,
|
|
349
|
+
type AuditSinkV1,
|
|
350
|
+
type DeviceUseV1,
|
|
351
|
+
} from "@frockbot/app/audit";
|
|
352
|
+
import {
|
|
353
|
+
createBotAuditEntryPageV1,
|
|
354
|
+
createUserAuditSinkV1,
|
|
355
|
+
type UserAuditRpc,
|
|
356
|
+
} from "./audit.js";
|
|
357
|
+
import {
|
|
358
|
+
createRoutedWorkspaceGenerationsV1,
|
|
359
|
+
createUserMemoryGroupsV1,
|
|
360
|
+
createUserWorkspaceGenerationsV1,
|
|
361
|
+
type UserMemoryRpc,
|
|
362
|
+
} from "./memory.js";
|
|
363
|
+
import { createMemoryEmbedder } from "@frockbot/app/memory/embeddings";
|
|
364
|
+
import { MemoryRecordsV1 } from "@frockbot/app/memory/owner";
|
|
365
|
+
import { createVectorMemorySearchV1 } from "@frockbot/app/memory/semantic";
|
|
366
|
+
import { createHostedMemoryExtractorV1 } from "@frockbot/app/memory/extraction-host";
|
|
367
|
+
import {
|
|
368
|
+
createBotMemoryEngineV1,
|
|
369
|
+
createUserMemoryRecordsRemoteV1,
|
|
370
|
+
drainDurableMemoryV1,
|
|
371
|
+
durableObjectHasSqlV1,
|
|
372
|
+
} from "./memory-records.js";
|
|
373
|
+
import type { MemoryEngineV1 } from "@frockbot/app/memory/engine";
|
|
374
|
+
import type {
|
|
375
|
+
MemoryAiBinding,
|
|
376
|
+
MemoryVectorIndex,
|
|
377
|
+
} from "@frockbot/app/memory/types";
|
|
378
|
+
import {
|
|
379
|
+
createFrockAiGatewayHostV1,
|
|
380
|
+
type FrockAiGatewayHostV1,
|
|
381
|
+
} from "./frock-ai.js";
|
|
382
|
+
import {
|
|
383
|
+
decodeBotAgentRunRpcV1,
|
|
384
|
+
decodeBotRunRpcV1,
|
|
385
|
+
type DecodedBotRunRpcV1,
|
|
386
|
+
decodeBotVoiceRunRpcV1,
|
|
387
|
+
decodeVoiceChatResultRpcV1,
|
|
388
|
+
decodeVoiceCallTranscriptRpcV1,
|
|
389
|
+
decodeBotGroupTurnRpcV1,
|
|
390
|
+
decodeBotEmailTurnRpcV1,
|
|
391
|
+
decodeRpcEnvelopeV1,
|
|
392
|
+
rpcBoolean,
|
|
393
|
+
rpcBotId,
|
|
394
|
+
rpcDecoded,
|
|
395
|
+
rpcEnum,
|
|
396
|
+
rpcIdentifier,
|
|
397
|
+
rpcInteger,
|
|
398
|
+
rpcJsonSnapshotV1,
|
|
399
|
+
rpcObject,
|
|
400
|
+
rpcOrigin,
|
|
401
|
+
rpcPattern,
|
|
402
|
+
rpcPluginIdOrNull,
|
|
403
|
+
rpcString,
|
|
404
|
+
} from "./durable-rpc.js";
|
|
405
|
+
import { answeredEntryV1, loggedEntryV1 } from "./entry-boundary.js";
|
|
406
|
+
import {
|
|
407
|
+
PLUGIN_ID_MAX_LENGTH_V1,
|
|
408
|
+
PLUGIN_ID_V1,
|
|
409
|
+
PluginEnablementConflictError,
|
|
410
|
+
readPluginEnablementV1,
|
|
411
|
+
setPluginEnabledV1,
|
|
412
|
+
} from "@frockbot/app/plugins/enablement";
|
|
413
|
+
import {
|
|
414
|
+
readBotPluginsFrameV1,
|
|
415
|
+
setBotPluginEnabledV1,
|
|
416
|
+
} from "@frockbot/app/plugins/bot";
|
|
417
|
+
import {
|
|
418
|
+
decodePluginToolCommandV1,
|
|
419
|
+
decodeSetBotPluginEnabledCommandV1,
|
|
420
|
+
} from "@frockbot/app/plugins/page";
|
|
421
|
+
import { executeBotPluginToolV1 } from "@frockbot/app/plugins/views-bot";
|
|
422
|
+
import {
|
|
423
|
+
applyPanelFocusV1,
|
|
424
|
+
openFocusedPanelV1,
|
|
425
|
+
panelDeviceUserV1,
|
|
426
|
+
readFocusedPanelV1,
|
|
427
|
+
} from "@frockbot/app/plugins/panels-bot";
|
|
428
|
+
import {
|
|
429
|
+
decodePluginPageReportV1,
|
|
430
|
+
recordPluginPageReportV1,
|
|
431
|
+
type PluginPageReportCommandV1,
|
|
432
|
+
} from "@frockbot/app/plugins/page-reports";
|
|
433
|
+
import { readBotPluginRosterV1 } from "@frockbot/app/plugins/worker-bot";
|
|
434
|
+
import { cleanBotAppletsV1 } from "./plugin-panels-cleanup.js";
|
|
435
|
+
import { cleanPackagePageShapesV1 } from "./package-page-shapes-cleanup.js";
|
|
436
|
+
import { cleanBotMachineMessagesV1 } from "./machine-messages-cleanup.js";
|
|
437
|
+
import {
|
|
438
|
+
groupOriginOfRunV1,
|
|
439
|
+
groupTurnStateOfRunV1,
|
|
440
|
+
groupWaitingKeyV1,
|
|
441
|
+
} from "@frockbot/app/groups/bot";
|
|
442
|
+
import { groupChatObjectNameV1 } from "@frockbot/app/groups/shared";
|
|
443
|
+
import { assembleBotThemeV1 } from "@frockbot/app/theme/assemble";
|
|
444
|
+
import {
|
|
445
|
+
deferThemeAssembleV1,
|
|
446
|
+
holdThemePickV1,
|
|
447
|
+
themeAssembleDeadlineV1,
|
|
448
|
+
} from "@frockbot/app/theme/owed";
|
|
449
|
+
import { decodeThemeDocumentV1, decodeBotLookV1 } from "@frockbot/core/theme";
|
|
450
|
+
|
|
451
|
+
function isFrockAiGatewayBindingV1(
|
|
452
|
+
value: BotStateEnv["AI"],
|
|
453
|
+
): value is NonNullable<BotStateEnv["AI"]> & Pick<Ai, "gateway"> {
|
|
454
|
+
return (
|
|
455
|
+
value !== undefined && typeof Reflect.get(value, "gateway") === "function"
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function optionalWorkerVarV1(env: object, name: string): string | undefined {
|
|
460
|
+
const value = Reflect.get(env, name);
|
|
461
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* A Frock AI setting, read under its current name and then under the
|
|
466
|
+
* pre-rename `FLOCK_AI_*` one. The vars are deployment configuration and the
|
|
467
|
+
* secrets are set outside this repo, so the fallback is what lets the rename
|
|
468
|
+
* land before they are re-added under the new names. Remove it once every
|
|
469
|
+
* environment names them `FROCK_AI_*`.
|
|
470
|
+
*/
|
|
471
|
+
export function frockAiWorkerVarV1(
|
|
472
|
+
env: object,
|
|
473
|
+
name: `FROCK_AI_${string}`,
|
|
474
|
+
): string | undefined {
|
|
475
|
+
return (
|
|
476
|
+
optionalWorkerVarV1(env, name) ??
|
|
477
|
+
optionalWorkerVarV1(env, `FLOCK_AI_${name.slice("FROCK_AI_".length)}`)
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** One Vectorize mutation per alarm firing, at the Workers binding limit. */
|
|
482
|
+
export const MEMORY_VECTOR_DELETE_BATCH_SIZE_V1 = 1_000;
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Voice answers handed over per drain pass. A call is one person speaking, so
|
|
486
|
+
* a queue this long already means something upstream is stuck; the bound is
|
|
487
|
+
* there to keep one pass from being unbounded, not because it is ever reached.
|
|
488
|
+
*/
|
|
489
|
+
export const VOICE_REPLY_DRAIN_LIMIT_V1 = 32;
|
|
490
|
+
const MEMORY_VECTOR_PURGE_JOURNAL_KEY_V1 = "memory:vector-purge:v1";
|
|
491
|
+
const MEMORY_VECTOR_PURGE_RETRY_DELAY_MS_V1 = 1_000;
|
|
492
|
+
|
|
493
|
+
interface MemoryVectorPurgeJournalV1 {
|
|
494
|
+
schemaVersion: 1;
|
|
495
|
+
userId: string;
|
|
496
|
+
botId: string;
|
|
497
|
+
status: "pending" | "complete" | "skipped";
|
|
498
|
+
deleted: number;
|
|
499
|
+
note?: string;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function decodeMemoryVectorPurgeJournalV1(
|
|
503
|
+
input: unknown,
|
|
504
|
+
): MemoryVectorPurgeJournalV1 {
|
|
505
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
506
|
+
throw new Error("Stored Memory vector purge journal is invalid");
|
|
507
|
+
}
|
|
508
|
+
const allowed = new Set([
|
|
509
|
+
"schemaVersion",
|
|
510
|
+
"userId",
|
|
511
|
+
"botId",
|
|
512
|
+
"status",
|
|
513
|
+
"deleted",
|
|
514
|
+
"note",
|
|
515
|
+
]);
|
|
516
|
+
const status = Reflect.get(input, "status");
|
|
517
|
+
const note = Reflect.get(input, "note");
|
|
518
|
+
if (
|
|
519
|
+
Object.keys(input).some((field) => !allowed.has(field)) ||
|
|
520
|
+
Reflect.get(input, "schemaVersion") !== 1 ||
|
|
521
|
+
typeof Reflect.get(input, "userId") !== "string" ||
|
|
522
|
+
typeof Reflect.get(input, "botId") !== "string" ||
|
|
523
|
+
(status !== "pending" && status !== "complete" && status !== "skipped") ||
|
|
524
|
+
!Number.isSafeInteger(Reflect.get(input, "deleted")) ||
|
|
525
|
+
(Reflect.get(input, "deleted") as number) < 0 ||
|
|
526
|
+
(note !== undefined && typeof note !== "string")
|
|
527
|
+
) {
|
|
528
|
+
throw new Error("Stored Memory vector purge journal is invalid");
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
schemaVersion: 1,
|
|
532
|
+
userId: Reflect.get(input, "userId") as string,
|
|
533
|
+
botId: Reflect.get(input, "botId") as string,
|
|
534
|
+
status,
|
|
535
|
+
deleted: Reflect.get(input, "deleted") as number,
|
|
536
|
+
...(typeof note === "string" ? { note } : {}),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export type { BotStateEnv, OwnedBotTurnCommand };
|
|
541
|
+
|
|
542
|
+
export interface BotStateDependencies {
|
|
543
|
+
outboundFetch?: typeof fetch;
|
|
544
|
+
/** A test's stand-in for Turn supervision; production builds it from env. */
|
|
545
|
+
turnSupervisor?: TurnSupervisor;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function decodeBotIdentityRpcV1(input: unknown): {
|
|
549
|
+
userId: string;
|
|
550
|
+
botId: string;
|
|
551
|
+
} {
|
|
552
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
553
|
+
userId: rpcIdentifier,
|
|
554
|
+
botId: rpcBotId,
|
|
555
|
+
});
|
|
556
|
+
if (typeof request.userId !== "string" || typeof request.botId !== "string") {
|
|
557
|
+
throw new Error("Computer identity RPC was not decoded");
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
userId: request.userId,
|
|
561
|
+
botId: request.botId,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function decodeIsolateCallRpcV1(
|
|
566
|
+
input: unknown,
|
|
567
|
+
decodeRequest: (value: unknown) => unknown,
|
|
568
|
+
) {
|
|
569
|
+
return decodeRpcEnvelopeV1(input, {
|
|
570
|
+
userId: rpcIdentifier,
|
|
571
|
+
botId: rpcBotId,
|
|
572
|
+
runId: rpcIdentifier,
|
|
573
|
+
sessionId: rpcString(257),
|
|
574
|
+
turnId: rpcIdentifier,
|
|
575
|
+
packageId: rpcIdentifier,
|
|
576
|
+
generationId: rpcIdentifier,
|
|
577
|
+
request: rpcDecoded(decodeRequest),
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export class BotState
|
|
582
|
+
extends DurableObject<BotStateEnv>
|
|
583
|
+
implements BotStateRpcTargetV1
|
|
584
|
+
{
|
|
585
|
+
private readonly outboundFetch?: typeof fetch;
|
|
586
|
+
private readonly turnSupervisor?: TurnSupervisor;
|
|
587
|
+
/**
|
|
588
|
+
* The environment the Shell Package runs under: the Durable Object's
|
|
589
|
+
* bindings plus the Workspace file surface built over them. `WORKSPACE_FILES`
|
|
590
|
+
* is not a Worker binding — it is `WorkspaceFilesV1` over the durable-root
|
|
591
|
+
* object store, with its generations recorded in this object — so it is
|
|
592
|
+
* constructed here and never reaches the deployed bindings map.
|
|
593
|
+
*/
|
|
594
|
+
protected readonly backendEnv: BotStateEnv & {
|
|
595
|
+
FROCK_AI?: FrockAiGatewayHostV1;
|
|
596
|
+
WORKSPACE_FILES?: WorkspaceFilesV1;
|
|
597
|
+
MEMORY_WORKSPACE_FILES?: WorkspaceFilesV1;
|
|
598
|
+
MEMORY_GROUPS?: MemoryGroupsV1;
|
|
599
|
+
MEMORY_RECORDS?: MemoryRecordsV1;
|
|
600
|
+
MEMORY_CHUNK_INDEX?: MemoryChunkIndexWriterV1;
|
|
601
|
+
WORKSPACE_SYNC_FILES?: WorkspaceFilesV1;
|
|
602
|
+
WORKSPACE_SYNC_EFFECTS?: WorkspaceSyncEffectsV1;
|
|
603
|
+
WORKSPACE_SYNC_GENERATIONS?: WorkspaceGenerationsV1;
|
|
604
|
+
/** The User-scoped transcript index a settled Turn projects into. */
|
|
605
|
+
SEARCH_SINK?: SearchSinkV1;
|
|
606
|
+
/** The User-scoped audit table this object's outbox drains into. */
|
|
607
|
+
AUDIT_SINK?: AuditSinkV1;
|
|
608
|
+
};
|
|
609
|
+
/** The identity the Workspace and Memory surfaces above were built for. */
|
|
610
|
+
private surfacesFor: string | undefined;
|
|
611
|
+
/**
|
|
612
|
+
* This object's generation ledger — one instance, for every root it owns.
|
|
613
|
+
*
|
|
614
|
+
* "The Bot's Durable Object is the authority for everything Bot-scoped", and
|
|
615
|
+
* an authority that exists twice is not one: each instance caches the
|
|
616
|
+
* minting cursor while resident, so two of them can mint the same id for two
|
|
617
|
+
* different files.
|
|
618
|
+
*/
|
|
619
|
+
protected readonly workspaceGenerations: DurableWorkspaceGenerations =
|
|
620
|
+
new DurableWorkspaceGenerations({ state: this.ctx });
|
|
621
|
+
/** Durable invalidation log plus hibernatable observer transport. */
|
|
622
|
+
private readonly stateChannel = new BotStateChannel(this.ctx);
|
|
623
|
+
private mountedShell: ShellBotBackendContribution["state"] | undefined;
|
|
624
|
+
private mounted:
|
|
625
|
+
| Promise<{
|
|
626
|
+
shell: ShellBotBackendContribution;
|
|
627
|
+
flock: FlockBotBackendContribution;
|
|
628
|
+
computer: ComputerBotBackendContribution;
|
|
629
|
+
dispose(): Promise<void>;
|
|
630
|
+
}>
|
|
631
|
+
| undefined;
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Adopt the User's Profile timezone before a Routine is listed or written.
|
|
635
|
+
*
|
|
636
|
+
* Best effort: the projection already holds a zone, and a User object that
|
|
637
|
+
* cannot be read is a "next run" column computed under the zone last
|
|
638
|
+
* projected — never a Routines list that refuses to open.
|
|
639
|
+
*/
|
|
640
|
+
private async syncRoutineTimezone(
|
|
641
|
+
identity: BotIdentity,
|
|
642
|
+
shell: ShellBotBackendContribution,
|
|
643
|
+
): Promise<void> {
|
|
644
|
+
try {
|
|
645
|
+
const rpc = this.env.USER_CONFIGURATIONS.get(
|
|
646
|
+
this.env.USER_CONFIGURATIONS.idFromName(identity.userId),
|
|
647
|
+
) as unknown as { readConfiguration(input: unknown): Promise<unknown> };
|
|
648
|
+
const user = decodeUserSettingsViewV1(
|
|
649
|
+
rpcJsonSnapshotV1(
|
|
650
|
+
await rpc.readConfiguration({
|
|
651
|
+
schemaVersion: 1,
|
|
652
|
+
userId: identity.userId,
|
|
653
|
+
view: 2,
|
|
654
|
+
}),
|
|
655
|
+
),
|
|
656
|
+
);
|
|
657
|
+
await projectRoutineAccountTimezoneV1(
|
|
658
|
+
shell.state,
|
|
659
|
+
userTimezoneV1(user.profile),
|
|
660
|
+
user.revision,
|
|
661
|
+
);
|
|
662
|
+
} catch {
|
|
663
|
+
// Left at the zone last projected, which the next Turn refreshes.
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
constructor(
|
|
668
|
+
ctx: DurableObjectState,
|
|
669
|
+
env: BotStateEnv,
|
|
670
|
+
dependencies: BotStateDependencies = {},
|
|
671
|
+
) {
|
|
672
|
+
super(ctx, env);
|
|
673
|
+
// Runs before any request or alarm can mount the old conversation.
|
|
674
|
+
this.ctx.blockConcurrencyWhile(async () => {
|
|
675
|
+
// A deleted Bot has no conversation left to clean, and a receipt
|
|
676
|
+
// written here would be the only thing in it besides its tombstone.
|
|
677
|
+
if (await isBotTombstoneV1(this.ctx.storage)) return;
|
|
678
|
+
// Before anything decodes the settings or a run: a profile `label` or
|
|
679
|
+
// `title` no longer parses.
|
|
680
|
+
await cleanBotLabelV1(this.ctx.storage);
|
|
681
|
+
await cleanBotTitleV1(this.ctx.storage);
|
|
682
|
+
await cleanIncidentTestChatsV1(this.ctx.storage);
|
|
683
|
+
await cleanNotificationTestState(this.ctx.storage);
|
|
684
|
+
await cleanHiddenBotNotifications(this.ctx.storage);
|
|
685
|
+
await cleanBotAvatarTestState(this.ctx.storage);
|
|
686
|
+
await cleanBotProfileMirrorTestState(this.ctx.storage);
|
|
687
|
+
await cleanRetiredRoutineStateV1(this.ctx.storage);
|
|
688
|
+
await cleanBotAppletsV1(this.ctx.storage);
|
|
689
|
+
// Before anything decodes a run: a `superseded` record no longer parses.
|
|
690
|
+
await cleanSupersedeStateV1(this.ctx.storage);
|
|
691
|
+
// Before anything decodes a run or an event: a `directTool` run and a
|
|
692
|
+
// `publish` sync no longer parse.
|
|
693
|
+
await cleanPackagePageShapesV1(this.ctx.storage);
|
|
694
|
+
// Before anything decodes a machine intent: a `messages` op no longer parses.
|
|
695
|
+
await cleanBotMachineMessagesV1(this.ctx.storage);
|
|
696
|
+
await cleanProjectEventsV1(this.ctx.storage);
|
|
697
|
+
await cleanUnpreparedRunsV1(this.ctx.storage);
|
|
698
|
+
await cleanUndecodableSkillIndexesV1(this.ctx.storage);
|
|
699
|
+
await cleanRetiredPublicationStateV1(this.ctx.storage);
|
|
700
|
+
await cleanCompactionAnnouncementsV1(this.ctx.storage);
|
|
701
|
+
await projectUnprojectedSessionsV1(this.ctx.storage);
|
|
702
|
+
const identity = await this.ctx.storage.get<{
|
|
703
|
+
userId: string;
|
|
704
|
+
botId: string;
|
|
705
|
+
}>(IDENTITY_KEY);
|
|
706
|
+
if (identity && this.env.MEMORY_FILES) {
|
|
707
|
+
await reseedInstructionRootV1({
|
|
708
|
+
storage: this.ctx.storage,
|
|
709
|
+
bucket: createR2ObjectBucketV1(this.env.MEMORY_FILES),
|
|
710
|
+
root: {
|
|
711
|
+
kind: "bot-instructions",
|
|
712
|
+
userId: identity.userId,
|
|
713
|
+
botId: identity.botId,
|
|
714
|
+
},
|
|
715
|
+
receiptKey: "maintenance:skill-index:bot:2026-09-22",
|
|
716
|
+
});
|
|
717
|
+
const bucket = createR2ObjectBucketV1(this.env.MEMORY_FILES);
|
|
718
|
+
const listing = {
|
|
719
|
+
list: async (options: {
|
|
720
|
+
prefix: string;
|
|
721
|
+
limit: number;
|
|
722
|
+
cursor?: string;
|
|
723
|
+
}) => {
|
|
724
|
+
const page = await bucket.list(options);
|
|
725
|
+
return {
|
|
726
|
+
keys: page.objects.map((object) => object.key),
|
|
727
|
+
...(page.cursor ? { cursor: page.cursor } : {}),
|
|
728
|
+
truncated: page.truncated,
|
|
729
|
+
};
|
|
730
|
+
},
|
|
731
|
+
delete: (key: string) => bucket.delete(key),
|
|
732
|
+
};
|
|
733
|
+
await cleanRetiredMemoryFactObjectsV1(
|
|
734
|
+
this.ctx.storage,
|
|
735
|
+
listing,
|
|
736
|
+
workspaceObjectPrefixV1({
|
|
737
|
+
kind: "bot-memory",
|
|
738
|
+
userId: identity.userId,
|
|
739
|
+
botId: identity.botId,
|
|
740
|
+
}),
|
|
741
|
+
);
|
|
742
|
+
await cleanRetiredComputerScreenshotsV1(
|
|
743
|
+
this.ctx.storage,
|
|
744
|
+
listing,
|
|
745
|
+
`${workspaceObjectPrefixV1({
|
|
746
|
+
kind: "package-declared",
|
|
747
|
+
userId: identity.userId,
|
|
748
|
+
packageId: "computer",
|
|
749
|
+
rootId: "screenshots",
|
|
750
|
+
})}${computerBotPathKeyV1(identity.botId)}/`,
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
if (durableObjectHasSqlV1(this.ctx.storage)) {
|
|
754
|
+
const memoryDue = createBotMemoryEngineV1(
|
|
755
|
+
this.ctx.storage,
|
|
756
|
+
).nextWakeupAt();
|
|
757
|
+
if (
|
|
758
|
+
memoryDue !== undefined &&
|
|
759
|
+
(await this.ctx.storage.getAlarm()) === null
|
|
760
|
+
) {
|
|
761
|
+
await this.ctx.storage.setAlarm(memoryDue);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
this.outboundFetch = dependencies.outboundFetch;
|
|
766
|
+
this.turnSupervisor = dependencies.turnSupervisor;
|
|
767
|
+
const emailSender = createBindingEmailSenderV1(
|
|
768
|
+
env as Parameters<typeof createBindingEmailSenderV1>[0],
|
|
769
|
+
BRAND_V1.emailSenderName,
|
|
770
|
+
);
|
|
771
|
+
// One copy of the deployment's rate table per Bot object: billing prices
|
|
772
|
+
// from it and the Gateway bounds each request by it.
|
|
773
|
+
const hostedRates = hostedBillingEnabledV1(env as BillingSwitchEnv)
|
|
774
|
+
? createHostedModelRatesReaderV1(
|
|
775
|
+
() =>
|
|
776
|
+
(
|
|
777
|
+
env as BotStateEnv & { DEPLOYMENT_POLICY: DurableObjectNamespace }
|
|
778
|
+
).DEPLOYMENT_POLICY.getByName(
|
|
779
|
+
DEPLOYMENT_POLICY_SINGLETON_NAME,
|
|
780
|
+
) as unknown as HostedModelRatesAuthorityV1,
|
|
781
|
+
)
|
|
782
|
+
: undefined;
|
|
783
|
+
// The surfaces are built per identity in `bindSurfaces`, not here: they
|
|
784
|
+
// carry the `owner` guard, and a Durable Object learns which User it
|
|
785
|
+
// serves from the RPC that addresses it, never from its constructor.
|
|
786
|
+
this.backendEnv = {
|
|
787
|
+
...env,
|
|
788
|
+
...(hostedBillingEnabledV1(env as BillingSwitchEnv) && env.COMPUTER_HOST
|
|
789
|
+
? {
|
|
790
|
+
COMPUTER_HOST: prepaidComputerHost(
|
|
791
|
+
env.COMPUTER_HOST,
|
|
792
|
+
(userId) =>
|
|
793
|
+
env.USER_CONFIGURATIONS.get(
|
|
794
|
+
env.USER_CONFIGURATIONS.idFromName(userId),
|
|
795
|
+
) as unknown as BillingAccountRpc,
|
|
796
|
+
undefined,
|
|
797
|
+
(botId, personal) => this.computerSpend(botId, personal),
|
|
798
|
+
),
|
|
799
|
+
}
|
|
800
|
+
: {}),
|
|
801
|
+
...(hostedRates
|
|
802
|
+
? {
|
|
803
|
+
BILLING: (
|
|
804
|
+
userId: string,
|
|
805
|
+
botId: string,
|
|
806
|
+
sessionId: string,
|
|
807
|
+
spend?: import("@frockbot/app/billing/ledger").UsageAttributionV1,
|
|
808
|
+
) => {
|
|
809
|
+
const account = env.USER_CONFIGURATIONS.get(
|
|
810
|
+
env.USER_CONFIGURATIONS.idFromName(userId),
|
|
811
|
+
) as unknown as BillingAccountRpc;
|
|
812
|
+
return {
|
|
813
|
+
botId,
|
|
814
|
+
sessionId,
|
|
815
|
+
...(spend ? { spend } : {}),
|
|
816
|
+
limits: {
|
|
817
|
+
paused: (scopes: string[]) =>
|
|
818
|
+
account.readSpendingPaused({ userId, scopes }),
|
|
819
|
+
claimSpike: (scope: string) =>
|
|
820
|
+
account.claimSpendingSpike({ userId, scope }),
|
|
821
|
+
},
|
|
822
|
+
rates: hostedRates.rates,
|
|
823
|
+
reportUnpriced: (report: {
|
|
824
|
+
servedModel: string | null;
|
|
825
|
+
route: string;
|
|
826
|
+
version: number;
|
|
827
|
+
}) => this.ctx.waitUntil(hostedRates.reportUnpriced(report)),
|
|
828
|
+
account: {
|
|
829
|
+
// A charge that names no cause of its own — a search —
|
|
830
|
+
// is recorded against the Turn this billing was made for.
|
|
831
|
+
reserve: (
|
|
832
|
+
reservation: import("@frockbot/app/billing/ledger").UsageReservation,
|
|
833
|
+
) =>
|
|
834
|
+
account.reserveUsage({
|
|
835
|
+
userId,
|
|
836
|
+
reservation:
|
|
837
|
+
spend && !reservation.attribution
|
|
838
|
+
? { ...reservation, attribution: spend }
|
|
839
|
+
: reservation,
|
|
840
|
+
}),
|
|
841
|
+
settle: (
|
|
842
|
+
settlement: import("@frockbot/app/billing/ledger").UsageSettlement,
|
|
843
|
+
) => account.settleUsage({ userId, settlement }),
|
|
844
|
+
},
|
|
845
|
+
};
|
|
846
|
+
},
|
|
847
|
+
}
|
|
848
|
+
: {}),
|
|
849
|
+
// The deployment's own sender, when this one bound both halves of it.
|
|
850
|
+
// Nothing is constructed without them, so a Worker with no email
|
|
851
|
+
// binding carries no sender rather than one that fails on use.
|
|
852
|
+
...(emailSender === undefined ? {} : { EMAIL_SENDER: emailSender }),
|
|
853
|
+
MEMORY_CHUNK_INDEX: {
|
|
854
|
+
record: async (vectorIds) => {
|
|
855
|
+
const entries = memoryChunkIndexEntriesV1(vectorIds);
|
|
856
|
+
if (Object.keys(entries).length > 0) {
|
|
857
|
+
await this.ctx.storage.put(entries);
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
...(isFrockAiGatewayBindingV1(env.AI)
|
|
862
|
+
? {
|
|
863
|
+
FROCK_AI: createFrockAiGatewayHostV1(env.AI, {
|
|
864
|
+
gatewayId: frockAiWorkerVarV1(env, "FROCK_AI_GATEWAY_ID"),
|
|
865
|
+
autoRoute: frockAiWorkerVarV1(env, "FROCK_AI_AUTO_ROUTE"),
|
|
866
|
+
accountId: frockAiWorkerVarV1(env, "FROCK_AI_ACCOUNT_ID"),
|
|
867
|
+
token: frockAiWorkerVarV1(env, "FROCK_AI_GATEWAY_TOKEN"),
|
|
868
|
+
...(hostedRates ? { billingLimits: hostedRates.limits } : {}),
|
|
869
|
+
}),
|
|
870
|
+
}
|
|
871
|
+
: {}),
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
private contributions(): Promise<{
|
|
876
|
+
shell: ShellBotBackendContribution;
|
|
877
|
+
flock: FlockBotBackendContribution;
|
|
878
|
+
computer: ComputerBotBackendContribution;
|
|
879
|
+
dispose(): Promise<void>;
|
|
880
|
+
}> {
|
|
881
|
+
if (!this.mounted) {
|
|
882
|
+
const pending = (async () => {
|
|
883
|
+
const computers = new ComputerRegistry();
|
|
884
|
+
// The Contribution's own registry, over the same host the Turn's
|
|
885
|
+
// runtime mounts. `./computer-host.ts` is where this deployment
|
|
886
|
+
// chooses which host that is.
|
|
887
|
+
const binding = computerHostBindingV1(this.backendEnv);
|
|
888
|
+
const computerConfigured = Boolean(binding);
|
|
889
|
+
const disposeComputers = await mountRuntimeFeaturesV1({ computers }, [
|
|
890
|
+
({ computers: registry }) =>
|
|
891
|
+
binding
|
|
892
|
+
? registry.register(createComputerHostV1(binding))
|
|
893
|
+
: undefined,
|
|
894
|
+
]);
|
|
895
|
+
// Where each descriptor's mounted value lands as the mount runs. The
|
|
896
|
+
// Shell and Flock Contributions need each other, and each reaches the
|
|
897
|
+
// other by naming the table entry it imported.
|
|
898
|
+
const mountedContributions = createFoundationMountedContributionsV1();
|
|
899
|
+
const requireShell = (): ShellBotBackendContribution => {
|
|
900
|
+
const shell = mountedContributions.get(shellBotContribution);
|
|
901
|
+
if (!shell) throw new Error("Shell Bot Contribution is unavailable");
|
|
902
|
+
return shell;
|
|
903
|
+
};
|
|
904
|
+
const mounted = await createFoundationBackendContributions<
|
|
905
|
+
| ShellBotBackendContribution
|
|
906
|
+
| FlockBotBackendContribution
|
|
907
|
+
| ComputerBotBackendContribution
|
|
908
|
+
>({
|
|
909
|
+
backendHost: "bot",
|
|
910
|
+
mountedContributions,
|
|
911
|
+
shell: {
|
|
912
|
+
...foundationShellApplicationV1(BRAND_V1),
|
|
913
|
+
state: this.ctx,
|
|
914
|
+
env: this.backendEnv,
|
|
915
|
+
outboundFetch: this.outboundFetch,
|
|
916
|
+
...(this.turnSupervisor
|
|
917
|
+
? { turnSupervisor: this.turnSupervisor }
|
|
918
|
+
: {}),
|
|
919
|
+
messagesCommitted: () => {
|
|
920
|
+
this.ctx.waitUntil(this.drainPush());
|
|
921
|
+
this.ctx.waitUntil(
|
|
922
|
+
(async () => {
|
|
923
|
+
const shell = this.mounted
|
|
924
|
+
? (await this.mounted).shell
|
|
925
|
+
: undefined;
|
|
926
|
+
this.nudgeGroup(
|
|
927
|
+
await shell?.state.authority.readActiveRunId(),
|
|
928
|
+
);
|
|
929
|
+
})().catch(() => undefined),
|
|
930
|
+
);
|
|
931
|
+
},
|
|
932
|
+
deliverPublication: (updates) => {
|
|
933
|
+
this.stateChannel.broadcastCommitted(updates);
|
|
934
|
+
return Promise.resolve();
|
|
935
|
+
},
|
|
936
|
+
deliverReplyDraft: (draft) => {
|
|
937
|
+
this.stateChannel.broadcastDraft(draft);
|
|
938
|
+
},
|
|
939
|
+
runSettled: (runId) => {
|
|
940
|
+
this.nudgeGroup(runId);
|
|
941
|
+
return this.projectSettled(requireShell(), runId);
|
|
942
|
+
},
|
|
943
|
+
// The Durable Object owns the kernel authority; the Shell
|
|
944
|
+
// Package supplies only its configuration and Composition
|
|
945
|
+
// hooks. Chat delivery is a commit contribution, not a
|
|
946
|
+
// storage interceptor: an observed write is not proof it
|
|
947
|
+
// committed.
|
|
948
|
+
createAuthority: (options) => new BotDurableAuthority(options),
|
|
949
|
+
// The Computer Contribution's projection cache and its share of
|
|
950
|
+
// the authority's one durable alarm, reached through the table
|
|
951
|
+
// once it has mounted.
|
|
952
|
+
// The same host, for the Turn's runtime. The app is handed a
|
|
953
|
+
// factory because two of its seams belong to one Turn.
|
|
954
|
+
...(binding
|
|
955
|
+
? {
|
|
956
|
+
computerHost: (options: ShellComputerHostOptionsV1) =>
|
|
957
|
+
createComputerHostV1(binding, options),
|
|
958
|
+
}
|
|
959
|
+
: {}),
|
|
960
|
+
invalidateComputerProjectionFile: (userId, botId, kind) => {
|
|
961
|
+
mountedContributions
|
|
962
|
+
.get(computerBotContribution)
|
|
963
|
+
?.invalidateProjectionFile(userId, botId, kind);
|
|
964
|
+
// Dropping the resident cache only makes the next read
|
|
965
|
+
// honest. The notice is what makes an attached client take
|
|
966
|
+
// that read, so a new frame reaches the card in about a
|
|
967
|
+
// second instead of at the next projection poll.
|
|
968
|
+
this.stateChannel.noticeComputer();
|
|
969
|
+
},
|
|
970
|
+
deleteComputerDemonstration: (demonstrationId) =>
|
|
971
|
+
mountedContributions
|
|
972
|
+
.get(computerBotContribution)
|
|
973
|
+
?.deleteDemonstration(demonstrationId) ??
|
|
974
|
+
Promise.resolve("missing" as const),
|
|
975
|
+
scheduledDeadlines: async (transaction) => [
|
|
976
|
+
...((await mountedContributions
|
|
977
|
+
.get(computerBotContribution)
|
|
978
|
+
?.scheduledDeadlines(transaction)) ?? []),
|
|
979
|
+
...((
|
|
980
|
+
await transaction.list({ prefix: PUSH_OUTBOX_PREFIX, limit: 1 })
|
|
981
|
+
).size || (await transaction.get(PUSH_READ_KEY))
|
|
982
|
+
? [Date.now() + 30_000]
|
|
983
|
+
: []),
|
|
984
|
+
...(await themeAssembleDeadlineV1(transaction)),
|
|
985
|
+
...(durableObjectHasSqlV1(this.ctx.storage)
|
|
986
|
+
? (() => {
|
|
987
|
+
const due = this.memoryEngine().nextWakeupAt();
|
|
988
|
+
return due === undefined ? [] : [due];
|
|
989
|
+
})()
|
|
990
|
+
: []),
|
|
991
|
+
],
|
|
992
|
+
scheduledWorkInFlight: () =>
|
|
993
|
+
mountedContributions
|
|
994
|
+
.get(computerBotContribution)
|
|
995
|
+
?.scheduledWorkInFlight() ?? false,
|
|
996
|
+
deferScheduledWork: async (transaction) => {
|
|
997
|
+
await (mountedContributions
|
|
998
|
+
.get(computerBotContribution)
|
|
999
|
+
?.deferScheduledWork(transaction) ?? Promise.resolve());
|
|
1000
|
+
await deferThemeAssembleV1(transaction, Date.now());
|
|
1001
|
+
},
|
|
1002
|
+
settleScheduledWork: async () => {
|
|
1003
|
+
await (mountedContributions
|
|
1004
|
+
.get(computerBotContribution)
|
|
1005
|
+
?.settleScheduledWork() ?? Promise.resolve());
|
|
1006
|
+
await this.assembleThemeIfDue();
|
|
1007
|
+
await this.deliverProfileMirror();
|
|
1008
|
+
await this.drainMemoryProcessing();
|
|
1009
|
+
},
|
|
1010
|
+
// An archived Bot admits no configuration command; the Flock
|
|
1011
|
+
// Contribution owns that durable lifecycle state.
|
|
1012
|
+
assertLifecycleActive: (storage, botId) => {
|
|
1013
|
+
const flock = mountedContributions.get(flockBotContribution);
|
|
1014
|
+
if (!flock) {
|
|
1015
|
+
throw new Error("Flock Bot Contribution is unavailable");
|
|
1016
|
+
}
|
|
1017
|
+
return flock.assertActive(storage, botId);
|
|
1018
|
+
},
|
|
1019
|
+
},
|
|
1020
|
+
flock: {
|
|
1021
|
+
storage: this.ctx.storage,
|
|
1022
|
+
materializeSettings: async (registration, userId) => {
|
|
1023
|
+
await requireShell().materializeSettings(
|
|
1024
|
+
{ userId, botId: registration.botId },
|
|
1025
|
+
{
|
|
1026
|
+
name: registration.initialName,
|
|
1027
|
+
...(registration.initialDescription === undefined
|
|
1028
|
+
? {}
|
|
1029
|
+
: { description: registration.initialDescription }),
|
|
1030
|
+
},
|
|
1031
|
+
);
|
|
1032
|
+
},
|
|
1033
|
+
archiveEligible: (storage) => archiveEligible(storage),
|
|
1034
|
+
tearDown: (identity) => this.tearDown(identity),
|
|
1035
|
+
},
|
|
1036
|
+
computer: {
|
|
1037
|
+
storage: this.stateChannel.computerStorage,
|
|
1038
|
+
workspace: this.backendEnv.WORKSPACE_FILES,
|
|
1039
|
+
providerLabel: "Computer",
|
|
1040
|
+
configured: computerConfigured,
|
|
1041
|
+
// A stopped recording becomes the Bot's uploads, kept and
|
|
1042
|
+
// counted exactly as a file the person attaches.
|
|
1043
|
+
demonstrations: {
|
|
1044
|
+
keep: ({ userId, botId, files }) =>
|
|
1045
|
+
keepBotFilesV1(
|
|
1046
|
+
botFileStoreV1(this.env, { userId, botId }, this.ctx.storage),
|
|
1047
|
+
{ userId, botId },
|
|
1048
|
+
files,
|
|
1049
|
+
),
|
|
1050
|
+
remove: ({ userId, botId, uploadIds }) =>
|
|
1051
|
+
removeBotFilesV1(
|
|
1052
|
+
botFileStoreV1(this.env, { userId, botId }, this.ctx.storage),
|
|
1053
|
+
{ userId, botId },
|
|
1054
|
+
uploadIds,
|
|
1055
|
+
),
|
|
1056
|
+
},
|
|
1057
|
+
openComputer: (userId, botId, effectId) => {
|
|
1058
|
+
const identity = { userId };
|
|
1059
|
+
if (!computers.assignment(identity)) {
|
|
1060
|
+
computers.assign(identity, "computer-host");
|
|
1061
|
+
}
|
|
1062
|
+
return computers.open(identity, { botId }, { effectId });
|
|
1063
|
+
},
|
|
1064
|
+
loginVault: (userId) =>
|
|
1065
|
+
createComputerLoginVaultV1({
|
|
1066
|
+
userId,
|
|
1067
|
+
keyring: this.backendEnv.CREDENTIAL_KEYRING,
|
|
1068
|
+
user: this.backendEnv.USER_CONFIGURATIONS.get(
|
|
1069
|
+
this.backendEnv.USER_CONFIGURATIONS.idFromName(userId),
|
|
1070
|
+
),
|
|
1071
|
+
}),
|
|
1072
|
+
},
|
|
1073
|
+
});
|
|
1074
|
+
// Every Bot-host Contribution the application lists must have
|
|
1075
|
+
// mounted, and each of the three the Bot Durable Object depends on
|
|
1076
|
+
// must be one of them.
|
|
1077
|
+
const shell = mounted.get(shellBotContribution);
|
|
1078
|
+
const flock = mounted.get(flockBotContribution);
|
|
1079
|
+
const computer = mounted.get(computerBotContribution);
|
|
1080
|
+
if (
|
|
1081
|
+
!shell ||
|
|
1082
|
+
!flock ||
|
|
1083
|
+
!computer ||
|
|
1084
|
+
mounted.contributions.length !==
|
|
1085
|
+
backendDescriptorsV1.filter(
|
|
1086
|
+
(descriptor) => descriptor.host === "bot",
|
|
1087
|
+
).length
|
|
1088
|
+
) {
|
|
1089
|
+
await mounted.dispose();
|
|
1090
|
+
await disposeComputers();
|
|
1091
|
+
throw new Error(
|
|
1092
|
+
"Foundation requires Shell, Flock and Computer Bot backend Contributions",
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
const alarmOwner = shell;
|
|
1096
|
+
this.stateChannel.setAlarmRefresher((transaction) =>
|
|
1097
|
+
refreshScheduledWork(alarmOwner.state, transaction),
|
|
1098
|
+
);
|
|
1099
|
+
return {
|
|
1100
|
+
shell,
|
|
1101
|
+
flock,
|
|
1102
|
+
computer,
|
|
1103
|
+
async dispose() {
|
|
1104
|
+
await mounted.dispose();
|
|
1105
|
+
await disposeComputers();
|
|
1106
|
+
},
|
|
1107
|
+
};
|
|
1108
|
+
})();
|
|
1109
|
+
this.mounted = pending;
|
|
1110
|
+
// Read by Computer billing, which must never wait on a mount: the
|
|
1111
|
+
// mount itself may be what is calling the Computer.
|
|
1112
|
+
void pending.then(
|
|
1113
|
+
(mounted) => {
|
|
1114
|
+
if (this.mounted === pending) this.mountedShell = mounted.shell.state;
|
|
1115
|
+
},
|
|
1116
|
+
() => undefined,
|
|
1117
|
+
);
|
|
1118
|
+
// A mount that failed is not a durable verdict. Memoizing the rejection
|
|
1119
|
+
// made one transient failure — an artifact read, a User RPC, a member
|
|
1120
|
+
// that would not resolve — final for the life of the object: every
|
|
1121
|
+
// later call awaited the same rejected promise, the recovery alarm
|
|
1122
|
+
// included, so nothing could heal it short of eviction. The next call
|
|
1123
|
+
// retries instead, exactly as `immutable-application.ts` already does.
|
|
1124
|
+
void pending.catch(() => {
|
|
1125
|
+
if (this.mounted === pending) {
|
|
1126
|
+
this.mounted = undefined;
|
|
1127
|
+
this.mountedShell = undefined;
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
return this.mounted;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
private async registration(identity: {
|
|
1135
|
+
userId: string;
|
|
1136
|
+
botId: string;
|
|
1137
|
+
}): Promise<BotRegistrationV1> {
|
|
1138
|
+
const id = this.env.USER_CONFIGURATIONS.idFromName(identity.userId);
|
|
1139
|
+
// SAFETY: USER_CONFIGURATIONS binds UserConfiguration; workers-types cannot infer its generated Flock RPC surface.
|
|
1140
|
+
const rpc = this.env.USER_CONFIGURATIONS.get(id) as unknown as {
|
|
1141
|
+
getBotRegistration(input: unknown): Promise<BotRegistrationV1>;
|
|
1142
|
+
};
|
|
1143
|
+
return decodeBotRegistrationV1(
|
|
1144
|
+
structuredClone(
|
|
1145
|
+
await rpc.getBotRegistration({ schemaVersion: 1, ...identity }),
|
|
1146
|
+
),
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/** The User Durable Object, as this object's Memory authority. */
|
|
1151
|
+
private userMemoryRpc(userId: string): UserMemoryRpc {
|
|
1152
|
+
const id = this.env.USER_CONFIGURATIONS.idFromName(userId);
|
|
1153
|
+
// SAFETY: USER_CONFIGURATIONS binds UserConfiguration; workers-types cannot
|
|
1154
|
+
// infer its generated Memory RPC surface.
|
|
1155
|
+
return this.env.USER_CONFIGURATIONS.get(id) as unknown as UserMemoryRpc;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
#memoryEngine: MemoryEngineV1 | undefined;
|
|
1159
|
+
|
|
1160
|
+
private memoryEngine(): MemoryEngineV1 {
|
|
1161
|
+
this.#memoryEngine ??= createBotMemoryEngineV1(this.ctx.storage);
|
|
1162
|
+
return this.#memoryEngine;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
private async drainMemoryProcessing(): Promise<void> {
|
|
1166
|
+
if (!durableObjectHasSqlV1(this.ctx.storage)) return;
|
|
1167
|
+
const extract = createHostedMemoryExtractorV1({
|
|
1168
|
+
displayName: BRAND_V1.builtInModelName,
|
|
1169
|
+
...(this.backendEnv.FROCK_AI
|
|
1170
|
+
? { gateway: this.backendEnv.FROCK_AI }
|
|
1171
|
+
: {}),
|
|
1172
|
+
...(this.backendEnv.BILLING ? { billing: this.backendEnv.BILLING } : {}),
|
|
1173
|
+
});
|
|
1174
|
+
await drainDurableMemoryV1(this.memoryEngine(), {
|
|
1175
|
+
...(this.env.MEMORY_INDEX
|
|
1176
|
+
? { vectors: this.env.MEMORY_INDEX as MemoryVectorIndex }
|
|
1177
|
+
: {}),
|
|
1178
|
+
...(this.env.AI ? { ai: this.env.AI as MemoryAiBinding } : {}),
|
|
1179
|
+
...(extract ? { extract } : {}),
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Builds the Workspace and Memory file surfaces for one identity.
|
|
1185
|
+
*
|
|
1186
|
+
* Three surfaces, deliberately, because the store refuses to be two things
|
|
1187
|
+
* at once: `WORKSPACE_FILES` is the kernel surface and refuses every Memory
|
|
1188
|
+
* root; `MEMORY_WORKSPACE_FILES` is the Memory Package's and serves Memory
|
|
1189
|
+
* roots and nothing else; `WORKSPACE_SYNC_FILES` is the durable-root sync's,
|
|
1190
|
+
* the only surface that reads every root and the only one that accepts an
|
|
1191
|
+
* `unattributed` writer — a shell wrote the file and nothing recorded who.
|
|
1192
|
+
* The Memory surface routes a shared root's generations to the User Durable
|
|
1193
|
+
* Object, which is the authority for the generation records of the User
|
|
1194
|
+
* Memory root, while the Bot's own Memory root stays in this object.
|
|
1195
|
+
*
|
|
1196
|
+
* The sync's effect records stay here too: a push records its intent in the
|
|
1197
|
+
* Bot's Durable Object before it runs (§ Computer and Workspace), so an
|
|
1198
|
+
* interrupted push is read back rather than repeated.
|
|
1199
|
+
*/
|
|
1200
|
+
protected async skillIndexLoad(identity: { userId: string; botId: string }) {
|
|
1201
|
+
const storage = this.ctx.storage;
|
|
1202
|
+
const bot = await readDurableSkillIndexV1(storage, {
|
|
1203
|
+
kind: "bot-instructions",
|
|
1204
|
+
userId: identity.userId,
|
|
1205
|
+
botId: identity.botId,
|
|
1206
|
+
});
|
|
1207
|
+
const userStub = this.env.USER_CONFIGURATIONS.get(
|
|
1208
|
+
this.env.USER_CONFIGURATIONS.idFromName(identity.userId),
|
|
1209
|
+
);
|
|
1210
|
+
const user = decodeSkillMetadataIndexV1(
|
|
1211
|
+
await userStub.readSkillIndex({
|
|
1212
|
+
schemaVersion: 1,
|
|
1213
|
+
userId: identity.userId,
|
|
1214
|
+
root: { kind: "user-instructions", userId: identity.userId },
|
|
1215
|
+
}),
|
|
1216
|
+
);
|
|
1217
|
+
return { bot, user, liveBot: bot, liveUser: user };
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
protected instructionPublication(
|
|
1221
|
+
userId: string,
|
|
1222
|
+
): (event: WorkspaceGenerationPublicationV1) => Promise<void> {
|
|
1223
|
+
const storage = this.ctx.storage;
|
|
1224
|
+
const bucket = this.env.MEMORY_FILES;
|
|
1225
|
+
const objects = createR2ObjectBucketV1(bucket);
|
|
1226
|
+
const user = this.env.USER_CONFIGURATIONS.get(
|
|
1227
|
+
this.env.USER_CONFIGURATIONS.idFromName(userId),
|
|
1228
|
+
);
|
|
1229
|
+
return (event) =>
|
|
1230
|
+
publishWorkspaceSkillGenerationV1({
|
|
1231
|
+
event,
|
|
1232
|
+
userId,
|
|
1233
|
+
storage,
|
|
1234
|
+
bodies: {
|
|
1235
|
+
put: async (key, bytes) => {
|
|
1236
|
+
await objects.put(key, bytes);
|
|
1237
|
+
},
|
|
1238
|
+
get: async (key) => {
|
|
1239
|
+
const object = await objects.get(key);
|
|
1240
|
+
return object ? object.bytes() : undefined;
|
|
1241
|
+
},
|
|
1242
|
+
delete: (key) => objects.delete(key),
|
|
1243
|
+
},
|
|
1244
|
+
user,
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
protected bindSurfaces(identity: { userId: string; botId: string }): void {
|
|
1249
|
+
const key = `${identity.userId}\u0000${identity.botId}`;
|
|
1250
|
+
if (this.surfacesFor === key) return;
|
|
1251
|
+
const owner = { userId: identity.userId };
|
|
1252
|
+
// One ledger per Durable Object, shared by every surface it builds. A
|
|
1253
|
+
// ledger caches its minting cursor while resident, so two instances on one
|
|
1254
|
+
// object can read one cursor and mint one generation id twice — two files
|
|
1255
|
+
// claiming one generation, which is the single thing the id exists to
|
|
1256
|
+
// prevent. The routed ledger is one instance too: the same Bot half serves
|
|
1257
|
+
// the Memory and sync surfaces, and only a shared Memory root is routed to
|
|
1258
|
+
// the User object.
|
|
1259
|
+
const bot = this.workspaceGenerations;
|
|
1260
|
+
const onInstructionPublication = this.instructionPublication(
|
|
1261
|
+
identity.userId,
|
|
1262
|
+
);
|
|
1263
|
+
const workspace = createDurableWorkspaceFilesV1(this.env, {
|
|
1264
|
+
owner,
|
|
1265
|
+
generations: bot,
|
|
1266
|
+
onInstructionPublication,
|
|
1267
|
+
});
|
|
1268
|
+
const rpc = this.userMemoryRpc(identity.userId);
|
|
1269
|
+
const routed = createRoutedWorkspaceGenerationsV1({
|
|
1270
|
+
bot,
|
|
1271
|
+
user: createUserWorkspaceGenerationsV1(rpc, identity.userId),
|
|
1272
|
+
});
|
|
1273
|
+
const memory = createDurableWorkspaceFilesV1(this.env, {
|
|
1274
|
+
owner,
|
|
1275
|
+
surface: "memory",
|
|
1276
|
+
generations: routed,
|
|
1277
|
+
});
|
|
1278
|
+
const sync = createDurableWorkspaceFilesV1(this.env, {
|
|
1279
|
+
owner,
|
|
1280
|
+
surface: "sync",
|
|
1281
|
+
generations: routed,
|
|
1282
|
+
onInstructionPublication,
|
|
1283
|
+
});
|
|
1284
|
+
if (workspace) this.backendEnv.WORKSPACE_FILES = workspace;
|
|
1285
|
+
if (sync) {
|
|
1286
|
+
this.backendEnv.WORKSPACE_SYNC_FILES = sync;
|
|
1287
|
+
this.backendEnv.WORKSPACE_SYNC_EFFECTS = new DurableWorkspaceSyncEffects({
|
|
1288
|
+
state: this.ctx,
|
|
1289
|
+
});
|
|
1290
|
+
this.backendEnv.WORKSPACE_SYNC_GENERATIONS = routed;
|
|
1291
|
+
}
|
|
1292
|
+
if (memory) {
|
|
1293
|
+
this.backendEnv.MEMORY_WORKSPACE_FILES = memory;
|
|
1294
|
+
this.backendEnv.MEMORY_GROUPS = createUserMemoryGroupsV1(rpc, identity);
|
|
1295
|
+
}
|
|
1296
|
+
if (durableObjectHasSqlV1(this.ctx.storage)) {
|
|
1297
|
+
const vectors = this.env.MEMORY_INDEX as MemoryVectorIndex | undefined;
|
|
1298
|
+
const ai = this.env.AI as MemoryAiBinding | undefined;
|
|
1299
|
+
this.backendEnv.MEMORY_RECORDS = new MemoryRecordsV1({
|
|
1300
|
+
owner: "bot",
|
|
1301
|
+
engine: this.memoryEngine(),
|
|
1302
|
+
remote: createUserMemoryRecordsRemoteV1(rpc, identity),
|
|
1303
|
+
...(vectors && ai
|
|
1304
|
+
? {
|
|
1305
|
+
semantic: createVectorMemorySearchV1(
|
|
1306
|
+
vectors,
|
|
1307
|
+
createMemoryEmbedder(ai),
|
|
1308
|
+
),
|
|
1309
|
+
}
|
|
1310
|
+
: {}),
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
// The transcript index is User-scoped state, so its authority is the User
|
|
1314
|
+
// Durable Object and this object reaches it through a narrow binding —
|
|
1315
|
+
// the same shape as `MEMORY_GROUPS` above. It is a projection, never an
|
|
1316
|
+
// authority, so nothing here waits on it and nothing here reads from it.
|
|
1317
|
+
this.backendEnv.SEARCH_SINK = createUserSearchSinkV1(
|
|
1318
|
+
rpc as unknown as UserSearchRpc,
|
|
1319
|
+
identity,
|
|
1320
|
+
);
|
|
1321
|
+
// The audit table is User-scoped too, and reached the same way — but
|
|
1322
|
+
// through a durable outbox rather than fire-and-forget, because
|
|
1323
|
+
// completeness is the parity item (register row 30b).
|
|
1324
|
+
this.backendEnv.AUDIT_SINK = createUserAuditSinkV1(
|
|
1325
|
+
rpc as unknown as UserAuditRpc,
|
|
1326
|
+
identity,
|
|
1327
|
+
);
|
|
1328
|
+
this.surfacesFor = key;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Everything this Bot owns, destroyed. The Flock Contribution calls this on
|
|
1333
|
+
* `bot/delete` and writes its tombstone afterwards.
|
|
1334
|
+
*
|
|
1335
|
+
* Order matters. The object's existing alarm is cancelled first, then its
|
|
1336
|
+
* Bot-Memory vector ids are deleted from Vectorize in bounded alarm pages.
|
|
1337
|
+
* Only once the durable chunk index is empty do the durable keys go in one
|
|
1338
|
+
* `deleteAll()`:
|
|
1339
|
+
* the session event log and its runs, the transcript and conversations, the
|
|
1340
|
+
* Bot's Memory and Skills generation ledger, Routines and their schedules,
|
|
1341
|
+
* Subagent tasks, approvals, notifications, unread and sidebar preview, the
|
|
1342
|
+
* Package composition generations, and the state-channel log. Then the two
|
|
1343
|
+
* object-store roots the Bot owns, which are the only Bot-scoped state that
|
|
1344
|
+
* does not live in this object.
|
|
1345
|
+
*
|
|
1346
|
+
* If Vectorize is not bound (local development and workerd by default), the
|
|
1347
|
+
* journal records that the derived cleanup was skipped before teardown.
|
|
1348
|
+
*
|
|
1349
|
+
* The mount memo is dropped last so the next call to this object rebuilds
|
|
1350
|
+
* from empty storage, reads the tombstone the caller is about to write, and
|
|
1351
|
+
* refuses rather than materializing the Bot again. Dropping it also
|
|
1352
|
+
* guarantees no surviving Contribution re-arms the alarm from a stale
|
|
1353
|
+
* transaction.
|
|
1354
|
+
*
|
|
1355
|
+
* Idempotent, because the delete saga replays: an empty object has nothing
|
|
1356
|
+
* to delete and an empty prefix has nothing to list.
|
|
1357
|
+
*/
|
|
1358
|
+
private async finishTearDown(identity: {
|
|
1359
|
+
userId: string;
|
|
1360
|
+
botId: string;
|
|
1361
|
+
}): Promise<void> {
|
|
1362
|
+
await this.ctx.storage.deleteAlarm();
|
|
1363
|
+
await this.ctx.storage.deleteAll();
|
|
1364
|
+
await deleteBotWorkspaceRootsV1(this.env, identity);
|
|
1365
|
+
// The Bot's uploads go with it, and the account gets their space back.
|
|
1366
|
+
await deleteBotUploadsV1(this.env, identity);
|
|
1367
|
+
await releaseBotUploadQuotaRpcV1(this.env, identity);
|
|
1368
|
+
this.mounted = undefined;
|
|
1369
|
+
this.mountedShell = undefined;
|
|
1370
|
+
this.surfacesFor = undefined;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* Delete one bounded page of this Bot's own Memory vectors.
|
|
1375
|
+
*
|
|
1376
|
+
* The index keys are the cursor. They leave storage only after the external
|
|
1377
|
+
* delete succeeds, so an eviction in either side of that boundary repeats a
|
|
1378
|
+
* harmless delete or resumes at the first id not yet acknowledged. The next
|
|
1379
|
+
* alarm is armed before the remote call: even a long Vectorize outage cannot
|
|
1380
|
+
* exhaust the platform's finite automatic alarm retries and strand data.
|
|
1381
|
+
*/
|
|
1382
|
+
private async purgeMemoryVectorBatch(
|
|
1383
|
+
journal: MemoryVectorPurgeJournalV1,
|
|
1384
|
+
): Promise<"complete" | "pending"> {
|
|
1385
|
+
const vectorize = this.env.MEMORY_INDEX;
|
|
1386
|
+
if (!vectorize) {
|
|
1387
|
+
await this.ctx.storage.put(MEMORY_VECTOR_PURGE_JOURNAL_KEY_V1, {
|
|
1388
|
+
...journal,
|
|
1389
|
+
status: "skipped",
|
|
1390
|
+
note: "MEMORY_INDEX binding is absent; no Vectorize purge was available",
|
|
1391
|
+
} satisfies MemoryVectorPurgeJournalV1);
|
|
1392
|
+
return "complete";
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
const page = await this.ctx.storage.list<unknown>({
|
|
1396
|
+
prefix: MEMORY_CHUNK_INDEX_PREFIX_V1,
|
|
1397
|
+
limit: MEMORY_VECTOR_DELETE_BATCH_SIZE_V1,
|
|
1398
|
+
});
|
|
1399
|
+
if (page.size === 0) {
|
|
1400
|
+
await this.ctx.storage.put(MEMORY_VECTOR_PURGE_JOURNAL_KEY_V1, {
|
|
1401
|
+
...journal,
|
|
1402
|
+
status: "complete",
|
|
1403
|
+
} satisfies MemoryVectorPurgeJournalV1);
|
|
1404
|
+
return "complete";
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
const entries = [...page].map(([key, value]) =>
|
|
1408
|
+
decodeMemoryChunkIndexEntryV1(key, value),
|
|
1409
|
+
);
|
|
1410
|
+
await this.ctx.storage.setAlarm(
|
|
1411
|
+
Date.now() + MEMORY_VECTOR_PURGE_RETRY_DELAY_MS_V1,
|
|
1412
|
+
);
|
|
1413
|
+
await vectorize.deleteByIds(entries.map((entry) => entry.vectorId));
|
|
1414
|
+
const keys = [...page.keys()];
|
|
1415
|
+
const deleted = journal.deleted + entries.length;
|
|
1416
|
+
const hasAnotherPage = page.size === MEMORY_VECTOR_DELETE_BATCH_SIZE_V1;
|
|
1417
|
+
await this.ctx.storage.transaction(async (storage) => {
|
|
1418
|
+
await storage.delete(keys);
|
|
1419
|
+
await storage.put(MEMORY_VECTOR_PURGE_JOURNAL_KEY_V1, {
|
|
1420
|
+
...journal,
|
|
1421
|
+
status: hasAnotherPage ? "pending" : "complete",
|
|
1422
|
+
deleted,
|
|
1423
|
+
} satisfies MemoryVectorPurgeJournalV1);
|
|
1424
|
+
if (hasAnotherPage) {
|
|
1425
|
+
await storage.setAlarm(
|
|
1426
|
+
Date.now() + MEMORY_VECTOR_PURGE_RETRY_DELAY_MS_V1,
|
|
1427
|
+
);
|
|
1428
|
+
} else {
|
|
1429
|
+
await storage.deleteAlarm();
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
return hasAnotherPage ? "pending" : "complete";
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
private async tearDown(identity: {
|
|
1436
|
+
userId: string;
|
|
1437
|
+
botId: string;
|
|
1438
|
+
}): Promise<"complete" | "pending"> {
|
|
1439
|
+
// A throttled channel notice and every pre-delete scheduled deadline must
|
|
1440
|
+
// stop before the alarm is repurposed for the purge journal.
|
|
1441
|
+
this.stateChannel.silence();
|
|
1442
|
+
const stored = await this.ctx.storage.get<unknown>(
|
|
1443
|
+
MEMORY_VECTOR_PURGE_JOURNAL_KEY_V1,
|
|
1444
|
+
);
|
|
1445
|
+
let journal: MemoryVectorPurgeJournalV1;
|
|
1446
|
+
if (stored === undefined) {
|
|
1447
|
+
await this.ctx.storage.deleteAlarm();
|
|
1448
|
+
journal = {
|
|
1449
|
+
schemaVersion: 1,
|
|
1450
|
+
...identity,
|
|
1451
|
+
status: "pending",
|
|
1452
|
+
deleted: 0,
|
|
1453
|
+
};
|
|
1454
|
+
await this.ctx.storage.put(MEMORY_VECTOR_PURGE_JOURNAL_KEY_V1, journal);
|
|
1455
|
+
} else {
|
|
1456
|
+
journal = decodeMemoryVectorPurgeJournalV1(stored);
|
|
1457
|
+
if (
|
|
1458
|
+
journal.userId !== identity.userId ||
|
|
1459
|
+
journal.botId !== identity.botId
|
|
1460
|
+
) {
|
|
1461
|
+
throw new Error("Memory vector purge journal identity does not match");
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
const outcome =
|
|
1465
|
+
journal.status === "pending"
|
|
1466
|
+
? await this.purgeMemoryVectorBatch(journal)
|
|
1467
|
+
: "complete";
|
|
1468
|
+
if (outcome === "pending") return outcome;
|
|
1469
|
+
await this.finishTearDown(identity);
|
|
1470
|
+
return "complete";
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/** What a Computer charge made from this object is recorded against. */
|
|
1474
|
+
private async computerSpend(
|
|
1475
|
+
botId: string,
|
|
1476
|
+
personal: boolean,
|
|
1477
|
+
): Promise<UsageAttributionV1> {
|
|
1478
|
+
const desktop: UsageAttributionV1 = { cause: { kind: "desktop", botId } };
|
|
1479
|
+
const state = this.mountedShell;
|
|
1480
|
+
// No Turn runs in an object that has not mounted.
|
|
1481
|
+
if (personal || !state) return desktop;
|
|
1482
|
+
const runId = await state.authority.readActiveRunId();
|
|
1483
|
+
if (!runId) return desktop;
|
|
1484
|
+
const run = await state.authority
|
|
1485
|
+
.readRunHeader(runId)
|
|
1486
|
+
.catch(() => undefined);
|
|
1487
|
+
return {
|
|
1488
|
+
runId,
|
|
1489
|
+
cause: await runCauseV1(
|
|
1490
|
+
botId,
|
|
1491
|
+
run?.admission?.origin,
|
|
1492
|
+
runCauseReadersV1(state),
|
|
1493
|
+
),
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
private async materialized(identity: { userId: string; botId: string }) {
|
|
1498
|
+
const mounted = await this.unmaterialized(identity);
|
|
1499
|
+
await mounted.flock.materialize(mounted.registration, identity.userId);
|
|
1500
|
+
return mounted;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
/** Mounted and registered, without the Flock's materialization. */
|
|
1504
|
+
private async unmaterialized(identity: { userId: string; botId: string }) {
|
|
1505
|
+
this.bindSurfaces(identity);
|
|
1506
|
+
const contributions = await this.contributions();
|
|
1507
|
+
const registration = await this.registration(identity);
|
|
1508
|
+
return { ...contributions, registration };
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
private async contribution(): Promise<ShellBotBackendContribution> {
|
|
1512
|
+
return (await this.contributions()).shell;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
async readConfiguration(input: unknown) {
|
|
1516
|
+
const request = decodeBotConfigurationReadRpcV1(input);
|
|
1517
|
+
const { shell } = await this.materialized({
|
|
1518
|
+
userId: request.userId,
|
|
1519
|
+
botId: request.botId,
|
|
1520
|
+
});
|
|
1521
|
+
return readConfiguration(shell.state, request);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
async executeConfiguration(input: unknown) {
|
|
1525
|
+
const request = decodeBotConfigurationExecuteRpcV1(input);
|
|
1526
|
+
const { shell } = await this.materialized({
|
|
1527
|
+
userId: request.userId,
|
|
1528
|
+
botId: request.botId,
|
|
1529
|
+
});
|
|
1530
|
+
const receipt = await executeConfiguration(shell.state, request);
|
|
1531
|
+
if (
|
|
1532
|
+
receipt.status === "applied" &&
|
|
1533
|
+
(request.command.type === "bot/update-profile" ||
|
|
1534
|
+
request.command.type === "bot/set-profile")
|
|
1535
|
+
) {
|
|
1536
|
+
this.ctx.waitUntil(this.deliverProfileMirror().catch(() => undefined));
|
|
1537
|
+
}
|
|
1538
|
+
if (request.command.type === "bot/set-package-settings") {
|
|
1539
|
+
this.ctx.waitUntil(
|
|
1540
|
+
this.assembleTheme({
|
|
1541
|
+
schemaVersion: 1,
|
|
1542
|
+
userId: request.userId,
|
|
1543
|
+
botId: request.botId,
|
|
1544
|
+
}).catch(() => undefined),
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
return receipt;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/** Which of the User's installed Plugins this Bot runs (ADR 0026). */
|
|
1551
|
+
async readPluginEnablement(input: unknown) {
|
|
1552
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
1553
|
+
userId: rpcIdentifier,
|
|
1554
|
+
botId: rpcBotId,
|
|
1555
|
+
});
|
|
1556
|
+
const identity = {
|
|
1557
|
+
userId: request.userId as string,
|
|
1558
|
+
botId: request.botId as string,
|
|
1559
|
+
};
|
|
1560
|
+
const { shell } = await this.materialized(identity);
|
|
1561
|
+
await shell.validateIdentity(identity);
|
|
1562
|
+
return readPluginEnablementV1(shell.state.ctx.storage);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/**
|
|
1566
|
+
* Switches one Plugin for this Bot, fenced on the revision the caller read.
|
|
1567
|
+
* A stale revision is a receipt, not a thrown error: the caller re-reads and
|
|
1568
|
+
* decides again, the way every other revision-fenced command answers.
|
|
1569
|
+
*/
|
|
1570
|
+
async setPluginEnabled(input: unknown) {
|
|
1571
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
1572
|
+
userId: rpcIdentifier,
|
|
1573
|
+
botId: rpcBotId,
|
|
1574
|
+
pluginId: rpcPattern(PLUGIN_ID_V1, PLUGIN_ID_MAX_LENGTH_V1),
|
|
1575
|
+
enabled: rpcBoolean,
|
|
1576
|
+
expectedRevision: rpcInteger({ minimum: 0, maximum: 1_000_000 }),
|
|
1577
|
+
});
|
|
1578
|
+
const identity = {
|
|
1579
|
+
userId: request.userId as string,
|
|
1580
|
+
botId: request.botId as string,
|
|
1581
|
+
};
|
|
1582
|
+
const { shell } = await this.materialized(identity);
|
|
1583
|
+
await shell.validateIdentity(identity);
|
|
1584
|
+
try {
|
|
1585
|
+
const enablement = await setPluginEnabledV1(shell.state.ctx.storage, {
|
|
1586
|
+
pluginId: request.pluginId as string,
|
|
1587
|
+
enabled: request.enabled as boolean,
|
|
1588
|
+
expectedRevision: request.expectedRevision as number,
|
|
1589
|
+
});
|
|
1590
|
+
return { status: "applied" as const, enablement };
|
|
1591
|
+
} catch (error) {
|
|
1592
|
+
if (error instanceof PluginEnablementConflictError) {
|
|
1593
|
+
return {
|
|
1594
|
+
status: "conflict" as const,
|
|
1595
|
+
currentRevision: error.currentRevision,
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
throw error;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
/** This Bot's Plugins page: one row per Plugin it could run, and whether it does. */
|
|
1603
|
+
async readBotPluginsFrame(input: unknown) {
|
|
1604
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
1605
|
+
userId: rpcIdentifier,
|
|
1606
|
+
botId: rpcBotId,
|
|
1607
|
+
});
|
|
1608
|
+
const identity = {
|
|
1609
|
+
userId: request.userId as string,
|
|
1610
|
+
botId: request.botId as string,
|
|
1611
|
+
};
|
|
1612
|
+
const { shell } = await this.materialized(identity);
|
|
1613
|
+
await shell.validateIdentity(identity);
|
|
1614
|
+
// The page's read draws every section; a switch re-reads without them.
|
|
1615
|
+
return readBotPluginsFrameV1(shell.state, identity, undefined, {
|
|
1616
|
+
sections: true,
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
/**
|
|
1621
|
+
* Runs the tool a section's control names, outside any Turn. A Plugin
|
|
1622
|
+
* that is off, has no section or no such tool is a rejection with the
|
|
1623
|
+
* reason, never a thrown error.
|
|
1624
|
+
*/
|
|
1625
|
+
async executeBotPluginTool(input: unknown) {
|
|
1626
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
1627
|
+
userId: rpcIdentifier,
|
|
1628
|
+
botId: rpcBotId,
|
|
1629
|
+
command: rpcDecoded(decodePluginToolCommandV1),
|
|
1630
|
+
});
|
|
1631
|
+
const identity = {
|
|
1632
|
+
userId: request.userId as string,
|
|
1633
|
+
botId: request.botId as string,
|
|
1634
|
+
};
|
|
1635
|
+
const { shell } = await this.materialized(identity);
|
|
1636
|
+
await shell.validateIdentity(identity);
|
|
1637
|
+
return executeBotPluginToolV1(
|
|
1638
|
+
shell.state,
|
|
1639
|
+
identity,
|
|
1640
|
+
request.command as ReturnType<typeof decodePluginToolCommandV1>,
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/** The canvas's one read: this Bot's panel bag, doors, and focused page. */
|
|
1645
|
+
async openFocusedPanel(input: unknown) {
|
|
1646
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
1647
|
+
userId: rpcIdentifier,
|
|
1648
|
+
botId: rpcBotId,
|
|
1649
|
+
appOrigin: rpcOrigin,
|
|
1650
|
+
});
|
|
1651
|
+
const identity = {
|
|
1652
|
+
userId: request.userId as string,
|
|
1653
|
+
botId: request.botId as string,
|
|
1654
|
+
};
|
|
1655
|
+
const { shell } = await this.materialized(identity);
|
|
1656
|
+
await shell.validateIdentity(identity);
|
|
1657
|
+
return openFocusedPanelV1(
|
|
1658
|
+
shell.state,
|
|
1659
|
+
identity,
|
|
1660
|
+
request.appOrigin as string,
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
async readFocusedPanel(input: unknown) {
|
|
1665
|
+
const identity = decodeBotIdentityRpcV1(input);
|
|
1666
|
+
const { shell } = await this.materialized(identity);
|
|
1667
|
+
await shell.validateIdentity(identity);
|
|
1668
|
+
const focus = await readFocusedPanelV1(shell.state.ctx.storage);
|
|
1669
|
+
return (
|
|
1670
|
+
focus ?? {
|
|
1671
|
+
schemaVersion: 1,
|
|
1672
|
+
pluginId: null,
|
|
1673
|
+
changedAt: new Date(0).toISOString(),
|
|
1674
|
+
}
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
/**
|
|
1679
|
+
* Records a Plugin page's use of a device ability once the client that
|
|
1680
|
+
* opened it says it has ended. Kept here first, so a rebuild reproduces the
|
|
1681
|
+
* row, then queued like any other; a retry of the same use is one row.
|
|
1682
|
+
*/
|
|
1683
|
+
async recordPanelDeviceUse(input: unknown) {
|
|
1684
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
1685
|
+
userId: rpcIdentifier,
|
|
1686
|
+
botId: rpcBotId,
|
|
1687
|
+
use: rpcDecoded(decodeDeviceUseV1),
|
|
1688
|
+
});
|
|
1689
|
+
const identity = {
|
|
1690
|
+
userId: request.userId as string,
|
|
1691
|
+
botId: request.botId as string,
|
|
1692
|
+
};
|
|
1693
|
+
const { shell } = await this.materialized(identity);
|
|
1694
|
+
await shell.validateIdentity(identity);
|
|
1695
|
+
const use = request.use as DeviceUseV1;
|
|
1696
|
+
const user = await panelDeviceUserV1(shell.state, identity, use);
|
|
1697
|
+
if ("refused" in user) {
|
|
1698
|
+
return { status: "refused" as const, reason: user.refused };
|
|
1699
|
+
}
|
|
1700
|
+
const entry = await auditEntryForDeviceUseV1(
|
|
1701
|
+
identity.botId,
|
|
1702
|
+
use,
|
|
1703
|
+
user.displayName,
|
|
1704
|
+
);
|
|
1705
|
+
await new DeviceUseLogV1(this.ctx.storage).record(entry);
|
|
1706
|
+
if (this.backendEnv.AUDIT_SINK) {
|
|
1707
|
+
// Appended even for a retry: the outbox and the table are both keyed
|
|
1708
|
+
// by the use, so a second append is one row, and a first append lost
|
|
1709
|
+
// between the two writes is not.
|
|
1710
|
+
await this.auditOutbox().append([entry]);
|
|
1711
|
+
await this.drainAuditOutbox();
|
|
1712
|
+
}
|
|
1713
|
+
return { status: "recorded" as const };
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/** What a Plugin's page reported, kept for its Bot to read back. */
|
|
1717
|
+
async recordPanelPageReport(input: unknown) {
|
|
1718
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
1719
|
+
userId: rpcIdentifier,
|
|
1720
|
+
botId: rpcBotId,
|
|
1721
|
+
report: rpcDecoded(decodePluginPageReportV1),
|
|
1722
|
+
});
|
|
1723
|
+
const identity = {
|
|
1724
|
+
userId: request.userId as string,
|
|
1725
|
+
botId: request.botId as string,
|
|
1726
|
+
};
|
|
1727
|
+
const { shell } = await this.materialized(identity);
|
|
1728
|
+
await shell.validateIdentity(identity);
|
|
1729
|
+
return recordPluginPageReportV1(
|
|
1730
|
+
this.ctx.storage,
|
|
1731
|
+
await readBotPluginRosterV1(shell.state, identity),
|
|
1732
|
+
request.report as PluginPageReportCommandV1,
|
|
1733
|
+
new Date(),
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
async setFocusedPanel(input: unknown) {
|
|
1738
|
+
const request = decodeRpcEnvelopeV1(
|
|
1739
|
+
input,
|
|
1740
|
+
{
|
|
1741
|
+
userId: rpcIdentifier,
|
|
1742
|
+
botId: rpcBotId,
|
|
1743
|
+
pluginId: rpcPluginIdOrNull,
|
|
1744
|
+
},
|
|
1745
|
+
{
|
|
1746
|
+
surfaceId: rpcPattern(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/, 128),
|
|
1747
|
+
},
|
|
1748
|
+
);
|
|
1749
|
+
const identity = {
|
|
1750
|
+
userId: request.userId as string,
|
|
1751
|
+
botId: request.botId as string,
|
|
1752
|
+
};
|
|
1753
|
+
const { shell } = await this.materialized(identity);
|
|
1754
|
+
await shell.validateIdentity(identity);
|
|
1755
|
+
return applyPanelFocusV1(shell.state, identity, {
|
|
1756
|
+
pluginId: request.pluginId as string | null,
|
|
1757
|
+
...(typeof request.surfaceId === "string"
|
|
1758
|
+
? { surfaceId: request.surfaceId }
|
|
1759
|
+
: {}),
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
/**
|
|
1764
|
+
* Flips one switch for this Bot, fenced on the revision the page read. A
|
|
1765
|
+
* stale revision, a locked Plugin or an unavailable feature is a receipt,
|
|
1766
|
+
* not a thrown error: the page re-reads and says why.
|
|
1767
|
+
*/
|
|
1768
|
+
async setBotPluginEnabled(input: unknown) {
|
|
1769
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
1770
|
+
userId: rpcIdentifier,
|
|
1771
|
+
botId: rpcBotId,
|
|
1772
|
+
command: rpcDecoded(decodeSetBotPluginEnabledCommandV1),
|
|
1773
|
+
});
|
|
1774
|
+
const identity = {
|
|
1775
|
+
userId: request.userId as string,
|
|
1776
|
+
botId: request.botId as string,
|
|
1777
|
+
};
|
|
1778
|
+
const { shell } = await this.materialized(identity);
|
|
1779
|
+
await shell.validateIdentity(identity);
|
|
1780
|
+
const receipt = await setBotPluginEnabledV1(
|
|
1781
|
+
shell.state,
|
|
1782
|
+
identity,
|
|
1783
|
+
request.command as ReturnType<typeof decodeSetBotPluginEnabledCommandV1>,
|
|
1784
|
+
);
|
|
1785
|
+
if (receipt.status === "applied") {
|
|
1786
|
+
this.ctx.waitUntil(
|
|
1787
|
+
this.assembleTheme({
|
|
1788
|
+
schemaVersion: 1,
|
|
1789
|
+
userId: identity.userId,
|
|
1790
|
+
botId: identity.botId,
|
|
1791
|
+
}).catch(() => undefined),
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
return receipt;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
/** A non-waking projection of this Bot's durable Computer presence. */
|
|
1798
|
+
async readComputerPresence(input: unknown) {
|
|
1799
|
+
const identity = decodeBotIdentityRpcV1(input);
|
|
1800
|
+
const { shell, computer } = await this.materialized(identity);
|
|
1801
|
+
await shell.validateIdentity(identity);
|
|
1802
|
+
return computer.read(identity.userId, identity.botId);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
/**
|
|
1806
|
+
* Keeps the frame a subagent's Turn left on the Bot's desktop as the card's.
|
|
1807
|
+
* The subagent runs in its task's own object; the card reads this one.
|
|
1808
|
+
*/
|
|
1809
|
+
async putComputerFrame(input: unknown): Promise<void> {
|
|
1810
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
1811
|
+
userId: rpcIdentifier,
|
|
1812
|
+
botId: rpcBotId,
|
|
1813
|
+
frame: rpcDecoded(decodeStoredComputerFrameV1),
|
|
1814
|
+
});
|
|
1815
|
+
const identity = {
|
|
1816
|
+
userId: request.userId as string,
|
|
1817
|
+
botId: request.botId as string,
|
|
1818
|
+
};
|
|
1819
|
+
const { shell, computer } = await this.materialized(identity);
|
|
1820
|
+
await shell.validateIdentity(identity);
|
|
1821
|
+
await computer.putFrame(request.frame as StoredComputerFrameV1);
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* The Bot's Computer frame, when `contentHash` still names it. Read from
|
|
1826
|
+
* this object's own storage; no Computer wakes to serve it.
|
|
1827
|
+
*/
|
|
1828
|
+
async readComputerFrame(
|
|
1829
|
+
input: unknown,
|
|
1830
|
+
): Promise<{ bytesBase64: string } | null> {
|
|
1831
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
1832
|
+
userId: rpcIdentifier,
|
|
1833
|
+
botId: rpcBotId,
|
|
1834
|
+
contentHash: rpcIdentifier,
|
|
1835
|
+
});
|
|
1836
|
+
const identity = {
|
|
1837
|
+
userId: request.userId as string,
|
|
1838
|
+
botId: request.botId as string,
|
|
1839
|
+
};
|
|
1840
|
+
const { shell, computer } = await this.materialized(identity);
|
|
1841
|
+
await shell.validateIdentity(identity);
|
|
1842
|
+
const frame = await computer.readFrame(request.contentHash as string);
|
|
1843
|
+
return frame ? { bytesBase64: bytesToBase64(frame.bytes) } : null;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/** One durably admitted User command against this Bot's Computer. */
|
|
1847
|
+
async executeComputerPresenceCommand(input: unknown) {
|
|
1848
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
1849
|
+
userId: rpcIdentifier,
|
|
1850
|
+
botId: rpcBotId,
|
|
1851
|
+
command: rpcDecoded(decodeComputerCommandV1),
|
|
1852
|
+
});
|
|
1853
|
+
if (
|
|
1854
|
+
typeof request.userId !== "string" ||
|
|
1855
|
+
typeof request.botId !== "string"
|
|
1856
|
+
) {
|
|
1857
|
+
throw new Error("Computer command RPC identity was not decoded");
|
|
1858
|
+
}
|
|
1859
|
+
const command = decodeComputerCommandV1(request.command);
|
|
1860
|
+
const identity = {
|
|
1861
|
+
userId: request.userId,
|
|
1862
|
+
botId: request.botId,
|
|
1863
|
+
};
|
|
1864
|
+
const { shell, computer } = await this.materialized(identity);
|
|
1865
|
+
await shell.validateIdentity(identity);
|
|
1866
|
+
return computer.execute(identity.userId, identity.botId, command);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
async readAvatar(input: unknown) {
|
|
1870
|
+
const identity = decodeBotIdentityRpcV1(input);
|
|
1871
|
+
const { flock, registration } = await this.materialized(identity);
|
|
1872
|
+
return flock.read(registration, identity.userId);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
async updateAvatar(input: unknown) {
|
|
1876
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
1877
|
+
userId: rpcIdentifier,
|
|
1878
|
+
botId: rpcBotId,
|
|
1879
|
+
command: rpcDecoded(decodeUpdateAvatarCommandV1),
|
|
1880
|
+
});
|
|
1881
|
+
const identity = {
|
|
1882
|
+
userId: request.userId as string,
|
|
1883
|
+
botId: request.botId as string,
|
|
1884
|
+
};
|
|
1885
|
+
const { flock, registration } = await this.materialized(identity);
|
|
1886
|
+
return flock.update(
|
|
1887
|
+
registration,
|
|
1888
|
+
identity.userId,
|
|
1889
|
+
request.command as ReturnType<typeof decodeUpdateAvatarCommandV1>,
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
async readVoice(input: unknown) {
|
|
1894
|
+
const identity = decodeBotIdentityRpcV1(input);
|
|
1895
|
+
const { flock, registration } = await this.materialized(identity);
|
|
1896
|
+
return flock.readVoice(registration, identity.userId);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
async updateVoice(input: unknown) {
|
|
1900
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
1901
|
+
userId: rpcIdentifier,
|
|
1902
|
+
botId: rpcBotId,
|
|
1903
|
+
command: rpcDecoded(decodeUpdateVoiceCommandV1),
|
|
1904
|
+
});
|
|
1905
|
+
const identity = {
|
|
1906
|
+
userId: request.userId as string,
|
|
1907
|
+
botId: request.botId as string,
|
|
1908
|
+
};
|
|
1909
|
+
const { flock, registration } = await this.materialized(identity);
|
|
1910
|
+
return flock.updateVoice(
|
|
1911
|
+
registration,
|
|
1912
|
+
identity.userId,
|
|
1913
|
+
request.command as ReturnType<typeof decodeUpdateVoiceCommandV1>,
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
async readLook(input: unknown) {
|
|
1918
|
+
const identity = decodeBotIdentityRpcV1(input);
|
|
1919
|
+
const { flock, registration } = await this.materialized(identity);
|
|
1920
|
+
return flock.readLook(registration, identity.userId);
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
async updateLook(input: unknown) {
|
|
1924
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
1925
|
+
userId: rpcIdentifier,
|
|
1926
|
+
botId: rpcBotId,
|
|
1927
|
+
command: rpcDecoded(decodeUpdateLookCommandV1),
|
|
1928
|
+
});
|
|
1929
|
+
const identity = {
|
|
1930
|
+
userId: request.userId as string,
|
|
1931
|
+
botId: request.botId as string,
|
|
1932
|
+
};
|
|
1933
|
+
const { flock, registration } = await this.materialized(identity);
|
|
1934
|
+
const receipt = await flock.updateLook(
|
|
1935
|
+
registration,
|
|
1936
|
+
identity.userId,
|
|
1937
|
+
request.command as ReturnType<typeof decodeUpdateLookCommandV1>,
|
|
1938
|
+
);
|
|
1939
|
+
if (receipt.status === "applied") {
|
|
1940
|
+
await holdThemePickV1(this.ctx.storage);
|
|
1941
|
+
// Another of the person's devices may have this Bot open.
|
|
1942
|
+
this.ctx.waitUntil(this.stateChannel.noticeLook().catch(() => undefined));
|
|
1943
|
+
this.ctx.waitUntil(
|
|
1944
|
+
this.assembleTheme({
|
|
1945
|
+
schemaVersion: 1,
|
|
1946
|
+
userId: identity.userId,
|
|
1947
|
+
botId: identity.botId,
|
|
1948
|
+
}).catch(() => undefined),
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
return receipt;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
async persistAssembledDocument(input: unknown) {
|
|
1955
|
+
const request = decodeRpcEnvelopeV1(
|
|
1956
|
+
input,
|
|
1957
|
+
{
|
|
1958
|
+
userId: rpcIdentifier,
|
|
1959
|
+
botId: rpcBotId,
|
|
1960
|
+
},
|
|
1961
|
+
{ document: rpcDecoded(decodeThemeDocumentV1) },
|
|
1962
|
+
);
|
|
1963
|
+
const identity = {
|
|
1964
|
+
userId: request.userId as string,
|
|
1965
|
+
botId: request.botId as string,
|
|
1966
|
+
};
|
|
1967
|
+
const { flock, registration } = await this.materialized(identity);
|
|
1968
|
+
return flock.persistAssembledDocument(
|
|
1969
|
+
registration,
|
|
1970
|
+
identity.userId,
|
|
1971
|
+
request.document === undefined
|
|
1972
|
+
? undefined
|
|
1973
|
+
: (request.document as ReturnType<typeof decodeThemeDocumentV1>),
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
async assembleTheme(input: unknown) {
|
|
1978
|
+
const identity = decodeBotIdentityRpcV1(input);
|
|
1979
|
+
const { flock, registration, shell } = await this.materialized(identity);
|
|
1980
|
+
const user = decodeUserSettingsViewV1(
|
|
1981
|
+
rpcJsonSnapshotV1(
|
|
1982
|
+
await userConfigurationV1(shell.state, identity).readConfiguration({
|
|
1983
|
+
schemaVersion: 1,
|
|
1984
|
+
userId: identity.userId,
|
|
1985
|
+
}),
|
|
1986
|
+
),
|
|
1987
|
+
);
|
|
1988
|
+
return assembleBotThemeV1(shell.state, identity, {
|
|
1989
|
+
flock,
|
|
1990
|
+
registration,
|
|
1991
|
+
appearance: user.appearance?.look ?? "ink",
|
|
1992
|
+
looks: BRAND_V1.looks,
|
|
1993
|
+
timezone: userTimezoneV1(user.profile),
|
|
1994
|
+
mirror: async (look, document) => {
|
|
1995
|
+
await userConfigurationV1(shell.state, identity).mirrorBotLook(
|
|
1996
|
+
identity.userId,
|
|
1997
|
+
identity.botId,
|
|
1998
|
+
look,
|
|
1999
|
+
document,
|
|
2000
|
+
);
|
|
2001
|
+
},
|
|
2002
|
+
changed: () => {
|
|
2003
|
+
this.ctx.waitUntil(
|
|
2004
|
+
this.stateChannel.noticeLook().catch(() => undefined),
|
|
2005
|
+
);
|
|
2006
|
+
},
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
/**
|
|
2011
|
+
* Sends the queued profile mirror, if one is due. The alarm path and the
|
|
2012
|
+
* post-commit attempt share this. A miss keeps the record for the alarm.
|
|
2013
|
+
*/
|
|
2014
|
+
private async deliverProfileMirror(): Promise<void> {
|
|
2015
|
+
const pending = await this.ctx.storage.get(PROFILE_MIRROR_KEY_V1);
|
|
2016
|
+
if (pending === undefined) return;
|
|
2017
|
+
const identity = await this.ctx.storage.get<BotIdentity>(IDENTITY_KEY);
|
|
2018
|
+
if (!identity) return;
|
|
2019
|
+
const shell = await this.contribution();
|
|
2020
|
+
await deliverProfileMirrorV1({
|
|
2021
|
+
storage: this.ctx.storage,
|
|
2022
|
+
now: Date.now(),
|
|
2023
|
+
botId: identity.botId,
|
|
2024
|
+
mirror: (profile) =>
|
|
2025
|
+
userConfigurationV1(shell.state, identity).mirrorBotProfile(
|
|
2026
|
+
identity.userId,
|
|
2027
|
+
identity.botId,
|
|
2028
|
+
profile,
|
|
2029
|
+
),
|
|
2030
|
+
refreshAlarm: (transaction) =>
|
|
2031
|
+
shell.state.authority.refreshRecoveryAlarm(
|
|
2032
|
+
transaction as unknown as Parameters<
|
|
2033
|
+
typeof shell.state.authority.refreshRecoveryAlarm
|
|
2034
|
+
>[0],
|
|
2035
|
+
),
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
private async assembleThemeIfDue(): Promise<void> {
|
|
2040
|
+
const due = await themeAssembleDeadlineV1(this.ctx.storage);
|
|
2041
|
+
if (due.length === 0 || due[0]! > Date.now()) return;
|
|
2042
|
+
const identity = await this.ctx.storage.get<BotIdentity>(IDENTITY_KEY);
|
|
2043
|
+
if (!identity) return;
|
|
2044
|
+
await this.assembleTheme({
|
|
2045
|
+
schemaVersion: 1,
|
|
2046
|
+
userId: identity.userId,
|
|
2047
|
+
botId: identity.botId,
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
// Neither lifecycle door materializes the Bot first: the Flock Contribution
|
|
2052
|
+
// reads the tombstone before it would, and materializing a deleted Bot
|
|
2053
|
+
// throws, which would leave a delete whose settlement was lost unable ever
|
|
2054
|
+
// to learn that it had happened.
|
|
2055
|
+
async readLifecycle(input: unknown) {
|
|
2056
|
+
const identity = decodeBotIdentityRpcV1(input);
|
|
2057
|
+
const { flock, registration } = await this.unmaterialized(identity);
|
|
2058
|
+
return flock.readLifecycle(registration, identity.userId);
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
async executeLifecycle(input: unknown) {
|
|
2062
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
2063
|
+
userId: rpcIdentifier,
|
|
2064
|
+
botId: rpcBotId,
|
|
2065
|
+
command: rpcDecoded(decodeBotLifecycleCommandV1),
|
|
2066
|
+
});
|
|
2067
|
+
const identity = {
|
|
2068
|
+
userId: request.userId as string,
|
|
2069
|
+
botId: request.botId as string,
|
|
2070
|
+
};
|
|
2071
|
+
const command = request.command as BotLifecycleCommandV1;
|
|
2072
|
+
if (command.botId !== identity.botId)
|
|
2073
|
+
throw new Error("lifecycle command does not match Bot authority");
|
|
2074
|
+
const { flock, registration } = await this.unmaterialized(identity);
|
|
2075
|
+
return flock.executeLifecycle(registration, identity.userId, command);
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
/**
|
|
2079
|
+
* D6: model invocation as a User-enabled binding. Without the resolved
|
|
2080
|
+
* model binding the answer is unavailable; with one, the
|
|
2081
|
+
* request is recorded and the credential lease taken through the existing
|
|
2082
|
+
* provider path before any event is streamed back.
|
|
2083
|
+
*/
|
|
2084
|
+
async isolateInvokeModel(input: unknown) {
|
|
2085
|
+
const request = decodeIsolateCallRpcV1(
|
|
2086
|
+
input,
|
|
2087
|
+
decodeNormalizedModelRequestV1,
|
|
2088
|
+
);
|
|
2089
|
+
const identity = {
|
|
2090
|
+
userId: request.userId as string,
|
|
2091
|
+
botId: request.botId as string,
|
|
2092
|
+
};
|
|
2093
|
+
const shell = await this.contribution();
|
|
2094
|
+
return isolateInvokeModel(shell.state, identity, {
|
|
2095
|
+
runId: request.runId as string,
|
|
2096
|
+
sessionId: request.sessionId as string,
|
|
2097
|
+
turnId: request.turnId as string,
|
|
2098
|
+
packageId: request.packageId as string,
|
|
2099
|
+
generationId: request.generationId as string,
|
|
2100
|
+
request: request.request as NormalizedModelRequest,
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
async isolateAuthority(input: unknown) {
|
|
2105
|
+
const request = decodeIsolateCallRpcV1(input, (value) => value ?? null);
|
|
2106
|
+
return isolateAuthority(
|
|
2107
|
+
(await this.contribution()).state,
|
|
2108
|
+
{ userId: request.userId as string, botId: request.botId as string },
|
|
2109
|
+
request as unknown as IsolateCallScopeV1,
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
async isolateStorageGet(input: unknown) {
|
|
2114
|
+
return isolateStorageGet(
|
|
2115
|
+
(await this.contribution()).state,
|
|
2116
|
+
decodeIsolateCallRpcV1(
|
|
2117
|
+
input,
|
|
2118
|
+
decodeIsolateStorageGetRequestV1,
|
|
2119
|
+
) as unknown as IsolateCallScopeV1,
|
|
2120
|
+
);
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
async isolateStoragePut(input: unknown) {
|
|
2124
|
+
return isolateStoragePut(
|
|
2125
|
+
(await this.contribution()).state,
|
|
2126
|
+
decodeIsolateCallRpcV1(
|
|
2127
|
+
input,
|
|
2128
|
+
decodeIsolateStoragePutRequestV1,
|
|
2129
|
+
) as unknown as IsolateCallScopeV1,
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
async isolateStorageDelete(input: unknown) {
|
|
2134
|
+
return isolateStorageDelete(
|
|
2135
|
+
(await this.contribution()).state,
|
|
2136
|
+
decodeIsolateCallRpcV1(
|
|
2137
|
+
input,
|
|
2138
|
+
decodeIsolateStorageDeleteRequestV1,
|
|
2139
|
+
) as unknown as IsolateCallScopeV1,
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
async isolateStorageList(input: unknown) {
|
|
2144
|
+
return isolateStorageList(
|
|
2145
|
+
(await this.contribution()).state,
|
|
2146
|
+
decodeIsolateCallRpcV1(input, (value) =>
|
|
2147
|
+
decodeIsolateStorageListRequestV1(value ?? {}),
|
|
2148
|
+
) as unknown as IsolateCallScopeV1,
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
async isolateSettings(input: unknown) {
|
|
2153
|
+
return isolateSettings(
|
|
2154
|
+
(await this.contribution()).state,
|
|
2155
|
+
decodeIsolateCallRpcV1(
|
|
2156
|
+
input,
|
|
2157
|
+
(value) => value ?? null,
|
|
2158
|
+
) as unknown as IsolateCallScopeV1,
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
async isolateJevDecide(input: unknown) {
|
|
2163
|
+
return isolateJevDecide(
|
|
2164
|
+
(await this.contribution()).state,
|
|
2165
|
+
decodeIsolateCallRpcV1(input, decodeIsolateJevRequestV1) as never,
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
async isolateMemoryRead(input: unknown) {
|
|
2170
|
+
return isolateMemoryRead(
|
|
2171
|
+
(await this.contribution()).state,
|
|
2172
|
+
decodeIsolateCallRpcV1(input, decodeIsolateMemoryReadRequestV1) as never,
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
async isolateMemoryWrite(input: unknown) {
|
|
2177
|
+
return isolateMemoryWrite(
|
|
2178
|
+
(await this.contribution()).state,
|
|
2179
|
+
decodeIsolateCallRpcV1(input, decodeIsolateMemoryWriteRequestV1) as never,
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
async isolateMemoryForget(input: unknown) {
|
|
2184
|
+
return isolateMemoryForget(
|
|
2185
|
+
(await this.contribution()).state,
|
|
2186
|
+
decodeIsolateCallRpcV1(input, decodeIsolateMemoryWriteRequestV1) as never,
|
|
2187
|
+
);
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
async isolateWorkspaceRead(input: unknown) {
|
|
2191
|
+
return isolateWorkspaceRead(
|
|
2192
|
+
(await this.contribution()).state,
|
|
2193
|
+
decodeIsolateCallRpcV1(input, decodeIsolateWorkspacePathV1) as never,
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
async isolateWorkspaceList(input: unknown) {
|
|
2198
|
+
return isolateWorkspaceList(
|
|
2199
|
+
(await this.contribution()).state,
|
|
2200
|
+
decodeIsolateCallRpcV1(
|
|
2201
|
+
input,
|
|
2202
|
+
decodeIsolateWorkspaceListRequestV1,
|
|
2203
|
+
) as never,
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
async isolateWorkspaceStat(input: unknown) {
|
|
2208
|
+
return isolateWorkspaceStat(
|
|
2209
|
+
(await this.contribution()).state,
|
|
2210
|
+
decodeIsolateCallRpcV1(input, decodeIsolateWorkspacePathV1) as never,
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
async isolateWorkspaceWrite(input: unknown) {
|
|
2215
|
+
return isolateWorkspaceWrite(
|
|
2216
|
+
(await this.contribution()).state,
|
|
2217
|
+
decodeIsolateCallRpcV1(
|
|
2218
|
+
input,
|
|
2219
|
+
decodeIsolateWorkspaceWriteRequestV1,
|
|
2220
|
+
) as never,
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
async isolateWorkspaceDelete(input: unknown) {
|
|
2225
|
+
return isolateWorkspaceDelete(
|
|
2226
|
+
(await this.contribution()).state,
|
|
2227
|
+
decodeIsolateCallRpcV1(
|
|
2228
|
+
input,
|
|
2229
|
+
decodeIsolateWorkspaceDeleteRequestV1,
|
|
2230
|
+
) as never,
|
|
2231
|
+
);
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
async isolateConnection(input: unknown) {
|
|
2235
|
+
return isolateConnection(
|
|
2236
|
+
(await this.contribution()).state,
|
|
2237
|
+
decodeIsolateCallRpcV1(input, (value) => {
|
|
2238
|
+
if (
|
|
2239
|
+
typeof value !== "string" ||
|
|
2240
|
+
value.length === 0 ||
|
|
2241
|
+
value.length > 256
|
|
2242
|
+
) {
|
|
2243
|
+
throw new Error("Connection id is invalid");
|
|
2244
|
+
}
|
|
2245
|
+
return value;
|
|
2246
|
+
}) as never,
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
async isolateModelTransport(input: unknown) {
|
|
2251
|
+
return isolateModelTransport(
|
|
2252
|
+
(await this.contribution()).state,
|
|
2253
|
+
// The transport request is decoded where the ticket is spent, so a
|
|
2254
|
+
// malformed call is refused without costing the dispatch its attempt.
|
|
2255
|
+
decodeIsolateCallRpcV1(input, (value) => value) as never,
|
|
2256
|
+
);
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
async isolateEmail(input: unknown) {
|
|
2260
|
+
return isolateEmail(
|
|
2261
|
+
(await this.contribution()).state,
|
|
2262
|
+
decodeIsolateCallRpcV1(input, decodeIsolateEmailRequestV1) as never,
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
async isolateDeviceCall(input: unknown) {
|
|
2267
|
+
return isolateDeviceCall(
|
|
2268
|
+
(await this.contribution()).state,
|
|
2269
|
+
decodeIsolateCallRpcV1(input, decodeIsolateDeviceCallRequestV1) as never,
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
async isolateSchedule(input: unknown) {
|
|
2274
|
+
return isolateSchedule(
|
|
2275
|
+
(await this.contribution()).state,
|
|
2276
|
+
decodeIsolateCallRpcV1(input, decodeIsolateScheduleRequestV1) as never,
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
async resolveConfiguration(input: unknown) {
|
|
2281
|
+
const identity = decodeBotIdentityRpcV1(input);
|
|
2282
|
+
const { shell } = await this.materialized(identity);
|
|
2283
|
+
return resolveConfiguration(shell.state, identity);
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
async run(input: unknown) {
|
|
2287
|
+
const request = decodeBotRunRpcV1(input);
|
|
2288
|
+
const identity = { userId: request.userId, botId: request.botId };
|
|
2289
|
+
const { shell } = await this.materialized(identity);
|
|
2290
|
+
return shell.run({
|
|
2291
|
+
...identity,
|
|
2292
|
+
...(await this.withAttachments(request.command)),
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
/**
|
|
2297
|
+
* A command's file refs, resolved against the uploads this Bot holds. A
|
|
2298
|
+
* ref to anything else is refused here, before the Turn is admitted, so a
|
|
2299
|
+
* message can only carry a file this Bot was given.
|
|
2300
|
+
*/
|
|
2301
|
+
private async withAttachments(
|
|
2302
|
+
command: DecodedBotRunRpcV1["command"],
|
|
2303
|
+
): Promise<BotTurnCommand> {
|
|
2304
|
+
const { attachments: refs, ...rest } = command;
|
|
2305
|
+
const attachments = await resolveUploadRefsV1(this.ctx.storage, refs);
|
|
2306
|
+
if (attachments) {
|
|
2307
|
+
// A recording the person just sent is theirs no longer to decide on:
|
|
2308
|
+
// the Computer stops offering it. Never a reason to refuse the message.
|
|
2309
|
+
const mounted = await this.mounted?.catch(() => undefined);
|
|
2310
|
+
await mounted?.computer
|
|
2311
|
+
.noteDemonstrationSent(attachments.map(({ uploadId }) => uploadId))
|
|
2312
|
+
.catch(() => undefined);
|
|
2313
|
+
}
|
|
2314
|
+
return { ...rest, ...(attachments ? { attachments } : {}) };
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
/**
|
|
2318
|
+
* Records one upload the gateway has just written to object storage. The
|
|
2319
|
+
* upload route answers only after this, so the file a later message names
|
|
2320
|
+
* is one this Bot durably holds.
|
|
2321
|
+
*/
|
|
2322
|
+
async recordUploadV1(input: unknown): Promise<StoredUploadV1> {
|
|
2323
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
2324
|
+
userId: rpcIdentifier,
|
|
2325
|
+
botId: rpcBotId,
|
|
2326
|
+
upload: rpcDecoded(decodeStoredUploadV1),
|
|
2327
|
+
});
|
|
2328
|
+
const identity = {
|
|
2329
|
+
userId: request.userId as string,
|
|
2330
|
+
botId: request.botId as string,
|
|
2331
|
+
};
|
|
2332
|
+
const { shell } = await this.materialized(identity);
|
|
2333
|
+
await shell.validateIdentity(identity);
|
|
2334
|
+
return recordUploadV1(this.ctx.storage, request.upload as StoredUploadV1);
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
/**
|
|
2338
|
+
* Durably accepts a composer command and returns its receipt.
|
|
2339
|
+
*
|
|
2340
|
+
* Execution continues on this object's drive. `waitUntil` keeps the
|
|
2341
|
+
* isolate awake for that drive; the recovery alarm is what resumes it
|
|
2342
|
+
* after eviction.
|
|
2343
|
+
*/
|
|
2344
|
+
async admitRun(input: unknown) {
|
|
2345
|
+
const request = decodeBotRunRpcV1(input);
|
|
2346
|
+
const identity = { userId: request.userId, botId: request.botId };
|
|
2347
|
+
const { shell } = await this.materialized(identity);
|
|
2348
|
+
const receipt = await shell.admit({
|
|
2349
|
+
...identity,
|
|
2350
|
+
...(await this.withAttachments(request.command)),
|
|
2351
|
+
});
|
|
2352
|
+
const work = shell.pendingWork();
|
|
2353
|
+
if (work) this.ctx.waitUntil(work);
|
|
2354
|
+
return receipt;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
/**
|
|
2358
|
+
* Finishes the resident drive, including the settled-run projections.
|
|
2359
|
+
*
|
|
2360
|
+
* The composer returns at admission. A caller that needs search and audit
|
|
2361
|
+
* rows waits here. Eviction drops this promise; the recovery alarm resumes
|
|
2362
|
+
* the run.
|
|
2363
|
+
*/
|
|
2364
|
+
async joinAdmittedDrive(): Promise<void> {
|
|
2365
|
+
const mounted = this.mounted;
|
|
2366
|
+
if (!mounted) return;
|
|
2367
|
+
const { shell } = await mounted;
|
|
2368
|
+
await shell.pendingWork();
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
async runAgent(input: unknown) {
|
|
2372
|
+
const request = decodeBotAgentRunRpcV1(input);
|
|
2373
|
+
const identity = { userId: request.userId, botId: request.botId };
|
|
2374
|
+
const { shell } = await this.materialized(identity);
|
|
2375
|
+
const turn = await shell.run({
|
|
2376
|
+
...identity,
|
|
2377
|
+
runId: request.command.runId,
|
|
2378
|
+
sessionId: request.command.sessionId,
|
|
2379
|
+
acceptedAt: request.command.acceptedAt,
|
|
2380
|
+
text: request.command.text,
|
|
2381
|
+
turnType: "agent",
|
|
2382
|
+
lane: "agent",
|
|
2383
|
+
origin: request.command.source,
|
|
2384
|
+
});
|
|
2385
|
+
return turn;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
/**
|
|
2389
|
+
* The account's voice session asking this Bot for something.
|
|
2390
|
+
*
|
|
2391
|
+
* The same agent lane a Bot-to-Bot question uses, for the same reason: it
|
|
2392
|
+
* queues behind whatever the person or a Routine already has running and
|
|
2393
|
+
* never takes its place. What differs is the return address — a call and a
|
|
2394
|
+
* spoken Turn rather than a Bot — and that the answer goes back through the
|
|
2395
|
+
* voice reply outbox instead of this call's return value.
|
|
2396
|
+
*/
|
|
2397
|
+
async runVoice(input: unknown) {
|
|
2398
|
+
const request = decodeBotVoiceRunRpcV1(input);
|
|
2399
|
+
const identity = { userId: request.userId, botId: request.botId };
|
|
2400
|
+
const { shell } = await this.materialized(identity);
|
|
2401
|
+
const turn = await shell.run({
|
|
2402
|
+
...identity,
|
|
2403
|
+
runId: request.command.runId,
|
|
2404
|
+
sessionId: request.command.sessionId,
|
|
2405
|
+
acceptedAt: request.command.acceptedAt,
|
|
2406
|
+
text: request.command.text,
|
|
2407
|
+
turnType: "agent",
|
|
2408
|
+
lane: "agent",
|
|
2409
|
+
origin: request.command.source,
|
|
2410
|
+
});
|
|
2411
|
+
await this.drainVoiceReplyOutbox(identity.userId);
|
|
2412
|
+
return turn;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
/**
|
|
2416
|
+
* A Group Chat asking this member for a Turn.
|
|
2417
|
+
*
|
|
2418
|
+
* The agent lane, like a Bot's question: it waits behind the person's own
|
|
2419
|
+
* chat and never makes it yield. The admission returns at once; the group
|
|
2420
|
+
* reads the Turn back by id as it runs.
|
|
2421
|
+
*/
|
|
2422
|
+
async admitGroupTurn(input: unknown) {
|
|
2423
|
+
const request = decodeBotGroupTurnRpcV1(input);
|
|
2424
|
+
const identity = { userId: request.userId, botId: request.botId };
|
|
2425
|
+
const { shell } = await this.materialized(identity);
|
|
2426
|
+
const receipt = await shell.admit({
|
|
2427
|
+
...identity,
|
|
2428
|
+
runId: request.command.runId,
|
|
2429
|
+
sessionId: request.command.sessionId,
|
|
2430
|
+
acceptedAt: request.command.acceptedAt,
|
|
2431
|
+
text: request.command.text,
|
|
2432
|
+
turnType: "agent",
|
|
2433
|
+
lane: "agent",
|
|
2434
|
+
origin: request.command.origin,
|
|
2435
|
+
});
|
|
2436
|
+
const work = shell.pendingWork();
|
|
2437
|
+
if (work) this.ctx.waitUntil(work);
|
|
2438
|
+
return receipt;
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
/**
|
|
2442
|
+
* The person, writing to this Bot's inbound address from one of their
|
|
2443
|
+
* confirmed mailboxes.
|
|
2444
|
+
*
|
|
2445
|
+
* The user lane, like a typed message. The `email()` handler returns only
|
|
2446
|
+
* once this does, so the message is durable before Email Routing is told it
|
|
2447
|
+
* arrived, and a redelivery — the same run id — replays this admission. Its
|
|
2448
|
+
* files are refs to uploads the handler recorded here first, resolved like a
|
|
2449
|
+
* composer's.
|
|
2450
|
+
*/
|
|
2451
|
+
async admitEmailTurn(input: unknown) {
|
|
2452
|
+
const request = decodeBotEmailTurnRpcV1(input);
|
|
2453
|
+
const identity = { userId: request.userId, botId: request.botId };
|
|
2454
|
+
const { shell } = await this.materialized(identity);
|
|
2455
|
+
await shell.validateIdentity(identity);
|
|
2456
|
+
const { origin, ...command } = request.command;
|
|
2457
|
+
// A message that only passes something on is answered without waking a
|
|
2458
|
+
// device. Unjudged, the Turn is as loud as ever.
|
|
2459
|
+
const triage = await shell.state.emailTriageJudge?.({ text: command.text });
|
|
2460
|
+
const receipt = await shell.admit({
|
|
2461
|
+
...identity,
|
|
2462
|
+
...(await this.withAttachments(command)),
|
|
2463
|
+
turnType: "chat",
|
|
2464
|
+
lane: "user",
|
|
2465
|
+
origin: triage?.quiet ? { ...origin, quiet: true } : origin,
|
|
2466
|
+
});
|
|
2467
|
+
const work = shell.pendingWork();
|
|
2468
|
+
if (work) this.ctx.waitUntil(work);
|
|
2469
|
+
return receipt;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
/** One of this member's group Turns, as its group reads it back. */
|
|
2473
|
+
async readGroupTurn(input: unknown) {
|
|
2474
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
2475
|
+
userId: rpcIdentifier,
|
|
2476
|
+
botId: rpcBotId,
|
|
2477
|
+
runId: rpcString(128),
|
|
2478
|
+
});
|
|
2479
|
+
const identity = {
|
|
2480
|
+
userId: request.userId as string,
|
|
2481
|
+
botId: request.botId as string,
|
|
2482
|
+
};
|
|
2483
|
+
const { shell } = await this.materialized(identity);
|
|
2484
|
+
const run = await shell.state.authority.readRun(request.runId as string);
|
|
2485
|
+
if (!run || !groupOriginOfRunV1(run)) {
|
|
2486
|
+
return { schemaVersion: 1, found: false } as const;
|
|
2487
|
+
}
|
|
2488
|
+
return {
|
|
2489
|
+
schemaVersion: 1,
|
|
2490
|
+
found: true,
|
|
2491
|
+
state: groupTurnStateOfRunV1(run),
|
|
2492
|
+
} as const;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
/**
|
|
2496
|
+
* A newer message in a group this member has a Turn open in. Kept as the
|
|
2497
|
+
* highest position heard of, so the Turn yields at its next step boundary.
|
|
2498
|
+
*/
|
|
2499
|
+
async signalGroupMessage(input: unknown) {
|
|
2500
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
2501
|
+
userId: rpcIdentifier,
|
|
2502
|
+
botId: rpcBotId,
|
|
2503
|
+
groupId: rpcPattern(/^g-[0-9a-f]{20}$/, 22),
|
|
2504
|
+
seq: rpcInteger({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }),
|
|
2505
|
+
});
|
|
2506
|
+
await this.materialized({
|
|
2507
|
+
userId: request.userId as string,
|
|
2508
|
+
botId: request.botId as string,
|
|
2509
|
+
});
|
|
2510
|
+
const key = groupWaitingKeyV1(request.groupId as string);
|
|
2511
|
+
await this.ctx.storage.transaction(async (transaction) => {
|
|
2512
|
+
const current = await transaction.get<number>(key);
|
|
2513
|
+
if (typeof current !== "number" || current < (request.seq as number)) {
|
|
2514
|
+
await transaction.put(key, request.seq as number);
|
|
2515
|
+
}
|
|
2516
|
+
});
|
|
2517
|
+
return { schemaVersion: 1 } as const;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
/**
|
|
2521
|
+
* Tells a group one of its member Turns here changed: it started, sent
|
|
2522
|
+
* something, or settled. Best effort: the group reads the Turn itself, and
|
|
2523
|
+
* its alarm reads it again if this is lost.
|
|
2524
|
+
*/
|
|
2525
|
+
private nudgeGroup(runId: string | undefined): void {
|
|
2526
|
+
const namespace = this.backendEnv.GROUP_CHATS;
|
|
2527
|
+
if (!namespace || !runId) return;
|
|
2528
|
+
this.ctx.waitUntil(
|
|
2529
|
+
(async () => {
|
|
2530
|
+
const stored = await this.ctx.storage.get<{
|
|
2531
|
+
admission?: { origin?: { kind: string; groupId?: string } };
|
|
2532
|
+
}>(`run:${runId}`);
|
|
2533
|
+
const groupId =
|
|
2534
|
+
stored?.admission?.origin?.kind === "group"
|
|
2535
|
+
? stored.admission.origin.groupId
|
|
2536
|
+
: undefined;
|
|
2537
|
+
if (!groupId) return;
|
|
2538
|
+
const identity = await this.ctx.storage.get<BotIdentity>(IDENTITY_KEY);
|
|
2539
|
+
if (!identity) return;
|
|
2540
|
+
// SAFETY: the binding names GroupChat; this is its reviewed RPC door.
|
|
2541
|
+
const group = namespace.get(
|
|
2542
|
+
namespace.idFromName(groupChatObjectNameV1(identity.userId, groupId)),
|
|
2543
|
+
) as unknown as { turnChanged(input: unknown): Promise<unknown> };
|
|
2544
|
+
await group.turnChanged({
|
|
2545
|
+
schemaVersion: 1,
|
|
2546
|
+
userId: identity.userId,
|
|
2547
|
+
groupId,
|
|
2548
|
+
botId: identity.botId,
|
|
2549
|
+
runId,
|
|
2550
|
+
});
|
|
2551
|
+
})().catch(() => undefined),
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
/**
|
|
2556
|
+
* A settled voice request written into this Bot's thread after hang-up.
|
|
2557
|
+
*
|
|
2558
|
+
* The voice object is the only caller. The receipt is the run id, so a
|
|
2559
|
+
* retried announce is one message. The send is projected onto the voice
|
|
2560
|
+
* request's own run, the way a Routine that broke after it had already
|
|
2561
|
+
* ended still lands a readable line.
|
|
2562
|
+
*/
|
|
2563
|
+
async deliverVoiceChatResult(input: unknown) {
|
|
2564
|
+
const request = decodeVoiceChatResultRpcV1(input);
|
|
2565
|
+
const identity = { userId: request.userId, botId: request.botId };
|
|
2566
|
+
const { shell } = await this.materialized(identity);
|
|
2567
|
+
await shell.validateIdentity(identity);
|
|
2568
|
+
const settings = await readBotSettingsV1(shell.state, identity);
|
|
2569
|
+
const receiptKey = `voice:chat-result:${request.command.runId}`;
|
|
2570
|
+
const createdAt = new Date().toISOString();
|
|
2571
|
+
let committed = false;
|
|
2572
|
+
await this.ctx.storage.transaction(async (transaction) => {
|
|
2573
|
+
if (await transaction.get(receiptKey)) return;
|
|
2574
|
+
const records = await visibleMessageRecordsV1({
|
|
2575
|
+
settings,
|
|
2576
|
+
read: (key) => transaction.get(key),
|
|
2577
|
+
messages: [
|
|
2578
|
+
{
|
|
2579
|
+
messageId: messageIdV1(
|
|
2580
|
+
request.command.runId,
|
|
2581
|
+
request.command.ordinal,
|
|
2582
|
+
),
|
|
2583
|
+
runId: request.command.runId,
|
|
2584
|
+
createdAt,
|
|
2585
|
+
body: request.command.body.slice(0, 2_000),
|
|
2586
|
+
automation: true,
|
|
2587
|
+
projectedSendOrdinal: request.command.ordinal,
|
|
2588
|
+
},
|
|
2589
|
+
],
|
|
2590
|
+
});
|
|
2591
|
+
for (const [key, value] of Object.entries(records)) {
|
|
2592
|
+
await transaction.put(key, value);
|
|
2593
|
+
}
|
|
2594
|
+
await transaction.put(receiptKey, { schemaVersion: 1, at: createdAt });
|
|
2595
|
+
committed = true;
|
|
2596
|
+
});
|
|
2597
|
+
if (committed) this.ctx.waitUntil(this.drainPush());
|
|
2598
|
+
return { status: "accepted" as const };
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
/**
|
|
2602
|
+
* Spoken turns of one ended call, written as a collapsible thread section.
|
|
2603
|
+
*
|
|
2604
|
+
* The voice object is the only caller. The receipt is the call id, so a
|
|
2605
|
+
* hang-up and the abandoned-call alarm that follows a dropped socket write
|
|
2606
|
+
* one section. The observer is told so the thread redraws without a poll.
|
|
2607
|
+
*/
|
|
2608
|
+
async deliverVoiceCallTranscript(input: unknown) {
|
|
2609
|
+
const request = decodeVoiceCallTranscriptRpcV1(input);
|
|
2610
|
+
const identity = { userId: request.userId, botId: request.botId };
|
|
2611
|
+
const { shell } = await this.materialized(identity);
|
|
2612
|
+
await shell.validateIdentity(identity);
|
|
2613
|
+
const receiptKey = `voice:call-transcript:${request.command.callId}`;
|
|
2614
|
+
let committed = false;
|
|
2615
|
+
await this.ctx.storage.transaction(async (transaction) => {
|
|
2616
|
+
if (await transaction.get(receiptKey)) return;
|
|
2617
|
+
await appendAnnouncement(transaction, (seq) => ({
|
|
2618
|
+
type: "voice/call",
|
|
2619
|
+
seq,
|
|
2620
|
+
timestamp: request.command.endedAt,
|
|
2621
|
+
callId: request.command.callId,
|
|
2622
|
+
startedAt: request.command.startedAt,
|
|
2623
|
+
endedAt: request.command.endedAt,
|
|
2624
|
+
turns: request.command.turns,
|
|
2625
|
+
}));
|
|
2626
|
+
await transaction.put({
|
|
2627
|
+
[receiptKey]: {
|
|
2628
|
+
schemaVersion: 1,
|
|
2629
|
+
at: request.command.endedAt,
|
|
2630
|
+
},
|
|
2631
|
+
});
|
|
2632
|
+
await shell.state.authority.refreshRecoveryAlarm(transaction);
|
|
2633
|
+
committed = true;
|
|
2634
|
+
});
|
|
2635
|
+
if (committed) await shell.state.authority.drainCommittedPublication();
|
|
2636
|
+
return { status: "accepted" as const };
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
/** This object's bounded, durable audit outbox. */
|
|
2640
|
+
private auditOutbox(): AuditOutboxV1 {
|
|
2641
|
+
return new AuditOutboxV1(this.ctx.storage);
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
/**
|
|
2645
|
+
* Queues one settled run's audit entries and drains what is pending.
|
|
2646
|
+
*
|
|
2647
|
+
* Queue first, deliver second, and never the other way round: the entries
|
|
2648
|
+
* are durable in this object before the User object is asked for anything,
|
|
2649
|
+
* so a User object that is away, slow, or mid-eviction costs a retry rather
|
|
2650
|
+
* than a gap. Whatever a drain leaves behind is picked up by the next
|
|
2651
|
+
* settlement or by the alarm this object already has.
|
|
2652
|
+
*/
|
|
2653
|
+
private async projectSettledAudit(
|
|
2654
|
+
shell: ShellBotBackendContribution,
|
|
2655
|
+
identity: { userId: string; botId: string },
|
|
2656
|
+
runId: string,
|
|
2657
|
+
): Promise<void> {
|
|
2658
|
+
const sink = this.backendEnv.AUDIT_SINK;
|
|
2659
|
+
if (!sink) return;
|
|
2660
|
+
const outbox = this.auditOutbox();
|
|
2661
|
+
try {
|
|
2662
|
+
const lookup = await shell.lookupRun({ schemaVersion: 1, runId });
|
|
2663
|
+
if (lookup.state === "terminal") {
|
|
2664
|
+
const stored = await shell.listRunEventPage();
|
|
2665
|
+
const run = stored.runs.find((candidate) => candidate.runId === runId);
|
|
2666
|
+
if (run) {
|
|
2667
|
+
await outbox.append(
|
|
2668
|
+
await auditEntriesFromStoredRunV1(identity.botId, run),
|
|
2669
|
+
);
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
} catch {
|
|
2673
|
+
// A projection this object could not build is a gap a rebuild closes;
|
|
2674
|
+
// it is never a reason for an admitted Turn to look as if it failed.
|
|
2675
|
+
}
|
|
2676
|
+
await this.drainAuditOutbox();
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
/**
|
|
2680
|
+
* Hands whatever is pending to the User Durable Object.
|
|
2681
|
+
*
|
|
2682
|
+
* The failure is swallowed *here* and not inside the outbox: the outbox
|
|
2683
|
+
* throws so that nothing is cleared that was not delivered, and this call
|
|
2684
|
+
* site swallows so that a derived projection never decides whether a Turn
|
|
2685
|
+
* settled.
|
|
2686
|
+
*/
|
|
2687
|
+
private async drainAuditOutbox(): Promise<void> {
|
|
2688
|
+
const sink = this.backendEnv.AUDIT_SINK;
|
|
2689
|
+
if (!sink) return;
|
|
2690
|
+
try {
|
|
2691
|
+
await this.auditOutbox().drain(sink);
|
|
2692
|
+
} catch {
|
|
2693
|
+
// Still pending, still durable, still visible as `pending` in the
|
|
2694
|
+
// outbox state the Activity surface reads.
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
/**
|
|
2699
|
+
* One page of this Bot's audit entries, projected from its own stored runs.
|
|
2700
|
+
*
|
|
2701
|
+
* The durable session events rather than the client projection: the client
|
|
2702
|
+
* projection drops `call.input`, and the argument digest needs the exact
|
|
2703
|
+
* arguments.
|
|
2704
|
+
*/
|
|
2705
|
+
async projectAuditEntries(input: unknown) {
|
|
2706
|
+
const request = decodeRpcEnvelopeV1(
|
|
2707
|
+
input,
|
|
2708
|
+
{ userId: rpcIdentifier, botId: rpcBotId },
|
|
2709
|
+
{ cursor: rpcString(512) },
|
|
2710
|
+
);
|
|
2711
|
+
const identity = {
|
|
2712
|
+
userId: request.userId as string,
|
|
2713
|
+
botId: request.botId as string,
|
|
2714
|
+
};
|
|
2715
|
+
const { shell } = await this.materialized(identity);
|
|
2716
|
+
await shell.validateIdentity(identity);
|
|
2717
|
+
const cursor = request.cursor as string | undefined;
|
|
2718
|
+
// Device uses come from no run, so this object's own record of them is
|
|
2719
|
+
// the first page; the runs follow from their start.
|
|
2720
|
+
if (cursor === undefined) {
|
|
2721
|
+
const devices = await new DeviceUseLogV1(this.ctx.storage).entries();
|
|
2722
|
+
if (devices.length > 0) {
|
|
2723
|
+
return {
|
|
2724
|
+
schemaVersion: 1 as const,
|
|
2725
|
+
botId: identity.botId,
|
|
2726
|
+
entries: devices,
|
|
2727
|
+
nextCursor: DEVICE_USE_PAGE_DONE_V1,
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
return createBotAuditEntryPageV1(
|
|
2732
|
+
identity.botId,
|
|
2733
|
+
await shell.listRunEventPage(
|
|
2734
|
+
cursor === DEVICE_USE_PAGE_DONE_V1 ? undefined : cursor,
|
|
2735
|
+
),
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
/**
|
|
2740
|
+
* Search and audit for one settled run. Called from the authority once the
|
|
2741
|
+
* run is terminal, including when the composer did not wait for it.
|
|
2742
|
+
*/
|
|
2743
|
+
private async projectSettled(
|
|
2744
|
+
shell: ShellBotBackendContribution,
|
|
2745
|
+
runId: string,
|
|
2746
|
+
): Promise<void> {
|
|
2747
|
+
const identity = await this.ctx.storage.get<BotIdentity>(IDENTITY_KEY);
|
|
2748
|
+
if (!identity) return;
|
|
2749
|
+
await this.projectSettledRun(shell, identity, runId);
|
|
2750
|
+
await this.projectSettledAudit(shell, identity, runId);
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
/**
|
|
2754
|
+
* Projects one settled run into the User's transcript index.
|
|
2755
|
+
*
|
|
2756
|
+
* After settlement, and never before it: the run is already durable in this
|
|
2757
|
+
* object, so an index write that fails costs a rebuild rather than a Turn.
|
|
2758
|
+
* The failure is swallowed for the same reason — a derived projection must
|
|
2759
|
+
* not decide whether the thing it derives from succeeded.
|
|
2760
|
+
*/
|
|
2761
|
+
private async projectSettledRun(
|
|
2762
|
+
shell: ShellBotBackendContribution,
|
|
2763
|
+
identity: { userId: string; botId: string },
|
|
2764
|
+
runId: string,
|
|
2765
|
+
): Promise<void> {
|
|
2766
|
+
const sink = this.backendEnv.SEARCH_SINK;
|
|
2767
|
+
if (!sink) return;
|
|
2768
|
+
try {
|
|
2769
|
+
const lookup = await shell.lookupRun({ schemaVersion: 1, runId });
|
|
2770
|
+
if (lookup.state !== "terminal") return;
|
|
2771
|
+
await sink.indexRows(
|
|
2772
|
+
searchRowsFromClientRunV1(identity.botId, lookup.run),
|
|
2773
|
+
);
|
|
2774
|
+
} catch {
|
|
2775
|
+
// Visible as a gap the index state and a rebuild both report; never a
|
|
2776
|
+
// reason for an admitted Turn to look as if it failed.
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
/**
|
|
2781
|
+
* One page of this Bot's rows, projected from its own stored runs.
|
|
2782
|
+
*
|
|
2783
|
+
* This is what makes the User's index disposable: every row it holds can be
|
|
2784
|
+
* read back out of the authority that owns the conversation.
|
|
2785
|
+
*/
|
|
2786
|
+
async projectSearchRows(input: unknown) {
|
|
2787
|
+
const request = decodeRpcEnvelopeV1(
|
|
2788
|
+
input,
|
|
2789
|
+
{ userId: rpcIdentifier, botId: rpcBotId },
|
|
2790
|
+
{ cursor: rpcString(512) },
|
|
2791
|
+
);
|
|
2792
|
+
const identity = {
|
|
2793
|
+
userId: request.userId as string,
|
|
2794
|
+
botId: request.botId as string,
|
|
2795
|
+
};
|
|
2796
|
+
const { shell } = await this.materialized(identity);
|
|
2797
|
+
await shell.validateIdentity(identity);
|
|
2798
|
+
const cursor = request.cursor as string | undefined;
|
|
2799
|
+
return createBotSearchRowPageV1(
|
|
2800
|
+
identity.botId,
|
|
2801
|
+
await shell.listRuns({
|
|
2802
|
+
schemaVersion: 1,
|
|
2803
|
+
...(cursor === undefined ? {} : { before: cursor }),
|
|
2804
|
+
}),
|
|
2805
|
+
);
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
/**
|
|
2809
|
+
* The Bot's invocable Skills, for the composer's `/` and `@` popover. A
|
|
2810
|
+
* read: it binds the Workspace surfaces the Turn path binds, and writes
|
|
2811
|
+
* nothing.
|
|
2812
|
+
*/
|
|
2813
|
+
/**
|
|
2814
|
+
* One durable-root file, read from object storage.
|
|
2815
|
+
*
|
|
2816
|
+
* The Workspace read path the hosted client needs: a screenshot the Bot
|
|
2817
|
+
* filed under a Package-declared root is durable content, and a card that
|
|
2818
|
+
* renders it has to be able to fetch it without reaching a Computer. It
|
|
2819
|
+
* wakes none — this is R2 and this object's own generation ledger — which is
|
|
2820
|
+
* exactly why the durable-root sync exists.
|
|
2821
|
+
*/
|
|
2822
|
+
async readWorkspaceFileV1(input: unknown): Promise<ClientWorkspaceFileV1> {
|
|
2823
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
2824
|
+
userId: rpcIdentifier,
|
|
2825
|
+
botId: rpcBotId,
|
|
2826
|
+
path: rpcDecoded(decodeWorkspacePathV1),
|
|
2827
|
+
});
|
|
2828
|
+
const identity = {
|
|
2829
|
+
userId: request.userId as string,
|
|
2830
|
+
botId: request.botId as string,
|
|
2831
|
+
};
|
|
2832
|
+
const { shell } = await this.materialized(identity);
|
|
2833
|
+
await shell.validateIdentity(identity);
|
|
2834
|
+
const files = this.backendEnv.WORKSPACE_FILES;
|
|
2835
|
+
if (!files) {
|
|
2836
|
+
return {
|
|
2837
|
+
schemaVersion: 1 as const,
|
|
2838
|
+
status: "unavailable" as const,
|
|
2839
|
+
reason: "This deployment binds no Workspace object store",
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
const outcome = await files.read(request.path as WorkspacePathV1);
|
|
2843
|
+
if (outcome.status !== "ok") {
|
|
2844
|
+
return {
|
|
2845
|
+
schemaVersion: 1 as const,
|
|
2846
|
+
status: outcome.status,
|
|
2847
|
+
reason: outcome.reason,
|
|
2848
|
+
};
|
|
2849
|
+
}
|
|
2850
|
+
return {
|
|
2851
|
+
schemaVersion: 1 as const,
|
|
2852
|
+
status: "ok" as const,
|
|
2853
|
+
contentHash: outcome.file.generation.contentHash,
|
|
2854
|
+
size: outcome.file.generation.size,
|
|
2855
|
+
bytesBase64: bytesToBase64(outcome.file.bytes),
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
async listSkills(input: unknown) {
|
|
2860
|
+
const identity = decodeBotIdentityRpcV1(input);
|
|
2861
|
+
const { shell } = await this.materialized(identity);
|
|
2862
|
+
return listSkills(shell.state, identity);
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
async stopRun(input: unknown) {
|
|
2866
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
2867
|
+
userId: rpcIdentifier,
|
|
2868
|
+
botId: rpcBotId,
|
|
2869
|
+
command: rpcDecoded(decodeClientRunStopCommandV1),
|
|
2870
|
+
});
|
|
2871
|
+
const identity = {
|
|
2872
|
+
userId: request.userId as string,
|
|
2873
|
+
botId: request.botId as string,
|
|
2874
|
+
};
|
|
2875
|
+
const { shell } = await this.materialized(identity);
|
|
2876
|
+
return stopRun(
|
|
2877
|
+
shell.state,
|
|
2878
|
+
identity,
|
|
2879
|
+
request.command as ClientRunStopCommandV1,
|
|
2880
|
+
);
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
/**
|
|
2884
|
+
* Hands every answer a voice call is owed to that account's voice object.
|
|
2885
|
+
*
|
|
2886
|
+
* The entry is written in the transaction that recorded the answer, so this
|
|
2887
|
+
* can lose a wake-up without losing the answer: an entry that was not
|
|
2888
|
+
* delivered is still there for the next settlement or this object's own
|
|
2889
|
+
* alarm, and the voice object's scheduled look-up settles it anyway. Delivery
|
|
2890
|
+
* is by request id, and the voice ledger refuses a request that is no longer
|
|
2891
|
+
* `admitted`, so a redelivery is a no-op rather than a second answer.
|
|
2892
|
+
*
|
|
2893
|
+
* An entry is deleted only after the voice object has acknowledged it. One
|
|
2894
|
+
* that names a call the account no longer has is acknowledged all the same:
|
|
2895
|
+
* the answer is in the Bot's thread, and the outbox is not where it lives.
|
|
2896
|
+
*/
|
|
2897
|
+
private async drainVoiceReplyOutbox(userId: string): Promise<void> {
|
|
2898
|
+
const namespace = (
|
|
2899
|
+
this.env as BotStateEnv & { VOICE_ASSISTANTS?: DurableObjectNamespace }
|
|
2900
|
+
).VOICE_ASSISTANTS;
|
|
2901
|
+
if (!namespace) return;
|
|
2902
|
+
const entries = await this.ctx.storage.list<unknown>({
|
|
2903
|
+
prefix: VOICE_REPLY_OUTBOX_PREFIX_V1,
|
|
2904
|
+
limit: VOICE_REPLY_DRAIN_LIMIT_V1,
|
|
2905
|
+
});
|
|
2906
|
+
if (entries.size === 0) return;
|
|
2907
|
+
// SAFETY: the binding names VoiceAssistant; this is its reviewed RPC door.
|
|
2908
|
+
const voice = namespace.get(namespace.idFromName(userId)) as unknown as {
|
|
2909
|
+
deliverVoiceReply(input: unknown): Promise<unknown>;
|
|
2910
|
+
};
|
|
2911
|
+
for (const [key, value] of entries) {
|
|
2912
|
+
const entry = isVoiceReplyOutboxEntryV1(value) ? value : undefined;
|
|
2913
|
+
if (!entry) {
|
|
2914
|
+
// Not a record this build wrote. It cannot be delivered and will never
|
|
2915
|
+
// become deliverable, so it goes rather than blocking the queue.
|
|
2916
|
+
await this.ctx.storage.delete(key);
|
|
2917
|
+
continue;
|
|
2918
|
+
}
|
|
2919
|
+
try {
|
|
2920
|
+
await voice.deliverVoiceReply({
|
|
2921
|
+
schemaVersion: 1,
|
|
2922
|
+
userId,
|
|
2923
|
+
requestId: entry.runId,
|
|
2924
|
+
});
|
|
2925
|
+
} catch {
|
|
2926
|
+
// Still durable, still owed, retried by the next settlement or alarm.
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
await this.ctx.storage.delete(key);
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
private pushDrain?: Promise<void>;
|
|
2934
|
+
/**
|
|
2935
|
+
* A commit landed while a drain was already in flight.
|
|
2936
|
+
*
|
|
2937
|
+
* The drain lists the outbox once and then awaits a delivery per entry, so an
|
|
2938
|
+
* entry written after that listing — the second `send_to_user` of one Turn is
|
|
2939
|
+
* the ordinary case — is invisible to it. Without this the second
|
|
2940
|
+
* notification waits for the next alarm. A pass that delivered anything is
|
|
2941
|
+
* followed by another, and this flag closes the last gap: a commit that
|
|
2942
|
+
* lands after the final, empty listing. Neither can spin, because a pass
|
|
2943
|
+
* that delivers nothing ends the drain and every delivered entry is deleted.
|
|
2944
|
+
*/
|
|
2945
|
+
private pushDrainAgain = false;
|
|
2946
|
+
private drainPush(): Promise<void> {
|
|
2947
|
+
if (this.pushDrain) {
|
|
2948
|
+
this.pushDrainAgain = true;
|
|
2949
|
+
return this.pushDrain;
|
|
2950
|
+
}
|
|
2951
|
+
this.pushDrain = (async () => {
|
|
2952
|
+
let delivered = 0;
|
|
2953
|
+
do {
|
|
2954
|
+
this.pushDrainAgain = false;
|
|
2955
|
+
delivered = await this.flushPush();
|
|
2956
|
+
} while (this.pushDrainAgain || delivered > 0);
|
|
2957
|
+
})()
|
|
2958
|
+
.catch(() => {
|
|
2959
|
+
console.error(JSON.stringify({ event: "push-outbox-pending" }));
|
|
2960
|
+
})
|
|
2961
|
+
.finally(() => {
|
|
2962
|
+
this.pushDrain = undefined;
|
|
2963
|
+
this.pushDrainAgain = false;
|
|
2964
|
+
});
|
|
2965
|
+
return this.pushDrain;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
/** How many outbox entries this pass delivered. Zero ends the drain. */
|
|
2969
|
+
private async flushPush(): Promise<number> {
|
|
2970
|
+
const identity = await this.ctx.storage.get<BotIdentity>(IDENTITY_KEY);
|
|
2971
|
+
if (!identity) return 0;
|
|
2972
|
+
const rpc = this.env.USER_CONFIGURATIONS.get(
|
|
2973
|
+
this.env.USER_CONFIGURATIONS.idFromName(identity.userId),
|
|
2974
|
+
) as unknown as {
|
|
2975
|
+
deliverPush(input: { userId: string; update: PushUpdate }): Promise<void>;
|
|
2976
|
+
};
|
|
2977
|
+
const entries = await this.ctx.storage.list<MessageNotice>({
|
|
2978
|
+
prefix: PUSH_OUTBOX_PREFIX,
|
|
2979
|
+
limit: PUSH_OUTBOX_DRAIN_LIMIT,
|
|
2980
|
+
});
|
|
2981
|
+
// Both are read per entry rather than once for the pass. Every delivery is
|
|
2982
|
+
// an await on another object, and a Bot muted or a conversation read during
|
|
2983
|
+
// one of them must decide the next entry: a hoisted snapshot would alert
|
|
2984
|
+
// for a Bot that is already silenced.
|
|
2985
|
+
for (const [key, notice] of entries) {
|
|
2986
|
+
const settings = await this.ctx.storage.get<BotSettingsViewV1>(
|
|
2987
|
+
BOT_CONFIGURATION_KEY,
|
|
2988
|
+
);
|
|
2989
|
+
const unread = optionalUnreadStateV1(
|
|
2990
|
+
await this.ctx.storage.get(UNREAD_STATE_KEY),
|
|
2991
|
+
);
|
|
2992
|
+
await rpc.deliverPush({
|
|
2993
|
+
userId: identity.userId,
|
|
2994
|
+
update: {
|
|
2995
|
+
botId: identity.botId,
|
|
2996
|
+
kind: "message",
|
|
2997
|
+
cursor: notice.notificationId,
|
|
2998
|
+
title: notice.title,
|
|
2999
|
+
body: notice.body,
|
|
3000
|
+
notify:
|
|
3001
|
+
notice.notify &&
|
|
3002
|
+
settings?.notifications.enabled !== false &&
|
|
3003
|
+
(unread.lastSeenCursor === undefined ||
|
|
3004
|
+
notice.notificationId > unread.lastSeenCursor),
|
|
3005
|
+
},
|
|
3006
|
+
});
|
|
3007
|
+
await this.ctx.storage.delete(key);
|
|
3008
|
+
}
|
|
3009
|
+
const read = await this.ctx.storage.get<{ cursor: string }>(PUSH_READ_KEY);
|
|
3010
|
+
if (read) {
|
|
3011
|
+
await rpc.deliverPush({
|
|
3012
|
+
userId: identity.userId,
|
|
3013
|
+
update: { botId: identity.botId, kind: "read", cursor: read.cursor },
|
|
3014
|
+
});
|
|
3015
|
+
await this.ctx.storage.transaction(async (tx) => {
|
|
3016
|
+
if (
|
|
3017
|
+
(await tx.get<{ cursor: string }>(PUSH_READ_KEY))?.cursor ===
|
|
3018
|
+
read.cursor
|
|
3019
|
+
)
|
|
3020
|
+
await tx.delete(PUSH_READ_KEY);
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
3023
|
+
return entries.size;
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
/** The Bot's unread projection; the Bot Durable Object derives the count. */
|
|
3027
|
+
async readUnread(input: unknown) {
|
|
3028
|
+
const identity = decodeBotIdentityRpcV1(input);
|
|
3029
|
+
const { shell } = await this.materialized(identity);
|
|
3030
|
+
return readUnread(shell.state, identity);
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
/** `bot/mark-read` / `bot/mark-unread`, idempotent on the command id. */
|
|
3034
|
+
async executeUnreadCommand(input: unknown) {
|
|
3035
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3036
|
+
userId: rpcIdentifier,
|
|
3037
|
+
botId: rpcBotId,
|
|
3038
|
+
command: rpcDecoded(decodeBotUnreadCommandV1),
|
|
3039
|
+
});
|
|
3040
|
+
const identity = {
|
|
3041
|
+
userId: request.userId as string,
|
|
3042
|
+
botId: request.botId as string,
|
|
3043
|
+
};
|
|
3044
|
+
const { shell } = await this.materialized(identity);
|
|
3045
|
+
const receipt = await executeUnreadCommand(
|
|
3046
|
+
shell.state,
|
|
3047
|
+
identity,
|
|
3048
|
+
request.command as BotUnreadCommandV1,
|
|
3049
|
+
);
|
|
3050
|
+
this.ctx.waitUntil(this.drainPush());
|
|
3051
|
+
return receipt;
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
/** The Bot's approvals, newest first, pending and decided alike. */
|
|
3055
|
+
async listApprovals(input: unknown) {
|
|
3056
|
+
const identity = decodeBotIdentityRpcV1(input);
|
|
3057
|
+
const { shell } = await this.materialized(identity);
|
|
3058
|
+
return listApprovals(shell.state, identity);
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
/**
|
|
3062
|
+
* One decision on one approval. First write wins: a replay answers with the
|
|
3063
|
+
* decision already recorded rather than overwriting it.
|
|
3064
|
+
*/
|
|
3065
|
+
async decideApproval(input: unknown) {
|
|
3066
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3067
|
+
userId: rpcIdentifier,
|
|
3068
|
+
botId: rpcBotId,
|
|
3069
|
+
approvalId: rpcIdentifier,
|
|
3070
|
+
command: rpcDecoded(decodeApprovalDecisionCommandV1),
|
|
3071
|
+
});
|
|
3072
|
+
const identity = {
|
|
3073
|
+
userId: request.userId as string,
|
|
3074
|
+
botId: request.botId as string,
|
|
3075
|
+
};
|
|
3076
|
+
const { shell } = await this.materialized(identity);
|
|
3077
|
+
return decideApproval(
|
|
3078
|
+
shell.state,
|
|
3079
|
+
identity,
|
|
3080
|
+
request.approvalId as string,
|
|
3081
|
+
request.command as ApprovalDecisionCommandV1,
|
|
3082
|
+
);
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
/** The Bot's Cards, newest first, tombstoned ones included (ADR 0030). */
|
|
3086
|
+
async listCards(input: unknown) {
|
|
3087
|
+
const identity = decodeBotIdentityRpcV1(input);
|
|
3088
|
+
const { shell } = await this.materialized(identity);
|
|
3089
|
+
return listCards(shell.state, identity);
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
/**
|
|
3093
|
+
* One Card by its surface id. The listing is bounded by bytes, so this is
|
|
3094
|
+
* how a client reads a surface that fell past the budget.
|
|
3095
|
+
*/
|
|
3096
|
+
async readCard(input: unknown) {
|
|
3097
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3098
|
+
userId: rpcIdentifier,
|
|
3099
|
+
botId: rpcBotId,
|
|
3100
|
+
surfaceId: rpcDecoded(decodeCardSurfaceIdV1),
|
|
3101
|
+
});
|
|
3102
|
+
const identity = {
|
|
3103
|
+
userId: request.userId as string,
|
|
3104
|
+
botId: request.botId as string,
|
|
3105
|
+
};
|
|
3106
|
+
const { shell } = await this.materialized(identity);
|
|
3107
|
+
return readCardView(shell.state, identity, request.surfaceId as string);
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
/**
|
|
3111
|
+
* One action on one Card. The kernel decides what the action's name means;
|
|
3112
|
+
* a stale revision is refused rather than applied to a surface the person
|
|
3113
|
+
* was not looking at.
|
|
3114
|
+
*/
|
|
3115
|
+
async cardAction(input: unknown) {
|
|
3116
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3117
|
+
userId: rpcIdentifier,
|
|
3118
|
+
botId: rpcBotId,
|
|
3119
|
+
command: rpcDecoded(decodeCardActionCommandV1),
|
|
3120
|
+
});
|
|
3121
|
+
const identity = {
|
|
3122
|
+
userId: request.userId as string,
|
|
3123
|
+
botId: request.botId as string,
|
|
3124
|
+
};
|
|
3125
|
+
const { shell } = await this.materialized(identity);
|
|
3126
|
+
const receipt = await cardAction(
|
|
3127
|
+
shell.state,
|
|
3128
|
+
identity,
|
|
3129
|
+
request.command as CardActionCommandV1,
|
|
3130
|
+
);
|
|
3131
|
+
return receipt;
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
/**
|
|
3135
|
+
* A value a person typed on one of this Bot's secret-request cards. It is
|
|
3136
|
+
* handed straight to the User Durable Object to seal and is never written
|
|
3137
|
+
* here; what this object keeps is that the request was answered.
|
|
3138
|
+
*/
|
|
3139
|
+
async submitSecret(input: unknown) {
|
|
3140
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3141
|
+
userId: rpcIdentifier,
|
|
3142
|
+
botId: rpcBotId,
|
|
3143
|
+
requestId: rpcPattern(/^secret-request-[0-9a-f]{32}$/, 128),
|
|
3144
|
+
command: rpcDecoded(decodeSecretSubmitCommandV1),
|
|
3145
|
+
});
|
|
3146
|
+
const identity = {
|
|
3147
|
+
userId: request.userId as string,
|
|
3148
|
+
botId: request.botId as string,
|
|
3149
|
+
};
|
|
3150
|
+
const { shell } = await this.materialized(identity);
|
|
3151
|
+
return submitSecretV1(
|
|
3152
|
+
shell.state,
|
|
3153
|
+
identity,
|
|
3154
|
+
request.requestId as string,
|
|
3155
|
+
request.command as SecretSubmitCommandV1,
|
|
3156
|
+
);
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
async listNotifications(input: unknown) {
|
|
3160
|
+
const identity = decodeBotIdentityRpcV1(input);
|
|
3161
|
+
const { shell } = await this.materialized(identity);
|
|
3162
|
+
await shell.validateIdentity(identity);
|
|
3163
|
+
return listNotifications(shell.state);
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
async acknowledgeNotification(input: unknown) {
|
|
3167
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3168
|
+
userId: rpcIdentifier,
|
|
3169
|
+
botId: rpcBotId,
|
|
3170
|
+
notificationId: rpcIdentifier,
|
|
3171
|
+
});
|
|
3172
|
+
const identity = {
|
|
3173
|
+
userId: request.userId as string,
|
|
3174
|
+
botId: request.botId as string,
|
|
3175
|
+
};
|
|
3176
|
+
const { shell } = await this.materialized(identity);
|
|
3177
|
+
await shell.validateIdentity(identity);
|
|
3178
|
+
return acknowledgeNotification(
|
|
3179
|
+
shell.state,
|
|
3180
|
+
request.notificationId as string,
|
|
3181
|
+
);
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
/**
|
|
3185
|
+
* The Bot's subagent tasks. Bot-scoped, so it proves directory membership the
|
|
3186
|
+
* same way the other Bot RPCs do: a Bot that is not this User's is not found.
|
|
3187
|
+
*
|
|
3188
|
+
* This is the *parent* object's answer. A Subagent Durable Object has no
|
|
3189
|
+
* route of its own and holds no task list: it holds one Session, and every
|
|
3190
|
+
* authority stays here.
|
|
3191
|
+
*/
|
|
3192
|
+
async listTasks(input: unknown) {
|
|
3193
|
+
const identity = decodeBotIdentityRpcV1(input);
|
|
3194
|
+
const { shell } = await this.materialized(identity);
|
|
3195
|
+
await shell.validateIdentity(identity);
|
|
3196
|
+
return listTasks(shell.state, identity);
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
/** One task, by id. The parent object's answer; a child holds no list. */
|
|
3200
|
+
async readTask(input: unknown) {
|
|
3201
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3202
|
+
userId: rpcIdentifier,
|
|
3203
|
+
botId: rpcBotId,
|
|
3204
|
+
taskId: rpcString(128),
|
|
3205
|
+
});
|
|
3206
|
+
const identity = {
|
|
3207
|
+
userId: request.userId as string,
|
|
3208
|
+
botId: request.botId as string,
|
|
3209
|
+
};
|
|
3210
|
+
const { shell } = await this.materialized(identity);
|
|
3211
|
+
return readTask(shell.state, identity, request.taskId as string);
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
/**
|
|
3215
|
+
* The User's own cancellation of one task.
|
|
3216
|
+
*
|
|
3217
|
+
* The same durable act `task_stop` performs, through a second authenticated
|
|
3218
|
+
* door: explicit, authenticated, and terminal — never a second mechanism.
|
|
3219
|
+
*/
|
|
3220
|
+
async stopTask(input: unknown) {
|
|
3221
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3222
|
+
userId: rpcIdentifier,
|
|
3223
|
+
botId: rpcBotId,
|
|
3224
|
+
taskId: rpcString(128),
|
|
3225
|
+
});
|
|
3226
|
+
const identity = {
|
|
3227
|
+
userId: request.userId as string,
|
|
3228
|
+
botId: request.botId as string,
|
|
3229
|
+
};
|
|
3230
|
+
const { shell } = await this.materialized(identity);
|
|
3231
|
+
return stopTaskForUser(shell.state, identity, request.taskId as string);
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
/** The Subagent Durable Object's cancellation door. */
|
|
3235
|
+
async stopSubagentTask(input: unknown) {
|
|
3236
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3237
|
+
userId: rpcIdentifier,
|
|
3238
|
+
botId: rpcBotId,
|
|
3239
|
+
taskId: rpcString(128),
|
|
3240
|
+
});
|
|
3241
|
+
const identity = {
|
|
3242
|
+
userId: request.userId as string,
|
|
3243
|
+
botId: request.botId as string,
|
|
3244
|
+
};
|
|
3245
|
+
const { shell } = await this.materialized(identity);
|
|
3246
|
+
return stopSubagentTask(shell.state, identity, request.taskId as string);
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
/**
|
|
3250
|
+
* The Subagent Durable Object's door.
|
|
3251
|
+
*
|
|
3252
|
+
* It records the task and arms its own alarm; the Turn runs on that alarm.
|
|
3253
|
+
* The parent is still inside the Turn that dispatched when this returns, so
|
|
3254
|
+
* anything longer than a write here would block it.
|
|
3255
|
+
*/
|
|
3256
|
+
async runTask(input: unknown) {
|
|
3257
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3258
|
+
userId: rpcIdentifier,
|
|
3259
|
+
botId: rpcBotId,
|
|
3260
|
+
request: rpcDecoded(decodeSubagentRunTaskRequestV1),
|
|
3261
|
+
});
|
|
3262
|
+
const identity = {
|
|
3263
|
+
userId: request.userId as string,
|
|
3264
|
+
botId: request.botId as string,
|
|
3265
|
+
};
|
|
3266
|
+
const { shell } = await this.materialized(identity);
|
|
3267
|
+
return acceptSubagentTask(
|
|
3268
|
+
shell.state,
|
|
3269
|
+
identity,
|
|
3270
|
+
request.request as SubagentRunTaskRequestV1,
|
|
3271
|
+
);
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
/** What a Subagent Durable Object holds for one task, for reconciliation. */
|
|
3275
|
+
async readSubagentTask(input: unknown) {
|
|
3276
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3277
|
+
userId: rpcIdentifier,
|
|
3278
|
+
botId: rpcBotId,
|
|
3279
|
+
taskId: rpcString(128),
|
|
3280
|
+
});
|
|
3281
|
+
const identity = {
|
|
3282
|
+
userId: request.userId as string,
|
|
3283
|
+
botId: request.botId as string,
|
|
3284
|
+
};
|
|
3285
|
+
const { shell } = await this.materialized(identity);
|
|
3286
|
+
return readSubagentTaskContext(shell.state, request.taskId as string);
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
/**
|
|
3290
|
+
* The messages a parent has queued for one of its tasks, claimed by the
|
|
3291
|
+
* child that is running it.
|
|
3292
|
+
*
|
|
3293
|
+
* The claim marks what it hands over in the parent's own transaction, so a
|
|
3294
|
+
* child that retries a step reads the marks back rather than the message.
|
|
3295
|
+
*/
|
|
3296
|
+
async claimTaskMessages(input: unknown) {
|
|
3297
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3298
|
+
userId: rpcIdentifier,
|
|
3299
|
+
botId: rpcBotId,
|
|
3300
|
+
taskId: rpcString(128),
|
|
3301
|
+
});
|
|
3302
|
+
const identity = {
|
|
3303
|
+
userId: request.userId as string,
|
|
3304
|
+
botId: request.botId as string,
|
|
3305
|
+
};
|
|
3306
|
+
const { shell } = await this.materialized(identity);
|
|
3307
|
+
return claimTaskMessages(shell.state, identity, request.taskId as string);
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
/** One terminal task outcome, recorded on the parent. Idempotent per task. */
|
|
3311
|
+
async settleTask(input: unknown) {
|
|
3312
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3313
|
+
userId: rpcIdentifier,
|
|
3314
|
+
botId: rpcBotId,
|
|
3315
|
+
taskId: rpcString(128),
|
|
3316
|
+
outcome: rpcDecoded(decodeTaskOutcomeV1),
|
|
3317
|
+
});
|
|
3318
|
+
const identity = {
|
|
3319
|
+
userId: request.userId as string,
|
|
3320
|
+
botId: request.botId as string,
|
|
3321
|
+
};
|
|
3322
|
+
const { shell } = await this.materialized(identity);
|
|
3323
|
+
return settleTask(
|
|
3324
|
+
shell.state,
|
|
3325
|
+
identity,
|
|
3326
|
+
request.taskId as string,
|
|
3327
|
+
request.outcome as TaskOutcomeV1,
|
|
3328
|
+
);
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
/**
|
|
3332
|
+
* The Bot's Routines. Bot-scoped, so it proves directory membership the same
|
|
3333
|
+
* way the other Bot RPCs do: a Bot that is not this User's is not found.
|
|
3334
|
+
*/
|
|
3335
|
+
async listRoutines(input: unknown) {
|
|
3336
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3337
|
+
userId: rpcIdentifier,
|
|
3338
|
+
botId: rpcBotId,
|
|
3339
|
+
});
|
|
3340
|
+
const identity = {
|
|
3341
|
+
userId: request.userId as string,
|
|
3342
|
+
botId: request.botId as string,
|
|
3343
|
+
};
|
|
3344
|
+
const { shell } = await this.materialized(identity);
|
|
3345
|
+
await shell.validateIdentity(identity);
|
|
3346
|
+
await this.syncRoutineTimezone(identity, shell);
|
|
3347
|
+
return listRoutines(shell.state, identity);
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
/**
|
|
3351
|
+
* The Routines list and the inbox as one RPC. The document read is this,
|
|
3352
|
+
* so the Worker is not addressing the object twice for one panel.
|
|
3353
|
+
*/
|
|
3354
|
+
async readRoutinesFrame(input: unknown) {
|
|
3355
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3356
|
+
userId: rpcIdentifier,
|
|
3357
|
+
botId: rpcBotId,
|
|
3358
|
+
});
|
|
3359
|
+
const identity = {
|
|
3360
|
+
userId: request.userId as string,
|
|
3361
|
+
botId: request.botId as string,
|
|
3362
|
+
};
|
|
3363
|
+
const { shell } = await this.materialized(identity);
|
|
3364
|
+
await shell.validateIdentity(identity);
|
|
3365
|
+
await this.syncRoutineTimezone(identity, shell);
|
|
3366
|
+
return readRoutinesFrame(shell.state, identity);
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
/** One Routine command, applied durably with the User recorded as writer. */
|
|
3370
|
+
async executeRoutineCommand(input: unknown) {
|
|
3371
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3372
|
+
userId: rpcIdentifier,
|
|
3373
|
+
botId: rpcBotId,
|
|
3374
|
+
command: rpcDecoded(decodeRoutineCommandV1),
|
|
3375
|
+
});
|
|
3376
|
+
const identity = {
|
|
3377
|
+
userId: request.userId as string,
|
|
3378
|
+
botId: request.botId as string,
|
|
3379
|
+
};
|
|
3380
|
+
const { shell } = await this.materialized(identity);
|
|
3381
|
+
await shell.validateIdentity(identity);
|
|
3382
|
+
await this.syncRoutineTimezone(identity, shell);
|
|
3383
|
+
return executeRoutineCommand(
|
|
3384
|
+
shell.state,
|
|
3385
|
+
identity,
|
|
3386
|
+
request.command as RoutineCommandV1,
|
|
3387
|
+
{ kind: "user" },
|
|
3388
|
+
connectionTriggersFromUserV1(userConfigurationV1(shell.state, identity)),
|
|
3389
|
+
pluginTriggerIndexFromUserV1(userConfigurationV1(shell.state, identity)),
|
|
3390
|
+
);
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
/**
|
|
3394
|
+
* One connected-app event, after the User object resolved the instance to
|
|
3395
|
+
* this Bot and this Routine. Directory membership is not proved here: the
|
|
3396
|
+
* caller is the provider door, and the instance mapping is the credential.
|
|
3397
|
+
*/
|
|
3398
|
+
async deliverConnectEvent(input: unknown) {
|
|
3399
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3400
|
+
userId: rpcIdentifier,
|
|
3401
|
+
botId: rpcBotId,
|
|
3402
|
+
routineId: rpcIdentifier,
|
|
3403
|
+
eventId: rpcIdentifier,
|
|
3404
|
+
payload: (value) => value,
|
|
3405
|
+
});
|
|
3406
|
+
const identity = {
|
|
3407
|
+
userId: request.userId as string,
|
|
3408
|
+
botId: request.botId as string,
|
|
3409
|
+
};
|
|
3410
|
+
const { shell } = await this.materialized(identity);
|
|
3411
|
+
return deliverConnectEvent(shell.state, {
|
|
3412
|
+
routineId: request.routineId as string,
|
|
3413
|
+
eventId: request.eventId as string,
|
|
3414
|
+
payload: request.payload,
|
|
3415
|
+
});
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3418
|
+
/**
|
|
3419
|
+
* One device module's event (ADR 0037), after the User object admitted it
|
|
3420
|
+
* against the active generation and found this Routine in its index. Like
|
|
3421
|
+
* a connected-app event, the User object's routing is the credential.
|
|
3422
|
+
*/
|
|
3423
|
+
async deliverPluginModuleEvent(input: unknown) {
|
|
3424
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3425
|
+
userId: rpcIdentifier,
|
|
3426
|
+
botId: rpcBotId,
|
|
3427
|
+
routineId: rpcIdentifier,
|
|
3428
|
+
deliveryId: rpcPattern(/^[0-9a-f]{64}$/, 64),
|
|
3429
|
+
pluginId: rpcPattern(/^[a-z][a-z0-9-]{0,63}$/, 64),
|
|
3430
|
+
trigger: rpcPattern(/^[a-z][a-z0-9_-]{0,63}$/, 64),
|
|
3431
|
+
body: rpcString(ROUTINE_HOOK_BODY_MAX_BYTES),
|
|
3432
|
+
source: rpcObject({
|
|
3433
|
+
kind: rpcEnum(["device-module"] as const),
|
|
3434
|
+
moduleId: rpcString(64),
|
|
3435
|
+
machineId: rpcString(200),
|
|
3436
|
+
key: rpcString(256),
|
|
3437
|
+
}),
|
|
3438
|
+
});
|
|
3439
|
+
const identity = {
|
|
3440
|
+
userId: request.userId as string,
|
|
3441
|
+
botId: request.botId as string,
|
|
3442
|
+
};
|
|
3443
|
+
const { shell } = await this.materialized(identity);
|
|
3444
|
+
return deliverRoutineModuleEvent(shell.state, {
|
|
3445
|
+
routineId: request.routineId as string,
|
|
3446
|
+
deliveryId: request.deliveryId as string,
|
|
3447
|
+
pluginId: request.pluginId as string,
|
|
3448
|
+
trigger: request.trigger as string,
|
|
3449
|
+
body: request.body as string,
|
|
3450
|
+
source: request.source as RoutineModuleEventDeliveryV1["source"],
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
/** What the User object rebuilds its Plugin-trigger index from. */
|
|
3455
|
+
async listPluginTriggerRoutines(input: unknown) {
|
|
3456
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3457
|
+
userId: rpcIdentifier,
|
|
3458
|
+
botId: rpcBotId,
|
|
3459
|
+
});
|
|
3460
|
+
const identity = {
|
|
3461
|
+
userId: request.userId as string,
|
|
3462
|
+
botId: request.botId as string,
|
|
3463
|
+
};
|
|
3464
|
+
const { shell } = await this.materialized(identity);
|
|
3465
|
+
return listPluginTriggerRoutines(shell.state);
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
/** Adopt a Profile timezone pushed by this User's authoritative object. */
|
|
3469
|
+
async refreshRoutineTimezone(input: unknown) {
|
|
3470
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3471
|
+
userId: rpcIdentifier,
|
|
3472
|
+
botId: rpcBotId,
|
|
3473
|
+
timezone: rpcString(64),
|
|
3474
|
+
revision: rpcInteger({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
|
|
3475
|
+
});
|
|
3476
|
+
const identity = {
|
|
3477
|
+
userId: request.userId as string,
|
|
3478
|
+
botId: request.botId as string,
|
|
3479
|
+
};
|
|
3480
|
+
const { shell } = await this.materialized(identity);
|
|
3481
|
+
await shell.validateIdentity(identity);
|
|
3482
|
+
await projectRoutineAccountTimezoneV1(
|
|
3483
|
+
shell.state,
|
|
3484
|
+
request.timezone as string,
|
|
3485
|
+
request.revision as number,
|
|
3486
|
+
);
|
|
3487
|
+
return { schemaVersion: 1 as const, status: "applied" as const };
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
/**
|
|
3491
|
+
* One webhook delivery, forwarded by the gateway after it verified the key's
|
|
3492
|
+
* signature. The Bot re-checks the key against its own durable record: the
|
|
3493
|
+
* edge proved the token was minted by this deployment, not that it is still
|
|
3494
|
+
* this Routine's key.
|
|
3495
|
+
*
|
|
3496
|
+
* Directory membership is deliberately *not* proved here. The caller is an
|
|
3497
|
+
* external system with no session; the key is the whole credential, and it
|
|
3498
|
+
* names the User the token was minted for.
|
|
3499
|
+
*/
|
|
3500
|
+
async deliverRoutineHook(input: unknown) {
|
|
3501
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3502
|
+
userId: rpcIdentifier,
|
|
3503
|
+
botId: rpcBotId,
|
|
3504
|
+
delivery: rpcDecoded(decodeRoutineHookDeliveryV1),
|
|
3505
|
+
});
|
|
3506
|
+
const identity = {
|
|
3507
|
+
userId: request.userId as string,
|
|
3508
|
+
botId: request.botId as string,
|
|
3509
|
+
};
|
|
3510
|
+
const { shell } = await this.materialized(identity);
|
|
3511
|
+
return deliverRoutineHook(
|
|
3512
|
+
shell.state,
|
|
3513
|
+
request.delivery as RoutineHookDeliveryV1,
|
|
3514
|
+
);
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
/**
|
|
3518
|
+
* One finished machine command, handed over by the Worker that answered the
|
|
3519
|
+
* machine.
|
|
3520
|
+
*
|
|
3521
|
+
* It names the Bot the command record named — a Bot cannot be told about a
|
|
3522
|
+
* command it never asked for, because it is the command that carries the
|
|
3523
|
+
* `botId`.
|
|
3524
|
+
*/
|
|
3525
|
+
async deliverMachineResult(input: unknown) {
|
|
3526
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3527
|
+
userId: rpcIdentifier,
|
|
3528
|
+
botId: rpcBotId,
|
|
3529
|
+
delivery: rpcDecoded(decodeMachineResultDeliveryV1),
|
|
3530
|
+
});
|
|
3531
|
+
const identity = {
|
|
3532
|
+
userId: request.userId as string,
|
|
3533
|
+
botId: request.botId as string,
|
|
3534
|
+
};
|
|
3535
|
+
const { shell } = await this.materialized(identity);
|
|
3536
|
+
return deliverMachineResult(
|
|
3537
|
+
shell.state,
|
|
3538
|
+
request.delivery as MachineResultDeliveryV1,
|
|
3539
|
+
);
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
/**
|
|
3543
|
+
* One request a `computer_exec` call's CLI made to a connected app, which
|
|
3544
|
+
* the Worker has already verified this object's token for. Answered by the
|
|
3545
|
+
* call's own handler while it runs, and refused once it has finished.
|
|
3546
|
+
*/
|
|
3547
|
+
async answerComputerEgress(input: unknown) {
|
|
3548
|
+
const request = decodeComputerEgressAnswerInputV1(input);
|
|
3549
|
+
return answerComputerEgressV1({
|
|
3550
|
+
object: request.object,
|
|
3551
|
+
nonce: request.nonce,
|
|
3552
|
+
request: request.request,
|
|
3553
|
+
});
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
/** One Routine's bounded run log, newest first. */
|
|
3557
|
+
async listRoutineRuns(input: unknown) {
|
|
3558
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3559
|
+
userId: rpcIdentifier,
|
|
3560
|
+
botId: rpcBotId,
|
|
3561
|
+
routineId: rpcIdentifier,
|
|
3562
|
+
});
|
|
3563
|
+
const identity = {
|
|
3564
|
+
userId: request.userId as string,
|
|
3565
|
+
botId: request.botId as string,
|
|
3566
|
+
};
|
|
3567
|
+
const { shell } = await this.materialized(identity);
|
|
3568
|
+
await shell.validateIdentity(identity);
|
|
3569
|
+
return listRoutineRuns(shell.state, identity, request.routineId as string);
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
/**
|
|
3573
|
+
* One automation run, read-only. It is not in the visible transcript and
|
|
3574
|
+
* never will be; the Routine's run log is the only door to it.
|
|
3575
|
+
*/
|
|
3576
|
+
async readRoutineRun(input: unknown) {
|
|
3577
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3578
|
+
userId: rpcIdentifier,
|
|
3579
|
+
botId: rpcBotId,
|
|
3580
|
+
routineId: rpcIdentifier,
|
|
3581
|
+
runId: rpcIdentifier,
|
|
3582
|
+
});
|
|
3583
|
+
const identity = {
|
|
3584
|
+
userId: request.userId as string,
|
|
3585
|
+
botId: request.botId as string,
|
|
3586
|
+
};
|
|
3587
|
+
const { shell } = await this.materialized(identity);
|
|
3588
|
+
await shell.validateIdentity(identity);
|
|
3589
|
+
return readRoutineRun(
|
|
3590
|
+
shell.state,
|
|
3591
|
+
identity,
|
|
3592
|
+
request.routineId as string,
|
|
3593
|
+
request.runId as string,
|
|
3594
|
+
);
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
/** The completion inbox: what the Bot's firings left for its User. */
|
|
3598
|
+
async listRoutineInbox(input: unknown) {
|
|
3599
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3600
|
+
userId: rpcIdentifier,
|
|
3601
|
+
botId: rpcBotId,
|
|
3602
|
+
});
|
|
3603
|
+
const identity = {
|
|
3604
|
+
userId: request.userId as string,
|
|
3605
|
+
botId: request.botId as string,
|
|
3606
|
+
};
|
|
3607
|
+
const { shell } = await this.materialized(identity);
|
|
3608
|
+
await shell.validateIdentity(identity);
|
|
3609
|
+
return listRoutineInbox(shell.state, identity);
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
/** Acknowledging inbox entries; an explicit command, never a read. */
|
|
3613
|
+
async executeRoutineInboxCommand(input: unknown) {
|
|
3614
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3615
|
+
userId: rpcIdentifier,
|
|
3616
|
+
botId: rpcBotId,
|
|
3617
|
+
command: rpcDecoded(decodeRoutineInboxCommandV1),
|
|
3618
|
+
});
|
|
3619
|
+
const identity = {
|
|
3620
|
+
userId: request.userId as string,
|
|
3621
|
+
botId: request.botId as string,
|
|
3622
|
+
};
|
|
3623
|
+
const { shell } = await this.materialized(identity);
|
|
3624
|
+
await shell.validateIdentity(identity);
|
|
3625
|
+
return executeRoutineInboxCommand(
|
|
3626
|
+
shell.state,
|
|
3627
|
+
identity,
|
|
3628
|
+
request.command as RoutineInboxCommandV1,
|
|
3629
|
+
);
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
/**
|
|
3633
|
+
* The Bot's durable Composition generations, newest first. Bot-scoped, so it
|
|
3634
|
+
* proves directory membership the same way the other Bot RPCs do.
|
|
3635
|
+
*/
|
|
3636
|
+
async listCompositionGenerations(input: unknown) {
|
|
3637
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3638
|
+
userId: rpcIdentifier,
|
|
3639
|
+
botId: rpcBotId,
|
|
3640
|
+
query: rpcObject(
|
|
3641
|
+
{
|
|
3642
|
+
limit: rpcInteger({
|
|
3643
|
+
minimum: 1,
|
|
3644
|
+
maximum: MAX_COMPOSITION_GENERATION_PAGE_V1,
|
|
3645
|
+
}),
|
|
3646
|
+
},
|
|
3647
|
+
{ cursor: rpcString(512) },
|
|
3648
|
+
),
|
|
3649
|
+
});
|
|
3650
|
+
const identity = {
|
|
3651
|
+
userId: request.userId as string,
|
|
3652
|
+
botId: request.botId as string,
|
|
3653
|
+
};
|
|
3654
|
+
const { shell } = await this.materialized(identity);
|
|
3655
|
+
await shell.validateIdentity(identity);
|
|
3656
|
+
return listCompositionGenerations(
|
|
3657
|
+
shell.state,
|
|
3658
|
+
identity,
|
|
3659
|
+
request.query as { limit: number; cursor?: string },
|
|
3660
|
+
);
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
/**
|
|
3664
|
+
* One generation, including the recorded source of each isolate member once
|
|
3665
|
+
* authoring records exist (plan Step 5); the member list until then.
|
|
3666
|
+
*/
|
|
3667
|
+
async getCompositionGeneration(input: unknown) {
|
|
3668
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3669
|
+
userId: rpcIdentifier,
|
|
3670
|
+
botId: rpcBotId,
|
|
3671
|
+
generationId: rpcDecoded(decodeCompositionGenerationIdV1),
|
|
3672
|
+
});
|
|
3673
|
+
const identity = {
|
|
3674
|
+
userId: request.userId as string,
|
|
3675
|
+
botId: request.botId as string,
|
|
3676
|
+
};
|
|
3677
|
+
const { shell } = await this.materialized(identity);
|
|
3678
|
+
await shell.validateIdentity(identity);
|
|
3679
|
+
return getCompositionGeneration(
|
|
3680
|
+
shell.state,
|
|
3681
|
+
identity,
|
|
3682
|
+
request.generationId as string,
|
|
3683
|
+
);
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
/** Reverting records a new pending generation; it never mutates a record. */
|
|
3687
|
+
async revertComposition(input: unknown) {
|
|
3688
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3689
|
+
userId: rpcIdentifier,
|
|
3690
|
+
botId: rpcBotId,
|
|
3691
|
+
command: rpcDecoded(decodeRevertCompositionCommandV1),
|
|
3692
|
+
});
|
|
3693
|
+
const identity = {
|
|
3694
|
+
userId: request.userId as string,
|
|
3695
|
+
botId: request.botId as string,
|
|
3696
|
+
};
|
|
3697
|
+
const command = request.command as RevertCompositionCommandV1;
|
|
3698
|
+
if (command.botId !== identity.botId) {
|
|
3699
|
+
throw new Error("Composition revert command does not match its Bot");
|
|
3700
|
+
}
|
|
3701
|
+
const { shell } = await this.materialized(identity);
|
|
3702
|
+
await shell.validateIdentity(identity);
|
|
3703
|
+
return revertComposition(shell.state, identity, command);
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
/**
|
|
3707
|
+
* Canonical Memory for the selected Bot. Voice uses this for prepared core,
|
|
3708
|
+
* blocking tools and standing preferences. Membership is filled here.
|
|
3709
|
+
*/
|
|
3710
|
+
async operateMemory(input: unknown): Promise<unknown> {
|
|
3711
|
+
if (!input || typeof input !== "object") {
|
|
3712
|
+
throw new Error("memory request is invalid");
|
|
3713
|
+
}
|
|
3714
|
+
const request = input as {
|
|
3715
|
+
schemaVersion?: number;
|
|
3716
|
+
userId?: string;
|
|
3717
|
+
botId?: string;
|
|
3718
|
+
action?: string;
|
|
3719
|
+
request?: unknown;
|
|
3720
|
+
};
|
|
3721
|
+
if (
|
|
3722
|
+
request.schemaVersion !== 1 ||
|
|
3723
|
+
typeof request.userId !== "string" ||
|
|
3724
|
+
typeof request.botId !== "string" ||
|
|
3725
|
+
typeof request.action !== "string"
|
|
3726
|
+
) {
|
|
3727
|
+
throw new Error("memory request is invalid");
|
|
3728
|
+
}
|
|
3729
|
+
const identity = { userId: request.userId, botId: request.botId };
|
|
3730
|
+
const { shell } = await this.materialized(identity);
|
|
3731
|
+
await shell.validateIdentity(identity);
|
|
3732
|
+
const records = this.backendEnv.MEMORY_RECORDS;
|
|
3733
|
+
if (!records) throw new Error("Memory is unavailable");
|
|
3734
|
+
const body = { ...((request.request ?? {}) as Record<string, unknown>) };
|
|
3735
|
+
const groups = this.backendEnv.MEMORY_GROUPS;
|
|
3736
|
+
const joined = groups ? await groups.memberOf() : [];
|
|
3737
|
+
const claimed = body.authority;
|
|
3738
|
+
if (claimed && typeof claimed === "object" && !Array.isArray(claimed)) {
|
|
3739
|
+
body.authority = {
|
|
3740
|
+
...(claimed as Record<string, unknown>),
|
|
3741
|
+
userId: identity.userId,
|
|
3742
|
+
botId: identity.botId,
|
|
3743
|
+
actor:
|
|
3744
|
+
(claimed as { actor?: unknown }).actor === "user" ? "user" : "bot",
|
|
3745
|
+
joinedGroupChatIds: joined,
|
|
3746
|
+
membershipRevision: memoryMembershipRevisionV1(joined),
|
|
3747
|
+
};
|
|
3748
|
+
}
|
|
3749
|
+
switch (request.action) {
|
|
3750
|
+
case "write":
|
|
3751
|
+
return records.write(body as never);
|
|
3752
|
+
case "forget":
|
|
3753
|
+
return records.forget(body as never);
|
|
3754
|
+
case "recall":
|
|
3755
|
+
return records.recall(body as never);
|
|
3756
|
+
case "expand":
|
|
3757
|
+
return records.expand(body as never);
|
|
3758
|
+
case "browse":
|
|
3759
|
+
return records.browse(body as never);
|
|
3760
|
+
case "preparedCore":
|
|
3761
|
+
return records.preparedCore(body as never);
|
|
3762
|
+
case "capture":
|
|
3763
|
+
return records.captureExtraction(body as never);
|
|
3764
|
+
default:
|
|
3765
|
+
throw new Error(`unknown Memory action ${request.action}`);
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
async readVoiceContext(input: unknown) {
|
|
3770
|
+
if (!input || typeof input !== "object") {
|
|
3771
|
+
throw new Error("voice context request is invalid");
|
|
3772
|
+
}
|
|
3773
|
+
const request = input as {
|
|
3774
|
+
schemaVersion?: number;
|
|
3775
|
+
userId?: string;
|
|
3776
|
+
botId?: string;
|
|
3777
|
+
limit?: number;
|
|
3778
|
+
};
|
|
3779
|
+
if (
|
|
3780
|
+
request.schemaVersion !== 1 ||
|
|
3781
|
+
typeof request.userId !== "string" ||
|
|
3782
|
+
typeof request.botId !== "string"
|
|
3783
|
+
) {
|
|
3784
|
+
throw new Error("voice context request is invalid");
|
|
3785
|
+
}
|
|
3786
|
+
const identity = { userId: request.userId, botId: request.botId };
|
|
3787
|
+
const { shell } = await this.materialized(identity);
|
|
3788
|
+
await shell.validateIdentity(identity);
|
|
3789
|
+
return shell.readVoiceContext(
|
|
3790
|
+
typeof request.limit === "number" ? request.limit : 6,
|
|
3791
|
+
);
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
async listRuns(input: unknown) {
|
|
3795
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3796
|
+
userId: rpcIdentifier,
|
|
3797
|
+
botId: rpcBotId,
|
|
3798
|
+
query: rpcDecoded(decodeClientRunListQueryV1),
|
|
3799
|
+
});
|
|
3800
|
+
const identity = {
|
|
3801
|
+
userId: request.userId as string,
|
|
3802
|
+
botId: request.botId as string,
|
|
3803
|
+
};
|
|
3804
|
+
const { shell } = await this.materialized(identity);
|
|
3805
|
+
await shell.validateIdentity(identity);
|
|
3806
|
+
return shell.listRuns(request.query as ClientRunListQueryV1);
|
|
3807
|
+
}
|
|
3808
|
+
|
|
3809
|
+
/**
|
|
3810
|
+
* The operator snapshot behind `/api/debug`. Bot-scoped like every other
|
|
3811
|
+
* Bot RPC — the debug token authorizes the *caller*, it does not widen what
|
|
3812
|
+
* a Bot will answer about itself.
|
|
3813
|
+
*/
|
|
3814
|
+
async debugSnapshot(input: unknown) {
|
|
3815
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3816
|
+
userId: rpcIdentifier,
|
|
3817
|
+
botId: rpcBotId,
|
|
3818
|
+
query: rpcDecoded(decodeBotDebugQueryV1),
|
|
3819
|
+
});
|
|
3820
|
+
const identity = {
|
|
3821
|
+
userId: request.userId as string,
|
|
3822
|
+
botId: request.botId as string,
|
|
3823
|
+
};
|
|
3824
|
+
const { shell } = await this.materialized(identity);
|
|
3825
|
+
await shell.validateIdentity(identity);
|
|
3826
|
+
return shell.debugSnapshot(identity, request.query);
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
async runQuestions(input: unknown) {
|
|
3830
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3831
|
+
userId: rpcIdentifier,
|
|
3832
|
+
botId: rpcBotId,
|
|
3833
|
+
query: rpcDecoded(decodeClientRunLookupQueryV1),
|
|
3834
|
+
});
|
|
3835
|
+
const identity = {
|
|
3836
|
+
userId: request.userId as string,
|
|
3837
|
+
botId: request.botId as string,
|
|
3838
|
+
};
|
|
3839
|
+
const { shell } = await this.materialized(identity);
|
|
3840
|
+
await shell.validateIdentity(identity);
|
|
3841
|
+
return shell.runQuestions(
|
|
3842
|
+
identity,
|
|
3843
|
+
request.query as ClientRunLookupQueryV1,
|
|
3844
|
+
);
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
async lookupRun(input: unknown) {
|
|
3848
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3849
|
+
userId: rpcIdentifier,
|
|
3850
|
+
botId: rpcBotId,
|
|
3851
|
+
query: rpcDecoded(decodeClientRunLookupQueryV1),
|
|
3852
|
+
});
|
|
3853
|
+
const identity = {
|
|
3854
|
+
userId: request.userId as string,
|
|
3855
|
+
botId: request.botId as string,
|
|
3856
|
+
};
|
|
3857
|
+
const { shell } = await this.materialized(identity);
|
|
3858
|
+
await shell.validateIdentity(identity);
|
|
3859
|
+
return shell.lookupRun(request.query as ClientRunLookupQueryV1);
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
async fenceRunAdmission(input: unknown) {
|
|
3863
|
+
const request = decodeRpcEnvelopeV1(input, {
|
|
3864
|
+
userId: rpcIdentifier,
|
|
3865
|
+
botId: rpcBotId,
|
|
3866
|
+
query: rpcDecoded(decodeClientRunLookupQueryV1),
|
|
3867
|
+
});
|
|
3868
|
+
const identity = {
|
|
3869
|
+
userId: request.userId as string,
|
|
3870
|
+
botId: request.botId as string,
|
|
3871
|
+
};
|
|
3872
|
+
const { shell } = await this.materialized(identity);
|
|
3873
|
+
return shell.fenceRunAdmission(
|
|
3874
|
+
identity,
|
|
3875
|
+
request.query as ClientRunLookupQueryV1,
|
|
3876
|
+
);
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3879
|
+
async alarm(): Promise<void> {
|
|
3880
|
+
await this.drainPush();
|
|
3881
|
+
const purgeValue = await this.ctx.storage.get<unknown>(
|
|
3882
|
+
MEMORY_VECTOR_PURGE_JOURNAL_KEY_V1,
|
|
3883
|
+
);
|
|
3884
|
+
if (purgeValue !== undefined) {
|
|
3885
|
+
await loggedEntryV1("Bot Memory vector purge", async () => {
|
|
3886
|
+
const journal = decodeMemoryVectorPurgeJournalV1(purgeValue);
|
|
3887
|
+
const outcome =
|
|
3888
|
+
journal.status === "pending"
|
|
3889
|
+
? await this.purgeMemoryVectorBatch(journal)
|
|
3890
|
+
: "complete";
|
|
3891
|
+
if (outcome === "complete") {
|
|
3892
|
+
await this.finishTearDown({
|
|
3893
|
+
userId: journal.userId,
|
|
3894
|
+
botId: journal.botId,
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3897
|
+
});
|
|
3898
|
+
return;
|
|
3899
|
+
}
|
|
3900
|
+
// The outbox drain is in a `finally` for the same reason the kernel's
|
|
3901
|
+
// re-arm is: it is the audit trail's second chance, and a throw anywhere in
|
|
3902
|
+
// the Bot's own settlement must not be what stops entries leaving.
|
|
3903
|
+
//
|
|
3904
|
+
// Both halves are then wrapped: an alarm has no caller, so anything that
|
|
3905
|
+
// escapes here is an uncaught exception in the object — which is one of the
|
|
3906
|
+
// ways the dev Worker died. The alarm is rescheduled by whatever owns it,
|
|
3907
|
+
// so recording the failure and letting the next firing try again is the
|
|
3908
|
+
// whole of the recovery.
|
|
3909
|
+
await loggedEntryV1("Bot alarm", async () => {
|
|
3910
|
+
try {
|
|
3911
|
+
await (await this.contribution()).alarm();
|
|
3912
|
+
} finally {
|
|
3913
|
+
// The alarm the Bot already has is also the audit outbox's second
|
|
3914
|
+
// chance: entries a settlement could not deliver leave on the next
|
|
3915
|
+
// firing rather than waiting for the Bot to be spoken to again.
|
|
3916
|
+
const identity = await this.ctx.storage.get<BotIdentity>(IDENTITY_KEY);
|
|
3917
|
+
await Promise.all([
|
|
3918
|
+
loggedEntryV1("Bot audit outbox drain", () =>
|
|
3919
|
+
this.drainAuditOutbox(),
|
|
3920
|
+
),
|
|
3921
|
+
// And the voice reply outbox's: an answer whose hand-off could not
|
|
3922
|
+
// reach the voice object leaves here rather than waiting for the
|
|
3923
|
+
// next thing anyone asks this Bot.
|
|
3924
|
+
loggedEntryV1("Bot voice reply outbox drain", () =>
|
|
3925
|
+
identity
|
|
3926
|
+
? this.drainVoiceReplyOutbox(identity.userId)
|
|
3927
|
+
: Promise.resolve(),
|
|
3928
|
+
),
|
|
3929
|
+
]);
|
|
3930
|
+
}
|
|
3931
|
+
});
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
/**
|
|
3935
|
+
* Internal fetch surface reached only after the gateway authenticates
|
|
3936
|
+
* ownership.
|
|
3937
|
+
*
|
|
3938
|
+
* This is the state-channel upgrade, and it is where a `BotNotFoundError`
|
|
3939
|
+
* escaped and took the dev Worker down twice: a Durable Object's own `fetch`
|
|
3940
|
+
* is an entry point, so a throw here has no caller inside the object to catch
|
|
3941
|
+
* it. A Bot that is not there is a 404, and every other failure is a 500 that
|
|
3942
|
+
* still carries a reason.
|
|
3943
|
+
*/
|
|
3944
|
+
fetch(request: Request): Promise<Response> {
|
|
3945
|
+
return answeredEntryV1("Bot-state channel failed", () =>
|
|
3946
|
+
this.#openChannel(request),
|
|
3947
|
+
);
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
async #openChannel(request: Request): Promise<Response> {
|
|
3951
|
+
const url = new URL(request.url);
|
|
3952
|
+
if (url.pathname !== BOT_STATE_CHANNEL_INTERNAL_PATH) {
|
|
3953
|
+
return new Response("Not found", { status: 404 });
|
|
3954
|
+
}
|
|
3955
|
+
const userId = request.headers.get("x-frockbot-user-id");
|
|
3956
|
+
const botId = request.headers.get("x-frockbot-bot-id");
|
|
3957
|
+
if (!userId || !botId) {
|
|
3958
|
+
return Response.json(
|
|
3959
|
+
{ error: "authenticated Bot identity required" },
|
|
3960
|
+
{ status: 401 },
|
|
3961
|
+
);
|
|
3962
|
+
}
|
|
3963
|
+
const identity = { userId, botId };
|
|
3964
|
+
const { shell } = await this.materialized(identity);
|
|
3965
|
+
await shell.validateIdentity(identity);
|
|
3966
|
+
return this.stateChannel.upgrade(request, identity);
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
// Three more entry points with no caller. A throw in any of them is an
|
|
3970
|
+
// uncaught exception in the object, and none of them has anybody to answer.
|
|
3971
|
+
webSocketMessage(
|
|
3972
|
+
socket: WebSocket,
|
|
3973
|
+
_message: string | ArrayBuffer,
|
|
3974
|
+
): Promise<void> {
|
|
3975
|
+
return loggedEntryV1("Bot-state channel message", () =>
|
|
3976
|
+
this.stateChannel.message(socket),
|
|
3977
|
+
);
|
|
3978
|
+
}
|
|
3979
|
+
|
|
3980
|
+
webSocketClose(
|
|
3981
|
+
socket: WebSocket,
|
|
3982
|
+
code: number,
|
|
3983
|
+
reason: string,
|
|
3984
|
+
): Promise<void> {
|
|
3985
|
+
return loggedEntryV1("Bot-state channel close", () =>
|
|
3986
|
+
this.stateChannel.close(socket, code, reason),
|
|
3987
|
+
);
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3990
|
+
webSocketError(socket: WebSocket, _error: unknown): Promise<void> {
|
|
3991
|
+
return loggedEntryV1("Bot-state channel error", () =>
|
|
3992
|
+
this.stateChannel.error(socket),
|
|
3993
|
+
);
|
|
3994
|
+
}
|
|
3995
|
+
}
|