@polderlabs/bizar 5.4.1 → 5.5.1
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/bizar-dash/dist/assets/{EnvVarsSection-DhLHZ5ua.js → EnvVarsSection-BUQnyVQD.js} +3 -3
- package/bizar-dash/dist/assets/{EnvVarsSection-DhLHZ5ua.js.map → EnvVarsSection-BUQnyVQD.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-9UpvDP6-.js → MobileChat-8FupFgTv.js} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-9UpvDP6-.js.map → MobileChat-8FupFgTv.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-BkXtglA6.js +1 -0
- package/bizar-dash/dist/assets/MobileSettings-BkXtglA6.js.map +1 -0
- package/bizar-dash/dist/assets/{Toast-BlcKl_dN.js → Toast-CUwkLRms.js} +1 -1
- package/bizar-dash/dist/assets/{Toast-BlcKl_dN.js.map → Toast-CUwkLRms.js.map} +1 -1
- package/bizar-dash/dist/assets/{icons-CMPxNV8t.js → icons-COpQr35l.js} +125 -110
- package/bizar-dash/dist/assets/icons-COpQr35l.js.map +1 -0
- package/bizar-dash/dist/assets/main-Bz_zWFCw.css +1 -0
- package/bizar-dash/dist/assets/main-gqVwliHz.js +18 -0
- package/bizar-dash/dist/assets/main-gqVwliHz.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-B7dAkmYd.js +1 -0
- package/bizar-dash/dist/assets/{mobile-DDY1lquG.js.map → mobile-B7dAkmYd.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-layout-ClC6Qa42.js +2 -0
- package/bizar-dash/dist/assets/mobile-layout-ClC6Qa42.js.map +1 -0
- package/bizar-dash/dist/assets/{useSlashCommands-eFCiWHVt.js → useSlashCommands-B3MZ1JXa.js} +2 -2
- package/bizar-dash/dist/assets/{useSlashCommands-eFCiWHVt.js.map → useSlashCommands-B3MZ1JXa.js.map} +1 -1
- package/bizar-dash/dist/index.html +7 -7
- package/bizar-dash/dist/mobile.html +3 -3
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/backup-store.mjs +6 -1
- package/bizar-dash/src/server/bg-spawner.mjs +791 -0
- package/bizar-dash/src/server/headroom.mjs +3 -1
- package/bizar-dash/src/server/memory-lightrag.mjs +268 -2
- package/bizar-dash/src/server/memory-store.mjs +43 -3
- package/bizar-dash/src/server/opencode-sdk.mjs +63 -3
- package/bizar-dash/src/server/plugins/registry.mjs +1 -1
- package/bizar-dash/src/server/routes/activity.mjs +85 -0
- package/bizar-dash/src/server/routes/background.mjs +161 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +10 -0
- package/bizar-dash/src/server/routes/memory.mjs +8 -0
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +1 -1
- package/bizar-dash/src/server/server.mjs +26 -1
- package/bizar-dash/src/web/App.tsx +46 -36
- package/bizar-dash/src/web/components/InstallConfirmDialog.tsx +1 -1
- package/bizar-dash/src/web/components/Sidebar.tsx +7 -27
- package/bizar-dash/src/web/components/Topbar.tsx +48 -11
- package/bizar-dash/src/web/lib/types.ts +39 -1
- package/bizar-dash/src/web/styles/main.css +93 -26
- package/bizar-dash/src/web/styles/memory.css +17 -0
- package/bizar-dash/src/web/styles/settings.css +9 -171
- package/bizar-dash/src/web/views/BackgroundAgents.tsx +399 -30
- package/bizar-dash/src/web/views/Marketplace.tsx +192 -31
- package/bizar-dash/src/web/views/Settings.tsx +205 -184
- package/bizar-dash/src/web/views/SpawnAgentModal.tsx +221 -0
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +157 -7
- package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +11 -0
- package/bizar-dash/src/web/views/settings/MemorySection.tsx +65 -11
- package/bizar-dash/tests/BackgroundAgents.test.tsx +160 -0
- package/bizar-dash/tests/background-pause-resume.test.mjs +46 -0
- package/bizar-dash/tests/background-sdk-session.test.mjs +94 -0
- package/bizar-dash/tests/background-session-events.test.mjs +74 -0
- package/bizar-dash/tests/background-spawn-from-ui.test.mjs +36 -0
- package/bizar-dash/tests/background-steer-sdk.test.mjs +71 -0
- package/bizar-dash/tests/background-steer.test.mjs +60 -0
- package/bizar-dash/tests/lightrag-startup-hook.test.mjs +206 -0
- package/bizar-dash/tests/memory-auto-reindex.test.mjs +196 -0
- package/bizar-dash/tests/memory-cli.test.mjs +1 -1
- package/bizar-dash/tests/memory-default-vault.test.mjs +90 -3
- package/bizar-dash/tests/memory-lightrag-extended.test.mjs +131 -0
- package/bizar-dash/tests/views/Marketplace.test.tsx +200 -0
- package/bizar-dash/tests/views/Memory.test.tsx +235 -0
- package/bizar-dash/tests/views/Settings.test.tsx +160 -0
- package/bizar-dash/tests/views/sidebar.test.tsx +116 -0
- package/bizar-dash/tests/views/topbar.test.tsx +102 -0
- package/cli/bin.mjs +16 -0
- package/cli/commands/deploy/docker.mjs +2 -1
- package/cli/commands/lightrag.mjs +171 -0
- package/cli/commands/marketplace.mjs +1 -1
- package/cli/commands/service.mjs +7 -0
- package/cli/commands/util.mjs +8 -1
- package/cli/init.mjs +1 -1
- package/cli/post-install-smoke.mjs +230 -0
- package/cli/provision.mjs +177 -0
- package/cli/provision.test.mjs +180 -0
- package/cli/service-controller.mjs +378 -28
- package/cli/service-controller.test.mjs +100 -0
- package/cli/service-env.mjs +139 -0
- package/cli/service.mjs +23 -0
- package/config/agents/_shared/AGENT_BASELINE.md +6 -6
- package/install.sh +87 -6
- package/package.json +1 -1
- package/plugins/bizar/index.ts +147 -0
- package/plugins/bizar/src/background-state.ts +95 -5
- package/plugins/bizar/src/background.ts +571 -15
- package/plugins/bizar/src/hooks/memory-inject.ts +247 -0
- package/plugins/bizar/src/hooks/memory-write-on-end.ts +188 -0
- package/plugins/bizar/src/opencode-runner.ts +100 -287
- package/plugins/bizar/src/tools/bg-pause.ts +77 -0
- package/plugins/bizar/src/tools/bg-report-progress.ts +101 -0
- package/plugins/bizar/src/tools/bg-resume.ts +72 -0
- package/plugins/bizar/src/tools/bg-send-message.ts +204 -0
- package/plugins/bizar/src/tools/bg-spawn.ts +237 -151
- package/plugins/bizar/src/tools/bg-status.ts +10 -0
- package/plugins/bizar/src/tools/memory-list.ts +135 -0
- package/plugins/bizar/src/tools/memory-read.ts +142 -0
- package/plugins/bizar/src/tools/memory-search.ts +228 -0
- package/plugins/bizar/src/tools/memory-write.ts +183 -0
- package/plugins/bizar/tests/memory-write-on-end.test.ts +92 -0
- package/plugins/bizar/tests/tools/bg-pause.test.ts +64 -0
- package/plugins/bizar/tests/tools/bg-report-progress.test.ts +82 -0
- package/plugins/bizar/tests/tools/bg-resume.test.ts +43 -0
- package/plugins/bizar/tests/tools/bg-send-message.test.ts +119 -0
- package/plugins/bizar/tests/tools/bg-spawn-http.test.ts +223 -0
- package/bizar-dash/dist/assets/MobileSettings-BFmN9ZWR.js +0 -1
- package/bizar-dash/dist/assets/MobileSettings-BFmN9ZWR.js.map +0 -1
- package/bizar-dash/dist/assets/icons-CMPxNV8t.js.map +0 -1
- package/bizar-dash/dist/assets/main-Dhrvp9Gt.js +0 -16
- package/bizar-dash/dist/assets/main-Dhrvp9Gt.js.map +0 -1
- package/bizar-dash/dist/assets/main-O2tZVdHm.css +0 -1
- package/bizar-dash/dist/assets/mobile-DDY1lquG.js +0 -1
- package/bizar-dash/dist/assets/mobile-layout-BjkoHh1W.js +0 -2
- package/bizar-dash/dist/assets/mobile-layout-BjkoHh1W.js.map +0 -1
- package/bizar-dash/src/web/components/SettingsNav.tsx +0 -101
- package/bizar-dash/tests/settings-layout.test.tsx +0 -129
- package/bizar-dash/tests/settings-mode-wiring.test.tsx +0 -151
- package/bizar-dash/tests/settings-nav.test.tsx +0 -126
|
@@ -1,23 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* plugins/bizar/src/tools/bg-spawn.ts
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* v5.5.1 — `bizar_spawn_background` tool, refactored to delegate to the
|
|
5
|
+
* dashboard's SDK-based spawner (`POST /api/background`) instead of
|
|
6
|
+
* spawning `opencode run` subprocesses directly.
|
|
7
|
+
*
|
|
8
|
+
* Why the plugin now talks to the dashboard instead of running `opencode run`:
|
|
9
|
+
* - The v5.5.0 design (opencode-runner.ts) spawned one `opencode run`
|
|
10
|
+
* subprocess per agent. Steering a running agent required
|
|
11
|
+
* kill+respawn with a `[STEERED <ts>]` marker — a poor approximation
|
|
12
|
+
* of "true mid-flight prompt".
|
|
13
|
+
* - opencode's serve child exposes long-lived SDK sessions that accept
|
|
14
|
+
* new prompts via `POST /api/session/{id}/prompt`. Steering is then
|
|
15
|
+
* a real mid-flight redirect: the same session keeps running, the
|
|
16
|
+
* new prompt becomes the next user turn.
|
|
17
|
+
* - The dashboard already owns the opencode SDK (see
|
|
18
|
+
* `bizar-dash/src/server/opencode-sdk.mjs`); it can mediate between
|
|
19
|
+
* the plugin's many bg instances and the single opencode serve child.
|
|
20
|
+
*
|
|
21
|
+
* This tool therefore:
|
|
22
|
+
* 1. Validates the request (Odin-only check, model parsing,
|
|
23
|
+
* timeoutMs clamping — unchanged from v5.5.0).
|
|
24
|
+
* 2. Builds the delegation wrapper prompt if the agent is a subagent.
|
|
25
|
+
* 3. POSTs `{ agent, prompt, worktree, ... }` to the dashboard at
|
|
26
|
+
* `POST /api/background`.
|
|
27
|
+
* 4. Tracks the returned instance in the local InstanceManager so the
|
|
28
|
+
* existing `bizar_status`, `bizar_collect`, `bizar_kill`,
|
|
29
|
+
* `bizar_pause`, `bizar_resume` tools work unchanged (they now
|
|
30
|
+
* delegate to the dashboard HTTP API too — see respective tool files).
|
|
31
|
+
*
|
|
32
|
+
* Backwards compat: the public tool args are unchanged. The return
|
|
33
|
+
* shape is backwards compatible — `instanceId` + `sessionId` are still
|
|
34
|
+
* present, but `processId` is `null` (no OS subprocess) and a new
|
|
35
|
+
* `liveSession: true` flag indicates the new SDK-backed mode.
|
|
9
36
|
*
|
|
10
37
|
* Spec §1, §6.3, §7.1.
|
|
11
38
|
*/
|
|
12
39
|
import { tool } from "@opencode-ai/plugin";
|
|
13
40
|
import { z } from "zod";
|
|
14
41
|
|
|
15
|
-
import { generateInstanceId
|
|
42
|
+
import { generateInstanceId } from "../background.js";
|
|
16
43
|
import type { InstanceManager } from "../background.js";
|
|
17
44
|
import type { Logger } from "../logger.js";
|
|
18
|
-
import { spawnAgent } from "../opencode-runner.js";
|
|
19
45
|
import { resolve as pathResolve } from "node:path";
|
|
20
46
|
import { homedir } from "node:os";
|
|
47
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
48
|
+
import { join } from "node:path";
|
|
21
49
|
|
|
22
50
|
/** Spec §7.3: `timeoutMs` clamped to [1000, 1800000] (1s..30min). */
|
|
23
51
|
const TIMEOUT_MIN_MS = 1000;
|
|
@@ -26,24 +54,11 @@ const TIMEOUT_DEFAULT_MS = 300_000;
|
|
|
26
54
|
|
|
27
55
|
/**
|
|
28
56
|
* Agents whose `mode` is `primary` and therefore accepted by
|
|
29
|
-
*
|
|
30
|
-
* any agent whose `mode` is `subagent` with a "Falling back to default
|
|
31
|
-
* agent" warning — the bg instance then runs odin regardless of what
|
|
32
|
-
* the caller asked for. See `wiki/Troubleshooting.md` for the full
|
|
33
|
-
* postmortem.
|
|
34
|
-
*
|
|
35
|
-
* To invoke a subagent cleanly, `spawnAgent` below routes the spawn
|
|
36
|
-
* through a primary agent (odin) with an explicit delegation prompt
|
|
37
|
-
* that tells odin to call its `task` tool to spawn the requested
|
|
38
|
-
* subagent. Subagent changes here MUST mirror `config/agents/*.md`.
|
|
57
|
+
* opencode's `--agent` flag.
|
|
39
58
|
*
|
|
40
59
|
* Exported for testability — the test asserts the set is in sync with
|
|
41
60
|
* the agent configs.
|
|
42
61
|
*/
|
|
43
|
-
// v3.20.11 — kept in sync with config/agents/*.md via the
|
|
44
|
-
// "is in sync with the on-disk agent configs" test below. The set
|
|
45
|
-
// must equal exactly the agents whose frontmatter declares
|
|
46
|
-
// `mode: primary` (currently odin, quick, browser-harness).
|
|
47
62
|
export const PRIMARY_AGENTS: ReadonlySet<string> = new Set([
|
|
48
63
|
"odin",
|
|
49
64
|
"quick",
|
|
@@ -59,18 +74,9 @@ export function needsDelegationWrapper(agent: string): boolean {
|
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
/**
|
|
62
|
-
* Build the delegation prompt that wraps a subagent request.
|
|
63
|
-
* wrapper runs as odin; odin must call its `task` tool to spawn the
|
|
64
|
-
* requested subagent and report the subagent's final output verbatim.
|
|
77
|
+
* Build the delegation prompt that wraps a subagent request.
|
|
65
78
|
*
|
|
66
|
-
*
|
|
67
|
-
* and a passive "please consider delegating" framing is too easily
|
|
68
|
-
* ignored. The explicit "do not interpret", "do not perform the work
|
|
69
|
-
* yourself", and "report only the subagent's output" constraints give
|
|
70
|
-
* the LLM no room to drift.
|
|
71
|
-
*
|
|
72
|
-
* Exported for testability — the test asserts the prompt is directive
|
|
73
|
-
* enough to override Odin's default routing behavior.
|
|
79
|
+
* Exported for testability.
|
|
74
80
|
*/
|
|
75
81
|
export function buildDelegationPrompt(requestedAgent: string, userPrompt: string): string {
|
|
76
82
|
return [
|
|
@@ -120,23 +126,139 @@ export interface BgSpawnDeps {
|
|
|
120
126
|
instanceManager: InstanceManager;
|
|
121
127
|
worktree: string;
|
|
122
128
|
logger: Logger;
|
|
129
|
+
/**
|
|
130
|
+
* Optional injection point for tests. When provided, the dashboard
|
|
131
|
+
* HTTP call is short-circuited and the test function is called with
|
|
132
|
+
* the resolved request shape. Production code never sets this.
|
|
133
|
+
*/
|
|
134
|
+
_dashboardPost?: (url: string, init: { headers: Record<string, string>; body: string }) => Promise<{ ok: boolean; status: number; json: () => Promise<unknown> }>;
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
/**
|
|
126
|
-
* Compute the LogWriter's actual log path for a given instanceId.
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* `~/.cache/bizar/logs` (overridable via the `BIZAR_LOG_DIR` env
|
|
130
|
-
* var). The instanceId is what we know at spawn time — the opencode
|
|
131
|
-
* sessionId is generated later by the subprocess and we don't
|
|
132
|
-
* pre-allocate it. We therefore use the instanceId as the log file
|
|
133
|
-
* name and let the runner append to it.
|
|
138
|
+
* Compute the LogWriter's actual log path for a given instanceId. Mirrors
|
|
139
|
+
* the convention used by `bg-spawn.ts` pre-v5.5.1 so existing log readers
|
|
140
|
+
* (dashboard's log viewer) find the file in the same place.
|
|
134
141
|
*/
|
|
135
142
|
function buildLogPath(instanceId: string): string {
|
|
136
143
|
const logDir = process.env.BIZAR_LOG_DIR || pathResolve(homedir(), ".cache", "bizar", "logs");
|
|
137
144
|
return pathResolve(logDir, `${instanceId}.log`);
|
|
138
145
|
}
|
|
139
146
|
|
|
147
|
+
// --- Dashboard HTTP wiring -----------------------------------------------
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolve the dashboard base URL. Order:
|
|
151
|
+
* 1. `BIZAR_DASHBOARD_URL` env override (matches dashboard-client.ts).
|
|
152
|
+
* 2. `BIZAR_DASHBOARD_PORT` env override → `http://127.0.0.1:<port>`.
|
|
153
|
+
* 3. Default `http://127.0.0.1:4098` (matches dashboard default port
|
|
154
|
+
* in install.sh + bizarre installer).
|
|
155
|
+
*/
|
|
156
|
+
function resolveDashboardUrl(): string {
|
|
157
|
+
const fromEnv = process.env.BIZAR_DASHBOARD_URL;
|
|
158
|
+
if (fromEnv && fromEnv.trim()) return fromEnv.trim().replace(/\/+$/, "");
|
|
159
|
+
const port = process.env.BIZAR_DASHBOARD_PORT;
|
|
160
|
+
if (port && /^\d+$/.test(port)) return `http://127.0.0.1:${port}`;
|
|
161
|
+
return "http://127.0.0.1:4098";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const DEFAULT_DASHBOARD_AUTH_PATHS = [
|
|
165
|
+
join(homedir(), ".config", "bizar", "dashboard-secret"),
|
|
166
|
+
join(homedir(), ".cache", "bizarharness", "dash-auth.json"),
|
|
167
|
+
join(homedir(), ".cache", "bizar", "dash-auth.json"),
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Read the dashboard bearer token from the on-disk secret file. The
|
|
172
|
+
* dashboard writes `~/.config/bizar/dashboard-secret` (mode 0600) on
|
|
173
|
+
* first boot. Loopback requests don't need the token (auth middleware
|
|
174
|
+
* trusts loopback automatically), but we send it anyway when available
|
|
175
|
+
* so a non-loopback deployment works without extra config.
|
|
176
|
+
*/
|
|
177
|
+
function readDashboardToken(): string {
|
|
178
|
+
for (const candidate of DEFAULT_DASHBOARD_AUTH_PATHS) {
|
|
179
|
+
try {
|
|
180
|
+
if (!existsSync(candidate)) continue;
|
|
181
|
+
const text = readFileSync(candidate, "utf-8").trim();
|
|
182
|
+
if (text && text.length >= 16) return text;
|
|
183
|
+
} catch {
|
|
184
|
+
/* ignore */
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Fall back to the password (dash-auth.json carries a `password` field).
|
|
188
|
+
for (const candidate of DEFAULT_DASHBOARD_AUTH_PATHS) {
|
|
189
|
+
try {
|
|
190
|
+
if (!existsSync(candidate)) continue;
|
|
191
|
+
const parsed = JSON.parse(readFileSync(candidate, "utf-8")) as { password?: unknown };
|
|
192
|
+
if (typeof parsed.password === "string" && parsed.password.length >= 16) {
|
|
193
|
+
return parsed.password;
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
/* ignore */
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return "";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* POST JSON to the dashboard. Returns the parsed JSON on 2xx; throws a
|
|
204
|
+
* structured `Error` (with `.httpStatus`) on transport / non-2xx so
|
|
205
|
+
* callers can surface a clear error to the agent.
|
|
206
|
+
*
|
|
207
|
+
* In tests, `deps._dashboardPost` overrides this with a mock.
|
|
208
|
+
*/
|
|
209
|
+
async function postJsonToDashboard(
|
|
210
|
+
url: string,
|
|
211
|
+
body: unknown,
|
|
212
|
+
logger: Logger,
|
|
213
|
+
override?: BgSpawnDeps["_dashboardPost"],
|
|
214
|
+
): Promise<unknown> {
|
|
215
|
+
if (override) {
|
|
216
|
+
const res = await override(url, {
|
|
217
|
+
headers: { "content-type": "application/json" },
|
|
218
|
+
body: JSON.stringify(body),
|
|
219
|
+
});
|
|
220
|
+
if (!res.ok) {
|
|
221
|
+
const text = await res.json().catch(() => ({}));
|
|
222
|
+
const err = new Error(
|
|
223
|
+
`dashboard HTTP ${res.status}: ${JSON.stringify(text).slice(0, 200)}`,
|
|
224
|
+
);
|
|
225
|
+
(err as Error & { httpStatus?: number }).httpStatus = res.status;
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
return res.json();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const token = readDashboardToken();
|
|
232
|
+
const headers: Record<string, string> = {
|
|
233
|
+
"content-type": "application/json",
|
|
234
|
+
accept: "application/json",
|
|
235
|
+
};
|
|
236
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
237
|
+
|
|
238
|
+
const ac = new AbortController();
|
|
239
|
+
const timer = setTimeout(() => ac.abort(), 8_000);
|
|
240
|
+
try {
|
|
241
|
+
const res = await fetch(url, {
|
|
242
|
+
method: "POST",
|
|
243
|
+
headers,
|
|
244
|
+
body: JSON.stringify(body),
|
|
245
|
+
signal: ac.signal,
|
|
246
|
+
});
|
|
247
|
+
if (!res.ok) {
|
|
248
|
+
const text = await res.text().catch(() => "");
|
|
249
|
+
const err = new Error(
|
|
250
|
+
`dashboard HTTP ${res.status}: ${text.slice(0, 200)}`,
|
|
251
|
+
);
|
|
252
|
+
(err as Error & { httpStatus?: number }).httpStatus = res.status;
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
return res.json();
|
|
256
|
+
} finally {
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
}
|
|
259
|
+
void logger;
|
|
260
|
+
}
|
|
261
|
+
|
|
140
262
|
/**
|
|
141
263
|
* Build the `bizar_spawn_background` tool. The plugin wires the
|
|
142
264
|
* result into `Hooks.tool`. The `deps` closure carries the
|
|
@@ -145,11 +267,13 @@ function buildLogPath(instanceId: string): string {
|
|
|
145
267
|
export function createBgSpawnTool(deps: BgSpawnDeps) {
|
|
146
268
|
return tool({
|
|
147
269
|
description:
|
|
148
|
-
"Spawn a background agent that runs asynchronously as a
|
|
270
|
+
"Spawn a background agent that runs asynchronously as a long-lived opencode serve session " +
|
|
271
|
+
"(SDK-backed via the dashboard). " +
|
|
149
272
|
"Only Odin may call this tool. " +
|
|
150
273
|
"Returns an instanceId immediately (sub-second), then the agent runs to completion in the background. " +
|
|
151
274
|
"Use `bizar_status` / `bizar_collect` / `bizar_kill` to manage the instance. " +
|
|
152
275
|
"Use `bizar_bg_view` (CLI) to watch all running agents in a tmux split window. " +
|
|
276
|
+
"Steering is TRUE mid-flight via `bizar_send_message` (no kill+respawn). " +
|
|
153
277
|
"IMPORTANT: do NOT block waiting for the agent. Return control to the user right after spawning.",
|
|
154
278
|
args: {
|
|
155
279
|
agent: z.string().min(1).describe("Agent name to spawn (e.g. 'mimir', 'thor', 'tyr')."),
|
|
@@ -223,14 +347,23 @@ export function createBgSpawnTool(deps: BgSpawnDeps) {
|
|
|
223
347
|
}
|
|
224
348
|
const timeoutMs = requested;
|
|
225
349
|
|
|
226
|
-
// 4.
|
|
227
|
-
|
|
228
|
-
|
|
350
|
+
// 4. Build the delegation wrapper (mirrors bg-spawn.ts pre-v5.5.1).
|
|
351
|
+
const isPrimary = PRIMARY_AGENTS.has(args.agent);
|
|
352
|
+
const wrapperPrompt = isPrimary
|
|
353
|
+
? args.prompt
|
|
354
|
+
: buildDelegationPrompt(args.agent, args.prompt);
|
|
355
|
+
|
|
356
|
+
// 5. Pre-allocate the instanceId so we can track BEFORE the
|
|
357
|
+
// dashboard call (track-before-HTTP, HIGH-21). The dashboard
|
|
358
|
+
// generates its own instanceId, but accepting ours would
|
|
359
|
+
// require a second round-trip — instead we accept the
|
|
360
|
+
// dashboard's id and patch it back in. The local InstanceManager
|
|
361
|
+
// mirrors the dashboard state so `bizar_status` works.
|
|
229
362
|
const instanceId = generateInstanceId();
|
|
230
363
|
const logPath = buildLogPath(instanceId);
|
|
231
364
|
const draft = {
|
|
232
365
|
instanceId,
|
|
233
|
-
sessionId: "", // filled in once the
|
|
366
|
+
sessionId: "", // filled in once the dashboard returns the sessionId
|
|
234
367
|
agent: args.agent,
|
|
235
368
|
model: modelOverride
|
|
236
369
|
? `${modelOverride.providerID}/${modelOverride.modelID}`
|
|
@@ -241,10 +374,11 @@ export function createBgSpawnTool(deps: BgSpawnDeps) {
|
|
|
241
374
|
logPath,
|
|
242
375
|
timeoutMs,
|
|
243
376
|
toolCallCount: 0,
|
|
244
|
-
// v0.5.5 — persistent auto-restart
|
|
245
377
|
persistent: args.persistent ?? false,
|
|
246
378
|
maxRestarts: args.maxRestarts ?? 3,
|
|
247
379
|
restartCount: 0,
|
|
380
|
+
progress: 0,
|
|
381
|
+
toolCalls: [],
|
|
248
382
|
};
|
|
249
383
|
const addRes = await deps.instanceManager.add(draft);
|
|
250
384
|
if (addRes === "cap_reached") {
|
|
@@ -255,41 +389,38 @@ export function createBgSpawnTool(deps: BgSpawnDeps) {
|
|
|
255
389
|
};
|
|
256
390
|
}
|
|
257
391
|
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
// opencode 1.17.x rejects `--agent <subagent>` with a silent
|
|
263
|
-
// fallback to the default agent. To invoke a subagent we
|
|
264
|
-
// route through a primary wrapper (odin) with a delegation
|
|
265
|
-
// prompt. The instance record still attributes the work to
|
|
266
|
-
// the requested agent — only the opencode process is odin's.
|
|
267
|
-
const messageID = generateMessageId();
|
|
268
|
-
const isPrimary = PRIMARY_AGENTS.has(args.agent);
|
|
269
|
-
const wrapperAgent = isPrimary ? args.agent : "odin";
|
|
270
|
-
const wrapperPrompt = isPrimary
|
|
271
|
-
? args.prompt
|
|
272
|
-
: buildDelegationPrompt(args.agent, args.prompt);
|
|
273
|
-
let spawnRes: Awaited<ReturnType<typeof spawnAgent>>;
|
|
392
|
+
// 6. POST to the dashboard. The dashboard owns the SDK and the
|
|
393
|
+
// underlying opencode session; we mirror its instanceId.
|
|
394
|
+
const dashboardUrl = `${resolveDashboardUrl()}/api/background`;
|
|
395
|
+
let spawnRes: { instanceId: string; sessionId: string | null; status?: string; liveSession?: boolean };
|
|
274
396
|
try {
|
|
275
|
-
spawnRes = await
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
397
|
+
spawnRes = (await postJsonToDashboard(
|
|
398
|
+
dashboardUrl,
|
|
399
|
+
{
|
|
400
|
+
agent: args.agent,
|
|
401
|
+
prompt: wrapperPrompt,
|
|
402
|
+
model: modelOverride
|
|
403
|
+
? `${modelOverride.providerID}/${modelOverride.modelID}`
|
|
404
|
+
: undefined,
|
|
405
|
+
worktree: deps.worktree,
|
|
406
|
+
timeoutMs,
|
|
407
|
+
persistent: Boolean(args.persistent),
|
|
408
|
+
maxRestarts: args.maxRestarts ?? 3,
|
|
409
|
+
tags: [`spawned-by:${ctx.agent}`, `plugin-instance:${instanceId}`],
|
|
410
|
+
},
|
|
411
|
+
deps.logger,
|
|
412
|
+
deps._dashboardPost,
|
|
413
|
+
)) as { instanceId: string; sessionId: string | null; status?: string; liveSession?: boolean };
|
|
283
414
|
} catch (err: unknown) {
|
|
284
415
|
const msg = err instanceof Error ? err.message : String(err);
|
|
285
416
|
await deps.instanceManager.update(instanceId, {
|
|
286
417
|
status: "failed",
|
|
287
|
-
error: `
|
|
418
|
+
error: `dashboard POST /api/background failed: ${msg}`,
|
|
288
419
|
completedAt: Date.now(),
|
|
289
420
|
});
|
|
290
421
|
return {
|
|
291
422
|
output: JSON.stringify({
|
|
292
|
-
error: `spawn
|
|
423
|
+
error: `spawn failed: dashboard unreachable (${msg}). Make sure the Bizar dashboard is running.`,
|
|
293
424
|
instanceId,
|
|
294
425
|
sessionId: null,
|
|
295
426
|
status: "failed",
|
|
@@ -297,100 +428,55 @@ export function createBgSpawnTool(deps: BgSpawnDeps) {
|
|
|
297
428
|
};
|
|
298
429
|
}
|
|
299
430
|
|
|
300
|
-
|
|
431
|
+
// 7. The dashboard allocates its own instanceId. Patch our local
|
|
432
|
+
// record to mirror the dashboard state — keep the original
|
|
433
|
+
// instanceId we returned to the caller (the plugin's existing
|
|
434
|
+
// InstanceManager tracks by that id, and the dashboard already
|
|
435
|
+
// has the same metadata via the `tags: ["plugin-instance:<id>"]`
|
|
436
|
+
// we sent above).
|
|
437
|
+
if (spawnRes.instanceId && spawnRes.instanceId !== instanceId) {
|
|
438
|
+
// Update the local record's sessionId + flags; the local id stays
|
|
439
|
+
// as-is so all subsequent tool calls (status/collect/kill/...)
|
|
440
|
+
// resolve correctly. The dashboard id is stored alongside as
|
|
441
|
+
// `dashboardInstanceId` for cross-referencing.
|
|
301
442
|
await deps.instanceManager.update(instanceId, {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
443
|
+
sessionId: spawnRes.sessionId || "",
|
|
444
|
+
status: "running",
|
|
445
|
+
runnerState: "running",
|
|
446
|
+
sessionIdAt: Date.now(),
|
|
447
|
+
liveSession: true,
|
|
448
|
+
dashboardInstanceId: spawnRes.instanceId,
|
|
305
449
|
});
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// 6. Persist the sessionId in the instance state. The high-level
|
|
317
|
-
// `status` stays "pending" until the runner reports a terminal
|
|
318
|
-
// state; we record the processId and the runner's
|
|
319
|
-
// intermediate states in the dedicated fields.
|
|
320
|
-
await deps.instanceManager.update(instanceId, {
|
|
321
|
-
sessionId: spawnRes.sessionId,
|
|
322
|
-
status: "running",
|
|
323
|
-
processId: spawnRes.processId,
|
|
324
|
-
runnerState: "running",
|
|
325
|
-
sessionIdAt: Date.now(),
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// 7. Wire the runner's exit event to the instance state. When
|
|
329
|
-
// the opencode run subprocess exits, the runner updates
|
|
330
|
-
// the status (done / failed / killed) and triggers the
|
|
331
|
-
// persistent auto-restart flow if appropriate.
|
|
332
|
-
if (spawnRes.processId !== undefined) {
|
|
333
|
-
const { onExit } = await import("../opencode-runner.js");
|
|
334
|
-
onExit(spawnRes.processId, (status) => {
|
|
335
|
-
// Map runner states to BackgroundStatus. The runner reports
|
|
336
|
-
// "starting" | "running" | "done" | "failed" | "killed";
|
|
337
|
-
// BackgroundStatus has "pending" | "running" | "done" |
|
|
338
|
-
// "failed" | "killed" | "timed_out". "starting" maps to
|
|
339
|
-
// "running" (in-flight, no terminal state).
|
|
340
|
-
const mapped: "pending" | "running" | "done" | "failed" | "killed" =
|
|
341
|
-
status.state === "starting" || status.state === "running"
|
|
342
|
-
? "running"
|
|
343
|
-
: status.state;
|
|
344
|
-
|
|
345
|
-
const update: Parameters<InstanceManager["update"]>[1] = {
|
|
346
|
-
status: mapped,
|
|
347
|
-
runnerState: status.state,
|
|
348
|
-
completedAt: status.endedAt ?? Date.now(),
|
|
349
|
-
};
|
|
350
|
-
if (status.exitCode !== undefined) update.exitCode = status.exitCode;
|
|
351
|
-
if (status.error) {
|
|
352
|
-
update.runnerError = status.error;
|
|
353
|
-
update.error = status.error;
|
|
354
|
-
}
|
|
355
|
-
if (status.endedAt !== undefined) update.runnerEndedAt = status.endedAt;
|
|
356
|
-
// Best-effort: if the InstanceManager has gone away (e.g.
|
|
357
|
-
// the plugin restarted), the update silently no-ops.
|
|
358
|
-
deps.instanceManager
|
|
359
|
-
.update(instanceId, update)
|
|
360
|
-
.then(() => {
|
|
361
|
-
// Persistent auto-restart: only for natural failures,
|
|
362
|
-
// not for explicit kills or successes.
|
|
363
|
-
if (status.state === "failed") {
|
|
364
|
-
return deps.instanceManager.maybeAutoRestart(instanceId);
|
|
365
|
-
}
|
|
366
|
-
return undefined;
|
|
367
|
-
})
|
|
368
|
-
.catch((err: unknown) => {
|
|
369
|
-
deps.logger.warn(
|
|
370
|
-
`bizar: bg-spawn exit update failed for ${instanceId}: ${
|
|
371
|
-
err instanceof Error ? err.message : String(err)
|
|
372
|
-
}`,
|
|
373
|
-
);
|
|
374
|
-
});
|
|
450
|
+
} else {
|
|
451
|
+
await deps.instanceManager.update(instanceId, {
|
|
452
|
+
sessionId: spawnRes.sessionId || "",
|
|
453
|
+
status: "running",
|
|
454
|
+
runnerState: "running",
|
|
455
|
+
sessionIdAt: Date.now(),
|
|
456
|
+
liveSession: true,
|
|
375
457
|
});
|
|
376
458
|
}
|
|
377
459
|
|
|
378
|
-
// 8. Return the spawn result.
|
|
379
|
-
// running in the background
|
|
380
|
-
//
|
|
460
|
+
// 8. Return the spawn result. v5.5.1 message: the agent is
|
|
461
|
+
// running in the background on an opencode serve session; Odin
|
|
462
|
+
// should return control to the user immediately.
|
|
381
463
|
return {
|
|
382
464
|
output: JSON.stringify({
|
|
383
465
|
instanceId,
|
|
466
|
+
dashboardInstanceId: spawnRes.instanceId,
|
|
384
467
|
sessionId: spawnRes.sessionId,
|
|
385
|
-
processId:
|
|
468
|
+
processId: null, // no subprocess; the opencode serve child owns the session
|
|
386
469
|
status: "running",
|
|
470
|
+
liveSession: true,
|
|
387
471
|
message:
|
|
388
|
-
"Background agent started. It
|
|
472
|
+
"Background agent started. It runs as an opencode serve SDK session managed by the Bizar dashboard. " +
|
|
389
473
|
"Use `bizar_status <instanceId>` to check progress, `bizar_collect <instanceId>` to wait for the result, " +
|
|
474
|
+
"`bizar_send_message <instanceId> <msg>` for true mid-flight steering, " +
|
|
390
475
|
"or `bizar_kill <instanceId>` to stop it. Run `bizar bg view` in another terminal to watch all running agents live.",
|
|
391
476
|
nextSteps: [
|
|
392
477
|
"Tell the user the agent is running and approximately how long they should expect to wait",
|
|
393
478
|
"If the user wants the result now, call `bizar_collect <instanceId>` (with a reasonable timeout)",
|
|
479
|
+
"If the user wants to redirect the agent, call `bizar_send_message <instanceId> <msg>` — this is TRUE mid-flight",
|
|
394
480
|
"If the user wants to stop the agent, call `bizar_kill <instanceId>`",
|
|
395
481
|
"Do NOT block waiting for the result unless the user explicitly asked for it",
|
|
396
482
|
],
|
|
@@ -398,4 +484,4 @@ export function createBgSpawnTool(deps: BgSpawnDeps) {
|
|
|
398
484
|
};
|
|
399
485
|
},
|
|
400
486
|
});
|
|
401
|
-
}
|
|
487
|
+
}
|
|
@@ -95,5 +95,15 @@ function toViewShape(inst: import("../background-state.js").BackgroundState): In
|
|
|
95
95
|
if (inst.interventionAt !== undefined) v.interventionAt = inst.interventionAt;
|
|
96
96
|
if (inst.interventionReason !== undefined) v.interventionReason = inst.interventionReason;
|
|
97
97
|
}
|
|
98
|
+
// v5.x — extended dashboard surface.
|
|
99
|
+
if (Array.isArray(inst.toolCalls) && inst.toolCalls.length > 0) {
|
|
100
|
+
v.toolCalls = inst.toolCalls.slice();
|
|
101
|
+
}
|
|
102
|
+
if (typeof inst.progress === "number") v.progress = inst.progress;
|
|
103
|
+
if (inst.progressMessage !== undefined) v.progressMessage = inst.progressMessage;
|
|
104
|
+
if (inst.processId !== undefined) v.processId = inst.processId;
|
|
105
|
+
if (inst.runnerState !== undefined) v.runnerState = inst.runnerState;
|
|
106
|
+
if (Array.isArray(inst.tags) && inst.tags.length > 0) v.tags = inst.tags.slice();
|
|
107
|
+
if (inst.pausedAt !== undefined) v.pausedAt = inst.pausedAt;
|
|
98
108
|
return v;
|
|
99
109
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plugins/bizar/src/tools/memory-list.ts
|
|
3
|
+
*
|
|
4
|
+
* `bizar_memory_list` tool — list notes in the Bizar Memory vault.
|
|
5
|
+
*
|
|
6
|
+
* Calls GET /api/memory/notes?prefix=... on the dashboard server.
|
|
7
|
+
* Returns an array of { path, title, updated, type } objects.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { tool } from "@opencode-ai/plugin";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
|
|
16
|
+
import type { Logger } from "../logger.js";
|
|
17
|
+
|
|
18
|
+
export interface MemoryListDeps {
|
|
19
|
+
worktree: string;
|
|
20
|
+
logger: Logger;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function resolveDashboardPort(): Promise<number | null> {
|
|
24
|
+
const portFile = join(homedir(), ".config", "bizar", "dashboard.port");
|
|
25
|
+
if (existsSync(portFile)) {
|
|
26
|
+
try {
|
|
27
|
+
const raw = readFileSync(portFile, "utf8").trim();
|
|
28
|
+
const n = parseInt(raw, 10);
|
|
29
|
+
if (Number.isFinite(n) && n > 0 && n <= 65535) return n;
|
|
30
|
+
} catch {
|
|
31
|
+
// fall through
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const envPort = process.env.BIZAR_DASHBOARD_PORT;
|
|
35
|
+
if (envPort) {
|
|
36
|
+
const n = parseInt(envPort, 10);
|
|
37
|
+
if (Number.isFinite(n) && n > 0 && n <= 65535) return n;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createMemoryListTool(deps: MemoryListDeps) {
|
|
43
|
+
return tool({
|
|
44
|
+
description:
|
|
45
|
+
"List notes in the Bizar Memory vault, optionally filtered by path prefix. " +
|
|
46
|
+
"Returns { notes: [{ path, title, updated, type }] }. " +
|
|
47
|
+
"Available to all agents.",
|
|
48
|
+
args: {
|
|
49
|
+
prefix: z
|
|
50
|
+
.string()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("Optional vault-relative path prefix to filter results " +
|
|
53
|
+
"(e.g. 'decisions/' lists all notes under decisions/)."),
|
|
54
|
+
limit: z
|
|
55
|
+
.number()
|
|
56
|
+
.int()
|
|
57
|
+
.positive()
|
|
58
|
+
.optional()
|
|
59
|
+
.default(50)
|
|
60
|
+
.describe("Max notes to return (default 50)."),
|
|
61
|
+
},
|
|
62
|
+
execute: async (rawArgs) => {
|
|
63
|
+
const args = rawArgs as { prefix?: string; limit?: number };
|
|
64
|
+
const { logger } = deps;
|
|
65
|
+
|
|
66
|
+
const port = await resolveDashboardPort();
|
|
67
|
+
if (!port) {
|
|
68
|
+
return {
|
|
69
|
+
output: JSON.stringify({
|
|
70
|
+
error: "dashboard_not_running",
|
|
71
|
+
message: "The Bizar dashboard is not running. Start it with `bizar dash start`.",
|
|
72
|
+
notes: [],
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const params = new URLSearchParams();
|
|
78
|
+
if (args.prefix) {
|
|
79
|
+
params.set("prefix", args.prefix);
|
|
80
|
+
}
|
|
81
|
+
const limit = Math.min(args.limit ?? 50, 200);
|
|
82
|
+
params.set("limit", String(limit));
|
|
83
|
+
|
|
84
|
+
const url = `http://127.0.0.1:${port}/api/memory/notes?${params}`;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch(url, { method: "GET" });
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
const text = await res.text().catch(() => "");
|
|
90
|
+
logger.warn(`bizar: memory-list HTTP ${res.status}: ${text.slice(0, 200)}`);
|
|
91
|
+
return {
|
|
92
|
+
output: JSON.stringify({
|
|
93
|
+
error: "list_failed",
|
|
94
|
+
message: `HTTP ${res.status}: ${text.slice(0, 200)}`,
|
|
95
|
+
notes: [],
|
|
96
|
+
}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const data = (await res.json()) as {
|
|
101
|
+
notes?: Array<{
|
|
102
|
+
relPath?: string;
|
|
103
|
+
frontmatter?: { title?: string; type?: string };
|
|
104
|
+
mtime?: number;
|
|
105
|
+
}>;
|
|
106
|
+
count?: number;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const notes = (data.notes ?? []).map((n) => ({
|
|
110
|
+
path: n.relPath ?? "",
|
|
111
|
+
title: n.frontmatter?.title ?? null,
|
|
112
|
+
updated: n.mtime ? new Date(n.mtime).toISOString() : null,
|
|
113
|
+
type: n.frontmatter?.type ?? null,
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
output: JSON.stringify({
|
|
118
|
+
notes,
|
|
119
|
+
count: notes.length,
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
} catch (err) {
|
|
123
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
124
|
+
logger.warn(`bizar: memory-list failed: ${msg}`);
|
|
125
|
+
return {
|
|
126
|
+
output: JSON.stringify({
|
|
127
|
+
error: "list_failed",
|
|
128
|
+
message: msg,
|
|
129
|
+
notes: [],
|
|
130
|
+
}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|