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

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 (64) 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 +41 -16
  20. package/dist/db.d.ts +2 -2
  21. package/dist/db.js +155 -21
  22. package/dist/headless-browser-host.js +283 -34
  23. package/dist/lifecycle.d.ts +1 -0
  24. package/dist/lifecycle.js +3 -0
  25. package/dist/migrations/cleanup-legacy-sessions.js +4 -3
  26. package/dist/plugin-cli.d.ts +10 -0
  27. package/dist/plugin-cli.js +19 -2
  28. package/dist/plugin-host-rpc.d.ts +48 -9
  29. package/dist/plugin-host-rpc.js +552 -80
  30. package/dist/plugin-installer.js +3 -2
  31. package/dist/plugin-management-service.js +0 -2
  32. package/dist/plugin-marketplace.js +2 -1
  33. package/dist/plugin-store.js +2 -4
  34. package/dist/plugin-supervisor.js +0 -4
  35. package/dist/remote-runtime-access-store.d.ts +2 -2
  36. package/dist/remote-runtime-access-store.js +6 -9
  37. package/dist/remote-runtime-connection-manager.js +1 -1
  38. package/dist/remote-runtime-target-store.d.ts +2 -2
  39. package/dist/remote-runtime-target-store.js +6 -6
  40. package/dist/session-emulator-binding-store.js +4 -6
  41. package/dist/session-fork-store.d.ts +1 -0
  42. package/dist/session-fork-store.js +82 -28
  43. package/dist/session-launch-operations.js +6 -5
  44. package/dist/session-log-store.d.ts +1 -0
  45. package/dist/session-log-store.js +19 -16
  46. package/dist/session-meta-store.d.ts +5 -0
  47. package/dist/session-meta-store.js +25 -2
  48. package/dist/session-portal-grant-store.d.ts +55 -0
  49. package/dist/session-portal-grant-store.js +160 -0
  50. package/dist/session-resource-store.d.ts +24 -6
  51. package/dist/session-resource-store.js +386 -68
  52. package/dist/session-timeline-projection-store.d.ts +24 -0
  53. package/dist/session-timeline-projection-store.js +53 -0
  54. package/dist/setup-service.d.ts +1 -0
  55. package/dist/setup-service.js +1 -0
  56. package/dist/skills-catalog.d.ts +11 -2
  57. package/dist/skills-catalog.js +317 -10
  58. package/dist/sqlite.d.ts +22 -0
  59. package/dist/sqlite.js +57 -0
  60. package/dist/system-dependencies.d.ts +22 -0
  61. package/dist/system-dependencies.js +79 -0
  62. package/package.json +15 -12
  63. package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +0 -1
  64. /package/bundled-plugins/plugins/lark/ui/assets/{index-D9N-AkLX.js → index-D8Amoe6a.js} +0 -0
@@ -17,9 +17,10 @@ import { REMOTE_RUNTIME_CORE_PROTOCOL } from "@rynx-ai/protocol/remote-runtime";
17
17
  import { RUNTIME_BROWSER_INSPECT_MAX_MESSAGE_BYTES, RUNTIME_BROWSER_INSPECT_SEMANTIC_CAPABILITY, } from "@rynx-ai/protocol/runtime-browser-inspect";
18
18
  import { RUNTIME_BROWSER_SURFACE_SEMANTIC_CAPABILITY } from "@rynx-ai/protocol/runtime-browser-surface";
19
19
  import { RUNTIME_EMULATOR_SURFACE_SEMANTIC_CAPABILITY } from "@rynx-ai/protocol/runtime-emulator-surface";
