@ixo/oracle-runtime-workers 0.10.0 → 0.11.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/README.md +5 -2
- package/package.json +1 -1
- package/src/core/context-budget.ts +201 -0
- package/src/core/context-window.ts +289 -0
- package/src/core/env.ts +29 -0
- package/src/core/main-agent-types.ts +46 -0
- package/src/core/main-agent.ts +86 -9
- package/src/core/middlewares/context-guard.ts +270 -0
- package/src/core/middlewares/result-cap.ts +180 -0
- package/src/core/middlewares/summarization.ts +87 -6
- package/src/core/middlewares/tool-marks.ts +209 -0
- package/src/core/openrouter-pricing.ts +42 -1
- package/src/core/read-result-tool.ts +77 -0
- package/src/core/runtime-context.ts +7 -0
- package/src/core/sub-agent-fallback.ts +20 -3
- package/src/core/wrap-plugin-tool.ts +24 -3
- package/src/do/ambient.ts +17 -0
- package/src/do/contracts.ts +229 -0
- package/src/do/flush-schedule.ts +40 -0
- package/src/do/result-store-test-do.ts +88 -0
- package/src/do/result-store.ts +339 -0
- package/src/do/run-buffer.ts +204 -0
- package/src/do/run-coordinator.ts +572 -0
- package/src/do/run-request.ts +52 -0
- package/src/do/run-store-test-do.ts +130 -0
- package/src/do/run-store.ts +604 -0
- package/src/do/session-id-migration.ts +85 -0
- package/src/do/speaker-prefix.ts +37 -0
- package/src/do/sse-stream.ts +567 -400
- package/src/do/transcript.ts +238 -0
- package/src/do/turn-body.ts +10 -0
- package/src/do/user-oracle-do.ts +948 -162
- package/src/matrix/gateway-do.ts +230 -29
- package/src/matrix/group-chat-summarizer.ts +107 -0
- package/src/matrix/group-chat.ts +1163 -0
- package/src/matrix/inbox-store.ts +23 -3
- package/src/matrix/ingest.ts +54 -11
- package/src/matrix/reply-chain.ts +34 -25
- package/src/plugin-api/tool-helper.ts +9 -1
- package/src/plugin-api/types.ts +86 -0
- package/src/plugins/index.ts +3 -0
- package/src/plugins/matrix-group-chats/README.md +70 -0
- package/src/plugins/matrix-group-chats/index.ts +132 -0
- package/src/plugins/matrix-group-chats/tools.ts +172 -0
- package/src/plugins/tasks/tasks-tools.ts +4 -0
- package/src/shell/app.ts +66 -0
- package/src/sqlite/sqlite-saver.ts +81 -0
- package/src/tasks/scheduler.ts +134 -3
- package/src/tasks/store.ts +22 -0
package/src/do/contracts.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TranscriptPageOptions } from './transcript';
|
|
1
2
|
import type { BotStatus, EncryptedFileInfo } from '@ixo/matrix-bot-workers-sdk';
|
|
2
3
|
import type { AttachmentInput } from '../attachments/types';
|
|
3
4
|
import type { RealtimeStatus } from '../realtime/realtime-endpoint';
|
|
@@ -61,6 +62,11 @@ export interface OracleWorkerEnv {
|
|
|
61
62
|
MATRIX_SEND_CONCURRENCY?: string;
|
|
62
63
|
/** Room turns in flight at once in the gateway (each ends in an encrypted reply; default 4). */
|
|
63
64
|
MATRIX_TURN_CONCURRENCY?: string;
|
|
65
|
+
/** Group rooms (gateway): `silent` (default) never speaks there, `gate` runs the Node lane, `answer` replies to everything. */
|
|
66
|
+
MATRIX_GROUP_ROOMS?: string;
|
|
67
|
+
GROUP_CHAT_ACTIVE_THREAD_TTL_MS?: string;
|
|
68
|
+
GROUP_CHAT_REQUIRE_POWER_LEVEL?: string;
|
|
69
|
+
GROUP_CHAT_ROOM_INFO_TTL_MS?: string;
|
|
64
70
|
/** Idle recycle of the gateway object after this many sends (default 300; 0 disables). */
|
|
65
71
|
MATRIX_RECYCLE_AFTER_SENDS?: string;
|
|
66
72
|
/** Rooms kept fully built in memory (SDK default 64; idle rooms are released). */
|
|
@@ -124,6 +130,26 @@ export interface OracleWorkerEnv {
|
|
|
124
130
|
MAIN_REASONING_EFFORT?: string;
|
|
125
131
|
/** LangGraph steps one turn may take before `GraphRecursionError` (default 600; Node hard-codes 200). */
|
|
126
132
|
TURN_RECURSION_LIMIT?: string;
|
|
133
|
+
/** Durable-run knobs (docs/plans/durable-runs.md); see `runDurabilityConfig`. */
|
|
134
|
+
RUN_KEEPALIVE_MS?: string;
|
|
135
|
+
RUN_SEGMENT_FLUSH_MS?: string;
|
|
136
|
+
RUN_SEGMENT_BYTES?: string;
|
|
137
|
+
RUN_RECOVERY_ATTEMPTS?: string;
|
|
138
|
+
RUN_RECOVERY_DELAYS_MS?: string;
|
|
139
|
+
TURN_MULTITASK_DEFAULT?: string;
|
|
140
|
+
/** Context-budget knobs (docs/plans/context-budgets.md). */
|
|
141
|
+
MODEL_CONTEXT_TOKENS?: string;
|
|
142
|
+
MODEL_CONTEXT_OVERRIDES?: string;
|
|
143
|
+
CONTEXT_SUMMARIZE_FRACTION?: string;
|
|
144
|
+
CONTEXT_PRUNE_FRACTION?: string;
|
|
145
|
+
CONTEXT_RESULT_CAP_FRACTION?: string;
|
|
146
|
+
CONTEXT_RESULT_CAP_MAX_CHARS?: string;
|
|
147
|
+
CONTEXT_REQUEST_FRACTION?: string;
|
|
148
|
+
CONTEXT_OUTPUT_RESERVE_TOKENS?: string;
|
|
149
|
+
CONTEXT_SUMMARIZE_MESSAGES?: string;
|
|
150
|
+
CONTEXT_KEEP_MESSAGES?: string;
|
|
151
|
+
TOOL_RESULT_TTL_HOURS?: string;
|
|
152
|
+
TOOL_RESULT_R2_MIN_BYTES?: string;
|
|
127
153
|
/** Platform model provider. Default `openrouter`; `nebius` for self-hosted. */
|
|
128
154
|
LLM_PROVIDER?: 'openrouter' | 'nebius';
|
|
129
155
|
/** Nebius Token Factory API key — required when `LLM_PROVIDER=nebius`. */
|
|
@@ -203,6 +229,14 @@ export interface TurnRequest {
|
|
|
203
229
|
threadId?: string;
|
|
204
230
|
/** The user's message event this turn answers (Matrix turns): anchors the `work_status` card. */
|
|
205
231
|
eventId?: string;
|
|
232
|
+
/**
|
|
233
|
+
* Matrix turns: whether the room is a direct room or a group room (more
|
|
234
|
+
* than two members) as the gateway's group-chat gate saw it. A group turn's
|
|
235
|
+
* message is prefixed `[DisplayName]: ` and gets the channel-memory tools.
|
|
236
|
+
*/
|
|
237
|
+
roomKind?: 'direct' | 'group';
|
|
238
|
+
/** The speaker's display name in the room (Matrix turns). */
|
|
239
|
+
senderDisplayName?: string;
|
|
206
240
|
requestId: string;
|
|
207
241
|
/** Per-request model override (validated against the catalog). */
|
|
208
242
|
model?: string;
|
|
@@ -210,6 +244,134 @@ export interface TurnRequest {
|
|
|
210
244
|
metadata?: JsonString;
|
|
211
245
|
/** Files attached to the message (validated `AttachmentDto` shape). */
|
|
212
246
|
attachments?: AttachmentInput[];
|
|
247
|
+
/**
|
|
248
|
+
* What this message does to a run already active on the same session:
|
|
249
|
+
* `interrupt` (default; the running turn is aborted, Node parity) or
|
|
250
|
+
* `enqueue` (it waits its turn). See docs/plans/durable-runs.md.
|
|
251
|
+
*/
|
|
252
|
+
multitask?: 'interrupt' | 'enqueue';
|
|
253
|
+
/** The scheduler's run id when this turn executes a scheduled task. */
|
|
254
|
+
taskRunId?: string;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** A run as the shell reports it (`GET /sessions/:id/run`, `GET /debug/runs`). */
|
|
258
|
+
/** `GET /debug/context` — the context budget a model gets on this object. */
|
|
259
|
+
/**
|
|
260
|
+
* Per-session context counters (`GET /debug/context?session=`): what the
|
|
261
|
+
* guard did across the session's turns plus the thread as stored, so a test
|
|
262
|
+
* or an operator can see pruning and summarization without the logs.
|
|
263
|
+
*/
|
|
264
|
+
export interface SessionContextStatus {
|
|
265
|
+
id: string;
|
|
266
|
+
/** Every row of the stored transcript (never condensed; what the user sees). */
|
|
267
|
+
threadMessages: number;
|
|
268
|
+
/**
|
|
269
|
+
* The working context: the messages the latest checkpoint carries into
|
|
270
|
+
* the next request — after a summary, one summary message plus the kept
|
|
271
|
+
* tail, however long the transcript is.
|
|
272
|
+
*/
|
|
273
|
+
contextMessages: number;
|
|
274
|
+
/** Summary messages in the working context (1 once the history was condensed). */
|
|
275
|
+
contextSummaries: number;
|
|
276
|
+
contextToolMessages: number;
|
|
277
|
+
/** chars/4 estimate of the working context. */
|
|
278
|
+
contextTokens: number;
|
|
279
|
+
/** Requests the guard pruned before sending (soft or hard). */
|
|
280
|
+
prunes: number;
|
|
281
|
+
hardPrunes: number;
|
|
282
|
+
/** Tool results demoted or de-duplicated in total. */
|
|
283
|
+
prunedResults: number;
|
|
284
|
+
overflowRetries: number;
|
|
285
|
+
refusals: number;
|
|
286
|
+
lastEventAt?: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export interface ContextStatus {
|
|
290
|
+
model: string;
|
|
291
|
+
byoProvider?: string;
|
|
292
|
+
window: { tokens: number; origin: string; catalogId?: string };
|
|
293
|
+
budget: {
|
|
294
|
+
summarizeAtTokens: number;
|
|
295
|
+
pruneAtTokens: number;
|
|
296
|
+
resultCapChars: number;
|
|
297
|
+
requestCapTokens: number;
|
|
298
|
+
outputReserveTokens: number;
|
|
299
|
+
keepMessages: number;
|
|
300
|
+
summarizeTriggerMessages?: number;
|
|
301
|
+
};
|
|
302
|
+
results: {
|
|
303
|
+
rows: number;
|
|
304
|
+
sqliteBytes: number;
|
|
305
|
+
r2Rows: number;
|
|
306
|
+
r2Bytes: number;
|
|
307
|
+
tier: 'sqlite+r2' | 'sqlite';
|
|
308
|
+
};
|
|
309
|
+
/** Present when a session was asked for; null when that session is unknown. */
|
|
310
|
+
session?: SessionContextStatus | null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface RunSummary {
|
|
314
|
+
runId: string;
|
|
315
|
+
sessionId: string;
|
|
316
|
+
requestId: string;
|
|
317
|
+
client: 'portal' | 'matrix';
|
|
318
|
+
status:
|
|
319
|
+
| 'queued'
|
|
320
|
+
| 'running'
|
|
321
|
+
| 'recovering'
|
|
322
|
+
| 'finished'
|
|
323
|
+
| 'aborted'
|
|
324
|
+
| 'interrupted'
|
|
325
|
+
| 'failed';
|
|
326
|
+
startedAt: string;
|
|
327
|
+
updatedAt: string;
|
|
328
|
+
/** Highest frame sequence a re-join can ask `after`. */
|
|
329
|
+
lastSeq: number;
|
|
330
|
+
messageId?: string;
|
|
331
|
+
/** The reply so far, for a run that ended without a committed reply. */
|
|
332
|
+
partialText?: string;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** `GET /debug/runs`: the durable-run configuration, live runs and recent rows. */
|
|
336
|
+
export interface RunsStatus {
|
|
337
|
+
config: {
|
|
338
|
+
keepAliveMs: number;
|
|
339
|
+
segmentFlushMs: number;
|
|
340
|
+
segmentBytes: number;
|
|
341
|
+
recoveryAttempts: number;
|
|
342
|
+
recoveryDelaysMs: readonly number[];
|
|
343
|
+
multitaskDefault: 'interrupt' | 'enqueue';
|
|
344
|
+
};
|
|
345
|
+
live: Array<{
|
|
346
|
+
runId: string;
|
|
347
|
+
sessionId: string;
|
|
348
|
+
status: string;
|
|
349
|
+
attemptInFlight: boolean;
|
|
350
|
+
generation: number;
|
|
351
|
+
lastSeq: number;
|
|
352
|
+
/** Highest frame sequence packed into a segment so far. */
|
|
353
|
+
packedSeq: number;
|
|
354
|
+
subscribers: number;
|
|
355
|
+
nextAttemptAt: number | null;
|
|
356
|
+
}>;
|
|
357
|
+
runs: Array<
|
|
358
|
+
RunSummary & {
|
|
359
|
+
marks: Array<{
|
|
360
|
+
toolCallId: string;
|
|
361
|
+
toolName: string;
|
|
362
|
+
effect: 'read' | 'write';
|
|
363
|
+
startedAt: string;
|
|
364
|
+
doneAt: string | null;
|
|
365
|
+
outcome: string | null;
|
|
366
|
+
attempts: number;
|
|
367
|
+
}>;
|
|
368
|
+
segments: number;
|
|
369
|
+
attempts: number;
|
|
370
|
+
nextAttemptAt: number | null;
|
|
371
|
+
error: string | null;
|
|
372
|
+
taskRunId: string | null;
|
|
373
|
+
}
|
|
374
|
+
>;
|
|
213
375
|
}
|
|
214
376
|
|
|
215
377
|
/** Non-streaming turn result. */
|
|
@@ -277,6 +439,8 @@ export interface StorageStatus {
|
|
|
277
439
|
pages: number;
|
|
278
440
|
ownerEtag?: string;
|
|
279
441
|
lastFlushAt?: number;
|
|
442
|
+
/** When the dirty copy is due to be uploaded (absent when clean). */
|
|
443
|
+
nextFlushAt?: number;
|
|
280
444
|
dirty: boolean;
|
|
281
445
|
flushInFlight: boolean;
|
|
282
446
|
/** VFS write generation now vs. at the last successful upload. */
|
|
@@ -323,6 +487,13 @@ export interface StorageStatus {
|
|
|
323
487
|
export interface UserOracleObject extends Rpc.DurableObjectBranded {
|
|
324
488
|
/** Run a turn and return the final text (used by the Matrix gateway). */
|
|
325
489
|
runTurn(req: TurnRequest): Promise<TurnResult>;
|
|
490
|
+
/**
|
|
491
|
+
* Compact a batch of group-room messages (JSON `ObservedMessage[]`) into
|
|
492
|
+
* one channel-memory summary with the platform model, for the gateway's
|
|
493
|
+
* per-room memory (`src/matrix/group-chat.ts`). `null` when the model
|
|
494
|
+
* answered nothing — the gateway keeps the batch buffered.
|
|
495
|
+
*/
|
|
496
|
+
summarizeGroupMessages(messages: JsonString): Promise<string | null>;
|
|
326
497
|
createSession(
|
|
327
498
|
identity: TurnIdentity,
|
|
328
499
|
opts?: { roomId?: string; sessionId?: string },
|
|
@@ -334,7 +505,33 @@ export interface UserOracleObject extends Rpc.DurableObjectBranded {
|
|
|
334
505
|
deleteSession(identity: TurnIdentity, sessionId: string): Promise<boolean>;
|
|
335
506
|
/** JSON-encoded `MessageDto[]` for `GET /messages/:sessionId`. */
|
|
336
507
|
listMessages(identity: TurnIdentity, sessionId: string): Promise<JsonString>;
|
|
508
|
+
/**
|
|
509
|
+
* `GET /sessions/:id/messages` — one turn-aligned page of the transcript
|
|
510
|
+
* (docs/plans/transcript-paging.md); the JSON of a `TranscriptPage`, or
|
|
511
|
+
* the 400 to answer with when the cursor names no row.
|
|
512
|
+
*/
|
|
513
|
+
listMessagesPage(
|
|
514
|
+
identity: TurnIdentity,
|
|
515
|
+
sessionId: string,
|
|
516
|
+
options: TranscriptPageOptions,
|
|
517
|
+
): Promise<
|
|
518
|
+
{ ok: true; json: JsonString } | { ok: false; status: 400; message: string }
|
|
519
|
+
>;
|
|
337
520
|
abortTurn(sessionId: string): Promise<boolean>;
|
|
521
|
+
/** The session's active (queued/running/recovering) run, if any. */
|
|
522
|
+
sessionRun(
|
|
523
|
+
identity: TurnIdentity,
|
|
524
|
+
sessionId: string,
|
|
525
|
+
): Promise<RunSummary | null>;
|
|
526
|
+
/** Diagnostics: recent runs with their tool marks and live state. */
|
|
527
|
+
runsStatus(): Promise<RunsStatus>;
|
|
528
|
+
/** `GET /debug/context` — the context budget a model gets on this object. */
|
|
529
|
+
contextStatus(
|
|
530
|
+
identity: TurnIdentity,
|
|
531
|
+
model?: string,
|
|
532
|
+
byoProvider?: string,
|
|
533
|
+
sessionId?: string,
|
|
534
|
+
): Promise<ContextStatus>;
|
|
338
535
|
/** Adopt a freshly deposited delegation for header-less turns (no boot). */
|
|
339
536
|
setDelegation(
|
|
340
537
|
userDid: string,
|
|
@@ -566,6 +763,28 @@ export interface MatrixGatewayObject extends Rpc.DurableObjectBranded {
|
|
|
566
763
|
getRoomState(roomId: string): Promise<JsonString>;
|
|
567
764
|
/** One timeline event (decrypted when E2EE) as JSON `{ event_id, type, content, sender, origin_server_ts }`, or null. */
|
|
568
765
|
getEvent(roomId: string, eventId: string): Promise<JsonString | null>;
|
|
766
|
+
/**
|
|
767
|
+
* Group rooms (`src/matrix/group-chat.ts`): whether a room is direct or a
|
|
768
|
+
* group room, and the room's channel memory — what the user object's
|
|
769
|
+
* `matrix-group-chats` tools read and write. Payloads are JSON strings.
|
|
770
|
+
*/
|
|
771
|
+
groupChatRoomInfo(roomId: string): Promise<GroupRoomInfo>;
|
|
772
|
+
/** `{ chunks, pinnedFacts, members }` — the newest `limit` summary chunks (1..30, default 10). */
|
|
773
|
+
channelMemoryRecall(roomId: string, limit?: number): Promise<JsonString>;
|
|
774
|
+
/** Matching summary chunks for the keywords. */
|
|
775
|
+
channelMemorySearch(
|
|
776
|
+
roomId: string,
|
|
777
|
+
query: string,
|
|
778
|
+
limit?: number,
|
|
779
|
+
): Promise<JsonString>;
|
|
780
|
+
/** The pinned fact as stored. */
|
|
781
|
+
channelMemoryPin(
|
|
782
|
+
roomId: string,
|
|
783
|
+
fact: string,
|
|
784
|
+
pinnedByDid: string,
|
|
785
|
+
sourceEventId?: string,
|
|
786
|
+
): Promise<JsonString>;
|
|
787
|
+
channelMemoryUnpin(roomId: string, factId: string): Promise<boolean>;
|
|
569
788
|
/**
|
|
570
789
|
* The oracle's P-256 secrets key, JSON-encoded private JWK. Read from the
|
|
571
790
|
* oracle's Matrix *account* room (`MATRIX_ACCOUNT_ROOM_ID`) — the published
|
|
@@ -626,8 +845,18 @@ export interface SnapshotStream extends MediaStream {
|
|
|
626
845
|
eventId: string;
|
|
627
846
|
}
|
|
628
847
|
|
|
848
|
+
export interface GroupRoomInfo {
|
|
849
|
+
isDirect: boolean;
|
|
850
|
+
memberCount: number;
|
|
851
|
+
joinedMemberIds: string[];
|
|
852
|
+
/** Whether this deployment runs the group-chat lane (`MATRIX_GROUP_ROOMS=gate`); the channel-memory tools exist only then. */
|
|
853
|
+
groupLane: boolean;
|
|
854
|
+
}
|
|
855
|
+
|
|
629
856
|
export interface GatewayStatus extends BotStatus {
|
|
630
857
|
turns: { inFlight: number; waiting: number };
|
|
858
|
+
/** Channel memory of group rooms: messages awaiting compaction, summary chunks, pinned facts. */
|
|
859
|
+
groupChat: { buffered: number; chunks: number; facts: number };
|
|
631
860
|
/** Debounce buffers waiting to become turns. */
|
|
632
861
|
ingestPending: number;
|
|
633
862
|
/** Room messages whose turn has not ended yet (durable; replayed after a reset). */
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a housekeeping wake should upload the working copy to the owner
|
|
3
|
+
* store. The object's single alarm serves several clients (realtime
|
|
4
|
+
* heartbeats, the durable-run keep-alive, task runs, compaction, the R2
|
|
5
|
+
* tier, idle eviction), so a wake says nothing about WHY it happened. The
|
|
6
|
+
* flush has its own persisted deadline (`meta:flushAt`, armed by the first
|
|
7
|
+
* write after an upload and replaced by the retry delay after a failure)
|
|
8
|
+
* and runs only when that deadline is due — every other wake leaves a
|
|
9
|
+
* dirty copy alone. Two exceptions upload right away: a copy about to be
|
|
10
|
+
* evicted (the wipe must never drop unsaved turns) and a copy marked dirty
|
|
11
|
+
* before the deadline existed (older objects; one upload, then the
|
|
12
|
+
* debounce applies).
|
|
13
|
+
*/
|
|
14
|
+
export interface FlushScheduleInput {
|
|
15
|
+
/** The object holds writes the owner store has not seen (and a database is open). */
|
|
16
|
+
dirty: boolean;
|
|
17
|
+
/** The persisted flush deadline (undefined = none recorded). */
|
|
18
|
+
flushAt: unknown;
|
|
19
|
+
now: number;
|
|
20
|
+
/** The idle-eviction path is about to wipe the working copy. */
|
|
21
|
+
idle: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type FlushScheduleDecision =
|
|
25
|
+
| { action: 'skip'; reason: 'clean' }
|
|
26
|
+
| { action: 'flush'; reason: 'due' | 'idle' | 'no-deadline' }
|
|
27
|
+
| { action: 'wait'; at: number };
|
|
28
|
+
|
|
29
|
+
/** Alarms fire a hair early; a deadline this close counts as due. */
|
|
30
|
+
export const FLUSH_DUE_SLACK_MS = 1000;
|
|
31
|
+
|
|
32
|
+
export function decideFlush(input: FlushScheduleInput): FlushScheduleDecision {
|
|
33
|
+
if (!input.dirty) return { action: 'skip', reason: 'clean' };
|
|
34
|
+
if (input.idle) return { action: 'flush', reason: 'idle' };
|
|
35
|
+
if (typeof input.flushAt !== 'number' || !Number.isFinite(input.flushAt))
|
|
36
|
+
return { action: 'flush', reason: 'no-deadline' };
|
|
37
|
+
if (input.flushAt <= input.now + FLUSH_DUE_SLACK_MS)
|
|
38
|
+
return { action: 'flush', reason: 'due' };
|
|
39
|
+
return { action: 'wait', at: input.flushAt };
|
|
40
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test-only Durable Object driving `ResultStore` over real DO SQLite and the
|
|
3
|
+
* test R2 bucket inside workerd. Bound as `RESULT_STORE_TEST` by
|
|
4
|
+
* `test/wrangler.test.jsonc`. Not part of the runtime.
|
|
5
|
+
*/
|
|
6
|
+
import { DurableObject } from 'cloudflare:workers';
|
|
7
|
+
import { DoSqliteDatabase } from '../sqlite/database';
|
|
8
|
+
import {
|
|
9
|
+
ResultStore,
|
|
10
|
+
type ReadResultOutcome,
|
|
11
|
+
type StoredResultRef,
|
|
12
|
+
} from './result-store';
|
|
13
|
+
|
|
14
|
+
export class ResultStoreTestDO extends DurableObject<{ TIER_TEST: R2Bucket }> {
|
|
15
|
+
private db: DoSqliteDatabase | undefined;
|
|
16
|
+
|
|
17
|
+
private store: ResultStore | undefined;
|
|
18
|
+
|
|
19
|
+
private nowMs = Date.parse('2026-09-14T10:00:00.000Z');
|
|
20
|
+
|
|
21
|
+
private r2MinBytes = 1_000_000;
|
|
22
|
+
|
|
23
|
+
private withBucket = true;
|
|
24
|
+
|
|
25
|
+
private async resultStore(): Promise<ResultStore> {
|
|
26
|
+
if (!this.db || !this.db.isOpen) {
|
|
27
|
+
this.db = await DoSqliteDatabase.open(this.ctx, 'results-test.db');
|
|
28
|
+
this.store = undefined;
|
|
29
|
+
}
|
|
30
|
+
this.store ??= new ResultStore(this.db, {
|
|
31
|
+
...(this.withBucket ? { bucket: this.env.TIER_TEST } : {}),
|
|
32
|
+
prefix: `test-${this.ctx.id.toString()}`,
|
|
33
|
+
ttlMs: 60 * 60 * 1000,
|
|
34
|
+
r2MinBytes: this.r2MinBytes,
|
|
35
|
+
now: () => this.nowMs,
|
|
36
|
+
});
|
|
37
|
+
return this.store;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async configure(opts: {
|
|
41
|
+
r2MinBytes?: number;
|
|
42
|
+
withBucket?: boolean;
|
|
43
|
+
}): Promise<void> {
|
|
44
|
+
if (opts.r2MinBytes !== undefined) this.r2MinBytes = opts.r2MinBytes;
|
|
45
|
+
if (opts.withBucket !== undefined) this.withBucket = opts.withBucket;
|
|
46
|
+
this.store = undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async setNow(ms: number): Promise<void> {
|
|
50
|
+
this.nowMs = ms;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async put(input: {
|
|
54
|
+
sessionId: string;
|
|
55
|
+
toolName: string;
|
|
56
|
+
content: string;
|
|
57
|
+
}): Promise<StoredResultRef | undefined> {
|
|
58
|
+
return (await this.resultStore()).put(input);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async read(
|
|
62
|
+
id: string,
|
|
63
|
+
offset?: number,
|
|
64
|
+
length?: number,
|
|
65
|
+
): Promise<ReadResultOutcome> {
|
|
66
|
+
return (await this.resultStore()).read(id, offset, length);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async sweep(): Promise<number> {
|
|
70
|
+
return (await this.resultStore()).sweep();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async deleteForSession(sessionId: string): Promise<number> {
|
|
74
|
+
return (await this.resultStore()).deleteForSession(sessionId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async stats(): Promise<Awaited<ReturnType<ResultStore['stats']>>> {
|
|
78
|
+
return (await this.resultStore()).stats();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async r2Has(id: string): Promise<boolean> {
|
|
82
|
+
return (
|
|
83
|
+
(await this.env.TIER_TEST.head(
|
|
84
|
+
`test-${this.ctx.id.toString()}/results/${id}`,
|
|
85
|
+
)) !== null
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|