@ixo/oracle-runtime-workers 0.17.1 → 0.19.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.
@@ -222,6 +222,7 @@ export function createCapabilityRouter(
222
222
  `capability=${verdict.capability ?? '-'} ` +
223
223
  `capabilityConfidence=${verdict.capabilityConfidence?.toFixed(2) ?? '-'} ` +
224
224
  `wouldPreload=${list(verdict.preload)} reason=${verdict.reason} ` +
225
+ `calibrationProfile=${verdict.calibrationProfileRef ?? '-'} ` +
225
226
  `latencyMs=${now() - started} provider=${evaluation.provider} model=${evaluation.model}`,
226
227
  );
227
228
  turn.onShadowVerdict?.(verdict.preload);
package/src/core/index.ts CHANGED
@@ -488,11 +488,27 @@ export function createRuntimeCore(opts: RuntimeCoreOptions): RuntimeCore {
488
488
  registries.manifests.register(plugin, manifestOverrides[plugin.name]);
489
489
  }
490
490
  const manifestErrors: string[] = [];
491
+ const lintVerbose = opts.env['BOOT_LINT_VERBOSE'] === 'true';
492
+ const lintCounts = new Map<string, number>();
491
493
  for (const { pluginName, manifest } of registries.manifests.collect()) {
492
494
  const result = validateManifest(manifest, pluginName);
493
- for (const warning of result.warnings) logger.warn(`[boot] ${warning}`);
495
+ if (lintVerbose)
496
+ for (const warning of result.warnings) logger.warn(`[boot] ${warning}`);
497
+ else if (result.warnings.length > 0)
498
+ lintCounts.set(pluginName, result.warnings.length);
494
499
  if (!result.valid) manifestErrors.push(...result.errors);
495
500
  }
501
+ // The soft-rule warnings are the same on every boot of an isolate (one
502
+ // core per isolate): one summary line, the full list with
503
+ // BOOT_LINT_VERBOSE=true.
504
+ if (lintCounts.size > 0)
505
+ logger.warn(
506
+ `[boot] manifest lint: ${[...lintCounts.values()].reduce((a, b) => a + b, 0)} soft warning(s) (${[
507
+ ...lintCounts,
508
+ ]
509
+ .map(([plugin, count]) => `${plugin}=${count}`)
510
+ .join(', ')}); set BOOT_LINT_VERBOSE=true to list them`,
511
+ );
496
512
  const guides = registries.manifests.operatingGuides();
