@sessionbus/dsh 0.1.0-pre.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/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # sessionbus for DSH
2
+
3
+ `@sessionbus/dsh` connects DSH roots to sessionbus and runs daemon-managed DSH lanes.
4
+ It depends on the exact `@sessionbus/kit` prerelease `0.1.0-pre.3`.
5
+ It supports DeepSeek Harness `0.1.5-rc.2` and later; tested versions are
6
+ `0.1.5-rc.2`, `0.1.6-alpha.1`, and `0.1.6-alpha.2`.
7
+ A DSH profile must install every DSH package at one uniform DSH version; adding
8
+ one prerelease package can otherwise pull newer prereleases through DSH's caret peers.
9
+ `@sessionbus/dsh` is a pkg.pr.new preview until a separately reviewed trusted-
10
+ publishing workflow exists; its first registry version must be published manually
11
+ before trusted publishing can be configured.
12
+ The plugin reads the token once, deletes it from process.env, and retains it
13
+ nowhere in the plugin; DSH's immutable launch snapshot keeps it for the process
14
+ lifetime (trusted host).
15
+
16
+ Create the base-only lane profile with:
17
+
18
+ ```sh
19
+ dsh plugin --profile sessionbus add @sessionbus/dsh && dsh plugin --profile sessionbus exec sessionbus-dsh-install
20
+ ```
21
+
22
+ Add the peer plugin to another profile, such as `web`, with:
23
+
24
+ ```sh
25
+ dsh plugin --profile web add @sessionbus/dsh && dsh plugin --profile web exec sessionbus-dsh-install --product dsh web
26
+ ```
27
+
28
+ The installer writes only profile-local rows and leaves an existing
29
+ `sessionbus` row's other fields unchanged while adding or updating its required
30
+ `product`. With no profile arguments it configures only the `sessionbus` lane
31
+ profile and derives product `sessionbus-dsh`. Use product `dashi` for the dashi
32
+ profile, `dsh` for a standalone web or custom peer profile, or another stable
33
+ operator-chosen identifier matching `^[a-z0-9][a-z0-9-]{0,31}$`.
34
+ Non-web profiles also receive the no-upload provider required by DSH's Session
35
+ Controller; ordinary text prompts work while file-upload receipts are rejected.
36
+
37
+ Register the daemon's `sessionbus-dsh` product command as the package's
38
+ `sessionbus-dsh` bin. With a launch token it selects the installed `sessionbus`
39
+ profile; without one it forwards arguments to `dsh` unchanged.
40
+ The daemon finds that command on `PATH`, so install this package alongside
41
+ `dsh` at the host level too—for example, run `pnpm add @sessionbus/dsh` in the
42
+ directory where `dsh` is installed—so both bins share one `node_modules/.bin`.
43
+
44
+ Uninstall without invoking DSH by running the installed bin from the profile:
45
+
46
+ ```sh
47
+ pnpm --dir "$DSH_HOME/profiles/web" exec sessionbus-dsh-install --remove web
48
+ ```
49
+
50
+ When the DSH CLI works, `dsh plugin --profile web exec
51
+ sessionbus-dsh-install --remove web` is the equivalent convenience form. The
52
+ installer removes the package first, then strips its managed `sessionbus` and
53
+ `file-uploads-none` rows.
54
+
55
+ `SESSIONBUS_GROUPS` configures peer identities only. Lane membership is owned
56
+ by the daemon; the plugin accepts and does not consume groups in `session.open`.
57
+
58
+ See [Lane without a TUI](docs/LANE-WITHOUT-TUI.md) for the daemon launch contract.
package/bin.mjs ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import { install, remove } from "./install.mjs";
3
+
4
+ const profiles = [];
5
+ let product;
6
+ let removing = false;
7
+ try {
8
+ for (let index = 2; index < process.argv.length; index++) {
9
+ if (process.argv[index] === "--remove") removing = true;
10
+ else if (process.argv[index] !== "--product") profiles.push(process.argv[index]);
11
+ else if ((product = process.argv[++index]) === undefined) throw new Error("--product requires a value");
12
+ }
13
+ if (removing) remove(profiles);
14
+ else install(profiles, { product });
15
+ } catch (error) {
16
+ process.stderr.write(`sessionbus-dsh-install: ${error.message}\n`);
17
+ process.exitCode = 1;
18
+ }
@@ -0,0 +1,7 @@
1
+ # Lane without a TUI
2
+
3
+ The Sessionbus daemon starts a DSH lane directly; dashi is not involved. Its
4
+ `sessionbus-dsh` product command executes the package-owned `sessionbus-dsh` bin
5
+ with `SESSIONBUS_LAUNCH_TOKEN` set and supplies no CLI task. The bin selects
6
+ `dsh --profile sessionbus`; the profile's `product: sessionbus-dsh` row identifies
7
+ the launched product and its resident base-only DSH app.
package/install.mjs ADDED
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { spawnSync } from "node:child_process";
8
+
9
+ const profilePatch = `- id: system-prompt
10
+ config:
11
+ persona: >-
12
+ You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
13
+ - id: session-title-llm
14
+ disabled: true
15
+ - id: permission
16
+ config:
17
+ presets:
18
+ read-only: { sandbox: read-only, approval: ask }
19
+ workspace-write: { sandbox: workspace-write, approval: ask }
20
+ workspace-write-noninteractive: { sandbox: workspace-write, approval: never }
21
+ danger-full-access: { sandbox: danger-full-access, approval: never }
22
+
23
+ - insert:
24
+ - { id: workspace, name: '@deepseek-ai/dsh-workspace' }
25
+ - { id: file-uploads-none, name: '@antst/dsh-file-uploads-none' }
26
+ - { id: session-controller, name: '@deepseek-ai/dsh-api-session-controller' }
27
+ - id: sessionbus
28
+ name: '@sessionbus/dsh'
29
+ config: { mode: lane, product: sessionbus-dsh }
30
+ `;
31
+ const peerPatch = (product) => `- insert:
32
+ - { id: sessionbus, name: '@sessionbus/dsh', config: { product: ${product} } }
33
+ `;
34
+ const noUploadsPatch = `- insert:
35
+ - { id: file-uploads-none, name: '@antst/dsh-file-uploads-none' }
36
+ `;
37
+ const sessionbusID = /(?:^\s*-\s*|[{,]\s*)['"]?id['"]?\s*:\s*['"]?sessionbus['"]?(?=\s|[,}]|$)/mu;
38
+ const noUploadsID = /(?:^\s*-\s*|[{,]\s*)['"]?id['"]?\s*:\s*['"]?file-uploads-none['"]?(?=\s|[,}]|$)/mu;
39
+ const productPattern = /^[a-z0-9][a-z0-9-]{0,31}$/u;
40
+ const ownedIDs = [sessionbusID, noUploadsID];
41
+ function writeChanged(file, body) {
42
+ if (!existsSync(file) || readFileSync(file, "utf8") !== body) writeFileSync(file, body);
43
+ }
44
+
45
+ function convergePatch(file, id, patch) {
46
+ const old = existsSync(file) ? readFileSync(file, "utf8") : "";
47
+ if (id.test(old)) return;
48
+ const empty = /^(?:\s*#.*\n)*\s*\[\]\s*$/u.test(old);
49
+ writeChanged(file, `${empty ? "" : old.trimEnd() + (old.trim() ? "\n\n" : "")}${patch}`);
50
+ }
51
+
52
+ function withProduct(body, product) {
53
+ const lines = body.split("\n");
54
+ const row = lines.findIndex((line) => sessionbusID.test(line));
55
+ if (row < 0) return body;
56
+ const value = /(['"]?product['"]?\s*:\s*)(?:'[^']*'|"[^"]*"|[^,\s}]+)/u;
57
+ const column = lines[row].search(/['"]?id['"]?\s*:/u);
58
+ const open = lines[row].lastIndexOf("{", column);
59
+ if (open >= 0) {
60
+ if (value.test(lines[row])) lines[row] = lines[row].replace(value, `$1${product}`);
61
+ else if (/['"]?config['"]?\s*:\s*\{/u.test(lines[row])) lines[row] = lines[row].replace(/(['"]?config['"]?\s*:\s*\{\s*)/u, `$1product: ${product}, `);
62
+ else {
63
+ let depth = 0, close = -1;
64
+ for (let index = open; index < lines[row].length; index++) {
65
+ if (lines[row][index] === "{") depth++;
66
+ if (lines[row][index] === "}" && --depth === 0) { close = index; break; }
67
+ }
68
+ lines[row] = `${lines[row].slice(0, close)}, config: { product: ${product} }${lines[row].slice(close)}`;
69
+ }
70
+ return lines.join("\n");
71
+ }
72
+ const indent = lines[row].match(/^\s*/u)[0].length;
73
+ let end = row + 1;
74
+ while (end < lines.length && !new RegExp(`^\\s{0,${indent}}-\\s+`, "u").test(lines[end])) end++;
75
+ for (let index = row; index < end; index++) {
76
+ if (value.test(lines[index])) { lines[index] = lines[index].replace(value, `$1${product}`); return lines.join("\n"); }
77
+ }
78
+ const config = lines.slice(row, end).findIndex((line) => /^\s*['"]?config['"]?\s*:/u.test(line));
79
+ if (config >= 0) {
80
+ const index = row + config;
81
+ if (lines[index].includes("{")) lines[index] = lines[index].replace(/\{\s*/u, `{ product: ${product}, `);
82
+ else lines.splice(index + 1, 0, `${lines[index].match(/^\s*/u)[0]} product: ${product}`);
83
+ } else {
84
+ if (end === lines.length && lines.at(-1) === "") end--;
85
+ lines.splice(end, 0, `${" ".repeat(indent + 2)}config: { product: ${product} }`);
86
+ }
87
+ return lines.join("\n");
88
+ }
89
+
90
+ function convergeSessionbus(file, product) {
91
+ const old = existsSync(file) ? readFileSync(file, "utf8") : "";
92
+ if (sessionbusID.test(old)) return writeChanged(file, withProduct(old, product));
93
+ convergePatch(file, sessionbusID, peerPatch(product));
94
+ }
95
+
96
+ function withoutOwnedRows(body) {
97
+ const lines = body.split(/(?<=\n)/u);
98
+ for (let row = lines.length - 1; row >= 0; row--) {
99
+ const text = lines[row].replace(/\r?\n$/u, "");
100
+ if (!ownedIDs.some((id) => id.test(text))) continue;
101
+ const indent = text.match(/^\s*/u)[0].length;
102
+ let end = row + 1;
103
+ if (!text.includes("{")) {
104
+ while (end < lines.length) {
105
+ const next = lines[end].replace(/\r?\n$/u, "");
106
+ const nextIndent = next.match(/^\s*/u)[0].length;
107
+ if ((/^\s*-\s+/u.test(next) || /^\s*#/u.test(next)) && nextIndent <= indent) break;
108
+ end++;
109
+ }
110
+ }
111
+ lines.splice(row, end - row);
112
+ }
113
+ for (let row = lines.length - 1; row >= 0; row--) {
114
+ const match = /^(\s*)-\s+insert:\s*(?:#.*)?(?:\r?\n)?$/u.exec(lines[row]);
115
+ if (!match) continue;
116
+ let child = row + 1;
117
+ while (child < lines.length && /^\s*(?:#.*)?(?:\r?\n)?$/u.test(lines[child])) child++;
118
+ if (child === lines.length || lines[child].match(/^\s*/u)[0].length <= match[1].length) {
119
+ if (row > 0 && /^\s*(?:\r?\n)?$/u.test(lines[row - 1])) lines.splice(row - 1, 2);
120
+ else lines.splice(row, 1);
121
+ }
122
+ }
123
+ const result = lines.join("");
124
+ return result.trim() ? result : "[]\n";
125
+ }
126
+
127
+ function validProfile(name) {
128
+ return name && !name.includes("/") && !name.includes("\\") && name !== "." && name !== ".." && name !== "node_modules";
129
+ }
130
+
131
+ export function install(profileNames = [], options = {}) {
132
+ const home = options.home || process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
133
+ const root = options.root || path.dirname(fileURLToPath(import.meta.url));
134
+ const run = options.run || ((args) => spawnSync("dsh", args, { cwd: root, encoding: "utf8" }));
135
+ const names = [...new Set(profileNames.length ? profileNames : ["sessionbus"])];
136
+ if (options.product !== undefined && (typeof options.product !== "string" || !productPattern.test(options.product))) throw new Error(`invalid product ${JSON.stringify(options.product)}; expected ^[a-z0-9][a-z0-9-]{0,31}$`);
137
+ if (names.includes("sessionbus") && options.product !== undefined && options.product !== "sessionbus-dsh") throw new Error("the sessionbus profile product must be sessionbus-dsh");
138
+ if (names.some((name) => name !== "sessionbus") && options.product === undefined) throw new Error("--product is required for peer profiles");
139
+ for (const name of names) {
140
+ if (!validProfile(name)) throw new Error(`invalid profile name ${JSON.stringify(name)}`);
141
+ const profile = path.join(home, "profiles", name);
142
+ const manifestFile = path.join(profile, "package.json");
143
+ let manifest = existsSync(manifestFile) ? JSON.parse(readFileSync(manifestFile, "utf8")) : null;
144
+ if (!manifest?.dependencies?.["@sessionbus/dsh"]) {
145
+ const result = run(["plugin", "--profile", name, "add", "@sessionbus/dsh"]);
146
+ if (result.status !== 0) throw new Error(String(result.stderr || "dsh plugin add failed").trim());
147
+ manifest = JSON.parse(readFileSync(manifestFile, "utf8"));
148
+ }
149
+ if (name !== "sessionbus") {
150
+ const patch = path.join(profile, "cordis.patch.yml");
151
+ convergeSessionbus(patch, options.product);
152
+ if (name !== "web") convergePatch(patch, noUploadsID, noUploadsPatch);
153
+ continue;
154
+ }
155
+ manifest = {
156
+ name: "dsh-profile-sessionbus",
157
+ private: true,
158
+ dependencies: { "@sessionbus/dsh": manifest.dependencies["@sessionbus/dsh"] },
159
+ dsh: { profile: { bundles: ["@deepseek-ai/dsh-base"], patchReload: "startup" } },
160
+ };
161
+ writeChanged(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`);
162
+ writeChanged(path.join(profile, "cordis.patch.yml"), profilePatch);
163
+ }
164
+ }
165
+
166
+ export function remove(profileNames = [], options = {}) {
167
+ if (profileNames.length === 0) throw new Error("--remove requires at least one profile");
168
+ const home = options.home || process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
169
+ const run = options.run || ((args, cwd) => spawnSync("pnpm", args, { cwd, encoding: "utf8" }));
170
+ for (const name of [...new Set(profileNames)]) {
171
+ if (!validProfile(name)) throw new Error(`invalid profile name ${JSON.stringify(name)}`);
172
+ const profile = path.join(home, "profiles", name);
173
+ const result = run(["remove", "@sessionbus/dsh"], profile);
174
+ if (result.status !== 0) throw new Error(String(result.stderr || result.error?.message || "pnpm remove failed").trim());
175
+ const patch = path.join(profile, "cordis.patch.yml");
176
+ if (existsSync(patch)) writeChanged(patch, withoutOwnedRows(readFileSync(patch, "utf8")));
177
+ }
178
+ }
package/launcher.mjs ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ // Signal and exit forwarding mirrors @antst/dashi-launcher's bin/dashi.js.
3
+ import { spawn } from "node:child_process";
4
+
5
+ const args = process.env.SESSIONBUS_LAUNCH_TOKEN === undefined
6
+ ? process.argv.slice(2)
7
+ : ["--profile", "sessionbus", ...process.argv.slice(2)];
8
+ const child = spawn("dsh", args, { stdio: "inherit" });
9
+ const signals = ["SIGINT", "SIGTERM"];
10
+ const forwards = signals.map((signal) => [signal, () => child.kill(signal)]);
11
+ for (const [signal, forward] of forwards) process.on(signal, forward);
12
+
13
+ let failed = false;
14
+ child.on("error", (error) => {
15
+ failed = true;
16
+ const missing = error.code === "ENOENT";
17
+ process.stderr.write(missing
18
+ ? "sessionbus-dsh: dsh not found; install it with 'npm install --global @deepseek-ai/dsh'\n"
19
+ : `sessionbus-dsh: could not start dsh: ${error.message}\n`);
20
+ process.exitCode = missing ? 127 : 1;
21
+ });
22
+ child.on("close", (code, signal) => {
23
+ for (const [name, forward] of forwards) process.off(name, forward);
24
+ if (failed) return;
25
+ if (code !== null) process.exitCode = code;
26
+ else if (signal !== null) process.kill(process.pid, signal);
27
+ });
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@sessionbus/dsh",
3
+ "version": "0.1.0-pre.2",
4
+ "type": "commonjs",
5
+ "main": "plugin.cjs",
6
+ "exports": "./plugin.cjs",
7
+ "bin": {
8
+ "sessionbus-dsh": "launcher.mjs",
9
+ "sessionbus-dsh-install": "bin.mjs"
10
+ },
11
+ "files": ["README.md", "docs/LANE-WITHOUT-TUI.md", "plugin.cjs", "launcher.mjs", "bin.mjs", "install.mjs"],
12
+ "scripts": {
13
+ "test": "node --test plugin.test.cjs install.test.mjs launcher.test.mjs package.test.cjs rename.test.mjs"
14
+ },
15
+ "dependencies": {
16
+ "@antst/dsh-file-uploads-none": "0.1.0-alpha.18",
17
+ "@sessionbus/kit": "0.1.0-pre.3"
18
+ },
19
+ "peerDependencies": {
20
+ "@deepseek-ai/cordis": "^4.0.2",
21
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
22
+ "@deepseek-ai/dsh-agent": ">=0.1.5-rc.2",
23
+ "@deepseek-ai/dsh-api-session-controller": ">=0.1.5-rc.2",
24
+ "@deepseek-ai/dsh-cmdline": ">=0.1.5-rc.2",
25
+ "@deepseek-ai/dsh-commands": ">=0.1.5-rc.2",
26
+ "@deepseek-ai/dsh-launch-environment": ">=0.1.5-rc.2",
27
+ "@deepseek-ai/dsh-llm": ">=0.1.5-rc.2",
28
+ "@deepseek-ai/dsh-permission-presets": ">=0.1.5-rc.2",
29
+ "@deepseek-ai/dsh-session": ">=0.1.5-rc.2",
30
+ "@deepseek-ai/dsh-session-title": ">=0.1.5-rc.2",
31
+ "@deepseek-ai/dsh-tools": ">=0.1.5-rc.2",
32
+ "@deepseek-ai/dsh-workspace": ">=0.1.5-rc.2"
33
+ },
34
+ "publishConfig": { "access": "public" },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/antst/sessionbus-dsh.git"
38
+ }
39
+ }
package/plugin.cjs ADDED
@@ -0,0 +1,388 @@
1
+ "use strict";
2
+
3
+ const os = require("node:os");
4
+ const path = require("node:path");
5
+ const kit = require("@sessionbus/kit");
6
+ const version = require("./package.json").version;
7
+ const { ACTIONS } = kit;
8
+
9
+ // Match the MCP tool's closed union; the public kit validates each action.
10
+ function argumentSchema() {
11
+ const properties = {};
12
+ for (const field of ["session_id", "host", "message", "target", "group", "product", "name", "resume_session_id", "notify_target", "input", "run_id"]) properties[field] = { type: "string" };
13
+ for (const field of ["targets", "extra_groups"]) properties[field] = { type: "array", items: { type: "string" } };
14
+ for (const field of ["persistent", "notify", "forget"]) properties[field] = { type: "boolean" };
15
+ for (const field of ["auto_close_ms", "timeout_ms"]) properties[field] = { type: "integer" };
16
+ properties.idle_message = { type: "string", enum: ["stage", "run"] };
17
+ properties.trace = { type: "string", enum: ["off", "events", "content"] };
18
+ properties.mode = { type: "string", enum: ["off", "events", "content"] };
19
+ const open = {};
20
+ for (const field of ["cwd", "permission_mode", "model", "reasoning_effort"]) open[field] = { type: "string" };
21
+ open.arguments = { type: "array", items: { type: "string" } };
22
+ properties.open = { type: "object", additionalProperties: false, properties: open };
23
+ return { type: "object", additionalProperties: false, properties, description: "Use only the fields listed for the selected action in the tool description. send has no summary field; put the complete content in message." };
24
+ }
25
+
26
+ function assertKnownArguments(value, schema, location = "arguments") {
27
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`sessionbus ${location} must be an object`);
28
+ for (const field of Object.keys(value)) {
29
+ if (!Object.hasOwn(schema.properties, field)) throw new Error(`sessionbus ${location}.${field} is not supported`);
30
+ const property = schema.properties[field];
31
+ if (property.type === "object") assertKnownArguments(value[field], property, `${location}.${field}`);
32
+ }
33
+ }
34
+
35
+ const name = "sessionbus-dsh";
36
+ const productPattern = /^[a-z0-9][a-z0-9-]{0,31}$/u;
37
+ const inject = [
38
+ "agents", "appReady", "appExit", "commands", "permissionPresets",
39
+ "sessionController", "sessions", "sessionTitle", "tools",
40
+ ];
41
+ function deferred() {
42
+ let resolve, reject;
43
+ const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
44
+ return { promise, resolve, reject };
45
+ }
46
+
47
+ function text(value) {
48
+ return typeof value === "string" && value.length > 0 && value.trim() === value && !/[\0\r\n]/u.test(value);
49
+ }
50
+
51
+ function clean(error) {
52
+ return String(error?.message || error || "DSH plugin failed").replace(/[\0\r\n]/gu, " ").slice(0, 4096);
53
+ }
54
+
55
+ function readConfiguration(ctx, config = {}, ambient = process.env) {
56
+ if (config.product === undefined) throw new Error("product is required; re-run sessionbus-dsh-install --product <name> <profile>");
57
+ if (typeof config.product !== "string" || !productPattern.test(config.product)) throw new Error("product must match ^[a-z0-9][a-z0-9-]{0,31}$");
58
+ const launch = ctx.launchEnvironment || ctx.get?.("launchEnvironment");
59
+ const value = (key) => {
60
+ if (launch) return launch.get(key)?.value;
61
+ return Object.hasOwn(ambient, key) ? ambient[key] : undefined;
62
+ };
63
+ const token = value("SESSIONBUS_LAUNCH_TOKEN");
64
+ delete process.env.SESSIONBUS_LAUNCH_TOKEN;
65
+ const mode = token === undefined ? "peer" : "lane";
66
+ if (config.mode !== undefined && config.mode !== mode) throw new Error(`mode ${config.mode} conflicts with launch environment`);
67
+ let groups = mode === "lane" ? [] : Object.hasOwn(config, "groups") ? config.groups : JSON.parse(value("SESSIONBUS_GROUPS") || "[]");
68
+ if (!Array.isArray(groups) || groups.some((group) => !text(group)) || new Set(groups).size !== groups.length) throw new Error("groups are invalid");
69
+ const socket = Object.hasOwn(config, "socket") ? config.socket : value("SESSIONBUS_SOCKET") || path.join(value("XDG_STATE_HOME") || path.join(value("HOME") || os.homedir(), ".local/state"), "sessionbus/run/presence.sock");
70
+ const localKey = Object.hasOwn(config, "local_key") ? config.local_key : value("SESSIONBUS_LOCAL_KEY");
71
+ if (!text(socket) || localKey !== undefined && !text(localKey)) throw new Error("connection settings are invalid");
72
+ return { settings: { mode, product: config.product, groups: [...groups], socket, localKey }, token };
73
+ }
74
+
75
+ function captureContext(ctx) {
76
+ const services = Object.fromEntries(inject.map((service) => [service, ctx[service]]));
77
+ let launchEnvironment;
78
+ try { launchEnvironment = ctx.launchEnvironment || ctx.get?.("launchEnvironment"); } catch {}
79
+ const fiber = ctx.fiber;
80
+ return {
81
+ active: () => fiber?.uid !== null,
82
+ context: {
83
+ ...services,
84
+ launchEnvironment,
85
+ on: ctx.on.bind(ctx),
86
+ effect: ctx.effect.bind(ctx),
87
+ },
88
+ };
89
+ }
90
+
91
+ function settings(ctx, config = {}, ambient = process.env) {
92
+ return readConfiguration(ctx, config, ambient).settings;
93
+ }
94
+
95
+ function connectionEnvironment(config, token) {
96
+ return {
97
+ SESSIONBUS_SOCKET: config.socket,
98
+ ...(config.localKey === undefined ? {} : { SESSIONBUS_LOCAL_KEY: config.localKey }),
99
+ ...(token === undefined ? {} : { SESSIONBUS_LAUNCH_TOKEN: token }),
100
+ };
101
+ }
102
+
103
+ function nativeName(value) {
104
+ const at = value.lastIndexOf("@");
105
+ return at < 0 ? value : value.slice(0, at);
106
+ }
107
+
108
+ function textOf(message) {
109
+ return Array.isArray(message?.content) ? message.content.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("") : "";
110
+ }
111
+
112
+ function terminal(reason) {
113
+ const native_stop_reason = reason?.kind;
114
+ if (native_stop_reason === "completed") return { outcome: "completed", native_stop_reason };
115
+ if (native_stop_reason === "aborted" || native_stop_reason === "interrupted") return { outcome: "interrupted", native_stop_reason };
116
+ if (["blocked", "error", "max-tokens"].includes(native_stop_reason)) return { outcome: "failed", native_stop_reason };
117
+ throw new Error(`unknown DSH turn end reason ${JSON.stringify(reason)}`);
118
+ }
119
+
120
+ class NativeSession {
121
+ constructor(ctx, createUserMessage) {
122
+ this.ctx = ctx;
123
+ this.createUserMessage = createUserMessage;
124
+ this.receipts = new Map();
125
+ this.removeEvents = ctx.on("session/event", (session, event) => this.event(session, event), { global: true });
126
+ }
127
+
128
+ async open(request) {
129
+ const options = request.open || {};
130
+ let resolved;
131
+ if (request.resume_session_id) {
132
+ resolved = await this.ctx.sessionController.resolveAgent(request.resume_session_id);
133
+ } else {
134
+ const created = await this.ctx.sessionController.create(options.cwd ? { cwd: options.cwd } : {});
135
+ resolved = await this.ctx.sessionController.resolveAgent(created.sessionId);
136
+ }
137
+ if (resolved.error) throw resolved.error;
138
+ const agent = resolved.agent;
139
+ const expected = request.resume_session_id || agent.id;
140
+ if (agent.id !== expected || agent.session.id !== expected) throw new Error("DSH resolved a different native session");
141
+ if (options.cwd !== undefined && agent.session.header.cwd !== options.cwd) throw new Error("DSH resolved a different native cwd");
142
+ await this.ctx.sessionController.rename({ sessionId: agent.id, title: nativeName(request.name) });
143
+ if (options.model !== undefined || options.reasoning_effort !== undefined) {
144
+ const model = options.model || `${agent.options?.provider || ""}/${agent.options?.model || ""}`;
145
+ const slash = model.indexOf("/");
146
+ if (slash < 1 || slash === model.length - 1) throw new Error(`unsupported value model=${model}`);
147
+ await this.ctx.sessionController.selectModel({ sessionId: agent.id, provider: model.slice(0, slash), model: model.slice(slash + 1), ...(options.reasoning_effort === undefined ? {} : { reasoningEffort: options.reasoning_effort }) });
148
+ }
149
+ if (options.permission_mode !== undefined) {
150
+ if (!this.ctx.permissionPresets.names.includes(options.permission_mode)) throw new Error(`unsupported value permission_mode=${options.permission_mode}`);
151
+ this.ctx.permissionPresets.set(agent.session, options.permission_mode);
152
+ }
153
+ await this.ctx.sessions.flush(agent.session);
154
+ this.agent = agent;
155
+ return { session_id: agent.id };
156
+ }
157
+
158
+ message(body) {
159
+ return this.createUserMessage({ content: [{ type: "text", text: body }], source: { kind: "plugin", plugin: name, form: "relay" } });
160
+ }
161
+
162
+ receipt(message, session, cancel) {
163
+ if (cancel?.aborted) throw cancel.reason || new Error("cancelled");
164
+ const accepted = { ...deferred(), session };
165
+ const abort = () => accepted.reject(cancel.reason || new Error("cancelled"));
166
+ cancel?.addEventListener("abort", abort, { once: true });
167
+ accepted.promise.catch(() => {});
168
+ accepted.close = () => {
169
+ cancel?.removeEventListener("abort", abort);
170
+ if (this.receipts.get(message.id) === accepted) this.receipts.delete(message.id);
171
+ };
172
+ this.receipts.set(message.id, accepted);
173
+ return accepted;
174
+ }
175
+
176
+ event(session, event) {
177
+ if (event.type === "agent/inbox/spliced") {
178
+ for (const message of event.data.inserted || []) {
179
+ const receipt = this.receipts.get(message.id);
180
+ if (receipt?.session === session) receipt.resolve();
181
+ }
182
+ }
183
+ const run = this.active;
184
+ if (!run || session !== this.agent?.session) return;
185
+ if (event.type === "turn/start") run.openTurn = event.data.turn;
186
+ if (event.type === "user/message" && event.data.id === run.message.id) {
187
+ if (!Number.isSafeInteger(run.openTurn)) {
188
+ run.endError = new Error("DSH consumed input outside a turn");
189
+ return;
190
+ }
191
+ run.turn = run.openTurn;
192
+ }
193
+ if (event.type === "assistant/message" && run.turn === event.data.turn) run.output += textOf(event.data.message);
194
+ if (event.type === "turn/end" && run.turn === event.data.turn) {
195
+ try { run.end = terminal(event.data.reason); }
196
+ catch (error) { run.endError = error; }
197
+ }
198
+ }
199
+
200
+ async run(cancel, token, input) {
201
+ if (token.Interrupted()) return { outcome: "interrupted", result: "" };
202
+ const message = this.message(input);
203
+ const record = { message, openTurn: null, turn: null, output: "", cancelled: deferred() };
204
+ const receipt = this.receipt(message, this.agent.session, cancel);
205
+ this.active = record;
206
+ token.Native = record;
207
+ record.cancelled.promise.catch(() => {});
208
+ const abort = () => { record.cancelled.reject(cancel.reason || new Error("cancelled")); this.receipts.get(message.id)?.reject(cancel.reason || new Error("cancelled")); };
209
+ cancel.addEventListener("abort", abort, { once: true });
210
+ try {
211
+ try { this.agent.followup(message); }
212
+ catch (error) { receipt.reject(error); throw error; }
213
+ await receipt.promise;
214
+ const idle = this.agent.whenIdle();
215
+ idle.catch(() => {});
216
+ await Promise.race([idle, record.cancelled.promise]);
217
+ if (record.endError) throw record.endError;
218
+ if (record.end) return { ...record.end, result: record.output };
219
+ if (record.cancelReason) return { outcome: "interrupted", native_stop_reason: record.cancelReason, result: record.output };
220
+ throw new Error("DSH reached idle without turn/end");
221
+ } finally {
222
+ receipt.close();
223
+ cancel.removeEventListener("abort", abort);
224
+ if (this.active === record) this.active = null;
225
+ if (token.Native === record) token.Native = null;
226
+ }
227
+ }
228
+
229
+ interrupt(_cancel, token) {
230
+ if (token.Native) {
231
+ token.Native.cancelReason = "aborted:user";
232
+ this.agent.cancel({ kind: "user" }, { keepInbox: true });
233
+ }
234
+ }
235
+
236
+ async deliver(cancel, request, agent = this.agent) {
237
+ const message = this.message(request.body);
238
+ if (agent.status === "running") {
239
+ const receipt = this.receipt(message, agent.session, cancel);
240
+ try {
241
+ try { agent.steer(message); }
242
+ catch (error) { receipt.reject(error); throw error; }
243
+ await receipt.promise;
244
+ } finally { receipt.close(); }
245
+ } else {
246
+ const event = await agent.session.append("user/message", message, { surfaceOp: "append" });
247
+ if (event?.type !== "user/message" || event.data?.id !== message.id) throw new Error("DSH did not commit the delivered message");
248
+ }
249
+ return { disposition: "injected" };
250
+ }
251
+
252
+ async close() {
253
+ if (!this.agent) return;
254
+ if (this.agent.status === "running") this.agent.cancel({ kind: "disposed" });
255
+ await this.agent.whenIdle();
256
+ await this.ctx.sessions.flush(this.agent.session);
257
+ this.removeEvents();
258
+ }
259
+ }
260
+
261
+ function identity(ctx, agent, product, groups, title) {
262
+ const sessionID = String(agent?.session?.id || agent?.id || "");
263
+ const cwd = agent?.session?.header?.cwd;
264
+ if (!text(sessionID) || !text(cwd)) throw new Error("DSH root identity is incomplete");
265
+ const current = title === undefined ? ctx.sessionTitle.get(agent.session)?.title : title;
266
+ const provider = agent.options?.provider;
267
+ const model = agent.options?.model;
268
+ return { product, session_id: sessionID, name: text(current) ? current : sessionID, groups: [...groups], info: { cwd, ...(text(provider) && text(model) ? { model: `${provider}/${model}` } : {}) } };
269
+ }
270
+
271
+ function createRuntime(ctx, config, dependencies, prepared) {
272
+ const configured = prepared || readConfiguration(ctx, config, dependencies.ambient);
273
+ const values = configured.settings;
274
+ const active = dependencies.active || (() => true);
275
+ let launchToken = configured.token;
276
+ const native = new NativeSession(ctx, dependencies.createUserMessage);
277
+ const peers = new Map();
278
+ let worker;
279
+ let workerExit = Promise.resolve();
280
+ let ready = false;
281
+ let warned = false;
282
+ const warn = (error) => { if (active() && !warned) { warned = true; dependencies.stderr(`sessionbus: ${clean(error)}\n`); } };
283
+ const root = (agent) => ctx.agents.roots().includes(agent);
284
+ const present = (agent) => {
285
+ if (values.mode !== "peer" || !ready || !root(agent) || peers.has(agent)) return;
286
+ try {
287
+ const current = identity(ctx, agent, values.product, values.groups);
288
+ const peer = dependencies.connectPeer(current, (cancel, request) => native.deliver(cancel, request, agent), connectionEnvironment(values));
289
+ peers.set(agent, { peer, identity: current, rehello: Promise.resolve() });
290
+ } catch (error) { warn(error); }
291
+ };
292
+ const forget = (agent) => { peers.get(agent)?.peer.shutdown(); peers.delete(agent); };
293
+ let removeCreated = () => {}, removeDisposed = () => {}, removeTitle = () => {};
294
+ if (values.mode === "peer") {
295
+ removeCreated = ctx.on("agent/created", ({ agent }) => present(agent));
296
+ removeDisposed = ctx.on("agent/disposed", ({ agent }) => forget(agent));
297
+ removeTitle = ctx.on("session/event", (session, event) => {
298
+ if (event.type !== "session/title") return;
299
+ const agent = ctx.agents.get(session.id);
300
+ const record = peers.get(agent);
301
+ if (!record || agent.session !== session) return;
302
+ const next = identity(ctx, agent, values.product, values.groups, event.data.title);
303
+ record.identity = next;
304
+ record.rehello = record.rehello.then(() => record.peer.rehello({ name: next.name, info: next.info })).catch(warn);
305
+ }, { global: true });
306
+ }
307
+ const caller = (agent) => worker && native.agent === agent ? worker.caller : peers.get(agent)?.peer.caller;
308
+ const argumentsSchema = argumentSchema();
309
+ const execute = (argumentsValue, execution) => {
310
+ const client = caller(execution?.agent);
311
+ if (!client) throw new Error("sessionbus requires an exact live DSH root");
312
+ const args = argumentsValue.arguments || {};
313
+ assertKnownArguments(args, argumentsSchema);
314
+ return client.action(argumentsValue.action, args);
315
+ };
316
+ const removeTool = ctx.tools.register(dependencies.defineTool({
317
+ name: "sessionbus",
318
+ description: "List, message, spawn and control Sessionbus sessions. Use trace mode off, events or content to configure live parent tracing for a direct child; spawn trace sets its initial mode.",
319
+ parameters: { action: { type: "string", enum: ACTIONS, required: true }, arguments: argumentsSchema },
320
+ output: { schema: { type: "object", additionalProperties: true, properties: {} }, render: (_args, result) => [{ type: "text", text: JSON.stringify(result) }] },
321
+ execute,
322
+ }));
323
+ const removeCommand = ctx.commands.register({
324
+ name: "sessionbus",
325
+ description: "list sessionbus sessions",
326
+ handler: async (invocation) => ({ kind: "success", text: JSON.stringify(await execute({ action: "list", arguments: {} }, { agent: invocation.agent })) }),
327
+ });
328
+ const start = () => {
329
+ ready = true;
330
+ if (values.mode === "lane") {
331
+ const environment = connectionEnvironment(values, launchToken);
332
+ launchToken = undefined;
333
+ worker = dependencies.serveWorker({
334
+ hello: () => ({ product: values.product, version, supported_open_fields: ["cwd", "permission_mode", "model", "reasoning_effort"], extra_arguments: [] }),
335
+ open: (_cancel, request) => native.open(request),
336
+ run: (cancel, token, input) => native.run(cancel, token, input),
337
+ interrupt: (cancel, token) => native.interrupt(cancel, token),
338
+ deliver: (cancel, request) => native.deliver(cancel, request),
339
+ close: () => native.close(),
340
+ }, environment);
341
+ workerExit = worker.closed.then(async () => {
342
+ const failure = await worker.serving?.catch((error) => error);
343
+ if (!active()) return;
344
+ if (failure && !worker.opened) { warn(failure); ctx.appExit(1); } else ctx.appExit(0);
345
+ });
346
+ } else {
347
+ for (const agent of ctx.agents.roots()) present(agent);
348
+ }
349
+ };
350
+ const removeReady = ctx.appReady.onReady(start);
351
+ const close = () => {
352
+ removeReady(); removeCreated(); removeDisposed(); removeTitle(); removeCommand(); removeTool();
353
+ for (const agent of peers.keys()) forget(agent);
354
+ worker?.shutdown(); native.removeEvents();
355
+ };
356
+ ctx.effect(() => close, "sessionbus-dsh.lifecycle");
357
+ return { close, execute, native, peers, settings: values, start, get worker() { return worker; }, get workerExit() { return workerExit; } };
358
+ }
359
+
360
+ async function activate(ctx, config = {}, dependencies = {}, prepared) {
361
+ const configured = prepared || readConfiguration(ctx, config, dependencies.ambient);
362
+ const [{ createUserMessage }, { defineTool }] = await Promise.all([
363
+ dependencies.createUserMessage ? { createUserMessage: dependencies.createUserMessage } : (dependencies.importLLM || (() => import("@deepseek-ai/dsh-llm")))(),
364
+ dependencies.defineTool ? { defineTool: dependencies.defineTool } : (dependencies.importTools || (() => import("@deepseek-ai/dsh-tools")))(),
365
+ ]);
366
+ if (dependencies.active && !dependencies.active()) return;
367
+ return createRuntime(ctx, config, { ...kit, active: () => true, stderr: (line) => process.stderr.write(line), ...dependencies, createUserMessage, defineTool }, configured);
368
+ }
369
+
370
+ function apply(ctx, config = {}, dependencies = {}) {
371
+ let captured, prepared;
372
+ try {
373
+ captured = captureContext(ctx);
374
+ prepared = readConfiguration(captured.context, config, dependencies.ambient);
375
+ } catch (error) {
376
+ (dependencies.stderr || ((line) => process.stderr.write(line)))(`sessionbus: ${clean(error)}\n`);
377
+ try { ctx.appExit(1); } catch {}
378
+ return;
379
+ }
380
+ const runtimeDependencies = { stderr: (line) => process.stderr.write(line), ...dependencies, active: captured.active };
381
+ void activate(captured.context, config, runtimeDependencies, prepared).catch((error) => {
382
+ if (!captured.active()) return;
383
+ runtimeDependencies.stderr(`sessionbus: ${clean(error)}\n`);
384
+ captured.context.appExit(1);
385
+ });
386
+ }
387
+
388
+ module.exports = { ACTIONS, NativeSession, activate, apply, connectionEnvironment, createRuntime, identity, inject, name, settings, terminal };