@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/sqlite/page-tier.ts
CHANGED
|
@@ -71,11 +71,24 @@ export const DEFAULT_TIER_MAX_SEGMENTS_PER_PASS = 64;
|
|
|
71
71
|
* with a clear error rather than thrashing the clean cache.
|
|
72
72
|
*/
|
|
73
73
|
export const DEFAULT_TIER_MISS_PIN_BYTES = 32 * 1024 * 1024;
|
|
74
|
-
/**
|
|
75
|
-
|
|
74
|
+
/**
|
|
75
|
+
* A complete orphan sweep (list the prefix, delete segment objects the map
|
|
76
|
+
* does not reference) at most this often. The time of the last complete
|
|
77
|
+
* sweep is persisted, so isolate restarts and object evictions do not
|
|
78
|
+
* postpone it. Orphans are rare (a crash between a segment upload and its
|
|
79
|
+
* map commit, a failed delete batch, deletes queued in memory by a
|
|
80
|
+
* truncate/import that never reached maintenance), so a weekly listing is
|
|
81
|
+
* enough: each sweep step is one R2 list (Class A).
|
|
82
|
+
*/
|
|
83
|
+
export const DEFAULT_TIER_ORPHAN_SWEEP_INTERVAL_MS = 7 * 24 * 60 * 60_000;
|
|
84
|
+
/** Keys one sweep step lists (one R2 list page) — and so at most deletes. */
|
|
85
|
+
const SWEEP_STEP_KEYS = 1000;
|
|
76
86
|
/** R2 `delete()` accepts up to 1000 keys per call. */
|
|
77
87
|
const R2_DELETE_BATCH = 1000;
|
|
78
88
|
|
|
89
|
+
/** What `PageTier.maintenance` sweeps besides the queued deletes. */
|
|
90
|
+
export type SweepStep = 'none' | 'step' | 'full';
|
|
91
|
+
|
|
79
92
|
export interface PageTierOptions {
|
|
80
93
|
bucket: R2Bucket;
|
|
81
94
|
/** Key prefix owned by this object (the DO id string); everything under it is ours to delete. */
|
|
@@ -86,6 +99,8 @@ export interface PageTierOptions {
|
|
|
86
99
|
maxSegmentsPerPass?: number;
|
|
87
100
|
/** Cold bytes one statement/transaction may pin while it is retried (default 32 MiB). */
|
|
88
101
|
missPinBytes?: number;
|
|
102
|
+
/** Minimum time between complete orphan sweeps (default 7 days). */
|
|
103
|
+
orphanSweepIntervalMs?: number;
|
|
89
104
|
/** Clock, injectable for tests. */
|
|
90
105
|
now?: () => number;
|
|
91
106
|
}
|
|
@@ -98,6 +113,7 @@ export interface ResolvedPageTierOptions {
|
|
|
98
113
|
evictAfterPeriods: number;
|
|
99
114
|
maxSegmentsPerPass: number;
|
|
100
115
|
missPinBytes: number;
|
|
116
|
+
orphanSweepIntervalMs: number;
|
|
101
117
|
now: () => number;
|
|
102
118
|
}
|
|
103
119
|
|
|
@@ -114,6 +130,8 @@ export function resolvePageTierOptions(
|
|
|
114
130
|
maxSegmentsPerPass:
|
|
115
131
|
options.maxSegmentsPerPass ?? DEFAULT_TIER_MAX_SEGMENTS_PER_PASS,
|
|
116
132
|
missPinBytes: options.missPinBytes ?? DEFAULT_TIER_MISS_PIN_BYTES,
|
|
133
|
+
orphanSweepIntervalMs:
|
|
134
|
+
options.orphanSweepIntervalMs ?? DEFAULT_TIER_ORPHAN_SWEEP_INTERVAL_MS,
|
|
117
135
|
now: options.now ?? (() => Date.now()),
|
|
118
136
|
};
|
|
119
137
|
}
|
|
@@ -152,6 +170,11 @@ interface AccessRow extends Record<string, SqlStorageValue> {
|
|
|
152
170
|
bits: ArrayBuffer;
|
|
153
171
|
}
|
|
154
172
|
|
|
173
|
+
interface SweepRow extends Record<string, SqlStorageValue> {
|
|
174
|
+
last_complete_at: number | null;
|
|
175
|
+
after_key: string | null;
|
|
176
|
+
}
|
|
177
|
+
|
|
155
178
|
export function segmentOf(chunkno: number): number {
|
|
156
179
|
return Math.floor(chunkno / SEGMENT_CHUNKS);
|
|
157
180
|
}
|
|
@@ -197,7 +220,6 @@ export class PageTier {
|
|
|
197
220
|
private readonly segments = new Map<string, Map<number, SegmentEntry>>();
|
|
198
221
|
private schemaReady = false;
|
|
199
222
|
private readonly pendingDeletes = new Set<string>();
|
|
200
|
-
private maintenanceRuns = 0;
|
|
201
223
|
/** Tiny LRU of raw segment bytes for sequential readers (snapshot windows). */
|
|
202
224
|
private readonly segmentCache = new Map<string, Uint8Array>();
|
|
203
225
|
private static readonly SEGMENT_CACHE_ENTRIES = 2;
|
|
@@ -243,6 +265,11 @@ export class PageTier {
|
|
|
243
265
|
this.storage.sql.exec(
|
|
244
266
|
'CREATE TABLE IF NOT EXISTS vfs2_tier_meta (file TEXT PRIMARY KEY, max_gen INTEGER NOT NULL)',
|
|
245
267
|
);
|
|
268
|
+
// Orphan sweep progress (one row): when the last complete sweep ended,
|
|
269
|
+
// and the last key a sweep still in progress has examined.
|
|
270
|
+
this.storage.sql.exec(
|
|
271
|
+
'CREATE TABLE IF NOT EXISTS vfs2_tier_sweep (id INTEGER PRIMARY KEY CHECK (id = 0), last_complete_at INTEGER, after_key TEXT)',
|
|
272
|
+
);
|
|
246
273
|
this.schemaReady = true;
|
|
247
274
|
}
|
|
248
275
|
|
|
@@ -474,17 +501,12 @@ export class PageTier {
|
|
|
474
501
|
// R2 (async)
|
|
475
502
|
// ---------------------------------------------------------------------------
|
|
476
503
|
|
|
477
|
-
/** The segment object bytes (exactly SEGMENT_BYTES, zero-padded), or null when the map has no entry. */
|
|
478
|
-
async getSegment(file: string, segno: number): Promise<Uint8Array | null> {
|
|
479
|
-
const entry = this.segmentsOf(file).get(segno);
|
|
480
|
-
if (entry === undefined) return null;
|
|
481
|
-
return this.getSegmentAt(file, segno, entry);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
504
|
/**
|
|
485
|
-
* The bytes
|
|
486
|
-
* at open time even if the map has
|
|
487
|
-
* only deleted once no snapshot is
|
|
505
|
+
* The bytes (exactly SEGMENT_BYTES, zero-padded) of a specific generation
|
|
506
|
+
* of a segment — what a snapshot took at open time even if the map has
|
|
507
|
+
* moved on since (the old object is only deleted once no snapshot is
|
|
508
|
+
* open). Callers that need the CURRENT generation re-check the map after
|
|
509
|
+
* the await (see `DoVfs.currentSegment`).
|
|
488
510
|
*/
|
|
489
511
|
async getSegmentAt(
|
|
490
512
|
file: string,
|
|
@@ -543,13 +565,16 @@ export class PageTier {
|
|
|
543
565
|
}
|
|
544
566
|
|
|
545
567
|
/**
|
|
546
|
-
* Delete queued keys (objects the map no longer references)
|
|
547
|
-
* `
|
|
548
|
-
*
|
|
549
|
-
*
|
|
568
|
+
* Delete queued keys (objects the map no longer references), then sweep
|
|
569
|
+
* orphans: `'step'` examines one listing page of the prefix (continuing
|
|
570
|
+
* where the previous step stopped), `'full'` the whole prefix now. Only
|
|
571
|
+
* keys shaped like this tier's segment objects are ever swept — the
|
|
572
|
+
* prefix is shared with other users of the bucket (the result store
|
|
573
|
+
* keeps `<prefix>/results/…`). The CALLER guarantees no segment upload
|
|
574
|
+
* is in flight while a sweep runs (see `DoVfs`). Never throws.
|
|
550
575
|
*/
|
|
551
576
|
async maintenance(
|
|
552
|
-
opts: { sweep?:
|
|
577
|
+
opts: { sweep?: SweepStep } = {},
|
|
553
578
|
): Promise<{ deleted: number }> {
|
|
554
579
|
let deleted = 0;
|
|
555
580
|
const keys = Array.from(this.pendingDeletes);
|
|
@@ -562,13 +587,14 @@ export class PageTier {
|
|
|
562
587
|
`[page-tier] delete of ${keys.length} object(s) failed: ${errorMessage(err)}`,
|
|
563
588
|
);
|
|
564
589
|
}
|
|
565
|
-
|
|
566
|
-
if (
|
|
567
|
-
opts.sweep === true ||
|
|
568
|
-
this.maintenanceRuns % ORPHAN_SWEEP_EVERY === 0
|
|
569
|
-
) {
|
|
590
|
+
const sweep = opts.sweep ?? 'none';
|
|
591
|
+
if (sweep !== 'none') {
|
|
570
592
|
try {
|
|
571
|
-
|
|
593
|
+
for (;;) {
|
|
594
|
+
const step = await this.sweepStep(sweep === 'full');
|
|
595
|
+
deleted += step.deleted;
|
|
596
|
+
if (sweep === 'step' || step.complete) break;
|
|
597
|
+
}
|
|
572
598
|
} catch (err) {
|
|
573
599
|
console.warn(`[page-tier] orphan sweep failed: ${errorMessage(err)}`);
|
|
574
600
|
}
|
|
@@ -577,6 +603,89 @@ export class PageTier {
|
|
|
577
603
|
return { deleted };
|
|
578
604
|
}
|
|
579
605
|
|
|
606
|
+
/**
|
|
607
|
+
* Whether a sweep step is due: one is in progress, or the last complete
|
|
608
|
+
* sweep is older than `orphanSweepIntervalMs` (or there never was one).
|
|
609
|
+
*/
|
|
610
|
+
sweepDue(now = this.options.now()): boolean {
|
|
611
|
+
const state = this.sweepState();
|
|
612
|
+
if (state.after_key !== null) return true;
|
|
613
|
+
return (
|
|
614
|
+
state.last_complete_at === null ||
|
|
615
|
+
now - state.last_complete_at >= this.options.orphanSweepIntervalMs
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private sweepState(): SweepRow {
|
|
620
|
+
this.ensureSchema();
|
|
621
|
+
return (
|
|
622
|
+
this.storage.sql
|
|
623
|
+
.exec<SweepRow>(
|
|
624
|
+
'SELECT last_complete_at, after_key FROM vfs2_tier_sweep WHERE id = 0',
|
|
625
|
+
)
|
|
626
|
+
.toArray()[0] ?? { last_complete_at: null, after_key: null }
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* One listing page of the prefix: delete the segment-shaped keys the map
|
|
632
|
+
* does not reference and persist how far the sweep got. `restart` starts
|
|
633
|
+
* from the beginning of the prefix (a full sweep).
|
|
634
|
+
*/
|
|
635
|
+
private async sweepStep(
|
|
636
|
+
restart: boolean,
|
|
637
|
+
): Promise<{ deleted: number; complete: boolean }> {
|
|
638
|
+
const afterKey = restart ? null : this.sweepState().after_key;
|
|
639
|
+
const page = await this.options.bucket.list({
|
|
640
|
+
prefix: `${this.options.prefix}/`,
|
|
641
|
+
limit: SWEEP_STEP_KEYS,
|
|
642
|
+
...(afterKey !== null && { startAfter: afterKey }),
|
|
643
|
+
});
|
|
644
|
+
this.stats.r2Lists++;
|
|
645
|
+
// The map is read after the listing: a segment committed meanwhile is
|
|
646
|
+
// referenced by then (and uploads in flight are excluded by the caller).
|
|
647
|
+
const referenced = this.referencedKeys();
|
|
648
|
+
const orphans = page.objects
|
|
649
|
+
.map((object) => object.key)
|
|
650
|
+
.filter((key) => this.isSegmentKey(key) && !referenced.has(key));
|
|
651
|
+
const deleted = await this.deleteKeys(orphans);
|
|
652
|
+
const lastKey = page.objects.at(-1)?.key;
|
|
653
|
+
const complete = !page.truncated || lastKey === undefined;
|
|
654
|
+
if (complete) {
|
|
655
|
+
this.storage.sql.exec(
|
|
656
|
+
'INSERT INTO vfs2_tier_sweep (id, last_complete_at, after_key) VALUES (0, ?, NULL) ON CONFLICT(id) DO UPDATE SET last_complete_at = excluded.last_complete_at, after_key = NULL',
|
|
657
|
+
this.options.now(),
|
|
658
|
+
);
|
|
659
|
+
} else {
|
|
660
|
+
this.storage.sql.exec(
|
|
661
|
+
'INSERT INTO vfs2_tier_sweep (id, last_complete_at, after_key) VALUES (0, NULL, ?) ON CONFLICT(id) DO UPDATE SET after_key = excluded.after_key',
|
|
662
|
+
lastKey,
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
return { deleted, complete };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** Keys of every segment object the map references, across files. */
|
|
669
|
+
private referencedKeys(): Set<string> {
|
|
670
|
+
this.ensureSchema();
|
|
671
|
+
const referenced = new Set<string>();
|
|
672
|
+
const rows = this.storage.sql
|
|
673
|
+
.exec<
|
|
674
|
+
SegmentRow & { file: string }
|
|
675
|
+
>('SELECT file, segno, gen, mask FROM vfs2_tier_segments')
|
|
676
|
+
.toArray();
|
|
677
|
+
for (const row of rows)
|
|
678
|
+
referenced.add(this.key(row.file, row.segno, row.gen));
|
|
679
|
+
return referenced;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/** `<prefix>/<file>/<segno>.<gen>` — the only keys a sweep may delete. */
|
|
683
|
+
private isSegmentKey(key: string): boolean {
|
|
684
|
+
const head = `${this.options.prefix}/`;
|
|
685
|
+
if (!key.startsWith(head)) return false;
|
|
686
|
+
return /^.+\/\d+\.\d+$/.test(key.slice(head.length));
|
|
687
|
+
}
|
|
688
|
+
|
|
580
689
|
/** Every object under the prefix (keys). */
|
|
581
690
|
async listKeys(): Promise<string[]> {
|
|
582
691
|
const keys: string[] = [];
|
|
@@ -593,22 +702,6 @@ export class PageTier {
|
|
|
593
702
|
return keys;
|
|
594
703
|
}
|
|
595
704
|
|
|
596
|
-
private async sweepOrphans(): Promise<number> {
|
|
597
|
-
const referenced = new Set<string>();
|
|
598
|
-
this.ensureSchema();
|
|
599
|
-
const rows = this.storage.sql
|
|
600
|
-
.exec<
|
|
601
|
-
SegmentRow & { file: string }
|
|
602
|
-
>('SELECT file, segno, gen, mask FROM vfs2_tier_segments')
|
|
603
|
-
.toArray();
|
|
604
|
-
for (const row of rows)
|
|
605
|
-
referenced.add(this.key(row.file, row.segno, row.gen));
|
|
606
|
-
const orphans = (await this.listKeys()).filter(
|
|
607
|
-
(key) => !referenced.has(key),
|
|
608
|
-
);
|
|
609
|
-
return this.deleteKeys(orphans);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
705
|
/** Delete everything under the prefix (the object's working copy is being wiped). */
|
|
613
706
|
async deleteAll(): Promise<number> {
|
|
614
707
|
this.pendingDeletes.clear();
|
|
@@ -12,6 +12,20 @@
|
|
|
12
12
|
* and LangGraph `Send` objects. `stringify` is fast-safe-stringify's
|
|
13
13
|
* circular-safe variant, typed.
|
|
14
14
|
*/
|
|
15
|
+
import { TURN_TIME_NOTE_KWARG } from '../core/turn-time-note';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `additional_kwargs` key of the tool calls a mid-turn summary carries for
|
|
19
|
+
* the turn it condensed (core/middlewares/turn-boundary.ts).
|
|
20
|
+
*/
|
|
21
|
+
export const TURN_CARRY_KWARG = 'turn_carry';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* JSON characters of a carry kept on a stored message. Each carried result
|
|
25
|
+
* is already cut short by the summarizer; the cap bounds how many calls a
|
|
26
|
+
* long turn can pile onto one row. The newest calls are kept.
|
|
27
|
+
*/
|
|
28
|
+
export const TURN_CARRY_MAX_CHARS = 64 * 1024;
|
|
15
29
|
|
|
16
30
|
type Replacer = (this: unknown, key: string, value: unknown) => unknown;
|
|
17
31
|
|
|
@@ -243,6 +257,10 @@ export interface CleanAdditionalKwargs {
|
|
|
243
257
|
oracleName: string;
|
|
244
258
|
/** Provenance marker set by LangChain middlewares (e.g. `"summarization"`). */
|
|
245
259
|
lc_source?: string;
|
|
260
|
+
/** The exact time note the turn's content starts with (see core/turn-time-note.ts). */
|
|
261
|
+
turn_time_note?: string;
|
|
262
|
+
/** Tool calls of the turn a mid-turn summary condensed (newest kept, capped). */
|
|
263
|
+
turn_carry?: unknown[];
|
|
246
264
|
reasoning?: string;
|
|
247
265
|
reasoningDetails?: ReasoningDetail[];
|
|
248
266
|
attachment?: AttachmentMeta;
|
|
@@ -302,6 +320,21 @@ function extractReasoning(additionalKwargs: Record<string, unknown>): {
|
|
|
302
320
|
: { reasoning };
|
|
303
321
|
}
|
|
304
322
|
|
|
323
|
+
/** The newest entries of a carry list whose JSON fits `TURN_CARRY_MAX_CHARS`. */
|
|
324
|
+
function cappedTurnCarry(value: unknown): unknown[] | undefined {
|
|
325
|
+
if (!Array.isArray(value)) return undefined;
|
|
326
|
+
const kept: unknown[] = [];
|
|
327
|
+
let chars = 2; // the brackets
|
|
328
|
+
for (let i = value.length - 1; i >= 0; i--) {
|
|
329
|
+
const entry: unknown = value[i];
|
|
330
|
+
const size = (JSON.stringify(entry) ?? '').length + (kept.length ? 1 : 0);
|
|
331
|
+
if (chars + size > TURN_CARRY_MAX_CHARS) break;
|
|
332
|
+
chars += size;
|
|
333
|
+
kept.unshift(entry);
|
|
334
|
+
}
|
|
335
|
+
return kept;
|
|
336
|
+
}
|
|
337
|
+
|
|
305
338
|
/**
|
|
306
339
|
* Reduce a message's `additional_kwargs` to the persisted allowlist. An
|
|
307
340
|
* existing `timestamp` is preserved: every checkpoint `put` rewrites every
|
|
@@ -324,6 +357,13 @@ export function cleanAdditionalKwargs(
|
|
|
324
357
|
};
|
|
325
358
|
if (typeof additionalKwargs.lc_source === 'string')
|
|
326
359
|
cleaned.lc_source = additionalKwargs.lc_source;
|
|
360
|
+
// Kept so readers can take the note off the stored content again.
|
|
361
|
+
const turnTimeNote = additionalKwargs[TURN_TIME_NOTE_KWARG];
|
|
362
|
+
if (typeof turnTimeNote === 'string')
|
|
363
|
+
cleaned[TURN_TIME_NOTE_KWARG] = turnTimeNote;
|
|
364
|
+
// Kept so a run resumed after the summary still knows what this turn ran.
|
|
365
|
+
const carry = cappedTurnCarry(additionalKwargs[TURN_CARRY_KWARG]);
|
|
366
|
+
if (carry !== undefined) cleaned[TURN_CARRY_KWARG] = carry;
|
|
327
367
|
if (isAttachmentMeta(additionalKwargs.attachment))
|
|
328
368
|
cleaned.attachment = additionalKwargs.attachment;
|
|
329
369
|
if (
|
|
@@ -201,15 +201,23 @@ export class SessionsStore {
|
|
|
201
201
|
params.push(excludeIdPrefix.length, excludeIdPrefix);
|
|
202
202
|
}
|
|
203
203
|
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
|
|
204
|
-
params.push(limit, offset);
|
|
205
204
|
const rows = await this.db.exec<SessionRowWithTotal>(
|
|
206
205
|
`SELECT ${SESSION_COLUMNS}, COUNT(*) OVER() AS total
|
|
207
206
|
FROM sessions ${where}
|
|
208
207
|
ORDER BY last_updated_at DESC, rowid DESC
|
|
209
208
|
LIMIT ? OFFSET ?`,
|
|
209
|
+
[...params, limit, offset],
|
|
210
|
+
);
|
|
211
|
+
const first = rows[0];
|
|
212
|
+
if (first !== undefined)
|
|
213
|
+
return { sessions: rows.map(rowToSession), total: first.total };
|
|
214
|
+
// A page past the last row carries no window count: count separately.
|
|
215
|
+
if (offset <= 0) return { sessions: [], total: 0 };
|
|
216
|
+
const count = await this.db.get<{ n: number }>(
|
|
217
|
+
`SELECT COUNT(*) AS n FROM sessions ${where}`,
|
|
210
218
|
params,
|
|
211
219
|
);
|
|
212
|
-
return { sessions:
|
|
220
|
+
return { sessions: [], total: Number(count?.n ?? 0) };
|
|
213
221
|
}
|
|
214
222
|
|
|
215
223
|
async getSession(sessionId: string): Promise<SessionRecord | undefined> {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* - `channel_values.messages` is stripped from the stored checkpoint and each
|
|
20
20
|
* message is upserted into `messages` (keyed by message id, so the table is
|
|
21
21
|
* the full transcript: rows survive checkpoint pruning and summarization).
|
|
22
|
+
* An unchanged message keeps its row in place (see `writeMessageRows`).
|
|
22
23
|
* - `put` prunes a thread to `maxCheckpointsPerThread` newest checkpoints (and
|
|
23
24
|
* their writes) once it exceeds the cap by `PRUNE_SLACK`.
|
|
24
25
|
* - `pending_sends` migration for pre-v4 checkpoints.
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
type CheckpointMetadata,
|
|
40
41
|
type CheckpointTuple,
|
|
41
42
|
copyCheckpoint,
|
|
43
|
+
emptyCheckpoint,
|
|
42
44
|
maxChannelVersion,
|
|
43
45
|
type PendingWrite,
|
|
44
46
|
type SerializerProtocol,
|
|
@@ -78,6 +80,9 @@ export const PRUNE_SLACK = 5;
|
|
|
78
80
|
|
|
79
81
|
const DEFAULT_ORACLE_NAME = 'IXO Oracle';
|
|
80
82
|
|
|
83
|
+
/** Position of `message_id` in the parameter rows `toMessageRow` returns. */
|
|
84
|
+
const MESSAGE_ID_COLUMN = 3;
|
|
85
|
+
|
|
81
86
|
type CheckpointRow = {
|
|
82
87
|
thread_id: string;
|
|
83
88
|
checkpoint_ns: string;
|
|
@@ -137,7 +142,12 @@ interface CheckpointWithMessages extends Checkpoint {
|
|
|
137
142
|
* Compile-time guard: the metadata keys `list()` accepts as filters must stay
|
|
138
143
|
* in sync with `CheckpointMetadata`. If this stops compiling, update the list.
|
|
139
144
|
*/
|
|
140
|
-
const checkpointMetadataKeys = [
|
|
145
|
+
const checkpointMetadataKeys = [
|
|
146
|
+
'source',
|
|
147
|
+
'step',
|
|
148
|
+
'parents',
|
|
149
|
+
'counters_since_delta_snapshot',
|
|
150
|
+
] as const;
|
|
141
151
|
type MetadataKey = keyof CheckpointMetadata;
|
|
142
152
|
type CheckKeys<K extends readonly MetadataKey[]> = [MetadataKey] extends [
|
|
143
153
|
K[number],
|
|
@@ -176,7 +186,23 @@ const migration001: Migration = {
|
|
|
176
186
|
},
|
|
177
187
|
};
|
|
178
188
|
|
|
179
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Migration 002: drop `idx_messages_thread_id` and `idx_messages_checkpoint_id`.
|
|
191
|
+
* No query uses them — every `messages` query filters on `thread_id` (served
|
|
192
|
+
* by `idx_messages_thread_created` / `idx_messages_lookup`, whose leading
|
|
193
|
+
* column it is) or looks a row up by `message_id` — yet every message write
|
|
194
|
+
* updated both b-trees.
|
|
195
|
+
*/
|
|
196
|
+
const migration002: Migration = {
|
|
197
|
+
version: 2,
|
|
198
|
+
name: 'drop_unused_message_indexes',
|
|
199
|
+
up: async (db) => {
|
|
200
|
+
await db.run('DROP INDEX IF EXISTS idx_messages_thread_id');
|
|
201
|
+
await db.run('DROP INDEX IF EXISTS idx_messages_checkpoint_id');
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const MIGRATIONS: readonly Migration[] = [migration001, migration002];
|
|
180
206
|
|
|
181
207
|
// `writes.value` blobs may be gzipped, so they travel as hex() — a
|
|
182
208
|
// CAST(... AS TEXT) of compressed bytes would mangle them irreversibly.
|
|
@@ -228,6 +254,17 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
228
254
|
protected readonly maxCheckpointsPerThread: number;
|
|
229
255
|
protected readonly oracleName: string;
|
|
230
256
|
private setupPromise: Promise<void> | undefined;
|
|
257
|
+
/**
|
|
258
|
+
* The stored blob of each message object last encoded, with the JSON it
|
|
259
|
+
* was compressed from. LangGraph hands the same message objects to every
|
|
260
|
+
* `put` of a run; one whose JSON is unchanged reuses its blob instead of
|
|
261
|
+
* running a gzip pipeline again. The JSON itself is always recomputed —
|
|
262
|
+
* it is what proves the object did not change.
|
|
263
|
+
*/
|
|
264
|
+
private readonly encoded = new WeakMap<
|
|
265
|
+
BaseMessage,
|
|
266
|
+
{ json: string; blob: Uint8Array }
|
|
267
|
+
>();
|
|
231
268
|
|
|
232
269
|
constructor(
|
|
233
270
|
db: DoSqliteDatabase,
|
|
@@ -298,12 +335,6 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
298
335
|
PRIMARY KEY (message_id)
|
|
299
336
|
)`);
|
|
300
337
|
await this.runMigrations();
|
|
301
|
-
await db.run(
|
|
302
|
-
`CREATE INDEX IF NOT EXISTS idx_messages_thread_id ON messages(thread_id)`,
|
|
303
|
-
);
|
|
304
|
-
await db.run(
|
|
305
|
-
`CREATE INDEX IF NOT EXISTS idx_messages_checkpoint_id ON messages(checkpoint_id)`,
|
|
306
|
-
);
|
|
307
338
|
await db.run(
|
|
308
339
|
`CREATE INDEX IF NOT EXISTS idx_messages_lookup ON messages(thread_id, checkpoint_ns, checkpoint_id)`,
|
|
309
340
|
);
|
|
@@ -637,8 +668,10 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
637
668
|
checkpointNs: string,
|
|
638
669
|
checkpointId: string,
|
|
639
670
|
): Promise<BaseMessage[]> {
|
|
671
|
+
// Rowid order is the order of the checkpoint's message array (see
|
|
672
|
+
// `writeMessageRows`); the lookup index already yields it, no sort.
|
|
640
673
|
const rows = await this.db.exec<MessageRow>(
|
|
641
|
-
'SELECT message FROM messages WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?',
|
|
674
|
+
'SELECT message FROM messages WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY rowid',
|
|
642
675
|
[threadId, checkpointNs, checkpointId],
|
|
643
676
|
);
|
|
644
677
|
return Promise.all(rows.map((row) => this.loadMessage(row.message)));
|
|
@@ -696,6 +729,39 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
696
729
|
}
|
|
697
730
|
}
|
|
698
731
|
|
|
732
|
+
/** Append a deterministic turn without running or replacing pending graph work. */
|
|
733
|
+
async appendTurnMessages(
|
|
734
|
+
threadId: string,
|
|
735
|
+
messages: BaseMessage[],
|
|
736
|
+
): Promise<void> {
|
|
737
|
+
const config = { configurable: { thread_id: threadId } };
|
|
738
|
+
const existing = await this.getTupleWithoutMessages(config);
|
|
739
|
+
if (!existing) {
|
|
740
|
+
const checkpoint = emptyCheckpoint();
|
|
741
|
+
checkpoint.channel_values.messages = messages;
|
|
742
|
+
await this.put(config, checkpoint, {
|
|
743
|
+
source: 'update',
|
|
744
|
+
step: -1,
|
|
745
|
+
parents: {},
|
|
746
|
+
});
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const rows = await Promise.all(
|
|
750
|
+
messages.map((message) =>
|
|
751
|
+
this.toMessageRow(message, threadId, '', existing.checkpoint.id),
|
|
752
|
+
),
|
|
753
|
+
);
|
|
754
|
+
await this.db.transaction(async () => {
|
|
755
|
+
for (const row of rows) {
|
|
756
|
+
await this.db.run(
|
|
757
|
+
`INSERT OR IGNORE INTO messages (thread_id, checkpoint_ns, checkpoint_id, message_id, message_type, message_content, message, created_at)
|
|
758
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
759
|
+
row,
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
699
765
|
async put(
|
|
700
766
|
config: RunnableConfig,
|
|
701
767
|
checkpointIn: Checkpoint,
|
|
@@ -748,13 +814,7 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
748
814
|
serializedMetadata,
|
|
749
815
|
],
|
|
750
816
|
);
|
|
751
|
-
|
|
752
|
-
await this.db.run(
|
|
753
|
-
`INSERT OR REPLACE INTO messages (thread_id, checkpoint_ns, checkpoint_id, message_id, message_type, message_content, message, created_at)
|
|
754
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
755
|
-
row,
|
|
756
|
-
);
|
|
757
|
-
}
|
|
817
|
+
await this.writeMessageRows(messageRows);
|
|
758
818
|
});
|
|
759
819
|
|
|
760
820
|
await this.pruneThread(threadId, checkpointNs);
|
|
@@ -768,6 +828,60 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
768
828
|
};
|
|
769
829
|
}
|
|
770
830
|
|
|
831
|
+
/**
|
|
832
|
+
* Upsert a checkpoint's message rows (in array order). A checkpoint's
|
|
833
|
+
* messages are read back in rowid order (`loadCheckpointMessages`), so
|
|
834
|
+
* rowids must increase along the array. A message whose row exists keeps
|
|
835
|
+
* it — updated in place, which leaves the pages whose bytes did not
|
|
836
|
+
* change untouched — as long as that holds; from the first message that
|
|
837
|
+
* is new or would break the order (a summary placed before the history
|
|
838
|
+
* it kept) on, every row is written with `INSERT OR REPLACE`, which
|
|
839
|
+
* gives it a new rowid after all others, exactly as every row used to be.
|
|
840
|
+
* Inside the caller's transaction.
|
|
841
|
+
*/
|
|
842
|
+
private async writeMessageRows(rows: SqlParam[][]): Promise<void> {
|
|
843
|
+
if (rows.length === 0) return;
|
|
844
|
+
const ids = rows.map((row) => row[MESSAGE_ID_COLUMN]);
|
|
845
|
+
const existing = new Map<string, number>();
|
|
846
|
+
for (const row of await this.db.exec<{
|
|
847
|
+
message_id: string;
|
|
848
|
+
rowid: number;
|
|
849
|
+
}>(
|
|
850
|
+
'SELECT m.message_id, m.rowid FROM json_each(?) AS j JOIN messages AS m ON m.message_id = j.value',
|
|
851
|
+
[JSON.stringify(ids)],
|
|
852
|
+
))
|
|
853
|
+
existing.set(row.message_id, row.rowid);
|
|
854
|
+
let lastRowid = -1;
|
|
855
|
+
let appending = false;
|
|
856
|
+
for (const row of rows) {
|
|
857
|
+
const id = row[MESSAGE_ID_COLUMN];
|
|
858
|
+
const rowid = typeof id === 'string' ? existing.get(id) : undefined;
|
|
859
|
+
if (!appending && rowid !== undefined && rowid > lastRowid) {
|
|
860
|
+
await this.db.run(
|
|
861
|
+
`INSERT INTO messages (thread_id, checkpoint_ns, checkpoint_id, message_id, message_type, message_content, message, created_at)
|
|
862
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
863
|
+
ON CONFLICT(message_id) DO UPDATE SET
|
|
864
|
+
thread_id = excluded.thread_id,
|
|
865
|
+
checkpoint_ns = excluded.checkpoint_ns,
|
|
866
|
+
checkpoint_id = excluded.checkpoint_id,
|
|
867
|
+
message_type = excluded.message_type,
|
|
868
|
+
message_content = excluded.message_content,
|
|
869
|
+
message = excluded.message,
|
|
870
|
+
created_at = excluded.created_at`,
|
|
871
|
+
row,
|
|
872
|
+
);
|
|
873
|
+
lastRowid = rowid;
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
appending = true;
|
|
877
|
+
await this.db.run(
|
|
878
|
+
`INSERT OR REPLACE INTO messages (thread_id, checkpoint_ns, checkpoint_id, message_id, message_type, message_content, message, created_at)
|
|
879
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
880
|
+
row,
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
771
885
|
/** Normalize a message's `additional_kwargs` and serialize it exactly as the Node runtime does. */
|
|
772
886
|
private async toMessageRow(
|
|
773
887
|
message: BaseMessage,
|
|
@@ -798,9 +912,15 @@ export class SqliteSaver extends BaseCheckpointSaver {
|
|
|
798
912
|
}
|
|
799
913
|
message.additional_kwargs = merged;
|
|
800
914
|
|
|
801
|
-
const
|
|
802
|
-
|
|
803
|
-
|
|
915
|
+
const json = stringify(message, (_key, value) => _default(value));
|
|
916
|
+
const cached = this.encoded.get(message);
|
|
917
|
+
let serialized: Uint8Array;
|
|
918
|
+
if (cached !== undefined && cached.json === json) {
|
|
919
|
+
serialized = cached.blob;
|
|
920
|
+
} else {
|
|
921
|
+
serialized = await compressForStorage(json);
|
|
922
|
+
this.encoded.set(message, { json, blob: serialized });
|
|
923
|
+
}
|
|
804
924
|
const messageId =
|
|
805
925
|
message.id ??
|
|
806
926
|
(typeof message.lc_kwargs.id === 'string'
|