Commit Graph
11 Commits
Author SHA1 Message Date
603519d158 MUL-6977: test: remove implementation-only coverage (#7932)
* test: remove implementation-only coverage

Co-authored-by: multica-agent <[email protected]>

* test: remove remaining implementation-only coverage

Co-authored-by: multica-agent <[email protected]>

* test: restore compatibility and fragment navigation coverage

Co-authored-by: multica-agent <[email protected]>

* test: preserve behavioral cleanup contracts

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-03 13:23:25 +08:00
3c7221492b fix(agent): deliver the OpenCode prompt on stdin, not argv (MUL-5841) (#6552)
* fix(agent): deliver the OpenCode prompt on stdin, not argv (#6538)

The daemon appended the whole task prompt as an argv element, so on Windows
every OpenCode task whose command line cleared CreateProcess's 32,767
character lpCommandLine limit failed to start at all. Go surfaces the
resulting ERROR_FILENAME_EXCED_RANGE (206) as "The filename or extension is
too long", which reads as an exe-path problem and is not one. A workspace
with a realistic set of models and skills clears that ceiling on the prompt
alone (39,629 bytes in the report), leaving no usable in-app workaround.

`opencode run` merges its variadic [message..] positional with whatever is
piped in, so passing no positional makes the piped text the entire run
message. That path is present in the reported v1.18.14 and back to v1.4.10.

Write the prompt from its own goroutine, as the Pi and Cursor backends
already do: a prompt larger than the pipe buffer blocks mid-write until
OpenCode drains it, and OpenCode cannot drain while nobody consumes its
stdout. OpenCode reads stdin to EOF, so stdin is closed exactly once as the
end-of-prompt signal — including on the cancellation path, where closing it
releases a writer still blocked on a full pipe. The existing process-group
SIGTERM -> SIGKILL ordering from #4533 is unchanged.

Keeping the prompt off argv also stops it being echoed into the "agent
command" log line; prompt_bytes replaces it there.

Scoped to OpenCode. DevEco, OpenClaw (#6032), Qwen, Copilot and Antigravity
inline their prompts the same way, but each CLI's input contract needs
verifying on its own before it moves.

Co-authored-by: multica-agent <[email protected]>

* fix(agent): do not fail a completed run on a benign prompt-write EPIPE

The backend CI run for this PR failed in TestPiExecuteRetainsOnlyLastTurnOutput
with "pi prompt write failed: write |1: broken pipe". The cause is a latent
race shared by pi and the OpenCode code added in the previous commit, which
copied pi's error handling.

A child that exits without draining stdin closes the read end while the parent
is still writing the prompt, so the parent's write returns EPIPE. Both backends
converted that into a failed result even when the agent had already emitted a
complete, successful stream — making the outcome depend on whether the write
landed before or after the child exited, i.e. on machine load.

OpenCode reads stdin to EOF before it does any work, so a run that produced a
complete stream necessarily received the whole prompt; an EPIPE recorded after
that only means the pipe closed on the way out. Report the write error only for
a run that did NOT complete, where it can actually explain the failure, and
append rather than overwrite so the stream's own diagnosis survives. cursor.go
already guards this correctly by ignoring writeErr once a result was seen.

Test fakes now drain stdin, matching what the real CLIs do. A fake that exits
without reading is what manufactures the spurious EPIPE, and that is what broke
CI — pi's piEventStreamScript is the fixture that failed.

Adds a deterministic regression test: a child that emits a full successful
stream and never reads a 1.2 MB prompt. Against the previous commit it
reproduces the CI signature exactly (status "failed", "broken pipe"); with this
change it passes.

pi.go's production handling has the same defect and still needs the equivalent
fix, but that belongs to whoever owns #6485 — this change keeps to OpenCode
plus the test fixture that was breaking the build.

Co-authored-by: multica-agent <[email protected]>

* fix(agent): gate prompt-write suppression on a positive terminal signal

Review caught a false-success path opened by the previous commit. It
suppressed a failed prompt write whenever status was "completed", on the
reasoning that a completed run must have received the whole prompt. That
reasoning does not hold: processEvents starts finalStatus at "completed" and
only fails closed on structural evidence — an open step, or a step awaiting a
continuation. A child that emits NOTHING and exits 0 sets neither, so the
default survives.

The full path: the parent writes a large prompt, the child exits 0 without
reading a byte, the write returns EPIPE, stdout hits EOF with no events, and
the run is reported as completed with empty output and no error. The prompt
never reached the agent and nothing said so. Reproduced deterministically
before the fix: status="completed" output="" error="".

Absence of a failure signal is not proof of success, so record the positive
one instead. eventResult.sawTerminalSignal is set only when a step_finish
closed the last step with no continuation pending, and the write error is
suppressed only on that evidence. It is deliberately not the negation of
noTerminalSignal: an empty stream sets neither, because there is nothing to
fail closed on and nothing proving completion either. When the signal is
absent the write error now also flips the default "completed" to "failed".

Adds the empty-stream regression alongside the existing complete-stream one,
so both directions of the branch are pinned.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-07 12:31:55 +08:00
37e2b07094 MUL-5831 fix(agent): fail opencode runs that end on an empty step (#6545)
* fix(agent): fail opencode runs that end on an empty step (MUL-5831)

`opencodeBackend.processEvents` required a positive terminal signal but only
failed closed on two shapes: EOF while a step was still open, and a step that
finished with reason "tool-calls" whose continuation never started.

A final step that opens, emits only a reasoning part, and closes with reason
"unknown" and an all-zero token block matches neither, so `opencode run` exited
0 and the daemon reported `completed` with an empty `agent_error` — a run with
no deliverable that leaves the issue stalled until something re-wakes the agent.
Zero input tokens means the provider round-trip never happened; that is a dead
stream wearing a clean finish.

Track whether each step produced anything at all — text, a tool call, or any
non-zero token counter — and fail closed when the run ends on a step that
produced none of them. The criterion is deliberately "this step produced
nothing", not "the run produced no text", so a task whose only deliverable is a
tool side effect stays green. The finish reason is not consulted: a missing or
unrecognised reason must stay terminal for protocol compatibility, and voidness
is orthogonal to it.

The run now enters the existing failure path with a diagnosable error, so the
task's retry rules apply instead of a silent false-green completion.

Refs #6522 — this fixes the first of its three symptoms; see the PR body.

Co-authored-by: multica-agent <[email protected]>

* fix(agent): count every opencode usage field as round-trip evidence (MUL-5831)

The void-step guard claimed "any non-zero token counter keeps the step
productive", but only checked input, output and cache. OpenCode's protocol
keeps reasoning and the aggregate total in fields of their own and reports cost
as a sibling of the token block, and none of the three were parsed at all — so
a real step that landed with reasoning or cost positive while input and output
happened to be zero would be called void and fail a healthy run.

Parse reasoning, total and cost, and move the check into stepReportedUsage so
the criterion is stated once. Reasoning and total are read as evidence only:
total is derived, and TokenUsage has no reasoning bucket, so folding either
into the usage accumulator would change billing figures rather than fix a bug.

Also renames the guard's error text to "no reported usage", which is what it
now means.

Co-authored-by: multica-agent <[email protected]>

* fix(taskfailure): make opencode stream-ended failures retryable (MUL-5831)

Turning a false-green completion red only helps if the failure then retries.
It did not: none of the three errors the OpenCode terminal-signal guard raises
was on the retry allowlist. The two "terminal signal" variants matched rule
13's "signal" substring by accident and landed in agent_error.process_failure;
the new empty-step one fell through to agent_error.unknown. Either way the task
died on the first attempt and max_attempts never applied.

All three mean one thing — the provider stream died and `opencode run` still
exited 0 — which is agent_error.provider_network by definition. Route them
there on the shared "opencode stream ended" prefix. That bucket is retryable
and resume-safe, so the retry child inherits the session and continues the
truncated conversation instead of redoing the work already paid for.

Covered end to end: the classifier cases, and a service-level test that walks
each guard error through Classify into retryEligible — retrying with an attempt
left, and still terminating at the ceiling so a deterministically broken
provider cannot loop.

Co-authored-by: multica-agent <[email protected]>

* fix(taskfailure): upgrade legacy opencode stream-ended reasons (MUL-5831)

Rule 7 only decides where these failures land when THIS server classifies
them, and FailTask classifies only when the daemon sent no reason at all. An
installed daemon predating that entry reports a non-empty
agent_error.process_failure instead — its rule 13 matching the word "signal" in
"terminal signal" — so the empty-reason branch skips it and the run stays off
the retry allowlist. The fix reached only hosts that happened to update, which
is the installed-daemon cadence problem NormalizeDaemonReason exists for.

Upgrade process_failure / unknown / coarse agent_error to provider_network when
the error opens with the guard's own prefix. Wider than the context-overflow
rule above it on purpose: that one leaves refined reasons alone because a
phrase somewhere inside an error blob says less than the bucket an earlier rule
picked, whereas this witness is the guard's message from its first character,
so the old bucket cannot be describing a better-identified cause.

The retry test becomes a matrix over both wire shapes — current daemon (server
classifies) and each legacy reason — since the previous version called Classify
directly and never crossed the normalize boundary where the gap was. A
negative case pins the prefix: an unrelated crash that merely mentions the
phrase keeps the daemon's label.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-07 11:46:44 +08:00
96d567d517 fix(agent): pair failed tool results for OpenCode (MUL-5802) (#6497)
Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-06 15:22:32 +08:00
0066ab259e refactor(agent): drop unreachable inline system-prompt branches (MUL-5392) (#6050)
* refactor(agent): drop unreachable inline system-prompt branches (MUL-5392)

The daemon only populates ExecOptions.SystemPrompt for openclaw, kimi and
traecli (providerNeedsInlineSystemPrompt); every other backend receives the
runtime brief as a per-task context file in the workdir. The inline branches in
claude, codex, opencode and pi were therefore dead, and read as if they were the
live delivery path.

Probed each backend over its real launch path with a canary in the context file
and no inline delivery — claude 2.1.220 (CLAUDE.md), codex 0.144.6 via the
app-server, opencode 1.17.7, pi 0.67.2, hermes 0.18.2 via ACP — and all of them
picked the brief up from disk, so the branches were removable with no behaviour
change. An empty workdir returned no canary, confirming the probe could fail.

opencode's branch was worse than dead: `opencode run` has no --prompt flag, so
enabling inline delivery there would have made every opencode task exit 1 with a
usage dump. The DevEco backend, forked from opencode, already documents this
constraint; opencode itself never got the fix.

Regression tests pin all three arg builders against re-adding the flag, and
providerNeedsInlineSystemPrompt now documents what was verified and what is
still unprobed (grok, qoder, codebuddy).

Hermes and kiro are untouched: their exclusion is deliberate and already tested.

Co-authored-by: multica-agent <[email protected]>

* test(agent): pin codex developerInstructions contract, drop stale pi flag doc

Review follow-up on MUL-5392.

buildPiArgs' doc comment still advertised --append-system-prompt after the
branch that emitted it was removed — exactly the stale-signal this PR set out
to delete.

The two codex sites fixed to a literal nil had no regression test, so restoring
nilIfEmpty(opts.SystemPrompt) would still have gone green. Both thread/start and
thread/resume now run with a canary SystemPrompt and assert developerInstructions
comes through as an explicit null. Mutation-checked: reverting either site fails
its test.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-07-28 22:02:55 +08:00
6455d390e2 fix(agent): fail opencode runs whose stream ends without a terminal signal (#5238)
* fix(agent): fail opencode runs whose stream ends without a terminal signal

* fix(agent): treat step_finish(reason "tool-calls") as non-terminal in the EOF guard

Review follow-up: step bracketing alone left a false-green window. A tool
step closes with step_finish(reason "tool-calls") before its continuation
step_start, so a stream that dies in that gap had no open step and still
reported "completed".

Parse part.reason (live-probed on opencode 1.17.16: tool loop emits
reasons "tool-calls" then "stop") and keep the run non-terminal after a
"tool-calls" finish until the next step_start. Any other reason —
including absent, for older opencode versions that predate the field —
stays terminal so healthy runs on old protocols are not mass-failed.

Tests: tool-calls finish then EOF now fails; the multi-step happy path
uses the real wire shape ending in reason "stop"; a reason-less
step_finish (legacy protocol) still completes.

* fix(agent): keep stop tool steps pending continuation

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-07-15 16:40:59 +08:00
Multica Eve e2720f7d33 feat: add opencode thinking variants
Adds OpenCode model variant discovery for thinking controls, passes saved thinking_level through opencode run --variant, and hardens verbose model parsing with fallback coverage.
2026-06-02 13:15:14 +08:00
YOMXXX 34f16e2c7a fix(opencode): deny interactive questions in daemon mode (#2878)
* fix(opencode): deny interactive questions in daemon mode

* fix(opencode): avoid permission env ordering bypass
2026-05-20 17:17:31 +08:00
Bohan Jiangandmultica-agent 9a577f3e11 fix(runtimes): anchor OpenCode skill + AGENTS.md discovery to task workdir (MUL-2416) (#2849)
* fix(runtimes): anchor OpenCode skill + AGENTS.md discovery to task workdir

OpenCode resolves its project discovery root from `--dir` and `PWD`
before falling back to `process.cwd()`. The daemon set `cmd.Dir =
workDir` but never overrode the inherited `PWD`, so OpenCode walked
from the daemon's shell directory and silently bypassed the per-task
workdir — agents lost visibility into `.opencode/skills/` and
`AGENTS.md`, falling back to whatever global skills the host had
installed (MUL-2416).

- Pass `opencode run --dir <workDir>` and override `PWD=<workDir>` in
  the child env so AGENTS.md walk-up + `.opencode/skills` project
  config scan both anchor on the task workdir.
- Block `--dir` from custom args so user overrides cannot re-introduce
  the regression.
- Plumb skill `description` from DB through service / daemon /
  execenv. `writeSkillFiles` synthesizes a YAML frontmatter block
  (`name`, optional `description`) when the stored content lacks one,
  since runtimes like OpenCode silently drop SKILL.md files without a
  parseable `name`. Existing frontmatter is preserved unchanged so
  upstream-imported skills (GitHub / ClawHub / Skills.sh) keep their
  hand-shaped metadata.

Tests:
- New fake-CLI test confirms argv carries `--dir <workDir>` and the
  child sees `PWD=<workDir>`.
- New test confirms a user-supplied `--dir` in custom_args is dropped.
- New execenv tests cover synthesized frontmatter and preservation of
  pre-existing frontmatter.

Co-authored-by: multica-agent <[email protected]>

* fix(runtimes): inject SKILL.md `name` when upstream frontmatter omits it

Skills imported with frontmatter that sets `description` but leaves `name`
implicit (relying on the directory slug, as common in GitHub/Skills.sh
imports) still hit OpenCode's "no parseable name → drop" path because the
DB Name fallback never made it into the SKILL.md body. ensureSkillFrontmatter
now scans the existing block and, when name is missing or empty, prepends
`name: <slug>` while preserving description, body, and any runtime-specific
keys verbatim.

Also tighten yamlEscapeInline to always double-quote so descriptions that
look like YAML keywords (`null`, `true`, `[foo]`, `{x: y}`, `2024-01-01`)
parse as strings rather than getting reinterpreted and rejected.

Adds regression test for the nameless-frontmatter case and updates the
existing OpenCode skill test for the always-quoted description format.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: multica-agent <[email protected]>
2026-05-19 16:21:02 +08:00
Truffle 8b340fcf21 fix(agent/opencode): bypass npm .cmd shim on Windows to preserve multi-line prompts (#1718)
* fix(agent/opencode): bypass npm .cmd shim on Windows to preserve multi-line prompts

The npm-generated `opencode.cmd` shim forwards argv via Windows batch `%*`,
which silently truncates positional arguments at the first newline. The
daemon spawns OpenCode with a multi-line prompt (system prompt + user
message), so on Windows the agent only ever sees the first line and
responds generically as if it never received the user's message
(reported in #1717 with native-binary repro confirming the same prompt
arrives intact when cmd.exe is skipped).

When `runtime.GOOS == "windows"` and `exec.LookPath` returns a `.cmd`
shim, walk to the native binary that npm bundles next to the shim:

  <prefix>\opencode.cmd
  <prefix>\node_modules\opencode-ai\node_modules\opencode-windows-x64\bin\opencode.exe

If the native binary is missing (unusual install layout), keep the
original shim path so PATH lookup still wins. The resolver is a pure
function with an injectable `statFn`, so layout assertions are testable
on Linux:

- shim resolves to the bundled native binary
- missing native returns "" (caller keeps original path)
- non-cmd paths (Linux/Mac binary, opencode.exe direct, empty) skip resolution
- uppercase `.CMD` is accepted (PATHEXT entries can be either case)

Closes the user-facing failure mode without restructuring exec resolution
across the rest of the agent backends — the other shim-aware fixes can
follow the same shape if/when they land in similar repros.

* fix(agent/opencode): cover x64-baseline and arm64 npm package variants

`npm install -g opencode-ai` ships three Windows platform packages
(opencode-windows-x64, opencode-windows-x64-baseline for older CPUs
without AVX2, opencode-windows-arm64 for Surface / Copilot+ PC) and
installs whichever matches the host. The previous resolver only knew
about opencode-windows-x64, so baseline-x64 and arm64 hosts would fall
back to the .cmd shim and hit the multi-line prompt truncation again.

Iterate the three package candidates in GOARCH-preferred order. ARM64
hosts try arm64 first; everything else tries x64, then baseline, then
arm64 as a last resort. Cost is one extra statFn call per miss when
the GOARCH-preferred package isn't installed.

Surfaced by review on #1718.

* test(agent): add Windows counterpart to writeTestExecutable

writeTestExecutable in exec_fixture_unix_test.go is referenced by
claude_test.go / codex_test.go / kimi_test.go, but the //go:build unix
constraint meant `go test ./pkg/agent` failed to build on Windows.

ETXTBSY is a Linux/Unix fork-exec race; Windows doesn't have that
pathology, so a plain os.WriteFile is sufficient.

Lifted from #1719 (Codex) with attribution. Surfaced by review on #1718.
2026-04-27 12:16:56 +08:00
Quake Wang 36db325d50 feat(daemon): add opencode as supported agent provider (#341)
* feat(daemon): add opencode as supported agent provider

Add opencode backend alongside claude and codex. The backend spawns
`opencode run --format json`, parses streaming JSON events (text,
tool_use, error, step_start/finish), and supports --prompt for system
prompts. Includes CLI detection, AGENTS.md runtime config, native skill
discovery via .config/opencode/skills/, and 21 tests covering handlers,
JSON parsing, and integration-level processEvents scenarios.

* chore: add .tool-versions to gitignore
2026-04-02 17:52:07 +08:00