@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.
- package/package.json +3 -3
- package/src/channels/auth.ts +95 -8
- package/src/channels/contract.ts +53 -10
- package/src/channels/failures.ts +64 -0
- package/src/channels/turns.ts +661 -106
- package/src/core/capability-router.ts +1 -0
- package/src/core/index.ts +36 -3
- package/src/delivery/profile.ts +4 -1
- package/src/delivery/schema.ts +1 -1
- package/src/do/contracts.ts +26 -5
- package/src/do/room-mirror.ts +8 -3
- package/src/do/run-store-test-do.ts +2 -2
- package/src/do/run-store.ts +41 -6
- package/src/do/user-oracle-do.ts +191 -80
- package/src/shell/app.ts +18 -10
|
@@ -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
|
-
|
|
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
|
-
|
|
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] ${
|
|
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) {
|
package/src/delivery/profile.ts
CHANGED
|
@@ -135,7 +135,10 @@ export function resolveDeliveryProfile(
|
|
|
135
135
|
const surface =
|
|
136
136
|
turn.client === 'matrix'
|
|
137
137
|
? 'matrix'
|
|
138
|
-
:
|
|
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 = {
|
package/src/delivery/schema.ts
CHANGED
|
@@ -46,7 +46,7 @@ function linkLabel(title: string): string {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* The plan as one Markdown message: what the
|
|
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
|
*/
|
package/src/do/contracts.ts
CHANGED
|
@@ -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 {
|
|
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?:
|
|
278
|
-
/**
|
|
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
|
|
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
|
/**
|
package/src/do/room-mirror.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface MirrorSend {
|
|
|
43
43
|
threadId: string;
|
|
44
44
|
txnId: string;
|
|
45
45
|
origin?: {
|
|
46
|
-
provider:
|
|
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).
|
|
101
|
-
*
|
|
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',
|
package/src/do/run-store.ts
CHANGED
|
@@ -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
|
-
//
|
|
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 }>(
|