@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
@@ -56,7 +56,7 @@ export const tasksManifest: PluginManifest = {
56
56
  {
57
57
  user: '(replying to an approval request) Yes — go ahead.',
58
58
  thought:
59
- 'The user approved the pending run — record it; approval executes the run and delivers the result.',
59
+ 'The user approved the pending run — record it; approval starts the run, and its result is posted to the room when it finishes.',
60
60
  tool: 'resolve_task_approval',
61
61
  args: {
62
62
  taskId: 'task_daily-linkedin-update_a1b2c3d4',
@@ -8,9 +8,10 @@
8
8
  * - There are no dedicated task rooms or room→session bindings — pending
9
9
  * approvals live on the task records themselves (`pendingApprovalAt`), so
10
10
  * the gate reads them straight off `OracleTasksSurface.list()`.
11
- * - The gate never resolves an approval itself: on Workers, resolving an
12
- * approval EXECUTES the run (a full agent turn), which does not belong
13
- * inside a model-call wrapper. Plain yes/no replies are still classified
11
+ * - The gate never resolves an approval itself: resolving an approval
12
+ * starts the run (a full agent turn, on the user object's alarm), which
13
+ * is the model's call to make through the tool, not a model-call
14
+ * wrapper's. Plain yes/no replies are still classified
14
15
  * deterministically (`classifyReplyFast`) — the classification shapes the
15
16
  * hint, and the model records the decision through the
16
17
  * `resolve_task_approval` tool.
@@ -22,6 +23,10 @@ import type { BaseMessage } from '@langchain/core/messages';
22
23
  import { createMiddleware, type AgentMiddleware } from 'langchain';
23
24
  import type { Logger, OracleTasksSurface } from '../../plugin-api/types';
24
25
  import { pendingApprovalOf } from '../../tasks/store';
26
+ import {
27
+ stripTurnTimeNote,
28
+ TURN_TIME_NOTE_KWARG,
29
+ } from '../../core/turn-time-note';
25
30
 
26
31
  const APPROVE = new Set([
27
32
  'yes',
@@ -114,7 +119,7 @@ export async function computeApprovalHint(
114
119
  if (only && decision === 'approved') {
115
120
  return (
116
121
  `\n\n[Task approval gate] The user's reply APPROVES the pending run of task ${only.id} ("${only.title}"). ` +
117
- 'Call `resolve_task_approval` with outcome "approved" NOW — approval executes the run and delivers its result — then confirm to the user.'
122
+ 'Call `resolve_task_approval` with outcome "approved" NOW — approval starts the run, and its result is posted to the room when it finishes — then tell the user it is running.'
118
123
  );
119
124
  }
120
125
  if (only && decision === 'rejected') {
@@ -128,7 +133,7 @@ export async function computeApprovalHint(
128
133
  .join(', ');
129
134
  return (
130
135
  `\n\n[Task approval gate] ${pending.length} task run(s) are waiting for the user's approval: ${listing}. ` +
131
- 'If this reply decides one of them (possibly with tweaks), call `resolve_task_approval` with the outcome — "approved" executes the run (pass requested tweaks in `note`), "declined" drops it. ' +
136
+ 'If this reply decides one of them (possibly with tweaks), call `resolve_task_approval` with the outcome — "approved" starts the run (pass requested tweaks in `note`), "declined" drops it. ' +
132
137
  'Otherwise answer normally and remind the user of the pending approval.'
133
138
  );
134
139
  }
@@ -144,12 +149,20 @@ export function userDidFromContext(context: unknown): string | undefined {
144
149
  return typeof did === 'string' && did.length > 0 ? did : undefined;
145
150
  }
146
151
 
147
- /** Text of the latest HumanMessage — string content or joined text parts. */
152
+ /**
153
+ * Text of the latest HumanMessage — string content or joined text parts —
154
+ * as the user sent it: without the turn time note the runtime puts in front.
155
+ */
148
156
  export function lastHumanText(messages: BaseMessage[]): string | null {
149
157
  for (let i = messages.length - 1; i >= 0; i--) {
150
158
  const message = messages[i];
151
159
  if (!message || message.type !== 'human') continue;
152
- return textOfContent(message.content);
160
+ return textOfContent(
161
+ stripTurnTimeNote(
162
+ message.content,
163
+ message.additional_kwargs?.[TURN_TIME_NOTE_KWARG],
164
+ ),
165
+ );
153
166
  }
154
167
  return null;
155
168
  }
@@ -38,7 +38,8 @@ const APPROVAL_FIELD_DESCRIPTION =
38
38
  'fires it does NOT execute — it posts an approval request into the ' +
39
39
  "user's oracle room and waits. The user approves or declines by replying " +
40
40
  'there; record the decision with `resolve_task_approval` — approval ' +
41
- 'executes the run and delivers its result, a decline drops it.';
41
+ 'starts the run in the background (its result is posted to the room when ' +
42
+ 'it finishes), a decline drops it.';
42
43
 
43
44
  const taskInputShape = {
44
45
  title: z.string().min(1).max(120),
@@ -72,6 +73,14 @@ function requireTasks(ctx: RuntimeContext): OracleTasksSurface | null {
72
73
  return ctx.tasks ?? null;
73
74
  }
74
75
 
76
+ /**
77
+ * A host-created supplied-context task: its intent is the authorized source
78
+ * text and its result was produced from it alone. Neither enters an ordinary
79
+ * conversation, and the task is one immutable attempt (cancel only).
80
+ */
81
+ const RESTRICTED_TASK_NOTE =
82
+ 'Restricted supplied-context task: its source text and result are not available in this conversation, and it cannot be edited, paused or resumed — only cancelled.';
83
+
75
84
  function summarizeRecord(record: OracleTaskRecord): Record<string, unknown> {
76
85
  const pendingApprovalAt = pendingApprovalOf(record);
77
86
  return {
@@ -84,6 +93,10 @@ function summarizeRecord(record: OracleTaskRecord): Record<string, unknown> {
84
93
  ...(pendingApprovalAt !== undefined && {
85
94
  awaitingApprovalSince: pendingApprovalAt,
86
95
  }),
96
+ ...(record.executionProfile && {
97
+ restricted: true,
98
+ executionProfile: record.executionProfile,
99
+ }),
87
100
  };
88
101
  }
89
102
 
@@ -234,6 +247,18 @@ function getTask(): PluginTool {
234
247
  try {
235
248
  const record = await tasks.get(taskId);
236
249
  if (!record) return { ok: false, error: 'Task not found.' };
250
+ if (record.executionProfile) {
251
+ return {
252
+ ok: true,
253
+ ...summarizeRecord(record),
254
+ lastRunAt: record.lastRunAt ?? null,
255
+ lastResult: record.lastResult
256
+ ? { ok: record.lastResult.ok, at: record.lastResult.at }
257
+ : null,
258
+ consecutiveFailures: record.consecutiveFailures,
259
+ note: RESTRICTED_TASK_NOTE,
260
+ };
261
+ }
237
262
  return {
238
263
  ok: true,
239
264
  ...summarizeRecord(record),
@@ -354,7 +379,7 @@ function resolveTaskApproval(): PluginTool {
354
379
  outcome: args.outcome,
355
380
  note:
356
381
  args.outcome === 'approved'
357
- ? "Decision recorded and the run was executed — its result was delivered to the user's room. Confirm to the user."
382
+ ? "Approved — the run is starting now in the background; its result will be posted to the user's room when it finishes. Tell the user it is running; do not claim a result yet."
358
383
  : 'Decision recorded — the pending run was dropped and nothing was executed.',
359
384
  };
360
385
  } catch (err) {
@@ -364,7 +389,7 @@ function resolveTaskApproval(): PluginTool {
364
389
  {
365
390
  name: 'resolve_task_approval',
366
391
  description:
367
- "Record the user's decision on a task run that is waiting for approval. 'approved' EXECUTES the run immediately (the result is delivered to the user's room); 'declined' drops it without running. Call this when the user answers a pending approval request — pass their requested tweaks in `note`.",
392
+ "Record the user's decision on a task run that is waiting for approval. 'approved' starts the run right away in the background (its result is posted to the user's room when it finishes); 'declined' drops it without running. Call this when the user answers a pending approval request — pass their requested tweaks in `note`.",
368
393
  schema: resolveApprovalInput,
369
394
  },
370
395
  );
@@ -381,6 +406,8 @@ function suggestSpecFix(): PluginTool {
381
406
  try {
382
407
  const record = await tasks.get(taskId);
383
408
  if (!record) return { ok: false, error: 'Task not found.' };
409
+ if (record.executionProfile)
410
+ return { ok: false, error: RESTRICTED_TASK_NOTE };
384
411
  const lastFailure =
385
412
  record.lastResult && !record.lastResult.ok
386
413
  ? record.lastResult
@@ -22,9 +22,10 @@
22
22
  * `TaskSchedulerHost.maxTasksPerUser` / `.minCronIntervalSec`.
23
23
  *
24
24
  * Middlewares are built once at boot while the tasks surface is per-user, so
25
- * the plugin stashes each request's `ctx.tasks` keyed by the authenticated
26
- * user DID (`getRequestTools` runs on every agent build, before any model
27
- * call) and the middleware looks the surface up per turn.
25
+ * the plugin holds each request's `ctx.tasks` keyed by the authenticated
26
+ * user DID for the length of the turn (`getRequestTools` runs on every agent
27
+ * build, before any model call; `onTurnEnd` releases it) and the middleware
28
+ * looks the surface up per model call — see `TaskSurfaceRegistry`.
28
29
  */
29
30
  import { z } from 'zod';
30
31
  import { defineOraclePlugin } from '../../plugin-api/define-plugin';
@@ -41,12 +42,59 @@ export const tasksConfigSchema = z.object({
41
42
 
42
43
  export const TASKS_PLUGIN_NAME = 'tasks';
43
44
 
44
- export function createTasksPlugin(): OraclePlugin {
45
- // ctx.tasks per user DID, refreshed on every agent build. One plugin
46
- // instance can serve several users' Durable Objects in the same isolate,
47
- // so the key is the authenticated DID from the request context.
48
- const surfaces = new Map<string, OracleTasksSurface>();
45
+ /**
46
+ * `ctx.tasks` per user DID, for the turns that are running. One plugin
47
+ * instance serves every user object in the isolate, and a surface keeps its
48
+ * user object's scheduler and store reachable, so an entry lives only while
49
+ * a turn of that user holds it: `hold` at the agent build, the returned
50
+ * release at the turn's end. Overlapping turns of one user (two sessions)
51
+ * share the entry; the last release drops it.
52
+ */
53
+ export class TaskSurfaceRegistry {
54
+ private readonly entries = new Map<
55
+ string,
56
+ { surface: OracleTasksSurface; turns: number }
57
+ >();
58
+
59
+ /** Hold `surface` for `did` for one turn. The release runs once. */
60
+ hold(did: string, surface: OracleTasksSurface): () => void {
61
+ let entry = this.entries.get(did);
62
+ if (entry) {
63
+ entry.surface = surface;
64
+ entry.turns += 1;
65
+ } else {
66
+ entry = { surface, turns: 1 };
67
+ this.entries.set(did, entry);
68
+ }
69
+ const held = entry;
70
+ let released = false;
71
+ return () => {
72
+ if (released) return;
73
+ released = true;
74
+ held.turns -= 1;
75
+ if (held.turns <= 0 && this.entries.get(did) === held)
76
+ this.entries.delete(did);
77
+ };
78
+ }
79
+
80
+ /** The user's turn runs without a scheduler: forget any surface. */
81
+ clear(did: string): void {
82
+ this.entries.delete(did);
83
+ }
84
+
85
+ surfaceFor(did: string): OracleTasksSurface | undefined {
86
+ return this.entries.get(did)?.surface;
87
+ }
88
+
89
+ /** Users with an entry (diagnostics, tests). */
90
+ get size(): number {
91
+ return this.entries.size;
92
+ }
93
+ }
49
94
 
95
+ export function createTasksPlugin(
96
+ surfaces: TaskSurfaceRegistry = new TaskSurfaceRegistry(),
97
+ ): OraclePlugin {
50
98
  return defineOraclePlugin({
51
99
  name: TASKS_PLUGIN_NAME,
52
100
  version: '1.0.0',
@@ -57,13 +105,18 @@ export function createTasksPlugin(): OraclePlugin {
57
105
  autoDetect: () => true,
58
106
  getTools: () => createTaskTools(),
59
107
  getRequestTools: (rtCtx) => {
60
- if (rtCtx.tasks) surfaces.set(rtCtx.user.did, rtCtx.tasks);
61
- else surfaces.delete(rtCtx.user.did);
108
+ if (!rtCtx.tasks) {
109
+ surfaces.clear(rtCtx.user.did);
110
+ return [];
111
+ }
112
+ // A host without per-turn cleanup keeps the entry until the user's
113
+ // next turn without a scheduler (the Workers host always has it).
114
+ rtCtx.onTurnEnd?.(surfaces.hold(rtCtx.user.did, rtCtx.tasks));
62
115
  return [];
63
116
  },
64
117
  getMiddlewares: (ctx) => [
65
118
  createTaskApprovalGateMiddleware({
66
- surfaceFor: (userDid) => surfaces.get(userDid),
119
+ surfaceFor: (userDid) => surfaces.surfaceFor(userDid),
67
120
  logger: ctx.logger,
68
121
  }),
69
122
  ],
@@ -18,11 +18,22 @@ export const FORMALITY_LEVELS = [
18
18
  'academic',
19
19
  ] as const;
20
20
 
21
+ /**
22
+ * Longest `tone` label. The tone is pasted into every system prompt, so new
23
+ * writes over this length are rejected by the tool schema, and a longer value
24
+ * already stored (an older write, or one from the Node runtime, which never
25
+ * capped it) is cut to this length when it is read.
26
+ */
27
+ export const MAX_TONE_LENGTH = 120;
28
+
21
29
  export const UserPreferencesSchema = z.object({
22
30
  agentName: z.string().max(80).optional(),
23
31
  userName: z.string().max(80).optional(),
24
32
  language: z.string().max(20).optional(), // BCP-47-ish, free-form
25
- tone: z.string().optional(), // e.g. "casual", "playful"
33
+ tone: z
34
+ .string()
35
+ .transform((t) => t.slice(0, MAX_TONE_LENGTH))
36
+ .optional(), // e.g. "casual", "playful"
26
37
  formality: z.enum(FORMALITY_LEVELS).optional(),
27
38
  customInstructions: z.string().max(2000).optional(),
28
39
  updatedAt: z.iso.datetime().optional(), // the store sets it on every write
@@ -86,29 +86,51 @@ export class UserPreferencesStore implements UserPreferencesSurface {
86
86
  * user without preferences costs one state read per TTL, not per turn.
87
87
  */
88
88
  async get(roomId: string): Promise<UserPreferences | undefined> {
89
+ try {
90
+ return await this.load(roomId);
91
+ } catch (error) {
92
+ this.logger?.warn(
93
+ `[user-preferences] failed to load prefs for room ${roomId}: ${
94
+ error instanceof Error ? error.message : String(error)
95
+ }`,
96
+ );
97
+ return undefined;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * The room's preferences: `undefined` when no event exists yet or the
103
+ * stored payload is unreadable (logged); THROWS when the gateway read
104
+ * itself fails, so a caller about to overwrite the event can tell "nothing
105
+ * stored" from "could not look".
106
+ */
107
+ private async load(
108
+ roomId: string,
109
+ ): Promise<StoredUserPreferences | undefined> {
89
110
  const hit = this.cache.get(roomId);
90
111
  if (hit && hit.expiresAt > this.now()) return hit.value;
91
112
 
113
+ const json = await this.access.getRoomStateEvent(
114
+ roomId,
115
+ ROOM_STATE_EVENT_TYPE,
116
+ USER_PREFS_STATE_KEY,
117
+ );
118
+ if (json === null) {
119
+ this.remember(roomId, undefined);
120
+ return undefined;
121
+ }
122
+
92
123
  let decoded: unknown;
93
124
  try {
94
- const json = await this.access.getRoomStateEvent(
95
- roomId,
96
- ROOM_STATE_EVENT_TYPE,
97
- USER_PREFS_STATE_KEY,
98
- );
99
- decoded =
100
- json === null
101
- ? null
102
- : await decodeRoomStateContent(JSON.parse(json) as unknown);
125
+ decoded = await decodeRoomStateContent(JSON.parse(json) as unknown);
103
126
  } catch (error) {
104
127
  this.logger?.warn(
105
- `[user-preferences] failed to load prefs for room ${roomId}: ${
128
+ `[user-preferences] unreadable prefs payload for room ${roomId}: ${
106
129
  error instanceof Error ? error.message : String(error)
107
130
  }`,
108
131
  );
109
132
  return undefined;
110
133
  }
111
-
112
134
  if (decoded === null) {
113
135
  this.remember(roomId, undefined);
114
136
  return undefined;
@@ -128,13 +150,16 @@ export class UserPreferencesStore implements UserPreferencesSurface {
128
150
  * Merge `partial` into the stored preferences (only the provided fields
129
151
  * change; `undefined` values are ignored rather than clearing a field) and
130
152
  * persist the result with a fresh `updatedAt`. Returns the merged record.
131
- * An empty `partial` is a no-op that returns the current record.
153
+ * An empty `partial` is a no-op that returns the current record. Throws —
154
+ * without writing — when the current preferences cannot be read, so a
155
+ * transient gateway failure never replaces the stored fields with only the
156
+ * new ones. A payload that exists but cannot be decoded is replaced.
132
157
  */
133
158
  async set(
134
159
  roomId: string,
135
160
  partial: Partial<UserPreferences>,
136
161
  ): Promise<UserPreferences> {
137
- const current = (await this.get(roomId)) ?? {};
162
+ const current = (await this.load(roomId)) ?? {};
138
163
  const provided = Object.fromEntries(
139
164
  Object.entries(partial).filter(([, value]) => value !== undefined),
140
165
  );
@@ -11,7 +11,7 @@ import type {
11
11
  PluginTool,
12
12
  RuntimeContext,
13
13
  } from '../../plugin-api/types';
14
- import { FORMALITY_LEVELS } from './schema';
14
+ import { FORMALITY_LEVELS, MAX_TONE_LENGTH } from './schema';
15
15
 
16
16
  export const SET_USER_PREFERENCES_TOOL_NAME = 'set_user_preferences';
17
17
 
@@ -46,10 +46,11 @@ export const setUserPreferencesSchema = z.object({
46
46
  ),
47
47
  tone: z
48
48
  .string()
49
+ .max(MAX_TONE_LENGTH)
49
50
  .optional()
50
51
  .describe(
51
52
  "Short tone label your future self will see at the top of every prompt (e.g. 'playful and warm', " +
52
- "'concise and dry', 'patient teacher'). Pick descriptive words, not vague ones.",
53
+ `'concise and dry', 'patient teacher'). Pick descriptive words, not vague ones. Max ${MAX_TONE_LENGTH} characters.`,
53
54
  ),
54
55
  formality: z
55
56
  .enum(FORMALITY_LEVELS)