@rynx-ai/daemon 0.1.11-beta.2 → 0.1.11-beta.21

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.
Files changed (88) hide show
  1. package/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +0 -12
  2. package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +3 -1
  3. package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +3 -3
  4. package/bundled-plugins/plugins/lark/dist/host-adapters.js +20 -18
  5. package/bundled-plugins/plugins/lark/dist/lark.js +18 -12
  6. package/bundled-plugins/plugins/lark/dist/runtime.js +6046 -6022
  7. package/bundled-plugins/plugins/lark/dist/settings.js +6006 -5988
  8. package/bundled-plugins/plugins/lark/package.json +1 -1
  9. package/bundled-plugins/plugins/lark/ui/assets/index-CGUVAMsv.css +1 -0
  10. package/bundled-plugins/plugins/lark/ui/index.html +2 -2
  11. package/dist/agent-file.js +0 -1
  12. package/dist/browser-artifact-management.d.ts +4 -1
  13. package/dist/browser-artifact-management.js +22 -11
  14. package/dist/browser-artifacts.d.ts +1 -0
  15. package/dist/browser-artifacts.js +1 -0
  16. package/dist/chrome-for-testing-runtime.d.ts +27 -0
  17. package/dist/chrome-for-testing-runtime.js +194 -0
  18. package/dist/chrome-for-testing-store.d.ts +2 -0
  19. package/dist/chrome-for-testing-store.js +37 -6
  20. package/dist/control-deps.d.ts +6 -2
  21. package/dist/control-deps.js +87 -4
  22. package/dist/control-link-store.js +4 -3
  23. package/dist/daemon-owner.js +4 -7
  24. package/dist/daemon-server.d.ts +4 -1
  25. package/dist/daemon-server.js +158 -45
  26. package/dist/db.d.ts +2 -2
  27. package/dist/db.js +55 -20
  28. package/dist/entry-path.d.ts +3 -0
  29. package/dist/entry-path.js +12 -0
  30. package/dist/index-daemon.js +3 -87
  31. package/dist/lifecycle.d.ts +13 -0
  32. package/dist/lifecycle.js +19 -0
  33. package/dist/maintenance-lease.d.ts +39 -0
  34. package/dist/maintenance-lease.js +250 -0
  35. package/dist/migrations/cleanup-legacy-sessions.js +4 -3
  36. package/dist/plugin-cli.d.ts +10 -0
  37. package/dist/plugin-cli.js +19 -2
  38. package/dist/plugin-host-rpc.d.ts +35 -6
  39. package/dist/plugin-host-rpc.js +222 -59
  40. package/dist/plugin-installer.js +62 -2
  41. package/dist/plugin-management-service.js +0 -2
  42. package/dist/plugin-marketplace.js +2 -1
  43. package/dist/plugin-store.js +2 -4
  44. package/dist/plugin-supervisor.d.ts +5 -0
  45. package/dist/plugin-supervisor.js +218 -33
  46. package/dist/pm2.d.ts +11 -2
  47. package/dist/pm2.js +116 -19
  48. package/dist/process.d.ts +12 -0
  49. package/dist/process.js +91 -0
  50. package/dist/remote-runtime-access-store.d.ts +2 -2
  51. package/dist/remote-runtime-access-store.js +6 -9
  52. package/dist/remote-runtime-target-store.d.ts +2 -2
  53. package/dist/remote-runtime-target-store.js +6 -6
  54. package/dist/session-emulator-binding-store.js +4 -6
  55. package/dist/session-fork-store.js +24 -8
  56. package/dist/session-launch-operations.d.ts +10 -1
  57. package/dist/session-launch-operations.js +21 -11
  58. package/dist/session-log-store.js +2 -1
  59. package/dist/session-meta-store.d.ts +5 -0
  60. package/dist/session-meta-store.js +25 -2
  61. package/dist/session-portal-grant-store.d.ts +29 -0
  62. package/dist/session-portal-grant-store.js +85 -0
  63. package/dist/session-resource-store.js +3 -2
  64. package/dist/setup-service.d.ts +21 -0
  65. package/dist/setup-service.js +39 -0
  66. package/dist/skills-catalog.d.ts +11 -2
  67. package/dist/skills-catalog.js +307 -10
  68. package/dist/sqlite.d.ts +22 -0
  69. package/dist/sqlite.js +57 -0
  70. package/dist/system-dependencies.d.ts +22 -0
  71. package/dist/system-dependencies.js +79 -0
  72. package/package.json +37 -21
  73. package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +0 -1
  74. package/dist/browser-cli-args.d.ts +0 -20
  75. package/dist/browser-cli-args.js +0 -100
  76. package/dist/cli.d.ts +0 -2
  77. package/dist/cli.js +0 -550
  78. package/dist/control-client.d.ts +0 -122
  79. package/dist/control-client.js +0 -855
  80. package/dist/control-endpoint.d.ts +0 -14
  81. package/dist/control-endpoint.js +0 -124
  82. package/dist/desktop-browser-host-client.d.ts +0 -35
  83. package/dist/desktop-browser-host-client.js +0 -415
  84. package/dist/setup.d.ts +0 -20
  85. package/dist/setup.js +0 -386
  86. package/dist/update.d.ts +0 -25
  87. package/dist/update.js +0 -133
  88. /package/bundled-plugins/plugins/lark/ui/assets/{index-D9N-AkLX.js → index-D8Amoe6a.js} +0 -0
