Repository navigation
Conversation
The repository stores every text file with LF endings, but nothing said so. A commit made on a machine whose core.autocrlf is false therefore stored whatever the editor happened to write, and a file saved with CRLF endings landed in the commit as CRLF. The next edit to that file then rewrote every line of it, which turned a small fix into a whole-file diff and hid the real change. A .gitattributes file pins the convention, so the line endings of a file no longer depend on the settings of the machine that makes the commit.
The plugin declared @opencode-ai/plugin as a runtime dependency and the host-provided @opentui/solid and solid-js peers as required peers. npm auto-installs required peers, so every host that installed this plugin also installed @opentui/core, typescript, web-tree-sitter, @babel and a second Effect runtime. The OpenCode log reported the resulting install as a stalled plugin preparation step: message="plugin operation stalled" component=plugin stage=prepare elapsedMs=5002 target=@prevalentware/opencode-goal-plugin install=true @opencode-ai/plugin is only imported with `import type`, so it never reaches the built bundle: dist/server.js imports zod, effect-goal-state and node builtins only. The TUI peers are supplied by the OpenCode host at runtime, which is why @opencode-ai/plugin itself declares the equivalent peers as optional. Measured on this repository by packing the published files and installing each variant into an isolated project: before: 10.20s, 157.81 MB, 11928 files after: 5.94s, 33.48 MB, 4589 files Adds test/dependencies.test.ts to keep the runtime imports and the declared dependencies in sync, and to assert that the host-provided peers stay optional so the install footprint cannot silently grow back.
The host resolves the plugin entrypoint through the exports map of the installed package, and it falls back to the main entry when the subpath does not resolve. The subpaths only answered to the "import" condition and the package had no main entry at all, so a host that resolved them under another condition found no entrypoint: the host reports that as "Plugin entrypoint not found", leaves the location without a fully activated plugin set, and fails the commands that need one with "Location plugins did not finish activating". Both subpaths now carry a default condition and the package has a main entry that points at the bundle, so the entrypoint resolves whichever condition the host resolves it under.
The goal plugin replaces its state file through a same-directory temp file and a rename. On Windows that rename fails with EPERM/EACCES/EBUSY while an antivirus scanner, a search indexer, or a concurrent reader still holds the target open, and those locks routinely outlive a couple of hundred milliseconds. The retry budget was three attempts with a 60ms total backoff, so a transient lock surfaced as a failed goal state write and the goal tools threw EPERM to the caller. The budget is now ten attempts with a capped exponential backoff (~2.6s worst case, still one rename when uncontended). Also fixes two Windows-only test failures that the upstream suite carried: the state-recovery log assertion compared a raw path against a JSON stringified payload (JSON escaping doubles the path separators), and the TUI test helpers replaced the state file with a bare rename that has no retry.
A transport failure was recorded with only requirePending, so a failure that belonged to a previous continuation could clear the attempt that happened to be pending when the write landed, and could count a phantom failure toward the ceiling that pauses the goal. recordToolProgress and recordAssistantProgress already correlate by attempt id or reservedAt; the continuation-result path did not. The failure is now recorded with the goal id and the attempt id, so a late failure can only affect the attempt it belongs to. When the attempt has already moved on, the failure is ignored instead of clearing the newer attempt. The two V2 continuation tests also get a deadline that covers the plugin's documented minimum-interval window: a configured interval of N seconds anchors on a whole second, so a continuation can legitimately be held for up to N + 1 seconds, which the previous 2500ms budget did not cover. Their per-test timeout is raised above Bun's 5 second default, which is shorter than the timeline those tests legitimately wait for. This is the same class of change as the repository's own fix/ci-timing-flakes branch. Adds state-level regression tests that pin the attempt-correlation contract for both the late success and the late failure case.
The minimum interval between two automatic continuations was compared against whole-second timestamps, so the gate was sub-second sensitive: as soon as the wall clock crossed into the next second a follow-up continuation was allowed, even when a fraction of the configured minimum had elapsed. A one second minimum interval could therefore be honoured with 200ms. The interval is now measured from a durable millisecond anchor (lastContinuationAtMs) that is persisted with the goal and restored when a reserved attempt is rolled back, so the configured interval is the real interval. The interval anchor alone was not enough. A fast execution that settles before the interval expires takes the "not yet" branch, and that branch only retained a timed wake-up when no attempt was outstanding. With an attempt still outstanding the wake-up was dropped, so the goal had neither a pending continuation nor a queued wake-up and was stranded until an unrelated event arrived. The wake-up is now retained in that case too, which is what the branch comment already claimed it did. The result is that the V2 continuation tests stop failing intermittently: "bun test test/server-v2.test.ts -t 'V2 fast execution success'" failed 6-8 times in 20 runs before and now fails 0 times in 20, and the sibling "timed continuation waits for a live run boundary" test goes from the same intermittent failure to 0 in 20. Adds state-level regression tests for the interval gate, for the follow-up continuation that must wait for the full interval, and for the anchor that a rolled-back attempt has to restore.
OpenCode's V2 plugin API has no logger: the context exposes the app metadata, the location and the domain APIs, but nothing that writes to the server log. Plugin console output does not reach the OpenCode log either, so every failure inside the plugin - a failed auto-continue, a turn watchdog retry that could not be delivered, a goal state that could not be quarantined - was invisible to anything that only watches the host log. The plugin now keeps its own diagnostics log next to the goal state file (goal-plugin.log) and writes its error reports there in addition to the console output. The log is rotated at 1 MB so a failure loop cannot fill the disk, and the writer never throws: a failing diagnostics writer must not turn a recoverable plugin error into a crash. The state recovery diagnostics are recorded the same way, because those are the failures that leave the goal state in an unexpected shape.
The state layer reports failures through Effect, so the error that reaches a caller is often a FiberFailure whose own message is just "An error has occurred". The real reason - a failed rename, a full disk, a state file that changed underneath - only lives in the cause chain, so the diagnostics log recorded a failure without saying what failed. The diagnostics now unwrap the cause chain, including the rendering Effect keeps on an internal Cause, so a line names the actual error. The first entry the new log captured was exactly this case: "Turn watchdog retry failed | (FiberFailure) StateWriteError: An error has occurred", which said nothing about the write that had failed.
The cause unwrapping followed the public `cause` property, but Effect keeps the cause of a failed fiber behind a symbol and the public getter does not return it. A recorded failure therefore stopped at the wrapper and still said "An error has occurred" instead of naming the error that failed. The diagnostics now read Effect's cause symbol and unwrap the cause itself: a failure carries the error, a defect carries the thrown value, and a composite cause carries a set of both. The regression test builds a real FiberFailure with Effect, so the shape the state layer produces is the shape that is covered.
ggrabarz
force-pushed
the
pr/04-failure-diagnostics
branch
from
October 21, 2026 09:47
2626a6a to
5b9e026
Compare
danyel117
reviewed
Oct 22, 2026
danyel117
left a comment
Contributor
There was a problem hiding this comment.
The new diagnostics file persists raw error cause chains. appendFileSync creates it with the process umask (commonly readable by other local users), and mkdirSync likewise uses default directory permissions. State files use owner-only permissions. Please create the diagnostics directory/file with owner-only permissions and add a permissions regression test. Also, rotateIfNeeded unlinks the prior archive before renaming; if rename fails, the active file continues growing without a cap despite the comment. Keep a bounded fallback on rotation failure. Please include the model/harness or manual-work disclosure required by CONTRIBUTING.md in this PR body.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes in this pull request
fix/plugin-diagnostics-log- A plugin failure was handed to the host, but nothing was written where a monitor could see it. Failures are now recorded in the plugin's own diagnostics log.fix/diagnostics-error-cause- Only the message of a failure was reported, not the cause behind it.fix/diagnostics-effect-cause- An Effect fiber failure hid the real error inside the fiber; the cause is now read out of it.Why
Without a log of its own, a failure of the plugin was only visible as a line in the host log, if at all, and without the cause it did not say what went wrong. These three fixes are what makes the rest of the fixes in this series findable: every later fix was diagnosed from this log.
Verification
This is the fourth pull request of a stack. Merge the earlier ones first; once they are merged this diff is only the fixes above.
Need help on this PR? Tag
@codesmith-botwith what you need.