@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.
- package/README.md +32 -10
- package/package.json +13 -8
- package/src/artifacts/config.ts +158 -0
- package/src/artifacts/crypto.ts +94 -0
- package/src/artifacts/routes.ts +73 -0
- package/src/artifacts/store.ts +238 -0
- package/src/artifacts/sweep.ts +200 -0
- package/src/artifacts/tool.ts +103 -0
- package/src/artifacts/viewer-page.ts +614 -0
- package/src/attachments/download.ts +168 -45
- package/src/attachments/index.ts +9 -0
- package/src/attachments/magic.ts +33 -0
- package/src/attachments/pipeline.ts +105 -49
- package/src/attachments/sandbox-archive.ts +9 -8
- package/src/attachments/view-cache.ts +142 -0
- package/src/attachments/view.ts +53 -12
- package/src/channels/auth.ts +310 -0
- package/src/channels/contract.ts +246 -0
- package/src/channels/turns.ts +280 -0
- package/src/core/budgeted-llm.ts +223 -0
- package/src/core/capability-router.ts +273 -0
- package/src/core/context-budget.ts +97 -10
- package/src/core/env.ts +111 -0
- package/src/core/execution-profile.ts +17 -0
- package/src/core/index.ts +267 -9
- package/src/core/llm.ts +30 -9
- package/src/core/main-agent-types.ts +41 -1
- package/src/core/main-agent.ts +367 -65
- package/src/core/manifest.ts +34 -0
- package/src/core/meta-tools.ts +128 -18
- package/src/core/middlewares/capability-gate.ts +130 -12
- package/src/core/middlewares/context-guard.ts +55 -24
- package/src/core/middlewares/index.ts +20 -2
- package/src/core/middlewares/page-context.ts +46 -2
- package/src/core/middlewares/return-direct-first.ts +47 -0
- package/src/core/middlewares/summarization.ts +152 -10
- package/src/core/middlewares/tool-execution.ts +323 -0
- package/src/core/middlewares/tool-repetition-guard.ts +202 -35
- package/src/core/middlewares/turn-boundary.ts +162 -0
- package/src/core/openrouter-pricing.ts +155 -22
- package/src/core/prompt-composer.ts +112 -7
- package/src/core/registries.ts +253 -48
- package/src/core/request-admission.ts +159 -0
- package/src/core/runtime-context.ts +107 -8
- package/src/core/state.ts +1 -1
- package/src/core/sub-agent-fallback.ts +21 -2
- package/src/core/subagent-as-tool.ts +78 -43
- package/src/core/tool-access.ts +189 -0
- package/src/core/tool-scheduler.ts +125 -0
- package/src/core/turn-budget.ts +205 -0
- package/src/core/turn-time-note.ts +43 -0
- package/src/core/user-kv.ts +446 -0
- package/src/core/wrap-plugin-tool.ts +20 -0
- package/src/delivery/plan.ts +278 -0
- package/src/delivery/profile.ts +161 -0
- package/src/delivery/prompt.ts +27 -0
- package/src/delivery/schema.ts +61 -0
- package/src/delivery/shaper.ts +389 -0
- package/src/delivery/types.ts +84 -0
- package/src/do/ambient.ts +51 -15
- package/src/do/contracts.ts +170 -6
- package/src/do/idle-eviction.ts +38 -0
- package/src/do/matrix-turn-ledger.ts +24 -5
- package/src/do/result-store-test-do.ts +137 -3
- package/src/do/result-store.ts +239 -58
- package/src/do/room-mirror.ts +29 -0
- package/src/do/run-buffer.ts +64 -9
- package/src/do/run-coordinator.ts +451 -76
- package/src/do/run-request.ts +2 -0
- package/src/do/run-store-test-do.ts +301 -1
- package/src/do/run-store.ts +385 -43
- package/src/do/secrets-adapter.ts +22 -3
- package/src/do/sse-stream.ts +60 -9
- package/src/do/transcript.ts +48 -5
- package/src/do/turn-body.ts +105 -5
- package/src/do/ucan-service.ts +204 -34
- package/src/do/user-oracle-do.ts +2003 -274
- package/src/feedback/config.ts +127 -0
- package/src/feedback/contract.ts +148 -0
- package/src/feedback/linear-sink.ts +285 -0
- package/src/feedback/privacy.ts +150 -0
- package/src/feedback/reservation.ts +298 -0
- package/src/feedback/submit.ts +247 -0
- package/src/index.ts +138 -6
- package/src/llm/byo-adapter.ts +27 -6
- package/src/llm/byo-catalog.ts +13 -0
- package/src/llm/byo-model-fallback.ts +248 -0
- package/src/llm/byo-service.ts +45 -8
- package/src/llm/provider-error.ts +139 -2
- package/src/matrix/gateway-do.ts +504 -108
- package/src/matrix/group-chat.ts +66 -23
- package/src/matrix/inbox-store.ts +16 -1
- package/src/matrix/ingest.ts +79 -14
- package/src/matrix/media-deadline.ts +130 -0
- package/src/matrix/replay-format.ts +5 -3
- package/src/matrix/reply-parts.ts +92 -0
- package/src/matrix/room-alias.ts +96 -0
- package/src/matrix/user-homeserver.ts +166 -7
- package/src/owner-store/ixo-vfs-store.ts +170 -43
- package/src/owner-store/migrating-store.ts +31 -15
- package/src/owner-store/types.ts +18 -3
- package/src/plugin-api/index.ts +43 -2
- package/src/plugin-api/oracle-plugin.ts +35 -0
- package/src/plugin-api/request-admission.ts +41 -0
- package/src/plugin-api/tool-helper.ts +25 -1
- package/src/plugin-api/tool-plane.ts +92 -0
- package/src/plugin-api/types.ts +194 -6
- package/src/plugins/agui/agui.plugin.ts +31 -16
- package/src/plugins/composio/composio-tools.ts +57 -6
- package/src/plugins/composio/composio-ucan.ts +3 -2
- package/src/plugins/composio/composio.plugin.ts +2 -0
- package/src/plugins/delegated-capabilities.ts +25 -0
- package/src/plugins/editor/content-session.ts +173 -83
- package/src/plugins/editor/content-tools.ts +76 -17
- package/src/plugins/editor/editor-mx.ts +28 -9
- package/src/plugins/editor/editor.plugin.ts +3 -1
- package/src/plugins/editor/failures.ts +28 -7
- package/src/plugins/editor/prompts.ts +2 -1
- package/src/plugins/editor/provider.ts +553 -40
- package/src/plugins/editor/standalone-editor-tool.ts +61 -18
- package/src/plugins/firecrawl/firecrawl-tools.ts +13 -5
- package/src/plugins/flows/action-metadata.ts +111 -11
- package/src/plugins/flows/actions.ts +18 -0
- package/src/plugins/flows/decision-context.ts +31 -0
- package/src/plugins/flows/edit.ts +223 -52
- package/src/plugins/flows/errors.ts +22 -1
- package/src/plugins/flows/flow-doc.ts +92 -9
- package/src/plugins/flows/flows.plugin.ts +11 -5
- package/src/plugins/flows/forms.ts +34 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/input-policy.ts +140 -0
- package/src/plugins/flows/prompts.ts +14 -11
- package/src/plugins/flows/read.ts +49 -9
- package/src/plugins/flows/ref-syntax.ts +129 -0
- package/src/plugins/flows/test-homeserver.ts +245 -0
- package/src/plugins/flows/tools/authoring.ts +216 -112
- package/src/plugins/flows/tools/discovery.ts +4 -1
- package/src/plugins/flows/tools/forms.ts +21 -2
- package/src/plugins/flows/tools/settings.ts +34 -13
- package/src/plugins/flows/translator.ts +13 -10
- package/src/plugins/flows/types.ts +167 -42
- package/src/plugins/index.ts +11 -1
- package/src/plugins/ixo-transaction/chain-ids.ts +43 -0
- package/src/plugins/ixo-transaction/index.ts +2 -0
- package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +119 -0
- package/src/plugins/ixo-transaction/receipts.ts +99 -0
- package/src/plugins/ixo-transaction/tools.ts +329 -0
- package/src/plugins/ixo-transaction/wallet-signing.ts +202 -0
- package/src/plugins/mcp-tool-adapter.ts +23 -2
- package/src/plugins/memory/memory-tools.ts +7 -2
- package/src/plugins/memory/memory-ucan.ts +3 -2
- package/src/plugins/memory/memory.plugin.ts +2 -0
- package/src/plugins/pod-creator/blueprint-store.ts +163 -0
- package/src/plugins/pod-creator/blueprint-types.ts +61 -0
- package/src/plugins/pod-creator/bounded-map.ts +87 -0
- package/src/plugins/pod-creator/capsule-content-client.ts +285 -0
- package/src/plugins/pod-creator/chain-gateway.ts +110 -0
- package/src/plugins/pod-creator/config.ts +85 -0
- package/src/plugins/pod-creator/create-session-store.ts +180 -0
- package/src/plugins/pod-creator/create-tools.ts +538 -0
- package/src/plugins/pod-creator/design-pod-roles.ts +111 -0
- package/src/plugins/pod-creator/index.ts +65 -0
- package/src/plugins/pod-creator/orchestration-tools.ts +172 -0
- package/src/plugins/pod-creator/pod-creator.plugin.ts +190 -0
- package/src/plugins/pod-creator/stage.ts +201 -0
- package/src/plugins/pod-creator/sub-agents.ts +189 -0
- package/src/plugins/portal/action-log.ts +21 -1
- package/src/plugins/portal/declared-tools.ts +90 -0
- package/src/plugins/portal/portal.plugin.ts +42 -16
- package/src/plugins/sandbox/sandbox-bridge.ts +79 -10
- package/src/plugins/sandbox/sandbox-mcp.ts +3 -2
- package/src/plugins/sandbox/sandbox-write-blob.ts +11 -1
- package/src/plugins/sandbox/sandbox.plugin.ts +8 -16
- package/src/plugins/tasks/manifest.ts +1 -1
- package/src/plugins/tasks/middleware.ts +20 -7
- package/src/plugins/tasks/tasks-tools.ts +30 -3
- package/src/plugins/tasks/tasks.plugin.ts +64 -11
- package/src/plugins/user-preferences/schema.ts +12 -1
- package/src/plugins/user-preferences/user-preferences-store.ts +38 -13
- package/src/plugins/user-preferences/user-preferences-tool.ts +3 -2
- package/src/plugins/vfs/vfs-client.ts +250 -71
- package/src/plugins/vfs/vfs-content.ts +30 -11
- package/src/plugins/vfs/vfs-errors.ts +22 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +12 -5
- package/src/plugins/vfs/vfs-tools.ts +60 -20
- package/src/realtime/frontend-call-registry.ts +228 -81
- package/src/realtime/realtime-endpoint.ts +215 -44
- package/src/realtime/session-socket-hub.ts +47 -2
- package/src/secrets/secrets-service.ts +9 -3
- package/src/shell/app.ts +562 -82
- package/src/shell/auth.ts +131 -37
- package/src/shell/turn-body-cap.ts +11 -13
- package/src/sqlite/blob-compactor.ts +61 -28
- package/src/sqlite/database.ts +156 -90
- package/src/sqlite/do-vfs.ts +416 -129
- package/src/sqlite/page-tier.ts +133 -40
- package/src/sqlite/serialization.ts +40 -0
- package/src/sqlite/sessions-store.ts +10 -2
- package/src/sqlite/sqlite-saver.ts +139 -19
- package/src/sqlite/user-kv-store.ts +317 -0
- package/src/tasks/schedule.ts +201 -12
- package/src/tasks/scheduler.ts +586 -98
- package/src/tasks/spec.ts +67 -3
- package/src/tasks/store.ts +292 -27
- package/src/tasks/topic-deliverables.ts +70 -0
package/src/do/result-store.ts
CHANGED
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
* R2 objects are removed by `sweep()` at boot; a session's results go with
|
|
19
19
|
* the session. Operators may add an R2 lifecycle rule on the `results/`
|
|
20
20
|
* prefix as a belt on top of the sweep. Identical results de-duplicate by
|
|
21
|
-
* hash (the row's expiry is refreshed instead of a second copy)
|
|
21
|
+
* hash (the row's expiry is refreshed instead of a second copy); every
|
|
22
|
+
* session that stored one is recorded in `tool_result_sessions`, and
|
|
23
|
+
* deleting a session removes only the results no other session holds.
|
|
24
|
+
* `tool_results.session_id` keeps the first session (the row format is
|
|
25
|
+
* unchanged).
|
|
22
26
|
*/
|
|
23
27
|
import type { DoSqliteDatabase } from '../sqlite/database';
|
|
24
28
|
import type { Logger } from '../plugin-api/types';
|
|
@@ -64,6 +68,38 @@ export const DEFAULT_RESULT_R2_MIN_BYTES = 1_000_000;
|
|
|
64
68
|
export const SQLITE_RESULT_MAX_BYTES = 1_500_000;
|
|
65
69
|
/** Longest chunk `read` returns (bytes). */
|
|
66
70
|
export const MAX_READ_BYTES = 64 * 1024;
|
|
71
|
+
/** Ids per `IN (…)` statement. */
|
|
72
|
+
const ID_BATCH = 100;
|
|
73
|
+
/** Longest UTF-8 sequence minus its lead byte. */
|
|
74
|
+
const UTF8_MAX_CONTINUATION = 3;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Move a byte range of UTF-8 text onto character boundaries: the start back
|
|
78
|
+
* to the lead byte of its character, the end back to the start of the
|
|
79
|
+
* character it splits (or, when that would leave nothing, forward past it).
|
|
80
|
+
* `bytes` holds the text from byte `base` on and covers the range plus up
|
|
81
|
+
* to three bytes on either side.
|
|
82
|
+
*/
|
|
83
|
+
export function alignUtf8Range(
|
|
84
|
+
bytes: Uint8Array,
|
|
85
|
+
base: number,
|
|
86
|
+
size: number,
|
|
87
|
+
start: number,
|
|
88
|
+
end: number,
|
|
89
|
+
): { start: number; end: number } {
|
|
90
|
+
const limit = Math.min(size, base + bytes.length);
|
|
91
|
+
const continues = (at: number) =>
|
|
92
|
+
at > base && at < limit && (bytes[at - base]! & 0xc0) === 0x80;
|
|
93
|
+
let from = start;
|
|
94
|
+
while (continues(from)) from -= 1;
|
|
95
|
+
let to = Math.max(from, Math.min(end, limit));
|
|
96
|
+
while (to > from && continues(to)) to -= 1;
|
|
97
|
+
if (to === from && from < limit) {
|
|
98
|
+
to = from + 1;
|
|
99
|
+
while (continues(to)) to += 1;
|
|
100
|
+
}
|
|
101
|
+
return { start: from, end: to };
|
|
102
|
+
}
|
|
67
103
|
|
|
68
104
|
type ResultRow = {
|
|
69
105
|
id: string;
|
|
@@ -86,6 +122,13 @@ export async function sha256Hex(bytes: Uint8Array): Promise<string> {
|
|
|
86
122
|
export class ResultStore {
|
|
87
123
|
private setupPromise: Promise<void> | undefined;
|
|
88
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Stores and removals, one at a time: a removal deletes R2 objects before
|
|
127
|
+
* their rows, so a put of the same content must not land in between (it
|
|
128
|
+
* would keep a row whose object is then deleted).
|
|
129
|
+
*/
|
|
130
|
+
private writes: Promise<unknown> = Promise.resolve();
|
|
131
|
+
|
|
89
132
|
private readonly now: () => number;
|
|
90
133
|
|
|
91
134
|
private readonly ttlMs: number;
|
|
@@ -130,6 +173,28 @@ export class ResultStore {
|
|
|
130
173
|
await this.db.run(
|
|
131
174
|
`CREATE INDEX IF NOT EXISTS idx_tool_results_expires ON tool_results(expires_at)`,
|
|
132
175
|
);
|
|
176
|
+
const hasRefs = await this.db.get(
|
|
177
|
+
`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'tool_result_sessions'`,
|
|
178
|
+
);
|
|
179
|
+
if (!hasRefs)
|
|
180
|
+
// Rows stored before results were shared across sessions name their
|
|
181
|
+
// one session; they become its references in the same transaction
|
|
182
|
+
// that creates the table, so a crash leaves either both or neither.
|
|
183
|
+
await this.db.transaction(async () => {
|
|
184
|
+
await this.db.run(`
|
|
185
|
+
CREATE TABLE IF NOT EXISTS tool_result_sessions (
|
|
186
|
+
id TEXT NOT NULL,
|
|
187
|
+
session_id TEXT NOT NULL,
|
|
188
|
+
PRIMARY KEY (id, session_id)
|
|
189
|
+
)`);
|
|
190
|
+
await this.db.run(
|
|
191
|
+
`CREATE INDEX IF NOT EXISTS idx_tool_result_sessions_session ON tool_result_sessions(session_id)`,
|
|
192
|
+
);
|
|
193
|
+
await this.db.run(
|
|
194
|
+
`INSERT OR IGNORE INTO tool_result_sessions (id, session_id)
|
|
195
|
+
SELECT id, session_id FROM tool_results`,
|
|
196
|
+
);
|
|
197
|
+
});
|
|
133
198
|
await this.sweep();
|
|
134
199
|
}
|
|
135
200
|
|
|
@@ -150,16 +215,32 @@ export class ResultStore {
|
|
|
150
215
|
await this.setup();
|
|
151
216
|
const bytes = new TextEncoder().encode(input.content);
|
|
152
217
|
const id = await sha256Hex(bytes);
|
|
218
|
+
return this.serialized(() => this.store(input, bytes, id));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private async store(
|
|
222
|
+
input: { sessionId: string; toolName: string; content: string },
|
|
223
|
+
bytes: Uint8Array,
|
|
224
|
+
id: string,
|
|
225
|
+
): Promise<StoredResultRef | undefined> {
|
|
153
226
|
const expiresAt = this.now() + this.ttlMs;
|
|
227
|
+
// An expired row is not reused: it may be one kept only because its R2
|
|
228
|
+
// delete failed, with its object already gone. It is stored afresh.
|
|
154
229
|
const existing = await this.db.get<ResultRow>(
|
|
155
|
-
`SELECT id, tier, size FROM tool_results WHERE id = ?`,
|
|
156
|
-
[id],
|
|
230
|
+
`SELECT id, tier, size FROM tool_results WHERE id = ? AND expires_at > ?`,
|
|
231
|
+
[id, this.now()],
|
|
157
232
|
);
|
|
158
233
|
if (existing) {
|
|
159
|
-
await this.db.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
234
|
+
await this.db.transaction(async () => {
|
|
235
|
+
await this.db.run(
|
|
236
|
+
`UPDATE tool_results SET expires_at = ? WHERE id = ?`,
|
|
237
|
+
[expiresAt, id],
|
|
238
|
+
);
|
|
239
|
+
await this.db.run(
|
|
240
|
+
`INSERT OR IGNORE INTO tool_result_sessions (id, session_id) VALUES (?, ?)`,
|
|
241
|
+
[id, input.sessionId],
|
|
242
|
+
);
|
|
243
|
+
});
|
|
163
244
|
return {
|
|
164
245
|
id,
|
|
165
246
|
size: Number(existing.size),
|
|
@@ -183,24 +264,39 @@ export class ResultStore {
|
|
|
183
264
|
},
|
|
184
265
|
});
|
|
185
266
|
}
|
|
186
|
-
await this.db.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
267
|
+
await this.db.transaction(async () => {
|
|
268
|
+
await this.db.run(
|
|
269
|
+
`INSERT INTO tool_results (id, session_id, tool_name, size, tier, content, created_at, expires_at)
|
|
270
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
271
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
272
|
+
tool_name = excluded.tool_name, size = excluded.size,
|
|
273
|
+
tier = excluded.tier, content = excluded.content,
|
|
274
|
+
created_at = excluded.created_at, expires_at = excluded.expires_at`,
|
|
275
|
+
[
|
|
276
|
+
id,
|
|
277
|
+
input.sessionId,
|
|
278
|
+
input.toolName,
|
|
279
|
+
bytes.byteLength,
|
|
280
|
+
useR2 ? 'r2' : 'sqlite',
|
|
281
|
+
useR2 ? null : input.content,
|
|
282
|
+
new Date(this.now()).toISOString(),
|
|
283
|
+
expiresAt,
|
|
284
|
+
],
|
|
285
|
+
);
|
|
286
|
+
await this.db.run(
|
|
287
|
+
`INSERT OR IGNORE INTO tool_result_sessions (id, session_id) VALUES (?, ?)`,
|
|
288
|
+
[id, input.sessionId],
|
|
289
|
+
);
|
|
290
|
+
});
|
|
200
291
|
return { id, size: bytes.byteLength, tier: useR2 ? 'r2' : 'sqlite' };
|
|
201
292
|
}
|
|
202
293
|
|
|
203
|
-
/**
|
|
294
|
+
/**
|
|
295
|
+
* A chunk of a stored result, by byte range, on UTF-8 character
|
|
296
|
+
* boundaries: an offset inside a character starts at that character, and
|
|
297
|
+
* a chunk never ends inside one (`length` and `next` say where it really
|
|
298
|
+
* ended), so concatenating the chunks gives the text back exactly.
|
|
299
|
+
*/
|
|
204
300
|
async read(
|
|
205
301
|
id: string,
|
|
206
302
|
offset = 0,
|
|
@@ -213,80 +309,165 @@ export class ResultStore {
|
|
|
213
309
|
);
|
|
214
310
|
if (!row) return { status: 'not-found' };
|
|
215
311
|
if (Number(row.expires_at) <= this.now()) {
|
|
216
|
-
await this.
|
|
312
|
+
await this.serialized(async () => {
|
|
313
|
+
// Still expired once it is this removal's turn (a put may have
|
|
314
|
+
// stored the same content again meanwhile).
|
|
315
|
+
const expired = await this.db.exec<ResultRow>(
|
|
316
|
+
`SELECT id, tier FROM tool_results WHERE id = ? AND expires_at <= ?`,
|
|
317
|
+
[id, this.now()],
|
|
318
|
+
);
|
|
319
|
+
await this.remove(expired);
|
|
320
|
+
});
|
|
217
321
|
return { status: 'expired' };
|
|
218
322
|
}
|
|
219
323
|
const size = Number(row.size);
|
|
220
|
-
const
|
|
324
|
+
const requested = Math.max(0, Math.min(Math.floor(offset), size));
|
|
221
325
|
const span = Math.max(
|
|
222
326
|
1,
|
|
223
|
-
Math.min(Math.floor(length), MAX_READ_BYTES, size -
|
|
327
|
+
Math.min(Math.floor(length), MAX_READ_BYTES, size - requested),
|
|
224
328
|
);
|
|
225
329
|
const tier: ResultTier = row.tier === 'r2' ? 'r2' : 'sqlite';
|
|
226
|
-
|
|
330
|
+
// The bytes around the range too, to find the character boundaries.
|
|
331
|
+
let base: number;
|
|
332
|
+
let bytes: Uint8Array;
|
|
227
333
|
if (tier === 'r2') {
|
|
334
|
+
base = Math.max(0, requested - UTF8_MAX_CONTINUATION);
|
|
335
|
+
const until = Math.min(size, requested + span + UTF8_MAX_CONTINUATION);
|
|
228
336
|
const object = await this.options.bucket?.get(this.key(id), {
|
|
229
|
-
range: { offset:
|
|
337
|
+
range: { offset: base, length: Math.max(1, until - base) },
|
|
230
338
|
});
|
|
231
339
|
if (!object) return { status: 'not-found' };
|
|
232
|
-
|
|
340
|
+
bytes = new Uint8Array(await object.arrayBuffer());
|
|
233
341
|
} else {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
.subarray(start, start + span);
|
|
342
|
+
base = 0;
|
|
343
|
+
bytes = new TextEncoder().encode(row.content ?? '');
|
|
237
344
|
}
|
|
238
|
-
const
|
|
239
|
-
|
|
345
|
+
const { start, end } = alignUtf8Range(
|
|
346
|
+
bytes,
|
|
347
|
+
base,
|
|
348
|
+
size,
|
|
349
|
+
requested,
|
|
350
|
+
Math.min(size, requested + span),
|
|
351
|
+
);
|
|
352
|
+
const text = new TextDecoder().decode(
|
|
353
|
+
bytes.subarray(start - base, end - base),
|
|
354
|
+
);
|
|
240
355
|
return {
|
|
241
356
|
status: 'ok',
|
|
242
357
|
id,
|
|
243
358
|
tier,
|
|
244
359
|
size,
|
|
245
360
|
offset: start,
|
|
246
|
-
length:
|
|
361
|
+
length: end - start,
|
|
247
362
|
text,
|
|
248
|
-
next,
|
|
363
|
+
next: end < size ? end : null,
|
|
249
364
|
};
|
|
250
365
|
}
|
|
251
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Drop the session's references; remove the results no other session
|
|
369
|
+
* holds (and their R2 objects). Returns how many results were removed.
|
|
370
|
+
*/
|
|
252
371
|
async deleteForSession(sessionId: string): Promise<number> {
|
|
253
372
|
await this.setup();
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
373
|
+
return this.serialized(() => this.removeSession(sessionId));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private async removeSession(sessionId: string): Promise<number> {
|
|
377
|
+
const rows = await this.db.transaction(async () => {
|
|
378
|
+
const orphaned = await this.db.exec<ResultRow>(
|
|
379
|
+
`SELECT r.id, r.tier FROM tool_results r
|
|
380
|
+
WHERE (r.session_id = ?
|
|
381
|
+
OR EXISTS (SELECT 1 FROM tool_result_sessions s
|
|
382
|
+
WHERE s.id = r.id AND s.session_id = ?))
|
|
383
|
+
AND NOT EXISTS (SELECT 1 FROM tool_result_sessions s
|
|
384
|
+
WHERE s.id = r.id AND s.session_id <> ?)`,
|
|
385
|
+
[sessionId, sessionId, sessionId],
|
|
386
|
+
);
|
|
387
|
+
await this.db.run(
|
|
388
|
+
`DELETE FROM tool_result_sessions WHERE session_id = ?`,
|
|
389
|
+
[sessionId],
|
|
390
|
+
);
|
|
391
|
+
return orphaned;
|
|
392
|
+
});
|
|
258
393
|
await this.remove(rows);
|
|
259
394
|
return rows.length;
|
|
260
395
|
}
|
|
261
396
|
|
|
262
397
|
/** Drop expired rows (and their R2 objects). Returns how many. */
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
398
|
+
sweep(): Promise<number> {
|
|
399
|
+
return this.serialized(async () => {
|
|
400
|
+
const rows = await this.db.exec<ResultRow>(
|
|
401
|
+
`SELECT id, tier FROM tool_results WHERE expires_at <= ?`,
|
|
402
|
+
[this.now()],
|
|
403
|
+
);
|
|
404
|
+
await this.remove(rows);
|
|
405
|
+
return rows.length;
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Run `work` after every store or removal already in progress. */
|
|
410
|
+
private serialized<T>(work: () => Promise<T>): Promise<T> {
|
|
411
|
+
const next = this.writes.then(work);
|
|
412
|
+
this.writes = next.catch(() => undefined);
|
|
413
|
+
return next;
|
|
270
414
|
}
|
|
271
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Remove rows and their R2 objects. An R2-tier row whose object could not
|
|
418
|
+
* be deleted stays, marked expired: it is the only record of the object,
|
|
419
|
+
* so the next sweep (or read) tries again instead of leaking it.
|
|
420
|
+
*/
|
|
272
421
|
private async remove(
|
|
273
422
|
rows: Array<Pick<ResultRow, 'id' | 'tier'>>,
|
|
274
423
|
): Promise<void> {
|
|
275
424
|
if (rows.length === 0) return;
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
)
|
|
425
|
+
const r2Rows = rows.filter((r) => r.tier === 'r2');
|
|
426
|
+
const kept = new Set<string>();
|
|
427
|
+
if (r2Rows.length > 0 && this.options.bucket) {
|
|
428
|
+
// R2 deletes at most 1,000 keys per call; a failed call keeps only
|
|
429
|
+
// its own rows.
|
|
430
|
+
for (let i = 0; i < r2Rows.length; i += 1_000) {
|
|
431
|
+
const batch = r2Rows.slice(i, i + 1_000);
|
|
432
|
+
try {
|
|
433
|
+
await this.options.bucket.delete(batch.map((r) => this.key(r.id)));
|
|
434
|
+
} catch (error) {
|
|
435
|
+
this.logger.warn(
|
|
436
|
+
`[results] could not delete ${batch.length} R2 object(s); their rows stay for the next sweep: ${error instanceof Error ? error.message : String(error)}`,
|
|
437
|
+
);
|
|
438
|
+
for (const row of batch) kept.add(row.id);
|
|
439
|
+
}
|
|
286
440
|
}
|
|
287
441
|
}
|
|
288
|
-
|
|
289
|
-
|
|
442
|
+
const gone = rows.filter((r) => !kept.has(r.id)).map((r) => r.id);
|
|
443
|
+
await this.db.transaction(async () => {
|
|
444
|
+
for (let i = 0; i < gone.length; i += ID_BATCH) {
|
|
445
|
+
const ids = gone.slice(i, i + ID_BATCH);
|
|
446
|
+
const marks = ids.map(() => '?').join(', ');
|
|
447
|
+
await this.db.run(
|
|
448
|
+
`DELETE FROM tool_results WHERE id IN (${marks})`,
|
|
449
|
+
ids,
|
|
450
|
+
);
|
|
451
|
+
await this.db.run(
|
|
452
|
+
`DELETE FROM tool_result_sessions WHERE id IN (${marks})`,
|
|
453
|
+
ids,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
const retry = [...kept];
|
|
457
|
+
for (let i = 0; i < retry.length; i += ID_BATCH) {
|
|
458
|
+
const ids = retry.slice(i, i + ID_BATCH);
|
|
459
|
+
const marks = ids.map(() => '?').join(', ');
|
|
460
|
+
await this.db.run(
|
|
461
|
+
`UPDATE tool_results SET expires_at = 0 WHERE id IN (${marks})`,
|
|
462
|
+
ids,
|
|
463
|
+
);
|
|
464
|
+
// No session holds it any more; the row only tracks the object.
|
|
465
|
+
await this.db.run(
|
|
466
|
+
`DELETE FROM tool_result_sessions WHERE id IN (${marks})`,
|
|
467
|
+
ids,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
290
471
|
}
|
|
291
472
|
|
|
292
473
|
async stats(): Promise<{
|
package/src/do/room-mirror.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { channelOrigin } from '../channels/contract';
|
|
1
2
|
/**
|
|
2
3
|
* The room mirror of HTTP chats: every Portal turn is replayed into the
|
|
3
4
|
* user's oracle room as a thread under the session's marker event, the user
|
|
@@ -41,6 +42,11 @@ export interface MirrorSend {
|
|
|
41
42
|
/** The session's marker event — the thread the mirror is posted under. */
|
|
42
43
|
threadId: string;
|
|
43
44
|
txnId: string;
|
|
45
|
+
origin?: {
|
|
46
|
+
provider: 'whatsapp';
|
|
47
|
+
bindingId: string;
|
|
48
|
+
remoteMessageRef: string;
|
|
49
|
+
};
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
export interface RoomMirrorDeps {
|
|
@@ -53,6 +59,27 @@ export interface RoomMirrorDeps {
|
|
|
53
59
|
retry?: Pick<RetryGatewayOptions, 'delaysMs' | 'sleep' | 'isTransient'>;
|
|
54
60
|
}
|
|
55
61
|
|
|
62
|
+
export function mirrorEventContent(send: MirrorSend) {
|
|
63
|
+
return {
|
|
64
|
+
msgtype: 'm.text',
|
|
65
|
+
body: send.body,
|
|
66
|
+
...(send.formattedBody
|
|
67
|
+
? { format: 'org.matrix.custom.html', formatted_body: send.formattedBody }
|
|
68
|
+
: {}),
|
|
69
|
+
'm.relates_to': {
|
|
70
|
+
rel_type: 'm.thread',
|
|
71
|
+
event_id: send.threadId,
|
|
72
|
+
is_falling_back: true,
|
|
73
|
+
'm.in_reply_to': { event_id: send.threadId },
|
|
74
|
+
},
|
|
75
|
+
...(send.origin
|
|
76
|
+
? {
|
|
77
|
+
'org.ixo.qi.origin': channelOrigin(send.origin),
|
|
78
|
+
}
|
|
79
|
+
: {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
56
83
|
const errorText = (err: unknown): string =>
|
|
57
84
|
err instanceof Error ? err.message : String(err);
|
|
58
85
|
|
|
@@ -77,12 +104,14 @@ export class RoomMirror {
|
|
|
77
104
|
sessionId: string,
|
|
78
105
|
prepare: () => Promise<MirrorSend | null>,
|
|
79
106
|
label: string,
|
|
107
|
+
required = false,
|
|
80
108
|
): Promise<void> {
|
|
81
109
|
const previous = this.chains.get(sessionId) ?? Promise.resolve();
|
|
82
110
|
const tracked: Promise<void> = previous
|
|
83
111
|
.then(() => prepare())
|
|
84
112
|
.then((send) => (send ? this.send(send, label) : undefined))
|
|
85
113
|
.catch((err: unknown) => {
|
|
114
|
+
if (required) throw err;
|
|
86
115
|
this.deps.warn(
|
|
87
116
|
`[user-do] Matrix replay (${label}) failed — session=${sessionId}: ${errorText(err)}`,
|
|
88
117
|
);
|
package/src/do/run-buffer.ts
CHANGED
|
@@ -89,9 +89,20 @@ export class RunBuffer {
|
|
|
89
89
|
/** Frames not yet packed into a segment. */
|
|
90
90
|
private readonly tail: RunFrame[] = [];
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Frames taken out of the tail whose segment write has not settled yet,
|
|
94
|
+
* one batch per pending pack, in order. A re-join reads the store and then
|
|
95
|
+
* the tail; without these, a frame packed between the two would be in
|
|
96
|
+
* neither.
|
|
97
|
+
*/
|
|
98
|
+
private readonly inFlight: RunFrame[][] = [];
|
|
99
|
+
|
|
92
100
|
private tailBytes = 0;
|
|
93
101
|
|
|
94
|
-
private readonly subscribers = new
|
|
102
|
+
private readonly subscribers = new Map<RunSubscriber, (() => void) | null>();
|
|
103
|
+
|
|
104
|
+
/** The `done` frame, once pushed (a closed buffer keeps it for late readers). */
|
|
105
|
+
private doneFrame: RunFrame | null = null;
|
|
95
106
|
|
|
96
107
|
private timer: unknown = null;
|
|
97
108
|
|
|
@@ -115,13 +126,19 @@ export class RunBuffer {
|
|
|
115
126
|
return this.closed;
|
|
116
127
|
}
|
|
117
128
|
|
|
129
|
+
/** A `done` frame has been pushed: the run told its subscribers it ended. */
|
|
130
|
+
get hasDone(): boolean {
|
|
131
|
+
return this.doneFrame !== null;
|
|
132
|
+
}
|
|
133
|
+
|
|
118
134
|
/** Append a frame, fan it out, and schedule or force a pack. */
|
|
119
135
|
push(event: string, data: unknown): RunFrame {
|
|
120
136
|
if (this.closed) throw new Error('run buffer is closed');
|
|
121
137
|
const frame: RunFrame = { seq: ++this.seq, event, data };
|
|
138
|
+
if (event === 'done' && this.doneFrame === null) this.doneFrame = frame;
|
|
122
139
|
this.tail.push(frame);
|
|
123
140
|
this.tailBytes += JSON.stringify(frame).length;
|
|
124
|
-
for (const subscriber of this.subscribers) {
|
|
141
|
+
for (const subscriber of this.subscribers.keys()) {
|
|
125
142
|
try {
|
|
126
143
|
subscriber(frame);
|
|
127
144
|
} catch {
|
|
@@ -144,19 +161,40 @@ export class RunBuffer {
|
|
|
144
161
|
}
|
|
145
162
|
|
|
146
163
|
/**
|
|
147
|
-
* The
|
|
148
|
-
*
|
|
164
|
+
* The frames after `after` that a store read may not hold yet — the ones
|
|
165
|
+
* whose segment write is still pending, then the unflushed tail — what a
|
|
166
|
+
* re-join needs on top of the segments it read from the store. A closed
|
|
167
|
+
* buffer also keeps its `done` frame, so a reader that arrives after the
|
|
168
|
+
* run ended still learns how it ended.
|
|
149
169
|
*/
|
|
150
170
|
tailAfter(after: number): RunFrame[] {
|
|
151
|
-
|
|
171
|
+
const out: RunFrame[] = [];
|
|
172
|
+
for (const batch of this.inFlight)
|
|
173
|
+
for (const frame of batch) if (frame.seq > after) out.push(frame);
|
|
174
|
+
for (const frame of this.tail) if (frame.seq > after) out.push(frame);
|
|
175
|
+
const done = this.doneFrame;
|
|
176
|
+
if (
|
|
177
|
+
this.closed &&
|
|
178
|
+
done !== null &&
|
|
179
|
+
done.seq > after &&
|
|
180
|
+
!out.some((frame) => frame.seq === done.seq)
|
|
181
|
+
)
|
|
182
|
+
out.push(done);
|
|
183
|
+
return out;
|
|
152
184
|
}
|
|
153
185
|
|
|
154
186
|
/**
|
|
155
187
|
* Attach a live subscriber. The caller replays the store's segments and
|
|
156
188
|
* `tailAfter(cursor)` first; from then on every new frame is delivered.
|
|
189
|
+
* `onEnd` runs once when the buffer closes (at once when it already is),
|
|
190
|
+
* after the subscriber received every frame.
|
|
157
191
|
*/
|
|
158
|
-
subscribe(subscriber: RunSubscriber): () => void {
|
|
159
|
-
this.
|
|
192
|
+
subscribe(subscriber: RunSubscriber, onEnd?: () => void): () => void {
|
|
193
|
+
if (this.closed) {
|
|
194
|
+
onEnd?.();
|
|
195
|
+
return () => undefined;
|
|
196
|
+
}
|
|
197
|
+
this.subscribers.set(subscriber, onEnd ?? null);
|
|
160
198
|
return () => {
|
|
161
199
|
this.subscribers.delete(subscriber);
|
|
162
200
|
};
|
|
@@ -175,6 +213,7 @@ export class RunBuffer {
|
|
|
175
213
|
if (this.tail.length === 0) return this.packing;
|
|
176
214
|
const frames = this.tail.splice(0, this.tail.length);
|
|
177
215
|
this.tailBytes = 0;
|
|
216
|
+
this.inFlight.push(frames);
|
|
178
217
|
const segment: PackedSegment = {
|
|
179
218
|
seqFrom: frames[0]!.seq,
|
|
180
219
|
seqTo: frames[frames.length - 1]!.seq,
|
|
@@ -190,15 +229,31 @@ export class RunBuffer {
|
|
|
190
229
|
// copy is missing. Report and carry on — the next pack retries
|
|
191
230
|
// nothing (the frames are gone) but the run itself is unaffected.
|
|
192
231
|
this.options.onPackError?.(error);
|
|
232
|
+
})
|
|
233
|
+
.finally(() => {
|
|
234
|
+
// Packs settle in order: this batch is the oldest one pending.
|
|
235
|
+
this.inFlight.shift();
|
|
193
236
|
});
|
|
194
237
|
return this.packing;
|
|
195
238
|
}
|
|
196
239
|
|
|
197
|
-
/**
|
|
240
|
+
/**
|
|
241
|
+
* Final pack; no more frames accepted afterwards. Every subscriber is
|
|
242
|
+
* told the buffer ended (after the frames it was already given) and
|
|
243
|
+
* detached.
|
|
244
|
+
*/
|
|
198
245
|
async close(): Promise<void> {
|
|
199
246
|
if (this.closed) return this.packing;
|
|
200
247
|
this.closed = true;
|
|
201
|
-
|
|
248
|
+
const ended = [...this.subscribers.values()];
|
|
202
249
|
this.subscribers.clear();
|
|
250
|
+
for (const onEnd of ended) {
|
|
251
|
+
try {
|
|
252
|
+
onEnd?.();
|
|
253
|
+
} catch {
|
|
254
|
+
/* a broken subscriber never breaks the run */
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
await this.flush();
|
|
203
258
|
}
|
|
204
259
|
}
|