@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.
Files changed (59) hide show
  1. package/README.md +5 -2
  2. package/package.json +1 -1
  3. package/src/core/context-budget.ts +201 -0
  4. package/src/core/context-window.ts +289 -0
  5. package/src/core/env.ts +38 -0
  6. package/src/core/main-agent-types.ts +46 -0
  7. package/src/core/main-agent.ts +86 -9
  8. package/src/core/middlewares/context-guard.ts +270 -0
  9. package/src/core/middlewares/result-cap.ts +180 -0
  10. package/src/core/middlewares/summarization.ts +87 -6
  11. package/src/core/middlewares/tool-marks.ts +209 -0
  12. package/src/core/openrouter-pricing.ts +42 -1
  13. package/src/core/read-result-tool.ts +77 -0
  14. package/src/core/runtime-context.ts +7 -0
  15. package/src/core/sub-agent-fallback.ts +20 -3
  16. package/src/core/wrap-plugin-tool.ts +24 -3
  17. package/src/do/ambient.ts +17 -0
  18. package/src/do/contracts.ts +249 -0
  19. package/src/do/result-store-test-do.ts +88 -0
  20. package/src/do/result-store.ts +339 -0
  21. package/src/do/run-buffer.ts +204 -0
  22. package/src/do/run-coordinator.ts +572 -0
  23. package/src/do/run-request.ts +52 -0
  24. package/src/do/run-store-test-do.ts +130 -0
  25. package/src/do/run-store.ts +604 -0
  26. package/src/do/session-id-migration.ts +85 -0
  27. package/src/do/speaker-prefix.ts +37 -0
  28. package/src/do/sse-stream.ts +567 -400
  29. package/src/do/transcript.ts +238 -0
  30. package/src/do/turn-body.ts +10 -0
  31. package/src/do/user-oracle-do.ts +1019 -169
  32. package/src/matrix/gateway-do.ts +230 -29
  33. package/src/matrix/group-chat-summarizer.ts +107 -0
  34. package/src/matrix/group-chat.ts +1163 -0
  35. package/src/matrix/inbox-store.ts +23 -3
  36. package/src/matrix/ingest.ts +54 -11
  37. package/src/matrix/reply-chain.ts +34 -25
  38. package/src/owner-store/ixo-vfs-store.ts +12 -5
  39. package/src/owner-store/matrix-media-store.ts +12 -7
  40. package/src/owner-store/measure.ts +39 -0
  41. package/src/owner-store/migrating-store.ts +9 -3
  42. package/src/owner-store/types.ts +14 -4
  43. package/src/plugin-api/tool-helper.ts +9 -1
  44. package/src/plugin-api/types.ts +86 -0
  45. package/src/plugins/index.ts +3 -0
  46. package/src/plugins/matrix-group-chats/README.md +70 -0
  47. package/src/plugins/matrix-group-chats/index.ts +132 -0
  48. package/src/plugins/matrix-group-chats/tools.ts +172 -0
  49. package/src/plugins/tasks/tasks-tools.ts +4 -0
  50. package/src/shell/app.ts +78 -0
  51. package/src/sqlite/cache-config.ts +27 -6
  52. package/src/sqlite/chunk-layout.ts +14 -0
  53. package/src/sqlite/database.ts +188 -19
  54. package/src/sqlite/do-vfs.ts +963 -99
  55. package/src/sqlite/index.ts +13 -0
  56. package/src/sqlite/page-tier.ts +668 -0
  57. package/src/sqlite/sqlite-saver.ts +81 -0
  58. package/src/tasks/scheduler.ts +134 -3
  59. package/src/tasks/store.ts +22 -0
@@ -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) vfs.configureCache(options.cachePages);
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(() => this.execUnlocked<T>(sql, params));
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(() => this.runUnlocked(sql, params));
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
- return await this.txContext.run({ depth: 1 }, async () => {
333
- await this.run('BEGIN IMMEDIATE');
334
- this.txDepth++;
335
- try {
336
- const result = await fn();
337
- this.txDepth--;
338
- await this.run('COMMIT');
339
- return result;
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('ROLLBACK');
344
- } catch {
345
- // The original error is more useful than a rollback failure.
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();