@rynx-ai/daemon 0.1.11-beta.24 → 0.1.11-beta.26

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.
@@ -126423,6 +126423,7 @@ var resolvedExecutionSnapshotSchema = external_exports.object({
126423
126423
  provider: external_exports.enum(["codex", "traex", "claude"]),
126424
126424
  model: nonEmptyString.nullable(),
126425
126425
  reasoningEffort: nonEmptyString.nullable(),
126426
+ collaborationMode: external_exports.enum(["plan", "default"]).optional(),
126426
126427
  instructions: external_exports.string().nullable(),
126427
126428
  skills: external_exports.array(skillRefSchema2),
126428
126429
  pluginSkills: external_exports.array(external_exports.object({
@@ -126291,6 +126291,7 @@ var resolvedExecutionSnapshotSchema = external_exports.object({
126291
126291
  provider: external_exports.enum(["codex", "traex", "claude"]),
126292
126292
  model: nonEmptyString.nullable(),
126293
126293
  reasoningEffort: nonEmptyString.nullable(),
126294
+ collaborationMode: external_exports.enum(["plan", "default"]).optional(),
126294
126295
  instructions: external_exports.string().nullable(),
126295
126296
  skills: external_exports.array(skillRefSchema2),
126296
126297
  pluginSkills: external_exports.array(external_exports.object({
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/plugin-channel-lark",
3
- "version": "0.1.11-beta.24",
3
+ "version": "0.1.11-beta.26",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -303,7 +303,9 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
303
303
  "semantic.session.create.v1",
304
304
  "semantic.session.fork.v1",
305
305
  "semantic.session.message.v1",
306
- "semantic.session.resources.v1",
306
+ "semantic.session.resources.v2",
307
+ "semantic.session.files.v1",
308
+ "semantic.session.workspace-files.v1",
307
309
  "semantic.session.message-queue.v1",
308
310
  "semantic.session.delete.v1",
309
311
  "semantic.session.interaction.v1",
@@ -447,6 +449,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
447
449
  sessionContextProvider: browserRunnerSessionContexts,
448
450
  admissionOpen,
449
451
  admissionReserve: reserveAdmission,
452
+ materializeGeneratedImage: (input) => sessionResources.materializeGeneratedImage(input),
450
453
  onMirrorError: (error, sessionId) => warn(`Session mirror failed for ${sessionId}: ${error instanceof Error ? error.message : String(error)}`),
451
454
  });
452
455
  const sessionBrowserHost = appBrowserHostSupervisor
package/dist/db.js CHANGED
@@ -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)
@@ -341,6 +345,7 @@ function migrate(conn) {
341
345
  CREATE INDEX IF NOT EXISTS idx_runtime_pending_enrollments_expires
342
346
  ON runtime_pending_enrollments(expires_at);
343
347
  `);
348
+ migrateSessionResourceFiles(conn);
344
349
  migratePluginCapabilityDeclarations(conn);
345
350
  migrateSessionSnapshots(conn);
346
351
  // Breaking plugin-runtime cutover: remove obsolete core-owned state.
@@ -351,6 +356,80 @@ function migrate(conn) {
351
356
  addColumnIfMissing(conn, "plugin_marketplaces", "built_in", "INTEGER NOT NULL DEFAULT 0");
352
357
  addColumnIfMissing(conn, "sessions", "forked_from_session_id", "TEXT");
353
358
  addColumnIfMissing(conn, "plugin_session_launch_operations", "raw_request_hash", "TEXT");
359
+ addColumnIfMissing(conn, "session_message_operations", "response_id", "TEXT");
360
+ addColumnIfMissing(conn, "session_message_operations", "message_item_id", "TEXT");
361
+ addColumnIfMissing(conn, "session_message_operations", "execution_snapshot", "TEXT");
362
+ }
363
+ /** Rebuild the original image-only table without losing committed resources. */
364
+ function migrateSessionResourceFiles(conn) {
365
+ const columns = conn
366
+ .prepare("PRAGMA table_info(session_resources)")
367
+ .all();
368
+ if (columns.some((column) => column.name === "kind"))
369
+ return;
370
+ conn.exec("PRAGMA foreign_keys = OFF");
371
+ try {
372
+ transaction(conn, () => {
373
+ conn.exec(`
374
+ ALTER TABLE session_message_operation_resources
375
+ RENAME TO session_message_operation_resources_image_v1;
376
+ ALTER TABLE session_resources RENAME TO session_resources_image_v1;
377
+
378
+ CREATE TABLE session_resources (
379
+ id TEXT PRIMARY KEY,
380
+ session_id TEXT NOT NULL,
381
+ client_upload_id TEXT NOT NULL,
382
+ upload_id TEXT NOT NULL UNIQUE,
383
+ kind TEXT NOT NULL DEFAULT 'image' CHECK(kind IN ('image', 'file')),
384
+ filename TEXT NOT NULL,
385
+ media_type TEXT NOT NULL,
386
+ byte_length INTEGER NOT NULL,
387
+ uploaded_bytes INTEGER NOT NULL DEFAULT 0,
388
+ sha256 TEXT,
389
+ width INTEGER,
390
+ height INTEGER,
391
+ state TEXT NOT NULL DEFAULT 'uploading'
392
+ CHECK(state IN ('uploading', 'staged', 'committed')),
393
+ message_item_id TEXT,
394
+ created_at INTEGER NOT NULL,
395
+ committed_at INTEGER,
396
+ UNIQUE(session_id, client_upload_id)
397
+ );
398
+ INSERT INTO session_resources (
399
+ id, session_id, client_upload_id, upload_id, kind, filename, media_type,
400
+ byte_length, uploaded_bytes, sha256, width, height, state,
401
+ message_item_id, created_at, committed_at
402
+ )
403
+ SELECT
404
+ id, session_id, client_upload_id, upload_id, 'image', filename, media_type,
405
+ byte_length, uploaded_bytes, sha256, width, height, state,
406
+ message_item_id, created_at, committed_at
407
+ FROM session_resources_image_v1;
408
+
409
+ CREATE TABLE session_message_operation_resources (
410
+ session_id TEXT NOT NULL,
411
+ client_message_id TEXT NOT NULL,
412
+ resource_id TEXT NOT NULL UNIQUE REFERENCES session_resources(id) ON DELETE CASCADE,
413
+ PRIMARY KEY(session_id, client_message_id, resource_id),
414
+ FOREIGN KEY(session_id, client_message_id)
415
+ REFERENCES session_message_operations(session_id, client_message_id)
416
+ ON DELETE CASCADE
417
+ );
418
+ INSERT INTO session_message_operation_resources
419
+ SELECT * FROM session_message_operation_resources_image_v1;
420
+
421
+ DROP TABLE session_message_operation_resources_image_v1;
422
+ DROP TABLE session_resources_image_v1;
423
+ CREATE INDEX idx_session_resources_session
424
+ ON session_resources(session_id, created_at);
425
+ CREATE INDEX idx_session_resources_message
426
+ ON session_resources(message_item_id);
427
+ `);
428
+ })();
429
+ }
430
+ finally {
431
+ conn.exec("PRAGMA foreign_keys = ON");
432
+ }
354
433
  }
355
434
  /** Remove the inert permission-declaration columns left by older installations. */
356
435
  function migratePluginCapabilityDeclarations(conn) {
@@ -1,6 +1,7 @@
1
- import { type AdmissionReservation, type AgentSpec, type AgentCapabilities, type AgentRuntimeId, type AgentSessionStore, type ConversationRuntime, type ResolvedExecutionSnapshot, type SessionRegistry, type SessionWorkspaceSnapshot } from "@rynx-ai/core";
1
+ import { type AdmissionReservation, type AgentSpec, type AgentCapabilities, type AgentRuntimeId, type AgentSessionStore, type ResolvedExecutionSnapshot, type RuntimeCollaborationMode, type SessionRegistry, type SessionWorkspaceSnapshot, type SteerSignal } from "@rynx-ai/core";
2
2
  import { type PluginAgentSummary, type PluginHostEvent, type PluginSessionActivitySnapshot, type PluginResolvedSessionExecution, type PluginSessionExecutionSnapshot, type PluginSessionInteractionResolveDisposition } from "@rynx-ai/plugin-sdk";
3
3
  import type { SessionInteractionResolution } from "@rynx-ai/protocol";
4
+ import type { MachineSessionMessageInput, StartedMachineSessionRun } from "@rynx-ai/server";
4
5
  import { PluginRunnerError } from "@rynx-ai/plugin-runner";
5
6
  /** Leaves ample room for the RPC envelope below the runner's 1 MiB line cap. */
6
7
  export declare const MAX_PLUGIN_HOST_EVENT_BYTES: number;
@@ -37,7 +38,16 @@ export interface PluginHostServices {
37
38
  publicKey: string;
38
39
  sign(pluginId: string, challenge: string): string;
39
40
  };
40
- conversationRuntime: ConversationRuntime;
41
+ sessionRuns: {
42
+ startRun: (sessionId: string, input: string | MachineSessionMessageInput, options?: {
43
+ signal?: AbortSignal;
44
+ steer?: SteerSignal;
45
+ collaborationMode?: RuntimeCollaborationMode;
46
+ }) => Promise<StartedMachineSessionRun>;
47
+ };
48
+ sessionFiles: {
49
+ resolveResourcePath(sessionId: string, resourceId: string): Promise<string>;
50
+ };
41
51
  capabilities: AgentCapabilities;
42
52
  sessionStore: AgentSessionStore;
43
53
  sessionRegistry: SessionRegistry;
@@ -124,6 +134,7 @@ export declare class PluginHostRpc {
124
134
  private runSession;
125
135
  private runSessionAdmitted;
126
136
  private pumpRun;
137
+ private projectSessionFileEvent;
127
138
  private releaseRun;
128
139
  private interruptRun;
129
140
  private sessionActivity;
@@ -116,6 +116,10 @@ export class PluginHostRpc {
116
116
  publicKey: this.services.installationProof.publicKey,
117
117
  },
118
118
  protocolVersion: 1,
119
+ capabilities: {
120
+ sessionRunContent: true,
121
+ sessionRunFiles: true,
122
+ },
119
123
  ...(this.services.sessionPortal
120
124
  ? { sessionPortal: { ...this.services.sessionPortal } }
121
125
  : {}),
@@ -352,7 +356,15 @@ export class PluginHostRpc {
352
356
  const input = record(params);
353
357
  const sessionId = requiredSessionId(input);
354
358
  const operationId = optionalString(input.operationId, "operationId", 512);
355
- const message = requiredText(input.message, "message", MAX_MESSAGE_LENGTH);
359
+ const hasMessage = input.message !== undefined;
360
+ const hasContent = input.content !== undefined;
361
+ if (hasMessage === hasContent) {
362
+ throw new PluginHostCallError("INVALID_PARAMS", "provide exactly one of message or content");
363
+ }
364
+ const message = hasMessage
365
+ ? requiredText(input.message, "message", MAX_MESSAGE_LENGTH)
366
+ : undefined;
367
+ const collaborationMode = optionalCollaborationMode(input.collaborationMode);
356
368
  let session = this.ownedSession(sessionId);
357
369
  if (operationId) {
358
370
  const replayed = this.activeRunsByOperation.get(operationId);
@@ -365,6 +377,13 @@ export class PluginHostRpc {
365
377
  }
366
378
  const active = this.activeSessionRuns.get(sessionId);
367
379
  if (active) {
380
+ if (hasContent) {
381
+ // Native steering currently accepts text only. Preserve the attachment
382
+ // as a new Turn after the active response reaches its terminal edge.
383
+ await waitForHostPromise(active.settled, signal);
384
+ throwIfHostCallAborted(signal);
385
+ return await this.runSessionAdmitted(params, signal);
386
+ }
368
387
  const queued = active.input.enqueue(message);
369
388
  if (!queued.accepted) {
370
389
  // The response reached its terminal boundary before Host cleanup won
@@ -416,6 +435,7 @@ export class PluginHostRpc {
416
435
  });
417
436
  session = this.ownedSession(sessionId);
418
437
  }
438
+ const content = hasContent ? await pluginRunContent(input.content) : undefined;
419
439
  const abort = new AbortController();
420
440
  const cancel = () => abort.abort(signal?.reason);
421
441
  if (signal?.aborted) {
@@ -457,15 +477,20 @@ export class PluginHostRpc {
457
477
  this.activeSessionRuns.set(sessionId, run);
458
478
  if (operationId)
459
479
  this.activeRunsByOperation.set(operationId, run);
460
- let startedExecution;
480
+ let started;
461
481
  try {
462
- startedExecution = await this.services.conversationRuntime.runLive({
463
- message,
464
- threadId: sessionId,
465
- workspace: session.workspace,
466
- execution: session.execution,
482
+ started = await this.services.sessionRuns.startRun(sessionId, message !== undefined
483
+ ? {
484
+ message,
485
+ clientMessageId: operationId ?? `plugin_run_${runId}`,
486
+ }
487
+ : {
488
+ content: content,
489
+ clientMessageId: operationId ?? `plugin_run_${runId}`,
490
+ }, {
467
491
  signal: abort.signal,
468
492
  steer: runInput,
493
+ ...(collaborationMode ? { collaborationMode } : {}),
469
494
  });
470
495
  }
471
496
  catch (error) {
@@ -488,18 +513,20 @@ export class PluginHostRpc {
488
513
  throwIfHostCallAborted(signal);
489
514
  throw error;
490
515
  }
516
+ run.responseId = started.responseId;
491
517
  const result = {
492
518
  runId,
519
+ responseId: started.responseId,
493
520
  sessionId,
494
- provider: startedExecution.provider,
495
- model: startedExecution.model,
496
- authSource: startedExecution.authSource,
521
+ provider: started.provider,
522
+ model: started.model,
523
+ authSource: started.authSource,
497
524
  };
498
525
  run.startupSettled = true;
499
526
  run.resolveStarted(result);
500
527
  // Let the RPC transport write the request response before stream events are
501
528
  // emitted. `runLive` has already subscribed, so no agent event is lost.
502
- setImmediate(() => void this.pumpRun(run, startedExecution.generator));
529
+ setImmediate(() => void this.pumpRun(run, started.generator));
503
530
  return result;
504
531
  }
505
532
  async pumpRun(run, generator) {
@@ -519,6 +546,16 @@ export class PluginHostRpc {
519
546
  for (const projected of projectSessionEvent(run, event)) {
520
547
  await this.emit(projected);
521
548
  }
549
+ try {
550
+ const fileEvent = await this.projectSessionFileEvent(run, event);
551
+ if (fileEvent)
552
+ await this.emit(fileEvent);
553
+ }
554
+ catch {
555
+ // File delivery is an optional projection of a canonical event. A
556
+ // missing borrowed path must not turn an otherwise successful native
557
+ // Turn into session.run.failed or stop consuming its event stream.
558
+ }
522
559
  terminalFailure = sessionRunFailure(event);
523
560
  if (terminalFailure)
524
561
  break;
@@ -581,6 +618,44 @@ export class PluginHostRpc {
581
618
  this.releaseRun(run);
582
619
  }
583
620
  }
621
+ async projectSessionFileEvent(run, event) {
622
+ const input = jsonRecord(event);
623
+ if (input?.type !== "response.output_item.done")
624
+ return undefined;
625
+ const item = jsonRecord(input.item);
626
+ if (item?.type !== "function_call_output")
627
+ return undefined;
628
+ const data = jsonRecord(item.data);
629
+ const image = jsonRecord(data?.generatedImage);
630
+ if (!image || typeof image.mediaType !== "string")
631
+ return undefined;
632
+ let filePath;
633
+ if (typeof image.path === "string" && path.isAbsolute(image.path)) {
634
+ filePath = image.path;
635
+ }
636
+ else if (typeof image.resourceId === "string") {
637
+ filePath = await this.services.sessionFiles.resolveResourcePath(run.sessionId, image.resourceId);
638
+ }
639
+ else {
640
+ return undefined;
641
+ }
642
+ return {
643
+ event: "session.run.file",
644
+ payload: {
645
+ runId: run.runId,
646
+ responseId: typeof item.responseId === "string"
647
+ ? item.responseId
648
+ : run.responseId,
649
+ sessionId: run.sessionId,
650
+ itemId: typeof item.id === "string" ? item.id : "unknown",
651
+ filename: typeof image.filename === "string" && image.filename
652
+ ? image.filename
653
+ : path.basename(filePath),
654
+ mediaType: image.mediaType,
655
+ path: filePath,
656
+ },
657
+ };
658
+ }
584
659
  releaseRun(run) {
585
660
  run.input.close();
586
661
  if (!run.startupSettled) {
@@ -941,6 +1016,93 @@ function sessionExecutionSnapshot(value) {
941
1016
  function requiredSessionId(params) {
942
1017
  return requiredString(record(params).sessionId, "sessionId", 512);
943
1018
  }
1019
+ async function pluginRunContent(value) {
1020
+ if (!Array.isArray(value) || value.length === 0 || value.length > 256) {
1021
+ throw new PluginHostCallError("INVALID_PARAMS", "content must be a non-empty array with at most 256 parts");
1022
+ }
1023
+ return Promise.all(value.map(async (entry, index) => {
1024
+ const input = record(entry);
1025
+ if (input.type === "input_text") {
1026
+ return {
1027
+ type: "input_text",
1028
+ text: requiredText(input.text, `content[${index}].text`, MAX_MESSAGE_LENGTH),
1029
+ };
1030
+ }
1031
+ if (input.type !== "input_image" && input.type !== "input_file") {
1032
+ throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].type is invalid`);
1033
+ }
1034
+ const suppliedPath = requiredPathString(input.path, `content[${index}].path`, 16_384);
1035
+ if (!path.isAbsolute(suppliedPath)) {
1036
+ throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].path must be absolute`);
1037
+ }
1038
+ let resolvedPath;
1039
+ let fileSize;
1040
+ try {
1041
+ resolvedPath = await realpath(suppliedPath);
1042
+ const metadata = await stat(resolvedPath);
1043
+ if (!metadata.isFile())
1044
+ throw new Error("not a regular file");
1045
+ fileSize = metadata.size;
1046
+ }
1047
+ catch (error) {
1048
+ throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].path is not a readable regular file: ${error instanceof Error ? error.message : String(error)}`);
1049
+ }
1050
+ const filename = input.type === "input_file"
1051
+ ? requiredString(input.filename, `content[${index}].filename`, 255)
1052
+ : optionalString(input.filename, `content[${index}].filename`, 255) ??
1053
+ path.basename(resolvedPath);
1054
+ if (input.type === "input_image") {
1055
+ return {
1056
+ type: "input_image",
1057
+ path: resolvedPath,
1058
+ filename,
1059
+ mediaType: pluginImageMediaType(input.mediaType, resolvedPath, `content[${index}].mediaType`),
1060
+ byteLength: fileSize,
1061
+ };
1062
+ }
1063
+ return {
1064
+ type: "input_file",
1065
+ path: resolvedPath,
1066
+ filename,
1067
+ mediaType: pluginMediaType(input.mediaType ?? "application/octet-stream", `content[${index}].mediaType`),
1068
+ byteLength: fileSize,
1069
+ };
1070
+ }));
1071
+ }
1072
+ function pluginImageMediaType(value, filePath, label) {
1073
+ const inferred = (() => {
1074
+ switch (path.extname(filePath).toLowerCase()) {
1075
+ case ".png": return "image/png";
1076
+ case ".jpg":
1077
+ case ".jpeg": return "image/jpeg";
1078
+ case ".webp": return "image/webp";
1079
+ case ".gif": return "image/gif";
1080
+ case ".svg": return "image/svg+xml";
1081
+ default: return undefined;
1082
+ }
1083
+ })();
1084
+ const mediaType = value === undefined ? inferred : pluginMediaType(value, label);
1085
+ if (mediaType !== "image/png" &&
1086
+ mediaType !== "image/jpeg" &&
1087
+ mediaType !== "image/webp" &&
1088
+ mediaType !== "image/gif" &&
1089
+ mediaType !== "image/svg+xml") {
1090
+ throw new PluginHostCallError("INVALID_PARAMS", `${label} is not a supported image type`);
1091
+ }
1092
+ return mediaType;
1093
+ }
1094
+ function pluginMediaType(value, label) {
1095
+ if (typeof value !== "string") {
1096
+ throw new PluginHostCallError("INVALID_PARAMS", `${label} must be a media type`);
1097
+ }
1098
+ const mediaType = value.trim().toLowerCase();
1099
+ if (mediaType.length === 0 ||
1100
+ mediaType.length > 255 ||
1101
+ !/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mediaType)) {
1102
+ throw new PluginHostCallError("INVALID_PARAMS", `${label} is invalid`);
1103
+ }
1104
+ return mediaType;
1105
+ }
944
1106
  function requiredSessionPortalAccess(params) {
945
1107
  const access = record(params).access;
946
1108
  if (access === "read_only" || access === "read_write")
@@ -989,6 +1151,13 @@ function sessionInteractionResolution(value) {
989
1151
  function optionalRuntime(params) {
990
1152
  return optionalRuntimeValue(record(params).runtime);
991
1153
  }
1154
+ function optionalCollaborationMode(value) {
1155
+ if (value === undefined)
1156
+ return undefined;
1157
+ if (value === "plan" || value === "default")
1158
+ return value;
1159
+ throw new PluginHostCallError("INVALID_PARAMS", "collaborationMode must be plan or default");
1160
+ }
992
1161
  function optionalRuntimeValue(value) {
993
1162
  if (value === undefined || value === null || value === "")
994
1163
  return undefined;
@@ -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,6 +1,6 @@
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";
@@ -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));
@@ -152,11 +165,11 @@ 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
  )`);
@@ -166,6 +179,7 @@ export class SqliteSessionForkStore {
166
179
  session_id: resource.session_id,
167
180
  client_upload_id: resource.client_upload_id,
168
181
  upload_id: resource.upload_id,
182
+ kind: resource.kind,
169
183
  filename: resource.filename,
170
184
  media_type: resource.media_type,
171
185
  byte_length: resource.byte_length,
@@ -250,7 +264,7 @@ export class SqliteSessionForkStore {
250
264
  return join(this.resourceRoot, key);
251
265
  }
252
266
  resourcePath(row) {
253
- 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)}`);
254
268
  }
