Skip to content

fix(config): gate Instance dispose on actual config change#722

Merged
Astro-Han merged 2 commits into
devfrom
claude/config-changed-gate
May 18, 2026
Merged

fix(config): gate Instance dispose on actual config change#722
Astro-Han merged 2 commits into
devfrom
claude/config-changed-gate

Conversation

@Astro-Han

@Astro-Han Astro-Han commented May 18, 2026

Copy link
Copy Markdown
Owner

Summary

Port upstream PR anomalyco/opencode#25114's changed byte-comparison gate to both Config.update and Config.updateGlobal so that a no-op config write does not tear down running instances.

Why

Without the gate, both Config.update (project config) and Config.updateGlobal (global config) call Instance.dispose() / Instance.disposeAll() on every write. A write of identical bytes — e.g., a UI mount that re-emits the existing setting — still disposes every running instance, which closes each SessionRunState scope mid-turn. The user sees the active assistant turn end with MessageAbortedError and an Tool execution aborted tool result, then has to resend.

Upstream already fixed this same class of bug in opencode commit a9d399699 ("Prevent Model response Interruption when opening settings dialog"), but the fix is not in PawWork's dev history (git branch -a --contains a9d399699 returns only remotes/upstream/dev). PawWork's Config.update and Config.updateGlobal are still in the pre-fix shape.

This PR ports just the changed gate, not the broader #25475 instance-lifecycle decoupling refactor. The minimal change is enough to remove the no-op-write trigger.

Related Issue

Candidate mitigation for #721. Deliberately not phrased with a closing keyword because we have not yet confirmed via post-#710 abort diagnostics that the silent writer is what's tearing down scopes in the reported cases. This PR removes one known trigger of that class. The #721 diagnostic comments lay out what evidence would confirm or refute that this was the root cause.

Human Review Status

Pending. A human should make the final merge decision after reviewing the final diff and verification evidence.

Review Focus

The !fileExisted clause in updateGlobal. PawWork's updateGlobal has a seed-migration path: on a fresh install with no global config file, it seeds from upstream config sources. If we gated purely on byte equality, the seeded content would never materialize because serialized === before on first run. The !fileExisted guard makes sure we always write on first run even when the merged result equals the seed text. Worth confirming this is correct for the existing seed test cases.

Risk Notes

Low behavior risk. The change only suppresses dispose when the write would have produced identical bytes. Any write that actually changes config still disposes as before. No public API change. No platform-specific concern.

How To Verify

Typecheck: bun --cwd packages/opencode run typecheck — pass
Targeted tests: bun --cwd packages/opencode test test/config/pawwork-global-config.test.ts test/config/config.test.ts — 153 pass, 0 fail
Diff check: git diff --check — clean

Screenshots or Recordings

Not applicable. No visible UI change.

Checklist

  • Human review status is stated above as pending, approved, or not required
  • I linked the related issue, or stated why there is no issue
  • This PR has exactly one type label (bug, enhancement, task, or documentation), at least one primary routing label (app, ui, platform, harness, or ci), and exactly one priority label (P0 to P3), or I requested maintainer labeling
  • I described the review focus and any meaningful risks
  • I listed the relevant verification steps and the key result for each
  • I did not introduce unrelated refactors, dependencies, generated files, or file changes beyond the stated scope
  • I manually checked visible UI or copy changes when needed, with screenshots or recordings
  • I considered macOS and Windows impact for platform, packaging, updater, signing, paths, shell, or permissions changes
  • I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, generated content, or local file changes when relevant
  • I reviewed the final diff for unrelated changes and suspicious dependency changes
  • I am targeting dev, and my PR title and commit messages use Conventional Commits in English

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@Astro-Han has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 59 minutes and 16 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b66abb3f-2952-410d-b96b-97c9e4b73213

📥 Commits

Reviewing files that changed from the base of the PR and between d5e6186 and ca2e368.

📒 Files selected for processing (2)
  • packages/opencode/src/config/config.ts
  • packages/opencode/test/config/pawwork-global-config.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/config-changed-gate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Astro-Han Astro-Han added bug Something isn't working harness Model harness, prompts, tool descriptions, and session mechanics P2 Medium priority labels May 18, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested priority: P2 (includes non-doc, non-test paths outside the low-risk bucket).

