@rynx-ai/daemon 0.1.11-beta.3 → 0.1.11-beta.30

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 (61) 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 +6046 -6018
  7. package/bundled-plugins/plugins/lark/dist/settings.js +6006 -5987
  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-runtime.js +4 -3
  14. package/dist/chrome-for-testing-store.js +29 -7
  15. package/dist/control-deps.d.ts +6 -2
  16. package/dist/control-deps.js +87 -4
  17. package/dist/control-link-store.js +4 -3
  18. package/dist/daemon-owner.js +4 -7
  19. package/dist/daemon-server.js +25 -14
  20. package/dist/db.d.ts +2 -2
  21. package/dist/db.js +133 -21
  22. package/dist/lifecycle.d.ts +1 -0
  23. package/dist/lifecycle.js +3 -0
  24. package/dist/migrations/cleanup-legacy-sessions.js +4 -3
  25. package/dist/plugin-cli.d.ts +10 -0
  26. package/dist/plugin-cli.js +19 -2
  27. package/dist/plugin-host-rpc.d.ts +31 -9
  28. package/dist/plugin-host-rpc.js +428 -80
  29. package/dist/plugin-installer.js +3 -2
  30. package/dist/plugin-management-service.js +0 -2
  31. package/dist/plugin-marketplace.js +2 -1
  32. package/dist/plugin-store.js +2 -4
  33. package/dist/plugin-supervisor.js +0 -4
  34. package/dist/remote-runtime-access-store.d.ts +2 -2
  35. package/dist/remote-runtime-access-store.js +6 -9
  36. package/dist/remote-runtime-connection-manager.js +1 -1
  37. package/dist/remote-runtime-target-store.d.ts +2 -2
  38. package/dist/remote-runtime-target-store.js +6 -6
  39. package/dist/session-emulator-binding-store.js +4 -6
  40. package/dist/session-fork-store.d.ts +1 -0
  41. package/dist/session-fork-store.js +82 -28
  42. package/dist/session-launch-operations.js +6 -5
  43. package/dist/session-log-store.d.ts +1 -0
  44. package/dist/session-log-store.js +19 -16
  45. package/dist/session-meta-store.d.ts +5 -0
  46. package/dist/session-meta-store.js +25 -2
  47. package/dist/session-portal-grant-store.d.ts +29 -0
  48. package/dist/session-portal-grant-store.js +85 -0
  49. package/dist/session-resource-store.d.ts +20 -6
  50. package/dist/session-resource-store.js +364 -68
  51. package/dist/setup-service.d.ts +1 -0
  52. package/dist/setup-service.js +1 -0
  53. package/dist/skills-catalog.d.ts +11 -2
  54. package/dist/skills-catalog.js +317 -10
  55. package/dist/sqlite.d.ts +22 -0
  56. package/dist/sqlite.js +57 -0
  57. package/dist/system-dependencies.d.ts +22 -0
  58. package/dist/system-dependencies.js +79 -0
  59. package/package.json +15 -12
  60. package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +0 -1
  61. /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",
@@ -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 });
@@ -1465,7 +1465,7 @@ function nonNegativeInteger(value, fallback, field) {
1465
1465
  return value;
1466
1466
  }