255
269
  }
256
270
  function toOperation(row) {
@@ -268,22 +282,40 @@ function toOperation(row) {
268
282
  function referencedResourceIds(items) {
269
283
  const ids = new Set();
270
284
  for (const item of items) {
271
- if (item.type !== "message" || item.data.role !== "user")
272
- continue;
273
- for (const part of item.data.content) {
274
- if (part.type === "input_image")
275
- 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
+ }
276
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);
277
296
  }
278
297
  return [...ids];
279
298
  }
280
299
  function remapItemResources(item, resources) {
281
300
  const cloned = structuredClone(item);
282
- if (cloned.type !== "message" || cloned.data.role !== "user")
283
- return cloned;
284
- cloned.data.content = cloned.data.content.map((part) => part.type === "input_image"
285
- ? { ...part, resourceId: resources.get(part.resourceId) ?? part.resourceId }
286
- : 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
+ }
287
319
  return cloned;
288
320
  }
289
321
  function deterministicId(prefix, targetSessionId, sourceId) {
@@ -292,13 +324,19 @@ function deterministicId(prefix, targetSessionId, sourceId) {
292
324
  .digest("hex");
293
325
  return `${prefix}_${digest.slice(0, 40)}`;
294
326
  }
295
- function extension(mediaType) {
296
- switch (mediaType) {
297
- case "image/png":
327
+ function extension(row) {
328
+ if (row.kind === "image") {
329
+ if (row.media_type === "image/png")
298
330
  return "png";
299
- case "image/jpeg":
331
+ if (row.media_type === "image/jpeg")
300
332
  return "jpg";
301
- case "image/webp":
333
+ if (row.media_type === "image/webp")
302
334
  return "webp";
335
+ if (row.media_type === "image/gif")
336
+ return "gif";
337
+ if (row.media_type === "image/svg+xml")
338
+ return "svg";
303
339
  }
340
+ const value = extname(row.filename).slice(1).toLowerCase();
341
+ return /^[a-z0-9][a-z0-9._-]{0,15}$/.test(value) ? value : "bin";
304
342
  }
@@ -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;