package/dist/pm2.js CHANGED
@@ -10,14 +10,18 @@
10
10
  import { spawn, spawnSync } from "node:child_process";
11
11
  import { mkdirSync, realpathSync } from "node:fs";
12
12
  import { createRequire } from "node:module";
13
- import { dirname, join, resolve } from "node:path";
14
- import { fileURLToPath } from "node:url";
13
+ import { userInfo } from "node:os";
14
+ import { delimiter, isAbsolute, join, resolve } from "node:path";
15
15
  import { loadConfig, rynxHome } from "@rynx-ai/core";
16
16
  import { daemonEntryRestartRequired } from "./daemon-build-status.js";
17
+ import { resolveDaemonEntry } from "./entry-path.js";
17
18
  const require = createRequire(import.meta.url);
18
19
  /** pm2 app name — single resident daemon per machine. */
19
20
  const APP_NAME = "rynx";
20
21
  const PM2_COMMAND_TIMEOUT_MS = 5_000;
22
+ const LOGIN_SHELL_PATH_TIMEOUT_MS = 5_000;
23
+ const LOGIN_SHELL_PATH_MAX_BYTES = 256 * 1024;
24
+ const REFRESH_LOGIN_SHELL_PATH_ENV = "RYNX_REFRESH_LOGIN_SHELL_PATH";
21
25
  const DAEMON_PACKAGE_VERSION = require("../package.json").version;
22
26
  /**
23
27
  * Build a dialable control-console URL from the daemon's bind host/port. Bind-all
@@ -49,10 +53,6 @@ function pm2Bin() {
49
53
  // pm2 ships its CLI at bin/pm2; run it with our own node.
50
54
  return require.resolve("pm2/bin/pm2");
51
55
  }
52
- /** `index-daemon.js` sits next to this file in `dist/`. */
53
- function daemonEntryPath() {
54
- return join(dirname(fileURLToPath(import.meta.url)), "index-daemon.js");
55
- }
56
56
  function logsDir() {
57
57
  const dir = join(rynxHome(), "logs");
58
58
  mkdirSync(dir, { recursive: true });
@@ -71,11 +71,58 @@ function nodeRuntimeEnv() {
71
71
  : {}),
72
72
  };
73
73
  }
