@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.
- package/bundled-plugins/plugins/lark/dist/runtime.js +1 -0
- package/bundled-plugins/plugins/lark/dist/settings.js +1 -0
- 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 +180 -11
- 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
|
@@ -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({
|
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,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
|
|
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
|
|
480
|
+
let started;
|
|
461
481
|
try {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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:
|
|
495
|
-
model:
|
|
496
|
-
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,
|
|
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
|
|
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;
|