@ixo/oracle-runtime-workers 0.13.0 → 0.16.0

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 (205) hide show
  1. package/README.md +32 -10
  2. package/package.json +13 -8
  3. package/src/artifacts/config.ts +158 -0
  4. package/src/artifacts/crypto.ts +94 -0
  5. package/src/artifacts/routes.ts +73 -0
  6. package/src/artifacts/store.ts +238 -0
  7. package/src/artifacts/sweep.ts +200 -0
  8. package/src/artifacts/tool.ts +103 -0
  9. package/src/artifacts/viewer-page.ts +614 -0
  10. package/src/attachments/download.ts +168 -45
  11. package/src/attachments/index.ts +9 -0
  12. package/src/attachments/magic.ts +33 -0
  13. package/src/attachments/pipeline.ts +105 -49
  14. package/src/attachments/sandbox-archive.ts +9 -8
  15. package/src/attachments/view-cache.ts +142 -0
  16. package/src/attachments/view.ts +53 -12
  17. package/src/channels/auth.ts +310 -0
  18. package/src/channels/contract.ts +246 -0
  19. package/src/channels/turns.ts +280 -0
  20. package/src/core/budgeted-llm.ts +223 -0
  21. package/src/core/capability-router.ts +273 -0
  22. package/src/core/context-budget.ts +97 -10
  23. package/src/core/env.ts +111 -0
  24. package/src/core/execution-profile.ts +17 -0
  25. package/src/core/index.ts +267 -9
  26. package/src/core/llm.ts +30 -9
  27. package/src/core/main-agent-types.ts +41 -1
  28. package/src/core/main-agent.ts +367 -65
  29. package/src/core/manifest.ts +34 -0
  30. package/src/core/meta-tools.ts +128 -18
  31. package/src/core/middlewares/capability-gate.ts +130 -12
  32. package/src/core/middlewares/context-guard.ts +55 -24
  33. package/src/core/middlewares/index.ts +20 -2
  34. package/src/core/middlewares/page-context.ts +46 -2
  35. package/src/core/middlewares/return-direct-first.ts +47 -0
  36. package/src/core/middlewares/summarization.ts +152 -10
  37. package/src/core/middlewares/tool-execution.ts +323 -0
  38. package/src/core/middlewares/tool-repetition-guard.ts +202 -35
  39. package/src/core/middlewares/turn-boundary.ts +162 -0
  40. package/src/core/openrouter-pricing.ts +155 -22
  41. package/src/core/prompt-composer.ts +112 -7
  42. package/src/core/registries.ts +253 -48
  43. package/src/core/request-admission.ts +159 -0
  44. package/src/core/runtime-context.ts +107 -8
  45. package/src/core/state.ts +1 -1
  46. package/src/core/sub-agent-fallback.ts +21 -2
  47. package/src/core/subagent-as-tool.ts +78 -43
  48. package/src/core/tool-access.ts +189 -0
  49. package/src/core/tool-scheduler.ts +125 -0
  50. package/src/core/turn-budget.ts +205 -0
  51. package/src/core/turn-time-note.ts +43 -0
  52. package/src/core/user-kv.ts +446 -0
  53. package/src/core/wrap-plugin-tool.ts +20 -0
  54. package/src/delivery/plan.ts +278 -0
  55. package/src/delivery/profile.ts +161 -0
  56. package/src/delivery/prompt.ts +27 -0
  57. package/src/delivery/schema.ts +61 -0
  58. package/src/delivery/shaper.ts +389 -0
  59. package/src/delivery/types.ts +84 -0
  60. package/src/do/ambient.ts +51 -15
  61. package/src/do/contracts.ts +170 -6
  62. package/src/do/idle-eviction.ts +38 -0
  63. package/src/do/matrix-turn-ledger.ts +24 -5
  64. package/src/do/result-store-test-do.ts +137 -3
  65. package/src/do/result-store.ts +239 -58
  66. package/src/do/room-mirror.ts +29 -0
  67. package/src/do/run-buffer.ts +64 -9
  68. package/src/do/run-coordinator.ts +451 -76
  69. package/src/do/run-request.ts +2 -0
  70. package/src/do/run-store-test-do.ts +301 -1
  71. package/src/do/run-store.ts +385 -43
  72. package/src/do/secrets-adapter.ts +22 -3
  73. package/src/do/sse-stream.ts +60 -9
  74. package/src/do/transcript.ts +48 -5
  75. package/src/do/turn-body.ts +105 -5
  76. package/src/do/ucan-service.ts +204 -34
  77. package/src/do/user-oracle-do.ts +2003 -274
  78. package/src/feedback/config.ts +127 -0
  79. package/src/feedback/contract.ts +148 -0
  80. package/src/feedback/linear-sink.ts +285 -0
  81. package/src/feedback/privacy.ts +150 -0
  82. package/src/feedback/reservation.ts +298 -0
  83. package/src/feedback/submit.ts +247 -0
  84. package/src/index.ts +138 -6
  85. package/src/llm/byo-adapter.ts +27 -6
  86. package/src/llm/byo-catalog.ts +13 -0
  87. package/src/llm/byo-model-fallback.ts +248 -0
  88. package/src/llm/byo-service.ts +45 -8
  89. package/src/llm/provider-error.ts +139 -2
  90. package/src/matrix/gateway-do.ts +504 -108
  91. package/src/matrix/group-chat.ts +66 -23
  92. package/src/matrix/inbox-store.ts +16 -1
  93. package/src/matrix/ingest.ts +79 -14
  94. package/src/matrix/media-deadline.ts +130 -0
  95. package/src/matrix/replay-format.ts +5 -3
  96. package/src/matrix/reply-parts.ts +92 -0
  97. package/src/matrix/room-alias.ts +96 -0
  98. package/src/matrix/user-homeserver.ts +166 -7
  99. package/src/owner-store/ixo-vfs-store.ts +170 -43
  100. package/src/owner-store/migrating-store.ts +31 -15
  101. package/src/owner-store/types.ts +18 -3
  102. package/src/plugin-api/index.ts +43 -2
  103. package/src/plugin-api/oracle-plugin.ts +35 -0
  104. package/src/plugin-api/request-admission.ts +41 -0
  105. package/src/plugin-api/tool-helper.ts +25 -1
  106. package/src/plugin-api/tool-plane.ts +92 -0
  107. package/src/plugin-api/types.ts +194 -6
  108. package/src/plugins/agui/agui.plugin.ts +31 -16
  109. package/src/plugins/composio/composio-tools.ts +57 -6
  110. package/src/plugins/composio/composio-ucan.ts +3 -2
  111. package/src/plugins/composio/composio.plugin.ts +2 -0
  112. package/src/plugins/delegated-capabilities.ts +25 -0
  113. package/src/plugins/editor/content-session.ts +173 -83
  114. package/src/plugins/editor/content-tools.ts +76 -17
  115. package/src/plugins/editor/editor-mx.ts +28 -9
  116. package/src/plugins/editor/editor.plugin.ts +3 -1
  117. package/src/plugins/editor/failures.ts +28 -7
  118. package/src/plugins/editor/prompts.ts +2 -1
  119. package/src/plugins/editor/provider.ts +553 -40
  120. package/src/plugins/editor/standalone-editor-tool.ts +61 -18
  121. package/src/plugins/firecrawl/firecrawl-tools.ts +13 -5
  122. package/src/plugins/flows/action-metadata.ts +111 -11
  123. package/src/plugins/flows/actions.ts +18 -0
  124. package/src/plugins/flows/decision-context.ts +31 -0
  125. package/src/plugins/flows/edit.ts +223 -52
  126. package/src/plugins/flows/errors.ts +22 -1
  127. package/src/plugins/flows/flow-doc.ts +92 -9
  128. package/src/plugins/flows/flows.plugin.ts +11 -5
  129. package/src/plugins/flows/forms.ts +34 -0
  130. package/src/plugins/flows/index.ts +2 -0
  131. package/src/plugins/flows/input-policy.ts +140 -0
  132. package/src/plugins/flows/prompts.ts +14 -11
  133. package/src/plugins/flows/read.ts +49 -9
  134. package/src/plugins/flows/ref-syntax.ts +129 -0
  135. package/src/plugins/flows/test-homeserver.ts +245 -0
  136. package/src/plugins/flows/tools/authoring.ts +216 -112
  137. package/src/plugins/flows/tools/discovery.ts +4 -1
  138. package/src/plugins/flows/tools/forms.ts +21 -2
  139. package/src/plugins/flows/tools/settings.ts +34 -13
  140. package/src/plugins/flows/translator.ts +13 -10
  141. package/src/plugins/flows/types.ts +167 -42
  142. package/src/plugins/index.ts +11 -1
  143. package/src/plugins/ixo-transaction/chain-ids.ts +43 -0
  144. package/src/plugins/ixo-transaction/index.ts +2 -0
  145. package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +119 -0
  146. package/src/plugins/ixo-transaction/receipts.ts +99 -0
  147. package/src/plugins/ixo-transaction/tools.ts +329 -0
  148. package/src/plugins/ixo-transaction/wallet-signing.ts +202 -0
  149. package/src/plugins/mcp-tool-adapter.ts +23 -2
  150. package/src/plugins/memory/memory-tools.ts +7 -2
  151. package/src/plugins/memory/memory-ucan.ts +3 -2
  152. package/src/plugins/memory/memory.plugin.ts +2 -0
  153. package/src/plugins/pod-creator/blueprint-store.ts +163 -0
  154. package/src/plugins/pod-creator/blueprint-types.ts +61 -0
  155. package/src/plugins/pod-creator/bounded-map.ts +87 -0
  156. package/src/plugins/pod-creator/capsule-content-client.ts +285 -0
  157. package/src/plugins/pod-creator/chain-gateway.ts +110 -0
  158. package/src/plugins/pod-creator/config.ts +85 -0
  159. package/src/plugins/pod-creator/create-session-store.ts +180 -0
  160. package/src/plugins/pod-creator/create-tools.ts +538 -0
  161. package/src/plugins/pod-creator/design-pod-roles.ts +111 -0
  162. package/src/plugins/pod-creator/index.ts +65 -0
  163. package/src/plugins/pod-creator/orchestration-tools.ts +172 -0
  164. package/src/plugins/pod-creator/pod-creator.plugin.ts +190 -0
  165. package/src/plugins/pod-creator/stage.ts +201 -0
  166. package/src/plugins/pod-creator/sub-agents.ts +189 -0
  167. package/src/plugins/portal/action-log.ts +21 -1
  168. package/src/plugins/portal/declared-tools.ts +90 -0
  169. package/src/plugins/portal/portal.plugin.ts +42 -16
  170. package/src/plugins/sandbox/sandbox-bridge.ts +79 -10
  171. package/src/plugins/sandbox/sandbox-mcp.ts +3 -2
  172. package/src/plugins/sandbox/sandbox-write-blob.ts +11 -1
  173. package/src/plugins/sandbox/sandbox.plugin.ts +8 -16
  174. package/src/plugins/tasks/manifest.ts +1 -1
  175. package/src/plugins/tasks/middleware.ts +20 -7
  176. package/src/plugins/tasks/tasks-tools.ts +30 -3
  177. package/src/plugins/tasks/tasks.plugin.ts +64 -11
  178. package/src/plugins/user-preferences/schema.ts +12 -1
  179. package/src/plugins/user-preferences/user-preferences-store.ts +38 -13
  180. package/src/plugins/user-preferences/user-preferences-tool.ts +3 -2
  181. package/src/plugins/vfs/vfs-client.ts +250 -71
  182. package/src/plugins/vfs/vfs-content.ts +30 -11
  183. package/src/plugins/vfs/vfs-errors.ts +22 -0
  184. package/src/plugins/vfs/vfs-sandbox-tools.ts +12 -5
  185. package/src/plugins/vfs/vfs-tools.ts +60 -20
  186. package/src/realtime/frontend-call-registry.ts +228 -81
  187. package/src/realtime/realtime-endpoint.ts +215 -44
  188. package/src/realtime/session-socket-hub.ts +47 -2
  189. package/src/secrets/secrets-service.ts +9 -3
  190. package/src/shell/app.ts +562 -82
  191. package/src/shell/auth.ts +131 -37
  192. package/src/shell/turn-body-cap.ts +11 -13
  193. package/src/sqlite/blob-compactor.ts +61 -28
  194. package/src/sqlite/database.ts +156 -90
  195. package/src/sqlite/do-vfs.ts +416 -129
  196. package/src/sqlite/page-tier.ts +133 -40
  197. package/src/sqlite/serialization.ts +40 -0
  198. package/src/sqlite/sessions-store.ts +10 -2
  199. package/src/sqlite/sqlite-saver.ts +139 -19
  200. package/src/sqlite/user-kv-store.ts +317 -0
  201. package/src/tasks/schedule.ts +201 -12
  202. package/src/tasks/scheduler.ts +586 -98
  203. package/src/tasks/spec.ts +67 -3
  204. package/src/tasks/store.ts +292 -27
  205. package/src/tasks/topic-deliverables.ts +70 -0