20
- import { createDaemonRuntimeHost, createDesktopFirstSessionBrowserHost, createRunnerSessionTerminalHost, createSessionRuntimeServices, DesktopBrowserHostAbsentError, DesktopBrowserHostRegistry, DirectRuntimeConnectionHub, DirectRuntimeBrowserInspectHostError, MachineSessionService, MachineSessionServiceFailure, listRuntimeModels, RemoteRuntimeDispatcher, SessionBrowserService, SessionBrowserServiceError, SessionBrowserSurfaceCoordinator, SessionEmulatorService, startServer, startDirectRuntimeServer, } from "@rynx-ai/server";
20
+ import { createDaemonRuntimeHost, createDesktopFirstSessionBrowserHost, createRunnerSessionTerminalHost, createSessionRuntimeServices, DesktopBrowserHostAbsentError, DesktopBrowserHostRegistry, DirectRuntimeConnectionHub, DirectRuntimeBrowserInspectHostError, MachineSessionService, MachineSessionServiceFailure, RemoteRuntimeDispatcher, SessionPendingInputIndex, SessionBrowserService, SessionBrowserServiceError, SessionBrowserSurfaceCoordinator, SessionEmulatorService, startServer, startDirectRuntimeServer, } from "@rynx-ai/server";
21
+ import { authorizeSessionPortalGrant, inspectSessionPortalGrant, onSessionPortalGrantRevoked, redeemSessionPortalTicket, revokeEmbeddedWritableSessionPortalGrants, } from "./session-portal-grant-store.js";
21
22
  import { APP_BROWSER_HOST_BOOTSTRAP_TYPE, AppBrowserHostSupervisor, parseAppBrowserHostLaunchConfig, } from "./app-browser-host-supervisor.js";
22
- import { buildControlDeps } from "./control-deps.js";
23
+ import { buildControlDeps, listControlAgentModels } from "./control-deps.js";
23
24
  import { createBrowserArtifactManagementService } from "./browser-artifact-management.js";
24
25
  import { createDaemonBuildStatus } from "./daemon-build-status.js";
25
26
  import { MaintenanceLeaseStore } from "./maintenance-lease.js";
@@ -49,7 +50,7 @@ import { SqliteSessionResourceStore } from "./session-resource-store.js";
49
50
  import { createSessionFallbackWorkspace, discardSessionFallbackWorkspace, } from "./session-fallback-workspace.js";
50
51
  import { SessionBrowserCapabilityStore } from "./session-browser-capability-store.js";
51
52
  import { SqliteSessionEmulatorBindingStore } from "./session-emulator-binding-store.js";
52
- import { sessionRegistry } from "./session-meta-store.js";
53
+ import { sessionBelongsToPlugin, sessionRegistry } from "./session-meta-store.js";
53
54
  import { sessionPendingMessageStore } from "./session-pending-message-store.js";
54
55
  import { RemoteRuntimeAccessStore } from "./remote-runtime-access-store.js";
55
56
  import { DaemonRemoteRuntimeAdmin } from "./remote-runtime-admin.js";
@@ -68,13 +69,7 @@ const require = createRequire(import.meta.url);
68
69
  const DAEMON_PACKAGE_VERSION = readDaemonPackageVersion();
69
70
  async function resolveSessionExecutionSnapshot(input) {
70
71
  return {
71
- ...await resolveExecutionSnapshot({
72
- ...input,
73
- resolveDefaultModel: async (provider) => {
74
- const models = await listRuntimeModels(input.config, provider);
75
- return models?.data.find((candidate) => candidate.isDefault)?.model;
76
- },
77
- }),
72
+ ...await resolveExecutionSnapshot(input),
78
73
  pluginSkills: snapshotEnabledPluginSkillSources(),
79
74
  };
80
75
  }
@@ -280,7 +275,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
280
275
  const directConfig = directHostingSupported
281
276
  ? await loadDirectConfig()
282
277
  : undefined;
283
- const control = buildControlDeps();
278
+ const control = buildControlDeps(appConfig);
284
279
  const installationIdentity = await loadOrCreateDaemonInstallationIdentity();
285
280
  const directEncryptionIdentity = directHostingSupported
286
281
  ? await loadOrCreateDaemonDirectRuntimeEncryptionIdentity()
@@ -304,16 +299,21 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
304
299
  "semantic.session.interrupt.v1",
305
300
  "semantic.session.agent-options.v1",
306
301
  "semantic.session.launch-options.v2",
302
+ "semantic.session.execution-settings.v1",
307
303
  "semantic.session.create.v1",
308
304
  "semantic.session.fork.v1",
309
305
  "semantic.session.message.v1",
310
- "semantic.session.resources.v1",
306
+ "semantic.session.resources.v2",
307
+ "semantic.session.files.v1",
308
+ "semantic.session.workspace-files.v1",
311
309
  "semantic.session.message-queue.v1",
312
310
  "semantic.session.delete.v1",
313
311
  "semantic.session.interaction.v1",
314
312
  "semantic.project.manage.v1",
