@rynx-ai/daemon 0.1.11-beta.4 → 0.1.11-beta.41

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 (72) 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 +24 -18
  5. package/bundled-plugins/plugins/lark/dist/lark.js +18 -12
  6. package/bundled-plugins/plugins/lark/dist/runtime.js +6043 -6016
  7. package/bundled-plugins/plugins/lark/dist/settings.js +6003 -5985
  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.js +4 -1
  13. package/dist/chrome-for-testing-http.d.ts +9 -0
  14. package/dist/chrome-for-testing-http.js +58 -0
  15. package/dist/chrome-for-testing-release-resolver.js +13 -6
  16. package/dist/chrome-for-testing-runtime.js +4 -3
  17. package/dist/chrome-for-testing-store.d.ts +2 -0
  18. package/dist/chrome-for-testing-store.js +99 -17
  19. package/dist/control-deps.d.ts +6 -2
  20. package/dist/control-deps.js +87 -4
  21. package/dist/control-link-store.js +4 -3
  22. package/dist/daemon-owner.js +4 -7
  23. package/dist/daemon-server.js +42 -17
  24. package/dist/db.d.ts +2 -2
  25. package/dist/db.js +157 -21
  26. package/dist/headless-browser-host.js +397 -41
  27. package/dist/lifecycle.d.ts +2 -1
  28. package/dist/lifecycle.js +4 -1
  29. package/dist/migrations/cleanup-legacy-sessions.js +4 -3
  30. package/dist/plugin-cli.d.ts +10 -0
  31. package/dist/plugin-cli.js +19 -2
  32. package/dist/plugin-host-rpc.d.ts +48 -9
  33. package/dist/plugin-host-rpc.js +593 -81
  34. package/dist/plugin-installer.js +3 -2
  35. package/dist/plugin-management-service.js +0 -2
  36. package/dist/plugin-marketplace.js +2 -1
  37. package/dist/plugin-store.js +2 -4
  38. package/dist/plugin-supervisor.js +0 -4
  39. package/dist/pm2.d.ts +2 -0
  40. package/dist/pm2.js +6 -0
  41. package/dist/remote-runtime-access-store.d.ts +2 -2
  42. package/dist/remote-runtime-access-store.js +6 -9
  43. package/dist/remote-runtime-connection-manager.js +25 -42
  44. package/dist/remote-runtime-target-store.d.ts +2 -2
  45. package/dist/remote-runtime-target-store.js +6 -6
  46. package/dist/session-emulator-binding-store.js +4 -6
  47. package/dist/session-fork-store.d.ts +1 -0
  48. package/dist/session-fork-store.js +85 -30
  49. package/dist/session-launch-operations.js +6 -5
  50. package/dist/session-log-store.d.ts +1 -0
  51. package/dist/session-log-store.js +46 -22
  52. package/dist/session-meta-store.d.ts +5 -0
  53. package/dist/session-meta-store.js +25 -2
  54. package/dist/session-portal-grant-store.d.ts +55 -0
  55. package/dist/session-portal-grant-store.js +160 -0
  56. package/dist/session-resource-store.d.ts +24 -6
  57. package/dist/session-resource-store.js +387 -69
  58. package/dist/session-timeline-projection-store.d.ts +24 -0
  59. package/dist/session-timeline-projection-store.js +53 -0
  60. package/dist/setup-service.d.ts +1 -0
  61. package/dist/setup-service.js +1 -0
  62. package/dist/skills-catalog.d.ts +11 -2
  63. package/dist/skills-catalog.js +317 -10
  64. package/dist/sqlite.d.ts +22 -0
  65. package/dist/sqlite.js +57 -0
  66. package/dist/stdio-epipe-guard.d.ts +1 -1
  67. package/dist/stdio-epipe-guard.js +1 -1
  68. package/dist/system-dependencies.d.ts +22 -0
  69. package/dist/system-dependencies.js +79 -0
  70. package/package.json +17 -12
  71. package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +0 -1
  72. /package/bundled-plugins/plugins/lark/ui/assets/{index-D9N-AkLX.js → index-D8Amoe6a.js} +0 -0
