@frockbot/cloudflare 0.0.0 → 0.7.291

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/build-artifact.ts +130 -0
  2. package/build-flutter-web.ts +508 -0
  3. package/deployment-config/README.md +414 -0
  4. package/deployment-config/cli.ts +228 -0
  5. package/deployment-config/generate.ts +689 -0
  6. package/deployment-config/jsonc.ts +18 -0
  7. package/deployment-config/profile-schema.generated.ts +391 -0
  8. package/deployment-config/profile.schema.json +358 -0
  9. package/deployment-config/profile.ts +124 -0
  10. package/migrations/0001_better_auth.sql +15 -0
  11. package/migrations/0002_drop_account_issuer.sql +8 -0
  12. package/package.json +61 -5
  13. package/release-version.ts +20 -0
  14. package/src/account-admission.ts +179 -0
  15. package/src/account-deletion.ts +274 -0
  16. package/src/admin-entrypoint.ts +194 -0
  17. package/src/admin-identities.ts +26 -0
  18. package/src/audit.ts +103 -0
  19. package/src/auth-package.access.ts +24 -0
  20. package/src/auth-package.ts +36 -0
  21. package/src/avatar-state-cleanup.ts +107 -0
  22. package/src/billing-computer.ts +267 -0
  23. package/src/billing-readiness.ts +21 -0
  24. package/src/billing.ts +461 -0
  25. package/src/bot-capabilities.ts +480 -0
  26. package/src/bot-recovery.ts +1 -0
  27. package/src/bot-state-channel.ts +702 -0
  28. package/src/bot-state.ts +3995 -0
  29. package/src/bot-template-cleanup.ts +98 -0
  30. package/src/bot-title-cleanup.ts +104 -0
  31. package/src/brand-icon.png +0 -0
  32. package/src/brand-logo.ts +2 -0
  33. package/src/brand.ts +36 -0
  34. package/src/client-compatibility.ts +51 -0
  35. package/src/compaction-announcement-cleanup.ts +113 -0
  36. package/src/computer-egress.ts +110 -0
  37. package/src/computer-host.ts +92 -0
  38. package/src/computer-screenshot-cleanup.ts +78 -0
  39. package/src/contracts.ts +832 -0
  40. package/src/debug.ts +272 -0
  41. package/src/default-packages-marker-cleanup.ts +39 -0
  42. package/src/deployment-policy-admin-host.ts +121 -0
  43. package/src/deployment-policy.ts +426 -0
  44. package/src/directory-profile-cleanup.ts +101 -0
  45. package/src/durable-rpc.ts +753 -0
  46. package/src/durable-session.ts +21 -0
  47. package/src/entry-boundary.ts +103 -0
  48. package/src/frock-ai.ts +338 -0
  49. package/src/gateway.ts +1392 -0
  50. package/src/group-chat.ts +736 -0
  51. package/src/hidden-bot-notifications-cleanup.ts +32 -0
  52. package/src/index.ts +2798 -0
  53. package/src/insights.ts +15 -0
  54. package/src/machine-messages-cleanup.ts +121 -0
  55. package/src/machine-socket.ts +164 -0
  56. package/src/memory-records.ts +181 -0
  57. package/src/memory.ts +145 -0
  58. package/src/model-rates.ts +252 -0
  59. package/src/native-auth.ts +1006 -0
  60. package/src/native-sessions.ts +121 -0
  61. package/src/notification-state-cleanup.ts +18 -0
  62. package/src/ollama-web-search-cleanup.ts +69 -0
  63. package/src/package-page-shapes-cleanup.ts +190 -0
  64. package/src/plugin-egress.ts +31 -0
  65. package/src/plugin-page-route.ts +72 -0
  66. package/src/plugin-panels-cleanup.ts +101 -0
  67. package/src/prepared-input-cleanup.ts +105 -0
  68. package/src/production-secrets.ts +484 -0
  69. package/src/project-cleanup.ts +123 -0
  70. package/src/project-events-cleanup.ts +152 -0
  71. package/src/publication-state-cleanup.ts +92 -0
  72. package/src/push.ts +498 -0
  73. package/src/request-body.ts +165 -0
  74. package/src/routine-state-cleanup.ts +57 -0
  75. package/src/search.ts +88 -0
  76. package/src/sidebar-label-cleanup.ts +165 -0
  77. package/src/skill-index-cleanup.ts +53 -0
  78. package/src/supersede-cleanup.ts +162 -0
  79. package/src/test-chat-cleanup.ts +93 -0
  80. package/src/uploads.ts +486 -0
  81. package/src/user-application.ts +1068 -0
  82. package/src/user-configuration.ts +4491 -0
  83. package/src/voice-assistant.ts +4999 -0
  84. package/src/voice-dictation.ts +743 -0
  85. package/src/working-context-cleanup.ts +96 -0
  86. package/src/workspace.ts +201 -0
  87. package/wrangler.jsonc +385 -0
  88. package/README.md +0 -3
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Cloudflare Insights, the zone's own analytics.
3
+ *
4
+ * The beacon is injected into every HTML response by the zone, above this
5
+ * Worker, so a page loads it whether or not that page's policy allows it. A
6
+ * policy that does not name these origins does not remove the beacon; it turns
7
+ * every page load into a refused request and a red console error. The
8
+ * application's policy therefore names them, and does not pretend the feature
9
+ * is off.
10
+ */
11
+
12
+ /** Where the zone's injected beacon script is served from. */
13
+ export const INSIGHTS_SCRIPT_ORIGIN = "https://static.cloudflareinsights.com";
14
+ /** Where that beacon posts its measurements — a different host from the script. */
15
+ export const INSIGHTS_REPORT_ORIGIN = "https://cloudflareinsights.com";
@@ -0,0 +1,121 @@
1
+ // Disposable pre-user cleanup for first-party Messages (ADR 0037).
2
+ //
3
+ // Messages left three shapes no decoder accepts any more: a machine record
4
+ // that reports the `messages` capability or carries its permission report, a
5
+ // queued command whose op is `messages`, and a Bot's intent for one. The User
6
+ // object rewrites the records and drops the commands with their requeue
7
+ // counters; the Bot drops its intents. A result carries no op, so results stay.
8
+ // Each walk runs once per object, in pages.
9
+
10
+ const USER_RECEIPT = "maintenance:machine-messages:2026-09-25";
11
+ const BOT_RECEIPT = "maintenance:machine-messages-intents:2026-09-25";
12
+ const MACHINE_PREFIX = "machine:";
13
+ const MACHINE_QUEUE_PREFIX = "machine-queue:";
14
+ const MACHINE_REQUEUE_PREFIX = "machine-requeue:";
15
+ const MACHINE_INTENT_PREFIX = "machine-command:";
16
+ const PAGE = 128;
17
+
18
+ export interface MachineMessagesCleanupStorageV1 {
19
+ get<T = unknown>(key: string): Promise<T | undefined>;
20
+ put(key: string, value: unknown): Promise<void>;
21
+ delete(key: string): Promise<boolean>;
22
+ list<T = unknown>(options: {
23
+ prefix: string;
24
+ limit: number;
25
+ startAfter?: string;
26
+ }): Promise<Map<string, T>>;
27
+ }
28
+
29
+ type Stored = Record<string, unknown>;
30
+
31
+ function record(value: unknown): Stored | undefined {
32
+ if (!value || typeof value !== "object" || Array.isArray(value))
33
+ return undefined;
34
+ return value as Stored;
35
+ }
36
+
37
+ function isMessagesOp(value: unknown): boolean {
38
+ return record(record(value)?.op)?.kind === "messages";
39
+ }
40
+
41
+ /** The machine record without Messages, or `undefined` to keep it. */
42
+ export function withoutMessagesCapabilityV1(
43
+ value: unknown,
44
+ ): Stored | undefined {
45
+ const machine = record(value);
46
+ if (!machine) return undefined;
47
+ const capabilities = Array.isArray(machine.capabilities)
48
+ ? machine.capabilities
49
+ : undefined;
50
+ const reportsMessages = capabilities?.includes("messages") === true;
51
+ if (!reportsMessages && !Object.hasOwn(machine, "messagesPermissions"))
52
+ return undefined;
53
+ const { messagesPermissions: _retired, ...kept } = machine;
54
+ return reportsMessages
55
+ ? {
56
+ ...kept,
57
+ capabilities: capabilities!.filter((entry) => entry !== "messages"),
58
+ }
59
+ : kept;
60
+ }
61
+
62
+ async function walk(
63
+ storage: MachineMessagesCleanupStorageV1,
64
+ prefix: string,
65
+ visit: (key: string, value: unknown) => Promise<void>,
66
+ ): Promise<void> {
67
+ let startAfter: string | undefined;
68
+ for (;;) {
69
+ const page = await storage.list({
70
+ prefix,
71
+ limit: PAGE,
72
+ ...(startAfter ? { startAfter } : {}),
73
+ });
74
+ for (const [key, value] of page) await visit(key, value);
75
+ if (page.size < PAGE) return;
76
+ startAfter = [...page.keys()].at(-1);
77
+ }
78
+ }
79
+
80
+ export async function cleanUserMachineMessagesV1(
81
+ storage: MachineMessagesCleanupStorageV1,
82
+ now: Date = new Date(),
83
+ ): Promise<void> {
84
+ if (await storage.get(USER_RECEIPT)) return;
85
+ let machines = 0;
86
+ let commands = 0;
87
+ await walk(storage, MACHINE_PREFIX, async (key, value) => {
88
+ const cleaned = withoutMessagesCapabilityV1(value);
89
+ if (!cleaned) return;
90
+ await storage.put(key, cleaned);
91
+ machines += 1;
92
+ });
93
+ await walk(storage, MACHINE_QUEUE_PREFIX, async (key, value) => {
94
+ if (!isMessagesOp(value)) return;
95
+ await storage.delete(key);
96
+ const commandId = record(value)?.commandId;
97
+ if (typeof commandId === "string") {
98
+ await storage.delete(`${MACHINE_REQUEUE_PREFIX}${commandId}`);
99
+ }
100
+ commands += 1;
101
+ });
102
+ await storage.put(USER_RECEIPT, {
103
+ at: now.toISOString(),
104
+ machines,
105
+ commands,
106
+ });
107
+ }
108
+
109
+ export async function cleanBotMachineMessagesV1(
110
+ storage: MachineMessagesCleanupStorageV1,
111
+ now: Date = new Date(),
112
+ ): Promise<void> {
113
+ if (await storage.get(BOT_RECEIPT)) return;
114
+ let intents = 0;
115
+ await walk(storage, MACHINE_INTENT_PREFIX, async (key, value) => {
116
+ if (!isMessagesOp(value)) return;
117
+ await storage.delete(key);
118
+ intents += 1;
119
+ });
120
+ await storage.put(BOT_RECEIPT, { at: now.toISOString(), intents });
121
+ }
@@ -0,0 +1,164 @@
1
+ // A registered machine's socket, as the User Durable Object holds it.
2
+ //
3
+ // The gateway verifies the machine token at the edge and forwards the upgrade
4
+ // here with what it proved in internal headers, the way the Bot-state channel
5
+ // carries identity. The token itself stops at the gateway: what crosses is its
6
+ // claims and its digest, and the Durable Object checks the digest against the
7
+ // machine record before it accepts anything.
8
+ //
9
+ // The sockets are hibernating and tagged by machine, so an idle desktop costs
10
+ // nothing and presence is simply whether a socket with that tag is open.
11
+
12
+ import {
13
+ MachineTokenError,
14
+ machineTokenClaimsV1,
15
+ type MachinePlatformV1,
16
+ type MachineSocketFrameV1,
17
+ } from "@frockbot/core/machine-protocol";
18
+ import type { MachineCallV1 } from "@frockbot/app/machine/backend";
19
+ import type { MachineSocketsV1 } from "@frockbot/app/machine/user";
20
+
21
+ export const MACHINE_SOCKET_INTERNAL_PATH_V1 = "/internal/machine-socket/v1";
22
+
23
+ const USER_HEADER = "x-frockbot-user-id";
24
+ const MACHINE_HEADER = "x-frockbot-machine-id";
25
+ const CLAIMS_HEADER = "x-frockbot-machine-claims";
26
+ const DIGEST_HEADER = "x-frockbot-machine-token-digest";
27
+ const DIGEST = /^[0-9a-f]{64}$/;
28
+ const OPEN = 1;
29
+
30
+ export function machineSocketTagV1(machineId: string): string {
31
+ return `machine:${machineId}`;
32
+ }
33
+
34
+ /** What each accepted socket carries through hibernation. */
35
+ export interface MachineSocketAttachmentV1 {
36
+ machineId: string;
37
+ tokenDigest: string;
38
+ keyVersion: number;
39
+ /** Which device modules the machine is sent. */
40
+ platform: MachinePlatformV1;
41
+ }
42
+
43
+ /** The upgrade, as the gateway hands it to the User Durable Object. */
44
+ export function internalMachineSocketRequestV1(
45
+ userId: string,
46
+ call: MachineCallV1,
47
+ request: Request,
48
+ ): Request {
49
+ const headers = new Headers(request.headers);
50
+ headers.delete("authorization");
51
+ headers.set(USER_HEADER, userId);
52
+ headers.set(MACHINE_HEADER, call.machineId);
53
+ headers.set(CLAIMS_HEADER, JSON.stringify(call.claims));
54
+ headers.set(DIGEST_HEADER, call.tokenDigest);
55
+ return new Request(
56
+ new URL(
57
+ MACHINE_SOCKET_INTERNAL_PATH_V1,
58
+ "https://user-configuration.internal",
59
+ ),
60
+ { method: "GET", headers },
61
+ );
62
+ }
63
+
64
+ /** The forwarded call, or a refusal. */
65
+ export function readMachineSocketCallV1(request: Request): {
66
+ userId: string;
67
+ call: MachineCallV1;
68
+ } {
69
+ const userId = request.headers.get(USER_HEADER);
70
+ const machineId = request.headers.get(MACHINE_HEADER);
71
+ const claims = request.headers.get(CLAIMS_HEADER);
72
+ const tokenDigest = request.headers.get(DIGEST_HEADER);
73
+ if (!userId || !machineId || !claims || !tokenDigest) {
74
+ throw new MachineTokenError(401, "machine token is required");
75
+ }
76
+ if (!DIGEST.test(tokenDigest)) {
77
+ throw new MachineTokenError(401, "machine token is invalid");
78
+ }
79
+ let parsed: unknown;
80
+ try {
81
+ parsed = JSON.parse(claims);
82
+ } catch {
83
+ throw new MachineTokenError(401, "machine token is invalid");
84
+ }
85
+ return {
86
+ userId,
87
+ call: { machineId, claims: machineTokenClaimsV1(parsed), tokenDigest },
88
+ };
89
+ }
90
+
91
+ /** The Contribution's view of the sockets, over the Durable Object's own list. */
92
+ export function durableObjectMachineSocketsV1(
93
+ ctx: DurableObjectState,
94
+ ): MachineSocketsV1 {
95
+ const open = (machineId: string): WebSocket[] =>
96
+ ctx
97
+ .getWebSockets(machineSocketTagV1(machineId))
98
+ .filter((socket) => socket.readyState === OPEN);
99
+ return {
100
+ push(machineId: string, frame: MachineSocketFrameV1): void {
101
+ if (frame.type === "commands" && frame.commands.length === 0) return;
102
+ const encoded = JSON.stringify(frame);
103
+ for (const socket of open(machineId)) {
104
+ try {
105
+ socket.send(encoded);
106
+ } catch {
107
+ // A socket that cannot take a frame is closing; the queue is durable
108
+ // and the machine is sent the command again when it reconnects.
109
+ }
110
+ }
111
+ },
112
+ connected: (machineId) => open(machineId).length > 0,
113
+ close(machineId: string, code: number, reason: string): void {
114
+ for (const socket of ctx.getWebSockets(machineSocketTagV1(machineId))) {
115
+ try {
116
+ socket.close(code, reason);
117
+ } catch {
118
+ // Already closed.
119
+ }
120
+ }
121
+ },
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Send every open machine socket its platform's frame. The frame is built once
127
+ * per platform, however many machines share it.
128
+ */
129
+ export function broadcastMachineFrameV1(
130
+ ctx: DurableObjectState,
131
+ frameFor: (platform: MachinePlatformV1) => MachineSocketFrameV1,
132
+ ): void {
133
+ const encoded = new Map<MachinePlatformV1, string>();
134
+ for (const socket of ctx.getWebSockets()) {
135
+ if (socket.readyState !== OPEN) continue;
136
+ const attachment =
137
+ socket.deserializeAttachment() as MachineSocketAttachmentV1 | null;
138
+ if (!attachment) continue;
139
+ let frame = encoded.get(attachment.platform);
140
+ if (frame === undefined) {
141
+ frame = JSON.stringify(frameFor(attachment.platform));
142
+ encoded.set(attachment.platform, frame);
143
+ }
144
+ try {
145
+ socket.send(frame);
146
+ } catch {
147
+ // A closing socket is sent the whole list again when it reconnects.
148
+ }
149
+ }
150
+ }
151
+
152
+ /** Every machine with an open socket, and the platform it is sent modules for. */
153
+ export function connectedMachinePlatformsV1(
154
+ ctx: DurableObjectState,
155
+ ): Map<string, MachinePlatformV1> {
156
+ const connected = new Map<string, MachinePlatformV1>();
157
+ for (const socket of ctx.getWebSockets()) {
158
+ if (socket.readyState !== OPEN) continue;
159
+ const attachment =
160
+ socket.deserializeAttachment() as MachineSocketAttachmentV1 | null;
161
+ if (attachment) connected.set(attachment.machineId, attachment.platform);
162
+ }
163
+ return connected;
164
+ }
@@ -0,0 +1,181 @@
1
+ // Cloudflare Durable Object SQL adapter for canonical Memory.
2
+ //
3
+ // Alarm arming stays on the object's existing async alarm owner: SQLite
4
+ // `transactionSync` cannot enclose `setAlarm`. Jobs still carry `next_attempt_at`
5
+ // so eviction re-arms from the due index.
6
+
7
+ import { MemoryEngineV1 } from "@frockbot/app/memory/engine";
8
+ import { createMemoryEmbedder } from "@frockbot/app/memory/embeddings";
9
+ import {
10
+ drainMemoryProcessingV1,
11
+ type MemoryDrainResultV1,
12
+ type MemoryProcessingAdaptersV1,
13
+ } from "@frockbot/app/memory/processing";
14
+ import {
15
+ MemoryRecordsV1,
16
+ type MemoryRemoteOwnerV1,
17
+ } from "@frockbot/app/memory/owner";
18
+ import type {
19
+ MemoryBrowseRequestV1,
20
+ MemoryBrowseResultV1,
21
+ MemoryExpandRequestV1,
22
+ MemoryExpandResultV1,
23
+ MemoryForgetRequestV1,
24
+ MemoryForgetResultV1,
25
+ MemoryPreparedCoreRequestV1,
26
+ MemoryPreparedCoreResultV1,
27
+ MemoryRecallRequestV1,
28
+ MemoryRecallResultV1,
29
+ MemoryWriteRequestV1,
30
+ MemoryWriteResultV1,
31
+ } from "@frockbot/app/memory/records";
32
+ import type { MemorySqlStorageV1 } from "@frockbot/app/memory/sql";
33
+ import type {
34
+ MemoryAiBinding,
35
+ MemoryVectorIndex,
36
+ } from "@frockbot/app/memory/types";
37
+ import { remoteCallV1 } from "@frockbot/core/contracts";
38
+
39
+ export interface DurableMemoryStorageV1 {
40
+ sql: MemorySqlStorageV1["sql"];
41
+ transactionSync<T>(callback: () => T): T;
42
+ }
43
+
44
+ export function durableObjectHasSqlV1(
45
+ storage: object,
46
+ ): storage is DurableMemoryStorageV1 {
47
+ return (
48
+ "sql" in storage &&
49
+ typeof (storage as { transactionSync?: unknown }).transactionSync ===
50
+ "function"
51
+ );
52
+ }
53
+
54
+ export function createDurableMemoryStorageV1(
55
+ storage: DurableMemoryStorageV1,
56
+ ): MemorySqlStorageV1 {
57
+ return {
58
+ sql: storage.sql,
59
+ transactionSync: (callback) => storage.transactionSync(callback),
60
+ };
61
+ }
62
+
63
+ export function createBotMemoryEngineV1(
64
+ storage: DurableMemoryStorageV1,
65
+ ): MemoryEngineV1 {
66
+ const engine = new MemoryEngineV1({
67
+ storage: createDurableMemoryStorageV1(storage),
68
+ ownedKinds: ["bot"],
69
+ });
70
+ engine.open();
71
+ return engine;
72
+ }
73
+
74
+ export function createUserMemoryEngineV1(
75
+ storage: DurableMemoryStorageV1,
76
+ ): MemoryEngineV1 {
77
+ const engine = new MemoryEngineV1({
78
+ storage: createDurableMemoryStorageV1(storage),
79
+ ownedKinds: ["user", "groupChat"],
80
+ });
81
+ engine.open();
82
+ return engine;
83
+ }
84
+
85
+ export type MemoryOperateActionV1 =
86
+ "write" | "forget" | "recall" | "expand" | "browse" | "preparedCore";
87
+
88
+ export interface UserMemoryRecordsRpc {
89
+ operateMemory(input: unknown): Promise<unknown>;
90
+ }
91
+
92
+ export function createUserMemoryRecordsRemoteV1(
93
+ rpc: UserMemoryRecordsRpc,
94
+ identity: { userId: string; botId: string },
95
+ ): MemoryRemoteOwnerV1 {
96
+ const call = async <T>(
97
+ action: MemoryOperateActionV1,
98
+ request: unknown,
99
+ ): Promise<T> =>
100
+ remoteCallV1("shared Memory", () =>
101
+ rpc.operateMemory({
102
+ schemaVersion: 1,
103
+ userId: identity.userId,
104
+ botId: identity.botId,
105
+ action,
106
+ request,
107
+ }),
108
+ ) as Promise<T>;
109
+ return {
110
+ write: (request) => call<MemoryWriteResultV1>("write", request),
111
+ forget: (request) => call<MemoryForgetResultV1>("forget", request),
112
+ recall: (request) => call<MemoryRecallResultV1>("recall", request),
113
+ expand: (request) => call<MemoryExpandResultV1>("expand", request),
114
+ browse: (request) => call<MemoryBrowseResultV1>("browse", request),
115
+ preparedCore: (request) =>
116
+ call<MemoryPreparedCoreResultV1>("preparedCore", request),
117
+ };
118
+ }
119
+
120
+ export function createBotMemoryRecordsV1(
121
+ storage: DurableMemoryStorageV1,
122
+ remote: MemoryRemoteOwnerV1,
123
+ ): MemoryRecordsV1 {
124
+ return new MemoryRecordsV1({
125
+ owner: "bot",
126
+ engine: createBotMemoryEngineV1(storage),
127
+ remote,
128
+ });
129
+ }
130
+
131
+ export function dispatchMemoryOperateV1(
132
+ engine: MemoryEngineV1,
133
+ action: MemoryOperateActionV1,
134
+ request: unknown,
135
+ ):
136
+ | MemoryWriteResultV1
137
+ | MemoryForgetResultV1
138
+ | MemoryRecallResultV1
139
+ | MemoryExpandResultV1
140
+ | MemoryBrowseResultV1
141
+ | MemoryPreparedCoreResultV1 {
142
+ switch (action) {
143
+ case "write":
144
+ return engine.write(request as MemoryWriteRequestV1);
145
+ case "forget":
146
+ return engine.forget(request as MemoryForgetRequestV1);
147
+ case "recall":
148
+ return engine.recall(request as MemoryRecallRequestV1);
149
+ case "expand":
150
+ return engine.expand(request as MemoryExpandRequestV1);
151
+ case "browse":
152
+ return engine.browse(request as MemoryBrowseRequestV1);
153
+ case "preparedCore": {
154
+ const coreRequest = request as MemoryPreparedCoreRequestV1;
155
+ engine.refreshPreparedCores(coreRequest);
156
+ return engine.preparedCore(coreRequest);
157
+ }
158
+ default: {
159
+ const exhausted: never = action;
160
+ throw new Error(`unknown Memory action ${String(exhausted)}`);
161
+ }
162
+ }
163
+ }
164
+
165
+ export interface DurableMemoryIndexBindingsV1 {
166
+ vectors?: MemoryVectorIndex;
167
+ ai?: MemoryAiBinding;
168
+ /** What a Turn's words hold worth remembering; absent, extraction waits. */
169
+ extract?: MemoryProcessingAdaptersV1["extract"];
170
+ }
171
+
172
+ export async function drainDurableMemoryV1(
173
+ engine: MemoryEngineV1,
174
+ bindings: DurableMemoryIndexBindingsV1 = {},
175
+ ): Promise<MemoryDrainResultV1> {
176
+ const adapters: MemoryProcessingAdaptersV1 = {};
177
+ if (bindings.vectors) adapters.vectors = bindings.vectors;
178
+ if (bindings.ai) adapters.embed = createMemoryEmbedder(bindings.ai);
179
+ if (bindings.extract) adapters.extract = bindings.extract;
180
+ return drainMemoryProcessingV1(engine, adapters);
181
+ }
package/src/memory.ts ADDED
@@ -0,0 +1,145 @@
1
+ // The Memory seam, bound in production.
2
+ //
3
+ // Two things live here, and both exist because the User's Durable Object is
4
+ // the authority for everything User-scoped:
5
+ //
6
+ // 1. `createRoutedWorkspaceGenerationsV1` sends the *shared* Memory root's
7
+ // generations — `user-memory` — to the User Durable Object over RPC, and
8
+ // everything else to the Bot's own ledger. Two Bots writing one shared
9
+ // root therefore record into one ledger, which is what makes "newest fact
10
+ // wins on conflict" answerable at all: the minted generation ids come
11
+ // from a single authority, so they order.
12
+ // 2. `createUserMemoryGroupsV1` reads which Group Chats the Bot is in, whose
13
+ // Memory it may use, from the User object's list of groups.
14
+ //
15
+ // Both are decoded at the seam. "Cross-runtime communication uses narrow,
16
+ // versioned DTOs, and every inbound value is decoded at its seam" — an answer
17
+ // from another Durable Object is inbound, so it is decoded here rather than
18
+ // trusted because of where it came from.
19
+ import {
20
+ decodeWorkspaceGenerationRecordV1,
21
+ isWorkspaceSharedMemoryRootV1,
22
+ remoteCallV1,
23
+ type WorkspaceGenerationRecordV1,
24
+ type WorkspaceGenerationsV1,
25
+ type WorkspaceRootV1,
26
+ } from "@frockbot/core/contracts";
27
+ import { isGroupIdV1 } from "@frockbot/app/groups/shared";
28
+ import type { MemoryGroupsV1 } from "@frockbot/app/memory/groups";
29
+
30
+ /** The User Durable Object's Memory RPC surface, as the Bot object calls it. */
31
+ export interface UserMemoryRpc {
32
+ mintWorkspaceGeneration(input: unknown): Promise<string>;
33
+ currentWorkspaceGeneration(input: unknown): Promise<unknown>;
34
+ recordWorkspaceGeneration(input: unknown): Promise<void>;
35
+ tombstoneWorkspaceGeneration(input: unknown): Promise<void>;
36
+ conflictWorkspaceGeneration(input: unknown): Promise<void>;
37
+ listWorkspaceConflicts(input: unknown): Promise<unknown>;
38
+ listMemoryGroups(input: unknown): Promise<unknown>;
39
+ operateMemory(input: unknown): Promise<unknown>;
40
+ }
41
+
42
+ function decodeRecord(value: unknown): WorkspaceGenerationRecordV1 | undefined {
43
+ return value === undefined || value === null
44
+ ? undefined
45
+ : decodeWorkspaceGenerationRecordV1(value);
46
+ }
47
+
48
+ /** `WorkspaceGenerationsV1` over the User Durable Object, for shared roots. */
49
+ export function createUserWorkspaceGenerationsV1(
50
+ rpc: UserMemoryRpc,
51
+ userId: string,
52
+ ): WorkspaceGenerationsV1 {
53
+ const envelope = (extra: Record<string, unknown>) => ({
54
+ schemaVersion: 1,
55
+ userId,
56
+ ...extra,
57
+ });
58
+ // Every one of these crosses a Durable Object boundary, so every one gets a
59
+ // deadline and one retry: a User object that is mid-eviction or briefly
60
+ // unreachable must cost a retry, never a Turn that hangs to the platform
61
+ // limit with nothing on screen.
62
+ const call = <T>(label: string, invoke: () => Promise<T>) =>
63
+ remoteCallV1(label, invoke);
64
+ return {
65
+ mint: (at, root) =>
66
+ call("the generation ledger", () =>
67
+ rpc.mintWorkspaceGeneration(envelope({ at: at.toISOString(), root })),
68
+ ),
69
+ current: async (root, path) =>
70
+ decodeRecord(
71
+ await call("the generation ledger", () =>
72
+ rpc.currentWorkspaceGeneration(envelope({ root, path })),
73
+ ),
74
+ ),
75
+ record: (entry) =>
76
+ call("the generation ledger", () =>
77
+ rpc.recordWorkspaceGeneration(envelope({ entry })),
78
+ ),
79
+ tombstone: (entry) =>
80
+ call("the generation ledger", () =>
81
+ rpc.tombstoneWorkspaceGeneration(envelope({ entry })),
82
+ ),
83
+ conflict: (entry) =>
84
+ call("the generation ledger", () =>
85
+ rpc.conflictWorkspaceGeneration(envelope({ entry })),
86
+ ),
87
+ conflicts: async (root, path) => {
88
+ const answer = await call("the generation ledger", () =>
89
+ rpc.listWorkspaceConflicts(envelope({ root, path })),
90
+ );
91
+ if (!Array.isArray(answer)) return [];
92
+ return answer.map((value) => decodeWorkspaceGenerationRecordV1(value));
93
+ },
94
+ };
95
+ }
96
+
97
+ /**
98
+ * One ledger interface over two authorities, routed by the root itself.
99
+ *
100
+ * A shared Memory root belongs to the User; every other durable root belongs
101
+ * to the Bot whose object holds it. The routing is a function of the root and
102
+ * nothing else, so no caller can send a record to the wrong authority by
103
+ * holding the wrong handle.
104
+ */
105
+ export function createRoutedWorkspaceGenerationsV1(options: {
106
+ bot: WorkspaceGenerationsV1;
107
+ user: WorkspaceGenerationsV1;
108
+ }): WorkspaceGenerationsV1 {
109
+ const owner = (root: WorkspaceRootV1): WorkspaceGenerationsV1 =>
110
+ isWorkspaceSharedMemoryRootV1(root) ? options.user : options.bot;
111
+ return {
112
+ // A minted id must order against the other ids in the ledger that holds
113
+ // it, so minting follows exactly the same routing as recording: shared
114
+ // roots mint in the User object, the Bot's own roots in its own.
115
+ mint: (at, root) => owner(root).mint(at, root),
116
+ current: (root, path) => owner(root).current(root, path),
117
+ record: (entry) => owner(entry.root).record(entry),
118
+ tombstone: (entry) => owner(entry.root).tombstone(entry),
119
+ conflict: (entry) => owner(entry.root).conflict(entry),
120
+ conflicts: (root, path) => owner(root).conflicts(root, path),
121
+ };
122
+ }
123
+
124
+ /** The Bot's Group Chat membership, over the User Durable Object. */
125
+ export function createUserMemoryGroupsV1(
126
+ rpc: UserMemoryRpc,
127
+ identity: { userId: string; botId: string },
128
+ ): MemoryGroupsV1 {
129
+ return {
130
+ memberOf: async () => {
131
+ const answer = await remoteCallV1("the Group Chat list", () =>
132
+ rpc.listMemoryGroups({
133
+ schemaVersion: 1,
134
+ userId: identity.userId,
135
+ botId: identity.botId,
136
+ }),
137
+ );
138
+ const groupIds = (answer as { groupIds?: unknown } | null)?.groupIds;
139
+ if (!Array.isArray(groupIds) || !groupIds.every(isGroupIdV1)) {
140
+ throw new Error("Group Chat membership answer is invalid");
141
+ }
142
+ return groupIds;
143
+ },
144
+ };
145
+ }