74
+ /**
75
+ * A detached plugin updater intentionally receives a reduced environment and
76
+ * therefore has no SHELL. Starting the replacement daemon with that reduced
77
+ * PATH would make user-installed runtimes (for example an fnm-managed Codex)
78
+ * disappear after every upgrade. Recover the user's login-shell PATH only at
79
+ * this boundary; normal interactive starts keep their exact caller environment.
80
+ */
81
+ function daemonStartEnv() {
82
+ const env = nodeRuntimeEnv();
83
+ const refreshLoginPath = env[REFRESH_LOGIN_SHELL_PATH_ENV] === "1";
84
+ delete env[REFRESH_LOGIN_SHELL_PATH_ENV];
85
+ if (!refreshLoginPath)
86
+ return env;
87
+ const shell = userInfo().shell?.trim();
88
+ if (!shell) {
89
+ throw new Error("cannot refresh daemon PATH: the user login shell is unavailable");
90
+ }
91
+ const result = spawnSync(shell, ["-lic", `printf '\\036%s\\037' "$PATH"`], {
92
+ encoding: "utf8",
93
+ // Do not feed an fnm/nvm "multishell" directory inherited from the old
94
+ // daemon back into the next login shell. Version managers prepend a new
95
+ // directory on every shell startup, so reusing the old PATH would grow
96
+ // it indefinitely across upgrades.
97
+ env: { ...env, SHELL: shell, PATH: loginShellBaselinePath() },
98
+ timeout: LOGIN_SHELL_PATH_TIMEOUT_MS,
99
+ killSignal: "SIGTERM",
100
+ maxBuffer: LOGIN_SHELL_PATH_MAX_BYTES,
101
+ });
102
+ if (result.error) {
103
+ throw new Error(`cannot refresh daemon PATH: ${result.error.message}`);
104
+ }
105
+ if (result.status !== 0) {
106
+ throw new Error(`cannot refresh daemon PATH: login shell exited ${result.status ?? "without status"}`);
107
+ }
108
+ const path = extractFramedPath(result.stdout);
109
+ if (!path) {
110
+ throw new Error("cannot refresh daemon PATH: login shell returned no framed PATH");
111
+ }
112
+ const npmPrefix = env.NPM_CONFIG_PREFIX?.trim();
113
+ const npmBin = npmPrefix && isAbsolute(npmPrefix)
114
+ ? join(npmPrefix, "bin")
115
+ : "";
116
+ const refreshedPath = npmBin === ""
117
+ ? path
118
+ : [npmBin, ...path.split(delimiter).filter((entry) => entry !== npmBin)].join(delimiter);
119
+ return { ...env, PATH: refreshedPath, SHELL: shell };
120
+ }
74
121
  /** Run a pm2 command inheriting stdio; returns its exit code. */