315
313
  "semantic.provider-cli.status.v1",
316
- ...(runtimeBrowserSupported ? ["semantic.browser.v1"] : []),
314
+ ...(runtimeBrowserSupported
315
+ ? ["semantic.browser.v1", "semantic.browser.request-headers.v1"]
316
+ : []),
317
317
  ...(runtimeBrowserSupported && directHostingSupported
318
318
  ? [
319
319
  RUNTIME_BROWSER_SURFACE_SEMANTIC_CAPABILITY,
@@ -389,6 +389,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
389
389
  };
390
390
  };
391
391
  let machineSessions;
392
+ let sessionBrowsers;
392
393
  const pluginRuntime = {
393
394
  bindHostServices: (services) => pluginSupervisor.bindHostServices({
394
395
  ...services,
@@ -398,6 +399,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
398
399
  publicKey: installationIdentity.publicKey,
399
400
  sign: installationIdentity.sign,
400
401
  },
402
+ sessionBrowserRequestHeaders: sessionBrowsers,
401
403
  sessionLaunch: {
402
404
  resolve: async (selection) => {
403
405
  const workspace = await resolveSessionWorkspace(runtimeProjects, selection.projectId, selection.managedCwd, selection.workspace);
@@ -410,7 +412,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
410
412
  ...((selection.provider || (!selection.agent && !selection.config))
411
413
  ? { provider: selection.provider ?? appConfig.DEFAULT_RUNTIME }
412
414
  : {}),
413
- ...(selection.model ? { model: selection.model } : {}),
415
+ ...(selection.model !== undefined ? { model: selection.model } : {}),
414
416
  ...(selection.reasoningEffort
415
417
  ? { reasoningEffort: selection.reasoningEffort }
416
418
  : {}),
@@ -419,6 +421,11 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
419
421
  };
420
422
  },
421
423
  },
424
+ sessionExecution: {
425
+ update: async (input) => {
426
+ await machineSessions.updateExecutionSettings(input);
427
+ },
428
+ },
422
429
  sessionFork: (input) => machineSessions.fork(input),
423
430
  admissionOpen,
424
431
  admissionReserve: reserveAdmission,
@@ -431,6 +438,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
431
438
  const directConnections = new DirectRuntimeConnectionHub();
432
439
  const sessionLog = new SqliteSessionLogStore();
433
440
  const sessionResources = new SqliteSessionResourceStore();
441
+ const sessionPendingInputs = new SessionPendingInputIndex();
434
442
  const sessionForks = new SqliteSessionForkStore();
435
443
  const runtimeProjects = new SqliteRuntimeProjectStore();
436
444
  const providerClis = new ProviderCliService();
@@ -443,15 +451,21 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
443
451
  const sessionRuntimeServices = createSessionRuntimeServices({
444
452
  config: appConfig,
445
453
  sessionLog,
454
+ pendingInputs: sessionPendingInputs,
455
+ onPendingInputPersisted: (input) => sessionResources.markMessageMirrored(input.sessionId, input.clientMessageId, {
456
+ responseId: input.responseId,
457
+ messageItemId: input.messageItemId,
458
+ }),
446
459
  sessionContextProvider: browserRunnerSessionContexts,
447
460
  admissionOpen,
448
461
  admissionReserve: reserveAdmission,
462
+ materializeGeneratedImage: (input) => sessionResources.materializeGeneratedImage(input),
449
463
  onMirrorError: (error, sessionId) => warn(`Session mirror failed for ${sessionId}: ${error instanceof Error ? error.message : String(error)}`),
450
464
  });
451
465
  const sessionBrowserHost = appBrowserHostSupervisor
452
466
  ? createSupervisedAppSessionBrowserHost(durableDesktopBrowserHost, appBrowserHostSupervisor)
453
467
  : createDesktopFirstSessionBrowserHost(durableDesktopBrowserHost, createHeadlessBrowserHost());
454
- const sessionBrowsers = new SessionBrowserService({
468
+ sessionBrowsers = new SessionBrowserService({
455
469
  host: sessionBrowserHost,
456
470
  sessions: {
457
471
  exists: async (sessionId) => Boolean(sessionRegistry.get(sessionId) ??
@@ -552,18 +566,21 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
552
566
  list: () => control.listAgents(),
553
567
  },
554
568
  pendingMessages: sessionPendingMessageStore,
569
+ pendingInputs: sessionPendingInputs,
555
570
  resources: sessionResources,
556
571
  forks: sessionForks,
557
572
  execution: {
558
573
  runtime: sessionRuntimeServices.conversationRuntime,
559
- resolve: ({ agent, provider, model, reasoningEffort, cwd }) => resolveSessionExecutionSnapshot({
574
+ resolve: ({ agent, provider, model, reasoningEffort, permissionPreset, cwd }) => resolveSessionExecutionSnapshot({
560
575
  config: appConfig,
561
576
  ...(agent ? { agentName: agent } : {}),
562
577
  ...(provider ? { provider } : {}),
563
578
  ...(model ? { model } : {}),
564
579
  ...(reasoningEffort ? { reasoningEffort } : {}),
580
+ ...(permissionPreset ? { permissionPreset } : {}),
565
581
  cwd,
566
582
  }),
583
+ listModels: (provider) => listControlAgentModels(appConfig, provider),
567
584
  runner: sessionRuntimeServices.runnerManager,
568
585
  },
569
586
  workspace: {
@@ -763,6 +780,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
763
780
  status: async () => asDaemonChromeInspectionStatus(await initializedChromeInspectionManager.status()),
764
781
  configure: async (input) => asDaemonChromeInspectionStatus(await initializedChromeInspectionManager.configure(input)),
765
782
  };
783
+ revokeEmbeddedWritableSessionPortalGrants();
766
784
  server = await startServer({
767
785
  config: appConfig,
768
786
  control,
@@ -794,6 +812,13 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
794
812
  registry: desktopBrowserHosts,
795
813
  daemonInstanceId,
796
814
  },
815
+ sessionPortalAuthorization: {
816
+ redeem: (ticket, embedOrigin) => redeemSessionPortalTicket(ticket, new Date(), embedOrigin),
817
+ authorize: authorizeSessionPortalGrant,
818
+ inspect: inspectSessionPortalGrant,
819
+ subscribeRevocations: onSessionPortalGrantRevoked,
820
+ ownsSession: sessionBelongsToPlugin,
821
+ },
797
822
  });
798
823
  baseShutdown = server.shutdown.bind(server);
799
824
  if (directConfig && directEncryptionIdentity) {
package/dist/db.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import Database from "better-sqlite3";
1
+ import { DatabaseSync } from "node:sqlite";
2
2
  /** Absolute path to the SQLite file (override via `RYNX_DB`, else `~/.rynx/rynx.db`). */
3
3
  export declare function dbPath(): string;
4
4
  /** Open (or reuse) the database at the current path, migrating + importing on first open. */
5
- export declare function db(): Database.Database;
5
+ export declare function db(): DatabaseSync;
package/dist/db.js CHANGED
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * `~/.rynx/rynx.db` — the daemon's SQLite store. Concerns kept apart:
3
- * - `plugin_installations` — source-qualified manifests, capabilities, and immutable packages.
3
+ * - `plugin_installations` — source-qualified manifests and immutable packages.
4
4
  * - `plugin_marketplaces` — catalog registrations; catalog data is never executable.
5
5
  * - `plugin_session_launch_operations` — durable idempotency/tombstones.
6
6
  * - `control_links` — generic, one-use links to the local session console.
7
+ * - `session_portal_tickets` / `session_portal_grants` — hashed, scoped,
8
+ * browser Session Portal authorization.
7
9
  * - `runtime_pairing_claims` / `runtime_client_grants` — one-use enrollment
8
10
  * claims and the durable client identities they issue for direct runtimes.
9
11
  * - `runtime_targets` / `runtime_direct_routes` / `runtime_pending_enrollments`
@@ -16,8 +18,9 @@
16
18
  */
17
19
  import { chmodSync, mkdirSync } from "node:fs";
18
20
  import { dirname, isAbsolute, join, resolve } from "node:path";
21
+ import { DatabaseSync } from "node:sqlite";
19
22
  import { rynxHome } from "@rynx-ai/core";
20
- import Database from "better-sqlite3";
23
+ import { transaction } from "./sqlite.js";
21
24
  const connections = new Map();
22
25
  /** Absolute path to the SQLite file (override via `RYNX_DB`, else `~/.rynx/rynx.db`). */
23
26
  export function dbPath() {
@@ -34,9 +37,7 @@ export function db() {
34
37
  if (cached)
35
38
  return cached;
36
39
  mkdirSync(dirname(path), { recursive: true });
37
- const conn = new Database(path);
38
- conn.pragma("busy_timeout = 5000");
39
- conn.pragma("foreign_keys = ON");
40
+ const conn = new DatabaseSync(path, { timeout: 5000, enableForeignKeyConstraints: true });
40
41
  migrate(conn);
41
42
  // Plugin state and daemon credentials are private.
42
43
  try {
@@ -86,7 +87,6 @@ function migrate(conn) {
86
87
  install_path TEXT NOT NULL,
87
88
  manifest TEXT NOT NULL,
88
89
  integrity TEXT NOT NULL,
89
- capabilities TEXT NOT NULL,
90
90
  runtime_state TEXT,
91
91
  runtime_error TEXT,
92
92
  UNIQUE(marketplace_id, plugin_id)
@@ -109,8 +109,9 @@ function migrate(conn) {
109
109
  session_id TEXT NOT NULL,
110
110
  client_upload_id TEXT NOT NULL,
111
111
  upload_id TEXT NOT NULL UNIQUE,
112
+ kind TEXT NOT NULL DEFAULT 'image' CHECK(kind IN ('image', 'file')),
112
113
  filename TEXT NOT NULL,
113
- media_type TEXT NOT NULL CHECK(media_type IN ('image/png', 'image/jpeg', 'image/webp')),
114
+ media_type TEXT NOT NULL,
114
115
  byte_length INTEGER NOT NULL,
115
116
  uploaded_bytes INTEGER NOT NULL DEFAULT 0,
116
117
  sha256 TEXT,
@@ -141,6 +142,9 @@ function migrate(conn) {
141
142
  'failed_not_started'
142
143
  )),
143
144
  error TEXT,
145
+ response_id TEXT,
146
+ message_item_id TEXT,
147
+ execution_snapshot TEXT,
144
148
  created_at INTEGER NOT NULL,
145
149
  updated_at INTEGER NOT NULL,
146
150
  PRIMARY KEY(session_id, client_message_id)
@@ -245,6 +249,50 @@ function migrate(conn) {
245
249
  );
246
250
  CREATE INDEX IF NOT EXISTS idx_control_links_expires
247
251
  ON control_links(expires_at);
252
+ CREATE TABLE IF NOT EXISTS session_portal_tickets (
253
+ ticket_hash TEXT PRIMARY KEY,
254
+ plugin_id TEXT NOT NULL,
255
+ scope TEXT NOT NULL CHECK(scope IN ('plugin_sessions', 'single_session')),
256
+ access TEXT NOT NULL CHECK(access IN ('read_only', 'read_write')),
257
+ session_id TEXT,
258
+ created_at TEXT NOT NULL,
259
+ expires_at TEXT NOT NULL,
260
+ used_at TEXT,
261
+ CHECK(
262
+ (scope = 'plugin_sessions' AND session_id IS NULL)
263
+ OR (scope = 'single_session' AND session_id IS NOT NULL)
264
+ )
265
+ );
266
+ CREATE INDEX IF NOT EXISTS idx_session_portal_tickets_expires
267
+ ON session_portal_tickets(expires_at);
268
+ CREATE TABLE IF NOT EXISTS session_portal_grants (
269
+ portal_id TEXT PRIMARY KEY,
270
+ secret_hash TEXT NOT NULL,
271
+ plugin_id TEXT NOT NULL,
272
+ scope TEXT NOT NULL CHECK(scope IN ('plugin_sessions', 'single_session')),
273
+ access TEXT NOT NULL CHECK(access IN ('read_only', 'read_write')),
274
+ session_id TEXT,
275
+ created_at TEXT NOT NULL,
276
+ expires_at TEXT NOT NULL,
277
+ revoked_at TEXT,
278
+ CHECK(
279
+ (scope = 'plugin_sessions' AND session_id IS NULL)
280
+ OR (scope = 'single_session' AND session_id IS NOT NULL)
281
+ )
282
+ );
283
+ CREATE INDEX IF NOT EXISTS idx_session_portal_grants_expires
284
+ ON session_portal_grants(expires_at);
285
+ CREATE TABLE IF NOT EXISTS session_timeline_projections (
286
+ plugin_id TEXT NOT NULL,
287
+ session_id TEXT NOT NULL,
288
+ projection_id TEXT NOT NULL,
289
+ request_hash TEXT NOT NULL,
290
+ response_id TEXT NOT NULL,
291
+ item_id TEXT NOT NULL UNIQUE,
292
+ state TEXT NOT NULL DEFAULT 'pending' CHECK(state IN ('pending', 'completed')),
293
+ created_at TEXT NOT NULL,
294
+ PRIMARY KEY(plugin_id, session_id, projection_id)
295
+ );
248
296
  CREATE TABLE IF NOT EXISTS runtime_pairing_claims (
249
297
  claim_id TEXT PRIMARY KEY,
250
298
  secret_hash TEXT NOT NULL,
@@ -308,7 +356,8 @@ function migrate(conn) {
308
356
  CREATE INDEX IF NOT EXISTS idx_runtime_pending_enrollments_expires
309
357
  ON runtime_pending_enrollments(expires_at);
310
358
  `);
311
- migratePluginCapabilities(conn);
359
+ migrateSessionResourceFiles(conn);
360
+ migratePluginCapabilityDeclarations(conn);
312
361
  migrateSessionSnapshots(conn);
313
362
  // Breaking plugin-runtime cutover: remove obsolete core-owned state.
314
363
  conn.exec(`
@@ -318,6 +367,103 @@ function migrate(conn) {
318
367
  addColumnIfMissing(conn, "plugin_marketplaces", "built_in", "INTEGER NOT NULL DEFAULT 0");
319
368
  addColumnIfMissing(conn, "sessions", "forked_from_session_id", "TEXT");
320
369
  addColumnIfMissing(conn, "plugin_session_launch_operations", "raw_request_hash", "TEXT");
370
+ addColumnIfMissing(conn, "session_message_operations", "response_id", "TEXT");
371
+ addColumnIfMissing(conn, "session_message_operations", "message_item_id", "TEXT");
372
+ addColumnIfMissing(conn, "session_message_operations", "execution_snapshot", "TEXT");
373
+ addColumnIfMissing(conn, "session_portal_tickets", "grant_handle", "TEXT");
374
+ addColumnIfMissing(conn, "session_portal_tickets", "embed_origin", "TEXT");
375
+ addColumnIfMissing(conn, "session_portal_grants", "grant_handle", "TEXT");
376
+ addColumnIfMissing(conn, "session_portal_grants", "embed_origin", "TEXT");
377
+ addColumnIfMissing(conn, "session_timeline_projections", "state", "TEXT NOT NULL DEFAULT 'pending'");
378
+ conn.exec(`
379
+ CREATE INDEX IF NOT EXISTS idx_session_portal_tickets_grant_handle
380
+ ON session_portal_tickets(plugin_id, grant_handle);
381
+ CREATE INDEX IF NOT EXISTS idx_session_portal_grants_grant_handle
382
+ ON session_portal_grants(plugin_id, grant_handle);
383
+ `);
384
+ }
385
+ /** Rebuild the original image-only table without losing committed resources. */
386
+ function migrateSessionResourceFiles(conn) {
387
+ const columns = conn
388
+ .prepare("PRAGMA table_info(session_resources)")
389
+ .all();
390
+ if (columns.some((column) => column.name === "kind"))
391
+ return;
392
+ conn.exec("PRAGMA foreign_keys = OFF");
393
+ try {
394
+ transaction(conn, () => {
395
+ conn.exec(`
396
+ ALTER TABLE session_message_operation_resources
397
+ RENAME TO session_message_operation_resources_image_v1;
398
+ ALTER TABLE session_resources RENAME TO session_resources_image_v1;
399
+
400
+ CREATE TABLE session_resources (
401
+ id TEXT PRIMARY KEY,
402
+ session_id TEXT NOT NULL,
403
+ client_upload_id TEXT NOT NULL,
404
+ upload_id TEXT NOT NULL UNIQUE,
405
+ kind TEXT NOT NULL DEFAULT 'image' CHECK(kind IN ('image', 'file')),
406
+ filename TEXT NOT NULL,
407
+ media_type TEXT NOT NULL,
408
+ byte_length INTEGER NOT NULL,
409
+ uploaded_bytes INTEGER NOT NULL DEFAULT 0,
410
+ sha256 TEXT,
411
+ width INTEGER,
412
+ height INTEGER,
413
+ state TEXT NOT NULL DEFAULT 'uploading'
414
+ CHECK(state IN ('uploading', 'staged', 'committed')),
415
+ message_item_id TEXT,
416
+ created_at INTEGER NOT NULL,
417
+ committed_at INTEGER,
418
+ UNIQUE(session_id, client_upload_id)
419
+ );
420
+ INSERT INTO session_resources (
421
+ id, session_id, client_upload_id, upload_id, kind, filename, media_type,
422
+ byte_length, uploaded_bytes, sha256, width, height, state,
423
+ message_item_id, created_at, committed_at
424
+ )
425
+ SELECT
426
+ id, session_id, client_upload_id, upload_id, 'image', filename, media_type,
427
+ byte_length, uploaded_bytes, sha256, width, height, state,
428
+ message_item_id, created_at, committed_at
429
+ FROM session_resources_image_v1;
430
+
431
+ CREATE TABLE session_message_operation_resources (
432
+ session_id TEXT NOT NULL,
433
+ client_message_id TEXT NOT NULL,
434
+ resource_id TEXT NOT NULL UNIQUE REFERENCES session_resources(id) ON DELETE CASCADE,
435
+ PRIMARY KEY(session_id, client_message_id, resource_id),
436
+ FOREIGN KEY(session_id, client_message_id)
437
+ REFERENCES session_message_operations(session_id, client_message_id)
438
+ ON DELETE CASCADE
439
+ );
440
+ INSERT INTO session_message_operation_resources
441
+ SELECT * FROM session_message_operation_resources_image_v1;
442
+
443
+ DROP TABLE session_message_operation_resources_image_v1;
444
+ DROP TABLE session_resources_image_v1;
445
+ CREATE INDEX idx_session_resources_session
446
+ ON session_resources(session_id, created_at);
447
+ CREATE INDEX idx_session_resources_message
448
+ ON session_resources(message_item_id);
449
+ `);
450
+ })();
451
+ }
452
+ finally {
453
+ conn.exec("PRAGMA foreign_keys = ON");
454
+ }
455
+ }
456
+ /** Remove the inert permission-declaration columns left by older installations. */
457
+ function migratePluginCapabilityDeclarations(conn) {
458
+ const columns = conn
459
+ .prepare("PRAGMA table_info(plugin_installations)")
460
+ .all();
461
+ const names = new Set(columns.map((column) => column.name));
462
+ for (const legacyColumn of ["capabilities", "grants"]) {
463
+ if (names.has(legacyColumn)) {
464
+ conn.exec(`ALTER TABLE plugin_installations DROP COLUMN ${legacyColumn}`);
465
+ }
466
+ }
321
467
  }
322
468
  /**
323
469
  * Breaking Session cutover: the previous rows contain mutable Agent/Provider
@@ -330,7 +476,7 @@ function migrateSessionSnapshots(conn) {
330
476
  const names = new Set(columns.map((column) => column.name));
331
477
  if (names.has("workspace") && names.has("execution"))
332
478
  return;
333
- conn.transaction(() => {
479
+ transaction(conn, () => {
334
480
  conn.exec(`
335
481
  INSERT OR IGNORE INTO session_snapshot_cleanup (session_id)
336
482
  SELECT id FROM sessions;
@@ -358,18 +504,6 @@ function migrateSessionSnapshots(conn) {
358
504
  `);
359
505
  }).immediate();
360
506
  }
361
- function migratePluginCapabilities(conn) {
362
- const columns = conn
363
- .prepare("PRAGMA table_info(plugin_installations)")
364
- .all();
365
- if (columns.some((column) => column.name === "capabilities"))
366
- return;
367
- if (columns.some((column) => column.name === "grants")) {
368
- conn.exec("ALTER TABLE plugin_installations RENAME COLUMN grants TO capabilities");
369
- return;
370
- }
371
- conn.exec("ALTER TABLE plugin_installations ADD COLUMN capabilities TEXT NOT NULL DEFAULT '[]'");
372
- }
373
507
  /** Idempotently add a column to an existing table (no-op if already present). */
374
508
  function addColumnIfMissing(conn, table, column, ddl) {
375
509
  const cols = conn.prepare(`PRAGMA table_info(${table})`).all();