497
513
  if (guides.length > 0) {
498
514
  logger.log(
@@ -707,9 +723,26 @@ export function createRuntimeCore(opts: RuntimeCoreOptions): RuntimeCore {
707
723
  registries.tools,
708
724
  registries.subAgents,
709
725
  );
710
- for (const issue of cross.errors) {
726
+ if (lintVerbose) {
727
+ for (const issue of cross.errors) {
728
+ logger.warn(
729
+ `[boot] ${issue} (boot-time tools only — ignore if the tool comes from getRequestTools)`,
730
+ );
731
+ }
732
+ } else if (cross.errors.length > 0) {
733
+ const perPlugin = new Map<string, number>();
734
+ for (const issue of cross.errors) {
735
+ const plugin = /^\[([^\]]+)\]/.exec(issue)?.[1] ?? 'unknown';
736
+ perPlugin.set(plugin, (perPlugin.get(plugin) ?? 0) + 1);
737
+ }
711
738
  logger.warn(
712
- `[boot] ${issue} (boot-time tools only — ignore if the tool comes from getRequestTools)`,
739
+ `[boot] manifest examples: ${cross.errors.length} reference(s) to tools not registered at boot (${[
740
+ ...perPlugin,
741
+ ]
742
+ .map(([plugin, count]) => `${plugin}=${count}`)
743
+ .join(
744
+ ', ',
745
+ )}; ignore those from getRequestTools); set BOOT_LINT_VERBOSE=true to list them`,
713
746
  );
714
747
  }
715
748
  } catch (err) {
@@ -135,7 +135,10 @@ export function resolveDeliveryProfile(
135
135
  const surface =
136
136
  turn.client === 'matrix'
137
137
  ? 'matrix'
138
- : turn.channel?.provider && DEFAULT_LIMITS[turn.channel.provider]
138
+ : // An unknown provider id uses the generic profile. `hasOwn`: a
139
+ // provider id such as `constructor` must not hit the prototype.
140
+ turn.channel?.provider &&
141
+ Object.hasOwn(DEFAULT_LIMITS, turn.channel.provider)
139
142
  ? turn.channel.provider
140
143
  : 'generic';
141
144
  const merged: ChatLimits = {
@@ -46,7 +46,7 @@ function linkLabel(title: string): string {
46
46
  }
47
47
 
48
48
  /**
49
- * The plan as one Markdown message: what the Companion room mirror of a
49
+ * The plan as one Markdown message: what the user's oracle room mirror of a
50
50
  * channel turn and gateways that predate plans receive. Never stored as a
51
51
  * run's text, which stays the model's own.
52
52
  */
@@ -8,7 +8,10 @@ import type {
8
8
  ChannelTurnOutcome,
9
9
  } from '../channels/contract';
10
10
  import type { TaskExecutionProfile } from '../core/execution-profile';
11
- import type { CapabilityRouterMode } from '@ixo/common/ai/decisions';
11
+ import type {
12
+ CapabilityRouterMode,
13
+ DecisionProviderName,
14
+ } from '@ixo/common/ai/decisions';
12
15
  import type { TranscriptPageOptions } from './transcript';
13
16
  import type { TurnUsage } from '../core/turn-budget';
14
17
  import type { BotStatus, EncryptedFileInfo } from '@ixo/matrix-bot-workers-sdk';
@@ -62,6 +65,7 @@ export interface OracleWorkerEnv {
62
65
  UCAN_AUTH_MAX_TTL_SECONDS?: string;
63
66
  /** `true` restores the legacy bare-delegation authentication (off by default). */
64
67
  UCAN_ALLOW_BARE_DELEGATION_AUTH?: string;
68
+ /** The channel service DID, or a comma-separated list of them. */
65
69
  CHANNEL_SERVICE_DID?: string;
66
70
  AUTH_HUB?: Fetcher;
67
71
  AUTH_HUB_URL?: string;
@@ -273,10 +277,16 @@ export interface OracleWorkerEnv {
273
277
  NEBIUS_API_KEY?: string;
274
278
 
275
279
  // --- decisions ----------------------------------------------------------
276
- /** Bounded semantic Decision provider; unset leaves Decisions unconfigured. `openrouter-jev` reuses `OPEN_ROUTER_API_KEY`. */
277
- DECISION_PROVIDER?: 'cloudflare-jev' | 'openrouter-jev';
278
- /** Jev model id override (`typesafe/jev` on Workers AI, `typesafe/jev-1.13` on OpenRouter). */
280
+ /** Bounded semantic Decision provider; unset leaves Decisions unconfigured. `openrouter-jev` reuses `OPEN_ROUTER_API_KEY`; the Databricks lanes need `DATABRICKS_WORKSPACE_URL` + `DATABRICKS_TOKEN`. */
281
+ DECISION_PROVIDER?: DecisionProviderName;
282
+ /** Model id override (`typesafe/jev` on Workers AI, `typesafe/jev-1.13` on OpenRouter, the Unity Catalog model service `system.ai.openjev-qwen35-4b` for `databricks-systemone`); not used by `databricks-ai-decide`. */
279
283
  DECISION_MODEL?: string;
284
+ /** Databricks workspace origin (`https://<host>`, no path or query) for both Databricks lanes. */
285
+ DATABRICKS_WORKSPACE_URL?: string;
286
+ /** Secret. Databricks workspace token for both Databricks lanes (sent as HTTP Basic `token:<token>`). */
287
+ DATABRICKS_TOKEN?: string;
288
+ /** `ai_decide` function API version for `databricks-ai-decide` (default and only accepted value `1.0`). */
289
+ DATABRICKS_AI_DECIDE_VERSION?: string;
280
290
  /** Cloudflare account for `cloudflare-jev` over the REST API; not needed when the `AI` binding is declared. */
281
291
  CLOUDFLARE_ACCOUNT_ID?: string;
282
292
  /** Workers AI token for `cloudflare-jev` over the REST API; not needed when the `AI` binding is declared. */
@@ -354,7 +364,8 @@ export interface TurnIdentity {
354
364
  /** Request the shell / gateway sends to `UserOracleDO` to run one turn. */
355
365
  export interface TurnRequest {
356
366
  channel?: {
357
- provider: 'whatsapp';
367
+ /** A provider id (`/^[a-z][a-z0-9_-]{0,31}$/`), e.g. `whatsapp`. */
368
+ provider: string;
358
369
  bindingId: string;
359
370
  remoteMessageRef: string;
360
371
  };
@@ -660,6 +671,16 @@ export interface UserOracleObject extends Rpc.DurableObjectBranded {
660
671
  input: ChannelTurnInput,
661
672
  requestHash: string,
662
673
  ): Promise<ChannelTurnOutcome>;
674
+ /**
675
+ * A poll of a channel request whose run exists, answered by a warm
676
+ * object; `null` = admit it through `channelTurn` (after the binding
677
+ * check).
678
+ */
679
+ channelPoll(
680
+ identity: TurnIdentity,
681
+ input: ChannelTurnInput,
682
+ requestHash: string,
683
+ ): Promise<ChannelTurnOutcome | null>;
663
684
  /** Run a turn and return the final text (used by the Matrix gateway). */
664
685
  runTurn(req: TurnRequest): Promise<TurnResult>;
665
686
  /**
@@ -43,7 +43,7 @@ export interface MirrorSend {
43
43
  threadId: string;
44
44
  txnId: string;
45
45
  origin?: {
46
- provider: 'whatsapp';
46
+ provider: string;
47
47
  bindingId: string;
48
48
  remoteMessageRef: string;
49
49
  };
@@ -97,8 +97,10 @@ export class RoomMirror {
97
97
  /**
98
98
  * Queue one mirror behind the session's previous ones. `prepare` runs when
99
99
  * the session's earlier mirrors have settled and resolves what to send
100
- * (`null` = nothing to mirror, e.g. no room). Never rejects: a mirror that
101
- * cannot be posted is logged and the chain moves on.
100
+ * (`null` = nothing to mirror, e.g. no room). A mirror that cannot be
101
+ * posted is logged and the chain moves on; a `required` one (a channel
102
+ * request's, whose receipt records the delivery) rejects its caller
103
+ * instead, and the chain still moves on.
102
104
  */
103
105
  enqueue(
104
106
  sessionId: string,
@@ -107,7 +109,10 @@ export class RoomMirror {
107
109
  required = false,
108
110
  ): Promise<void> {
109
111
  const previous = this.chains.get(sessionId) ?? Promise.resolve();
112
+ // Starts once the previous mirror settled either way: a required mirror
113
+ // that failed (and rejected its own caller) must not skip this one.
110
114
  const tracked: Promise<void> = previous
115
+ .catch(() => undefined)
111
116
  .then(() => prepare())
112
117
  .then((send) => (send ? this.send(send, label) : undefined))
113
118
  .catch((err: unknown) => {
@@ -412,8 +412,8 @@ async function legacyPrune(db: DoSqliteDatabase, now: number): Promise<void> {
412
412
  await db.transaction(async () => {
413
413
  if (row.client === 'channel')
414
414
  await db.run(
415
- 'INSERT OR IGNORE INTO channel_run_tombstones (run_id, status) VALUES (?, ?)',
416
- [row.run_id, row.status],
415
+ 'INSERT OR IGNORE INTO channel_run_tombstones (run_id, status, pruned_at) VALUES (?, ?, ?)',
416
+ [row.run_id, row.status, now],
417
417
  );
418
418
  for (const table of [
419
419
  'turn_tool_marks',
@@ -486,8 +486,26 @@ export class RunStore {
486
486
  state TEXT NOT NULL DEFAULT 'pending'
487
487
  ) WITHOUT ROWID`);
488
488
  await this.db.run(`CREATE TABLE IF NOT EXISTS channel_run_tombstones (
489
- run_id TEXT PRIMARY KEY, status TEXT NOT NULL
489
+ run_id TEXT PRIMARY KEY, status TEXT NOT NULL, pruned_at INTEGER
490
490
  ) WITHOUT ROWID`);
491
+ // When the run was pruned (epoch ms): tombstones are removed a while
492
+ // later (`pruneChannelTombstones`). Rows from before the column count
493
+ // from its migration.
494
+ const tombstoneColumns = await this.db.exec<{ name: string }>(
495
+ 'PRAGMA table_info(channel_run_tombstones)',
496
+ );
497
+ if (!tombstoneColumns.some((c) => c.name === 'pruned_at')) {
498
+ await this.db.run(
499
+ 'ALTER TABLE channel_run_tombstones ADD COLUMN pruned_at INTEGER',
500
+ );
501
+ await this.db.run(
502
+ 'UPDATE channel_run_tombstones SET pruned_at = ? WHERE pruned_at IS NULL',
503
+ [this.now()],
504
+ );
505
+ }
506
+ await this.db.run(
507
+ 'CREATE INDEX IF NOT EXISTS idx_channel_run_tombstones_pruned ON channel_run_tombstones(pruned_at)',
508
+ );
491
509
  // The Reply Plan a finished chat-surface run delivers (chat delivery).
492
510
  await this.db.run(`CREATE TABLE IF NOT EXISTS turn_run_plans (
493
511
  run_id TEXT PRIMARY KEY, plan TEXT NOT NULL
@@ -519,9 +537,9 @@ export class RunStore {
519
537
  if (await this.db.get(`${stale} LIMIT 1`, [cutoff]))
520
538
  await this.db.transaction(async () => {
521
539
  await this.db.run(
522
- `INSERT OR IGNORE INTO channel_run_tombstones (run_id, status)
523
- SELECT run_id, status FROM turn_runs WHERE client = 'channel' AND ${STALE_RUN_FILTER}`,
524
- [cutoff],
540
+ `INSERT OR IGNORE INTO channel_run_tombstones (run_id, status, pruned_at)
541
+ SELECT run_id, status, ? FROM turn_runs WHERE client = 'channel' AND ${STALE_RUN_FILTER}`,
542
+ [this.now(), cutoff],
525
543
  );
526
544
  for (const table of [
527
545
  'turn_tool_marks',
@@ -537,8 +555,9 @@ export class RunStore {
537
555
  });
538
556
  // Every receipt whose run is tombstoned, this boot or an earlier one
539
557
  // that stopped before getting here. Driven by the receipts (at most the
540
- // retention window of them), one key lookup each into the tombstones,
541
- // which are never pruned and never scanned. The table exists once the
558
+ // retention window of them), one key lookup each into the tombstones
559
+ // (pruned separately, by age, from the housekeeping alarm through
560
+ // `pruneChannelTombstones`, never scanned here). The table exists once the
542
561
  // object has served a channel turn.
543
562
  const receipts = await this.db.get<{ name: string }>(
544
563
  `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'channel_requests'`,
@@ -569,6 +588,22 @@ export class RunStore {
569
588
  return row?.plan ?? null;
570
589
  }
571
590
 
591
+ /**
592
+ * Remove up to `limit` tombstones of channel runs pruned before `before`
593
+ * (epoch ms); returns how many went. A request id reused after its
594
+ * tombstone is gone is a new request and runs again.
595
+ */
596
+ async pruneChannelTombstones(before: number, limit: number): Promise<number> {
597
+ await this.setup();
598
+ const pruned = await this.db.run(
599
+ `DELETE FROM channel_run_tombstones WHERE run_id IN (
600
+ SELECT run_id FROM channel_run_tombstones WHERE pruned_at < ? LIMIT ?
601
+ )`,
602
+ [before, limit],
603
+ );
604
+ return pruned.changes;
605
+ }
606
+
572
607
  async wasChannelRunPruned(runId: string): Promise<boolean> {
573
608
  await this.setup();
574
609
  return !!(await this.db.get<{ run_id: string }>(