@ixo/oracle-runtime-workers 0.9.0 → 0.11.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 +5 -2
- package/package.json +1 -1
- package/src/core/context-budget.ts +201 -0
- package/src/core/context-window.ts +289 -0
- package/src/core/env.ts +38 -0
- package/src/core/main-agent-types.ts +46 -0
- package/src/core/main-agent.ts +86 -9
- package/src/core/middlewares/context-guard.ts +270 -0
- package/src/core/middlewares/result-cap.ts +180 -0
- package/src/core/middlewares/summarization.ts +87 -6
- package/src/core/middlewares/tool-marks.ts +209 -0
- package/src/core/openrouter-pricing.ts +42 -1
- package/src/core/read-result-tool.ts +77 -0
- package/src/core/runtime-context.ts +7 -0
- package/src/core/sub-agent-fallback.ts +20 -3
- package/src/core/wrap-plugin-tool.ts +24 -3
- package/src/do/ambient.ts +17 -0
- package/src/do/contracts.ts +249 -0
- package/src/do/result-store-test-do.ts +88 -0
- package/src/do/result-store.ts +339 -0
- package/src/do/run-buffer.ts +204 -0
- package/src/do/run-coordinator.ts +572 -0
- package/src/do/run-request.ts +52 -0
- package/src/do/run-store-test-do.ts +130 -0
- package/src/do/run-store.ts +604 -0
- package/src/do/session-id-migration.ts +85 -0
- package/src/do/speaker-prefix.ts +37 -0
- package/src/do/sse-stream.ts +567 -400
- package/src/do/transcript.ts +238 -0
- package/src/do/turn-body.ts +10 -0
- package/src/do/user-oracle-do.ts +1019 -169
- package/src/matrix/gateway-do.ts +230 -29
- package/src/matrix/group-chat-summarizer.ts +107 -0
- package/src/matrix/group-chat.ts +1163 -0
- package/src/matrix/inbox-store.ts +23 -3
- package/src/matrix/ingest.ts +54 -11
- package/src/matrix/reply-chain.ts +34 -25
- package/src/owner-store/ixo-vfs-store.ts +12 -5
- package/src/owner-store/matrix-media-store.ts +12 -7
- package/src/owner-store/measure.ts +39 -0
- package/src/owner-store/migrating-store.ts +9 -3
- package/src/owner-store/types.ts +14 -4
- package/src/plugin-api/tool-helper.ts +9 -1
- package/src/plugin-api/types.ts +86 -0
- package/src/plugins/index.ts +3 -0
- package/src/plugins/matrix-group-chats/README.md +70 -0
- package/src/plugins/matrix-group-chats/index.ts +132 -0
- package/src/plugins/matrix-group-chats/tools.ts +172 -0
- package/src/plugins/tasks/tasks-tools.ts +4 -0
- package/src/shell/app.ts +78 -0
- package/src/sqlite/cache-config.ts +27 -6
- package/src/sqlite/chunk-layout.ts +14 -0
- package/src/sqlite/database.ts +188 -19
- package/src/sqlite/do-vfs.ts +963 -99
- package/src/sqlite/index.ts +13 -0
- package/src/sqlite/page-tier.ts +668 -0
- package/src/sqlite/sqlite-saver.ts +81 -0
- package/src/tasks/scheduler.ts +134 -3
- package/src/tasks/store.ts +22 -0
package/src/sqlite/database.ts
CHANGED
|
@@ -31,14 +31,28 @@ import {
|
|
|
31
31
|
VFS_PAGE_SIZE,
|
|
32
32
|
type DoVfsOptions,
|
|
33
33
|
type DoVfsStats,
|
|
34
|
+
type TierFlushResult,
|
|
35
|
+
type TierStatus,
|
|
34
36
|
type VfsSnapshot,
|
|
35
37
|
} from './do-vfs';
|
|
38
|
+
import type { PageTierOptions } from './page-tier';
|
|
36
39
|
import {
|
|
37
40
|
getOrRegisterVfs,
|
|
38
41
|
loadSqlite,
|
|
39
42
|
type SqliteRuntime,
|
|
40
43
|
} from './wa-sqlite-loader';
|
|
41
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Cold-miss retries. A statement outside a transaction re-runs after each
|
|
47
|
+
* resolve; a transaction rolls back, resolves, and re-runs its callback.
|
|
48
|
+
* Every resolve fetches whole 1 MiB segments (plus one of read-ahead) and
|
|
49
|
+
* pins them for the retry, so even a scan across a cold file converges in
|
|
50
|
+
* a handful of rounds; the caps only guard against a bug that keeps
|
|
51
|
+
* recording misses.
|
|
52
|
+
*/
|
|
53
|
+
const MAX_STATEMENT_MISS_RETRIES = 64;
|
|
54
|
+
const MAX_TRANSACTION_MISS_RETRIES = 32;
|
|
55
|
+
|
|
42
56
|
export type SqlValue = null | number | bigint | string | Uint8Array;
|
|
43
57
|
export type SqlParam = SqlValue | boolean | undefined;
|
|
44
58
|
export type SqlParams =
|
|
@@ -136,7 +150,10 @@ export class DoSqliteDatabase {
|
|
|
136
150
|
}
|
|
137
151
|
vfs.attach(ctx.storage);
|
|
138
152
|
}
|
|
139
|
-
if (!created)
|
|
153
|
+
if (!created) {
|
|
154
|
+
vfs.configureCache(options.cachePages);
|
|
155
|
+
vfs.configureTier(options.tier);
|
|
156
|
+
}
|
|
140
157
|
const pragmas = options.pragmas ?? DEFAULT_PRAGMAS;
|
|
141
158
|
const db = await DoSqliteDatabase.openConnection(
|
|
142
159
|
runtime,
|
|
@@ -152,6 +169,38 @@ export class DoSqliteDatabase {
|
|
|
152
169
|
vfs: DoVfs,
|
|
153
170
|
fileName: string,
|
|
154
171
|
pragmas: readonly string[],
|
|
172
|
+
): Promise<number> {
|
|
173
|
+
// Opening reads the file header and the pragmas read page 1 — before
|
|
174
|
+
// any statement of ours could retry a cold miss. Chunk 0 is never
|
|
175
|
+
// evicted, but a pin-less retry here keeps a reopen robust regardless.
|
|
176
|
+
let pinned = false;
|
|
177
|
+
try {
|
|
178
|
+
for (let attempt = 0; ; attempt++) {
|
|
179
|
+
try {
|
|
180
|
+
return await DoSqliteDatabase.openConnectionOnce(
|
|
181
|
+
runtime,
|
|
182
|
+
vfs,
|
|
183
|
+
fileName,
|
|
184
|
+
pragmas,
|
|
185
|
+
);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (!vfs.hasMisses() || attempt >= MAX_STATEMENT_MISS_RETRIES)
|
|
188
|
+
throw error;
|
|
189
|
+
await vfs.resolveMisses();
|
|
190
|
+
pinned = true;
|
|
191
|
+
vfs.missRetries++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} finally {
|
|
195
|
+
if (pinned) vfs.releasePins();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private static async openConnectionOnce(
|
|
200
|
+
runtime: SqliteRuntime,
|
|
201
|
+
vfs: DoVfs,
|
|
202
|
+
fileName: string,
|
|
203
|
+
pragmas: readonly string[],
|
|
155
204
|
): Promise<number> {
|
|
156
205
|
const db = await runtime.sqlite3.open_v2(
|
|
157
206
|
fileName,
|
|
@@ -203,12 +252,54 @@ export class DoSqliteDatabase {
|
|
|
203
252
|
sql: string,
|
|
204
253
|
params?: SqlParams,
|
|
205
254
|
): Promise<T[]> {
|
|
206
|
-
return this.serialized(() =>
|
|
255
|
+
return this.serialized(() =>
|
|
256
|
+
this.retryingColdMisses(() => this.execUnlocked<T>(sql, params)),
|
|
257
|
+
);
|
|
207
258
|
}
|
|
208
259
|
|
|
209
260
|
/** Run a statement that produces no rows of interest; returns the change count. */
|
|
210
261
|
async run(sql: string, params?: SqlParams): Promise<RunResult> {
|
|
211
|
-
return this.serialized(() =>
|
|
262
|
+
return this.serialized(() =>
|
|
263
|
+
this.retryingColdMisses(() => this.runUnlocked(sql, params)),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Run one statement, re-running it after a cold-miss resolve. Only
|
|
269
|
+
* outside a transaction: inside one, SQLite may have put the pager into
|
|
270
|
+
* its error state, so the transaction as a whole is rolled back and
|
|
271
|
+
* retried by `transaction()` instead.
|
|
272
|
+
*/
|
|
273
|
+
private async retryingColdMisses<T>(op: () => Promise<T>): Promise<T> {
|
|
274
|
+
if (this.txContext.getStore() !== undefined || this.txDepth > 0)
|
|
275
|
+
return op();
|
|
276
|
+
let pinned = false;
|
|
277
|
+
try {
|
|
278
|
+
for (let attempt = 0; ; attempt++) {
|
|
279
|
+
try {
|
|
280
|
+
const result = await op();
|
|
281
|
+
// A few statements swallow an I/O error into their result instead
|
|
282
|
+
// of failing (`PRAGMA integrity_check` reports it as corruption):
|
|
283
|
+
// misses left behind by a "successful" statement mean exactly
|
|
284
|
+
// that, so resolve and run it again.
|
|
285
|
+
if (this.vfs.hasMisses() && attempt < MAX_STATEMENT_MISS_RETRIES) {
|
|
286
|
+
await this.vfs.resolveMisses();
|
|
287
|
+
pinned = true;
|
|
288
|
+
this.vfs.missRetries++;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (!this.vfs.hasMisses() || attempt >= MAX_STATEMENT_MISS_RETRIES)
|
|
294
|
+
throw error;
|
|
295
|
+
await this.vfs.resolveMisses();
|
|
296
|
+
pinned = true;
|
|
297
|
+
this.vfs.missRetries++;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} finally {
|
|
301
|
+
if (pinned) this.vfs.releasePins();
|
|
302
|
+
}
|
|
212
303
|
}
|
|
213
304
|
|
|
214
305
|
/**
|
|
@@ -329,24 +420,50 @@ export class DoSqliteDatabase {
|
|
|
329
420
|
});
|
|
330
421
|
await previous;
|
|
331
422
|
try {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
} catch (error) {
|
|
341
|
-
this.txDepth--;
|
|
423
|
+
// A cold page inside `fn` fails a statement with SQLITE_IOERR; the
|
|
424
|
+
// transaction is rolled back, the segment(s) fetched and pinned, and
|
|
425
|
+
// `fn` re-run from the top (it must be re-runnable: the checkpointer's
|
|
426
|
+
// and stores' callbacks only issue statements). The pins are released
|
|
427
|
+
// to the LRU once the transaction ends either way.
|
|
428
|
+
let pinned = false;
|
|
429
|
+
try {
|
|
430
|
+
for (let attempt = 0; ; attempt++) {
|
|
342
431
|
try {
|
|
343
|
-
await this.run(
|
|
344
|
-
|
|
345
|
-
|
|
432
|
+
return await this.txContext.run({ depth: 1 }, async () => {
|
|
433
|
+
await this.run('BEGIN IMMEDIATE');
|
|
434
|
+
this.txDepth++;
|
|
435
|
+
try {
|
|
436
|
+
const result = await fn();
|
|
437
|
+
this.txDepth--;
|
|
438
|
+
await this.run('COMMIT');
|
|
439
|
+
// Misses a statement swallowed (see `retryingColdMisses`)
|
|
440
|
+
// must not trigger a retry of some later, unrelated error.
|
|
441
|
+
this.vfs.clearMisses();
|
|
442
|
+
return result;
|
|
443
|
+
} catch (error) {
|
|
444
|
+
this.txDepth--;
|
|
445
|
+
try {
|
|
446
|
+
await this.run('ROLLBACK');
|
|
447
|
+
} catch {
|
|
448
|
+
// The original error is more useful than a rollback failure.
|
|
449
|
+
}
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
} catch (error) {
|
|
454
|
+
if (
|
|
455
|
+
!this.vfs.hasMisses() ||
|
|
456
|
+
attempt >= MAX_TRANSACTION_MISS_RETRIES
|
|
457
|
+
)
|
|
458
|
+
throw error;
|
|
459
|
+
await this.vfs.resolveMisses();
|
|
460
|
+
pinned = true;
|
|
461
|
+
this.vfs.missRetries++;
|
|
346
462
|
}
|
|
347
|
-
throw error;
|
|
348
463
|
}
|
|
349
|
-
}
|
|
464
|
+
} finally {
|
|
465
|
+
if (pinned) this.vfs.releasePins();
|
|
466
|
+
}
|
|
350
467
|
} finally {
|
|
351
468
|
release();
|
|
352
469
|
}
|
|
@@ -387,15 +504,17 @@ export class DoSqliteDatabase {
|
|
|
387
504
|
static async wipe(
|
|
388
505
|
ctx: DoSqliteContext,
|
|
389
506
|
fileName = 'oracle.db',
|
|
507
|
+
options: { tier?: PageTierOptions } = {},
|
|
390
508
|
): Promise<void> {
|
|
391
509
|
const runtime = await loadSqlite();
|
|
392
510
|
const vfsName = vfsNameForObject(ctx);
|
|
393
511
|
const vfs = getOrRegisterVfs(
|
|
394
512
|
runtime,
|
|
395
513
|
vfsName,
|
|
396
|
-
() => new DoVfs(vfsName, ctx.storage),
|
|
514
|
+
() => new DoVfs(vfsName, ctx.storage, { tier: options.tier }),
|
|
397
515
|
isDoVfs,
|
|
398
516
|
);
|
|
517
|
+
vfs.configureTier(options.tier);
|
|
399
518
|
for (const handle of vfs.takeConnections()) {
|
|
400
519
|
try {
|
|
401
520
|
await runtime.sqlite3.close(handle);
|
|
@@ -410,6 +529,53 @@ export class DoSqliteDatabase {
|
|
|
410
529
|
// leaked xOpen handle cannot block the delete.
|
|
411
530
|
vfs.attach(ctx.storage);
|
|
412
531
|
vfs.deleteFile(fileName);
|
|
532
|
+
// The R2 side of the working copy goes with it (nothing references it
|
|
533
|
+
// any more; the owner's file upstream is untouched).
|
|
534
|
+
await vfs.deleteTierObjects();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
// R2 page tier controls (see do-vfs.ts / page-tier.ts)
|
|
539
|
+
// ---------------------------------------------------------------------------
|
|
540
|
+
|
|
541
|
+
get tierEnabled(): boolean {
|
|
542
|
+
return this.vfs.tierEnabled;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** One eviction pass; holds the transaction mutex so no write interleaves. */
|
|
546
|
+
async tierFlush(
|
|
547
|
+
opts: { force?: boolean; maxSegments?: number } = {},
|
|
548
|
+
): Promise<TierFlushResult> {
|
|
549
|
+
if (this.txDepth > 0)
|
|
550
|
+
throw new DoVfsError('tierFlush() is not allowed inside a transaction');
|
|
551
|
+
return this.withoutTransactions(() =>
|
|
552
|
+
this.vfs.tierFlush(this.fileName, opts),
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Persist the chunks touched since the last call (call at the end of a turn). */
|
|
557
|
+
recordAccess(): void {
|
|
558
|
+
this.vfs.recordAccess(this.fileName);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
tierStatus(): TierStatus {
|
|
562
|
+
return this.vfs.tierStatus(this.fileName);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Delete queued/orphaned R2 objects. */
|
|
566
|
+
async tierMaintenance(
|
|
567
|
+
opts: { sweep?: boolean } = {},
|
|
568
|
+
): Promise<{ deleted: number }> {
|
|
569
|
+
return this.vfs.tierMaintenance(opts);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** Pull every cold chunk back into storage (the file is then fully hot). */
|
|
573
|
+
async materializeTier(): Promise<{ chunks: number }> {
|
|
574
|
+
if (this.txDepth > 0)
|
|
575
|
+
throw new DoVfsError(
|
|
576
|
+
'materializeTier() is not allowed inside a transaction',
|
|
577
|
+
);
|
|
578
|
+
return this.withoutTransactions(() => this.vfs.materialize(this.fileName));
|
|
413
579
|
}
|
|
414
580
|
|
|
415
581
|
/** Close the connection. Buffered pages are flushed by the VFS on close. */
|
|
@@ -535,6 +701,9 @@ export class DoSqliteDatabase {
|
|
|
535
701
|
return this.withoutTransactions(async () => {
|
|
536
702
|
const beforeBytes = this.fileSize;
|
|
537
703
|
if (this.vfs.fileExists(target)) this.vfs.deleteFile(target);
|
|
704
|
+
// VACUUM reads the whole file through the synchronous path; a cold
|
|
705
|
+
// chunk would fail and restart it. Bring everything home first.
|
|
706
|
+
if (this.vfs.tierEnabled) await this.vfs.materialize(this.fileName);
|
|
538
707
|
// `fileName` is a constant of the runtime, never user input.
|
|
539
708
|
await this.run(`VACUUM INTO '${target.replace(/'/g, "''")}'`);
|
|
540
709
|
await this.close();
|