P1/P0 are reserved for maintainer confirmation. Please relabel manually if this is a release blocker, security issue, data-loss risk, or updater/runtime failure.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request optimizes configuration updates in packages/opencode/src/config/config.ts by ensuring that disk writes and instance teardowns only occur when the configuration content has actually changed. By comparing the new configuration against the existing file content, the code avoids unnecessary interruptions to active processes caused by no-op writes. I have no feedback to provide as there were no review comments.

Astro-Han added 2 commits May 18, 2026 13:17
Port upstream PR #25114's `changed` byte-comparison gate to both
`Config.update` and `Config.updateGlobal`. Without it, a no-op global
or project config write tears down all running instances via
`Instance.disposeAll()`, which closes each SessionRunState scope mid-
turn and surfaces to the user as `MessageAbortedError` with a tool
showing `Tool execution aborted`.

PawWork's existing `update` path always called `Instance.dispose()`
unconditionally, and `updateGlobal` always called `invalidate()`
(which calls `Instance.disposeAll()`) unconditionally. Upstream
already fixed the same class of bug in opencode commit `a9d399699`
("Prevent Model response Interruption when opening settings dialog"),
but the fix was not in PawWork's `dev` history.

This is a candidate fix for #721. It does not change behavior when
the write genuinely changes bytes on disk. The first global write on
a fresh install still materializes the seeded file because the gate
also keys on `!fileExisted`.

Verification: bun --cwd packages/opencode run typecheck passes,
bun --cwd packages/opencode test test/config/pawwork-global-config.test.ts
test/config/config.test.ts passes 153/153.
Sentinel-mtime regression for the changed gate added in the previous
commit. If the gate ever regresses, the second saveGlobal would rewrite
the file and bump mtime past the sentinel, failing this test.

Reviewer-requested guard so we explicitly assert the behavior rather
than rely on the existing 153 config tests covering only functional
correctness.
@Astro-Han

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Astro-Han Astro-Han force-pushed the claude/config-changed-gate branch from 1a476b6 to ca2e368 Compare May 18, 2026 05:18
@Astro-Han

Copy link
Copy Markdown
Owner Author

On the P2 follow-up around cache potentially lagging disk after a no-op Config.update / Config.updateGlobal: I'd push back and not open a follow-up, for three reasons.

1. The premise does not hold in PawWork. PawWork is a single-instance Electron app (packages/desktop-electron/src/main/index.ts:334 calls app.requestSingleInstanceLock()), so a second process writing pawwork.json concurrently does not happen in the desktop product. The only remaining "external write" path is a user manually editing pawwork.json, and the race only triggers if the UI then writes an input whose serialized bytes happen to be identical to what the user just wrote. Three coincident conditions, with a recovery path of restarting PawWork.

2. Upstream evaluated the same trade-off and did not split. The fix we are porting (opencode commit a9d399699, "Prevent Model response Interruption when opening settings dialog") ends Config.updateGlobal with if (changed) yield* invalidate() — no separate "refresh cachedGlobal but skip Instance dispose" branch. The larger upstream PR #25475 (fix(instance): run bootstrap from instance store, 41 files, Fixes #25450) addresses a different bug (plugin-registered agents lost on reload) and leaves cachedGlobal's Duration.infinity TTL behavior unchanged, so this same race persists after #25475.

3. Splitting cache refresh from instance dispose is not a small change. cachedGlobal is built via Effect.cachedInvalidateWithTTL(loadGlobal(), Duration.infinity) and invalidate() currently shoulders both "drop the in-memory copy" and "tear down running instances." Decoupling them — refreshing the cache without aborting in-flight turns — would require reshaping the cache write path, approaching the scope of #25475-style refactors, beyond this PR's deliberate "remove one known trigger" boundary.

Plan: keep #722 at its current minimal scope and merge. No follow-up issue. If PawWork later gains a multi-process write path, or if production logs show this race actually firing, we revisit then.

@Astro-Han Astro-Han merged commit 0e64c23 into dev May 18, 2026
27 checks passed
@Astro-Han Astro-Han deleted the claude/config-changed-gate branch May 18, 2026 05:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working harness Model harness, prompts, tool descriptions, and session mechanics P2 Medium priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant