@polderlabs/bizar 5.4.0 → 5.5.0
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-DqvB8ZcJ.js → EnvVarsSection-L413CQEI.js} +3 -3
- package/bizar-dash/dist/assets/{EnvVarsSection-DqvB8ZcJ.js.map → EnvVarsSection-L413CQEI.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-DRXP2ERQ.js → MobileChat-0kGQb1S6.js} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-DRXP2ERQ.js.map → MobileChat-0kGQb1S6.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-Cq1oFIdO.js +1 -0
- package/bizar-dash/dist/assets/{MobileSettings-CVW9VhEo.js.map → MobileSettings-Cq1oFIdO.js.map} +1 -1
- package/bizar-dash/dist/assets/{Toast-CcZVVN4i.js → Toast-C6f1zFJ2.js} +1 -1
- package/bizar-dash/dist/assets/{Toast-CcZVVN4i.js.map → Toast-C6f1zFJ2.js.map} +1 -1
- package/bizar-dash/dist/assets/{icons-OMEGx5KA.js → icons-BsWXWkEF.js} +121 -126
- package/bizar-dash/dist/assets/icons-BsWXWkEF.js.map +1 -0
- package/bizar-dash/dist/assets/main-BklYy02e.css +1 -0
- package/bizar-dash/dist/assets/main-CBTgYSeG.js +18 -0
- package/bizar-dash/dist/assets/main-CBTgYSeG.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-CsaN4CxH.js +1 -0
- package/bizar-dash/dist/assets/{mobile-XN2P7YfM.js.map → mobile-CsaN4CxH.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-layout-D6V-7WnA.css → mobile-layout-CJnZNLy3.css} +1 -1
- package/bizar-dash/dist/assets/mobile-layout-iFBdjEE4.js +2 -0
- package/bizar-dash/dist/assets/mobile-layout-iFBdjEE4.js.map +1 -0
- package/bizar-dash/dist/assets/{useSlashCommands-iyoW95UB.js → useSlashCommands-DGJcC9iQ.js} +2 -2
- package/bizar-dash/dist/assets/{useSlashCommands-iyoW95UB.js.map → useSlashCommands-DGJcC9iQ.js.map} +1 -1
- package/bizar-dash/dist/index.html +8 -8
- package/bizar-dash/dist/mobile.html +4 -4
- 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 +574 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +242 -0
- package/bizar-dash/src/server/memory-store.mjs +43 -3
- package/bizar-dash/src/server/routes/background.mjs +143 -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/server.mjs +22 -0
- package/bizar-dash/src/web/App.tsx +3 -0
- package/bizar-dash/src/web/MobileApp.tsx +1 -10
- package/bizar-dash/src/web/components/InstallConfirmDialog.tsx +1 -1
- package/bizar-dash/src/web/components/SettingsNav.tsx +1 -10
- package/bizar-dash/src/web/lib/types.ts +39 -1
- package/bizar-dash/src/web/styles/main.css +43 -0
- package/bizar-dash/src/web/styles/mobile-layout.css +7 -5
- package/bizar-dash/src/web/styles/settings.css +46 -0
- package/bizar-dash/src/web/views/BackgroundAgents.tsx +399 -30
- package/bizar-dash/src/web/views/Settings.tsx +12 -12
- package/bizar-dash/src/web/views/SpawnAgentModal.tsx +221 -0
- 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-spawn-from-ui.test.mjs +36 -0
- package/bizar-dash/tests/background-steer.test.mjs +42 -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/mobile-layout.test.tsx +36 -0
- package/bizar-dash/tests/settings-layout.test.tsx +3 -3
- package/bizar-dash/tests/settings-nav.test.tsx +7 -5
- package/cli/bin.mjs +16 -0
- package/cli/commands/deploy/docker.mjs +2 -1
- package/cli/commands/lightrag.mjs +171 -0
- 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 +84 -5
- package/plugins/bizar/src/background.ts +388 -10
- 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 +87 -1
- 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 +105 -0
- package/plugins/bizar/src/tools/bg-spawn.ts +13 -6
- 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 +46 -0
- package/bizar-dash/dist/assets/MobileSettings-CVW9VhEo.js +0 -1
- package/bizar-dash/dist/assets/icons-OMEGx5KA.js.map +0 -1
- package/bizar-dash/dist/assets/main-CMAGZouj.js +0 -16
- package/bizar-dash/dist/assets/main-CMAGZouj.js.map +0 -1
- package/bizar-dash/dist/assets/main-O2tZVdHm.css +0 -1
- package/bizar-dash/dist/assets/mobile-XN2P7YfM.js +0 -1
- package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js +0 -2
- package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js.map +0 -1
|
@@ -43,17 +43,60 @@ import os from "node:os";
|
|
|
43
43
|
|
|
44
44
|
// --- Public types (spec §3.2) ---------------------------------------------
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* v5.x — Per-tool-call history entry. One row per tool invocation.
|
|
48
|
+
* `result` is the truncated text returned by the tool; `error` is the
|
|
49
|
+
* error message if the tool threw.
|
|
50
|
+
*
|
|
51
|
+
* Persistence: stored as `toolCalls` on `BackgroundState`, capped at
|
|
52
|
+
* {@link MAX_TOOL_CALL_HISTORY} entries to keep the JSON state file
|
|
53
|
+
* small. The `args` and `result` fields are truncated to
|
|
54
|
+
* {@link MAX_TOOL_ARG_CHARS} characters each.
|
|
55
|
+
*/
|
|
56
|
+
export interface ToolCallEntry {
|
|
57
|
+
/** Plugin-generated tool call id. */
|
|
58
|
+
id: string;
|
|
59
|
+
/** Tool name (e.g. "bash", "edit", "read"). */
|
|
60
|
+
name: string;
|
|
61
|
+
/** JSON-serialized args (truncated). */
|
|
62
|
+
args: string;
|
|
63
|
+
/** "running" while the tool is in-flight; "ok" or "error" after. */
|
|
64
|
+
status: "running" | "ok" | "error";
|
|
65
|
+
/** Epoch ms when the tool call started. */
|
|
66
|
+
startedAt: number;
|
|
67
|
+
/** Epoch ms when the tool call completed (omitted while running). */
|
|
68
|
+
endedAt?: number;
|
|
69
|
+
/** Result preview (truncated). */
|
|
70
|
+
result?: string;
|
|
71
|
+
/** Error message (if status === "error"). */
|
|
72
|
+
error?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** v5.x — Cap on how many tool calls we remember per instance. */
|
|
76
|
+
export const MAX_TOOL_CALL_HISTORY = 100;
|
|
77
|
+
/** v5.x — Cap on per-tool args/result length (chars). */
|
|
78
|
+
export const MAX_TOOL_ARG_CHARS = 1_000;
|
|
79
|
+
|
|
46
80
|
/**
|
|
47
81
|
* Background instance status. Maps opencode events and lifecycle transitions
|
|
48
82
|
* to a small, stable set of terminal and in-flight states.
|
|
83
|
+
*
|
|
84
|
+
* v5.x — `paused` added for pause/resume support. `steered` added for
|
|
85
|
+
* mid-flight redirect (kill + restart with new prompt). Both are
|
|
86
|
+
* informational and don't change the wire-state contract: `paused` is
|
|
87
|
+
* a non-terminal in-flight status (the underlying subprocess is alive
|
|
88
|
+
* but suspended), and `steered` is treated as terminal for cleanup
|
|
89
|
+
* purposes because the original subprocess has exited.
|
|
49
90
|
*/
|
|
50
91
|
export type BackgroundStatus =
|
|
51
92
|
| "pending"
|
|
52
93
|
| "running"
|
|
94
|
+
| "paused"
|
|
53
95
|
| "done"
|
|
54
96
|
| "failed"
|
|
55
97
|
| "killed"
|
|
56
|
-
| "timed_out"
|
|
98
|
+
| "timed_out"
|
|
99
|
+
| "steered";
|
|
57
100
|
|
|
58
101
|
/**
|
|
59
102
|
* Per-instance state for a background agent.
|
|
@@ -78,10 +121,11 @@ export type BackgroundStatus =
|
|
|
78
121
|
* - `parentAgent` — who spawned it (always "odin" in v0.4 per §6.3).
|
|
79
122
|
* - `parentInstanceId` — for nested spawns (reserved, not in v0.4).
|
|
80
123
|
* - `logPath` — path to `~/.cache/bizar/logs/<sessionId>.log`.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
124
|
+
* - `toolCallCount` — updated via `EventMessagePartUpdated` events.
|
|
125
|
+
* For tool call history with names/args/results, see `toolCalls`.
|
|
126
|
+
* - `loopGuardTool` — set when threshold-12 throw is captured. Used at
|
|
127
|
+
* `bizar_collect` to prepend the marker.
|
|
128
|
+
* - `timeoutMs` — collect-time timeout requested by caller.
|
|
85
129
|
* - `lastEventAt` — epoch ms when the most recent SSE event arrived for
|
|
86
130
|
* this instance. Set on every event the manager observes (tool/text
|
|
87
131
|
* part updates, session.idle, session.error, session.created/updated/
|
|
@@ -151,6 +195,28 @@ export interface BackgroundState {
|
|
|
151
195
|
timeoutMs: number;
|
|
152
196
|
toolCallCount: number;
|
|
153
197
|
loopGuardTool?: string;
|
|
198
|
+
/**
|
|
199
|
+
* v5.x — Per-tool-call history. Records each tool invocation with its
|
|
200
|
+
* args, status, timing, and (when available) the tool result or
|
|
201
|
+
* error. Capped at the last 100 entries per instance (see
|
|
202
|
+
* `MAX_TOOL_CALL_HISTORY`). Exposed via `InstanceView.toolCalls` so
|
|
203
|
+
* the dashboard can render a tool-call list.
|
|
204
|
+
*/
|
|
205
|
+
toolCalls?: ToolCallEntry[];
|
|
206
|
+
/**
|
|
207
|
+
* v5.x — Progress reporting. Set by `bizar_report_progress` from the
|
|
208
|
+
* running agent. Range 0..100; -1 means "indeterminate". The
|
|
209
|
+
* dashboard renders this as a progress bar.
|
|
210
|
+
*/
|
|
211
|
+
progress?: number;
|
|
212
|
+
/** v5.x — Free-form progress message from the agent. */
|
|
213
|
+
progressMessage?: string;
|
|
214
|
+
/** v5.x — When the progress was last updated. */
|
|
215
|
+
progressAt?: number;
|
|
216
|
+
/** v5.x — Pause timestamp (when the subprocess was SIGSTOPed). */
|
|
217
|
+
pausedAt?: number;
|
|
218
|
+
/** v5.x — Optional list of tags supplied at spawn time. */
|
|
219
|
+
tags?: string[];
|
|
154
220
|
// v0.3.0 — stall and thinking-loop protection. These are typed as
|
|
155
221
|
// optional in the schema because (a) the field can be absent in older
|
|
156
222
|
// files on disk, and (b) the `InstanceManager.add()` input (AddDraft)
|
|
@@ -229,12 +295,18 @@ export const EMPTY_BACKGROUND_STATE: Omit<
|
|
|
229
295
|
|
|
230
296
|
/**
|
|
231
297
|
* Terminal states per spec §1.6 / §4.4. Used by collect/await logic.
|
|
298
|
+
*
|
|
299
|
+
* v5.x — `steered` is treated as terminal because the original
|
|
300
|
+
* subprocess has exited; a new instance is created to carry the
|
|
301
|
+
* steered work. Collect on a steered instance returns whatever text
|
|
302
|
+
* the subprocess emitted before the kill.
|
|
232
303
|
*/
|
|
233
304
|
export const TERMINAL_STATUSES: ReadonlySet<BackgroundStatus> = new Set<BackgroundStatus>([
|
|
234
305
|
"done",
|
|
235
306
|
"failed",
|
|
236
307
|
"killed",
|
|
237
308
|
"timed_out",
|
|
309
|
+
"steered",
|
|
238
310
|
]);
|
|
239
311
|
|
|
240
312
|
// --- Logger interface -----------------------------------------------------
|
|
@@ -391,6 +463,13 @@ function readState(
|
|
|
391
463
|
if (typeof parsed.maxRestarts !== "number") {
|
|
392
464
|
parsed.maxRestarts = 3;
|
|
393
465
|
}
|
|
466
|
+
// v5.x — backfill tool call history and progress for old files.
|
|
467
|
+
if (!Array.isArray(parsed.toolCalls)) {
|
|
468
|
+
parsed.toolCalls = [];
|
|
469
|
+
}
|
|
470
|
+
if (typeof parsed.progress !== "number") {
|
|
471
|
+
parsed.progress = 0;
|
|
472
|
+
}
|
|
394
473
|
return parsed;
|
|
395
474
|
} catch (err: unknown) {
|
|
396
475
|
logger.log({
|
|
@@ -61,13 +61,24 @@
|
|
|
61
61
|
* map is never left in a half-state.
|
|
62
62
|
*/
|
|
63
63
|
|
|
64
|
-
import type {
|
|
65
|
-
|
|
64
|
+
import type {
|
|
65
|
+
BackgroundState,
|
|
66
|
+
BackgroundStateStore,
|
|
67
|
+
Logger,
|
|
68
|
+
ToolCallEntry,
|
|
69
|
+
} from "./background-state.js";
|
|
70
|
+
import {
|
|
71
|
+
MAX_TOOL_CALL_HISTORY,
|
|
72
|
+
MAX_TOOL_ARG_CHARS,
|
|
73
|
+
TERMINAL_STATUSES,
|
|
74
|
+
} from "./background-state.js";
|
|
66
75
|
import type { HttpClient } from "./http-client.js";
|
|
67
76
|
import type { EventStream, StreamEvent, SessionEventHandler } from "./event-stream.js";
|
|
68
77
|
import type { ServeLifecycle } from "./serve.js";
|
|
69
78
|
import { researchInterventionPrompt } from "./research-prompt.js";
|
|
70
79
|
|
|
80
|
+
import * as opencodeRunner from "./opencode-runner.js";
|
|
81
|
+
|
|
71
82
|
// --- Public surface -------------------------------------------------------
|
|
72
83
|
|
|
73
84
|
/** A snapshot of an instance for the `bizar_status` tool. */
|
|
@@ -89,6 +100,21 @@ export interface InstanceView {
|
|
|
89
100
|
interventionCount?: number;
|
|
90
101
|
interventionAt?: number;
|
|
91
102
|
interventionReason?: string;
|
|
103
|
+
// v5.x — extended surface for dashboard integration
|
|
104
|
+
/** Tool call history (cap 100). Each entry has name, args, status, timing. */
|
|
105
|
+
toolCalls?: ToolCallEntry[];
|
|
106
|
+
/** Progress percentage (0..100; -1 = indeterminate). */
|
|
107
|
+
progress?: number;
|
|
108
|
+
/** Progress message (free-form). */
|
|
109
|
+
progressMessage?: string;
|
|
110
|
+
/** PID of the opencode run subprocess, if known. */
|
|
111
|
+
processId?: number;
|
|
112
|
+
/** Current runner state (one of "starting"|"running"|"done"|"failed"|"killed"). */
|
|
113
|
+
runnerState?: string;
|
|
114
|
+
/** Tags from spawn (free-form). */
|
|
115
|
+
tags?: string[];
|
|
116
|
+
/** When the instance was paused (epoch ms), if paused. */
|
|
117
|
+
pausedAt?: number;
|
|
92
118
|
}
|
|
93
119
|
|
|
94
120
|
/** The return shape of `bizar_collect`. */
|
|
@@ -706,9 +732,22 @@ export class InstanceManager {
|
|
|
706
732
|
|
|
707
733
|
/**
|
|
708
734
|
* Scan the bg directory, load every instance, and rebuild the in-memory
|
|
709
|
-
* map. Any `running` or `pending` instance is
|
|
710
|
-
*
|
|
711
|
-
*
|
|
735
|
+
* map. Any `running` or `pending` instance is checked for liveness:
|
|
736
|
+
*
|
|
737
|
+
* - If the instance has a `processId` AND the opencode run subprocess
|
|
738
|
+
* is still alive (per `opencodeRunner.isAlive()`), we RE-ADOPT it:
|
|
739
|
+
* status flips back to `running`, `runnerState: "adopted"`, and the
|
|
740
|
+
* the manager continues to monitor via the existing
|
|
741
|
+
* `onExit` callback registered at spawn time.
|
|
742
|
+
* - If the subprocess is gone (or there was no PID on file), we mark
|
|
743
|
+
* the instance `failed` with reason "subprocess died during restart".
|
|
744
|
+
*
|
|
745
|
+
* Historical terminal records (done, failed, killed, timed_out, steered)
|
|
746
|
+
* are preserved as-is.
|
|
747
|
+
*
|
|
748
|
+
* Best-effort: liveness check uses `process.kill(pid, 0)` which is fast
|
|
749
|
+
* (no signal is sent) and safe (returns false on EPERM / ESRCH without
|
|
750
|
+
* raising to us).
|
|
712
751
|
*/
|
|
713
752
|
async rebuildInMemoryMap(): Promise<void> {
|
|
714
753
|
let all: BackgroundState[];
|
|
@@ -724,14 +763,27 @@ export class InstanceManager {
|
|
|
724
763
|
}
|
|
725
764
|
let rebuilt = 0;
|
|
726
765
|
let failed = 0;
|
|
766
|
+
let adopted = 0;
|
|
727
767
|
for (const inst of all) {
|
|
728
768
|
this.instances.set(inst.instanceId, inst);
|
|
729
769
|
rebuilt += 1;
|
|
730
770
|
if (inst.status === "running" || inst.status === "pending") {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
771
|
+
// Try to adopt a still-alive subprocess.
|
|
772
|
+
const pid = inst.processId;
|
|
773
|
+
const alive = typeof pid === "number" && opencodeRunner.isAlive(pid);
|
|
774
|
+
if (alive) {
|
|
775
|
+
await this.update(inst.instanceId, {
|
|
776
|
+
status: "running",
|
|
777
|
+
completedAt: undefined,
|
|
778
|
+
runnerState: "adopted",
|
|
779
|
+
});
|
|
780
|
+
adopted += 1;
|
|
781
|
+
this.logger.info(
|
|
782
|
+
`bizar: adopted still-alive subprocess for instance ${inst.instanceId} (pid=${pid})`,
|
|
783
|
+
);
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
const message = "subprocess died during restart";
|
|
735
787
|
await this.update(inst.instanceId, {
|
|
736
788
|
status: "failed",
|
|
737
789
|
error: message,
|
|
@@ -742,7 +794,7 @@ export class InstanceManager {
|
|
|
742
794
|
}
|
|
743
795
|
if (rebuilt > 0) {
|
|
744
796
|
this.logger.info(
|
|
745
|
-
`bizar: rebuilt in-memory map (${rebuilt} instances, ${failed} marked failed)`,
|
|
797
|
+
`bizar: rebuilt in-memory map (${rebuilt} instances, ${adopted} adopted, ${failed} marked failed)`,
|
|
746
798
|
);
|
|
747
799
|
}
|
|
748
800
|
}
|
|
@@ -991,6 +1043,192 @@ export class InstanceManager {
|
|
|
991
1043
|
await this._maybeAutoRestart(instanceId);
|
|
992
1044
|
}
|
|
993
1045
|
|
|
1046
|
+
// --- v5.x — pause / resume / progress / tool-call instrumentation -------
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Pause a running instance. Sends SIGSTOP to the opencode run subprocess
|
|
1050
|
+
* (POSIX only — returns `{ ok: false, error: "..." }` on Windows and
|
|
1051
|
+
* for processes we can't signal). On success, the in-memory status
|
|
1052
|
+
* flips to `"paused"` and `pausedAt` is stamped.
|
|
1053
|
+
*
|
|
1054
|
+
* Already-terminal instances return `{ ok: false, error: "already_terminal" }`.
|
|
1055
|
+
* Already-paused instances return `{ ok: true }` (no-op).
|
|
1056
|
+
*/
|
|
1057
|
+
async pause(instanceId: string): Promise<{ ok: boolean; error?: string }> {
|
|
1058
|
+
const inst = this.instances.get(instanceId);
|
|
1059
|
+
if (!inst) return { ok: false, error: "instance_not_found" };
|
|
1060
|
+
if (TERMINAL_STATUSES.has(inst.status)) {
|
|
1061
|
+
return { ok: false, error: "already_terminal" };
|
|
1062
|
+
}
|
|
1063
|
+
if (inst.status === "paused") return { ok: true };
|
|
1064
|
+
if (inst.processId === undefined) {
|
|
1065
|
+
// No PID on file — for v0.8.0 the runner is the only owner of the
|
|
1066
|
+
// subprocess. Try the runner's registry as a fallback before giving up.
|
|
1067
|
+
const runnerStatus = opencodeRunner.getStatus(NaN as unknown as number);
|
|
1068
|
+
if (runnerStatus === null) {
|
|
1069
|
+
return { ok: false, error: "no_subprocess" };
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
const pid = inst.processId;
|
|
1073
|
+
if (pid === undefined) {
|
|
1074
|
+
return { ok: false, error: "no_subprocess" };
|
|
1075
|
+
}
|
|
1076
|
+
const res = opencodeRunner.pauseAgent(pid);
|
|
1077
|
+
if (!res.ok) return { ok: false, error: res.error || "pause_failed" };
|
|
1078
|
+
await this.update(instanceId, {
|
|
1079
|
+
status: "paused",
|
|
1080
|
+
pausedAt: Date.now(),
|
|
1081
|
+
});
|
|
1082
|
+
this.logger.info(
|
|
1083
|
+
`bizar: paused background instance ${instanceId} (pid=${pid})`,
|
|
1084
|
+
);
|
|
1085
|
+
return { ok: true };
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Resume a paused instance. Sends SIGCONT to the subprocess and flips
|
|
1090
|
+
* status back to `"running"`. Idempotent: resuming a non-paused
|
|
1091
|
+
* instance is a no-op that returns `{ ok: true }`.
|
|
1092
|
+
*/
|
|
1093
|
+
async resume(instanceId: string): Promise<{ ok: boolean; error?: string }> {
|
|
1094
|
+
const inst = this.instances.get(instanceId);
|
|
1095
|
+
if (!inst) return { ok: false, error: "instance_not_found" };
|
|
1096
|
+
if (inst.status !== "paused") return { ok: true };
|
|
1097
|
+
if (inst.processId === undefined) {
|
|
1098
|
+
return { ok: false, error: "no_subprocess" };
|
|
1099
|
+
}
|
|
1100
|
+
const res = opencodeRunner.resumeAgent(inst.processId);
|
|
1101
|
+
if (!res.ok) return { ok: false, error: res.error || "resume_failed" };
|
|
1102
|
+
await this.update(instanceId, {
|
|
1103
|
+
status: "running",
|
|
1104
|
+
pausedAt: undefined,
|
|
1105
|
+
});
|
|
1106
|
+
this.logger.info(
|
|
1107
|
+
`bizar: resumed background instance ${instanceId} (pid=${inst.processId})`,
|
|
1108
|
+
);
|
|
1109
|
+
return { ok: true };
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Update progress (and optional message) for an instance. Called by the
|
|
1114
|
+
* `bizar_report_progress` tool. Pinned to [0, 100] unless `indeterminate`
|
|
1115
|
+
* is true, in which case we set progress to -1.
|
|
1116
|
+
*/
|
|
1117
|
+
async updateProgress(
|
|
1118
|
+
instanceId: string,
|
|
1119
|
+
step: number,
|
|
1120
|
+
total: number,
|
|
1121
|
+
message?: string,
|
|
1122
|
+
indeterminate = false,
|
|
1123
|
+
): Promise<{ ok: boolean; progress?: number; error?: string }> {
|
|
1124
|
+
const inst = this.instances.get(instanceId);
|
|
1125
|
+
if (!inst) return { ok: false, error: "instance_not_found" };
|
|
1126
|
+
if (TERMINAL_STATUSES.has(inst.status)) {
|
|
1127
|
+
return { ok: false, error: "already_terminal" };
|
|
1128
|
+
}
|
|
1129
|
+
let pct: number;
|
|
1130
|
+
if (indeterminate) {
|
|
1131
|
+
pct = -1;
|
|
1132
|
+
} else {
|
|
1133
|
+
const safeTotal = Math.max(1, Math.floor(total));
|
|
1134
|
+
const safeStep = Math.max(0, Math.floor(step));
|
|
1135
|
+
pct = Math.max(0, Math.min(100, Math.round((safeStep / safeTotal) * 100)));
|
|
1136
|
+
}
|
|
1137
|
+
const patch: Partial<BackgroundState> = {
|
|
1138
|
+
progress: pct,
|
|
1139
|
+
progressAt: Date.now(),
|
|
1140
|
+
};
|
|
1141
|
+
if (message !== undefined) patch.progressMessage = message.slice(0, 500);
|
|
1142
|
+
await this.update(instanceId, patch);
|
|
1143
|
+
return { ok: true, progress: pct };
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* Record a tool-call start. Returns the generated id so callers can
|
|
1148
|
+
* pass it to `completeToolCall()` later. Caps the history at
|
|
1149
|
+
* `MAX_TOOL_CALL_HISTORY` entries.
|
|
1150
|
+
*/
|
|
1151
|
+
async recordToolCallStart(
|
|
1152
|
+
instanceId: string,
|
|
1153
|
+
name: string,
|
|
1154
|
+
args: unknown,
|
|
1155
|
+
): Promise<string> {
|
|
1156
|
+
const id = `tc_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
1157
|
+
const inst = this.instances.get(instanceId);
|
|
1158
|
+
if (!inst) return id;
|
|
1159
|
+
const entry: ToolCallEntry = {
|
|
1160
|
+
id,
|
|
1161
|
+
name: String(name ?? "").slice(0, 200),
|
|
1162
|
+
args: truncateForToolHistory(args),
|
|
1163
|
+
status: "running",
|
|
1164
|
+
startedAt: Date.now(),
|
|
1165
|
+
};
|
|
1166
|
+
const existing = (inst.toolCalls ?? []).slice(-MAX_TOOL_CALL_HISTORY + 1);
|
|
1167
|
+
const next = [...existing, entry].slice(-MAX_TOOL_CALL_HISTORY);
|
|
1168
|
+
await this.update(instanceId, { toolCalls: next });
|
|
1169
|
+
return id;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Mark a tool-call complete. `result` and `error` are both truncated.
|
|
1174
|
+
* A no-op if no matching entry is found (the cap may have rolled it off).
|
|
1175
|
+
*/
|
|
1176
|
+
async completeToolCall(
|
|
1177
|
+
instanceId: string,
|
|
1178
|
+
id: string,
|
|
1179
|
+
result?: string,
|
|
1180
|
+
error?: string,
|
|
1181
|
+
): Promise<void> {
|
|
1182
|
+
const inst = this.instances.get(instanceId);
|
|
1183
|
+
if (!inst || !Array.isArray(inst.toolCalls)) return;
|
|
1184
|
+
const idx = inst.toolCalls.findIndex((t) => t.id === id);
|
|
1185
|
+
if (idx < 0) return;
|
|
1186
|
+
const cur = inst.toolCalls[idx];
|
|
1187
|
+
if (!cur) return;
|
|
1188
|
+
const next = inst.toolCalls.slice();
|
|
1189
|
+
const updated: ToolCallEntry = {
|
|
1190
|
+
...cur,
|
|
1191
|
+
status: error ? "error" : "ok",
|
|
1192
|
+
endedAt: Date.now(),
|
|
1193
|
+
};
|
|
1194
|
+
if (error !== undefined) {
|
|
1195
|
+
updated.error = String(error).slice(0, MAX_TOOL_ARG_CHARS);
|
|
1196
|
+
} else if (result !== undefined) {
|
|
1197
|
+
updated.result = String(result).slice(0, MAX_TOOL_ARG_CHARS);
|
|
1198
|
+
}
|
|
1199
|
+
next[idx] = updated;
|
|
1200
|
+
await this.update(instanceId, { toolCalls: next });
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* Public accessor used by the opencode-runner when it wants to surface
|
|
1205
|
+
* the recorded tool-call list (read-only). Returns a copy.
|
|
1206
|
+
*/
|
|
1207
|
+
listToolCalls(instanceId: string): ToolCallEntry[] {
|
|
1208
|
+
const inst = this.instances.get(instanceId);
|
|
1209
|
+
if (!inst || !Array.isArray(inst.toolCalls)) return [];
|
|
1210
|
+
return inst.toolCalls.slice();
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Public: mark an instance as "adopted" — used by `rebuildInMemoryMap`
|
|
1215
|
+
* when a previously running instance's subprocess is still alive. The
|
|
1216
|
+
* status flips back to "running" and the runner is resumed.
|
|
1217
|
+
*/
|
|
1218
|
+
async adoptInstance(instanceId: string): Promise<void> {
|
|
1219
|
+
const inst = this.instances.get(instanceId);
|
|
1220
|
+
if (!inst) return;
|
|
1221
|
+
if (!TERMINAL_STATUSES.has(inst.status)) {
|
|
1222
|
+
// Already alive in our map; nothing to adopt.
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
await this.update(instanceId, {
|
|
1226
|
+
status: "running",
|
|
1227
|
+
completedAt: undefined,
|
|
1228
|
+
runnerState: "adopted",
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
|
|
994
1232
|
/** v0.5.5 — Attempt an auto-restart for a persistent failed instance. */
|
|
995
1233
|
private async _maybeAutoRestart(instanceId: string): Promise<void> {
|
|
996
1234
|
const inst = this.instances.get(instanceId);
|
|
@@ -1066,6 +1304,59 @@ export class InstanceManager {
|
|
|
1066
1304
|
}
|
|
1067
1305
|
}
|
|
1068
1306
|
|
|
1307
|
+
// --- v5.x — Tool call history recording -----------------------------
|
|
1308
|
+
// The tool part re-fires on every state transition (pending ->
|
|
1309
|
+
// running -> completed). We key the history entry by `partID` so
|
|
1310
|
+
// the same call's "running" and "completed" messages converge to
|
|
1311
|
+
// one row. When a part has no entry yet, we record a fresh "running"
|
|
1312
|
+
// entry; when it already exists, we update status/result/error.
|
|
1313
|
+
if (part.type === "tool") {
|
|
1314
|
+
const toolName = readToolName(part);
|
|
1315
|
+
const existing = Array.isArray(inst.toolCalls)
|
|
1316
|
+
? inst.toolCalls.find((t) => t.id === ev.partID)
|
|
1317
|
+
: undefined;
|
|
1318
|
+
if (existing === undefined) {
|
|
1319
|
+
// Fresh tool call — record start.
|
|
1320
|
+
const tcId = ev.partID;
|
|
1321
|
+
const entry: ToolCallEntry = {
|
|
1322
|
+
id: tcId,
|
|
1323
|
+
name: toolName,
|
|
1324
|
+
args: readToolArgs(part),
|
|
1325
|
+
status: readToolStatus(part),
|
|
1326
|
+
startedAt: Date.now(),
|
|
1327
|
+
};
|
|
1328
|
+
const merged = [...(inst.toolCalls ?? []), entry].slice(
|
|
1329
|
+
-MAX_TOOL_CALL_HISTORY,
|
|
1330
|
+
);
|
|
1331
|
+
await this.update(instanceId, { toolCalls: merged });
|
|
1332
|
+
} else {
|
|
1333
|
+
// Existing entry — update status/result/error.
|
|
1334
|
+
const next = (inst.toolCalls ?? []).slice();
|
|
1335
|
+
const idx = next.findIndex((t) => t.id === ev.partID);
|
|
1336
|
+
if (idx >= 0) {
|
|
1337
|
+
const cur = next[idx];
|
|
1338
|
+
if (cur) {
|
|
1339
|
+
const status = readToolStatus(part);
|
|
1340
|
+
const updated: ToolCallEntry = {
|
|
1341
|
+
...cur,
|
|
1342
|
+
status,
|
|
1343
|
+
endedAt: status === "ok" || status === "error" ? Date.now() : cur.endedAt,
|
|
1344
|
+
};
|
|
1345
|
+
const errText = readToolError(part);
|
|
1346
|
+
if (errText) updated.error = errText.slice(0, MAX_TOOL_ARG_CHARS);
|
|
1347
|
+
// If the part carries a result string in `state.output` or
|
|
1348
|
+
// a result-shaped field, surface it. The opencode schema
|
|
1349
|
+
// is loose here — we accept either `state.output` or raw
|
|
1350
|
+
// `output`.
|
|
1351
|
+
const output = readToolOutput(part);
|
|
1352
|
+
if (output) updated.result = output.slice(0, MAX_TOOL_ARG_CHARS);
|
|
1353
|
+
next[idx] = updated;
|
|
1354
|
+
await this.update(instanceId, { toolCalls: next });
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1069
1360
|
// --- Loop-guard threshold-12 detection (spec §4.1) ---
|
|
1070
1361
|
if (part.type === "tool" && !inst.loopGuardTool) {
|
|
1071
1362
|
const errorText = readToolError(part);
|
|
@@ -1221,6 +1512,16 @@ function toView(inst: BackgroundState): InstanceView {
|
|
|
1221
1512
|
if (inst.interventionAt !== undefined) v.interventionAt = inst.interventionAt;
|
|
1222
1513
|
if (inst.interventionReason !== undefined) v.interventionReason = inst.interventionReason;
|
|
1223
1514
|
}
|
|
1515
|
+
// v5.x — extended dashboard surface.
|
|
1516
|
+
if (Array.isArray(inst.toolCalls) && inst.toolCalls.length > 0) {
|
|
1517
|
+
v.toolCalls = inst.toolCalls.slice();
|
|
1518
|
+
}
|
|
1519
|
+
if (typeof inst.progress === "number") v.progress = inst.progress;
|
|
1520
|
+
if (inst.progressMessage !== undefined) v.progressMessage = inst.progressMessage;
|
|
1521
|
+
if (inst.processId !== undefined) v.processId = inst.processId;
|
|
1522
|
+
if (inst.runnerState !== undefined) v.runnerState = inst.runnerState;
|
|
1523
|
+
if (Array.isArray(inst.tags) && inst.tags.length > 0) v.tags = inst.tags.slice();
|
|
1524
|
+
if (inst.pausedAt !== undefined) v.pausedAt = inst.pausedAt;
|
|
1224
1525
|
return v;
|
|
1225
1526
|
}
|
|
1226
1527
|
|
|
@@ -1237,6 +1538,66 @@ function readToolError(part: { error?: string; state?: { error?: string } }): st
|
|
|
1237
1538
|
return null;
|
|
1238
1539
|
}
|
|
1239
1540
|
|
|
1541
|
+
/**
|
|
1542
|
+
* v5.x — Read the tool name from a tool part. opencode's wire format
|
|
1543
|
+
* is loose: the tool name may live on `part.tool`, `part.name`, or as
|
|
1544
|
+
* the value of the first entries. Returns "unknown" when nothing
|
|
1545
|
+
* matches.
|
|
1546
|
+
*/
|
|
1547
|
+
function readToolName(part: { tool?: unknown; name?: unknown; [k: string]: unknown }): string {
|
|
1548
|
+
const candidates = [part.tool, part.name];
|
|
1549
|
+
for (const c of candidates) {
|
|
1550
|
+
if (typeof c === "string" && c.length > 0) return c.slice(0, 200);
|
|
1551
|
+
}
|
|
1552
|
+
return "unknown";
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* v5.x — Read the tool args from a tool part. opencode may carry
|
|
1557
|
+
* `args`, `input`, or a JSON-stringified `arguments`. Returns "" when
|
|
1558
|
+
* nothing useful is found.
|
|
1559
|
+
*/
|
|
1560
|
+
function readToolArgs(part: { args?: unknown; input?: unknown; arguments?: unknown; [k: string]: unknown }): string {
|
|
1561
|
+
const candidates = [part.args, part.input, part.arguments];
|
|
1562
|
+
for (const c of candidates) {
|
|
1563
|
+
if (c === undefined || c === null) continue;
|
|
1564
|
+
if (typeof c === "string") return c.slice(0, MAX_TOOL_ARG_CHARS);
|
|
1565
|
+
try {
|
|
1566
|
+
return JSON.stringify(c).slice(0, MAX_TOOL_ARG_CHARS);
|
|
1567
|
+
} catch {
|
|
1568
|
+
// fall through
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
return "";
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/**
|
|
1575
|
+
* v5.x — Map the part's state.status (or top-level status) to our
|
|
1576
|
+
* ToolCallEntry status enum.
|
|
1577
|
+
*/
|
|
1578
|
+
function readToolStatus(part: { state?: { status?: unknown }; status?: unknown }): "running" | "ok" | "error" {
|
|
1579
|
+
const s = part.state?.status ?? part.status;
|
|
1580
|
+
if (typeof s !== "string") return "running";
|
|
1581
|
+
const lower = s.toLowerCase();
|
|
1582
|
+
if (lower === "ok" || lower === "success" || lower === "completed" || lower === "done") return "ok";
|
|
1583
|
+
if (lower === "error" || lower === "failed") return "error";
|
|
1584
|
+
return "running";
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function readToolOutput(part: { state?: { output?: unknown; result?: unknown; status?: unknown; error?: unknown }; output?: unknown; result?: unknown }): string {
|
|
1588
|
+
const candidates = [part.state?.output, part.state?.result, part.output, part.result];
|
|
1589
|
+
for (const c of candidates) {
|
|
1590
|
+
if (c === undefined || c === null) continue;
|
|
1591
|
+
if (typeof c === "string") return c;
|
|
1592
|
+
try {
|
|
1593
|
+
return JSON.stringify(c);
|
|
1594
|
+
} catch {
|
|
1595
|
+
// fall through
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
return "";
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1240
1601
|
async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
1241
1602
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
1242
1603
|
const timeout = new Promise<never>((_, reject) => {
|
|
@@ -1248,3 +1609,20 @@ async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
|
1248
1609
|
if (timer !== null) clearTimeout(timer);
|
|
1249
1610
|
}
|
|
1250
1611
|
}
|
|
1612
|
+
|
|
1613
|
+
/**
|
|
1614
|
+
* Serialize an unknown tool-args value into a string suitable for the
|
|
1615
|
+
* `toolCalls` history. JSON-serializes objects, truncates to
|
|
1616
|
+
* {@link MAX_TOOL_ARG_CHARS}. Returns `""` for empty/undefined.
|
|
1617
|
+
*/
|
|
1618
|
+
function truncateForToolHistory(value: unknown): string {
|
|
1619
|
+
if (value === undefined || value === null) return "";
|
|
1620
|
+
let s: string;
|
|
1621
|
+
try {
|
|
1622
|
+
s = typeof value === "string" ? value : JSON.stringify(value);
|
|
1623
|
+
} catch {
|
|
1624
|
+
s = String(value);
|
|
1625
|
+
}
|
|
1626
|
+
if (s.length > MAX_TOOL_ARG_CHARS) s = s.slice(0, MAX_TOOL_ARG_CHARS) + "…";
|
|
1627
|
+
return s;
|
|
1628
|
+
}
|