1467
1467
  function parseCallTimeout(value) {
1468
- if (value === undefined)
1468
+ if (value === undefined || value === null)
1469
1469
  return undefined;
1470
1470
  if (!Number.isSafeInteger(value) ||
1471
1471
  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,16 +165,33 @@ 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
197
  data, created_by, created_at
@@ -203,9 +233,9 @@ export class SqliteSessionForkStore {
203
233
  ORDER BY created_at`).all().map(toOperation);
204
234
  }
205
235
  hasReservedSource(sessionId) {
206
- return Boolean(db().prepare(`SELECT 1
236
+ return Boolean(scalar(db().prepare(`SELECT 1
207
237
  FROM session_fork_operations
208
- WHERE source_session_id = ? AND state = 'reserved'`).pluck().get(sessionId));
238
+ WHERE source_session_id = ? AND state = 'reserved'`)).get(sessionId));
209
239
  }
210
240
  markTargetDeleted(sessionId) {
211
241
  const now = new Date().toISOString();
@@ -234,7 +264,7 @@ export class SqliteSessionForkStore {
234
264
  return join(this.resourceRoot, key);
235
265
  }
236
266
  resourcePath(row) {
237
- return join(this.sessionDirectory(row.session_id), `${row.id}.${extension(row.media_type)}`);
267
+ return join(this.sessionDirectory(row.session_id), `${row.id}.${extension(row)}`);
238
268
  }
239
269
  }
240
270
  function toOperation(row) {
@@ -252,22 +282,40 @@ function toOperation(row) {
252
282
  function referencedResourceIds(items) {
253
283
  const ids = new Set();
254
284
  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);
285
+ if (item.type === "message" && item.data.role === "user") {
286
+ for (const part of item.data.content) {
287
+ if ((part.type === "input_image" || part.type === "input_file") &&
288
+ typeof part.resourceId === "string")
289
+ ids.add(part.resourceId);
290
+ }
260
291
  }
292
+ if (item.type === "function_call_output" &&
293
+ item.data.generatedImage &&
294
+ typeof item.data.generatedImage.resourceId === "string")
295
+ ids.add(item.data.generatedImage.resourceId);
261
296
  }
262
297
  return [...ids];
263
298
  }
264
299
  function remapItemResources(item, resources) {
265
300
  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);
301
+ if (cloned.type === "message" && cloned.data.role === "user") {
302
+ cloned.data.content = cloned.data.content.map((part) => {
303
+ if ((part.type === "input_image" || part.type === "input_file") &&
304
+ typeof part.resourceId === "string") {
305
+ return { ...part, resourceId: resources.get(part.resourceId) ?? part.resourceId };
306
+ }
307
+ return part;
308
+ });
309
+ }
310
+ if (cloned.type === "function_call_output" &&
311
+ cloned.data.generatedImage &&
312
+ typeof cloned.data.generatedImage.resourceId === "string") {
313
+ const resourceId = cloned.data.generatedImage.resourceId;
314
+ cloned.data.generatedImage = {
315
+ ...cloned.data.generatedImage,
316
+ resourceId: resources.get(resourceId) ?? resourceId,
317
+ };
318
+ }
271
319
  return cloned;
272
320
  }
273
321
  function deterministicId(prefix, targetSessionId, sourceId) {
@@ -276,13 +324,19 @@ function deterministicId(prefix, targetSessionId, sourceId) {
276
324
  .digest("hex");
277
325
  return `${prefix}_${digest.slice(0, 40)}`;
278
326
  }
279
- function extension(mediaType) {
280
- switch (mediaType) {
281
- case "image/png":
327
+ function extension(row) {
328
+ if (row.kind === "image") {
329
+ if (row.media_type === "image/png")
282
330
  return "png";
283
- case "image/jpeg":
331
+ if (row.media_type === "image/jpeg")
284
332
  return "jpg";
285
- case "image/webp":
333
+ if (row.media_type === "image/webp")
286
334
  return "webp";
335
+ if (row.media_type === "image/gif")
336
+ return "gif";
337
+ if (row.media_type === "image/svg+xml")
338
+ return "svg";
287
339
  }
340
+ const value = extname(row.filename).slice(1).toLowerCase();
341
+ return /^[a-z0-9][a-z0-9._-]{0,15}$/.test(value) ? value : "bin";
288
342
  }
@@ -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();
@@ -14,6 +14,7 @@ export declare class SqliteSessionLogStore implements SessionLogStore {
14
14
  afterId?: string;
15
15
  limit?: number;
16
16
  }): Promise<SessionItem[]>;
17
+ get(sessionId: string, itemId: string): Promise<SessionItem | undefined>;
17
18
  snapshot(sessionId: string): Promise<SessionItem[]>;
18
19
  listSessions(opts?: {
19
20
  limit?: number;
@@ -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) => {
@@ -37,9 +38,18 @@ export class SqliteSessionLogStore {
37
38
  created_by: stored.createdBy ?? null,
38
39
  created_at: stored.createdAt,
39
40
  });
40
- if (stored.type === "message" && stored.data.role === "user") {
41
- const resourceIds = stored.data.content.flatMap((part) => part.type === "input_image" ? [part.resourceId] : []);
42
- const operationIds = new Set();
41
+ const resourceIds = stored.type === "message" && stored.data.role === "user"
42
+ ? stored.data.content.flatMap((part) => (part.type === "input_image" || part.type === "input_file") &&
43
+ "resourceId" in part && part.resourceId
44
+ ? [part.resourceId]
45
+ : [])
46
+ : stored.type === "function_call_output" &&
47
+ stored.data.generatedImage &&
48
+ "resourceId" in stored.data.generatedImage &&
49
+ stored.data.generatedImage.resourceId
50
+ ? [stored.data.generatedImage.resourceId]
51
+ : [];
52
+ if (resourceIds.length > 0) {
43
53
  for (const resourceId of resourceIds) {
44
54
  const resource = conn.prepare(`SELECT state, message_item_id
45
55
  FROM session_resources
@@ -47,23 +57,12 @@ export class SqliteSessionLogStore {
47
57
  if (!resource ||
48
58
  resource.state === "uploading" ||
49
59
  (resource.message_item_id && resource.message_item_id !== stored.id)) {
50
- throw new Error(`Session image resource ${resourceId} is not committable`);
60
+ throw new Error(`Session resource ${resourceId} is not committable`);
51
61
  }
52
62
  conn.prepare(`UPDATE session_resources
53
63
  SET state = 'committed', message_item_id = ?,
54
64
  committed_at = COALESCE(committed_at, ?)
55
65
  WHERE id = ? AND session_id = ?`).run(stored.id, stored.createdAt, resourceId, sessionId);
56
- const operation = conn.prepare(`SELECT client_message_id
57
- FROM session_message_operation_resources
58
- WHERE session_id = ? AND resource_id = ?`).get(sessionId, resourceId);
59
- if (operation)
60
- operationIds.add(operation.client_message_id);
61
- }
62
- for (const clientMessageId of operationIds) {
63
- conn.prepare(`UPDATE session_message_operations
64
- SET state = 'mirrored', error = NULL, updated_at = ?
65
- WHERE session_id = ? AND client_message_id = ?
66
- AND state IN ('injecting', 'injected', 'outcome_unknown')`).run(stored.createdAt, sessionId, clientMessageId);
67
66
  }
68
67
  }
69
68
  return stored;
@@ -92,6 +91,10 @@ export class SqliteSessionLogStore {
92
91
  .all(sessionId, afterPosition, limit);
93
92
  return rows.map(toItem);
94
93
  }
94
+ async get(sessionId, itemId) {
95
+ const row = db().prepare("SELECT * FROM session_items WHERE session_id = ? AND id = ?").get(sessionId, itemId);
96
+ return row ? toItem(row) : undefined;
97
+ }
95
98
  async snapshot(sessionId) {
96
99
  const rows = db()
97
100
  .prepare("SELECT * FROM session_items WHERE session_id = ? ORDER BY position ASC")
@@ -18,6 +18,11 @@ export declare function listSessionMetas(): StoredSessionMeta[];
18
18
  export declare function getSessionMeta(id: string): StoredSessionMeta | undefined;
19
19
  export declare function createSessionMeta(meta: StoredSessionMeta): void;
20
20
  export declare function setSessionTitle(id: string, title: string): void;
21
+ export declare function setSessionExecution(id: string, execution: MachineSessionRecord["execution"]): void;
21
22
  export declare function removeSessionMeta(id: string): void;
22
23
  /** {@link SessionRegistry} over the same `sessions` table. */
23
24
  export declare const sessionRegistry: SessionRegistry;
25
+ /** Generic plugin-principal ownership, including descendants forked from a
26
+ * plugin-created Session. Portal authorization calls this on every request so
27
+ * a grant cannot cross into console or another plugin's Sessions. */
28
+ export declare function sessionBelongsToPlugin(pluginId: string, sessionId: string): boolean;