@@ -1,8 +1,26 @@
1
+ import type {
2
+ TopicDeliverableCommand,
3
+ TopicDeliverableResult,
4
+ } from '../tasks/topic-deliverables';
5
+ import type {
6
+ ChannelIdentity,
7
+ ChannelTurnInput,
8
+ ChannelTurnOutcome,
9
+ } from '../channels/contract';
10
+ import type { TaskExecutionProfile } from '../core/execution-profile';
11
+ import type { CapabilityRouterMode } from '@ixo/common/ai/decisions';
1
12
  import type { TranscriptPageOptions } from './transcript';
13
+ import type { TurnUsage } from '../core/turn-budget';
2
14
  import type { BotStatus, EncryptedFileInfo } from '@ixo/matrix-bot-workers-sdk';
3
15
  import type { AttachmentInput } from '../attachments/types';
4
16
  import type { RealtimeStatus } from '../realtime/realtime-endpoint';
5
17
  import type { TierFlushResult, TierStatus } from '../sqlite/do-vfs';
18
+ import type { OwnedArtifact } from '../artifacts/store';
19
+ import type {
20
+ FeedbackReservation,
21
+ FeedbackSettlement,
22
+ FeedbackTarget,
23
+ } from '../feedback/contract';
6
24
  /**
7
25
  * Cross-object contracts for the Workers runtime.
8
26
  *
@@ -25,6 +43,7 @@ import type { TierFlushResult, TierStatus } from '../sqlite/do-vfs';
25
43
 
26
44
  /** Bindings every oracle Worker must declare (see the example `wrangler.jsonc`). */
