@rynx-ai/daemon 0.1.11-beta.11 → 0.1.11-beta.12

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/plugin-channel-lark",
3
- "version": "0.1.11-beta.11",
3
+ "version": "0.1.11-beta.12",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -9,9 +9,10 @@ import { createHash } from "node:crypto";
9
9
  import { accessSync, chmodSync, closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, writeSync, } from "node:fs";
10
10
  import { constants as fsConstants } from "node:fs";
11
11
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
12
+ import { DatabaseSync } from "node:sqlite";
12
13
  import { rynxHome } from "@rynx-ai/core";
13
- import Database from "better-sqlite3";
14
14
  import extractZipArchive from "extract-zip";
15
+ import { hasSqlitePrimaryCode, SQLITE_BUSY, SQLITE_LOCKED } from "./sqlite.js";
15
16
  const BUILD_OWNER_FILE = ".rynx-cft-owner.json";
16
17
  const STAGING_OWNER_FILE = ".rynx-cft-staging-owner.json";
17
18
  const ACTIVE_MANIFEST_FILE = "active.json";
@@ -360,7 +361,7 @@ function acquireStoreLock(rootDir) {
360
361
  throw new Error("Chrome for Testing store lock is not a direct regular file");
361
362
  }
362
363
  }
363
- const connection = new Database(lockPath, { timeout: 0 });
364
+ const connection = new DatabaseSync(lockPath, { timeout: 0 });
364
365
  try {
365
366
  const stat = lstatSync(lockPath);
366
367
  if (!stat.isFile() ||
@@ -369,14 +370,13 @@ function acquireStoreLock(rootDir) {
369
370
  throw new Error("Chrome for Testing store lock escapes its owned root");
370
371
  }
371
372
  chmodSync(lockPath, 0o600);
372
- connection.pragma("busy_timeout = 0");
373
373
  connection.exec("BEGIN EXCLUSIVE");
374
374
  connection.exec("CREATE TABLE IF NOT EXISTS cft_store_guard (id INTEGER PRIMARY KEY)");
375
375
  return connection;
376
376
  }
377
377
  catch (error) {
378
378
  connection.close();
379
- if (hasErrorCode(error, "SQLITE_BUSY") || hasErrorCode(error, "SQLITE_LOCKED")) {
379
+ if (hasSqlitePrimaryCode(error, SQLITE_BUSY) || hasSqlitePrimaryCode(error, SQLITE_LOCKED)) {
380
380
  throw new Error("Chrome for Testing store is busy in another process", { cause: error });
381
381
  }
382
382
  throw error;
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { db } from "./db.js";
3
+ import { transaction } from "./sqlite.js";
3
4
  const DEFAULT_TTL_MS = 5 * 60_000;
4
5
  const MAX_TTL_MS = 15 * 60_000;
5
6
  /** Create a short-lived, one-use link into the generic session console. */
@@ -23,7 +24,7 @@ export function consumeControlLink(tokenInput) {
23
24
  if (!token || token.length > 512)
24
25
  return null;
25
26
  const now = new Date().toISOString();
26
- const consume = db().transaction(() => {
27
+ const consume = transaction(db(), () => {
27
28
  const row = db()
28
29
  .prepare(`SELECT session_id
29
30
  FROM control_links
@@ -39,9 +40,9 @@ export function consumeControlLink(tokenInput) {
39
40
  return consume.immediate();
40
41
  }
41
42
  export function pruneControlLinks(now = new Date().toISOString()) {
42
- return db()
43
+ return Number(db()
43
44
  .prepare("DELETE FROM control_links WHERE expires_at <= ? OR used_at IS NOT NULL")
44
- .run(now).changes;
45
+ .run(now).changes);
45
46
  }
46
47
  function required(value, label) {
47
48
  const normalized = value?.trim();
@@ -1,8 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
4
5
  import { rynxHome } from "@rynx-ai/core";
5
- import Database from "better-sqlite3";
6
+ import { hasSqlitePrimaryCode, SQLITE_BUSY, SQLITE_LOCKED } from "./sqlite.js";
6
7
  const LOCK_FILE = "daemon-owner.sqlite";
7
8
  const OWNER_FILE = "daemon-owner.json";
8
9
  /**
@@ -18,16 +19,15 @@ export async function acquireDaemonOwner(env = process.env) {
18
19
  const lockPath = path.join(home, LOCK_FILE);
19
20
  const ownerFile = path.join(home, OWNER_FILE);
20
21
  await mkdir(home, { recursive: true, mode: 0o700 });
21
- const connection = new Database(lockPath, { timeout: 0 });
22
+ const connection = new DatabaseSync(lockPath, { timeout: 0 });
22
23
  try {
23
- connection.pragma("busy_timeout = 0");
24
24
  connection.exec("BEGIN EXCLUSIVE");
25
25
  // Force this transaction to hold a write lock even on a brand-new file.
26
26
  connection.exec("CREATE TABLE IF NOT EXISTS daemon_owner_guard (id INTEGER PRIMARY KEY)");
27
27
  }
28
28
  catch (error) {
29
29
  connection.close();
30
- if (hasCode(error, "SQLITE_BUSY") || hasCode(error, "SQLITE_LOCKED")) {
30
+ if (hasSqlitePrimaryCode(error, SQLITE_BUSY) || hasSqlitePrimaryCode(error, SQLITE_LOCKED)) {
31
31
  throw await ownerConflictError(home, ownerFile);
32
32
  }
33
33
  throw error;
@@ -99,6 +99,3 @@ function rollbackAndClose(connection) {
99
99
  connection.close();
100
100
  }
101
101
  }
102
- function hasCode(error, code) {
103
- return typeof error === "object" && error !== null && "code" in error && error.code === code;
104
- }
package/dist/db.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import Database from "better-sqlite3";
1
+ import { DatabaseSync } from "node:sqlite";
2
2
  /** Absolute path to the SQLite file (override via `RYNX_DB`, else `~/.rynx/rynx.db`). */
3
3
  export declare function dbPath(): string;
4
4
  /** Open (or reuse) the database at the current path, migrating + importing on first open. */
5
- export declare function db(): Database.Database;
5
+ export declare function db(): DatabaseSync;
package/dist/db.js CHANGED
@@ -18,8 +18,9 @@
18
18
  */
19
19
  import { chmodSync, mkdirSync } from "node:fs";
20
20
  import { dirname, isAbsolute, join, resolve } from "node:path";
21
+ import { DatabaseSync } from "node:sqlite";
21
22
  import { rynxHome } from "@rynx-ai/core";
22
- import Database from "better-sqlite3";
23
+ import { transaction } from "./sqlite.js";
23
24
  const connections = new Map();
24
25
  /** Absolute path to the SQLite file (override via `RYNX_DB`, else `~/.rynx/rynx.db`). */
25
26
  export function dbPath() {
@@ -36,9 +37,7 @@ export function db() {
36
37
  if (cached)
37
38
  return cached;
38
39
  mkdirSync(dirname(path), { recursive: true });
39
- const conn = new Database(path);
40
- conn.pragma("busy_timeout = 5000");
41
- conn.pragma("foreign_keys = ON");
40
+ const conn = new DatabaseSync(path, { timeout: 5000, enableForeignKeyConstraints: true });
42
41
  migrate(conn);
43
42
  // Plugin state and daemon credentials are private.
44
43
  try {
@@ -376,7 +375,7 @@ function migrateSessionSnapshots(conn) {
376
375
  const names = new Set(columns.map((column) => column.name));
377
376
  if (names.has("workspace") && names.has("execution"))
378
377
  return;
379
- conn.transaction(() => {
378
+ transaction(conn, () => {
380
379
  conn.exec(`
381
380
  INSERT OR IGNORE INTO session_snapshot_cleanup (session_id)
382
381
  SELECT id FROM sessions;
@@ -20,6 +20,7 @@ import { mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
20
20
  import path from "node:path";
21
21
  import { rynxHome } from "@rynx-ai/core";
22
22
  import { db } from "../db.js";
23
+ import { transaction } from "../sqlite.js";
23
24
  /** Mirrors the daemon runtime store's default resolution. */
24
25
  function defaultProxyDir() {
25
26
  return path.join(rynxHome(), ".codex-proxy");
@@ -46,9 +47,9 @@ export function cleanupLegacySessions(opts = {}) {
46
47
  let deletedLogRows = 0;
47
48
  let deletedSessions = 0;
48
49
  let prunedStoreEntries = 0;
49
- conn.transaction(() => {
50
- deletedLogRows = conn.prepare(`DELETE FROM session_items WHERE ${LEGACY_LOG}`).run().changes;
51
- deletedSessions = conn.prepare(`DELETE FROM sessions WHERE ${LEGACY_META}`).run().changes;
50
+ transaction(conn, () => {
51
+ deletedLogRows = Number(conn.prepare(`DELETE FROM session_items WHERE ${LEGACY_LOG}`).run().changes);
52
+ deletedSessions = Number(conn.prepare(`DELETE FROM sessions WHERE ${LEGACY_META}`).run().changes);
52
53
  })();
53
54
  // The runtime store is daemon-owned. Plugin routing stores remain private to
54
55
  // their plugins and are never inspected by this migration.
@@ -3,6 +3,7 @@ import { mkdirSync, realpathSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { createPluginCliInvocationLeaseId, removePluginRuntimeLease, writePluginRuntimeLease, } from "@rynx-ai/plugin-sdk";
5
5
  import { db } from "./db.js";
6
+ import { transaction } from "./sqlite.js";
6
7
  import { pluginDataDir, pluginRuntimeCacheDir } from "./plugin-installer.js";
7
8
  import { validatePluginPackageSnapshot } from "./plugin-package.js";
8
9
  import { getPlugin } from "./plugin-store.js";
@@ -74,7 +75,7 @@ export async function runPluginCliCommand(pluginId, args, options = {}) {
74
75
  removePluginRuntimeLease(dataDir, leaseId);
75
76
  };
76
77
  try {
77
- db().transaction(() => {
78
+ transaction(db(), () => {
78
79
  const record = getPlugin(pluginId);
79
80
  if (!record)
80
81
  throw new Error(`plugin "${pluginId}" is not installed`);
@@ -4,6 +4,7 @@ import { join, resolve } from "node:path";
4
4
  import { MARKETPLACE_ID_PATTERN, pluginCliInvocationLeasePid, readPluginRuntimeLeases, removePluginRuntimeLease, } from "@rynx-ai/plugin-sdk";
5
5
  import { rynxHome } from "@rynx-ai/core";
6
6
  import { db } from "./db.js";
7
+ import { transaction } from "./sqlite.js";
7
8
  import { isBuiltInPluginMarketplace } from "./plugin-marketplace.js";
8
9
  import { detectPluginSource, inspectPluginPackage, materializePluginPackage, } from "./plugin-package.js";
9
10
  import { canonicalPluginId, getPlugin, removePlugin, upsertPlugin, } from "./plugin-store.js";
@@ -117,7 +118,7 @@ export async function preparePluginInstallation(spec, onProgress = () => { }, op
117
118
  promotedByThisTransaction = true;
118
119
  }
119
120
  promotedPath = realpathSync(target);
120
- db().transaction(() => {
121
+ transaction(db(), () => {
121
122
  const current = getPlugin(canonicalId);
122
123
  if (!samePluginGeneration(existing, current)) {
123
124
  throw new Error(`plugin "${canonicalId}" changed concurrently; retry`);
@@ -172,7 +173,7 @@ export async function preparePluginInstallation(spec, onProgress = () => { }, op
172
173
  }
173
174
  export async function uninstallPlugin(identity, onProgress = () => { }, options = {}) {
174
175
  let record;
175
- const removed = db().transaction(() => {
176
+ const removed = transaction(db(), () => {
176
177
  record = getPlugin(identity);
177
178
  if (!record)
178
179
  return false;
@@ -3,6 +3,7 @@ import { isAbsolute, join, posix, relative, resolve, sep } from "node:path";
3
3
  import { MARKETPLACE_ID_PATTERN, parsePluginMarketplace, } from "@rynx-ai/plugin-sdk";
4
4
  import { rynxHome } from "@rynx-ai/core";
5
5
  import { db } from "./db.js";
6
+ import { transaction } from "./sqlite.js";
6
7
  import { detectPluginSource, materializePluginPackage, parsePluginSourceSpec, pluginSourceFingerprint, } from "./plugin-package.js";
7
8
  export function listPluginMarketplaces() {
8
9
  return db().prepare("SELECT * FROM plugin_marketplaces ORDER BY marketplace_id").all().map(toRecord);
@@ -27,7 +28,7 @@ export async function addPluginMarketplace(spec, options = {}) {
27
28
  throw new Error(`built-in marketplace "${marketplaceId}" is managed by Rynx`);
28
29
  }
29
30
  const now = new Date().toISOString();
30
- db().transaction(() => {
31
+ transaction(db(), () => {
31
32
  const binding = db().prepare(`SELECT source_fingerprint AS sourceFingerprint
32
33
  FROM plugin_marketplace_bindings
33
34
  WHERE marketplace_id = ?`).get(marketplaceId);
@@ -1,4 +1,4 @@
1
- import Database from "better-sqlite3";
1
+ import { type DatabaseSync } from "node:sqlite";
2
2
  declare const DEFAULT_AUTHORITIES: readonly ["daemon.full"];
3
3
  export type RuntimeAuthority = (typeof DEFAULT_AUTHORITIES)[number];
4
4
  export type RemoteRuntimeAccessErrorCode = "invalid_claim" | "claim_expired" | "claim_used" | "grant_not_found" | "grant_revoked" | "grant_identity_mismatch" | "conflict" | "corrupt_state";
@@ -46,7 +46,7 @@ type Clock = () => Date;
46
46
  export declare class RemoteRuntimeAccessStore {
47
47
  private readonly connection;
48
48
  private readonly clock;
49
- constructor(connection?: Database.Database, clock?: Clock);
49
+ constructor(connection?: DatabaseSync, clock?: Clock);
50
50
  createPairingClaim(input: CreatePairingClaimInput): RuntimePairingClaim;
51
51
  redeemPairingClaim(input: RedeemPairingClaimInput): RuntimeClientGrant;
52
52
  resolveActiveGrant(input: ResolveActiveGrantInput): RuntimeClientGrant;
@@ -1,5 +1,6 @@
1
1
  import { createHash, createPublicKey, randomBytes, timingSafeEqual, } from "node:crypto";
2
2
  import { db } from "./db.js";
3
+ import { hasSqlitePrimaryCode, SQLITE_CONSTRAINT, transaction } from "./sqlite.js";
3
4
  const CLAIM_SECRET_HASH_DOMAIN = "rynx-runtime-pairing-claim-secret-v1\0";
4
5
  const DEFAULT_AUTHORITIES = Object.freeze(["daemon.full"]);
5
6
  const MIN_TTL_MS = 1_000;
@@ -35,7 +36,7 @@ export class RemoteRuntimeAccessStore {
35
36
  const created = this.now();
36
37
  const createdAt = created.toISOString();
37
38
  const expiresAt = new Date(created.getTime() + ttlMs).toISOString();
38
- const create = this.connection.transaction(() => {
39
+ const create = transaction(this.connection, () => {
39
40
  this.deleteRetainedExpiredClaims(created);
40
41
  this.connection
41
42
  .prepare(`INSERT INTO runtime_pairing_claims
@@ -60,7 +61,7 @@ export class RemoteRuntimeAccessStore {
60
61
  const clientId = validId(input.clientId, "clientId", "invalid_claim");
61
62
  const clientPublicKey = canonicalEd25519PublicKey(input.clientPublicKey, "invalid_claim");
62
63
  const requestedLabel = optionalLabel(input.label, "invalid_claim");
63
- const redeem = this.connection.transaction(() => {
64
+ const redeem = transaction(this.connection, () => {
64
65
  const now = this.now().toISOString();
65
66
  const claim = this.connection
66
67
  .prepare(`SELECT claim_id, secret_hash, authorities, client_label, created_at, expires_at,
@@ -124,7 +125,7 @@ export class RemoteRuntimeAccessStore {
124
125
  const grantId = validId(input.grantId, "grantId", "grant_not_found");
125
126
  const clientId = validId(input.clientId, "clientId", "grant_identity_mismatch");
126
127
  const clientPublicKey = canonicalEd25519PublicKey(input.clientPublicKey, "grant_identity_mismatch");
127
- const resolve = this.connection.transaction(() => {
128
+ const resolve = transaction(this.connection, () => {
128
129
  const row = this.connection
129
130
  .prepare(`SELECT grant_id, client_id, client_public_key, client_label, authorities,
130
131
  created_at, revoked_at, last_used_at
@@ -180,7 +181,7 @@ export class RemoteRuntimeAccessStore {
180
181
  revokeClientGrant(grantIdInput) {
181
182
  const grantId = validId(grantIdInput, "grantId", "grant_not_found");
182
183
  const revokedAt = this.now().toISOString();
183
- const revoke = this.connection.transaction(() => {
184
+ const revoke = transaction(this.connection, () => {
184
185
  const revoked = this.connection
185
186
  .prepare(`UPDATE runtime_client_grants
186
187
  SET revoked_at = ?
@@ -382,11 +383,7 @@ function grantFromRow(row) {
382
383
  };
383
384
  }
384
385
  function isSqliteConstraint(error) {
385
- return (typeof error === "object" &&
386
- error !== null &&
387
- "code" in error &&
388
- typeof error.code === "string" &&
389
- error.code.startsWith("SQLITE_CONSTRAINT"));
386
+ return hasSqlitePrimaryCode(error, SQLITE_CONSTRAINT);
390
387
  }
391
388
  function accessError(code, message, cause) {
392
389
  return new RemoteRuntimeAccessError(code, message, cause === undefined ? undefined : { cause });
@@ -1,4 +1,4 @@
1
- import Database from "better-sqlite3";
1
+ import { type DatabaseSync } from "node:sqlite";
2
2
  export interface RemoteRuntimeTarget {
3
3
  daemonId: string;
4
4
  displayName: string;
@@ -40,7 +40,7 @@ export interface CommitRemoteRuntimePendingEnrollmentOptions {
40
40
  export declare class RemoteRuntimeTargetStore {
41
41
  private readonly connection;
42
42
  private readonly clock;
43
- constructor(connection?: Database.Database, clock?: () => Date);
43
+ constructor(connection?: DatabaseSync, clock?: () => Date);
44
44
  add(input: AddRemoteRuntimeTargetInput): RemoteRuntimeTarget;
45
45
  addPendingEnrollment(input: AddRemoteRuntimePendingEnrollmentInput): RemoteRuntimePendingEnrollment;
46
46
  getPendingEnrollment(claimIdInput: string): RemoteRuntimePendingEnrollment | undefined;
@@ -1,5 +1,6 @@
1
1
  import { createPublicKey } from "node:crypto";
2
2
  import { db } from "./db.js";
3
+ import { hasSqlitePrimaryCode, SQLITE_CONSTRAINT, transaction } from "./sqlite.js";
3
4
  const OPAQUE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,255}$/;
4
5
  const CREDENTIAL_REF_PATTERN = /^credential_[A-Za-z0-9_-]{24}$/;
5
6
  const MAX_DISPLAY_NAME_CHARS = 128;
@@ -13,7 +14,7 @@ export class RemoteRuntimeTargetStore {
13
14
  add(input) {
14
15
  const normalized = normalizeTargetInput(input);
15
16
  const now = validNow(this.clock()).toISOString();
16
- const insert = this.connection.transaction(() => {
17
+ const insert = transaction(this.connection, () => {
17
18
  this.connection.prepare(`INSERT INTO runtime_targets
18
19
  (daemon_id, display_name, identity_public_key, credential_ref, created_at, updated_at)
19
20
  VALUES (?, ?, ?, ?, ?, ?)`).run(normalized.daemonId, normalized.displayName, normalized.identityPublicKey, normalized.credentialRef, now, now);
@@ -67,7 +68,7 @@ export class RemoteRuntimeTargetStore {
67
68
  }
68
69
  removePendingEnrollment(claimIdInput) {
69
70
  const claimId = validId(claimIdInput, "claimId");
70
- const remove = this.connection.transaction(() => {
71
+ const remove = transaction(this.connection, () => {
71
72
  const pending = this.getPendingEnrollment(claimId);
72
73
  if (!pending)
73
74
  return undefined;
@@ -82,7 +83,7 @@ export class RemoteRuntimeTargetStore {
82
83
  const claimId = validId(claimIdInput, "claimId");
83
84
  const normalized = normalizeTargetInput(input);
84
85
  const now = validNow(this.clock()).toISOString();
85
- const commit = this.connection.transaction(() => {
86
+ const commit = transaction(this.connection, () => {
86
87
  const pending = this.getPendingEnrollment(claimId);
87
88
  if (!pending)
88
89
  throw new Error("pending Runtime enrollment was not found");
@@ -134,7 +135,7 @@ export class RemoteRuntimeTargetStore {
134
135
  }
135
136
  remove(daemonIdInput) {
136
137
  const daemonId = validId(daemonIdInput, "daemonId");
137
- const remove = this.connection.transaction(() => {
138
+ const remove = transaction(this.connection, () => {
138
139
  const target = this.get(daemonId);
139
140
  if (!target)
140
141
  return undefined;
@@ -321,6 +322,5 @@ function validNow(value) {
321
322
  return value;
322
323
  }
323
324
  function isSqliteConstraint(error) {
324
- return typeof error === "object" && error !== null && "code" in error &&
325
- typeof error.code === "string" && error.code.startsWith("SQLITE_CONSTRAINT");
325
+ return hasSqlitePrimaryCode(error, SQLITE_CONSTRAINT);
326
326
  }
@@ -1,5 +1,6 @@
1
1
  import { SessionEmulatorBindingConflictError, } from "@rynx-ai/server";
2
2
  import { db } from "./db.js";
3
+ import { hasSqlitePrimaryCode, SQLITE_CONSTRAINT, transaction } from "./sqlite.js";
3
4
  export class SqliteSessionEmulatorBindingStore {
4
5
  get(sessionId) {
5
6
  const row = db().prepare(`SELECT session_id, binding_id, backend, device_id, device_name, attached_at
@@ -13,7 +14,7 @@ export class SqliteSessionEmulatorBindingStore {
13
14
  }
14
15
  bind(record) {
15
16
  const connection = db();
16
- const replace = connection.transaction(() => {
17
+ const replace = transaction(connection, () => {
17
18
  connection.prepare("DELETE FROM session_emulator_bindings WHERE session_id = ?")
18
19
  .run(record.sessionId);
19
20
  connection.prepare(`INSERT INTO session_emulator_bindings
@@ -40,7 +41,7 @@ export class SqliteSessionEmulatorBindingStore {
40
41
  return db().prepare("DELETE FROM session_emulator_bindings WHERE session_id = ?").run(sessionId).changes > 0;
41
42
  }
42
43
  releaseDevice(backend, deviceId) {
43
- return db().prepare("DELETE FROM session_emulator_bindings WHERE backend = ? AND device_id = ?").run(backend, deviceId).changes;
44
+ return Number(db().prepare("DELETE FROM session_emulator_bindings WHERE backend = ? AND device_id = ?").run(backend, deviceId).changes);
44
45
  }
45
46
  }
46
47
  function bindingRecord(row) {
@@ -54,8 +55,5 @@ function bindingRecord(row) {
54
55
  };
55
56
  }
56
57
  function isSqliteConstraint(error) {
57
- if (!error || typeof error !== "object")
58
- return false;
59
- const code = error.code;
60
- return typeof code === "string" && code.startsWith("SQLITE_CONSTRAINT");
58
+ return hasSqlitePrimaryCode(error, SQLITE_CONSTRAINT);
61
59
  }
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import { newSessionId, newSessionItemId, normalizeSessionTitle, resolvedExecutionSnapshotSchema, rynxHome, sessionWorkspaceSnapshotSchema, } from "@rynx-ai/core";
5
5
  import { MachineSessionServiceFailure, } from "@rynx-ai/server";
6
6
  import { db } from "./db.js";
7
+ import { scalar, transaction } from "./sqlite.js";
7
8
  const FORK_COLS = [
8
9
  "operation_id",
9
10
  "request_hash",
@@ -39,9 +40,8 @@ export class SqliteSessionForkStore {
39
40
  }
40
41
  const now = new Date().toISOString();
41
42
  let targetSessionId = newSessionId();
42
- while (db().prepare("SELECT 1 FROM sessions WHERE id = ?").pluck().get(targetSessionId) ||
43
- db().prepare("SELECT 1 FROM session_fork_operations WHERE target_session_id = ?")
44
- .pluck().get(targetSessionId)) {
43
+ while (scalar(db().prepare("SELECT 1 FROM sessions WHERE id = ?")).get(targetSessionId) ||
44
+ scalar(db().prepare("SELECT 1 FROM session_fork_operations WHERE target_session_id = ?")).get(targetSessionId)) {
45
45
  targetSessionId = newSessionId();
46
46
  }
47
47
  try {
@@ -130,7 +130,7 @@ export class SqliteSessionForkStore {
130
130
  };
131
131
  });
132
132
  const conn = db();
133
- conn.transaction(() => {
133
+ transaction(conn, () => {
134
134
  if (input.operationId) {
135
135
  const current = this.read(input.operationId);
136
136
  if (current?.state === "completed")
@@ -160,8 +160,24 @@ export class SqliteSessionForkStore {
160
160
  @byte_length, @uploaded_bytes, @sha256, @width, @height, 'committed',
161
161
  @message_item_id, @created_at, @committed_at
162
162
  )`);
163
- for (const resource of resources)
164
- insertResource.run(resource);
163
+ for (const resource of resources) {
164
+ insertResource.run({
165
+ id: resource.id,
166
+ session_id: resource.session_id,
167
+ client_upload_id: resource.client_upload_id,
168
+ upload_id: resource.upload_id,
169
+ filename: resource.filename,
170
+ media_type: resource.media_type,
171
+ byte_length: resource.byte_length,
172
+ uploaded_bytes: resource.uploaded_bytes,
173
+ sha256: resource.sha256,
174
+ width: resource.width,
175
+ height: resource.height,
176
+ message_item_id: resource.message_item_id,
177
+ created_at: resource.created_at,
178
+ committed_at: resource.committed_at,
179
+ });
180
+ }
165
181
  const insertItem = conn.prepare(`INSERT INTO session_items (
166
182
  id, session_id, position, response_id, type, status,
167
183
  data, created_by, created_at
@@ -203,9 +219,9 @@ export class SqliteSessionForkStore {
203
219
  ORDER BY created_at`).all().map(toOperation);
204
220
  }
205
221
  hasReservedSource(sessionId) {
206
- return Boolean(db().prepare(`SELECT 1
222
+ return Boolean(scalar(db().prepare(`SELECT 1
207
223
  FROM session_fork_operations
208
- WHERE source_session_id = ? AND state = 'reserved'`).pluck().get(sessionId));
224
+ WHERE source_session_id = ? AND state = 'reserved'`)).get(sessionId));
209
225
  }
210
226
  markTargetDeleted(sessionId) {
211
227
  const now = new Date().toISOString();
@@ -1,5 +1,6 @@
1
1
  import { newSessionId } from "@rynx-ai/core";
2
2
  import { db } from "./db.js";
3
+ import { scalar, transaction } from "./sqlite.js";
3
4
  import { createSessionMeta } from "./session-meta-store.js";
4
5
  const MAX_ID_LENGTH = 512;
5
6
  const SHA256_PATTERN = /^[a-fA-F0-9]{64}$/;
@@ -58,7 +59,7 @@ export function replayPluginSessionLaunch(input) {
58
59
  throw new SessionLaunchOperationTargetDeletedError(pluginId, scopeId, operationId, existing.session_id);
59
60
  }
60
61
  if (!sessionExists(existing.session_id)) {
61
- db().transaction(() => {
62
+ transaction(db(), () => {
62
63
  tombstoneOperations(existing.session_id, new Date().toISOString());
63
64
  }).immediate();
64
65
  throw new SessionLaunchOperationTargetDeletedError(pluginId, scopeId, operationId, existing.session_id);
@@ -81,7 +82,7 @@ export function ensurePluginSessionLaunch(input) {
81
82
  const rawRequestHash = input.rawRequestHash
82
83
  ? requiredRequestHash(input.rawRequestHash)
83
84
  : requestHash;
84
- const ensure = db().transaction(() => {
85
+ const ensure = transaction(db(), () => {
85
86
  const existing = readOperation(pluginId, scopeId, operationId);
86
87
  if (existing) {
87
88
  if (existing.request_hash !== requestHash &&
@@ -139,14 +140,14 @@ function readOperation(pluginId, scopeId, operationId) {
139
140
  .get(pluginId, scopeId, operationId);
140
141
  }
141
142
  function sessionExists(sessionId) {
142
- return Boolean(db().prepare("SELECT 1 FROM sessions WHERE id = ?").pluck().get(sessionId));
143
+ return Boolean(scalar(db().prepare("SELECT 1 FROM sessions WHERE id = ?")).get(sessionId));
143
144
  }
144
145
  function tombstoneOperations(sessionId, now) {
145
- return db()
146
+ return Number(db()
146
147
  .prepare(`UPDATE plugin_session_launch_operations
147
148
  SET state = 'deleted', updated_at = ?, deleted_at = ?
148
149
  WHERE session_id = ? AND state = 'active'`)
149
- .run(now, now, sessionId).changes;
150
+ .run(now, now, sessionId).changes);
150
151
  }
151
152
  function requiredId(value, label) {
152
153
  const normalized = value?.trim();
@@ -1,4 +1,5 @@
1
1
  import { db } from "./db.js";
2
+ import { transaction } from "./sqlite.js";
2
3
  function toItem(row) {
3
4
  return {
4
5
  id: row.id,
@@ -21,7 +22,7 @@ export class SqliteSessionLogStore {
21
22
  (id, session_id, position, response_id, type, status, data, created_by, created_at)
22
23
  VALUES (@id, @session_id, @position, @response_id, @type, @status, @data, @created_by, @created_at)`);
23
24
  const maxStmt = conn.prepare("SELECT MAX(position) AS max FROM session_items WHERE session_id = ?");
24
- const tx = conn.transaction((batch) => {
25
+ const tx = transaction(conn, (batch) => {
25
26
  const { max } = maxStmt.get(sessionId);
26
27
  let next = (max ?? -1) + 1;
27
28
  return batch.map((item) => {
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { normalizeSessionTitle, resolvedExecutionSnapshotSchema, sessionWorkspaceSnapshotSchema, } from "@rynx-ai/core";
15
15
  import { db } from "./db.js";
16
+ import { transaction } from "./sqlite.js";
16
17
  const COLS = "id, workspace, execution, title, forked_from_session_id, source, created_at, updated_at";
17
18
  export function listSessionMetas() {
18
19
  return db()
@@ -46,7 +47,7 @@ export function createSessionMeta(meta) {
46
47
  export function setSessionTitle(id, title) {
47
48
  db()
48
49
  .prepare("UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?")
49
- .run(normalizeSessionTitle(title), new Date().toISOString(), id);
50
+ .run(normalizeSessionTitle(title) ?? null, new Date().toISOString(), id);
50
51
  }
51
52
  export function setSessionExecution(id, execution) {
52
53
  db()
@@ -54,7 +55,7 @@ export function setSessionExecution(id, execution) {
54
55
  .run(JSON.stringify(resolvedExecutionSnapshotSchema.parse(execution)), new Date().toISOString(), id);
55
56
  }
56
57
  export function removeSessionMeta(id) {
57
- db().transaction(() => {
58
+ transaction(db(), () => {
58
59
  const now = new Date().toISOString();
59
60
  db()
60
61
  .prepare(`UPDATE plugin_session_launch_operations
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
2
2
  import { db } from "./db.js";
3
+ import { transaction } from "./sqlite.js";
3
4
  const TICKET_TTL_MS = 5 * 60 * 1_000;
4
5
  const GRANT_TTL_MS = 30 * 60 * 1_000;
5
6
  /** Issue a browser handoff into the native Session workbench. The Host knows
@@ -24,7 +25,7 @@ export function issueSessionPortalTicket(input) {
24
25
  /** Atomically consume a one-use ticket and mint a fixed-lifetime browser grant. */
25
26
  export function redeemSessionPortalTicket(ticket, now = new Date()) {
26
27
  const connection = db();
27
- return connection.transaction(() => {
28
+ return transaction(connection, () => {
28
29
  const ticketHash = hash(ticket);
29
30
  const row = connection.prepare(`SELECT plugin_id, scope, access, session_id, expires_at, used_at
30
31
  FROM session_portal_tickets
@@ -5,6 +5,7 @@ import { rynxHome, } from "@rynx-ai/core";
5
5
  import { defaultSessionResourcePolicy, SESSION_RESOURCE_MAX_IMAGE_PIXELS, SESSION_RESOURCE_MAX_IMAGE_SIDE, SESSION_RESOURCE_MAX_MESSAGE_BYTES, SESSION_RESOURCE_TRANSFER_CHUNK_BYTES, } from "@rynx-ai/protocol/remote-runtime-rpc";
6
6
  import { MachineSessionServiceFailure, } from "@rynx-ai/server";
7
7
  import { db } from "./db.js";
8
+ import { transaction } from "./sqlite.js";
8
9
  const MAX_STAGED_RESOURCES_PER_SESSION = 16;
9
10
  const STAGED_TTL_MS = 24 * 60 * 60 * 1_000;
10
11
  export class SqliteSessionResourceStore {
@@ -244,7 +245,7 @@ export class SqliteSessionResourceStore {
244
245
  rmSync(this.partialPath(row), { force: true });
245
246
  rmSync(this.finalPath(row), { force: true });
246
247
  }
247
- db().transaction(() => {
248
+ transaction(db(), () => {
248
249
  db().prepare("DELETE FROM session_message_operations WHERE session_id = ?").run(sessionId);
249
250
  db().prepare("DELETE FROM session_resources WHERE session_id = ?").run(sessionId);
250
251
  })();
@@ -252,7 +253,7 @@ export class SqliteSessionResourceStore {
252
253
  }
253
254
  prepareMessageOperation(sessionId, clientMessageId, requestHash, resourceIds) {
254
255
  const conn = db();
255
- return conn.transaction(() => {
256
+ return transaction(conn, () => {
256
257
  const existing = conn.prepare(`SELECT request_hash, state
257
258
  FROM session_message_operations
258
259
  WHERE session_id = ? AND client_message_id = ?`).get(sessionId, clientMessageId);
@@ -0,0 +1,22 @@
1
+ import { type DatabaseSync, type StatementSync } from "node:sqlite";
2
+ type SQLiteInput = null | number | bigint | string | NodeJS.ArrayBufferView;
3
+ type SQLiteParameter = SQLiteInput | Record<string, SQLiteInput>;
4
+ type SQLiteOutput = null | number | bigint | string | Uint8Array;
5
+ export type SQLiteTransaction<Args extends unknown[], Result> = ((...args: Args) => Result) & {
6
+ deferred: (...args: Args) => Result;
7
+ immediate: (...args: Args) => Result;
8
+ exclusive: (...args: Args) => Result;
9
+ };
10
+ /** Wrap a synchronous operation in a SQLite transaction. Nested calls use savepoints. */
11
+ export declare function transaction<Args extends unknown[], Result>(connection: DatabaseSync, operation: (...args: Args) => Result): SQLiteTransaction<Args, Result>;
12
+ /** Return only the first column from a native statement, matching scalar-query intent. */
13
+ export declare function scalar(statement: StatementSync): {
14
+ get(...parameters: SQLiteParameter[]): SQLiteOutput | undefined;
15
+ all(...parameters: SQLiteParameter[]): SQLiteOutput[];
16
+ };
17
+ /** Match a primary SQLite result code, including extended result codes. */
18
+ export declare function hasSqlitePrimaryCode(error: unknown, primaryCode: number): boolean;
19
+ export declare const SQLITE_BUSY = 5;
20
+ export declare const SQLITE_LOCKED = 6;
21
+ export declare const SQLITE_CONSTRAINT = 19;
22
+ export {};
package/dist/sqlite.js ADDED
@@ -0,0 +1,57 @@
1
+ let nextSavepointId = 0;
2
+ /** Wrap a synchronous operation in a SQLite transaction. Nested calls use savepoints. */
3
+ export function transaction(connection, operation) {
4
+ const run = (mode, args) => {
5
+ if (connection.isTransaction) {
6
+ const savepoint = `rynx_transaction_${++nextSavepointId}`;
7
+ connection.exec(`SAVEPOINT ${savepoint}`);
8
+ try {
9
+ const result = operation(...args);
10
+ connection.exec(`RELEASE SAVEPOINT ${savepoint}`);
11
+ return result;
12
+ }
13
+ catch (error) {
14
+ if (connection.isTransaction) {
15
+ connection.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
16
+ connection.exec(`RELEASE SAVEPOINT ${savepoint}`);
17
+ }
18
+ throw error;
19
+ }
20
+ }
21
+ connection.exec(`BEGIN ${mode}`);
22
+ try {
23
+ const result = operation(...args);
24
+ connection.exec("COMMIT");
25
+ return result;
26
+ }
27
+ catch (error) {
28
+ if (connection.isTransaction)
29
+ connection.exec("ROLLBACK");
30
+ throw error;
31
+ }
32
+ };
33
+ const wrapped = ((...args) => run("DEFERRED", args));
34
+ wrapped.deferred = (...args) => run("DEFERRED", args);
35
+ wrapped.immediate = (...args) => run("IMMEDIATE", args);
36
+ wrapped.exclusive = (...args) => run("EXCLUSIVE", args);
37
+ return wrapped;
38
+ }
39
+ /** Return only the first column from a native statement, matching scalar-query intent. */
40
+ export function scalar(statement) {
41
+ statement.setReturnArrays(true);
42
+ const arrayStatement = statement;
43
+ return {
44
+ get: (...parameters) => arrayStatement.get(...parameters)?.[0],
45
+ all: (...parameters) => arrayStatement.all(...parameters).map((row) => row[0]),
46
+ };
47
+ }
48
+ /** Match a primary SQLite result code, including extended result codes. */
49
+ export function hasSqlitePrimaryCode(error, primaryCode) {
50
+ if (!error || typeof error !== "object" || !("errcode" in error))
51
+ return false;
52
+ const errcode = error.errcode;
53
+ return typeof errcode === "number" && (errcode & 0xff) === primaryCode;
54
+ }
55
+ export const SQLITE_BUSY = 5;
56
+ export const SQLITE_LOCKED = 6;
57
+ export const SQLITE_CONSTRAINT = 19;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/daemon",
3
- "version": "0.1.11-beta.11",
3
+ "version": "0.1.11-beta.12",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -9,7 +9,7 @@
9
9
  "description": "Rynx resident control-plane service and standalone lifecycle implementation.",
10
10
  "type": "module",
11
11
  "engines": {
12
- "node": ">=22"
12
+ "node": ">=22.16"
13
13
  },
14
14
  "publishConfig": {
15
15
  "registry": "https://registry.npmjs.org/",
@@ -42,22 +42,21 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "better-sqlite3": "^12.11.1",
46
45
  "extract-zip": "2.0.1",
47
46
  "pm2": "^6.0.0",
48
47
  "tar": "^7.5.19",
49
48
  "ws": "^8.21.0",
50
- "@rynx-ai/core": "0.1.11-beta.11",
51
- "@rynx-ai/emulator": "0.1.11-beta.11",
52
- "@rynx-ai/plugin-runner": "0.1.11-beta.11",
53
- "@rynx-ai/plugin-sdk": "0.1.11-beta.11",
54
- "@rynx-ai/protocol": "0.1.11-beta.11",
55
- "@rynx-ai/remote-runtime-client": "0.1.11-beta.11",
56
- "@rynx-ai/server": "0.1.11-beta.11"
49
+ "@rynx-ai/core": "0.1.11-beta.12",
50
+ "@rynx-ai/plugin-runner": "0.1.11-beta.12",
51
+ "@rynx-ai/plugin-sdk": "0.1.11-beta.12",
52
+ "@rynx-ai/protocol": "0.1.11-beta.12",
53
+ "@rynx-ai/remote-runtime-client": "0.1.11-beta.12",
54
+ "@rynx-ai/emulator": "0.1.11-beta.12",
55
+ "@rynx-ai/server": "0.1.11-beta.12"
57
56
  },
58
57
  "devDependencies": {
59
58
  "@types/ws": "^8.18.1",
60
- "@rynx-ai/plugin-channel-lark": "0.1.11-beta.11"
59
+ "@rynx-ai/plugin-channel-lark": "0.1.11-beta.12"
61
60
  },
62
61
  "scripts": {
63
62
  "build": "rm -rf dist bundled-plugins && tsc -p tsconfig.json && chmod +x dist/index-daemon.js && node ../../scripts/stage-bundled-plugins.mjs",