@frockbot/plugin-shell 0.0.0 โ 0.1.1
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/frockbot.json +68 -0
- package/package.json +87 -6
- package/src/agent.test.ts +372 -0
- package/src/agent.ts +335 -0
- package/src/approvals.test.ts +224 -0
- package/src/approvals.ts +530 -0
- package/src/backend-assignment.test.ts +161 -0
- package/src/backend-assignment.ts +274 -0
- package/src/backend-authoring.test.ts +518 -0
- package/src/backend-authoring.ts +531 -0
- package/src/backend-bot-identity.test.ts +215 -0
- package/src/backend-completion.test.ts +289 -0
- package/src/backend-completion.ts +95 -0
- package/src/backend-composition.ts +242 -0
- package/src/backend-computer.ts +76 -0
- package/src/backend-configuration.test.ts +1757 -0
- package/src/backend-contracts.test.ts +189 -0
- package/src/backend-contracts.ts +44 -0
- package/src/backend-debug.test.ts +202 -0
- package/src/backend-execution.ts +55 -0
- package/src/backend-flock.ts +96 -0
- package/src/backend-image.test.ts +115 -0
- package/src/backend-image.ts +180 -0
- package/src/backend-isolate.test.ts +238 -0
- package/src/backend-isolate.ts +409 -0
- package/src/backend-machine.ts +144 -0
- package/src/backend-memory.ts +89 -0
- package/src/backend-recovery-integration.test.ts +1575 -0
- package/src/backend-recovery.ts +106 -0
- package/src/backend-routines.ts +375 -0
- package/src/backend-runner.ts +251 -0
- package/src/backend-skills.test.ts +126 -0
- package/src/backend-skills.ts +198 -0
- package/src/backend-stop.test.ts +356 -0
- package/src/backend-subagents.ts +459 -0
- package/src/backend.ts +6035 -0
- package/src/client/FrockBotApp.vue +1026 -0
- package/src/client/SendPayloadView.vue +337 -0
- package/src/client/composer-draft.test.ts +31 -0
- package/src/client/composer-draft.ts +35 -0
- package/src/client/cordis-client-shim.d.ts +15 -0
- package/src/client/index.test.ts +2548 -0
- package/src/client/index.ts +2346 -0
- package/src/client/model-presentation.test.ts +35 -0
- package/src/client/model-presentation.ts +19 -0
- package/src/client/notify.test.ts +89 -0
- package/src/client/notify.ts +101 -0
- package/src/client/skill-invocation.test.ts +143 -0
- package/src/client/skill-invocation.ts +175 -0
- package/src/client/styles.css +1043 -0
- package/src/composition-views.ts +118 -0
- package/src/debug-protocol.test.ts +80 -0
- package/src/debug-protocol.ts +165 -0
- package/src/env.d.ts +10 -0
- package/src/history.test.ts +163 -0
- package/src/history.ts +108 -0
- package/src/host.ts +20 -0
- package/src/index.ts +2 -0
- package/src/manifest.ts +3 -0
- package/src/run-cursor.ts +28 -0
- package/src/run-protocol.test.ts +1281 -0
- package/src/run-protocol.ts +1417 -0
- package/src/settings-links.test.ts +106 -0
- package/src/settings-links.ts +289 -0
- package/src/shared.ts +338 -0
- package/src/skill-protocol.ts +117 -0
- package/src/terminal-records.test.ts +217 -0
- package/src/terminal-records.ts +150 -0
- package/src/unread.test.ts +362 -0
- package/src/unread.ts +675 -0
- package/tsconfig.json +18 -0
- package/vite.config.ts +32 -0
- package/README.md +0 -3
|
@@ -0,0 +1,1575 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { type SessionEvent } from "@frockbot/kernel-contracts";
|
|
3
|
+
import {
|
|
4
|
+
parseCredentialKeyringV1,
|
|
5
|
+
sealCredentialV1,
|
|
6
|
+
} from "@frockbot/connection-core";
|
|
7
|
+
import {
|
|
8
|
+
initializeBotSettingsV1,
|
|
9
|
+
type UserSettingsViewV1,
|
|
10
|
+
} from "@frockbot/configuration-core";
|
|
11
|
+
import { createShellBotBackendContribution } from "./backend.js";
|
|
12
|
+
import {
|
|
13
|
+
botTurnCommandFingerprintV1,
|
|
14
|
+
type StoredRun,
|
|
15
|
+
} from "./backend-contracts.js";
|
|
16
|
+
import { planBotRunRecovery } from "./backend-recovery.js";
|
|
17
|
+
import {
|
|
18
|
+
CLIENT_RUN_LIST_MAX_BYTES,
|
|
19
|
+
CLIENT_RUN_PAGE_LIMIT,
|
|
20
|
+
clientRunListWireBytes,
|
|
21
|
+
} from "./run-protocol.js";
|
|
22
|
+
|
|
23
|
+
class MemoryStorage {
|
|
24
|
+
readonly values = new Map<string, unknown>();
|
|
25
|
+
readonly listRequests: Array<{
|
|
26
|
+
prefix?: string;
|
|
27
|
+
end?: string;
|
|
28
|
+
reverse?: boolean;
|
|
29
|
+
limit?: number;
|
|
30
|
+
}> = [];
|
|
31
|
+
readonly gets: string[] = [];
|
|
32
|
+
alarmAt: number | undefined;
|
|
33
|
+
|
|
34
|
+
get<T>(key: string): Promise<T | undefined> {
|
|
35
|
+
this.gets.push(key);
|
|
36
|
+
return Promise.resolve(this.values.get(key) as T | undefined);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
put(key: string | Record<string, unknown>, value?: unknown): Promise<void> {
|
|
40
|
+
if (typeof key === "string") this.values.set(key, structuredClone(value));
|
|
41
|
+
else {
|
|
42
|
+
for (const [entry, item] of Object.entries(key)) {
|
|
43
|
+
this.values.set(entry, structuredClone(item));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return Promise.resolve();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
delete(key: string): Promise<boolean> {
|
|
50
|
+
return Promise.resolve(this.values.delete(key));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
list<T>(options: {
|
|
54
|
+
prefix?: string;
|
|
55
|
+
end?: string;
|
|
56
|
+
reverse?: boolean;
|
|
57
|
+
limit?: number;
|
|
58
|
+
}): Promise<Map<string, T>> {
|
|
59
|
+
this.listRequests.push(options);
|
|
60
|
+
const entries = [...this.values.entries()]
|
|
61
|
+
.filter(
|
|
62
|
+
([key]) =>
|
|
63
|
+
key.startsWith(options.prefix ?? "") &&
|
|
64
|
+
(options.end === undefined || key < options.end),
|
|
65
|
+
)
|
|
66
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
67
|
+
if (options.reverse) entries.reverse();
|
|
68
|
+
return Promise.resolve(
|
|
69
|
+
new Map(entries.slice(0, options.limit) as Array<[string, T]>),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
transaction<T>(callback: (storage: MemoryStorage) => Promise<T>): Promise<T> {
|
|
74
|
+
return callback(this);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setAlarm(scheduledTime: number): Promise<void> {
|
|
78
|
+
this.alarmAt = scheduledTime;
|
|
79
|
+
return Promise.resolve();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
deleteAlarm(): Promise<void> {
|
|
83
|
+
this.alarmAt = undefined;
|
|
84
|
+
return Promise.resolve();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
describe("Bot recovery", () => {
|
|
89
|
+
test("executes and reconstructs an Ollama-bound Bot without Foundation fallback", async () => {
|
|
90
|
+
const storage = new MemoryStorage();
|
|
91
|
+
const credentialKeyring =
|
|
92
|
+
'{"schemaVersion":1,"currentKeyId":"primary","keys":{"primary":"MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY"}}';
|
|
93
|
+
const envelope = await sealCredentialV1({
|
|
94
|
+
keyring: parseCredentialKeyringV1(credentialKeyring),
|
|
95
|
+
context: {
|
|
96
|
+
accountId: "user-1",
|
|
97
|
+
connectionId: "ollama-1",
|
|
98
|
+
packageId: "provider-ollama-cloud",
|
|
99
|
+
credentialGeneration: "generation-1",
|
|
100
|
+
},
|
|
101
|
+
plaintext: "ollama-secret",
|
|
102
|
+
});
|
|
103
|
+
const userSettings: UserSettingsViewV1 = {
|
|
104
|
+
schemaVersion: 1,
|
|
105
|
+
revision: 1,
|
|
106
|
+
profile: { name: "User" },
|
|
107
|
+
packages: [
|
|
108
|
+
{
|
|
109
|
+
packageId: "provider-ollama-cloud",
|
|
110
|
+
version: "0.0.1",
|
|
111
|
+
state: "installed",
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
connections: [
|
|
115
|
+
{
|
|
116
|
+
connectionId: "ollama-1",
|
|
117
|
+
packageId: "provider-ollama-cloud",
|
|
118
|
+
connectionTypeId: "ollama-cloud-account",
|
|
119
|
+
displayName: "Work",
|
|
120
|
+
state: "ready",
|
|
121
|
+
providerType: "ollama-cloud",
|
|
122
|
+
generation: "generation-1",
|
|
123
|
+
safeMetadata: {},
|
|
124
|
+
modelCatalog: {
|
|
125
|
+
schemaVersion: 1,
|
|
126
|
+
generation: "catalog-1",
|
|
127
|
+
state: "fresh",
|
|
128
|
+
models: [
|
|
129
|
+
{
|
|
130
|
+
providerModelId: "glm-5.3-flash:cloud",
|
|
131
|
+
displayName: "GLM",
|
|
132
|
+
capabilities: {
|
|
133
|
+
tools: true,
|
|
134
|
+
vision: false,
|
|
135
|
+
reasoning: false,
|
|
136
|
+
},
|
|
137
|
+
source: "discovered",
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
const leasedRequests: Array<Record<string, unknown>> = [];
|
|
145
|
+
const settledEffects: string[] = [];
|
|
146
|
+
let settlementFailures = 0;
|
|
147
|
+
const rpc = {
|
|
148
|
+
readConfiguration: () => Promise.resolve(structuredClone(userSettings)),
|
|
149
|
+
listBots: () =>
|
|
150
|
+
Promise.resolve({ schemaVersion: 1 as const, revision: 0, bots: [] }),
|
|
151
|
+
getConnection: () =>
|
|
152
|
+
Promise.resolve(structuredClone(userSettings.connections[0])),
|
|
153
|
+
executeConnectionDependency: (request: { action: string }) => {
|
|
154
|
+
if (request.action === "claim") {
|
|
155
|
+
return Promise.resolve({
|
|
156
|
+
schemaVersion: 1 as const,
|
|
157
|
+
status: "claimed" as const,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (request.action === "acknowledge") {
|
|
161
|
+
return Promise.resolve({
|
|
162
|
+
schemaVersion: 1 as const,
|
|
163
|
+
status: "acknowledged" as const,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (request.action === "read") {
|
|
167
|
+
return Promise.resolve({
|
|
168
|
+
schemaVersion: 1 as const,
|
|
169
|
+
status: "acknowledged" as const,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return Promise.resolve({
|
|
173
|
+
schemaVersion: 1 as const,
|
|
174
|
+
status: "released" as const,
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
claimConnectionDependency: () => Promise.resolve(true),
|
|
178
|
+
acknowledgeConnectionDependency: () => Promise.resolve(true),
|
|
179
|
+
compensateConnectionDependency: () => Promise.resolve(true),
|
|
180
|
+
leaseModelCredential: (input: unknown) => {
|
|
181
|
+
leasedRequests.push(input as Record<string, unknown>);
|
|
182
|
+
const request = input as { effectId: string };
|
|
183
|
+
return Promise.resolve({
|
|
184
|
+
schemaVersion: 1,
|
|
185
|
+
leaseId: `lease-${leasedRequests.length}`,
|
|
186
|
+
effectId: request.effectId,
|
|
187
|
+
connectionId: "ollama-1",
|
|
188
|
+
credentialGeneration: "generation-1",
|
|
189
|
+
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
190
|
+
envelope,
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
settleModelCredential: (input: unknown) => {
|
|
194
|
+
settledEffects.push((input as { effectId: string }).effectId);
|
|
195
|
+
if (settlementFailures > 0) {
|
|
196
|
+
settlementFailures -= 1;
|
|
197
|
+
return Promise.reject(new Error("settlement unavailable"));
|
|
198
|
+
}
|
|
199
|
+
return Promise.resolve();
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
const requests: Request[] = [];
|
|
203
|
+
let failRequests = false;
|
|
204
|
+
const outboundFetch = ((input, init) => {
|
|
205
|
+
const request = new Request(input, init);
|
|
206
|
+
requests.push(request);
|
|
207
|
+
if (!request.url.startsWith("https://ollama.com/")) {
|
|
208
|
+
return Promise.reject(new Error("Foundation fallback invoked"));
|
|
209
|
+
}
|
|
210
|
+
if (failRequests) return Promise.reject(new Error("response lost"));
|
|
211
|
+
return Promise.resolve(
|
|
212
|
+
new Response(
|
|
213
|
+
'data: {"choices":[{"delta":{"content":"Ollama reply"}}]}\n\n' +
|
|
214
|
+
'data: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\n' +
|
|
215
|
+
"data: [DONE]\n\n",
|
|
216
|
+
{
|
|
217
|
+
status: 200,
|
|
218
|
+
headers: { "content-type": "text/event-stream" },
|
|
219
|
+
},
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
}) as typeof fetch;
|
|
223
|
+
const host = () =>
|
|
224
|
+
createShellBotBackendContribution({
|
|
225
|
+
state: { storage } as unknown as DurableObjectState,
|
|
226
|
+
env: {
|
|
227
|
+
CREDENTIAL_KEYRING: credentialKeyring,
|
|
228
|
+
USER_CONFIGURATIONS: {
|
|
229
|
+
idFromName: () => "user-configuration-id",
|
|
230
|
+
get: () => rpc,
|
|
231
|
+
},
|
|
232
|
+
MEMORY_FILES: {},
|
|
233
|
+
MEMORY_INDEX: {},
|
|
234
|
+
AI: {},
|
|
235
|
+
} as unknown as Parameters<
|
|
236
|
+
typeof createShellBotBackendContribution
|
|
237
|
+
>[0]["env"],
|
|
238
|
+
outboundFetch,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const configured = host();
|
|
242
|
+
await configured.materializeSettings(
|
|
243
|
+
{ userId: "user-1", botId: "primary" },
|
|
244
|
+
{
|
|
245
|
+
name: "Ollama Bot",
|
|
246
|
+
model: {
|
|
247
|
+
connectionId: "ollama-1",
|
|
248
|
+
providerModelId: "glm-5.3-flash:cloud",
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
await configured.executeConfiguration({
|
|
253
|
+
schemaVersion: 1,
|
|
254
|
+
userId: "user-1",
|
|
255
|
+
botId: "primary",
|
|
256
|
+
command: {
|
|
257
|
+
schemaVersion: 1,
|
|
258
|
+
type: "bot/assign-capability",
|
|
259
|
+
commandId: "assign-ollama-model",
|
|
260
|
+
botId: "primary",
|
|
261
|
+
expectedRevision: 0,
|
|
262
|
+
assignment: {
|
|
263
|
+
assignmentId: "ollama-model",
|
|
264
|
+
packageId: "provider-ollama-cloud",
|
|
265
|
+
capabilityId: "ollama-cloud-models",
|
|
266
|
+
connectionId: "ollama-1",
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
const first = await host().run({
|
|
271
|
+
userId: "user-1",
|
|
272
|
+
botId: "primary",
|
|
273
|
+
runId: "ollama-run-1",
|
|
274
|
+
sessionId: "user-1:primary",
|
|
275
|
+
acceptedAt: "2026-08-30T00:00:00.000Z",
|
|
276
|
+
text: "hello",
|
|
277
|
+
});
|
|
278
|
+
const second = await host().run({
|
|
279
|
+
userId: "user-1",
|
|
280
|
+
botId: "primary",
|
|
281
|
+
runId: "ollama-run-2",
|
|
282
|
+
sessionId: "user-1:primary",
|
|
283
|
+
acceptedAt: "2026-08-30T00:01:00.000Z",
|
|
284
|
+
text: "again",
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(first.text).toBe("Ollama reply");
|
|
288
|
+
expect(second.text).toBe("Ollama reply");
|
|
289
|
+
expect(requests).toHaveLength(2);
|
|
290
|
+
expect(
|
|
291
|
+
await Promise.all(requests.map((request) => request.clone().json())),
|
|
292
|
+
).toEqual([
|
|
293
|
+
expect.objectContaining({ model: "glm-5.3-flash:cloud" }),
|
|
294
|
+
expect.objectContaining({ model: "glm-5.3-flash:cloud" }),
|
|
295
|
+
]);
|
|
296
|
+
expect(
|
|
297
|
+
leasedRequests.map((request) => ({
|
|
298
|
+
connectionId: request.connectionId,
|
|
299
|
+
providerModelId: request.providerModelId,
|
|
300
|
+
connectionGeneration: request.connectionGeneration,
|
|
301
|
+
})),
|
|
302
|
+
).toEqual([
|
|
303
|
+
{
|
|
304
|
+
connectionId: "ollama-1",
|
|
305
|
+
providerModelId: "glm-5.3-flash:cloud",
|
|
306
|
+
connectionGeneration: "generation-1",
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
connectionId: "ollama-1",
|
|
310
|
+
providerModelId: "glm-5.3-flash:cloud",
|
|
311
|
+
connectionGeneration: "generation-1",
|
|
312
|
+
},
|
|
313
|
+
]);
|
|
314
|
+
expect(settledEffects).toHaveLength(2);
|
|
315
|
+
for (const runId of ["ollama-run-1", "ollama-run-2"]) {
|
|
316
|
+
const run = await storage.get<StoredRun>(`run:${runId}`);
|
|
317
|
+
expect(
|
|
318
|
+
run?.events.find((event) => event.type === "model/request"),
|
|
319
|
+
).toMatchObject({
|
|
320
|
+
request: {
|
|
321
|
+
provider: "ollama-cloud",
|
|
322
|
+
model: "glm-5.3-flash:cloud",
|
|
323
|
+
modelBinding: {
|
|
324
|
+
connectionId: "ollama-1",
|
|
325
|
+
connectionGeneration: "generation-1",
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
settlementFailures = 1;
|
|
332
|
+
await expect(
|
|
333
|
+
host().run({
|
|
334
|
+
userId: "user-1",
|
|
335
|
+
botId: "primary",
|
|
336
|
+
runId: "ollama-run-settlement",
|
|
337
|
+
sessionId: "user-1:primary",
|
|
338
|
+
acceptedAt: "2026-08-30T00:01:30.000Z",
|
|
339
|
+
text: "settle durably",
|
|
340
|
+
}),
|
|
341
|
+
).rejects.toThrow("durable outcome settlement pending");
|
|
342
|
+
expect(
|
|
343
|
+
await storage.get<StoredRun>("run:ollama-run-settlement"),
|
|
344
|
+
).toMatchObject({ status: "running", phase: "executing" });
|
|
345
|
+
expect(await storage.get<string>("active-run")).toBe(
|
|
346
|
+
"ollama-run-settlement",
|
|
347
|
+
);
|
|
348
|
+
expect(requests).toHaveLength(3);
|
|
349
|
+
userSettings.packages[0] = {
|
|
350
|
+
...userSettings.packages[0]!,
|
|
351
|
+
state: "disabled",
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
await host().alarm();
|
|
355
|
+
|
|
356
|
+
expect(
|
|
357
|
+
await storage.get<StoredRun>("run:ollama-run-settlement"),
|
|
358
|
+
).toMatchObject({ status: "completed" });
|
|
359
|
+
expect(await storage.get("active-run")).toBeUndefined();
|
|
360
|
+
expect(requests).toHaveLength(3);
|
|
361
|
+
expect(settledEffects).toHaveLength(4);
|
|
362
|
+
userSettings.packages[0] = {
|
|
363
|
+
...userSettings.packages[0]!,
|
|
364
|
+
state: "installed",
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
failRequests = true;
|
|
368
|
+
await expect(
|
|
369
|
+
host().run({
|
|
370
|
+
userId: "user-1",
|
|
371
|
+
botId: "primary",
|
|
372
|
+
runId: "ollama-run-uncertain",
|
|
373
|
+
sessionId: "user-1:primary",
|
|
374
|
+
acceptedAt: "2026-08-30T00:02:00.000Z",
|
|
375
|
+
text: "uncertain",
|
|
376
|
+
}),
|
|
377
|
+
).rejects.toThrow("response lost");
|
|
378
|
+
expect(
|
|
379
|
+
await storage.get<StoredRun>("run:ollama-run-uncertain"),
|
|
380
|
+
).toMatchObject({ status: "reconciliation-required" });
|
|
381
|
+
|
|
382
|
+
await host().alarm();
|
|
383
|
+
expect(
|
|
384
|
+
await storage.get<StoredRun>("run:ollama-run-uncertain"),
|
|
385
|
+
).toMatchObject({ status: "reconciliation-required" });
|
|
386
|
+
await expect(
|
|
387
|
+
host().reconcileRun(
|
|
388
|
+
{ userId: "user-1", botId: "primary" },
|
|
389
|
+
"ollama-run-uncertain",
|
|
390
|
+
),
|
|
391
|
+
).rejects.toThrow();
|
|
392
|
+
expect(
|
|
393
|
+
await storage.get<StoredRun>("run:ollama-run-uncertain"),
|
|
394
|
+
).toMatchObject({
|
|
395
|
+
status: "failed",
|
|
396
|
+
failure: expect.stringContaining("explicitly abandoned"),
|
|
397
|
+
});
|
|
398
|
+
expect(await storage.get("active-run")).toBeUndefined();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("does not clear active work whose durable run is malformed", async () => {
|
|
402
|
+
const storage = new MemoryStorage();
|
|
403
|
+
await storage.put({
|
|
404
|
+
"active-run": "run-malformed",
|
|
405
|
+
"run:run-malformed": {
|
|
406
|
+
runId: "run-malformed",
|
|
407
|
+
commandFingerprint: "fingerprint",
|
|
408
|
+
sessionId: "user:primary",
|
|
409
|
+
acceptedAt: "2026-08-28T00:00:00.000Z",
|
|
410
|
+
input: "hello",
|
|
411
|
+
events: [],
|
|
412
|
+
phase: "executing",
|
|
413
|
+
compositionGenerationId: "test-composition-generation",
|
|
414
|
+
configurationSnapshot: initializeBotSettingsV1("primary"),
|
|
415
|
+
previousEventCount: 0,
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
const contribution = createShellBotBackendContribution({
|
|
419
|
+
state: { storage } as unknown as DurableObjectState,
|
|
420
|
+
env: {} as never,
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
await expect(contribution.listRuns({ schemaVersion: 1 })).rejects.toThrow(
|
|
424
|
+
"stored run has invalid fields",
|
|
425
|
+
);
|
|
426
|
+
expect(await storage.get<string>("active-run")).toBe("run-malformed");
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("preserves an active marker whose referenced run is missing", async () => {
|
|
430
|
+
const storage = new MemoryStorage();
|
|
431
|
+
await storage.put("active-run", "run-missing");
|
|
432
|
+
const contribution = createShellBotBackendContribution({
|
|
433
|
+
state: { storage } as unknown as DurableObjectState,
|
|
434
|
+
env: {} as never,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
await expect(contribution.listRuns({ schemaVersion: 1 })).resolves.toEqual({
|
|
438
|
+
schemaVersion: 1,
|
|
439
|
+
runs: [],
|
|
440
|
+
page: { truncated: false },
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
expect(await storage.get<string>("active-run")).toBe("run-missing");
|
|
444
|
+
expect(typeof storage.alarmAt).toBe("number");
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("preserves active work when a recovery failure exceeds its durable bound", async () => {
|
|
448
|
+
const storage = new MemoryStorage();
|
|
449
|
+
const occurrenceId = `tool:${"x".repeat(9_000)}`;
|
|
450
|
+
const events = [
|
|
451
|
+
{
|
|
452
|
+
type: "tool/result" as const,
|
|
453
|
+
seq: 0,
|
|
454
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
455
|
+
turn: 1,
|
|
456
|
+
step: 1,
|
|
457
|
+
occurrenceId,
|
|
458
|
+
name: "echo",
|
|
459
|
+
content: "unsafe",
|
|
460
|
+
isError: false,
|
|
461
|
+
status: "completed" as const,
|
|
462
|
+
},
|
|
463
|
+
];
|
|
464
|
+
const run = {
|
|
465
|
+
runId: "run-oversized-failure",
|
|
466
|
+
commandFingerprint: "fingerprint",
|
|
467
|
+
sessionId: "user:primary",
|
|
468
|
+
acceptedAt: "2026-08-28T00:00:00.000Z",
|
|
469
|
+
input: "hello",
|
|
470
|
+
events,
|
|
471
|
+
effectAdmissions: [],
|
|
472
|
+
status: "running",
|
|
473
|
+
phase: "executing",
|
|
474
|
+
compositionGenerationId: "test-composition-generation",
|
|
475
|
+
configurationSnapshot: initializeBotSettingsV1("primary"),
|
|
476
|
+
previousEventCount: 0,
|
|
477
|
+
} satisfies StoredRun;
|
|
478
|
+
await storage.put({
|
|
479
|
+
identity: { userId: "user-1", botId: "primary" },
|
|
480
|
+
"active-run": run.runId,
|
|
481
|
+
[`run:${run.runId}`]: run,
|
|
482
|
+
"latest-events": events,
|
|
483
|
+
});
|
|
484
|
+
const contribution = createShellBotBackendContribution({
|
|
485
|
+
state: { storage } as unknown as DurableObjectState,
|
|
486
|
+
env: {} as never,
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
await expect(contribution.listRuns({ schemaVersion: 1 })).rejects.toThrow(
|
|
490
|
+
`run "${run.runId}" has invalid failure`,
|
|
491
|
+
);
|
|
492
|
+
expect(await storage.get<string>("active-run")).toBe(run.runId);
|
|
493
|
+
expect(await storage.get<StoredRun>(`run:${run.runId}`)).toEqual(run);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("preserves reconciliation state when durable history is malformed", async () => {
|
|
497
|
+
const storage = new MemoryStorage();
|
|
498
|
+
const run = {
|
|
499
|
+
runId: "run-reconciliation",
|
|
500
|
+
commandFingerprint: "fingerprint",
|
|
501
|
+
sessionId: "user:primary",
|
|
502
|
+
acceptedAt: "2026-08-28T00:00:00.000Z",
|
|
503
|
+
input: "hello",
|
|
504
|
+
events: [],
|
|
505
|
+
effectAdmissions: [],
|
|
506
|
+
status: "reconciliation-required",
|
|
507
|
+
phase: "reconciliation-required",
|
|
508
|
+
failure: "Provider confirmation required",
|
|
509
|
+
compositionGenerationId: "test-composition-generation",
|
|
510
|
+
configurationSnapshot: initializeBotSettingsV1("primary"),
|
|
511
|
+
previousEventCount: 0,
|
|
512
|
+
} satisfies StoredRun;
|
|
513
|
+
await storage.put({
|
|
514
|
+
identity: { userId: "user-1", botId: "primary" },
|
|
515
|
+
"active-run": run.runId,
|
|
516
|
+
[`run:${run.runId}`]: run,
|
|
517
|
+
"latest-events": [{ type: "model/request" }],
|
|
518
|
+
});
|
|
519
|
+
const contribution = createShellBotBackendContribution({
|
|
520
|
+
state: { storage } as unknown as DurableObjectState,
|
|
521
|
+
env: {} as never,
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
await expect(
|
|
525
|
+
contribution.reconcileRun(
|
|
526
|
+
{ userId: "user-1", botId: "primary" },
|
|
527
|
+
run.runId,
|
|
528
|
+
),
|
|
529
|
+
).rejects.toThrow("session event.seq must be an integer");
|
|
530
|
+
expect(await storage.get<string>("active-run")).toBe(run.runId);
|
|
531
|
+
expect(await storage.get<StoredRun>(`run:${run.runId}`)).toEqual(run);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("atomically restores the admitted notification intent after eviction", async () => {
|
|
535
|
+
const storage = new MemoryStorage();
|
|
536
|
+
const admittedSettings = {
|
|
537
|
+
...initializeBotSettingsV1("primary"),
|
|
538
|
+
profile: { name: "Admitted Bot" },
|
|
539
|
+
notifications: { enabled: true },
|
|
540
|
+
};
|
|
541
|
+
const currentSettings = {
|
|
542
|
+
...admittedSettings,
|
|
543
|
+
profile: { name: "Current Bot" },
|
|
544
|
+
notifications: { enabled: false },
|
|
545
|
+
};
|
|
546
|
+
const events = [
|
|
547
|
+
{
|
|
548
|
+
type: "turn/start" as const,
|
|
549
|
+
seq: 0,
|
|
550
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
551
|
+
turn: 1,
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
type: "step/start" as const,
|
|
555
|
+
seq: 1,
|
|
556
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
557
|
+
turn: 1,
|
|
558
|
+
step: 1,
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
type: "assistant/message" as const,
|
|
562
|
+
seq: 2,
|
|
563
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
564
|
+
turn: 1,
|
|
565
|
+
step: 1,
|
|
566
|
+
requestId: "request-1",
|
|
567
|
+
text: "Durable reply",
|
|
568
|
+
toolCalls: [],
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
type: "step/end" as const,
|
|
572
|
+
seq: 3,
|
|
573
|
+
timestamp: "2026-08-28T00:00:01.000Z",
|
|
574
|
+
turn: 1,
|
|
575
|
+
step: 1,
|
|
576
|
+
outcome: "completed" as const,
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
type: "turn/end" as const,
|
|
580
|
+
seq: 4,
|
|
581
|
+
timestamp: "2026-08-28T00:00:01.000Z",
|
|
582
|
+
turn: 1,
|
|
583
|
+
outcome: "completed" as const,
|
|
584
|
+
},
|
|
585
|
+
] satisfies SessionEvent[];
|
|
586
|
+
const run = {
|
|
587
|
+
runId: "run-1",
|
|
588
|
+
commandFingerprint: botTurnCommandFingerprintV1({
|
|
589
|
+
userId: "user-1",
|
|
590
|
+
botId: "primary",
|
|
591
|
+
runId: "run-1",
|
|
592
|
+
sessionId: "user:primary",
|
|
593
|
+
acceptedAt: "2026-08-28T00:00:00.000Z",
|
|
594
|
+
text: "hello",
|
|
595
|
+
}),
|
|
596
|
+
sessionId: "user:primary",
|
|
597
|
+
acceptedAt: "2026-08-28T00:00:00.000Z",
|
|
598
|
+
input: "hello",
|
|
599
|
+
events,
|
|
600
|
+
effectAdmissions: [],
|
|
601
|
+
status: "running",
|
|
602
|
+
phase: "executing",
|
|
603
|
+
compositionGenerationId: "test-composition-generation",
|
|
604
|
+
configurationSnapshot: admittedSettings,
|
|
605
|
+
previousEventCount: 0,
|
|
606
|
+
} satisfies StoredRun;
|
|
607
|
+
await storage.put({
|
|
608
|
+
"active-run": run.runId,
|
|
609
|
+
"run:run-1": run,
|
|
610
|
+
"run-index:2026-08-28T00:00:00.000Z:run-1": run.runId,
|
|
611
|
+
"latest-events": events,
|
|
612
|
+
"bot-configuration": currentSettings,
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
const recovered = createShellBotBackendContribution({
|
|
616
|
+
state: { storage } as unknown as DurableObjectState,
|
|
617
|
+
env: {} as never,
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
await expect(recovered.listRuns()).resolves.toEqual({
|
|
621
|
+
schemaVersion: 1,
|
|
622
|
+
runs: [
|
|
623
|
+
expect.objectContaining({
|
|
624
|
+
schemaVersion: 2,
|
|
625
|
+
runId: "run-1",
|
|
626
|
+
status: "completed",
|
|
627
|
+
outcome: { type: "completed", text: "Durable reply" },
|
|
628
|
+
}),
|
|
629
|
+
],
|
|
630
|
+
page: { truncated: false },
|
|
631
|
+
});
|
|
632
|
+
const notifications = await recovered.listNotifications();
|
|
633
|
+
expect(notifications).toEqual([
|
|
634
|
+
expect.objectContaining({
|
|
635
|
+
notificationId: "run-1",
|
|
636
|
+
runId: "run-1",
|
|
637
|
+
title: "Admitted Bot replied",
|
|
638
|
+
body: "Durable reply",
|
|
639
|
+
}),
|
|
640
|
+
]);
|
|
641
|
+
|
|
642
|
+
const recoveredAgain = createShellBotBackendContribution({
|
|
643
|
+
state: { storage } as unknown as DurableObjectState,
|
|
644
|
+
env: {} as never,
|
|
645
|
+
});
|
|
646
|
+
await recoveredAgain.listRuns();
|
|
647
|
+
expect(await recoveredAgain.listNotifications()).toEqual(notifications);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test("preserves an unresolved request for an explicit decision", async () => {
|
|
651
|
+
const storage = new MemoryStorage();
|
|
652
|
+
const settings = initializeBotSettingsV1("primary");
|
|
653
|
+
const events = [
|
|
654
|
+
{
|
|
655
|
+
type: "turn/start" as const,
|
|
656
|
+
seq: 0,
|
|
657
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
658
|
+
turn: 1,
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
type: "step/start" as const,
|
|
662
|
+
seq: 1,
|
|
663
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
664
|
+
turn: 1,
|
|
665
|
+
step: 1,
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
type: "model/request" as const,
|
|
669
|
+
seq: 2,
|
|
670
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
671
|
+
turn: 1,
|
|
672
|
+
step: 1,
|
|
673
|
+
request: {
|
|
674
|
+
requestId: "request-with-lost-marker",
|
|
675
|
+
provider: "provider-1",
|
|
676
|
+
model: "model-1",
|
|
677
|
+
system: "",
|
|
678
|
+
messages: [],
|
|
679
|
+
tools: [],
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
] satisfies SessionEvent[];
|
|
683
|
+
const run = {
|
|
684
|
+
runId: "run-lost-marker",
|
|
685
|
+
commandFingerprint: botTurnCommandFingerprintV1({
|
|
686
|
+
userId: "user-1",
|
|
687
|
+
botId: "primary",
|
|
688
|
+
runId: "run-lost-marker",
|
|
689
|
+
sessionId: "user:primary",
|
|
690
|
+
acceptedAt: "2026-08-28T00:00:00.000Z",
|
|
691
|
+
text: "hello",
|
|
692
|
+
}),
|
|
693
|
+
sessionId: "user:primary",
|
|
694
|
+
acceptedAt: "2026-08-28T00:00:00.000Z",
|
|
695
|
+
input: "hello",
|
|
696
|
+
events,
|
|
697
|
+
effectAdmissions: [],
|
|
698
|
+
status: "running",
|
|
699
|
+
phase: "executing",
|
|
700
|
+
compositionGenerationId: "test-composition-generation",
|
|
701
|
+
configurationSnapshot: settings,
|
|
702
|
+
previousEventCount: 0,
|
|
703
|
+
} satisfies StoredRun;
|
|
704
|
+
await storage.put({
|
|
705
|
+
"active-run": run.runId,
|
|
706
|
+
"run:run-lost-marker": run,
|
|
707
|
+
"latest-events": events,
|
|
708
|
+
});
|
|
709
|
+
const recovered = createShellBotBackendContribution({
|
|
710
|
+
state: { storage } as unknown as DurableObjectState,
|
|
711
|
+
env: {} as never,
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
await expect(recovered.listRuns()).resolves.toEqual({
|
|
715
|
+
schemaVersion: 1,
|
|
716
|
+
runs: [
|
|
717
|
+
expect.objectContaining({
|
|
718
|
+
schemaVersion: 2,
|
|
719
|
+
runId: "run-lost-marker",
|
|
720
|
+
status: "reconciliation-required",
|
|
721
|
+
recovery: expect.objectContaining({ action: "resume" }),
|
|
722
|
+
}),
|
|
723
|
+
],
|
|
724
|
+
page: { truncated: false },
|
|
725
|
+
});
|
|
726
|
+
expect(storage.values.get("active-run")).toBe("run-lost-marker");
|
|
727
|
+
expect(storage.alarmAt).toBeUndefined();
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
test("resumes a request whose durable journal proves no effect started", () => {
|
|
731
|
+
const events = [
|
|
732
|
+
{
|
|
733
|
+
type: "model/request" as const,
|
|
734
|
+
seq: 0,
|
|
735
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
736
|
+
turn: 1,
|
|
737
|
+
step: 1,
|
|
738
|
+
request: {
|
|
739
|
+
requestId: "request-with-no-effect",
|
|
740
|
+
provider: "provider-1",
|
|
741
|
+
model: "model-1",
|
|
742
|
+
system: "",
|
|
743
|
+
messages: [],
|
|
744
|
+
tools: [],
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
type: "model/effect-not-started" as const,
|
|
749
|
+
seq: 1,
|
|
750
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
751
|
+
turn: 1,
|
|
752
|
+
step: 1,
|
|
753
|
+
requestId: "request-with-no-effect",
|
|
754
|
+
reason: "provider rejected before dispatch",
|
|
755
|
+
},
|
|
756
|
+
] satisfies SessionEvent[];
|
|
757
|
+
const run = {
|
|
758
|
+
runId: "run-no-effect",
|
|
759
|
+
commandFingerprint: botTurnCommandFingerprintV1({
|
|
760
|
+
userId: "user-1",
|
|
761
|
+
botId: "primary",
|
|
762
|
+
runId: "run-no-effect",
|
|
763
|
+
sessionId: "user:primary",
|
|
764
|
+
acceptedAt: "2026-08-28T00:00:00.000Z",
|
|
765
|
+
text: "hello",
|
|
766
|
+
}),
|
|
767
|
+
sessionId: "user:primary",
|
|
768
|
+
acceptedAt: "2026-08-28T00:00:00.000Z",
|
|
769
|
+
input: "hello",
|
|
770
|
+
events,
|
|
771
|
+
effectAdmissions: [],
|
|
772
|
+
status: "running",
|
|
773
|
+
phase: "executing",
|
|
774
|
+
compositionGenerationId: "test-composition-generation",
|
|
775
|
+
configurationSnapshot: initializeBotSettingsV1("primary"),
|
|
776
|
+
previousEventCount: 0,
|
|
777
|
+
} satisfies StoredRun;
|
|
778
|
+
|
|
779
|
+
expect(planBotRunRecovery(run, events)).toEqual({ kind: "resume" });
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
test("fails an ended step whose tool result has no durable intent", () => {
|
|
783
|
+
const events = [
|
|
784
|
+
{
|
|
785
|
+
type: "turn/start" as const,
|
|
786
|
+
seq: 0,
|
|
787
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
788
|
+
turn: 1,
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
type: "step/start" as const,
|
|
792
|
+
seq: 1,
|
|
793
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
794
|
+
turn: 1,
|
|
795
|
+
step: 1,
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
type: "assistant/message" as const,
|
|
799
|
+
seq: 2,
|
|
800
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
801
|
+
turn: 1,
|
|
802
|
+
step: 1,
|
|
803
|
+
requestId: "completed-request",
|
|
804
|
+
text: "",
|
|
805
|
+
toolCalls: [
|
|
806
|
+
{ id: "provider-call", name: "echo", input: { value: "unsafe" } },
|
|
807
|
+
],
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
type: "tool/result" as const,
|
|
811
|
+
seq: 3,
|
|
812
|
+
timestamp: "2026-08-28T00:00:01.000Z",
|
|
813
|
+
turn: 1,
|
|
814
|
+
step: 1,
|
|
815
|
+
occurrenceId: "tool:1:1:0",
|
|
816
|
+
name: "echo",
|
|
817
|
+
content: "unsafe",
|
|
818
|
+
isError: false,
|
|
819
|
+
status: "completed" as const,
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
type: "step/end" as const,
|
|
823
|
+
seq: 4,
|
|
824
|
+
timestamp: "2026-08-28T00:00:02.000Z",
|
|
825
|
+
turn: 1,
|
|
826
|
+
step: 1,
|
|
827
|
+
outcome: "completed" as const,
|
|
828
|
+
},
|
|
829
|
+
] satisfies SessionEvent[];
|
|
830
|
+
const run = {
|
|
831
|
+
runId: "run-malformed-tool",
|
|
832
|
+
commandFingerprint: "fingerprint",
|
|
833
|
+
sessionId: "user:primary",
|
|
834
|
+
acceptedAt: "2026-08-28T00:00:00.000Z",
|
|
835
|
+
input: "hello",
|
|
836
|
+
events,
|
|
837
|
+
effectAdmissions: [],
|
|
838
|
+
status: "running",
|
|
839
|
+
phase: "executing",
|
|
840
|
+
compositionGenerationId: "test-composition-generation",
|
|
841
|
+
configurationSnapshot: initializeBotSettingsV1("primary"),
|
|
842
|
+
previousEventCount: 0,
|
|
843
|
+
} satisfies StoredRun;
|
|
844
|
+
|
|
845
|
+
expect(planBotRunRecovery(run, events)).toEqual({
|
|
846
|
+
kind: "fail",
|
|
847
|
+
failure:
|
|
848
|
+
'Invalid durable tool journal: tool occurrence "tool:1:1:0" has a result without intent',
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
test("rejects tool effects journaled after their step closed", () => {
|
|
853
|
+
const events = [
|
|
854
|
+
{
|
|
855
|
+
type: "turn/start" as const,
|
|
856
|
+
seq: 0,
|
|
857
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
858
|
+
turn: 1,
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
type: "step/start" as const,
|
|
862
|
+
seq: 1,
|
|
863
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
864
|
+
turn: 1,
|
|
865
|
+
step: 1,
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
type: "model/request" as const,
|
|
869
|
+
seq: 2,
|
|
870
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
871
|
+
turn: 1,
|
|
872
|
+
step: 1,
|
|
873
|
+
request: {
|
|
874
|
+
requestId: "completed-request",
|
|
875
|
+
provider: "provider-1",
|
|
876
|
+
model: "model-1",
|
|
877
|
+
system: "",
|
|
878
|
+
messages: [],
|
|
879
|
+
tools: [],
|
|
880
|
+
},
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
type: "assistant/message" as const,
|
|
884
|
+
seq: 3,
|
|
885
|
+
timestamp: "2026-08-28T00:00:01.000Z",
|
|
886
|
+
turn: 1,
|
|
887
|
+
step: 1,
|
|
888
|
+
requestId: "completed-request",
|
|
889
|
+
text: "",
|
|
890
|
+
toolCalls: [
|
|
891
|
+
{ id: "provider-call", name: "echo", input: { value: "unsafe" } },
|
|
892
|
+
],
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
type: "step/end" as const,
|
|
896
|
+
seq: 4,
|
|
897
|
+
timestamp: "2026-08-28T00:00:02.000Z",
|
|
898
|
+
turn: 1,
|
|
899
|
+
step: 1,
|
|
900
|
+
outcome: "completed" as const,
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
type: "tool/call" as const,
|
|
904
|
+
seq: 5,
|
|
905
|
+
timestamp: "2026-08-28T00:00:03.000Z",
|
|
906
|
+
turn: 1,
|
|
907
|
+
step: 1,
|
|
908
|
+
occurrenceId: "tool:1:1:0",
|
|
909
|
+
name: "echo",
|
|
910
|
+
input: { value: "unsafe" },
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
type: "tool/result" as const,
|
|
914
|
+
seq: 6,
|
|
915
|
+
timestamp: "2026-08-28T00:00:04.000Z",
|
|
916
|
+
turn: 1,
|
|
917
|
+
step: 1,
|
|
918
|
+
occurrenceId: "tool:1:1:0",
|
|
919
|
+
name: "echo",
|
|
920
|
+
content: "unsafe",
|
|
921
|
+
isError: false,
|
|
922
|
+
status: "completed" as const,
|
|
923
|
+
},
|
|
924
|
+
] satisfies SessionEvent[];
|
|
925
|
+
const run = {
|
|
926
|
+
runId: "run-post-closure-tool",
|
|
927
|
+
commandFingerprint: "fingerprint",
|
|
928
|
+
sessionId: "user:primary",
|
|
929
|
+
acceptedAt: "2026-08-28T00:00:00.000Z",
|
|
930
|
+
input: "hello",
|
|
931
|
+
events,
|
|
932
|
+
effectAdmissions: [],
|
|
933
|
+
status: "running",
|
|
934
|
+
phase: "executing",
|
|
935
|
+
compositionGenerationId: "test-composition-generation",
|
|
936
|
+
configurationSnapshot: initializeBotSettingsV1("primary"),
|
|
937
|
+
previousEventCount: 0,
|
|
938
|
+
} satisfies StoredRun;
|
|
939
|
+
|
|
940
|
+
expect(planBotRunRecovery(run, events)).toEqual({
|
|
941
|
+
kind: "fail",
|
|
942
|
+
failure:
|
|
943
|
+
'Invalid durable tool journal: tool occurrence "tool:1:1:0" was not settled before step end',
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
test.each([
|
|
948
|
+
["text response", []],
|
|
949
|
+
[
|
|
950
|
+
"assistant tool calls before tool intent",
|
|
951
|
+
[
|
|
952
|
+
{
|
|
953
|
+
id: "durable-call",
|
|
954
|
+
name: "echo",
|
|
955
|
+
input: { value: "resumed" },
|
|
956
|
+
},
|
|
957
|
+
],
|
|
958
|
+
],
|
|
959
|
+
])("resumes a durable %s", (_label, toolCalls) => {
|
|
960
|
+
const events = [
|
|
961
|
+
{
|
|
962
|
+
type: "turn/start" as const,
|
|
963
|
+
seq: 0,
|
|
964
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
965
|
+
turn: 1,
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
type: "step/start" as const,
|
|
969
|
+
seq: 1,
|
|
970
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
971
|
+
turn: 1,
|
|
972
|
+
step: 1,
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
type: "model/request" as const,
|
|
976
|
+
seq: 2,
|
|
977
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
978
|
+
turn: 1,
|
|
979
|
+
step: 1,
|
|
980
|
+
request: {
|
|
981
|
+
requestId: "completed-request",
|
|
982
|
+
provider: "provider-1",
|
|
983
|
+
model: "model-1",
|
|
984
|
+
system: "",
|
|
985
|
+
messages: [],
|
|
986
|
+
tools: [],
|
|
987
|
+
},
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
type: "assistant/message" as const,
|
|
991
|
+
seq: 3,
|
|
992
|
+
timestamp: "2026-08-28T00:00:01.000Z",
|
|
993
|
+
turn: 1,
|
|
994
|
+
step: 1,
|
|
995
|
+
requestId: "completed-request",
|
|
996
|
+
text: toolCalls.length === 0 ? "Already durable." : "",
|
|
997
|
+
toolCalls,
|
|
998
|
+
},
|
|
999
|
+
] satisfies SessionEvent[];
|
|
1000
|
+
const run = {
|
|
1001
|
+
runId: "run-completed-request",
|
|
1002
|
+
commandFingerprint: "fingerprint",
|
|
1003
|
+
sessionId: "user:primary",
|
|
1004
|
+
acceptedAt: "2026-08-28T00:00:00.000Z",
|
|
1005
|
+
input: "hello",
|
|
1006
|
+
events,
|
|
1007
|
+
effectAdmissions: [],
|
|
1008
|
+
status: "running",
|
|
1009
|
+
phase: "executing",
|
|
1010
|
+
compositionGenerationId: "test-composition-generation",
|
|
1011
|
+
configurationSnapshot: initializeBotSettingsV1("primary"),
|
|
1012
|
+
previousEventCount: 0,
|
|
1013
|
+
} satisfies StoredRun;
|
|
1014
|
+
|
|
1015
|
+
expect(planBotRunRecovery(run, events)).toEqual({ kind: "resume" });
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
test("keeps a journaled tool effect in reconciliation", () => {
|
|
1019
|
+
const events = [
|
|
1020
|
+
{
|
|
1021
|
+
type: "turn/start" as const,
|
|
1022
|
+
seq: 0,
|
|
1023
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
1024
|
+
turn: 1,
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
type: "step/start" as const,
|
|
1028
|
+
seq: 1,
|
|
1029
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
1030
|
+
turn: 1,
|
|
1031
|
+
step: 1,
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
type: "model/request" as const,
|
|
1035
|
+
seq: 2,
|
|
1036
|
+
timestamp: "2026-08-28T00:00:00.000Z",
|
|
1037
|
+
turn: 1,
|
|
1038
|
+
step: 1,
|
|
1039
|
+
request: {
|
|
1040
|
+
requestId: "completed-request",
|
|
1041
|
+
provider: "provider-1",
|
|
1042
|
+
model: "model-1",
|
|
1043
|
+
system: "",
|
|
1044
|
+
messages: [],
|
|
1045
|
+
tools: [],
|
|
1046
|
+
},
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
type: "assistant/message" as const,
|
|
1050
|
+
seq: 3,
|
|
1051
|
+
timestamp: "2026-08-28T00:00:01.000Z",
|
|
1052
|
+
turn: 1,
|
|
1053
|
+
step: 1,
|
|
1054
|
+
requestId: "completed-request",
|
|
1055
|
+
text: "",
|
|
1056
|
+
toolCalls: [
|
|
1057
|
+
{ id: "uncertain-call", name: "echo", input: { value: "hello" } },
|
|
1058
|
+
],
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
type: "tool/call" as const,
|
|
1062
|
+
seq: 4,
|
|
1063
|
+
timestamp: "2026-08-28T00:00:02.000Z",
|
|
1064
|
+
turn: 1,
|
|
1065
|
+
step: 1,
|
|
1066
|
+
occurrenceId: "tool:1:1:0",
|
|
1067
|
+
name: "echo",
|
|
1068
|
+
input: { value: "hello" },
|
|
1069
|
+
},
|
|
1070
|
+
] satisfies SessionEvent[];
|
|
1071
|
+
const run = {
|
|
1072
|
+
runId: "run-uncertain-tool",
|
|
1073
|
+
commandFingerprint: "fingerprint",
|
|
1074
|
+
sessionId: "user:primary",
|
|
1075
|
+
acceptedAt: "2026-08-28T00:00:00.000Z",
|
|
1076
|
+
input: "hello",
|
|
1077
|
+
events,
|
|
1078
|
+
effectAdmissions: [],
|
|
1079
|
+
status: "running",
|
|
1080
|
+
phase: "executing",
|
|
1081
|
+
compositionGenerationId: "test-composition-generation",
|
|
1082
|
+
configurationSnapshot: initializeBotSettingsV1("primary"),
|
|
1083
|
+
previousEventCount: 0,
|
|
1084
|
+
} satisfies StoredRun;
|
|
1085
|
+
|
|
1086
|
+
expect(planBotRunRecovery(run, events).kind).toBe("reconcile");
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
test("replays only an identical completed Turn command", async () => {
|
|
1090
|
+
const storage = new MemoryStorage();
|
|
1091
|
+
const original = {
|
|
1092
|
+
userId: "user-1",
|
|
1093
|
+
botId: "primary",
|
|
1094
|
+
runId: "run-replay",
|
|
1095
|
+
sessionId: "user:primary",
|
|
1096
|
+
acceptedAt: "2026-08-28T00:00:00.000Z",
|
|
1097
|
+
text: "hello",
|
|
1098
|
+
};
|
|
1099
|
+
const run = {
|
|
1100
|
+
runId: original.runId,
|
|
1101
|
+
commandFingerprint: botTurnCommandFingerprintV1(original),
|
|
1102
|
+
sessionId: original.sessionId,
|
|
1103
|
+
acceptedAt: original.acceptedAt,
|
|
1104
|
+
input: original.text,
|
|
1105
|
+
events: [],
|
|
1106
|
+
effectAdmissions: [],
|
|
1107
|
+
status: "completed",
|
|
1108
|
+
phase: "executing",
|
|
1109
|
+
compositionGenerationId: "test-composition-generation",
|
|
1110
|
+
configurationSnapshot: initializeBotSettingsV1("primary"),
|
|
1111
|
+
previousEventCount: 0,
|
|
1112
|
+
responseText: "Durable reply",
|
|
1113
|
+
} satisfies StoredRun;
|
|
1114
|
+
const notification = {
|
|
1115
|
+
notificationId: run.runId,
|
|
1116
|
+
runId: run.runId,
|
|
1117
|
+
createdAt: "2026-08-28T00:00:01.000Z",
|
|
1118
|
+
title: "Bot replied",
|
|
1119
|
+
body: "Durable reply",
|
|
1120
|
+
};
|
|
1121
|
+
await storage.put({
|
|
1122
|
+
[`run:${run.runId}`]: run,
|
|
1123
|
+
[`notification:${run.runId}`]: notification,
|
|
1124
|
+
});
|
|
1125
|
+
const contribution = createShellBotBackendContribution({
|
|
1126
|
+
state: { storage } as unknown as DurableObjectState,
|
|
1127
|
+
env: {} as never,
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
await expect(
|
|
1131
|
+
contribution.run({
|
|
1132
|
+
...original,
|
|
1133
|
+
acceptedAt: "2026-08-29T00:00:00.000Z",
|
|
1134
|
+
}),
|
|
1135
|
+
).resolves.toMatchObject({
|
|
1136
|
+
runId: "run-replay",
|
|
1137
|
+
text: "Durable reply",
|
|
1138
|
+
notification,
|
|
1139
|
+
});
|
|
1140
|
+
await expect(
|
|
1141
|
+
contribution.run({ ...original, text: "different input" }),
|
|
1142
|
+
).rejects.toThrow(
|
|
1143
|
+
'Turn idempotency key "run-replay" was reused for a different command',
|
|
1144
|
+
);
|
|
1145
|
+
await expect(
|
|
1146
|
+
contribution.run({ ...original, sessionId: "user:other" }),
|
|
1147
|
+
).rejects.toThrow(
|
|
1148
|
+
'Turn idempotency key "run-replay" was reused for a different command',
|
|
1149
|
+
);
|
|
1150
|
+
await expect(
|
|
1151
|
+
contribution.run({ ...original, userId: "user-2" }),
|
|
1152
|
+
).rejects.toThrow(
|
|
1153
|
+
'Turn idempotency key "run-replay" was reused for a different command',
|
|
1154
|
+
);
|
|
1155
|
+
expect(storage.values.get(`run:${run.runId}`)).toEqual(run);
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
test("rejects a Turn collision before recovering durable work", async () => {
|
|
1159
|
+
const storage = new MemoryStorage();
|
|
1160
|
+
const original = {
|
|
1161
|
+
userId: "user-1",
|
|
1162
|
+
botId: "primary",
|
|
1163
|
+
runId: "run-collision",
|
|
1164
|
+
sessionId: "user:primary",
|
|
1165
|
+
acceptedAt: "2026-08-28T00:00:00.000Z",
|
|
1166
|
+
text: "original input",
|
|
1167
|
+
};
|
|
1168
|
+
const events = [
|
|
1169
|
+
{
|
|
1170
|
+
type: "assistant/message" as const,
|
|
1171
|
+
seq: 0,
|
|
1172
|
+
timestamp: "2026-08-28T00:00:01.000Z",
|
|
1173
|
+
turn: 1,
|
|
1174
|
+
step: 1,
|
|
1175
|
+
requestId: "request-collision",
|
|
1176
|
+
text: "Durable reply",
|
|
1177
|
+
toolCalls: [],
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
type: "turn/end" as const,
|
|
1181
|
+
seq: 1,
|
|
1182
|
+
timestamp: "2026-08-28T00:00:02.000Z",
|
|
1183
|
+
turn: 1,
|
|
1184
|
+
outcome: "completed" as const,
|
|
1185
|
+
},
|
|
1186
|
+
] satisfies SessionEvent[];
|
|
1187
|
+
const run = {
|
|
1188
|
+
runId: original.runId,
|
|
1189
|
+
commandFingerprint: botTurnCommandFingerprintV1(original),
|
|
1190
|
+
sessionId: original.sessionId,
|
|
1191
|
+
acceptedAt: original.acceptedAt,
|
|
1192
|
+
input: original.text,
|
|
1193
|
+
events,
|
|
1194
|
+
effectAdmissions: [],
|
|
1195
|
+
status: "running",
|
|
1196
|
+
phase: "executing",
|
|
1197
|
+
compositionGenerationId: "test-composition-generation",
|
|
1198
|
+
configurationSnapshot: initializeBotSettingsV1("primary"),
|
|
1199
|
+
previousEventCount: 0,
|
|
1200
|
+
} satisfies StoredRun;
|
|
1201
|
+
await storage.put({
|
|
1202
|
+
"active-run": run.runId,
|
|
1203
|
+
[`run:${run.runId}`]: run,
|
|
1204
|
+
"latest-events": events,
|
|
1205
|
+
});
|
|
1206
|
+
storage.alarmAt = Date.parse("2026-08-28T00:05:00.000Z");
|
|
1207
|
+
const before = structuredClone([...storage.values.entries()]);
|
|
1208
|
+
const alarmBefore = storage.alarmAt;
|
|
1209
|
+
const contribution = createShellBotBackendContribution({
|
|
1210
|
+
state: { storage } as unknown as DurableObjectState,
|
|
1211
|
+
env: {} as never,
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
await expect(
|
|
1215
|
+
contribution.run({ ...original, text: "colliding input" }),
|
|
1216
|
+
).rejects.toThrow(
|
|
1217
|
+
'Turn idempotency key "run-collision" was reused for a different command',
|
|
1218
|
+
);
|
|
1219
|
+
expect([...storage.values.entries()]).toEqual(before);
|
|
1220
|
+
expect(storage.alarmAt).toBe(alarmBefore);
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
test("looks up one durable command without replaying or scanning runs", async () => {
|
|
1224
|
+
const storage = new MemoryStorage();
|
|
1225
|
+
const contribution = createShellBotBackendContribution({
|
|
1226
|
+
state: { storage } as unknown as DurableObjectState,
|
|
1227
|
+
env: {} as never,
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
await expect(
|
|
1231
|
+
contribution.lookupRun({ schemaVersion: 1, runId: "command-1" }),
|
|
1232
|
+
).resolves.toEqual({ schemaVersion: 1, state: "not-admitted" });
|
|
1233
|
+
|
|
1234
|
+
const running = {
|
|
1235
|
+
runId: "command-1",
|
|
1236
|
+
commandFingerprint: "fingerprint",
|
|
1237
|
+
sessionId: "user:primary",
|
|
1238
|
+
acceptedAt: "2026-08-29T00:00:00.000Z",
|
|
1239
|
+
input: "continue",
|
|
1240
|
+
events: [],
|
|
1241
|
+
effectAdmissions: [],
|
|
1242
|
+
status: "running",
|
|
1243
|
+
phase: "executing",
|
|
1244
|
+
compositionGenerationId: "test-composition-generation",
|
|
1245
|
+
configurationSnapshot: initializeBotSettingsV1("primary"),
|
|
1246
|
+
previousEventCount: 0,
|
|
1247
|
+
} satisfies StoredRun;
|
|
1248
|
+
await storage.put("run:command-1", running);
|
|
1249
|
+
await expect(
|
|
1250
|
+
contribution.lookupRun({ schemaVersion: 1, runId: "command-1" }),
|
|
1251
|
+
).resolves.toMatchObject({
|
|
1252
|
+
schemaVersion: 1,
|
|
1253
|
+
state: "running",
|
|
1254
|
+
run: { runId: "command-1", status: "running" },
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
await storage.put("run:command-1", {
|
|
1258
|
+
...running,
|
|
1259
|
+
status: "completed",
|
|
1260
|
+
responseText: "done",
|
|
1261
|
+
} satisfies StoredRun);
|
|
1262
|
+
await expect(
|
|
1263
|
+
contribution.lookupRun({ schemaVersion: 1, runId: "command-1" }),
|
|
1264
|
+
).resolves.toMatchObject({
|
|
1265
|
+
schemaVersion: 1,
|
|
1266
|
+
state: "terminal",
|
|
1267
|
+
run: {
|
|
1268
|
+
runId: "command-1",
|
|
1269
|
+
status: "completed",
|
|
1270
|
+
outcome: { type: "completed", text: "done" },
|
|
1271
|
+
},
|
|
1272
|
+
});
|
|
1273
|
+
expect(storage.listRequests).toEqual([]);
|
|
1274
|
+
expect(storage.gets).toEqual([
|
|
1275
|
+
"run:command-1",
|
|
1276
|
+
"run:command-1",
|
|
1277
|
+
"run:command-1",
|
|
1278
|
+
]);
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
test("authoritatively fences delayed Turn admission", async () => {
|
|
1282
|
+
const storage = new MemoryStorage();
|
|
1283
|
+
const contribution = createShellBotBackendContribution({
|
|
1284
|
+
state: { storage } as unknown as DurableObjectState,
|
|
1285
|
+
env: {} as never,
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
await expect(
|
|
1289
|
+
contribution.fenceRunAdmission(
|
|
1290
|
+
{ userId: "user-1", botId: "primary" },
|
|
1291
|
+
{
|
|
1292
|
+
schemaVersion: 1,
|
|
1293
|
+
runId: "command-fenced",
|
|
1294
|
+
},
|
|
1295
|
+
),
|
|
1296
|
+
).resolves.toEqual({ schemaVersion: 1, state: "not-admitted" });
|
|
1297
|
+
expect(await storage.get<string[]>("run-admission-fences")).toEqual([
|
|
1298
|
+
"command-fenced",
|
|
1299
|
+
]);
|
|
1300
|
+
expect(
|
|
1301
|
+
await storage.get<{ userId: string; botId: string }>("identity"),
|
|
1302
|
+
).toEqual({
|
|
1303
|
+
userId: "user-1",
|
|
1304
|
+
botId: "primary",
|
|
1305
|
+
});
|
|
1306
|
+
await expect(
|
|
1307
|
+
contribution.fenceRunAdmission(
|
|
1308
|
+
{ userId: "other-user", botId: "primary" },
|
|
1309
|
+
{ schemaVersion: 1, runId: "other-command" },
|
|
1310
|
+
),
|
|
1311
|
+
).rejects.toThrow("Bot authority does not match its durable identity");
|
|
1312
|
+
expect(await storage.get<string[]>("run-admission-fences")).toEqual([
|
|
1313
|
+
"command-fenced",
|
|
1314
|
+
]);
|
|
1315
|
+
|
|
1316
|
+
await expect(
|
|
1317
|
+
contribution.run({
|
|
1318
|
+
userId: "user-1",
|
|
1319
|
+
botId: "primary",
|
|
1320
|
+
runId: "command-fenced",
|
|
1321
|
+
sessionId: "user-1:primary",
|
|
1322
|
+
acceptedAt: "2026-08-29T00:00:00.000Z",
|
|
1323
|
+
text: "must not execute",
|
|
1324
|
+
}),
|
|
1325
|
+
).rejects.toThrow('run "command-fenced" admission was fenced');
|
|
1326
|
+
expect(await storage.get("run:command-fenced")).toBeUndefined();
|
|
1327
|
+
|
|
1328
|
+
for (let index = 0; index < 255; index += 1) {
|
|
1329
|
+
await contribution.fenceRunAdmission(
|
|
1330
|
+
{ userId: "user-1", botId: "primary" },
|
|
1331
|
+
{ schemaVersion: 1, runId: `bounded-fence-${index}` },
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
await expect(
|
|
1335
|
+
contribution.fenceRunAdmission(
|
|
1336
|
+
{ userId: "user-1", botId: "primary" },
|
|
1337
|
+
{ schemaVersion: 1, runId: "fence-over-capacity" },
|
|
1338
|
+
),
|
|
1339
|
+
).rejects.toThrow("Run admission fence capacity reached");
|
|
1340
|
+
const fences = await storage.get<string[]>("run-admission-fences");
|
|
1341
|
+
expect(fences).toHaveLength(256);
|
|
1342
|
+
expect(fences).toContain("command-fenced");
|
|
1343
|
+
expect(fences).toContain("bounded-fence-0");
|
|
1344
|
+
expect(fences).not.toContain("fence-over-capacity");
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
test("rechecks a fence committed during execution-context resolution", async () => {
|
|
1348
|
+
const storage = new MemoryStorage();
|
|
1349
|
+
const contextStarted = Promise.withResolvers<void>();
|
|
1350
|
+
const continueContext = Promise.withResolvers<void>();
|
|
1351
|
+
const user: UserSettingsViewV1 = {
|
|
1352
|
+
schemaVersion: 1,
|
|
1353
|
+
revision: 1,
|
|
1354
|
+
profile: { name: "User" },
|
|
1355
|
+
packages: [
|
|
1356
|
+
{
|
|
1357
|
+
packageId: "provider-ollama-cloud",
|
|
1358
|
+
version: "0.0.1",
|
|
1359
|
+
state: "installed",
|
|
1360
|
+
},
|
|
1361
|
+
],
|
|
1362
|
+
connections: [
|
|
1363
|
+
{
|
|
1364
|
+
connectionId: "ollama-race",
|
|
1365
|
+
packageId: "provider-ollama-cloud",
|
|
1366
|
+
connectionTypeId: "ollama-cloud-account",
|
|
1367
|
+
displayName: "Race",
|
|
1368
|
+
state: "ready",
|
|
1369
|
+
providerType: "ollama-cloud",
|
|
1370
|
+
generation: "generation-race",
|
|
1371
|
+
safeMetadata: {},
|
|
1372
|
+
},
|
|
1373
|
+
],
|
|
1374
|
+
};
|
|
1375
|
+
const settings = {
|
|
1376
|
+
...initializeBotSettingsV1("primary"),
|
|
1377
|
+
model: {
|
|
1378
|
+
connectionId: "ollama-race",
|
|
1379
|
+
providerModelId: "model:cloud",
|
|
1380
|
+
},
|
|
1381
|
+
assignments: [
|
|
1382
|
+
{
|
|
1383
|
+
assignmentId: "model-race",
|
|
1384
|
+
packageId: "provider-ollama-cloud",
|
|
1385
|
+
capabilityId: "ollama-cloud-models",
|
|
1386
|
+
connectionId: "ollama-race",
|
|
1387
|
+
state: "enabled" as const,
|
|
1388
|
+
},
|
|
1389
|
+
],
|
|
1390
|
+
};
|
|
1391
|
+
await storage.put("bot-configuration", settings);
|
|
1392
|
+
const contribution = createShellBotBackendContribution({
|
|
1393
|
+
state: { storage } as unknown as DurableObjectState,
|
|
1394
|
+
env: {
|
|
1395
|
+
USER_CONFIGURATIONS: {
|
|
1396
|
+
idFromName: () => "user-1",
|
|
1397
|
+
get: () => ({
|
|
1398
|
+
readConfiguration: async () => {
|
|
1399
|
+
contextStarted.resolve();
|
|
1400
|
+
await continueContext.promise;
|
|
1401
|
+
return user;
|
|
1402
|
+
},
|
|
1403
|
+
}),
|
|
1404
|
+
},
|
|
1405
|
+
} as never,
|
|
1406
|
+
});
|
|
1407
|
+
const run = contribution.run({
|
|
1408
|
+
userId: "user-1",
|
|
1409
|
+
botId: "primary",
|
|
1410
|
+
runId: "fence-race",
|
|
1411
|
+
sessionId: "user-1:primary",
|
|
1412
|
+
acceptedAt: "2026-08-29T00:00:00.000Z",
|
|
1413
|
+
text: "must remain fenced",
|
|
1414
|
+
});
|
|
1415
|
+
await contextStarted.promise;
|
|
1416
|
+
await contribution.fenceRunAdmission(
|
|
1417
|
+
{ userId: "user-1", botId: "primary" },
|
|
1418
|
+
{ schemaVersion: 1, runId: "fence-race" },
|
|
1419
|
+
);
|
|
1420
|
+
continueContext.resolve();
|
|
1421
|
+
|
|
1422
|
+
await expect(run).rejects.toThrow('run "fence-race" admission was fenced');
|
|
1423
|
+
expect(await storage.get("run:fence-race")).toBeUndefined();
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
test("returns admitted state when admission wins the fence transaction", async () => {
|
|
1427
|
+
const storage = new MemoryStorage();
|
|
1428
|
+
const running = {
|
|
1429
|
+
runId: "command-running",
|
|
1430
|
+
commandFingerprint: "fingerprint",
|
|
1431
|
+
sessionId: "user:primary",
|
|
1432
|
+
acceptedAt: "2026-08-29T00:00:00.000Z",
|
|
1433
|
+
input: "continue",
|
|
1434
|
+
events: [],
|
|
1435
|
+
effectAdmissions: [],
|
|
1436
|
+
status: "running",
|
|
1437
|
+
phase: "executing",
|
|
1438
|
+
compositionGenerationId: "test-composition-generation",
|
|
1439
|
+
configurationSnapshot: initializeBotSettingsV1("primary"),
|
|
1440
|
+
previousEventCount: 0,
|
|
1441
|
+
} satisfies StoredRun;
|
|
1442
|
+
await storage.put("run:command-running", running);
|
|
1443
|
+
const contribution = createShellBotBackendContribution({
|
|
1444
|
+
state: { storage } as unknown as DurableObjectState,
|
|
1445
|
+
env: {} as never,
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
await expect(
|
|
1449
|
+
contribution.fenceRunAdmission(
|
|
1450
|
+
{ userId: "user-1", botId: "primary" },
|
|
1451
|
+
{
|
|
1452
|
+
schemaVersion: 1,
|
|
1453
|
+
runId: "command-running",
|
|
1454
|
+
},
|
|
1455
|
+
),
|
|
1456
|
+
).resolves.toMatchObject({
|
|
1457
|
+
schemaVersion: 1,
|
|
1458
|
+
state: "running",
|
|
1459
|
+
run: { runId: "command-running" },
|
|
1460
|
+
});
|
|
1461
|
+
expect(
|
|
1462
|
+
await storage.get("run-admission-fence:command-running"),
|
|
1463
|
+
).toBeUndefined();
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
test("does not scan pre-index run records as a compatibility path", async () => {
|
|
1467
|
+
const storage = new MemoryStorage();
|
|
1468
|
+
await storage.put("run:unindexed", {
|
|
1469
|
+
runId: "unindexed",
|
|
1470
|
+
commandFingerprint: "fingerprint",
|
|
1471
|
+
sessionId: "user:primary",
|
|
1472
|
+
acceptedAt: "2026-08-28T00:00:00.000Z",
|
|
1473
|
+
input: "legacy",
|
|
1474
|
+
events: [],
|
|
1475
|
+
effectAdmissions: [],
|
|
1476
|
+
status: "completed",
|
|
1477
|
+
phase: "executing",
|
|
1478
|
+
compositionGenerationId: "test-composition-generation",
|
|
1479
|
+
configurationSnapshot: initializeBotSettingsV1("primary"),
|
|
1480
|
+
previousEventCount: 0,
|
|
1481
|
+
responseText: "legacy",
|
|
1482
|
+
} satisfies StoredRun);
|
|
1483
|
+
const contribution = createShellBotBackendContribution({
|
|
1484
|
+
state: { storage } as unknown as DurableObjectState,
|
|
1485
|
+
env: {} as never,
|
|
1486
|
+
});
|
|
1487
|
+
storage.listRequests.length = 0;
|
|
1488
|
+
|
|
1489
|
+
await expect(
|
|
1490
|
+
contribution.listRuns({ schemaVersion: 1 }),
|
|
1491
|
+
).resolves.toMatchObject({ schemaVersion: 1, runs: [] });
|
|
1492
|
+
expect(
|
|
1493
|
+
storage.listRequests.some((request) => request.prefix === "run:"),
|
|
1494
|
+
).toBe(false);
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
test("pages large run history with bounded indexed reads and wire bytes", async () => {
|
|
1498
|
+
const storage = new MemoryStorage();
|
|
1499
|
+
const baseTime = Date.parse("2026-08-28T00:00:00.000Z");
|
|
1500
|
+
for (let index = 0; index < 100; index += 1) {
|
|
1501
|
+
const runId = `run-${index.toString().padStart(3, "0")}`;
|
|
1502
|
+
const acceptedAt = new Date(baseTime + index * 1_000).toISOString();
|
|
1503
|
+
const active = index === 99;
|
|
1504
|
+
const run = {
|
|
1505
|
+
runId,
|
|
1506
|
+
commandFingerprint: `fingerprint-${index}`,
|
|
1507
|
+
sessionId: "user:primary",
|
|
1508
|
+
acceptedAt,
|
|
1509
|
+
input: "๐งช".repeat(8_000),
|
|
1510
|
+
events: [],
|
|
1511
|
+
effectAdmissions: [],
|
|
1512
|
+
status: active ? "reconciliation-required" : "completed",
|
|
1513
|
+
phase: active ? "reconciliation-required" : "executing",
|
|
1514
|
+
compositionGenerationId: "test-composition-generation",
|
|
1515
|
+
configurationSnapshot: initializeBotSettingsV1("primary"),
|
|
1516
|
+
previousEventCount: 0,
|
|
1517
|
+
...(active
|
|
1518
|
+
? { failure: "Provider confirmation required" }
|
|
1519
|
+
: { responseText: "๐ฆ".repeat(16_000) }),
|
|
1520
|
+
} satisfies StoredRun;
|
|
1521
|
+
await storage.put({
|
|
1522
|
+
[`run:${runId}`]: run,
|
|
1523
|
+
[`run-index:${acceptedAt}:${runId}`]: runId,
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
await storage.put("active-run", "run-099");
|
|
1527
|
+
const contribution = createShellBotBackendContribution({
|
|
1528
|
+
state: { storage } as unknown as DurableObjectState,
|
|
1529
|
+
env: {} as never,
|
|
1530
|
+
});
|
|
1531
|
+
storage.gets.length = 0;
|
|
1532
|
+
storage.listRequests.length = 0;
|
|
1533
|
+
|
|
1534
|
+
const first = await contribution.listRuns({ schemaVersion: 1 });
|
|
1535
|
+
|
|
1536
|
+
expect(first.runs.length).toBeLessThanOrEqual(CLIENT_RUN_PAGE_LIMIT);
|
|
1537
|
+
expect(first.runs.map((run) => run.runId)).toContain("run-099");
|
|
1538
|
+
expect(first.runs.map((run) => run.runId)).toContain("run-098");
|
|
1539
|
+
expect(first.page).toMatchObject({ truncated: true });
|
|
1540
|
+
expect(clientRunListWireBytes(first)).toBeLessThanOrEqual(
|
|
1541
|
+
CLIENT_RUN_LIST_MAX_BYTES,
|
|
1542
|
+
);
|
|
1543
|
+
expect(
|
|
1544
|
+
storage.listRequests.find((request) => request.prefix === "run-index:"),
|
|
1545
|
+
).toMatchObject({
|
|
1546
|
+
reverse: true,
|
|
1547
|
+
limit: CLIENT_RUN_PAGE_LIMIT + 1,
|
|
1548
|
+
});
|
|
1549
|
+
expect(
|
|
1550
|
+
storage.listRequests.some((request) => request.prefix === "run:"),
|
|
1551
|
+
).toBe(false);
|
|
1552
|
+
expect(
|
|
1553
|
+
storage.gets.filter((key) => key.startsWith("run:")).length,
|
|
1554
|
+
).toBeLessThan(CLIENT_RUN_PAGE_LIMIT);
|
|
1555
|
+
|
|
1556
|
+
const nextCursor = first.page.nextCursor;
|
|
1557
|
+
if (!nextCursor) throw new Error("expected a paginated run cursor");
|
|
1558
|
+
const second = await contribution.listRuns({
|
|
1559
|
+
schemaVersion: 1,
|
|
1560
|
+
before: nextCursor,
|
|
1561
|
+
});
|
|
1562
|
+
expect(second.runs.map((run) => run.runId)).not.toContain("run-099");
|
|
1563
|
+
expect(second.runs.map((run) => run.runId)).not.toContain("run-098");
|
|
1564
|
+
expect(clientRunListWireBytes(second)).toBeLessThanOrEqual(
|
|
1565
|
+
CLIENT_RUN_LIST_MAX_BYTES,
|
|
1566
|
+
);
|
|
1567
|
+
expect(
|
|
1568
|
+
second.runs.every(
|
|
1569
|
+
(run, index) =>
|
|
1570
|
+
index === 0 ||
|
|
1571
|
+
second.runs[index - 1]!.admittedAt.localeCompare(run.admittedAt) <= 0,
|
|
1572
|
+
),
|
|
1573
|
+
).toBe(true);
|
|
1574
|
+
});
|
|
1575
|
+
});
|