75
- function runPm2(args, quiet = false) {
122
+ function runPm2(args, quiet = false, env = nodeRuntimeEnv()) {
76
123
  const res = spawnSync(process.execPath, [pm2Bin(), ...args], {
77
124
  stdio: quiet ? "ignore" : "inherit",
78
- env: nodeRuntimeEnv(),
125
+ env,
79
126
  timeout: PM2_COMMAND_TIMEOUT_MS,
80
127
  killSignal: "SIGTERM",
81
128
  });
@@ -117,7 +164,7 @@ function pm2ErrorMessage(error) {
117
164
  * entry from scratch (delete + start) so config flags are always fresh; the
118
165
  * `force === false` path short-circuits when it's already online.
119
166
  */
120
- export function startDaemon({ force, quiet = false }) {
167
+ export function startDaemon({ force, quiet = false, launchEnvironment, }) {
121
168
  let existing;
122
169
  try {
123
170
  existing = describe();
@@ -139,6 +186,20 @@ export function startDaemon({ force, quiet = false }) {
139
186
  if (staleBuild && !quiet) {
140
187
  console.log("A newer daemon build is present on disk; restarting Rynx.");
141
188
  }
189
+ let startEnv;
190
+ try {
191
+ // Resolve the replacement environment before deleting an existing app.
192
+ // A broken/hung login shell must leave the known-good daemon untouched.
193
+ startEnv = {
194
+ ...daemonStartEnv(),
195
+ ...launchEnvironment,
196
+ };
197
+ }
198
+ catch (error) {
199
+ if (!quiet)
200
+ console.error(error instanceof Error ? error.message : String(error));
201
+ return 1;
202
+ }
142
203
  const dir = logsDir();
143
204
  if (existing) {
144
205
  const removed = runPm2(["delete", APP_NAME], quiet);
@@ -147,7 +208,7 @@ export function startDaemon({ force, quiet = false }) {
147
208
  }
148
209
  const status = runPm2([
149
210
  "start",
150
- daemonEntryPath(),
211
+ resolveDaemonEntry(),
151
212
  "--name",
152
213
  APP_NAME,
153
214
  // App and CLI share one daemon-owned state/workspace root.
@@ -163,7 +224,7 @@ export function startDaemon({ force, quiet = false }) {
163
224
  join(dir, "err.log"),
164
225
  "--merge-logs",
165
226
  "--time",
166
- ], quiet);
227
+ ], quiet, startEnv);
167
228
  if (status === 0 && !quiet) {
168
229
  console.log(`rynx ${force ? "restarted" : "started"} (pm2 app "${APP_NAME}", logs: ${dir}).`);
169
230
  console.log(`Control console: ${styleUrl(controlConsoleUrl())}`);
@@ -173,16 +234,35 @@ export function startDaemon({ force, quiet = false }) {
173
234
  export function isPm2DaemonOnline() {
174
235
  return describe()?.pm2_env?.status === "online";
175
236
  }
176
- /** Stop and remove the daemon from pm2's process list. */
237
+ /**
238
+ * Stop the daemon and terminate its dedicated pm2 supervisor.
239
+ *
240
+ * PM2_HOME lives below RYNX_HOME, so this supervisor cannot own unrelated
241
+ * applications. Leaving it alive across an npm replacement is unsafe: the
242
+ * daemon process container path is resolved inside the old pm2 package. Once
243
+ * npm swaps @rynx-ai/daemon, that cached path may no longer exist and every
244
+ * subsequent `rynx start` enters a restart loop before our code can run.
245
+ */
177
246
  export function stopDaemon() {
178
- if (!describe()) {
247
+ const existing = describe();
248
+ if (!existing) {
249
+ // Even an empty process list can belong to a resident PM2 daemon whose
250
+ // module path is about to be replaced. `pm2 kill` is idempotent and is
251
+ // the only reliable package-upgrade boundary.
252
+ const killed = runPm2(["kill"], true);
253
+ if (killed !== 0)
254
+ return killed;
179
255
  console.log("rynx is not running.");
180
256
  return 0;
181
257
  }
182
- const status = runPm2(["delete", APP_NAME]);
183
- if (status === 0)
184
- console.log("rynx stopped.");
185
- return status;
258
+ const removed = runPm2(["delete", APP_NAME]);
259
+ if (removed !== 0)
260
+ return removed;
261
+ const killed = runPm2(["kill"]);
262
+ if (killed !== 0)
263
+ return killed;
264
+ console.log("rynx stopped.");
265
+ return 0;
186
266
  }
187
267
  /** Print a one-line status; exit 0 only when online. */
188
268
  export function statusDaemon() {
@@ -209,7 +289,7 @@ export function streamLogs() {
209
289
  }
210
290
  function residentBuildRestartRequired(app) {
211
291
  const runningEntry = app.pm2_env?.pm_exec_path;
212
- if (runningEntry && !sameEntryPath(runningEntry, daemonEntryPath()))
292
+ if (runningEntry && !sameEntryPath(runningEntry, resolveDaemonEntry()))
213
293
  return true;
214
294
  const runningVersion = app.pm2_env?.version;
215
295
  if (runningVersion &&
@@ -220,7 +300,7 @@ function residentBuildRestartRequired(app) {
220
300
  const startedAt = app.pm2_env?.pm_uptime;
221
301
  return typeof startedAt === "number" &&
222
302
  Number.isFinite(startedAt) &&
223
- daemonEntryRestartRequired(daemonEntryPath(), startedAt);
303
+ daemonEntryRestartRequired(resolveDaemonEntry(), startedAt);
224
304
  }
225
305
  function sameEntryPath(left, right) {
226
306
  try {
@@ -230,3 +310,20 @@ function sameEntryPath(left, right) {
230
310
  return resolve(left) === resolve(right);
231
311
  }
232
312
  }
313
+ function extractFramedPath(stdout) {
314
+ if (!stdout)
315
+ return undefined;
316
+ const start = stdout.lastIndexOf("\x1e");
317
+ if (start < 0)
318
+ return undefined;
319
+ const end = stdout.indexOf("\x1f", start + 1);
320
+ if (end < 0)
321
+ return undefined;
322
+ const value = stdout.slice(start + 1, end);
323
+ return value.trim() ? value : undefined;
324
+ }
325
+ function loginShellBaselinePath() {
326
+ return process.platform === "darwin"
327
+ ? "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
328
+ : "/usr/local/bin:/usr/bin:/bin";
329
+ }
@@ -0,0 +1,12 @@
1
+ interface SupervisorProcess {
2
+ connected?: boolean;
3
+ disconnect?: () => void;
4
+ }
5
+ /** Release a parent/supervisor IPC handle after graceful daemon shutdown. */
6
+ export declare function disconnectSupervisorIpc(target?: SupervisorProcess): void;
7
+ /**
8
+ * Run the daemon in the current process. Process ownership and executable
9
+ * selection belong to the caller (CLI standalone lifecycle or App lifecycle).
10
+ */
11
+ export declare function runDaemonProcess(): Promise<void>;
12
+ export {};
@@ -0,0 +1,91 @@
1
+ import { startRynxDaemonServer } from "./daemon-server.js";
2
+ import { ensureBundledOfficialPlugins } from "./bundled-plugins.js";
3
+ import { installStdioEpipeGuard } from "./stdio-epipe-guard.js";
4
+ /** Release a parent/supervisor IPC handle after graceful daemon shutdown. */
5
+ export function disconnectSupervisorIpc(target = process) {
6
+ if (target.connected && typeof target.disconnect === "function") {
7
+ target.disconnect();
8
+ }
9
+ }
10
+ /**
11
+ * Run the daemon in the current process. Process ownership and executable
12
+ * selection belong to the caller (CLI standalone lifecycle or App lifecycle).
13
+ */
14
+ export async function runDaemonProcess() {
15
+ installStdioEpipeGuard();
16
+ const followsApp = process.env.RYNX_DAEMON_LIFECYCLE === "follow-app";
17
+ let parentDisconnected = followsApp && !process.connected;
18
+ const recordParentDisconnect = () => {
19
+ parentDisconnected = true;
20
+ };
21
+ if (followsApp)
22
+ process.once("disconnect", recordParentDisconnect);
23
+ const preparePlugins = async () => {
24
+ try {
25
+ const bundled = await ensureBundledOfficialPlugins({ reclaimHostLeases: true });
26
+ if (bundled.status === "ready") {
27
+ console.log(JSON.stringify({
28
+ level: "info",
29
+ type: "bundled_plugins_ready",
30
+ installed: bundled.installed,
31
+ updated: bundled.updated,
32
+ }));
33
+ }
34
+ }
35
+ catch (error) {
36
+ console.error(JSON.stringify({
37
+ level: "error",
38
+ type: "bundled_plugins_failed",
39
+ error: error instanceof Error ? error.message : String(error),
40
+ }));
41
+ }
42
+ };
43
+ let server;
44
+ try {
45
+ server = await startRynxDaemonServer({
46
+ preparePlugins,
47
+ onShutdownComplete: () => disconnectSupervisorIpc(),
48
+ });
49
+ }
50
+ catch (error) {
51
+ process.off("disconnect", recordParentDisconnect);
52
+ throw error;
53
+ }
54
+ let shutdownPromise;
55
+ const shutdown = (signal) => {
56
+ if (shutdownPromise)
57
+ return shutdownPromise;
58
+ process.off("SIGINT", onSigint);
59
+ process.off("SIGTERM", onSigterm);
60
+ process.off("disconnect", onDisconnect);
61
+ shutdownPromise = (async () => {
62
+ console.log(JSON.stringify({ level: "info", type: "shutdown", signal }));
63
+ await server.shutdown();
64
+ })();
65
+ shutdownPromise.catch((error) => {
66
+ process.exitCode = 1;
67
+ console.error(`Shutdown failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
68
+ });
69
+ return shutdownPromise;
70
+ };
71
+ const onSigint = () => {
72
+ void shutdown("SIGINT");
73
+ };
74
+ const onSigterm = () => {
75
+ void shutdown("SIGTERM");
76
+ };
77
+ const onDisconnect = () => {
78
+ if (process.env.RYNX_DAEMON_LIFECYCLE === "follow-app") {
79
+ void shutdown("APP_DISCONNECT");
80
+ }
81
+ };
82
+ process.once("SIGINT", onSigint);
83
+ process.once("SIGTERM", onSigterm);
84
+ if (followsApp) {
85
+ process.once("disconnect", onDisconnect);
86
+ process.off("disconnect", recordParentDisconnect);
87
+ if (parentDisconnected || !process.connected) {
88
+ void shutdown("APP_DISCONNECT");
89
+ }
90
+ }
91
+ }
@@ -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();
@@ -9,13 +9,22 @@ export interface EnsurePluginSessionLaunchInput {
9
9
  operationId: string;
10
10
  /** SHA-256 of the host-normalized, execution-affecting launch request. */
11
11
  requestHash: string;
12
+ /** SHA-256 before mutable filesystem selectors are canonicalized. */
13
+ rawRequestHash?: string;
12
14
  session: PluginSessionLaunchMeta;
13
15
  }
14
16
  export interface EnsurePluginSessionLaunchResult {
15
17
  sessionId: string;
16
18
  disposition: "created" | "replayed";
17
19
  }
18
- export type ReplayPluginSessionLaunchInput = Omit<EnsurePluginSessionLaunchInput, "session">;
20
+ export type ReplayPluginSessionLaunchInput = Omit<EnsurePluginSessionLaunchInput, "session"> & {
21
+ /**
22
+ * A caller doing a pre-canonicalization lookup may treat a hash mismatch as
23
+ * a cache miss, then retry with the canonical hash. The canonical lookup
24
+ * must keep the default strict conflict behavior.
25
+ */
26
+ allowRequestHashMismatch?: boolean;
27
+ };
19
28
  export declare class SessionLaunchOperationConflictError extends Error {
20
29
  readonly pluginId: string;
21
30
  readonly scopeId: string;