@rynx-ai/daemon 0.1.11-beta.25 → 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.
- package/bundled-plugins/plugins/lark/package.json +1 -1
- package/dist/daemon-server.js +4 -1
- package/dist/db.js +80 -1
- package/dist/plugin-host-rpc.d.ts +13 -2
- package/dist/plugin-host-rpc.js +172 -12
- package/dist/session-fork-store.d.ts +1 -0
- package/dist/session-fork-store.js +58 -20
- package/dist/session-log-store.d.ts +1 -0
- package/dist/session-log-store.js +17 -15
- package/dist/session-resource-store.d.ts +20 -6
- package/dist/session-resource-store.js +361 -66
- package/package.json +9 -9
package/dist/daemon-server.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|
package/dist/plugin-host-rpc.js
CHANGED
|
@@ -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,14 @@ 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
|
|
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;
|
|
356
367
|
const collaborationMode = optionalCollaborationMode(input.collaborationMode);
|
|
357
368
|
let session = this.ownedSession(sessionId);
|
|
358
369
|
if (operationId) {
|
|
@@ -366,6 +377,13 @@ export class PluginHostRpc {
|
|
|
366
377
|
}
|
|
367
378
|
const active = this.activeSessionRuns.get(sessionId);
|
|
368
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
|
+
}
|
|
369
387
|
const queued = active.input.enqueue(message);
|
|
370
388
|
if (!queued.accepted) {
|
|
371
389
|
// The response reached its terminal boundary before Host cleanup won
|
|
@@ -417,6 +435,7 @@ export class PluginHostRpc {
|
|
|
417
435
|
});
|
|
418
436
|
session = this.ownedSession(sessionId);
|
|
419
437
|
}
|
|
438
|
+
const content = hasContent ? await pluginRunContent(input.content) : undefined;
|
|
420
439
|
const abort = new AbortController();
|
|
421
440
|
const cancel = () => abort.abort(signal?.reason);
|
|
422
441
|
if (signal?.aborted) {
|
|
@@ -458,16 +477,20 @@ export class PluginHostRpc {
|
|
|
458
477
|
this.activeSessionRuns.set(sessionId, run);
|
|
459
478
|
if (operationId)
|
|
460
479
|
this.activeRunsByOperation.set(operationId, run);
|
|
461
|
-
let
|
|
480
|
+
let started;
|
|
462
481
|
try {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
+
}, {
|
|
468
491
|
signal: abort.signal,
|
|
469
|
-
...(collaborationMode ? { collaborationMode } : {}),
|
|
470
492
|
steer: runInput,
|
|
493
|
+
...(collaborationMode ? { collaborationMode } : {}),
|
|
471
494
|
});
|
|
472
495
|
}
|
|
473
496
|
catch (error) {
|
|
@@ -490,18 +513,20 @@ export class PluginHostRpc {
|
|
|
490
513
|
throwIfHostCallAborted(signal);
|
|
491
514
|
throw error;
|
|
492
515
|
}
|
|
516
|
+
run.responseId = started.responseId;
|
|
493
517
|
const result = {
|
|
494
518
|
runId,
|
|
519
|
+
responseId: started.responseId,
|
|
495
520
|
sessionId,
|
|
496
|
-
provider:
|
|
497
|
-
model:
|
|
498
|
-
authSource:
|
|
521
|
+
provider: started.provider,
|
|
522
|
+
model: started.model,
|
|
523
|
+
authSource: started.authSource,
|
|
499
524
|
};
|
|
500
525
|
run.startupSettled = true;
|
|
501
526
|
run.resolveStarted(result);
|
|
502
527
|
// Let the RPC transport write the request response before stream events are
|
|
503
528
|
// emitted. `runLive` has already subscribed, so no agent event is lost.
|
|
504
|
-
setImmediate(() => void this.pumpRun(run,
|
|
529
|
+
setImmediate(() => void this.pumpRun(run, started.generator));
|
|
505
530
|
return result;
|
|
506
531
|
}
|
|
507
532
|
async pumpRun(run, generator) {
|
|
@@ -521,6 +546,16 @@ export class PluginHostRpc {
|
|
|
521
546
|
for (const projected of projectSessionEvent(run, event)) {
|
|
522
547
|
await this.emit(projected);
|
|
523
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
|
+
}
|
|
524
559
|
terminalFailure = sessionRunFailure(event);
|
|
525
560
|
if (terminalFailure)
|
|
526
561
|
break;
|
|
@@ -583,6 +618,44 @@ export class PluginHostRpc {
|
|
|
583
618
|
this.releaseRun(run);
|
|
584
619
|
}
|
|
585
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
|
+
}
|
|
586
659
|
releaseRun(run) {
|
|
587
660
|
run.input.close();
|
|
588
661
|
if (!run.startupSettled) {
|
|
@@ -943,6 +1016,93 @@ function sessionExecutionSnapshot(value) {
|
|
|
943
1016
|
function requiredSessionId(params) {
|
|
944
1017
|
return requiredString(record(params).sessionId, "sessionId", 512);
|
|
945
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
|
+
}
|
|
946
1106
|
function requiredSessionPortalAccess(params) {
|
|
947
1107
|
const access = record(params).access;
|
|
948
1108
|
if (access === "read_only" || access === "read_write")
|
|
@@ -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
|
|
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
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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(
|
|
296
|
-
|
|
297
|
-
|
|
327
|
+
function extension(row) {
|
|
328
|
+
if (row.kind === "image") {
|
|
329
|
+
if (row.media_type === "image/png")
|
|
298
330
|
return "png";
|
|
299
|
-
|
|
331
|
+
if (row.media_type === "image/jpeg")
|
|
300
332
|
return "jpg";
|
|
301
|
-
|
|
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;
|
|
@@ -38,9 +38,18 @@ export class SqliteSessionLogStore {
|
|
|
38
38
|
created_by: stored.createdBy ?? null,
|
|
39
39
|
created_at: stored.createdAt,
|
|
40
40
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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) {
|
|
44
53
|
for (const resourceId of resourceIds) {
|
|
45
54
|
const resource = conn.prepare(`SELECT state, message_item_id
|
|
46
55
|
FROM session_resources
|
|
@@ -48,23 +57,12 @@ export class SqliteSessionLogStore {
|
|
|
48
57
|
if (!resource ||
|
|
49
58
|
resource.state === "uploading" ||
|
|
50
59
|
(resource.message_item_id && resource.message_item_id !== stored.id)) {
|
|
51
|
-
throw new Error(`Session
|
|
60
|
+
throw new Error(`Session resource ${resourceId} is not committable`);
|
|
52
61
|
}
|
|
53
62
|
conn.prepare(`UPDATE session_resources
|
|
54
63
|
SET state = 'committed', message_item_id = ?,
|
|
55
64
|
committed_at = COALESCE(committed_at, ?)
|
|
56
65
|
WHERE id = ? AND session_id = ?`).run(stored.id, stored.createdAt, resourceId, sessionId);
|
|
57
|
-
const operation = conn.prepare(`SELECT client_message_id
|
|
58
|
-
FROM session_message_operation_resources
|
|
59
|
-
WHERE session_id = ? AND resource_id = ?`).get(sessionId, resourceId);
|
|
60
|
-
if (operation)
|
|
61
|
-
operationIds.add(operation.client_message_id);
|
|
62
|
-
}
|
|
63
|
-
for (const clientMessageId of operationIds) {
|
|
64
|
-
conn.prepare(`UPDATE session_message_operations
|
|
65
|
-
SET state = 'mirrored', error = NULL, updated_at = ?
|
|
66
|
-
WHERE session_id = ? AND client_message_id = ?
|
|
67
|
-
AND state IN ('injecting', 'injected', 'outcome_unknown')`).run(stored.createdAt, sessionId, clientMessageId);
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
68
|
return stored;
|
|
@@ -93,6 +91,10 @@ export class SqliteSessionLogStore {
|
|
|
93
91
|
.all(sessionId, afterPosition, limit);
|
|
94
92
|
return rows.map(toItem);
|
|
95
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
|
+
}
|
|
96
98
|
async snapshot(sessionId) {
|
|
97
99
|
const rows = db()
|
|
98
100
|
.prepare("SELECT * FROM session_items WHERE session_id = ? ORDER BY position ASC")
|