27
45
  export interface OracleWorkerEnv {
46
+ TOPIC_DELIVERABLES_ENABLED?: string;
28
47
  USER_ORACLE: DurableObjectNamespace<UserOracleObject>;
29
48
  MATRIX_GATEWAY: DurableObjectNamespace<MatrixGatewayObject>;
30
49
 
@@ -40,6 +59,12 @@ export interface OracleWorkerEnv {
40
59
  /** Blocksync GraphQL endpoint used to resolve `did:ixo` verification keys. */
41
60
  BLOCKSYNC_GRAPHQL_URL: string;
42
61
  UCAN_AUTH_MAX_TTL_SECONDS?: string;
62
+ /** `true` restores the legacy bare-delegation authentication (off by default). */
63
+ UCAN_ALLOW_BARE_DELEGATION_AUTH?: string;
64
+ CHANNEL_SERVICE_DID?: string;
65
+ AUTH_HUB?: Fetcher;
66
+ AUTH_HUB_URL?: string;
67
+ AUTH_HUB_CHANNEL_SERVICE_KEY?: string;
43
68
 
44
69
  // --- matrix -------------------------------------------------------------
45
70
  MATRIX_BASE_URL: string;
@@ -131,6 +156,22 @@ export interface OracleWorkerEnv {
131
156
  TIER_EVICT_AFTER_PERIODS?: string;
132
157
  /** Length of one access-tracking period in ms (default one day; tests shorten it). */
133
158
  TIER_PERIOD_MS?: string;
159
+ /**
160
+ * Artefacts (chat delivery): long replies on chat surfaces become documents
161
+ * behind a link. The R2 bucket holds only ciphertext share copies; absent
162
+ * bucket = artefacts off; a bucket without a public origin keeps stored
163
+ * artefacts readable and revocable but makes no new ones. Long replies are
164
+ * then split into messages instead.
165
+ */
166
+ ARTIFACT_BUCKET?: R2Bucket;
167
+ /** The oracle Worker's public origin; artefact links point at `/a/:id` under it. */
168
+ ORACLE_PUBLIC_URL?: string;
169
+ /** A shared viewer page (Qi.Space). Unset = the oracle's own `/a/:id` page. */
170
+ ARTIFACT_VIEWER_URL?: string;
171
+ /** Days an artefact link stays valid (default 30). */
172
+ ARTIFACT_LINK_TTL_DAYS?: string;
173
+ /** Hours between the cron's sweeps of expired share copies (default 24, 1–168). */
174
+ ARTIFACT_SWEEP_INTERVAL_HOURS?: string;
134
175
  /** IXO VFS worker base URL when `OWNER_STORE=vfs` (defaults per NETWORK). */
135
176
  VFS_BASE_URL?: string;
136
177
  /** UCAN store worker base URL when `OWNER_STORE=vfs` (defaults per NETWORK). */
@@ -149,6 +190,36 @@ export interface OracleWorkerEnv {
149
190
  * local harness and vitest pool, where the shell degrades to no limiting.
150
191
  */
151
192
  RATE_LIMIT?: RateLimit;
193
+ /**
194
+ * Optional rate-limit binding for anonymous feedback, keyed per client IP
195
+ * pseudonym (declare it with `simple: { limit: 3, period: 60 }`). Without
196
+ * it the per-IP check uses `RATE_LIMIT`'s limit under a feedback-only key.
197
+ */
198
+ FEEDBACK_RATE_LIMIT?: RateLimit;
199
+
200
+ // --- anonymous response feedback (docs/configuration.md) ------------------
201
+ // Read by the shell only (`src/feedback/config.ts`); never part of the
202
+ // validated config plugins receive. Off unless the key and secret are set.
203
+ /** Linear API key allowed to create issues in the feedback team. */
204
+ FEEDBACK_LINEAR_API_KEY?: string;
205
+ /** At least 32 characters; derives the user/session/message pseudonyms. */
206
+ FEEDBACK_HMAC_SECRET?: string;
207
+ /** Linear team (default: Studio). */
208
+ FEEDBACK_LINEAR_TEAM_ID?: string;
209
+ /** Linear project (default: "User Feedback from Portal"). */
210
+ FEEDBACK_LINEAR_PROJECT_ID?: string;
211
+ /** Optional comma-separated Linear label ids. */
212
+ FEEDBACK_LINEAR_LABEL_IDS?: string;
213
+ /** Linear GraphQL endpoint override (default `https://api.linear.app/graphql`). */
214
+ FEEDBACK_LINEAR_API_URL?: string;
215
+ /** Release or commit identifier shown in feedback issues (default `unknown`). */
216
+ QIFORGE_BUILD_VERSION?: string;
217
+ /**
218
+ * Workers AI binding. Optional: `DECISION_PROVIDER=cloudflare-jev` runs the
219
+ * Jev model through it instead of the account credentials. Declare it with
220
+ * `"ai": { "binding": "AI" }` in the wrangler config.
221
+ */
222
+ AI?: Ai;
152
223
 
153
224
  // --- llm ----------------------------------------------------------------
154
225
  OPEN_ROUTER_API_KEY: string;
@@ -158,6 +229,15 @@ export interface OracleWorkerEnv {
158
229
  MAIN_REASONING_EFFORT?: string;
159
230
  /** LangGraph steps one turn may take before `GraphRecursionError` (default 600; Node hard-codes 200). */
160
231
  TURN_RECURSION_LIMIT?: string;
232
+ /** Turn budget: cumulative model tokens (default 500000), tool attempts (120), deadline in ms (600000). */
233
+ TURN_MAX_TOKENS?: string;
234
+ TURN_MAX_TOOL_CALLS?: string;
235
+ TURN_TIMEOUT_MS?: string;
236
+ /** Repetition guard: identical successful writes (default 1) and reads (5) per turn. */
237
+ TURN_MAX_IDENTICAL_WRITES?: string;
238
+ TURN_MAX_IDENTICAL_READS?: string;
239
+ /** Time limit per request-admission handler in ms (default 2000). */
240
+ REQUEST_ADMISSION_TIMEOUT_MS?: string;
161
241
  /** Durable-run knobs (docs/plans/durable-runs.md); see `runDurabilityConfig`. */
162
242
  RUN_KEEPALIVE_MS?: string;
163
243
  RUN_SEGMENT_FLUSH_MS?: string;
@@ -183,6 +263,22 @@ export interface OracleWorkerEnv {
183
263
  /** Nebius Token Factory API key — required when `LLM_PROVIDER=nebius`. */
184
264
  NEBIUS_API_KEY?: string;
185
265
 
266
+ // --- decisions ----------------------------------------------------------
267
+ /** Bounded semantic Decision provider; unset leaves Decisions unconfigured. `openrouter-jev` reuses `OPEN_ROUTER_API_KEY`. */
268
+ DECISION_PROVIDER?: 'cloudflare-jev' | 'openrouter-jev';
269
+ /** Jev model id override (`typesafe/jev` on Workers AI, `typesafe/jev-1.13` on OpenRouter). */
270
+ DECISION_MODEL?: string;
271
+ /** Cloudflare account for `cloudflare-jev` over the REST API; not needed when the `AI` binding is declared. */
272
+ CLOUDFLARE_ACCOUNT_ID?: string;
273
+ /** Workers AI token for `cloudflare-jev` over the REST API; not needed when the `AI` binding is declared. */
274
+ CLOUDFLARE_API_TOKEN?: string;
275
+ /**
276
+ * Capability router (needs a `DECISION_PROVIDER`). `off` (default) never
277
+ * evaluates; `shadow` evaluates and logs the verdict without preloading;
278
+ * `on` preloads the predicted on-demand plugin's tools for the turn.
279
+ */
280
+ CAPABILITY_ROUTER?: CapabilityRouterMode;
281
+
186
282
  // --- langsmith tracing ---------------------------------------------------
187
283
  /** `'true'` traces every turn (explicit tracer — Workers has no env auto-attach). */
188
284
  LANGSMITH_TRACING?: string;
@@ -234,6 +330,7 @@ export type JsonString = string;
234
330
 
235
331
  /** Who is talking and through which transport. */
236
332
  export interface TurnIdentity {
333
+ channel?: ChannelIdentity;
237
334
  /** Validated user DID (from the UCAN signer or the Matrix sender mapping). */
238
335
  userDid: string;
239
336
  /** `@did-ixo-…:server` when known. */
@@ -247,10 +344,16 @@ export interface TurnIdentity {
247
344
 
248
345
  /** Request the shell / gateway sends to `UserOracleDO` to run one turn. */
249
346
  export interface TurnRequest {
347
+ channel?: {
348
+ provider: 'whatsapp';
349
+ bindingId: string;
350
+ remoteMessageRef: string;
351
+ };
352
+ executionProfile?: TaskExecutionProfile;
250
353
  identity: TurnIdentity;
251
354
  sessionId: string;
252
355
  message: string;
253
- client: 'portal' | 'matrix';
356
+ client: 'portal' | 'matrix' | 'channel';
254
357
  /** Matrix room the turn arrived in (Matrix turns) or the user's DM room. */
255
358
  roomId?: string;
256
359
  /** Thread root when the user replied inside a thread. */
@@ -342,7 +445,7 @@ export interface RunSummary {
342
445
  runId: string;
343
446
  sessionId: string;
344
447
  requestId: string;
345
- client: 'portal' | 'matrix';
448
+ client: 'portal' | 'matrix' | 'channel';
346
449
  status:
347
450
  | 'queued'
348
451
  | 'running'
@@ -398,16 +501,29 @@ export interface RunsStatus {
398
501
  nextAttemptAt: number | null;
399
502
  error: string | null;
400
503
  taskRunId: string | null;
504
+ /** The turn's budget usage, once the run ended (`TurnUsage`). */
505
+ usage: TurnUsage | null;
401
506
  }
402
507
  >;
508
+ /** Writes whose outcome is still unknown (tool-execution.ts); never their arguments. */
509
+ claims: Array<{
510
+ fingerprint: string;
511
+ toolName: string;
512
+ runId: string;
513
+ sessionId: string;
514
+ startedAt: string;
515
+ state: 'pending' | 'warned';
516
+ }>;
403
517
  }
404
518
 
405
519
  /** Non-streaming turn result. */
406
520
  export interface TurnResult {
407
521
  sessionId: string;
408
522
  requestId: string;
409
- /** Final assistant text. */
523
+ /** The model's final text, on every surface (never a chat rendering). */
410
524
  text: string;
525
+ /** Chat-surface turns: the Reply Plan (JSON) to deliver part by part. */
526
+ plan?: JsonString;
411
527
  /** Id of the final assistant message, when one was produced. */
412
528
  messageId?: string;
413
529
  /** Tool calls made during the turn (name + summarized status), for logging. */
@@ -513,6 +629,16 @@ export interface StorageStatus {
513
629
  * piped straight to the client.
514
630
  */
515
631
  export interface UserOracleObject extends Rpc.DurableObjectBranded {
632
+ topicDeliverable(
633
+ identity: TurnIdentity,
634
+ operationId: string,
635
+ command: TopicDeliverableCommand,
636
+ ): Promise<TopicDeliverableResult>;
637
+ channelTurn(
638
+ identity: TurnIdentity,
639
+ input: ChannelTurnInput,
640
+ requestHash: string,
641
+ ): Promise<ChannelTurnOutcome>;
516
642
  /** Run a turn and return the final text (used by the Matrix gateway). */
517
643
  runTurn(req: TurnRequest): Promise<TurnResult>;
518
644
  /**
@@ -531,6 +657,13 @@ export interface UserOracleObject extends Rpc.DurableObjectBranded {
531
657
  opts?: { limit?: number; offset?: number },
532
658
  ): Promise<{ sessions: SessionSummary[]; total: number }>;
533
659
  deleteSession(identity: TurnIdentity, sessionId: string): Promise<boolean>;
660
+ /** `GET /artifacts/:id` — the owner's canonical copy, or null. */
661
+ artifact(
662
+ identity: TurnIdentity,
663
+ artifactId: string,
664
+ ): Promise<OwnedArtifact | null>;
665
+ /** `DELETE /artifacts/:id` — false when there is no such artefact. */
666
+ revokeArtifact(identity: TurnIdentity, artifactId: string): Promise<boolean>;
534
667
  /** JSON-encoded `MessageDto[]` for `GET /messages/:sessionId`. */
535
668
  listMessages(identity: TurnIdentity, sessionId: string): Promise<JsonString>;
536
669
  /**
@@ -545,6 +678,20 @@ export interface UserOracleObject extends Rpc.DurableObjectBranded {
545
678
  ): Promise<
546
679
  { ok: true; json: JsonString } | { ok: false; status: 400; message: string }
547
680
  >;
681
+ /**
682
+ * Anonymous feedback (src/feedback/): validate the target message in this
683
+ * user's database and reserve its idempotency marker. Never sees the text.
684
+ */
685
+ reserveMessageFeedback(
686
+ identity: TurnIdentity,
687
+ target: FeedbackTarget,
688
+ ): Promise<FeedbackReservation>;
689
+ /** Record the delivery, keep the earlier submission's, or release the reservation. */
690
+ settleMessageFeedback(
691
+ identity: TurnIdentity,
692
+ target: FeedbackTarget,
693
+ outcome: FeedbackSettlement,
694
+ ): Promise<void>;
548
695
  abortTurn(sessionId: string): Promise<boolean>;
549
696
  /** The session's active (queued/running/recovering) run, if any. */
550
697
  sessionRun(
@@ -595,9 +742,12 @@ export interface UserOracleObject extends Rpc.DurableObjectBranded {
595
742
  /**
596
743
  * Drop the working copy (pages + caches) so the next access re-imports from
597
744
  * the owner store. Simulates eviction/loss of the DO cache in tests and is
598
- * the "user deleted their file upstream" path.
745
+ * the "user deleted their file upstream" path. `userDid` is the caller the
746
+ * shell authenticated, so an object that never booted boots for it.
599
747
  */
600
- resetWorkingCopy(): Promise<{ reloadedFromOwnerStore: boolean }>;
748
+ resetWorkingCopy(
749
+ identity?: TurnIdentity,
750
+ ): Promise<{ reloadedFromOwnerStore: boolean }>;
601
751
  /** Run one R2 page-tier eviction pass now (debug routes; `force` ignores recency). */
602
752
  tierFlush(opts?: {
603
753
  force?: boolean;
@@ -658,6 +808,16 @@ export interface CreateRoomOptions {
658
808
  userDid: string;
659
809
  }
660
810
 
811
+ /**
812
+ * Deadline for one gateway media download, from opening it to the last byte
813
+ * (the gateway's default applies when omitted). Only the timeout crosses the
814
+ * RPC: an `AbortSignal` cannot, so the caller stops a transfer early by
815
+ * cancelling the returned stream.
816
+ */
817
+ export interface MediaDownloadDeadline {
818
+ timeoutMs?: number;
819
+ }
820
+
661
821
  /**
662
822
  * RPC surface of `MatrixGatewayDO`. User objects call it for anything that
663
823
  * needs the bot identity (sending, media, room lookups); the shell calls
@@ -676,9 +836,13 @@ export interface MatrixGatewayObject extends Rpc.DurableObjectBranded {
676
836
  downloadEventMediaStream(
677
837
  roomId: string,
678
838
  eventId: string,
839
+ opts?: MediaDownloadDeadline,
679
840
  ): Promise<MediaStream | null>;
680
841
  /** Authenticated media download of an `mxc://` URI, as a stream. */
681
- downloadMxcMediaStream(mxc: string): Promise<ReadableStream<Uint8Array>>;
842
+ downloadMxcMediaStream(
843
+ mxc: string,
844
+ opts?: MediaDownloadDeadline,
845
+ ): Promise<ReadableStream<Uint8Array>>;
682
846
  /** Forcibly reset the gateway object like a platform host drain (debug routes only). The call itself rejects. */
683
847
  debugAbortObject(): Promise<void>;
684
848
  /** Stop the bot (operator/debug; `ensureStarted`/cron brings it back). */
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Dropping an idle user's working copy. The object only drops it when the
3
+ * owner copy upstream is verified current, and that check awaits (the
4
+ * checksum reads the whole file, from R2 for tiered pages), and while an
5
+ * object awaits I/O other than its own storage a request can arrive. So
6
+ * whatever the tick decided before is re-read after the check, and the
7
+ * in-memory handles are dropped before the first await of the wipe: from
8
+ * then on a request boots afresh from the owner copy instead of reaching a
9
+ * database being deleted under it.
10
+ */
11
+ export interface IdleEvictionSteps {
12
+ /** The owner copy matches the working copy (may await network I/O). */
13
+ ownerCopyIsCurrent(): Promise<boolean>;
14
+ /** Everything that decides idleness, read again: still no activity. */
15
+ stillIdle(): Promise<boolean>;
16
+ /** Forget the in-memory handles. Synchronous, so nothing interleaves. */
17
+ dropHandles(): void;
18
+ /** Delete the working copy. */
19
+ wipe(): Promise<void>;
20
+ }
21
+
22
+ export type IdleEvictionOutcome =
23
+ /** The working copy was deleted. */
24
+ | 'evicted'
25
+ /** The owner copy is not verified current: keep the working copy. */
26
+ | 'not-current'
27
+ /** Activity arrived while the owner copy was checked: keep it. */
28
+ | 'active';
29
+
30
+ export async function evictIdleWorkingCopy(
31
+ steps: IdleEvictionSteps,
32
+ ): Promise<IdleEvictionOutcome> {
33
+ if (!(await steps.ownerCopyIsCurrent())) return 'not-current';
34
+ if (!(await steps.stillIdle())) return 'active';
35
+ steps.dropHandles();
36
+ await steps.wipe();
37
+ return 'evicted';
38
+ }
@@ -29,6 +29,8 @@ export interface MatrixTurnRecord {
29
29
  startedAt: string;
30
30
  answeredAt: string | null;
31
31
  replyText: string | null;
32
+ /** The Reply Plan (JSON) a chat-style reply was delivered as. */
33
+ replyPlan?: string;
32
34
  }
33
35
 
34
36
  export type MatrixTurnDecision = 'run' | 'answered' | 'interrupted';
@@ -50,6 +52,7 @@ type LedgerRow = {
50
52
  started_at: string;
51
53
  answered_at: string | null;
52
54
  reply_text: string | null;
55
+ reply_plan: string | null;
53
56
  } & Record<string, string | null>;
54
57
 
55
58
  export class MatrixTurnLedger {
@@ -76,8 +79,14 @@ export class MatrixTurnLedger {
76
79
  request_id TEXT NOT NULL,
77
80
  started_at TEXT NOT NULL,
78
81
  answered_at TEXT,
79
- reply_text TEXT
82
+ reply_text TEXT,
83
+ reply_plan TEXT
80
84
  )`);
85
+ const columns = await this.db.exec<{ name: string }>(
86
+ `PRAGMA table_info(matrix_turns)`,
87
+ );
88
+ if (!columns.some((c) => c.name === 'reply_plan'))
89
+ await this.db.run(`ALTER TABLE matrix_turns ADD COLUMN reply_plan TEXT`);
81
90
  await this.db.run(`DELETE FROM matrix_turns WHERE started_at < ?`, [
82
91
  new Date(this.now() - RETENTION_MS).toISOString(),
83
92
  ]);
@@ -86,7 +95,7 @@ export class MatrixTurnLedger {
86
95
  async get(eventId: string): Promise<MatrixTurnRecord | undefined> {
87
96
  await this.setup();
88
97
  const row = await this.db.get<LedgerRow>(
89
- `SELECT event_id, session_id, request_id, started_at, answered_at, reply_text
98
+ `SELECT event_id, session_id, request_id, started_at, answered_at, reply_text, reply_plan
90
99
  FROM matrix_turns WHERE event_id = ?`,
91
100
  [eventId],
92
101
  );
@@ -98,6 +107,7 @@ export class MatrixTurnLedger {
98
107
  startedAt: row.started_at,
99
108
  answeredAt: row.answered_at,
100
109
  replyText: row.reply_text,
110
+ ...(row.reply_plan !== null ? { replyPlan: row.reply_plan } : {}),
101
111
  };
102
112
  }
103
113
 
@@ -116,11 +126,20 @@ export class MatrixTurnLedger {
116
126
  }
117
127
 
118
128
  /** Record the reply; from now on the event is answered, never re-run. */
119
- async answer(eventId: string, replyText: string): Promise<void> {
129
+ async answer(
130
+ eventId: string,
131
+ replyText: string,
132
+ replyPlan?: string,
133
+ ): Promise<void> {
120
134
  await this.setup();
121
135
  await this.db.run(
122
- `UPDATE matrix_turns SET reply_text = ?, answered_at = ? WHERE event_id = ?`,
123
- [replyText, new Date(this.now()).toISOString(), eventId],
136
+ `UPDATE matrix_turns SET reply_text = ?, reply_plan = ?, answered_at = ? WHERE event_id = ?`,
137
+ [
138
+ replyText,
139
+ replyPlan ?? null,
140
+ new Date(this.now()).toISOString(),
141
+ eventId,
142
+ ],
124
143
  );
125
144
  }
126
145
  }
@@ -22,13 +22,58 @@ export class ResultStoreTestDO extends DurableObject<{ TIER_TEST: R2Bucket }> {
22
22
 
23
23
  private withBucket = true;
24
24
 
25
- private async resultStore(): Promise<ResultStore> {
25
+ /**
26
+ * How R2 deletes behave: `fail` throws (an outage during a sweep),
27
+ * `delete-then-fail` deletes and then throws (a lost response),
28
+ * `fail-first` throws on the first call only.
29
+ */
30
+ private deletes: 'ok' | 'fail' | 'delete-then-fail' | 'fail-first' = 'ok';
31
+
32
+ private deleteCalls = 0;
33
+
34
+ /** While set, an R2 delete waits for `release` (after signalling `reached`). */
35
+ private deleteHold: { reached: () => void; release: Promise<void> } | null =
36
+ null;
37
+
38
+ private async database(): Promise<DoSqliteDatabase> {
26
39
  if (!this.db || !this.db.isOpen) {
27
40
  this.db = await DoSqliteDatabase.open(this.ctx, 'results-test.db');
28
41
  this.store = undefined;
29
42
  }
30
- this.store ??= new ResultStore(this.db, {
31
- ...(this.withBucket ? { bucket: this.env.TIER_TEST } : {}),
43
+ return this.db;
44
+ }
45
+
46
+ private bucket(): R2Bucket {
47
+ const remove = async (keys: string | string[]): Promise<void> => {
48
+ this.deleteCalls += 1;
49
+ const hold = this.deleteHold;
50
+ if (hold) {
51
+ hold.reached();
52
+ await hold.release;
53
+ }
54
+ if (
55
+ this.deletes === 'fail' ||
56
+ (this.deletes === 'fail-first' && this.deleteCalls === 1)
57
+ )
58
+ throw new Error('R2 unavailable');
59
+ await this.env.TIER_TEST.delete(keys);
60
+ if (this.deletes === 'delete-then-fail')
61
+ throw new Error('R2 response lost');
62
+ };
63
+ return new Proxy(this.env.TIER_TEST, {
64
+ get: (target, property) => {
65
+ if (property === 'delete') return remove;
66
+ // The binding's methods only run on the binding itself.
67
+ const value: unknown = Reflect.get(target, property);
68
+ return typeof value === 'function' ? value.bind(target) : value;
69
+ },
70
+ });
71
+ }
72
+
73
+ private async resultStore(): Promise<ResultStore> {
74
+ const db = await this.database();
75
+ this.store ??= new ResultStore(db, {
76
+ ...(this.withBucket ? { bucket: this.bucket() } : {}),
32
77
  prefix: `test-${this.ctx.id.toString()}`,
33
78
  ttlMs: 60 * 60 * 1000,
34
79
  r2MinBytes: this.r2MinBytes,
@@ -40,12 +85,101 @@ export class ResultStoreTestDO extends DurableObject<{ TIER_TEST: R2Bucket }> {
40
85
  async configure(opts: {
41
86
  r2MinBytes?: number;
42
87
  withBucket?: boolean;
88
+ failingDeletes?: boolean;
89
+ deletes?: 'ok' | 'fail' | 'delete-then-fail' | 'fail-first';
43
90
  }): Promise<void> {
44
91
  if (opts.r2MinBytes !== undefined) this.r2MinBytes = opts.r2MinBytes;
45
92
  if (opts.withBucket !== undefined) this.withBucket = opts.withBucket;
93
+ if (opts.failingDeletes !== undefined)
94
+ this.deletes = opts.failingDeletes ? 'fail' : 'ok';
95
+ if (opts.deletes !== undefined) this.deletes = opts.deletes;
96
+ this.deleteCalls = 0;
46
97
  this.store = undefined;
47
98
  }
48
99
 
100
+ /**
101
+ * Delete one session's results while another session stores the same
102
+ * content: the put starts while the delete waits on R2, and R2 answers
103
+ * after the put had time to finish. Returns how the result reads then.
104
+ */
105
+ async deleteWhilePutting(input: {
106
+ deleting: string;
107
+ putting: string;
108
+ content: string;
109
+ }): Promise<ReadResultOutcome> {
110
+ const store = await this.resultStore();
111
+ await store.setup();
112
+ let reached!: () => void;
113
+ const atDelete = new Promise<void>((resolve) => {
114
+ reached = resolve;
115
+ });
116
+ let release!: () => void;
117
+ this.deleteHold = {
118
+ reached,
119
+ release: new Promise<void>((resolve) => {
120
+ release = resolve;
121
+ }),
122
+ };
123
+ const deleting = store.deleteForSession(input.deleting);
124
+ await atDelete;
125
+ const putting = store.put({
126
+ sessionId: input.putting,
127
+ toolName: 't',
128
+ content: input.content,
129
+ });
130
+ await new Promise((resolve) => setTimeout(resolve, 50));
131
+ release();
132
+ const [, ref] = await Promise.all([deleting, putting]);
133
+ this.deleteHold = null;
134
+ if (!ref) throw new Error('the result was not stored');
135
+ return store.read(ref.id, 0, 5);
136
+ }
137
+
138
+ /**
139
+ * Rows as a store written before results were shared across sessions
140
+ * left them: the `tool_results` table alone. Call before any other method.
141
+ */
142
+ async seedLegacy(
143
+ rows: Array<{ id: string; sessionId: string; content: string }>,
144
+ ): Promise<void> {
145
+ const db = await this.database();
146
+ await db.run(`
147
+ CREATE TABLE IF NOT EXISTS tool_results (
148
+ id TEXT PRIMARY KEY,
149
+ session_id TEXT NOT NULL,
150
+ tool_name TEXT NOT NULL,
151
+ size INTEGER NOT NULL,
152
+ tier TEXT NOT NULL,
153
+ content TEXT,
154
+ created_at TEXT NOT NULL,
155
+ expires_at INTEGER NOT NULL
156
+ )`);
157
+ for (const row of rows)
158
+ await db.run(
159
+ `INSERT INTO tool_results VALUES (?, ?, 't', ?, 'sqlite', ?, ?, ?)`,
160
+ [
161
+ row.id,
162
+ row.sessionId,
163
+ new TextEncoder().encode(row.content).byteLength,
164
+ row.content,
165
+ new Date(this.nowMs).toISOString(),
166
+ this.nowMs + 60 * 60 * 1000,
167
+ ],
168
+ );
169
+ }
170
+
171
+ /** Re-open the store over the same database (the next boot). */
172
+ async reboot(): Promise<void> {
173
+ this.store = undefined;
174
+ }
175
+
176
+ async references(): Promise<Array<{ id: string; session_id: string }>> {
177
+ const db = await this.database();
178
+ return db.exec<{ id: string; session_id: string }>(
179
+ `SELECT id, session_id FROM tool_result_sessions ORDER BY id, session_id`,
180
+ );
181
+ }
182
+
49
183
  async setNow(ms: number): Promise<void> {
50
184
  this.nowMs = ms;
51
185
  }