@@ -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;
@@ -295,7 +295,6 @@ function toManagementItem(record, settingsAvailable = false, updateState = "unkn
295
295
  enabled: record.enabled,
296
296
  runtimeState: record.manifest.runtime ? record.runtimeState : "absent",
297
297
  ...(record.runtimeError ? { runtimeError: record.runtimeError } : {}),
298
- capabilities: [...record.manifest.capabilities],
299
298
  hasSettingsUi: settingsAvailable,
300
299
  updatedAt: record.updatedAt,
301
300
  };
@@ -322,7 +321,6 @@ function toInstallPreparation(token, expiresAtMs, prepared) {
322
321
  subdirectory: prepared.requestedSource.subdir ?? ".",
323
322
  },
324
323
  digest: prepared.integrity,
325
- capabilities: [...prepared.manifest.capabilities],
326
324
  hasRuntime: Boolean(prepared.manifest.runtime),
327
325
  hasSettingsUi: Boolean(prepared.manifest.settings),
328
326
  ...(prepared.manifest.runtime ? {
@@ -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);
@@ -57,11 +57,11 @@ export function upsertPlugin(rec) {
57
57
  db().prepare(`INSERT INTO plugin_installations (
58
58
  canonical_id, marketplace_id, plugin_id, source_type, requested_source,
59
59
  resolved_revision, source_fingerprint, version, enabled, installed_at,
60
- updated_at, install_path, manifest, integrity, capabilities, runtime_state, runtime_error
60
+ updated_at, install_path, manifest, integrity, runtime_state, runtime_error
61
61
  ) VALUES (
62
62
  @canonicalId, @marketplaceId, @pluginId, @source, @requestedSource,
63
63
  @resolvedRevision, @sourceFingerprint, @version, @enabled, @now,
64
- @now, @installPath, @manifest, @integrity, @capabilities, 'stopped', NULL
64
+ @now, @installPath, @manifest, @integrity, 'stopped', NULL
65
65
  )
66
66
  ON CONFLICT(canonical_id) DO UPDATE SET
67
67
  source_type = excluded.source_type,
@@ -74,7 +74,6 @@ export function upsertPlugin(rec) {
74
74
  install_path = excluded.install_path,
75
75
  manifest = excluded.manifest,
76
76
  integrity = excluded.integrity,
77
- capabilities = excluded.capabilities,
78
77
  runtime_state = 'stopped',
79
78
  runtime_error = NULL`).run({
80
79
  canonicalId: rec.canonicalId,
@@ -90,7 +89,6 @@ export function upsertPlugin(rec) {
90
89
  installPath: rec.installPath,
91
90
  manifest: JSON.stringify(rec.manifest),
92
91
  integrity: rec.integrity,
93
- capabilities: JSON.stringify(rec.manifest.capabilities),
94
92
  });
95
93
  }
96
94
  export function setPluginEnabled(identity, enabled) {
@@ -294,7 +294,6 @@ export class PluginSupervisor {
294
294
  daemonInstanceId: this.daemonInstanceId,
295
295
  productVersion: this.productVersion,
296
296
  cliVersion: this.cliVersion,
297
- capabilities: state.record.manifest.capabilities,
298
297
  services: this.services,
299
298
  ...(state.record.pluginId === "workspace"
300
299
  && isBuiltInPluginMarketplace(state.record.marketplaceId)
@@ -324,7 +323,6 @@ export class PluginSupervisor {
324
323
  role: "runtime",
325
324
  dataDir,
326
325
  cacheDir,
327
- capabilities: state.record.manifest.capabilities,
328
326
  handleHostCall: (method, params, signal) => hostRpc.call(method, params, signal),
329
327
  onStderr: (line) => this.log({
330
328
  level: "info",
@@ -411,7 +409,6 @@ export class PluginSupervisor {
411
409
  daemonInstanceId: this.daemonInstanceId,
412
410
  productVersion: this.productVersion,
413
411
  cliVersion: this.cliVersion,
414
- capabilities: state.record.manifest.capabilities,
415
412
  services: this.services,
416
413
  ...(state.record.pluginId === "workspace"
417
414
  && isBuiltInPluginMarketplace(state.record.marketplaceId)
@@ -440,7 +437,6 @@ export class PluginSupervisor {
440
437
  role: "settings",
441
438
  dataDir,
442
439
  cacheDir,
443
- capabilities: state.record.manifest.capabilities,
444
440
  handleHostCall: (method, params, signal) => hostRpc.call(method, params, signal),
445
441
  onStderr: (line) => this.log({
446
442
  level: "info",
package/dist/pm2.d.ts CHANGED
@@ -14,6 +14,8 @@ export declare function startDaemon({ force, quiet, launchEnvironment, }: {
14
14
  launchEnvironment?: NodeJS.ProcessEnv;
15
15
  }): number;
16
16
  export declare function isPm2DaemonOnline(): boolean;
17
+ /** Whether `start` must replace or recreate the resident daemon generation. */
18
+ export declare function isDaemonStartRequired(): boolean;
17
19
  /**
18
20
  * Stop the daemon and terminate its dedicated pm2 supervisor.
19
21
  *
package/dist/pm2.js CHANGED
@@ -234,6 +234,12 @@ export function startDaemon({ force, quiet = false, launchEnvironment, }) {
234
234
  export function isPm2DaemonOnline() {
235
235
  return describe()?.pm2_env?.status === "online";
236
236
  }
237
+ /** Whether `start` must replace or recreate the resident daemon generation. */
238
+ export function isDaemonStartRequired() {
239
+ const existing = describe();
240
+ return existing?.pm2_env?.status !== "online" ||
241
+ residentBuildRestartRequired(existing);
242
+ }
237
243
  /**
238
244
  * Stop the daemon and terminate its dedicated pm2 supervisor.
239
245
  *
@@ -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 });
@@ -977,7 +977,22 @@ export class DaemonRuntimeConnectionManager {
977
977
  return {
978
978
  sessionId: terminal.sessionId,
979
979
  role: terminal.role,
980
+ seedBytes: terminal.seedBytes,
981
+ ...(terminal.dimensions ? { dimensions: terminal.dimensions } : {}),
980
982
  closed: terminal.closed,
983
+ onResize: (listener) => terminal.onResize?.(listener),
984
+ readSeed: (offset, maxBytes) => {
985
+ this.assertSlotCurrent(slot);
986
+ return terminal.readSeed(offset, maxBytes);
987
+ },
988
+ start: () => {
989
+ this.assertSlotCurrent(slot);
990
+ return terminal.start();
991
+ },
992
+ read: (offset, maxBytes) => {
993
+ this.assertSlotCurrent(slot);
994
+ return terminal.read(offset, maxBytes);
995
+ },
981
996
  write: (data) => {
982
997
  this.assertSlotCurrent(slot);
983
998
  return terminal.write(data);
@@ -987,19 +1002,6 @@ export class DaemonRuntimeConnectionManager {
987
1002
  return terminal.resize(cols, rows);
988
1003
  },
989
1004
  close,
990
- [Symbol.asyncIterator]: () => {
991
- const iterator = terminal[Symbol.asyncIterator]();
992
- return {
993
- next: () => {
994
- this.assertSlotCurrent(slot);
995
- return iterator.next();
996
- },
997
- return: async () => {
998
- await close();
999
- return { done: true, value: undefined };
1000
- },
1001
- };
1002
- },
1003
1005
  };
1004
1006
  }
1005
1007
  trackDedicatedBrowserSurface(slot, surface) {
@@ -1214,44 +1216,25 @@ function dedicatedDirectTerminal(connection, terminal, signal) {
1214
1216
  onAbort();
1215
1217
  else
1216
1218
  signal?.addEventListener("abort", onAbort, { once: true });
1217
- const closed = terminal.closed
1218
- .then((frame) => ({
1219
+ const closed = terminal.closed.then((frame) => ({
1219
1220
  reason: frame.reason,
1220
1221
  ...(frame.exitCode === undefined ? {} : { exitCode: frame.exitCode }),
1221
- }))
1222
- .finally(() => {
1223
- signal?.removeEventListener("abort", onAbort);
1224
- void closeConnection().catch(() => undefined);
1225
- });
1222
+ ...(frame.finalOffset === undefined ? {} : { finalOffset: frame.finalOffset }),
1223
+ }));
1226
1224
  void closed.catch(() => undefined);
1227
1225
  return {
1228
1226
  sessionId: terminal.sessionId,
1229
1227
  role: terminal.role,
1228
+ seedBytes: terminal.seedBytes,
1229
+ ...(terminal.dimensions ? { dimensions: terminal.dimensions } : {}),
1230
1230
  closed,
1231
+ onResize: (listener) => terminal.onResize(listener),
1232
+ readSeed: (offset, maxBytes) => terminal.readSeed(offset, maxBytes),
1233
+ start: () => terminal.start(),
1234
+ read: (offset, maxBytes) => terminal.read(offset, maxBytes),
1231
1235
  write: (data) => terminal.write(data),
1232
1236
  resize: (cols, rows) => terminal.resize(cols, rows),
1233
1237
  close,
1234
- [Symbol.asyncIterator]: () => {
1235
- const iterator = terminal[Symbol.asyncIterator]();
1236
- return {
1237
- next: async () => {
1238
- try {
1239
- const result = await iterator.next();
1240
- if (result.done)
1241
- await closeConnection().catch(() => undefined);
1242
- return result;
1243
- }
1244
- catch (error) {
1245
- await closeConnection().catch(() => undefined);
1246
- throw error;
1247
- }
1248
- },
1249
- return: async () => {
1250
- await close();
1251
- return { done: true, value: undefined };
1252
- },
1253
- };
1254
- },
1255
1238
  };
1256
1239
  }
1257
1240
  function remoteTerminalOpenErrorCode(reason) {
@@ -1465,7 +1448,7 @@ function nonNegativeInteger(value, fallback, field) {
1465
1448
  return value;
1466
1449
  }
1467
1450
  function parseCallTimeout(value) {
1468
- if (value === undefined)
1451
+ if (value === undefined || value === null)
1469
1452
  return undefined;
1470
1453
  if (!Number.isSafeInteger(value) ||
1471
1454
  value < MIN_CALL_TIMEOUT_MS ||
@@ -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
  }
@@ -30,6 +30,7 @@ export declare class SqliteSessionForkStore implements MachineSessionForkStorePo
30
30
  items: SessionItem[];
31
31
  }): void;
32
32
  private publishTarget;
33
+ private publishTargetUnchecked;
33
34
  markError(operationId: string, error: string): void;
34
35
  listReserved(): MachineSessionForkOperationRecord[];
35
36
  hasReservedSource(sessionId: string): boolean;
@@ -1,9 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
- import { chmodSync, copyFileSync, existsSync, mkdirSync, } from "node:fs";
3
- import { join } from "node:path";
2
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, rmSync, } from "node:fs";
3
+ import { extname, 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 {
@@ -94,6 +94,19 @@ export class SqliteSessionForkStore {
94
94
  this.publishTarget(input);
95
95
  }
96
96
  publishTarget(input) {
97
+ try {
98
+ this.publishTargetUnchecked(input);
99
+ }
100
+ catch (error) {
101
+ const published = scalar(db().prepare("SELECT 1 FROM sessions WHERE id = ?"))
102
+ .get(input.target.id);
103
+ if (!published) {
104
+ rmSync(this.sessionDirectory(input.target.id), { recursive: true, force: true });
105
+ }
106
+ throw error;
107
+ }
108
+ }
109
+ publishTargetUnchecked(input) {
97
110
  const itemIds = new Map();
98
111
  for (const item of input.items)
99
112
  itemIds.set(item.id, newSessionItemId(item.type));
@@ -130,7 +143,7 @@ export class SqliteSessionForkStore {
130
143
  };
131
144
  });
132
145
  const conn = db();
133
- conn.transaction(() => {
146
+ transaction(conn, () => {
134
147
  if (input.operationId) {
135
148
  const current = this.read(input.operationId);
136
149
  if (current?.state === "completed")
@@ -152,22 +165,39 @@ export class SqliteSessionForkStore {
152
165
  source, created_at, updated_at
153
166
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(input.target.id, JSON.stringify(sessionWorkspaceSnapshotSchema.parse(input.target.workspace)), JSON.stringify(resolvedExecutionSnapshotSchema.parse(input.target.execution)), normalizeSessionTitle(input.target.title) ?? null, input.target.forkedFromSessionId ?? null, input.target.source, input.target.createdAt, input.target.updatedAt ?? input.target.createdAt);
154
167
  const insertResource = conn.prepare(`INSERT INTO session_resources (
155
- id, session_id, client_upload_id, upload_id, filename, media_type,
168
+ id, session_id, client_upload_id, upload_id, kind, filename, media_type,
156
169
  byte_length, uploaded_bytes, sha256, width, height, state,
157
170
  message_item_id, created_at, committed_at
158
171
  ) VALUES (
159
- @id, @session_id, @client_upload_id, @upload_id, @filename, @media_type,
172
+ @id, @session_id, @client_upload_id, @upload_id, @kind, @filename, @media_type,
160
173
  @byte_length, @uploaded_bytes, @sha256, @width, @height, 'committed',
161
174
  @message_item_id, @created_at, @committed_at
162
175
  )`);
163
- for (const resource of resources)
164
- insertResource.run(resource);
176
+ for (const resource of resources) {
177
+ insertResource.run({
178
+ id: resource.id,
179
+ session_id: resource.session_id,
180
+ client_upload_id: resource.client_upload_id,
181
+ upload_id: resource.upload_id,
182
+ kind: resource.kind,
183
+ filename: resource.filename,
184
+ media_type: resource.media_type,
185
+ byte_length: resource.byte_length,
186
+ uploaded_bytes: resource.uploaded_bytes,
187
+ sha256: resource.sha256,
188
+ width: resource.width,
189
+ height: resource.height,
190
+ message_item_id: resource.message_item_id,
191
+ created_at: resource.created_at,
192
+ committed_at: resource.committed_at,
193
+ });
194
+ }
165
195
  const insertItem = conn.prepare(`INSERT INTO session_items (
166
196
  id, session_id, position, response_id, type, status,
167
- data, created_by, created_at
197
+ data, created_by, parent_tool_call_id, created_at
168
198
  ) VALUES (
169
199
  @id, @session_id, @position, @response_id, @type, @status,
170
- @data, @created_by, @created_at
200
+ @data, @created_by, @parent_tool_call_id, @created_at
171
201
  )`);
172
202
  for (const item of clonedItems) {
173
203
  insertItem.run({
@@ -179,6 +209,7 @@ export class SqliteSessionForkStore {
179
209
  status: item.status,
180
210
  data: JSON.stringify(item.data),
181
211
  created_by: item.createdBy ?? null,
212
+ parent_tool_call_id: item.parentToolCallId ?? null,
182
213
  created_at: item.createdAt,
183
214
  });
184
215
  }
@@ -203,9 +234,9 @@ export class SqliteSessionForkStore {
203
234
  ORDER BY created_at`).all().map(toOperation);
204
235
  }
205
236
  hasReservedSource(sessionId) {
206
- return Boolean(db().prepare(`SELECT 1
237
+ return Boolean(scalar(db().prepare(`SELECT 1
207
238
  FROM session_fork_operations
208
- WHERE source_session_id = ? AND state = 'reserved'`).pluck().get(sessionId));
239
+ WHERE source_session_id = ? AND state = 'reserved'`)).get(sessionId));
209
240
  }
210
241
  markTargetDeleted(sessionId) {
211
242
  const now = new Date().toISOString();
@@ -234,7 +265,7 @@ export class SqliteSessionForkStore {
234
265
  return join(this.resourceRoot, key);
235
266
  }
236
267
  resourcePath(row) {
237
- return join(this.sessionDirectory(row.session_id), `${row.id}.${extension(row.media_type)}`);
268
+ return join(this.sessionDirectory(row.session_id), `${row.id}.${extension(row)}`);
238
269
  }
239
270
  }
240
271
  function toOperation(row) {
@@ -252,22 +283,40 @@ function toOperation(row) {
252
283
  function referencedResourceIds(items) {
253
284
  const ids = new Set();
254
285
  for (const item of items) {
255
- if (item.type !== "message" || item.data.role !== "user")
256
- continue;
257
- for (const part of item.data.content) {
258
- if (part.type === "input_image")
259
- ids.add(part.resourceId);
286
+ if (item.type === "message" && item.data.role === "user") {
287
+ for (const part of item.data.content) {
288
+ if ((part.type === "input_image" || part.type === "input_file") &&
289
+ typeof part.resourceId === "string")
290
+ ids.add(part.resourceId);
291
+ }
260
292
  }
293
+ if (item.type === "function_call_output" &&
294
+ item.data.generatedImage &&
295
+ typeof item.data.generatedImage.resourceId === "string")
296
+ ids.add(item.data.generatedImage.resourceId);
261
297
  }
262
298
  return [...ids];
263
299
  }
264
300
  function remapItemResources(item, resources) {
265
301
  const cloned = structuredClone(item);
266
- if (cloned.type !== "message" || cloned.data.role !== "user")
267
- return cloned;
268
- cloned.data.content = cloned.data.content.map((part) => part.type === "input_image"
269
- ? { ...part, resourceId: resources.get(part.resourceId) ?? part.resourceId }
270
- : part);
302
+ if (cloned.type === "message" && cloned.data.role === "user") {
303
+ cloned.data.content = cloned.data.content.map((part) => {
304
+ if ((part.type === "input_image" || part.type === "input_file") &&
305
+ typeof part.resourceId === "string") {
306
+ return { ...part, resourceId: resources.get(part.resourceId) ?? part.resourceId };
307
+ }
308
+ return part;
309
+ });
310
+ }
311
+ if (cloned.type === "function_call_output" &&
312
+ cloned.data.generatedImage &&
313
+ typeof cloned.data.generatedImage.resourceId === "string") {
314
+ const resourceId = cloned.data.generatedImage.resourceId;
315
+ cloned.data.generatedImage = {
316
+ ...cloned.data.generatedImage,
317
+ resourceId: resources.get(resourceId) ?? resourceId,
318
+ };
319
+ }
271
320
  return cloned;
272
321
  }
273
322
  function deterministicId(prefix, targetSessionId, sourceId) {
@@ -276,13 +325,19 @@ function deterministicId(prefix, targetSessionId, sourceId) {
276
325
  .digest("hex");
277
326
  return `${prefix}_${digest.slice(0, 40)}`;
278
327
  }
279
- function extension(mediaType) {
280
- switch (mediaType) {
281
- case "image/png":
328
+ function extension(row) {
329
+ if (row.kind === "image") {
330
+ if (row.media_type === "image/png")
282
331
  return "png";
283
- case "image/jpeg":
332
+ if (row.media_type === "image/jpeg")
284
333
  return "jpg";
285
- case "image/webp":
334
+ if (row.media_type === "image/webp")
286
335
  return "webp";
336
+ if (row.media_type === "image/gif")
337
+ return "gif";
338
+ if (row.media_type === "image/svg+xml")
339
+ return "svg";
287
340
  }
341
+ const value = extname(row.filename).slice(1).toLowerCase();
342
+ return /^[a-z0-9][a-z0-9._-]{0,15}$/.test(value) ? value : "bin";
288
343
  }