negotium 0.2.13 → 0.2.14
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/dist/agent-helpers.js +13 -12
- package/dist/agent-helpers.js.map +6 -6
- package/dist/background-bash.js +11 -11
- package/dist/background-bash.js.map +4 -4
- package/dist/browser-runtime.js +2 -3
- package/dist/browser-runtime.js.map +3 -3
- package/dist/{chunk-rnsnhcye.js → chunk-5g5a63vv.js} +11 -11
- package/dist/{chunk-rnsnhcye.js.map → chunk-5g5a63vv.js.map} +4 -4
- package/dist/hosted-agent.js +12 -12
- package/dist/hosted-agent.js.map +5 -5
- package/dist/main.js +582 -357
- package/dist/main.js.map +12 -11
- package/dist/mcp-factories.js +13 -12
- package/dist/mcp-factories.js.map +6 -6
- package/dist/mcp-servers.js +3 -2
- package/dist/mcp-servers.js.map +3 -3
- package/dist/prompts.js +2 -3
- package/dist/prompts.js.map +3 -3
- package/dist/query-runtime.js +2 -3
- package/dist/query-runtime.js.map +3 -3
- package/dist/registry.js +3 -3
- package/dist/registry.js.map +2 -2
- package/dist/rollout.js +1 -1
- package/dist/runtime/src/index.ts +9 -0
- package/dist/runtime/src/node-host.ts +9 -0
- package/dist/runtime/src/platform/background-bash/manager.ts +30 -22
- package/dist/runtime/src/platform/config.ts +8 -7
- package/dist/runtime/src/runtime/bashrs-completions.ts +74 -14
- package/dist/runtime/src/runtime/turn-runner.ts +6 -0
- package/dist/runtime/src/version.ts +1 -1
- package/dist/runtime-helpers.js +2 -3
- package/dist/runtime-helpers.js.map +3 -3
- package/dist/types/apps/negotium/src/mcp-servers.d.ts +16 -3
- package/dist/types/packages/core/src/platform/background-bash/manager.d.ts +6 -1
- package/dist/types/packages/core/src/platform/config.d.ts +8 -6
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/dist/vault.js +2 -3
- package/dist/vault.js.map +3 -3
- package/install-bash-rs.mjs +5 -5
- package/package.json +1 -1
- package/dist/runtime/src/mcp/background-bash-server.ts +0 -756
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type ChildProcess, execFileSync, spawn } from "node:child_process";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import {
|
|
4
|
-
BACKGROUND_BASH_SERVER,
|
|
5
4
|
BASH_RS_BIN,
|
|
5
|
+
BASH_RS_VERSION,
|
|
6
6
|
BASHRS_SPILL_ROOT,
|
|
7
7
|
BG_BASH_BASE_PORT,
|
|
8
8
|
BG_BASH_MAX_PORT,
|
|
@@ -31,7 +31,12 @@ export interface BackgroundBashManager {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export interface BackgroundBashManagerOptions {
|
|
34
|
-
|
|
34
|
+
/**
|
|
35
|
+
* bash-rs executable. Defaults to the installed binary; injectable so a test
|
|
36
|
+
* can drive the manager's bookkeeping with a fake `spawn` on a host that has
|
|
37
|
+
* no binary at all.
|
|
38
|
+
*/
|
|
39
|
+
bashRsBin?: string;
|
|
35
40
|
basePort?: number;
|
|
36
41
|
maxPort?: number;
|
|
37
42
|
capability?: string;
|
|
@@ -71,7 +76,7 @@ export function createBackgroundBashManager(
|
|
|
71
76
|
const knownContexts = new Map<string, { userId: string; topic: string }>();
|
|
72
77
|
const runtimeCapability = options.capability ?? randomBytes(32).toString("hex");
|
|
73
78
|
const runtimeServerId = options.serverId ?? randomBytes(16).toString("hex");
|
|
74
|
-
const
|
|
79
|
+
const bashRsBin = options.bashRsBin ?? BASH_RS_BIN;
|
|
75
80
|
const basePort = options.basePort ?? BG_BASH_BASE_PORT;
|
|
76
81
|
const maxPort = options.maxPort ?? BG_BASH_MAX_PORT;
|
|
77
82
|
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
@@ -110,20 +115,17 @@ export function createBackgroundBashManager(
|
|
|
110
115
|
signal: AbortSignal.timeout(2000),
|
|
111
116
|
});
|
|
112
117
|
if (!response.ok) return false;
|
|
118
|
+
// Confirm this port answers for *our* spawned process rather than a
|
|
119
|
+
// stale one left behind by a previous manager instance.
|
|
113
120
|
const body = await response.text();
|
|
114
|
-
// bash-rs (Rust binary) answers with JSON + `instance_id`; the legacy
|
|
115
|
-
// TS server answers with the bare id as `text/plain`. Both exist to
|
|
116
|
-
// confirm this port is answering *our* spawned process, not a stale
|
|
117
|
-
// one left over from a previous manager instance.
|
|
118
121
|
try {
|
|
119
122
|
const parsed = JSON.parse(body);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
return (
|
|
124
|
+
Boolean(parsed) && typeof parsed === "object" && parsed.instance_id === runtimeServerId
|
|
125
|
+
);
|
|
123
126
|
} catch {
|
|
124
|
-
|
|
127
|
+
return false;
|
|
125
128
|
}
|
|
126
|
-
return body === runtimeServerId;
|
|
127
129
|
} catch {
|
|
128
130
|
return false;
|
|
129
131
|
}
|
|
@@ -146,17 +148,23 @@ export function createBackgroundBashManager(
|
|
|
146
148
|
reservedPort?: number,
|
|
147
149
|
excludedPorts: ReadonlySet<number> = new Set(),
|
|
148
150
|
): Promise<number> {
|
|
151
|
+
// bash-rs is the only implementation. A TS server used to stand in when the
|
|
152
|
+
// binary was absent, but the two agreed on the HTTP protocol while exposing
|
|
153
|
+
// different MCP tool names (bash_run vs background_bash_run), so which one
|
|
154
|
+
// spawned silently changed the tool surface an agent saw. Failing loudly
|
|
155
|
+
// beats serving a different API than the caller was told to expect.
|
|
156
|
+
//
|
|
157
|
+
// Checked before allocating: the caller retries once per turn, and a port
|
|
158
|
+
// reserved by a throw is never released, so allocating first leaked one
|
|
159
|
+
// port per attempt until the range was exhausted and the real cause was
|
|
160
|
+
// buried under "No available ports".
|
|
161
|
+
if (!bashRsBin) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`background-bash requires the bash-rs ${BASH_RS_VERSION} binary; run install-bash-rs.mjs (or set NEGOTIUM_BASH_RS_BIN)`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
149
166
|
const port = reservedPort ?? (await allocatePort(excludedPorts));
|
|
150
|
-
|
|
151
|
-
// one in place; otherwise fall back to the TS server unconditionally —
|
|
152
|
-
// e.g. no prebuilt binary for this platform/arch yet. Both speak the
|
|
153
|
-
// same wire protocol (X-Background-Bash-* headers, HMAC capability
|
|
154
|
-
// derived from the same `runtimeCapability` root secret), so either one
|
|
155
|
-
// is a transparent swap from the caller's point of view.
|
|
156
|
-
const [command, args] = BASH_RS_BIN
|
|
157
|
-
? [BASH_RS_BIN, [String(port)]]
|
|
158
|
-
: ["bun", ["run", serverFile, `--port=${port}`]];
|
|
159
|
-
const process = spawnImpl(command, args, {
|
|
167
|
+
const process = spawnImpl(bashRsBin, [String(port)], {
|
|
160
168
|
stdio: "ignore",
|
|
161
169
|
detached: false,
|
|
162
170
|
env: {
|
|
@@ -168,13 +168,16 @@ export const SNIPPETS_API_URL = (
|
|
|
168
168
|
export const BROWSER_RS_BIN = resolveBrowserRsBin(envText("NEGOTIUM_BROWSER_RS_BIN"));
|
|
169
169
|
|
|
170
170
|
/** bash-rs release tested with this Negotium version — see apps/negotium/install-bash-rs.mjs. */
|
|
171
|
-
export const BASH_RS_VERSION = "v0.1.
|
|
171
|
+
export const BASH_RS_VERSION = "v0.1.5";
|
|
172
172
|
|
|
173
173
|
/**
|
|
174
174
|
* Resolve the bash-rs binary the same way `resolveBrowserRsBin` resolves
|
|
175
175
|
* Browser.rs: a versioned private location, no PATH lookup, `undefined`
|
|
176
|
-
* (rather than throwing) when it's missing
|
|
177
|
-
*
|
|
176
|
+
* (rather than throwing) when it's missing. There is no substitute: the manager
|
|
177
|
+
* throws when a turn tries to prepare background-bash without a binary (see
|
|
178
|
+
* `background-bash/manager.ts`), the turn runner catches that and continues
|
|
179
|
+
* with a reminder that the tools are gone, so background-bash is simply
|
|
180
|
+
* unavailable on a platform without a prebuilt binary.
|
|
178
181
|
*/
|
|
179
182
|
export function resolveBashRsBin(envValue?: string): string | undefined {
|
|
180
183
|
const override = envValue?.trim();
|
|
@@ -281,8 +284,6 @@ export const SYSTEM_HEALTH_SERVER = resolve(PROJECT_ROOT, "src/mcp/system-health
|
|
|
281
284
|
|
|
282
285
|
export const AGENT_HEALTH_SERVER = resolve(PROJECT_ROOT, "src/mcp/agent-health-server.ts");
|
|
283
286
|
|
|
284
|
-
export const BACKGROUND_BASH_SERVER = resolve(PROJECT_ROOT, "src/mcp/background-bash-server.ts");
|
|
285
|
-
|
|
286
287
|
export const VAULT_SERVER = resolve(PROJECT_ROOT, "src/mcp/vault-server.ts");
|
|
287
288
|
|
|
288
289
|
export const BG_BASH_BASE_PORT = parsePortEnv(process.env.BG_BASH_BASE_PORT, 9700);
|
|
@@ -385,8 +386,8 @@ export const SESSION_ASKS_DIR = resolve(RUN_DIR, "session-asks");
|
|
|
385
386
|
* Passed to `bash-rs` as `BASHRS_SPILL_ROOT`. Each job gets a subdirectory
|
|
386
387
|
* here (`{bash_id}/meta.json`, `result.json`, `stdout.log`, `stderr.log`) —
|
|
387
388
|
* see bash-rs-mcp's `journal.rs`. `runtime/bashrs-completions.ts` watches
|
|
388
|
-
* this directory and turns `result.json` into a session-inbox `tell
|
|
389
|
-
*
|
|
389
|
+
* this directory and turns `result.json` into a session-inbox `tell`. The
|
|
390
|
+
* retired TypeScript server used to write one directly instead.
|
|
390
391
|
*/
|
|
391
392
|
export const BASHRS_SPILL_ROOT = resolve(RUN_DIR, "bashrs");
|
|
392
393
|
export const PLAYWRIGHT_BASE_PORT = parsePortEnv(process.env.PLAYWRIGHT_BASE_PORT, 9100);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* bash-rs completion watcher — turns `result.json` files written by the Rust
|
|
3
|
-
* `bash-rs` background-bash daemon into session-inbox `tell`s
|
|
4
|
-
*
|
|
3
|
+
* `bash-rs` background-bash daemon into session-inbox `tell`s. bash-rs is the
|
|
4
|
+
* only backend; the retired TypeScript server called `injectMessage()` itself.
|
|
5
5
|
*
|
|
6
6
|
* Why a watcher instead of bash-rs calling back into negotium: bash-rs is a
|
|
7
7
|
* standalone project (github.com/maestrojeong/bash-rs-mcp, sibling of
|
|
@@ -40,6 +40,46 @@ import {
|
|
|
40
40
|
type RuntimeProcessLeaseHandle,
|
|
41
41
|
} from "#storage/runtime-process-leases";
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Where a finished background job's turn is delivered.
|
|
45
|
+
*
|
|
46
|
+
* The default appends to negotium's own session inbox. An embedding host that
|
|
47
|
+
* owns a different inbox installs its own sink here — otium, for one, resolves
|
|
48
|
+
* `RUN_DIR` to its own state dir and runs its own inbox worker, so writing to
|
|
49
|
+
* negotium's path would drop the turn on the floor rather than deliver it.
|
|
50
|
+
* Mirrors the `setFileHooks()` seam in `runtime/file-hooks.ts`.
|
|
51
|
+
*
|
|
52
|
+
* A sink must throw to signal failure: the caller leaves `result.json` in
|
|
53
|
+
* place so the next sweep retries, which is what keeps delivery at-least-once.
|
|
54
|
+
*/
|
|
55
|
+
export interface BashrsCompletion {
|
|
56
|
+
userId: string;
|
|
57
|
+
topicId: string;
|
|
58
|
+
/** Stable per-job id; use it to collapse a retried delivery into one turn. */
|
|
59
|
+
bashId: string;
|
|
60
|
+
message: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type BashrsCompletionSink = (completion: BashrsCompletion) => void;
|
|
64
|
+
|
|
65
|
+
const defaultSink: BashrsCompletionSink = ({ userId, topicId, bashId, message }) => {
|
|
66
|
+
appendJsonlEntry(sessionInboxPath(userId, topicId), {
|
|
67
|
+
type: "tell",
|
|
68
|
+
from: "__bg_bash__",
|
|
69
|
+
message,
|
|
70
|
+
depth: 0,
|
|
71
|
+
requestId: bashId,
|
|
72
|
+
timestamp: new Date().toISOString(),
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
let completionSink: BashrsCompletionSink = defaultSink;
|
|
77
|
+
|
|
78
|
+
/** Install a host's delivery sink, or pass null to restore the default. */
|
|
79
|
+
export function setBashrsCompletionSink(sink: BashrsCompletionSink | null): void {
|
|
80
|
+
completionSink = sink ?? defaultSink;
|
|
81
|
+
}
|
|
82
|
+
|
|
43
83
|
const PROCESS_ROLE = "worker:bashrs-completions";
|
|
44
84
|
/** How long a delivered job's spill dir (logs + result.json.injected) is kept
|
|
45
85
|
* around before being swept — mirrors the TS server's own
|
|
@@ -56,6 +96,12 @@ interface JobResult {
|
|
|
56
96
|
exit_code: number | null;
|
|
57
97
|
finished_at_ms: number;
|
|
58
98
|
matched_line: string | null;
|
|
99
|
+
/**
|
|
100
|
+
* Which arm ended a watch. Absent for a plain background run, and absent
|
|
101
|
+
* from results written by bash-rs before v0.1.4 — those fall back to the
|
|
102
|
+
* generic completion notice.
|
|
103
|
+
*/
|
|
104
|
+
watch_outcome?: "matched" | "timeout" | "exited";
|
|
59
105
|
unknown: boolean;
|
|
60
106
|
}
|
|
61
107
|
|
|
@@ -96,15 +142,31 @@ function buildMessage(dir: string, result: JobResult): string {
|
|
|
96
142
|
`stderr${stderr.truncated ? ` (truncated, full output: ${join(dir, "stderr.log")})` : ""}:\n${stderr.text.trim()}`,
|
|
97
143
|
);
|
|
98
144
|
}
|
|
99
|
-
const header = result.
|
|
100
|
-
? `[background_bash_watch ${result.bash_id} matched]\nmatched line: ${result.matched_line.slice(0, 500)}`
|
|
101
|
-
: `[background_bash ${result.bash_id} finished]`;
|
|
145
|
+
const header = watchHeader(result) ?? `[background_bash ${result.bash_id} finished]`;
|
|
102
146
|
const exitLine = result.unknown
|
|
103
147
|
? "exit code: unknown (bash-rs restarted while this job was running)"
|
|
104
148
|
: `exit code: ${result.exit_code ?? "unknown"}`;
|
|
105
149
|
return `${header}\n${exitLine}\n${parts.join("\n") || "(no output)"}`;
|
|
106
150
|
}
|
|
107
151
|
|
|
152
|
+
/**
|
|
153
|
+
* A watch promises exactly one turn, and which outcome produced it is the
|
|
154
|
+
* whole point of the notice: "timed out" and "exited before matching" mean
|
|
155
|
+
* opposite things about whether the condition is still coming.
|
|
156
|
+
*/
|
|
157
|
+
function watchHeader(result: JobResult): string | null {
|
|
158
|
+
if (result.matched_line) {
|
|
159
|
+
return `[background_bash_watch ${result.bash_id} matched]\nmatched line: ${result.matched_line.slice(0, 500)}`;
|
|
160
|
+
}
|
|
161
|
+
if (result.watch_outcome === "timeout") {
|
|
162
|
+
return `[background_bash_watch ${result.bash_id} timed out without a match]`;
|
|
163
|
+
}
|
|
164
|
+
if (result.watch_outcome === "exited") {
|
|
165
|
+
return `[background_bash_watch ${result.bash_id} exited before matching]`;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
108
170
|
function parseOwner(owner: string): { userId: string; topicId: string } | null {
|
|
109
171
|
const nul = owner.indexOf("\0");
|
|
110
172
|
if (nul < 0) return null;
|
|
@@ -171,16 +233,14 @@ export async function flushBashrsCompletions(): Promise<void> {
|
|
|
171
233
|
}
|
|
172
234
|
|
|
173
235
|
try {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
236
|
+
// The stable bash_id is carried through so at-least-once delivery on
|
|
237
|
+
// either side — this watcher crashing before the rename, or the sink's
|
|
238
|
+
// own retry semantics — collapses to one turn rather than several.
|
|
239
|
+
completionSink({
|
|
240
|
+
userId: parsed.userId,
|
|
241
|
+
topicId: parsed.topicId,
|
|
242
|
+
bashId: result.bash_id,
|
|
177
243
|
message: buildMessage(dir, result),
|
|
178
|
-
depth: 0,
|
|
179
|
-
// Stable per-job id: at-least-once delivery on both sides (this
|
|
180
|
-
// watcher's own crash-before-rename, and the session-inbox worker's
|
|
181
|
-
// own at-least-once semantics) collapses to one turn, not several.
|
|
182
|
-
requestId: result.bash_id,
|
|
183
|
-
timestamp: new Date().toISOString(),
|
|
184
244
|
});
|
|
185
245
|
renameSync(resultPath, marker);
|
|
186
246
|
logger.info(
|
|
@@ -1556,6 +1556,12 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1556
1556
|
{ topicId, err },
|
|
1557
1557
|
"ai: ensureBgBash failed — proceeding without background bash tools",
|
|
1558
1558
|
);
|
|
1559
|
+
// Say so in the turn. Losing the tool silently makes the model plan
|
|
1560
|
+
// around a capability it does not have, and the operator only finds
|
|
1561
|
+
// out by reading server logs.
|
|
1562
|
+
turnReminders.push(
|
|
1563
|
+
"<system-reminder>Background bash tools are UNAVAILABLE this turn. The `background_bash_*` tools could not be prepared, most often because the bash-rs binary is not installed on this host. Do not attempt to call them; run short commands in the foreground instead, and tell the user if the work genuinely needs a background job.</system-reminder>",
|
|
1564
|
+
);
|
|
1559
1565
|
}
|
|
1560
1566
|
}
|
|
1561
1567
|
if (playwrightRequested && !browserProfileOwner) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const NEGOTIUM_VERSION = "0.2.
|
|
1
|
+
export const NEGOTIUM_VERSION = "0.2.14";
|
package/dist/runtime-helpers.js
CHANGED
|
@@ -496,7 +496,7 @@ function resolveBrowserRsBin(envValue) {
|
|
|
496
496
|
}
|
|
497
497
|
var SNIPPETS_API_URL = (envText("NEGOTIUM_SNIPPETS_API_URL") ?? envText("SNIPPETS_API_URL") ?? "").replace(/\/+$/, "");
|
|
498
498
|
var BROWSER_RS_BIN = resolveBrowserRsBin(envText("NEGOTIUM_BROWSER_RS_BIN"));
|
|
499
|
-
var BASH_RS_VERSION = "v0.1.
|
|
499
|
+
var BASH_RS_VERSION = "v0.1.5";
|
|
500
500
|
function resolveBashRsBin(envValue) {
|
|
501
501
|
const override = envValue?.trim();
|
|
502
502
|
const candidate = override ? resolve(override) : resolve(BINARIES_DIR, "bash-rs", BASH_RS_VERSION, "bash-rs");
|
|
@@ -527,7 +527,6 @@ var TOKEN_STATS_SERVER = resolve(PROJECT_ROOT, "src/mcp/token-stats-server.ts");
|
|
|
527
527
|
var COMPACTION_LOG_SERVER = resolve(PROJECT_ROOT, "src/mcp/compaction-log-server.ts");
|
|
528
528
|
var SYSTEM_HEALTH_SERVER = resolve(PROJECT_ROOT, "src/mcp/system-health-server.ts");
|
|
529
529
|
var AGENT_HEALTH_SERVER = resolve(PROJECT_ROOT, "src/mcp/agent-health-server.ts");
|
|
530
|
-
var BACKGROUND_BASH_SERVER = resolve(PROJECT_ROOT, "src/mcp/background-bash-server.ts");
|
|
531
530
|
var VAULT_SERVER = resolve(PROJECT_ROOT, "src/mcp/vault-server.ts");
|
|
532
531
|
var BG_BASH_BASE_PORT = parsePortEnv(process.env.BG_BASH_BASE_PORT, 9700);
|
|
533
532
|
var BG_BASH_MAX_PORT = parsePortEnv(process.env.BG_BASH_MAX_PORT, 9799);
|
|
@@ -1146,4 +1145,4 @@ export {
|
|
|
1146
1145
|
CLAUDE_EFFORT_VALUES
|
|
1147
1146
|
};
|
|
1148
1147
|
|
|
1149
|
-
//# debugId=
|
|
1148
|
+
//# debugId=856D33930FED17E364756E2164756E21
|