or3-provider-sqlite 0.0.12 → 0.0.14

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 CHANGED
@@ -227,6 +227,13 @@ bun run type-check # TypeScript validation
227
227
  bun run build # build for distribution
228
228
  ```
229
229
 
230
+ The normal test suite runs in a provider-only checkout using hash-verified,
231
+ byte-for-byte host runtime fixtures. `bun run test:host-integration` reruns the
232
+ same background-job, canonical-history and registration cases against a prepared
233
+ sibling `../or3-chat` at the exact reviewed source tree. It fails on missing or
234
+ mismatched source rather than falling back to fixtures. See the
235
+ [fixture provenance and prerequisites](test/fixtures/host/README.md).
236
+
230
237
  ## Compatibility
231
238
 
232
239
  - Works with multiple auth providers (`basic-auth`, `clerk`, or custom)
@@ -273,3 +280,27 @@ prints its selected path; restart it after provider edits. Missing repositories
273
280
  or failed builds fall back to installed packages with a warning.
274
281
  `OR3_LOCAL_PROVIDERS=false` disables local selection. Production builds use the
275
282
  installed package, so local development does not publish these changes.
283
+
284
+ ### Canonical compaction history reader (unreleased)
285
+
286
+ `canonicalChatHistory: 'v1'` and `readChatHistory` read current `s_threads`/`s_messages`, with explicit workspace membership (owner/editor/viewer), bounded by-ID queries and canonical `(index, order_key, id)` keyset pages. Migration 022 adds transactional per-thread revision triggers. Migration 023 matches the index to legacy missing-order-key normalization and preserves the existing orphan-row cleanup lifecycle; revision bookkeeping does not add a foreign key to otherwise independent sync rows. Current-chat tool result writes do not invalidate ancestor search cursors. The reader never consults retained sync logs. D1 omits the capability.
287
+
288
+ The additive local qualification command is `bun run build` followed by `bunx vitest run --config vitest.canonical-history.config.ts`. It checks committed current host contract bytes without replacing the older pinned fixture. The current-host lane passes 119 cases across three files, including the real gateway owner and source-built artifact reads after change-log deletion, legacy tie paging in both directions, bounded queries, revision changes, cancellation, revoked membership and an index plan without a temporary sort. These checks do not qualify a deployed rollout, second-client/background reconciliation or released pins. This compaction capability is not yet included in a published package.
289
+
290
+ The canonical-history qualification lane defaults to a sibling `../or3-chat` checkout. For an isolated host worktree, set `OR3_CANONICAL_HOST_ROOT=/absolute/path/to/or3-chat` before the command. Install the host dependencies too; the two-client fixture resolves its IndexedDB test dependency from that checkout. Its contract-byte checks fail closed on uncommitted changes; this does not replace the pinned host fixture or certify deployed/released provider behavior.
291
+
292
+ ## Workspace Files and Trash admission
293
+
294
+ Native synchronous adapters advertise workspace-item capability v1. Canonical
295
+ posts/projects with catalog, logical Trash, or file-membership semantics require
296
+ a current writer even when the incoming update omits those fields. Admission is
297
+ checked inside the same write transaction, before versions or changes are
298
+ allocated. Canonical retained post references include logically trashed catalog
299
+ entries/documents and checkpoints. D1 does not advertise this transactional
300
+ capability.
301
+
302
+ This source change is a provider publication dependency for the host feature.
303
+ Rebuild the provider before testing a file-linked host; follow the existing OR3
304
+ release/update workflow separately. Local qualification does not authorize
305
+ publication or a production migration. Files cloud exposure remains gated until
306
+ the supported profile is qualified.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "or3-provider-sqlite",
3
3
  "configKey": "or3-provider-sqlite",
4
- "version": "0.0.12",
4
+ "version": "0.0.14",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -1,4 +1,5 @@
1
1
  import { getJobConfig } from "~~/server/utils/background-jobs/store";
2
+ import { readRequestUsage } from "~~/shared/chat/compaction";
2
3
  import { randomUUID } from "node:crypto";
3
4
  import { d1All, d1Run } from "../db/d1.js";
4
5
  import { getRawDb, isD1Driver } from "../db/kysely.js";
@@ -9,6 +10,13 @@ function parseJson(value) {
9
10
  function json(value) {
10
11
  return value === void 0 ? null : JSON.stringify(value);
11
12
  }
13
+ function readStoredUsage(value) {
14
+ try {
15
+ return readRequestUsage(parseJson(value));
16
+ } catch {
17
+ return void 0;
18
+ }
19
+ }
12
20
  function toJob(row) {
13
21
  return {
14
22
  id: row.id,
@@ -29,6 +37,7 @@ function toJob(row) {
29
37
  completedAt: row.completed_at ?? void 0,
30
38
  error: row.error ?? void 0,
31
39
  tool_calls: parseJson(row.tool_calls_json),
40
+ usage: readStoredUsage(row.usage_json),
32
41
  workflow_state: parseJson(
33
42
  row.workflow_state_json
34
43
  ),
@@ -326,12 +335,19 @@ export class SqliteBackgroundJobProvider {
326
335
  sets.push("workflow_state_json = ?");
327
336
  values.push(json(update.workflow_state));
328
337
  }
338
+ const usage = readRequestUsage(update.usage);
339
+ if (usage) {
340
+ sets.push("usage_json = ?");
341
+ values.push(json(usage));
342
+ }
329
343
  values.push(jobId);
330
344
  let where = "id = ? AND status = 'streaming'";
331
345
  if (update.leaseOwner) {
332
346
  where += " AND lease_owner = ? AND lease_expires_at > ?";
333
347
  values.push(update.leaseOwner);
334
348
  values.push(Date.now());
349
+ } else {
350
+ where += " AND lease_owner IS NULL";
335
351
  }
336
352
  const changes = await run(
337
353
  `UPDATE background_jobs SET ${sets.join(", ")} WHERE ${where}`,
@@ -353,8 +369,10 @@ export class SqliteBackgroundJobProvider {
353
369
  values.push(jobId);
354
370
  let where = "id = ? AND status = 'streaming'";
355
371
  if (leaseOwner) {
356
- where += " AND lease_owner = ?";
357
- values.push(leaseOwner);
372
+ where += " AND lease_owner = ? AND lease_expires_at > ?";
373
+ values.push(leaseOwner, now);
374
+ } else {
375
+ where += " AND lease_owner IS NULL";
358
376
  }
359
377
  const changes = await run(
360
378
  `UPDATE background_jobs SET status = ?, completed_at = ?, last_activity_at = ?,
