@rynx-ai/daemon 0.1.10 → 0.1.11-beta.2
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/.rynx-plugin/marketplace.json +10 -0
- package/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +37 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.d.ts +48 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +54 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-mirror.d.ts +85 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-mirror.js +333 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-watcher.d.ts +109 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-watcher.js +347 -0
- package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +95 -0
- package/bundled-plugins/plugins/lark/dist/host-adapters.js +504 -0
- package/bundled-plugins/plugins/lark/dist/index.d.ts +7 -0
- package/bundled-plugins/plugins/lark/dist/index.js +7 -0
- package/bundled-plugins/plugins/lark/dist/lark-card-stream.d.ts +363 -0
- package/bundled-plugins/plugins/lark/dist/lark-card-stream.js +1335 -0
- package/bundled-plugins/plugins/lark/dist/lark-config-store.d.ts +30 -0
- package/bundled-plugins/plugins/lark/dist/lark-config-store.js +135 -0
- package/bundled-plugins/plugins/lark/dist/lark-content.d.ts +30 -0
- package/bundled-plugins/plugins/lark/dist/lark-content.js +138 -0
- package/bundled-plugins/plugins/lark/dist/lark-conversation-directory.d.ts +65 -0
- package/bundled-plugins/plugins/lark/dist/lark-conversation-directory.js +106 -0
- package/bundled-plugins/plugins/lark/dist/lark-diff-card.d.ts +25 -0
- package/bundled-plugins/plugins/lark/dist/lark-diff-card.js +58 -0
- package/bundled-plugins/plugins/lark/dist/lark-fork-transcript.d.ts +16 -0
- package/bundled-plugins/plugins/lark/dist/lark-fork-transcript.js +141 -0
- package/bundled-plugins/plugins/lark/dist/lark-resume-card.d.ts +31 -0
- package/bundled-plugins/plugins/lark/dist/lark-resume-card.js +105 -0
- package/bundled-plugins/plugins/lark/dist/lark-runtime-state-store.d.ts +18 -0
- package/bundled-plugins/plugins/lark/dist/lark-runtime-state-store.js +95 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/client.d.ts +8 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/client.js +32 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/index.d.ts +1 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/index.js +1 -0
- package/bundled-plugins/plugins/lark/dist/lark-sender.d.ts +114 -0
- package/bundled-plugins/plugins/lark/dist/lark-sender.js +323 -0
- package/bundled-plugins/plugins/lark/dist/lark-session-store.d.ts +13 -0
- package/bundled-plugins/plugins/lark/dist/lark-session-store.js +14 -0
- package/bundled-plugins/plugins/lark/dist/lark-settings-card.d.ts +55 -0
- package/bundled-plugins/plugins/lark/dist/lark-settings-card.js +158 -0
- package/bundled-plugins/plugins/lark/dist/lark-setup.d.ts +39 -0
- package/bundled-plugins/plugins/lark/dist/lark-setup.js +103 -0
- package/bundled-plugins/plugins/lark/dist/lark.d.ts +507 -0
- package/bundled-plugins/plugins/lark/dist/lark.js +1977 -0
- package/bundled-plugins/plugins/lark/dist/runtime.d.ts +13 -0
- package/bundled-plugins/plugins/lark/dist/runtime.js +131316 -0
- package/bundled-plugins/plugins/lark/dist/settings.d.ts +7 -0
- package/bundled-plugins/plugins/lark/dist/settings.js +126958 -0
- package/bundled-plugins/plugins/lark/package.json +57 -0
- package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +1 -0
- package/bundled-plugins/plugins/lark/ui/assets/index-D9N-AkLX.js +201 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/index.html +13 -0
- package/dist/bundled-plugins.d.ts +15 -0
- package/dist/bundled-plugins.js +134 -0
- package/dist/cli.js +26 -208
- package/dist/control-deps.js +2 -79
- package/dist/daemon-server.d.ts +4 -1
- package/dist/daemon-server.js +167 -57
- package/dist/db.js +145 -124
- package/dist/index-daemon.js +25 -4
- package/dist/migrations/cleanup-legacy-sessions.d.ts +1 -1
- package/dist/migrations/cleanup-legacy-sessions.js +33 -15
- package/dist/official-plugins.d.ts +2 -0
- package/dist/official-plugins.js +2 -0
- package/dist/plugin-cli.d.ts +1 -3
- package/dist/plugin-cli.js +56 -45
- package/dist/plugin-host-leases.d.ts +4 -0
- package/dist/plugin-host-leases.js +7 -0
- package/dist/plugin-host-rpc.d.ts +42 -4
- package/dist/plugin-host-rpc.js +144 -34
- package/dist/plugin-installer.d.ts +24 -55
- package/dist/plugin-installer.js +202 -415
- package/dist/plugin-management-service.d.ts +30 -23
- package/dist/plugin-management-service.js +290 -105
- package/dist/plugin-marketplace.d.ts +35 -0
- package/dist/plugin-marketplace.js +299 -0
- package/dist/plugin-package.d.ts +22 -24
- package/dist/plugin-package.js +520 -571
- package/dist/plugin-skill-context.d.ts +4 -0
- package/dist/plugin-skill-context.js +35 -0
- package/dist/plugin-store.d.ts +36 -37
- package/dist/plugin-store.js +140 -92
- package/dist/plugin-supervisor.d.ts +45 -125
- package/dist/plugin-supervisor.js +516 -1053
- package/dist/pm2.js +15 -10
- package/dist/runtime-project-store.d.ts +9 -0
- package/dist/runtime-project-store.js +83 -0
- package/dist/session-fallback-workspace.d.ts +22 -0
- package/dist/session-fallback-workspace.js +120 -0
- package/dist/session-fork-store.d.ts +41 -0
- package/dist/session-fork-store.js +288 -0
- package/dist/session-launch-operations.d.ts +14 -2
- package/dist/session-launch-operations.js +34 -2
- package/dist/session-meta-store.d.ts +7 -9
- package/dist/session-meta-store.js +28 -49
- package/dist/setup.js +26 -14
- package/package.json +14 -11
- package/dist/channel-store.d.ts +0 -22
- package/dist/channel-store.js +0 -65
- package/dist/instance-store.d.ts +0 -38
- package/dist/instance-store.js +0 -85
- package/dist/plugin-install-restart.d.ts +0 -13
- package/dist/plugin-install-restart.js +0 -24
- package/dist/plugin-runtime-control.d.ts +0 -2
- package/dist/plugin-runtime-control.js +0 -37
- package/dist/registry.d.ts +0 -10
- package/dist/registry.js +0 -52
package/dist/pm2.js
CHANGED
|
@@ -10,10 +10,9 @@
|
|
|
10
10
|
import { spawn, spawnSync } from "node:child_process";
|
|
11
11
|
import { mkdirSync, realpathSync } from "node:fs";
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
|
-
import { homedir } from "node:os";
|
|
14
13
|
import { dirname, join, resolve } from "node:path";
|
|
15
14
|
import { fileURLToPath } from "node:url";
|
|
16
|
-
import { loadConfig } from "@rynx-ai/core";
|
|
15
|
+
import { loadConfig, rynxHome } from "@rynx-ai/core";
|
|
17
16
|
import { daemonEntryRestartRequired } from "./daemon-build-status.js";
|
|
18
17
|
const require = createRequire(import.meta.url);
|
|
19
18
|
/** pm2 app name — single resident daemon per machine. */
|
|
@@ -55,16 +54,22 @@ function daemonEntryPath() {
|
|
|
55
54
|
return join(dirname(fileURLToPath(import.meta.url)), "index-daemon.js");
|
|
56
55
|
}
|
|
57
56
|
function logsDir() {
|
|
58
|
-
const
|
|
59
|
-
const dir = join(home, "logs");
|
|
57
|
+
const dir = join(rynxHome(), "logs");
|
|
60
58
|
mkdirSync(dir, { recursive: true });
|
|
61
59
|
return dir;
|
|
62
60
|
}
|
|
63
61
|
function nodeRuntimeEnv() {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
return {
|
|
62
|
+
const home = rynxHome();
|
|
63
|
+
const pm2Home = join(home, "pm2");
|
|
64
|
+
mkdirSync(pm2Home, { recursive: true, mode: 0o700 });
|
|
65
|
+
return {
|
|
66
|
+
...process.env,
|
|
67
|
+
RYNX_HOME: home,
|
|
68
|
+
PM2_HOME: pm2Home,
|
|
69
|
+
...(process.versions.electron
|
|
70
|
+
? { ELECTRON_RUN_AS_NODE: "1" }
|
|
71
|
+
: {}),
|
|
72
|
+
};
|
|
68
73
|
}
|
|
69
74
|
/** Run a pm2 command inheriting stdio; returns its exit code. */
|
|
70
75
|
function runPm2(args, quiet = false) {
|
|
@@ -145,9 +150,9 @@ export function startDaemon({ force, quiet = false }) {
|
|
|
145
150
|
daemonEntryPath(),
|
|
146
151
|
"--name",
|
|
147
152
|
APP_NAME,
|
|
148
|
-
//
|
|
153
|
+
// App and CLI share one daemon-owned state/workspace root.
|
|
149
154
|
"--cwd",
|
|
150
|
-
|
|
155
|
+
rynxHome(),
|
|
151
156
|
"--max-restarts",
|
|
152
157
|
"10",
|
|
153
158
|
"--restart-delay",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type RuntimeProject, type RuntimeProjectInput, type RuntimeProjectStore } from "@rynx-ai/core";
|
|
2
|
+
/** SQLite-backed Project catalog owned by the current target Runtime. */
|
|
3
|
+
export declare class SqliteRuntimeProjectStore implements RuntimeProjectStore {
|
|
4
|
+
list(): Promise<RuntimeProject[]>;
|
|
5
|
+
get(id: string): Promise<RuntimeProject | undefined>;
|
|
6
|
+
create(input: RuntimeProjectInput): Promise<RuntimeProject>;
|
|
7
|
+
update(id: string, input: RuntimeProjectInput): Promise<RuntimeProject | undefined>;
|
|
8
|
+
remove(id: string): Promise<boolean>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { canonicalizeRuntimeProjectInput, runtimeProjectSchema, } from "@rynx-ai/core";
|
|
3
|
+
import { db } from "./db.js";
|
|
4
|
+
const COLUMNS = "id, name, directories_json, primary_dir, created_at, updated_at";
|
|
5
|
+
/** SQLite-backed Project catalog owned by the current target Runtime. */
|
|
6
|
+
export class SqliteRuntimeProjectStore {
|
|
7
|
+
async list() {
|
|
8
|
+
return db()
|
|
9
|
+
.prepare(`SELECT ${COLUMNS} FROM runtime_projects ORDER BY updated_at DESC, id`)
|
|
10
|
+
.all()
|
|
11
|
+
.map((row) => rowToProject(row));
|
|
12
|
+
}
|
|
13
|
+
async get(id) {
|
|
14
|
+
const row = db()
|
|
15
|
+
.prepare(`SELECT ${COLUMNS} FROM runtime_projects WHERE id = ?`)
|
|
16
|
+
.get(id);
|
|
17
|
+
return row ? rowToProject(row) : undefined;
|
|
18
|
+
}
|
|
19
|
+
async create(input) {
|
|
20
|
+
const resolved = await canonicalizeRuntimeProjectInput(input);
|
|
21
|
+
const timestamp = new Date().toISOString();
|
|
22
|
+
const project = {
|
|
23
|
+
id: randomUUID(),
|
|
24
|
+
...resolved,
|
|
25
|
+
createdAt: timestamp,
|
|
26
|
+
updatedAt: timestamp,
|
|
27
|
+
};
|
|
28
|
+
const parsed = runtimeProjectSchema.parse(project);
|
|
29
|
+
db()
|
|
30
|
+
.prepare(`INSERT INTO runtime_projects
|
|
31
|
+
(id, name, directories_json, primary_dir, created_at, updated_at)
|
|
32
|
+
VALUES (@id, @name, @directories, @primary, @createdAt, @updatedAt)`)
|
|
33
|
+
.run({
|
|
34
|
+
id: parsed.id,
|
|
35
|
+
name: parsed.name,
|
|
36
|
+
directories: JSON.stringify(parsed.directories),
|
|
37
|
+
primary: parsed.primary,
|
|
38
|
+
createdAt: parsed.createdAt,
|
|
39
|
+
updatedAt: parsed.updatedAt,
|
|
40
|
+
});
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
async update(id, input) {
|
|
44
|
+
const current = await this.get(id);
|
|
45
|
+
if (!current)
|
|
46
|
+
return undefined;
|
|
47
|
+
const resolved = await canonicalizeRuntimeProjectInput(input);
|
|
48
|
+
const project = runtimeProjectSchema.parse({
|
|
49
|
+
id: current.id,
|
|
50
|
+
...resolved,
|
|
51
|
+
createdAt: current.createdAt,
|
|
52
|
+
updatedAt: new Date().toISOString(),
|
|
53
|
+
});
|
|
54
|
+
db()
|
|
55
|
+
.prepare(`UPDATE runtime_projects
|
|
56
|
+
SET name = @name,
|
|
57
|
+
directories_json = @directories,
|
|
58
|
+
primary_dir = @primary,
|
|
59
|
+
updated_at = @updatedAt
|
|
60
|
+
WHERE id = @id`)
|
|
61
|
+
.run({
|
|
62
|
+
id: project.id,
|
|
63
|
+
name: project.name,
|
|
64
|
+
directories: JSON.stringify(project.directories),
|
|
65
|
+
primary: project.primary,
|
|
66
|
+
updatedAt: project.updatedAt,
|
|
67
|
+
});
|
|
68
|
+
return project;
|
|
69
|
+
}
|
|
70
|
+
async remove(id) {
|
|
71
|
+
return db().prepare("DELETE FROM runtime_projects WHERE id = ?").run(id).changes > 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function rowToProject(row) {
|
|
75
|
+
return runtimeProjectSchema.parse({
|
|
76
|
+
id: row.id,
|
|
77
|
+
name: row.name,
|
|
78
|
+
directories: JSON.parse(row.directories_json),
|
|
79
|
+
primary: row.primary_dir,
|
|
80
|
+
createdAt: row.created_at,
|
|
81
|
+
updatedAt: row.updated_at,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type SessionWorkspaceSnapshot } from "@rynx-ai/core";
|
|
2
|
+
export interface SessionFallbackRootOptions {
|
|
3
|
+
platform?: NodeJS.Platform;
|
|
4
|
+
home?: string;
|
|
5
|
+
env?: NodeJS.ProcessEnv;
|
|
6
|
+
readTextFile?: (path: string) => Promise<string>;
|
|
7
|
+
}
|
|
8
|
+
export interface SessionFallbackWorkspaceOptions extends SessionFallbackRootOptions {
|
|
9
|
+
now?: () => Date;
|
|
10
|
+
randomId?: () => string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the user-visible root for Sessions created without a Project.
|
|
14
|
+
*
|
|
15
|
+
* Linux desktop environments can localize or relocate Documents through the
|
|
16
|
+
* XDG user-dirs file. Headless installations commonly omit that file, so they
|
|
17
|
+
* fall back to a visible `$HOME/Rynx` instead of creating an arbitrary
|
|
18
|
+
* `$HOME/Documents` tree. Other platforms retain the existing Documents root.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveSessionFallbackRoot(options?: SessionFallbackRootOptions): Promise<string>;
|
|
21
|
+
export declare function createSessionFallbackWorkspace(options?: SessionFallbackWorkspaceOptions): Promise<SessionWorkspaceSnapshot>;
|
|
22
|
+
export declare function discardSessionFallbackWorkspace(workspace: SessionWorkspaceSnapshot, options?: SessionFallbackRootOptions): Promise<void>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, realpath, rm } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { isAbsolute, join, normalize, relative } from "node:path";
|
|
5
|
+
import { sessionWorkspaceSnapshotSchema, } from "@rynx-ai/core";
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the user-visible root for Sessions created without a Project.
|
|
8
|
+
*
|
|
9
|
+
* Linux desktop environments can localize or relocate Documents through the
|
|
10
|
+
* XDG user-dirs file. Headless installations commonly omit that file, so they
|
|
11
|
+
* fall back to a visible `$HOME/Rynx` instead of creating an arbitrary
|
|
12
|
+
* `$HOME/Documents` tree. Other platforms retain the existing Documents root.
|
|
13
|
+
*/
|
|
14
|
+
export async function resolveSessionFallbackRoot(options = {}) {
|
|
15
|
+
const platform = options.platform ?? process.platform;
|
|
16
|
+
const home = normalize(options.home ?? homedir());
|
|
17
|
+
if (!isAbsolute(home))
|
|
18
|
+
throw new Error("Session fallback home must be absolute");
|
|
19
|
+
if (platform !== "linux")
|
|
20
|
+
return join(home, "Documents", "Rynx");
|
|
21
|
+
const env = options.env ?? process.env;
|
|
22
|
+
const configHome = env.XDG_CONFIG_HOME && isAbsolute(env.XDG_CONFIG_HOME)
|
|
23
|
+
? normalize(env.XDG_CONFIG_HOME)
|
|
24
|
+
: join(home, ".config");
|
|
25
|
+
const readTextFile = options.readTextFile ?? ((path) => readFile(path, "utf8"));
|
|
26
|
+
try {
|
|
27
|
+
const contents = await readTextFile(join(configHome, "user-dirs.dirs"));
|
|
28
|
+
const documents = parseXdgDocumentsDirectory(contents, home);
|
|
29
|
+
if (documents)
|
|
30
|
+
return join(documents, "Rynx");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Missing or unreadable XDG configuration is normal on headless Linux.
|
|
34
|
+
}
|
|
35
|
+
return join(home, "Rynx");
|
|
36
|
+
}
|
|
37
|
+
export async function createSessionFallbackWorkspace(options = {}) {
|
|
38
|
+
const now = (options.now ?? (() => new Date()))();
|
|
39
|
+
const date = [
|
|
40
|
+
now.getFullYear(),
|
|
41
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
42
|
+
String(now.getDate()).padStart(2, "0"),
|
|
43
|
+
].join("-");
|
|
44
|
+
const fallbackRoot = await resolveSessionFallbackRoot(options);
|
|
45
|
+
await mkdir(fallbackRoot, { recursive: true });
|
|
46
|
+
const canonicalFallbackRoot = await realpath(fallbackRoot);
|
|
47
|
+
const dateDirectory = join(canonicalFallbackRoot, date);
|
|
48
|
+
await mkdir(dateDirectory, { recursive: true });
|
|
49
|
+
const canonicalDateDirectory = await realpath(dateDirectory);
|
|
50
|
+
if (relative(canonicalFallbackRoot, canonicalDateDirectory) !== date) {
|
|
51
|
+
throw new Error("Session fallback date directory resolves outside the managed root");
|
|
52
|
+
}
|
|
53
|
+
const randomId = options.randomId ?? randomUUID;
|
|
54
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
55
|
+
const directory = join(canonicalDateDirectory, randomId().slice(0, 8));
|
|
56
|
+
try {
|
|
57
|
+
await mkdir(directory);
|
|
58
|
+
const canonical = await realpath(directory);
|
|
59
|
+
return sessionWorkspaceSnapshotSchema.parse({
|
|
60
|
+
cwd: canonical,
|
|
61
|
+
allowedDirs: [canonical],
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error.code !== "EEXIST")
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw new Error("could not allocate a unique Session workspace directory");
|
|
70
|
+
}
|
|
71
|
+
export async function discardSessionFallbackWorkspace(workspace, options = {}) {
|
|
72
|
+
if (workspace.allowedDirs.length !== 1
|
|
73
|
+
|| workspace.allowedDirs[0] !== workspace.cwd) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let canonicalRoot;
|
|
77
|
+
try {
|
|
78
|
+
canonicalRoot = await realpath(await resolveSessionFallbackRoot(options));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const relativePath = relative(canonicalRoot, workspace.cwd);
|
|
84
|
+
const segments = relativePath.split(/[\\/]/);
|
|
85
|
+
if (isAbsolute(relativePath)
|
|
86
|
+
|| relativePath.startsWith("..")
|
|
87
|
+
|| segments.length !== 2
|
|
88
|
+
|| !/^\d{4}-\d{2}-\d{2}$/.test(segments[0] ?? "")
|
|
89
|
+
|| !/^[0-9a-f]{8}$/.test(segments[1] ?? "")) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await rm(workspace.cwd, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
function parseXdgDocumentsDirectory(contents, home) {
|
|
95
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
96
|
+
const match = line.match(/^\s*XDG_DOCUMENTS_DIR\s*=\s*"((?:\\.|[^"])*)"\s*(?:#.*)?$/);
|
|
97
|
+
if (!match)
|
|
98
|
+
continue;
|
|
99
|
+
const raw = match[1];
|
|
100
|
+
let expanded;
|
|
101
|
+
if (raw === "$HOME" || raw === "${HOME}") {
|
|
102
|
+
expanded = home;
|
|
103
|
+
}
|
|
104
|
+
else if (raw.startsWith("$HOME/")) {
|
|
105
|
+
expanded = join(home, decodeXdgPath(raw.slice("$HOME/".length)));
|
|
106
|
+
}
|
|
107
|
+
else if (raw.startsWith("${HOME}/")) {
|
|
108
|
+
expanded = join(home, decodeXdgPath(raw.slice("${HOME}/".length)));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
expanded = decodeXdgPath(raw);
|
|
112
|
+
}
|
|
113
|
+
if (!expanded.includes("\0") && isAbsolute(expanded))
|
|
114
|
+
return normalize(expanded);
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
function decodeXdgPath(value) {
|
|
119
|
+
return value.replace(/\\(.)/g, "$1");
|
|
120
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type MachineSessionRecord } from "@rynx-ai/core";
|
|
2
|
+
import type { SessionItem } from "@rynx-ai/protocol";
|
|
3
|
+
import { type MachineSessionForkOperationRecord, type MachineSessionForkStorePort } from "@rynx-ai/server";
|
|
4
|
+
/**
|
|
5
|
+
* SQLite/resource-file implementation of the fork publication boundary.
|
|
6
|
+
*
|
|
7
|
+
* Provider context is prepared before {@link commit}; this store then publishes
|
|
8
|
+
* Session metadata, an independent canonical transcript, referenced committed
|
|
9
|
+
* resources, and the completed operation in one SQLite transaction. Resource
|
|
10
|
+
* files use deterministic target ids, so a crash before the transaction can be
|
|
11
|
+
* retried safely.
|
|
12
|
+
*/
|
|
13
|
+
export declare class SqliteSessionForkStore implements MachineSessionForkStorePort {
|
|
14
|
+
private readonly resourceRoot;
|
|
15
|
+
constructor(resourceRoot?: string);
|
|
16
|
+
claim(input: {
|
|
17
|
+
operationId: string;
|
|
18
|
+
requestHash: string;
|
|
19
|
+
sourceSessionId: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
}): MachineSessionForkOperationRecord;
|
|
22
|
+
commit(input: {
|
|
23
|
+
operationId: string;
|
|
24
|
+
target: MachineSessionRecord;
|
|
25
|
+
items: SessionItem[];
|
|
26
|
+
}): void;
|
|
27
|
+
publishNativeFork(input: {
|
|
28
|
+
sourceSessionId: string;
|
|
29
|
+
target: MachineSessionRecord;
|
|
30
|
+
items: SessionItem[];
|
|
31
|
+
}): void;
|
|
32
|
+
private publishTarget;
|
|
33
|
+
markError(operationId: string, error: string): void;
|
|
34
|
+
listReserved(): MachineSessionForkOperationRecord[];
|
|
35
|
+
hasReservedSource(sessionId: string): boolean;
|
|
36
|
+
markTargetDeleted(sessionId: string): void;
|
|
37
|
+
private read;
|
|
38
|
+
private copyResourceFile;
|
|
39
|
+
private sessionDirectory;
|
|
40
|
+
private resourcePath;
|
|
41
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { newSessionId, newSessionItemId, normalizeSessionTitle, resolvedExecutionSnapshotSchema, rynxHome, sessionWorkspaceSnapshotSchema, } from "@rynx-ai/core";
|
|
5
|
+
import { MachineSessionServiceFailure, } from "@rynx-ai/server";
|
|
6
|
+
import { db } from "./db.js";
|
|
7
|
+
const FORK_COLS = [
|
|
8
|
+
"operation_id",
|
|
9
|
+
"request_hash",
|
|
10
|
+
"source_session_id",
|
|
11
|
+
"target_session_id",
|
|
12
|
+
"title",
|
|
13
|
+
"fork_position",
|
|
14
|
+
"state",
|
|
15
|
+
"error",
|
|
16
|
+
].join(", ");
|
|
17
|
+
/**
|
|
18
|
+
* SQLite/resource-file implementation of the fork publication boundary.
|
|
19
|
+
*
|
|
20
|
+
* Provider context is prepared before {@link commit}; this store then publishes
|
|
21
|
+
* Session metadata, an independent canonical transcript, referenced committed
|
|
22
|
+
* resources, and the completed operation in one SQLite transaction. Resource
|
|
23
|
+
* files use deterministic target ids, so a crash before the transaction can be
|
|
24
|
+
* retried safely.
|
|
25
|
+
*/
|
|
26
|
+
export class SqliteSessionForkStore {
|
|
27
|
+
resourceRoot;
|
|
28
|
+
constructor(resourceRoot = join(rynxHome(), "resources")) {
|
|
29
|
+
this.resourceRoot = resourceRoot;
|
|
30
|
+
}
|
|
31
|
+
claim(input) {
|
|
32
|
+
const existing = this.read(input.operationId);
|
|
33
|
+
if (existing) {
|
|
34
|
+
if (existing.request_hash !== input.requestHash ||
|
|
35
|
+
existing.source_session_id !== input.sourceSessionId) {
|
|
36
|
+
throw new MachineSessionServiceFailure("conflict", "operationId was already used for a different Session fork");
|
|
37
|
+
}
|
|
38
|
+
return toOperation(existing);
|
|
39
|
+
}
|
|
40
|
+
const now = new Date().toISOString();
|
|
41
|
+
let targetSessionId = newSessionId();
|
|
42
|
+
while (db().prepare("SELECT 1 FROM sessions WHERE id = ?").pluck().get(targetSessionId) ||
|
|
43
|
+
db().prepare("SELECT 1 FROM session_fork_operations WHERE target_session_id = ?")
|
|
44
|
+
.pluck().get(targetSessionId)) {
|
|
45
|
+
targetSessionId = newSessionId();
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
db().prepare(`INSERT INTO session_fork_operations (
|
|
49
|
+
operation_id, request_hash, source_session_id, target_session_id,
|
|
50
|
+
title, state, created_at, updated_at
|
|
51
|
+
) VALUES (?, ?, ?, ?, ?, 'reserved', ?, ?)`).run(input.operationId, input.requestHash, input.sourceSessionId, targetSessionId, normalizeSessionTitle(input.title) ?? null, now, now);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const raced = this.read(input.operationId);
|
|
55
|
+
if (raced)
|
|
56
|
+
return this.claim(input);
|
|
57
|
+
const busy = db().prepare(`SELECT operation_id
|
|
58
|
+
FROM session_fork_operations
|
|
59
|
+
WHERE source_session_id = ? AND state = 'reserved'`).get(input.sourceSessionId);
|
|
60
|
+
if (busy) {
|
|
61
|
+
throw new MachineSessionServiceFailure("conflict", "source Session already has a fork in progress");
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
return toOperation(this.read(input.operationId));
|
|
66
|
+
}
|
|
67
|
+
commit(input) {
|
|
68
|
+
const operation = this.read(input.operationId);
|
|
69
|
+
if (!operation) {
|
|
70
|
+
throw new MachineSessionServiceFailure("not_found", "Session fork operation not found");
|
|
71
|
+
}
|
|
72
|
+
if (operation.state === "target_deleted") {
|
|
73
|
+
throw new MachineSessionServiceFailure("conflict", "fork target was deleted");
|
|
74
|
+
}
|
|
75
|
+
if (operation.target_session_id !== input.target.id) {
|
|
76
|
+
throw new MachineSessionServiceFailure("conflict", "fork target identity changed");
|
|
77
|
+
}
|
|
78
|
+
if (operation.state === "completed")
|
|
79
|
+
return;
|
|
80
|
+
this.publishTarget({
|
|
81
|
+
sourceSessionId: operation.source_session_id,
|
|
82
|
+
target: input.target,
|
|
83
|
+
items: input.items,
|
|
84
|
+
operationId: input.operationId,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
publishNativeFork(input) {
|
|
88
|
+
const existing = db().prepare("SELECT forked_from_session_id FROM sessions WHERE id = ?").get(input.target.id);
|
|
89
|
+
if (existing) {
|
|
90
|
+
if (existing.forked_from_session_id === input.sourceSessionId)
|
|
91
|
+
return;
|
|
92
|
+
throw new MachineSessionServiceFailure("conflict", "target Session already exists");
|
|
93
|
+
}
|
|
94
|
+
this.publishTarget(input);
|
|
95
|
+
}
|
|
96
|
+
publishTarget(input) {
|
|
97
|
+
const itemIds = new Map();
|
|
98
|
+
for (const item of input.items)
|
|
99
|
+
itemIds.set(item.id, newSessionItemId(item.type));
|
|
100
|
+
const resourceIds = referencedResourceIds(input.items);
|
|
101
|
+
const resourceMap = new Map();
|
|
102
|
+
const resources = resourceIds.map((resourceId) => {
|
|
103
|
+
const source = db().prepare(`SELECT * FROM session_resources
|
|
104
|
+
WHERE session_id = ? AND id = ? AND state = 'committed'`).get(input.sourceSessionId, resourceId);
|
|
105
|
+
if (!source) {
|
|
106
|
+
throw new MachineSessionServiceFailure("failed_precondition", `fork history references unavailable resource ${resourceId}`);
|
|
107
|
+
}
|
|
108
|
+
const targetId = deterministicId("res", input.target.id, source.id);
|
|
109
|
+
resourceMap.set(source.id, targetId);
|
|
110
|
+
const target = {
|
|
111
|
+
...source,
|
|
112
|
+
id: targetId,
|
|
113
|
+
session_id: input.target.id,
|
|
114
|
+
client_upload_id: deterministicId("fork", input.target.id, source.client_upload_id),
|
|
115
|
+
upload_id: deterministicId("upl", input.target.id, source.upload_id),
|
|
116
|
+
message_item_id: source.message_item_id
|
|
117
|
+
? itemIds.get(source.message_item_id) ?? null
|
|
118
|
+
: null,
|
|
119
|
+
};
|
|
120
|
+
this.copyResourceFile(source, target);
|
|
121
|
+
return target;
|
|
122
|
+
});
|
|
123
|
+
const clonedItems = input.items.map((item, position) => {
|
|
124
|
+
const remapped = remapItemResources(item, resourceMap);
|
|
125
|
+
return {
|
|
126
|
+
...remapped,
|
|
127
|
+
id: itemIds.get(item.id),
|
|
128
|
+
sessionId: input.target.id,
|
|
129
|
+
position,
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
const conn = db();
|
|
133
|
+
conn.transaction(() => {
|
|
134
|
+
if (input.operationId) {
|
|
135
|
+
const current = this.read(input.operationId);
|
|
136
|
+
if (current?.state === "completed")
|
|
137
|
+
return;
|
|
138
|
+
if (!current || current.state !== "reserved") {
|
|
139
|
+
throw new MachineSessionServiceFailure("conflict", "Session fork state changed");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const existing = conn.prepare("SELECT forked_from_session_id FROM sessions WHERE id = ?").get(input.target.id);
|
|
144
|
+
if (existing?.forked_from_session_id === input.sourceSessionId)
|
|
145
|
+
return;
|
|
146
|
+
if (existing) {
|
|
147
|
+
throw new MachineSessionServiceFailure("conflict", "target Session already exists");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
conn.prepare(`INSERT INTO sessions (
|
|
151
|
+
id, workspace, execution, title, forked_from_session_id,
|
|
152
|
+
source, created_at, updated_at
|
|
153
|
+
) 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
|
+
const insertResource = conn.prepare(`INSERT INTO session_resources (
|
|
155
|
+
id, session_id, client_upload_id, upload_id, filename, media_type,
|
|
156
|
+
byte_length, uploaded_bytes, sha256, width, height, state,
|
|
157
|
+
message_item_id, created_at, committed_at
|
|
158
|
+
) VALUES (
|
|
159
|
+
@id, @session_id, @client_upload_id, @upload_id, @filename, @media_type,
|
|
160
|
+
@byte_length, @uploaded_bytes, @sha256, @width, @height, 'committed',
|
|
161
|
+
@message_item_id, @created_at, @committed_at
|
|
162
|
+
)`);
|
|
163
|
+
for (const resource of resources)
|
|
164
|
+
insertResource.run(resource);
|
|
165
|
+
const insertItem = conn.prepare(`INSERT INTO session_items (
|
|
166
|
+
id, session_id, position, response_id, type, status,
|
|
167
|
+
data, created_by, created_at
|
|
168
|
+
) VALUES (
|
|
169
|
+
@id, @session_id, @position, @response_id, @type, @status,
|
|
170
|
+
@data, @created_by, @created_at
|
|
171
|
+
)`);
|
|
172
|
+
for (const item of clonedItems) {
|
|
173
|
+
insertItem.run({
|
|
174
|
+
id: item.id,
|
|
175
|
+
session_id: item.sessionId,
|
|
176
|
+
position: item.position,
|
|
177
|
+
response_id: item.responseId,
|
|
178
|
+
type: item.type,
|
|
179
|
+
status: item.status,
|
|
180
|
+
data: JSON.stringify(item.data),
|
|
181
|
+
created_by: item.createdBy ?? null,
|
|
182
|
+
created_at: item.createdAt,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (input.operationId) {
|
|
186
|
+
const completedAt = new Date().toISOString();
|
|
187
|
+
conn.prepare(`UPDATE session_fork_operations
|
|
188
|
+
SET state = 'completed', fork_position = ?, error = NULL,
|
|
189
|
+
updated_at = ?, completed_at = ?
|
|
190
|
+
WHERE operation_id = ? AND state = 'reserved'`).run(clonedItems.at(-1)?.position ?? -1, completedAt, completedAt, input.operationId);
|
|
191
|
+
}
|
|
192
|
+
}).immediate();
|
|
193
|
+
}
|
|
194
|
+
markError(operationId, error) {
|
|
195
|
+
db().prepare(`UPDATE session_fork_operations
|
|
196
|
+
SET error = ?, updated_at = ?
|
|
197
|
+
WHERE operation_id = ? AND state = 'reserved'`).run(error.slice(0, 4_096), new Date().toISOString(), operationId);
|
|
198
|
+
}
|
|
199
|
+
listReserved() {
|
|
200
|
+
return db().prepare(`SELECT ${FORK_COLS}
|
|
201
|
+
FROM session_fork_operations
|
|
202
|
+
WHERE state = 'reserved'
|
|
203
|
+
ORDER BY created_at`).all().map(toOperation);
|
|
204
|
+
}
|
|
205
|
+
hasReservedSource(sessionId) {
|
|
206
|
+
return Boolean(db().prepare(`SELECT 1
|
|
207
|
+
FROM session_fork_operations
|
|
208
|
+
WHERE source_session_id = ? AND state = 'reserved'`).pluck().get(sessionId));
|
|
209
|
+
}
|
|
210
|
+
markTargetDeleted(sessionId) {
|
|
211
|
+
const now = new Date().toISOString();
|
|
212
|
+
db().prepare(`UPDATE session_fork_operations
|
|
213
|
+
SET state = 'target_deleted', updated_at = ?, deleted_at = ?
|
|
214
|
+
WHERE target_session_id = ? AND state IN ('reserved', 'completed')`).run(now, now, sessionId);
|
|
215
|
+
}
|
|
216
|
+
read(operationId) {
|
|
217
|
+
return db().prepare(`SELECT ${FORK_COLS}
|
|
218
|
+
FROM session_fork_operations
|
|
219
|
+
WHERE operation_id = ?`).get(operationId);
|
|
220
|
+
}
|
|
221
|
+
copyResourceFile(source, target) {
|
|
222
|
+
const sourcePath = this.resourcePath(source);
|
|
223
|
+
if (!existsSync(sourcePath)) {
|
|
224
|
+
throw new MachineSessionServiceFailure("failed_precondition", `fork resource file is missing: ${source.id}`);
|
|
225
|
+
}
|
|
226
|
+
const targetDir = this.sessionDirectory(target.session_id);
|
|
227
|
+
mkdirSync(targetDir, { recursive: true, mode: 0o700 });
|
|
228
|
+
chmodSync(targetDir, 0o700);
|
|
229
|
+
copyFileSync(sourcePath, this.resourcePath(target));
|
|
230
|
+
chmodSync(this.resourcePath(target), 0o600);
|
|
231
|
+
}
|
|
232
|
+
sessionDirectory(sessionId) {
|
|
233
|
+
const key = createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
|
|
234
|
+
return join(this.resourceRoot, key);
|
|
235
|
+
}
|
|
236
|
+
resourcePath(row) {
|
|
237
|
+
return join(this.sessionDirectory(row.session_id), `${row.id}.${extension(row.media_type)}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function toOperation(row) {
|
|
241
|
+
return {
|
|
242
|
+
operationId: row.operation_id,
|
|
243
|
+
requestHash: row.request_hash,
|
|
244
|
+
sourceSessionId: row.source_session_id,
|
|
245
|
+
targetSessionId: row.target_session_id,
|
|
246
|
+
...(row.title ? { title: row.title } : {}),
|
|
247
|
+
...(row.fork_position === null ? {} : { forkPosition: row.fork_position }),
|
|
248
|
+
state: row.state,
|
|
249
|
+
...(row.error ? { error: row.error } : {}),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function referencedResourceIds(items) {
|
|
253
|
+
const ids = new Set();
|
|
254
|
+
for (const item of items) {
|
|
255
|
+
if (item.type !== "message" || item.data.role !== "user")
|
|
256
|
+
continue;
|
|
257
|
+
for (const part of item.data.content) {
|
|
258
|
+
if (part.type === "input_image")
|
|
259
|
+
ids.add(part.resourceId);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return [...ids];
|
|
263
|
+
}
|
|
264
|
+
function remapItemResources(item, resources) {
|
|
265
|
+
const cloned = structuredClone(item);
|
|
266
|
+
if (cloned.type !== "message" || cloned.data.role !== "user")
|
|
267
|
+
return cloned;
|
|
268
|
+
cloned.data.content = cloned.data.content.map((part) => part.type === "input_image"
|
|
269
|
+
? { ...part, resourceId: resources.get(part.resourceId) ?? part.resourceId }
|
|
270
|
+
: part);
|
|
271
|
+
return cloned;
|
|
272
|
+
}
|
|
273
|
+
function deterministicId(prefix, targetSessionId, sourceId) {
|
|
274
|
+
const digest = createHash("sha256")
|
|
275
|
+
.update(`${targetSessionId}\0${sourceId}`)
|
|
276
|
+
.digest("hex");
|
|
277
|
+
return `${prefix}_${digest.slice(0, 40)}`;
|
|
278
|
+
}
|
|
279
|
+
function extension(mediaType) {
|
|
280
|
+
switch (mediaType) {
|
|
281
|
+
case "image/png":
|
|
282
|
+
return "png";
|
|
283
|
+
case "image/jpeg":
|
|
284
|
+
return "jpg";
|
|
285
|
+
case "image/webp":
|
|
286
|
+
return "webp";
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -15,6 +15,7 @@ export interface EnsurePluginSessionLaunchResult {
|
|
|
15
15
|
sessionId: string;
|
|
16
16
|
disposition: "created" | "replayed";
|
|
17
17
|
}
|
|
18
|
+
export type ReplayPluginSessionLaunchInput = Omit<EnsurePluginSessionLaunchInput, "session">;
|
|
18
19
|
export declare class SessionLaunchOperationConflictError extends Error {
|
|
19
20
|
readonly pluginId: string;
|
|
20
21
|
readonly scopeId: string;
|
|
@@ -30,12 +31,23 @@ export declare class SessionLaunchOperationTargetDeletedError extends Error {
|
|
|
30
31
|
readonly code = "SESSION_LAUNCH_OPERATION_TARGET_DELETED";
|
|
31
32
|
constructor(pluginId: string, scopeId: string, operationId: string, sessionId: string);
|
|
32
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Read the durable idempotency fence before resolving mutable launch selectors
|
|
36
|
+
* such as a Project or Agent. A replay must not fail merely because the
|
|
37
|
+
* original template was edited or deleted after the Session snapshot existed.
|
|
38
|
+
*
|
|
39
|
+
* `undefined` means the caller must resolve snapshots and proceed through
|
|
40
|
+
* {@link ensurePluginSessionLaunch}; that function remains the atomic race
|
|
41
|
+
* winner when two first attempts arrive concurrently.
|
|
42
|
+
*/
|
|
43
|
+
export declare function replayPluginSessionLaunch(input: ReplayPluginSessionLaunchInput): EnsurePluginSessionLaunchResult | undefined;
|
|
33
44
|
/**
|
|
34
45
|
* Atomically allocate a persisted machine session for one plugin operation.
|
|
35
46
|
*
|
|
36
47
|
* The durable operation row is the idempotency boundary across runner crashes,
|
|
37
|
-
* daemon restarts, and remote lease re-delivery.
|
|
38
|
-
*
|
|
48
|
+
* daemon restarts, and remote lease re-delivery. `scopeId` and `operationId`
|
|
49
|
+
* remain opaque; the Session row stores only the already-resolved workspace
|
|
50
|
+
* snapshot, never a Project identity.
|
|
39
51
|
*/
|
|
40
52
|
export declare function ensurePluginSessionLaunch(input: EnsurePluginSessionLaunchInput): EnsurePluginSessionLaunchResult;
|
|
41
53
|
export {};
|