@@ -403,6 +421,11 @@ export class SqliteBackgroundJobProvider {
403
421
  reasoning = CASE WHEN attempts > 0
404
422
  THEN COALESCE(json_extract(execution_json, '$.reasoningBase'), '')
405
423
  ELSE reasoning END,
424
+ usage_json = CASE WHEN attempts > 0
425
+ THEN CASE WHEN json_type(execution_json, '$.normalizedToolState.requestUsage') = 'object'
426
+ THEN json_extract(execution_json, '$.normalizedToolState.requestUsage')
427
+ ELSE NULL END
428
+ ELSE usage_json END,
406
429
  chunks_received = CASE WHEN attempts > 0 THEN 0 ELSE chunks_received END,
407
430
  attempts = attempts + 1
408
431
  WHERE id = ? AND status = 'streaming' AND execution_json IS NOT NULL
@@ -430,6 +453,11 @@ export class SqliteBackgroundJobProvider {
430
453
  reasoning = CASE WHEN attempts > 0
431
454
  THEN COALESCE(json_extract(execution_json, '$.reasoningBase'), '')
432
455
  ELSE reasoning END,
456
+ usage_json = CASE WHEN attempts > 0
457
+ THEN CASE WHEN json_type(execution_json, '$.normalizedToolState.requestUsage') = 'object'
458
+ THEN json_extract(execution_json, '$.normalizedToolState.requestUsage')
459
+ ELSE NULL END
460
+ ELSE usage_json END,
433
461
  chunks_received = CASE WHEN attempts > 0 THEN 0 ELSE chunks_received END,
434
462
  attempts = attempts + 1
435
463
  WHERE id = (
@@ -462,11 +490,13 @@ export class SqliteBackgroundJobProvider {
462
490
  async updateJobExecution(jobId, execution, leaseOwner) {
463
491
  return await run(
464
492
  `UPDATE background_jobs SET execution_json = ?, last_activity_at = ?
465
- WHERE id = ? AND status = 'streaming' AND lease_owner = ?`,
493
+ WHERE id = ? AND status = 'streaming' AND lease_owner = ?
494
+ AND lease_expires_at > ?`,
466
495
  json(execution),
467
496
  Date.now(),
468
497
  jobId,
469
- leaseOwner
498
+ leaseOwner,
499
+ Date.now()
470
500
  ) > 0;
471
501
  }
472
502
  async claimClientToolCall(jobId, userId, callId, claimToken, claimExpiresAt) {
@@ -521,6 +551,7 @@ export class SqliteBackgroundJobProvider {
521
551
  snapshot.content,
522
552
  snapshot.reasoning,
523
553
  json(snapshot.toolCalls),
554
+ json(readRequestUsage(snapshot.usage)),
524
555
  snapshot.error ?? null,
525
556
  snapshot.completedAt,
526
557
  snapshot.completedAt
@@ -530,10 +561,12 @@ export class SqliteBackgroundJobProvider {
530
561
  if (leaseOwner) {
531
562
  where += " AND lease_owner = ? AND lease_expires_at > ?";
532
563
  values.push(leaseOwner, Date.now());
564
+ } else {
565
+ where += " AND lease_owner IS NULL";
533
566
  }
534
567
  return await run(
535
568
  `UPDATE background_jobs SET status = ?, content = ?, reasoning = ?,
536
- tool_calls_json = ?, error = ?, completed_at = ?,
569
+ tool_calls_json = ?, usage_json = COALESCE(?, usage_json), error = ?, completed_at = ?,
537
570
  history_phase = 'finalization_pending',
538
571
  last_activity_at = ?, lease_owner = NULL, lease_expires_at = NULL
539
572
  WHERE ${where}`,
@@ -19,6 +19,9 @@ import * as m017 from "./migrations/017_background_jobs.js";
19
19
  import * as m018 from "./migrations/018_background_admission_cancels.js";
20
20
  import * as m019 from "./migrations/019_background_generation_history.js";
21
21
  import * as m020 from "./migrations/020_background_generation_receipts.js";
22
+ import * as m021 from "./migrations/021_background_request_usage.js";
23
+ import * as m022 from "./migrations/022_canonical_chat_reader.js";
24
+ import * as m023 from "./migrations/023_legacy_chat_seek_index.js";
22
25
  const REQUIRED_SCHEMA_TABLES = [
23
26
  "users",
24
27
  "auth_accounts",
@@ -68,7 +71,10 @@ const migrations = {
68
71
  "017_background_jobs": m017,
69
72
  "018_background_admission_cancels": m018,
70
73
  "019_background_generation_history": m019,
71
- "020_background_generation_receipts": m020
74
+ "020_background_generation_receipts": m020,
75
+ "021_background_request_usage": m021,
76
+ "022_canonical_chat_reader": m022,
77
+ "023_legacy_chat_seek_index": m023
72
78
  };
73
79
  class StaticMigrationProvider {
74
80
  async getMigrations() {
@@ -0,0 +1,4 @@
1
+ import { type Kysely } from 'kysely';
2
+ /** Optional last measured request; existing jobs remain unmeasured. */
3
+ export declare function up(db: Kysely<unknown>): Promise<void>;
4
+ export declare function down(db: Kysely<unknown>): Promise<void>;
@@ -0,0 +1,7 @@
1
+ import { sql } from "kysely";
2
+ export async function up(db) {
3
+ await sql`ALTER TABLE background_jobs ADD COLUMN usage_json TEXT`.execute(db);
4
+ }
5
+ export async function down(db) {
6
+ await sql`ALTER TABLE background_jobs DROP COLUMN usage_json`.execute(db);
7
+ }
@@ -0,0 +1,4 @@
1
+ import { type Kysely } from 'kysely';
2
+ /** Workspace-scoped canonical message keysets, independent of retained logs. */
3
+ export declare function up(db: Kysely<unknown>): Promise<void>;
4
+ export declare function down(db: Kysely<unknown>): Promise<void>;
@@ -0,0 +1,26 @@
1
+ import { sql } from "kysely";
2
+ export async function up(db) {
3
+ await sql`CREATE TABLE IF NOT EXISTS chat_history_revisions (
4
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
5
+ thread_id TEXT NOT NULL, value INTEGER NOT NULL DEFAULT 0,
6
+ PRIMARY KEY (workspace_id, thread_id))`.execute(db);
7
+ await sql`CREATE INDEX IF NOT EXISTS s_messages_history_order ON s_messages
8
+ (workspace_id, json_extract(data_json, '$.thread_id'), json_extract(data_json, '$.index'), json_extract(data_json, '$.order_key'), id)`.execute(db);
9
+ for (const table of ["s_threads", "s_messages"]) for (const event of ["INSERT", "UPDATE", "DELETE"]) {
10
+ const rowNames = event === "UPDATE" ? ["OLD", "NEW"] : [event === "DELETE" ? "OLD" : "NEW"];
11
+ const statements = rowNames.map((row) => {
12
+ const thread = table === "s_threads" ? `${row}.id` : `json_extract(${row}.data_json, '$.thread_id')`;
13
+ return `INSERT INTO chat_history_revisions (workspace_id, thread_id, value)
14
+ SELECT ${row}.workspace_id, ${thread}, 1 WHERE ${thread} IS NOT NULL
15
+ ON CONFLICT (workspace_id, thread_id) DO UPDATE SET value = chat_history_revisions.value + 1;`;
16
+ }).join("\n");
17
+ await sql.raw(`CREATE TRIGGER IF NOT EXISTS ${table}_history_${event.toLowerCase()} AFTER ${event} ON ${table} BEGIN ${statements} END`).execute(db);
18
+ }
19
+ }
20
+ export async function down(db) {
21
+ for (const table of ["s_threads", "s_messages"]) for (const event of ["insert", "update", "delete"]) {
22
+ await sql.raw(`DROP TRIGGER IF EXISTS ${table}_history_${event}`).execute(db);
23
+ }
24
+ await sql`DROP INDEX IF EXISTS s_messages_history_order`.execute(db);
25
+ await sql`DROP TABLE IF EXISTS chat_history_revisions`.execute(db);
26
+ }
@@ -0,0 +1,4 @@
1
+ import { type Kysely } from 'kysely';
2
+ /** Match the reader's legacy missing-order-key normalization exactly. */
3
+ export declare function up(db: Kysely<unknown>): Promise<void>;
4
+ export declare function down(db: Kysely<unknown>): Promise<void>;
@@ -0,0 +1,24 @@
1
+ import { sql } from "kysely";
2
+ import { up as installCanonicalHistory } from "./022_canonical_chat_reader.js";
3
+ export async function up(db) {
4
+ for (const table of ["s_threads", "s_messages"]) for (const event of ["insert", "update", "delete"]) {
5
+ await sql.raw(`DROP TRIGGER IF EXISTS ${table}_history_${event}`).execute(db);
6
+ }
7
+ await sql`CREATE TABLE chat_history_revisions_v2 (
8
+ workspace_id TEXT NOT NULL, thread_id TEXT NOT NULL, value INTEGER NOT NULL DEFAULT 0,
9
+ PRIMARY KEY (workspace_id, thread_id))`.execute(db);
10
+ await sql`INSERT INTO chat_history_revisions_v2 SELECT workspace_id, thread_id, value FROM chat_history_revisions`.execute(db);
11
+ await sql`DROP TABLE chat_history_revisions`.execute(db);
12
+ await sql`ALTER TABLE chat_history_revisions_v2 RENAME TO chat_history_revisions`.execute(db);
13
+ await installCanonicalHistory(db);
14
+ await sql`DROP INDEX IF EXISTS s_messages_history_order`.execute(db);
15
+ await sql`CREATE INDEX s_messages_history_order ON s_messages
16
+ (workspace_id, json_extract(data_json, '$.thread_id'), json_extract(data_json, '$.index'),
17
+ COALESCE(json_extract(data_json, '$.order_key'), ''), id)`.execute(db);
18
+ }
19
+ export async function down(db) {
20
+ await sql`DROP INDEX IF EXISTS s_messages_history_order`.execute(db);
21
+ await sql`CREATE INDEX s_messages_history_order ON s_messages
22
+ (workspace_id, json_extract(data_json, '$.thread_id'), json_extract(data_json, '$.index'),
23
+ json_extract(data_json, '$.order_key'), id)`.execute(db);
24
+ }
@@ -207,6 +207,7 @@ export interface BackgroundJobsTable {
207
207
  completed_at: number | null;
208
208
  error: string | null;
209
209
  tool_calls_json: string | null;
210
+ usage_json: string | null;
210
211
  workflow_state_json: string | null;
211
212
  execution_json: string | null;
212
213
  idempotency_key: string | null;
@@ -9,18 +9,24 @@ import type { H3Event } from 'h3';
9
9
  import type { CanonicalStorageQueryRequest, CanonicalStorageQueryResponse, SyncGatewayAdapter, SyncMaintenanceState, UploadIntentConsumptionRequest, UploadIntentReservationRequest } from '~~/server/sync/gateway/types';
10
10
  import type { AdmitChatGenerationResult, CanonicalGenerationSnapshot, CanonicalHistoryActor, ChatGenerationAdmissionEnvelope, FinalizeChatGenerationResult } from '~~/shared/chat/background-history';
11
11
  import type { PullRequest, PullResponse, PushBatch, PushResult, SnapshotRequest, SnapshotResponse } from '~~/shared/sync/types';
12
+ import { type CanonicalChatQuery, type CanonicalChatReadResult } from '~~/shared/chat/history-reader';
12
13
  export declare class SqliteSyncGatewayAdapter implements SyncGatewayAdapter {
13
14
  id: string;
14
15
  get capabilities(): {
15
16
  readonly snapshotBootstrap: "snapshot-v1";
16
17
  readonly historyRetention: "snapshot-v1";
17
18
  readonly backgroundGenerationHistory?: undefined;
19
+ readonly canonicalChatHistory?: undefined;
20
+ readonly workspaceItems?: undefined;
18
21
  } | {
19
22
  readonly snapshotBootstrap: "snapshot-v1";
20
23
  readonly historyRetention: "snapshot-v1";
21
24
  readonly backgroundGenerationHistory: "v1";
25
+ readonly canonicalChatHistory: "v1";
26
+ readonly workspaceItems: "v1";
22
27
  };
23
28
  private get db();
29
+ readChatHistory(actor: CanonicalHistoryActor, query: CanonicalChatQuery, signal?: AbortSignal): Promise<CanonicalChatReadResult>;
24
30
  private assertCanonicalActor;
25
31
  admitChatGeneration(actor: CanonicalHistoryActor, input: ChatGenerationAdmissionEnvelope): Promise<AdmitChatGenerationResult>;
26
32
  finalizeChatGeneration(actor: CanonicalHistoryActor, input: {
@@ -1,3 +1,4 @@
1
+ import { hasWorkspaceItemSemantics, WORKSPACE_ITEM_CAPABILITY } from "./workspace-item-capability.js";
1
2
  import { createHash } from "node:crypto";
2
3
  import { canRunSyncHistoryGc } from "./history-gc-policy.js";
3
4
  import { createError } from "h3";
@@ -19,6 +20,8 @@ import { SYNCED_TABLE_MAP, ALLOWED_SYNC_TABLES } from "../db/schema.js";
19
20
  import { emitWebhookSystemHook } from "~~/server/utils/webhooks/runtime";
20
21
  import { incomingRevisionWins } from "~~/shared/sync/revision";
21
22
  import { sanitizePayloadForSync } from "~~/shared/sync/sanitize";
23
+ import { readRequestUsage } from "~~/shared/chat/compaction";
24
+ import { validateCanonicalChatQuery, encodeCanonicalChatSeek, parseCanonicalChatSeek } from "~~/shared/chat/history-reader";
22
25
  import { computePullRetention } from "./history-gc-policy.js";
23
26
  const DEFAULT_PULL_LIMIT = 100;
24
27
  const MAX_PULL_LIMIT = 1e3;
@@ -428,12 +431,59 @@ export class SqliteSyncGatewayAdapter {
428
431
  } : {
429
432
  snapshotBootstrap: "snapshot-v1",
430
433
  historyRetention: "snapshot-v1",
431
- backgroundGenerationHistory: "v1"
434
+ backgroundGenerationHistory: "v1",
435
+ canonicalChatHistory: "v1",
436
+ workspaceItems: "v1"
432
437
  };
433
438
  }
434
439
  get db() {
435
440
  return getSqliteDb();
436
441
  }
442
+ async readChatHistory(actor, query, signal) {
443
+ validateCanonicalChatQuery(query);
444
+ signal?.throwIfAborted();
445
+ if (isD1Driver()) return { status: "scope_incomplete" };
446
+ const raw = getRawDb();
447
+ const result = raw.transaction(() => {
448
+ const membership = raw.prepare(`SELECT m.role FROM workspace_members m JOIN workspaces w ON w.id = m.workspace_id
449
+ WHERE m.workspace_id = ? AND m.user_id = ? AND COALESCE(w.deleted, 0) = 0`).get(actor.workspaceId, actor.userId);
450
+ if (!membership || !["owner", "editor", "viewer"].includes(membership.role)) throw new Error("Forbidden canonical history actor");
451
+ const counter = raw.prepare("SELECT value FROM server_version_counter WHERE workspace_id = ?").get(actor.workspaceId);
452
+ const revision = String(counter?.value ?? 0);
453
+ const parse = (row) => {
454
+ const payload = JSON.parse(row.data_json);
455
+ if (payload.id !== row.id) throw new Error("Canonical history identity mismatch");
456
+ return { ...payload, id: row.id, clock: row.clock, deleted: row.deleted === 1 };
457
+ };
458
+ if (query.kind === "thread") {
459
+ const threadRevision = raw.prepare("SELECT value FROM chat_history_revisions WHERE workspace_id = ? AND thread_id = ?").get(actor.workspaceId, query.thread_id);
460
+ const row = raw.prepare("SELECT id, data_json, clock, deleted FROM s_threads WHERE workspace_id = ? AND id = ?").get(actor.workspaceId, query.thread_id);
461
+ return { status: "ok", revision: String(threadRevision?.value ?? 0), thread: row ? parse(row) : void 0 };
462
+ }
463
+ if (query.kind === "messages") {
464
+ if (!query.message_ids.length) return { status: "ok", revision, messages: [] };
465
+ const rows2 = raw.prepare(`SELECT id, data_json, clock, deleted FROM s_messages WHERE workspace_id = ? AND id IN (${query.message_ids.map(() => "?").join(",")})`).all(actor.workspaceId, ...query.message_ids);
466
+ return { status: "ok", revision, messages: rows2.map(parse) };
467
+ }
468
+ let after;
469
+ const seek = parseCanonicalChatSeek(query.cursor, query.thread_id);
470
+ if (seek) after = seek.key;
471
+ else if (query.cursor) {
472
+ const value = JSON.parse(query.cursor);
473
+ if (!Array.isArray(value) || value.length !== 4 || value[0] !== query.thread_id || !Number.isSafeInteger(value[1]) || typeof value[2] !== "string" || typeof value[3] !== "string") throw new Error("Invalid canonical history cursor");
474
+ after = [value[1], value[2], value[3]];
475
+ }
476
+ const rows = raw.prepare(`SELECT id, data_json, clock, deleted FROM s_messages
477
+ WHERE workspace_id = ? AND json_extract(data_json, '$.thread_id') = ?
478
+ ${after ? `AND (json_extract(data_json, '$.index'), COALESCE(json_extract(data_json, '$.order_key'), ''), id) ${seek?.backward ? "<" : ">"} (?, ?, ?)` : ""}
479
+ ORDER BY json_extract(data_json, '$.index') ${seek?.backward ? "DESC" : "ASC"}, COALESCE(json_extract(data_json, '$.order_key'), '') ${seek?.backward ? "DESC" : "ASC"}, id ${seek?.backward ? "DESC" : "ASC"} LIMIT ?`).all(actor.workspaceId, query.thread_id, ...after ?? [], query.limit);
480
+ const messages = rows.map(parse);
481
+ const last = messages.at(-1);
482
+ return { status: "ok", revision, messages, next_cursor: messages.length === query.limit && last ? seek ? encodeCanonicalChatSeek({ ...seek, key: [Number(last.index), String(last.order_key ?? ""), last.id] }) : JSON.stringify([query.thread_id, last.index, last.order_key ?? "", last.id]) : void 0 };
483
+ })();
484
+ signal?.throwIfAborted();
485
+ return result;
486
+ }
437
487
  assertCanonicalActor(actor) {
438
488
  if (isD1Driver()) {
439
489
  throw new Error("SQLite D1 does not support background generation history v1");
@@ -639,6 +689,7 @@ export class SqliteSyncGatewayAdapter {
639
689
  const hlc = `${Date.now().toString(36).padStart(9, "0")}:000:background`;
640
690
  const opId = backgroundHistoryOpId(admission, "finalize");
641
691
  const terminalState = input.snapshot.status === "complete" ? "complete" : input.snapshot.status === "aborted" ? "aborted" : "failed";
692
+ const usage = readRequestUsage(input.snapshot.usage);
642
693
  const payload = {
643
694
  ...current,
644
695
  pending: false,
@@ -649,6 +700,7 @@ export class SqliteSyncGatewayAdapter {
649
700
  op_id: opId,
650
701
  data: {
651
702
  ...data,
703
+ ...usage ? { usage } : {},
652
704
  content: input.snapshot.content,
653
705
  reasoning_text: input.snapshot.reasoning || null,
654
706
  tool_calls: input.snapshot.toolCalls ?? null,
@@ -1068,6 +1120,20 @@ export class SqliteSyncGatewayAdapter {
1068
1120
  let finalServerVersion = 0;
1069
1121
  const hookEmissions = [];
1070
1122
  const runTx = raw.transaction(() => {
1123
+ if (input.workspaceItemCapability !== WORKSPACE_ITEM_CAPABILITY) {
1124
+ for (const op of uniqueOps) {
1125
+ const table = SYNCED_TABLE_MAP[op.tableName];
1126
+ if (!table || !["posts", "projects"].includes(op.tableName)) continue;
1127
+ const canonical = raw.prepare("SELECT data_json FROM " + table + " WHERE id = ? AND workspace_id = ?").get(op.pk, workspaceId);
1128
+ if (hasWorkspaceItemSemantics(op.tableName, op.payload) || hasWorkspaceItemSemantics(op.tableName, canonical ? JSON.parse(canonical.data_json) : void 0)) {
1129
+ throw createError({
1130
+ statusCode: 426,
1131
+ statusMessage: "Update OR3 Chat to use workspace Files and Trash",
1132
+ data: { code: "OR3_WORKSPACE_ITEM_UPDATE_REQUIRED", requiredCapability: WORKSPACE_ITEM_CAPABILITY }
1133
+ });
1134
+ }
1135
+ }
1136
+ }
1071
1137
  const opIds = uniqueOps.map((o) => o.stamp.opId);
1072
1138
  const existingOps = /* @__PURE__ */ new Map();
1073
1139
  const chunkSize = 500;
@@ -0,0 +1,4 @@
1
+ export declare const WORKSPACE_ITEM_CAPABILITY: "v1";
2
+ export type WorkspaceItemCapability = typeof WORKSPACE_ITEM_CAPABILITY;
3
+ /** Detect semantics even when their version/content is malformed or unknown. */
4
+ export declare function hasWorkspaceItemSemantics(tableName: string, payload: unknown): boolean;
@@ -0,0 +1,26 @@
1
+ const FILE_CATALOG_POST_TYPE = "or3:file";
2
+ const WORKSPACE_ITEM_META_KEY = "or3.workspace-item";
3
+ export const WORKSPACE_ITEM_CAPABILITY = "v1";
4
+ function decoded(value) {
5
+ if (typeof value !== "string") return value;
6
+ try {
7
+ return JSON.parse(value);
8
+ } catch {
9
+ return null;
10
+ }
11
+ }
12
+ export function hasWorkspaceItemSemantics(tableName, payload) {
13
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
14
+ const row = payload;
15
+ if (tableName === "posts") {
16
+ if ((row.post_type ?? row.postType) === FILE_CATALOG_POST_TYPE) return true;
17
+ const meta = decoded(row.meta);
18
+ if (Array.isArray(meta)) return meta.some((entry) => entry?.key === WORKSPACE_ITEM_META_KEY);
19
+ return !!meta && typeof meta === "object" && (Object.hasOwn(meta, WORKSPACE_ITEM_META_KEY) || "key" in meta && meta.key === WORKSPACE_ITEM_META_KEY);
20
+ }
21
+ if (tableName === "projects") {
22
+ const entries = decoded(row.data);
23
+ return Array.isArray(entries) && entries.some((entry) => entry && typeof entry === "object" && entry.kind === "file");
24
+ }
25
+ return false;
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "or3-provider-sqlite",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Saluana/or3-provider-sqlite.git"
@@ -38,7 +38,8 @@
38
38
  "lint": "eslint src --max-warnings 0",
39
39
  "type-check": "tsc --noEmit",
40
40
  "type-check:standalone": "tsc -p tsconfig.release.json --noEmit",
41
- "test": "vitest run"
41
+ "test": "vitest run",
42
+ "test:host-integration": "vitest run --config vitest.host-integration.config.ts"
42
43
  },
43
44
  "publishConfig": {
44
45
  "access": "public"
@@ -65,6 +66,7 @@
65
66
  "h3": "^1.15.4",
66
67
  "pathe": "^2.0.3",
67
68
  "typescript": "^5.9.3",
68
- "vitest": "^4.0.18"
69
+ "vitest": "^4.0.18",
70
+ "zod": "4.6.5"
69
71
  }
70
72
  }