Skip to content

fix: withhold auto-apply when response streams do not finish safely - #4341

Closed
canblmz1 wants to merge 10 commits into
dyad-sh:mainfrom
canblmz1:fix/stream-safety-gate-before-apply
Closed

canblmz1 wants to merge 10 commits into
dyad-sh:mainfrom
canblmz1:fix/stream-safety-gate-before-apply

Conversation

@canblmz1

@canblmz1 canblmz1 commented Aug 21, 2026 •

Copy link
Copy Markdown

Problem

When autoApproveChanges is enabled, Dyad applies actions parsed from the
accumulated assistant response through processFullResponseActions().

Before this change, the final auto-apply gate checked whether the request had
been explicitly aborted, but it did not verify how the underlying model stream
terminated.

That means a response can contain a syntactically complete <dyad-write> (or
another actionable Dyad tag) and still belong to a generation that later ends
because of a token limit, provider error, content filter, abort, or an
unrecognized terminal state.

A complete tag is not sufficient evidence that the surrounding generation
finished normally.

Fix

Track the termination state of each AI SDK fullStream consumed while building
the response and require positive safe completion before automatic application.

The stream classifier is backed by [email protected]'s AI SDK
stream adapter, which normalizes terminal states fail-closed:

  • stop / normal completion -> confirmed safe
  • length -> unsafe
  • provider error -> unsafe
  • content filter -> unsafe
  • abort -> unsafe
  • unknown/unrecognized terminal state -> unsafe

The integration intentionally uses the package here for its AI SDK
terminal-state normalization and fail-closed behavior. Dyad's <dyad-*> actions
are plain-text response tags rather than JSON tool calls, so this change does
not claim to use the package's streamed JSON-completeness guarantees.

Accumulated responses

fullResponse can contain output from more than one provider stream, including
search/replace repair and continuation attempts.

Safety is therefore accumulated with sticky-unsafe semantics:

  • all contributing streams must finish safely for auto-apply to remain enabled;
  • once any contributing stream terminates unsafely or without confirmation,
    later successful continuation does not restore automatic execution.

The generated response is still persisted and visible for manual review; only
the automatic filesystem/SQL/dependency side effect is withheld.

Scope

This patch only changes the existing response-level auto-apply path.

processFullResponseActions() itself is unchanged.

Dyad's Local Agent tool-execution path is intentionally out of scope; it has a
different AI SDK multi-step execution lifecycle and would require a separate
design and test surface.

Tests

Added focused coverage for terminal-state classification and the real
auto-apply execution path.

Safety cases include:

  • normal completion -> allowed
  • token-limit cutoff -> withheld
  • provider error -> withheld
  • abort -> withheld
  • unknown terminal state -> withheld
  • malformed/unrecognized termination -> withheld

Accumulated multi-stream cases include:

  • unsafe -> safe -> withheld
  • safe -> unsafe -> withheld
  • safe -> safe -> normal auto-apply preserved
  • safe -> unsafe -> safe -> still withheld

Validation performed:

  • stream_execution_guard.test.ts: 24/24 passing
  • real chat_stream_handlers auto-apply integration cases: 8/8 passing
  • chat_stream_handlers.actor_terminal.test.ts: 5/5 passing
  • relevant suite
    (chat_stream_handlers.test.ts,
    response_processor.test.ts,
    stream_execution_guard.test.ts):
    122/122 passing
  • oxlint on changed source/test files: 0 warnings, 0 errors (this update's
    own diff is dependency-only — package.json/package-lock.json — so
    there is no new source for oxlint to check; the original claim above
    still stands unchanged)

The repository-wide TypeScript run currently reports 90 errors under
testing/fake-llm-server/* because that sandbox lacks the corresponding
Express/CORS type dependencies. The exact same error set was reproduced on
pristine upstream before attributing it to this change.

A broader suite comparison was also performed against pristine upstream.
Existing environment failures reproduced on both branches; two additional
full-suite timeout cases on the patched run passed when rerun independently and
were unrelated to the changed paths.

Dependency update

prefix-safe-json moved from the 0.0.1-alpha.4 prerelease to the published
exact release:

0.2.0

No source changes were required — createAiSdkExecutionGuard() and the
AiSdkExecutionGuard.push()/.finish() surface this integration uses are
unchanged between the two. Re-verified against the real 0.2.0 install
(resolved version confirmed exactly 0.2.0, from the public npm registry,
lockfile regenerated with npm install): all of the above test counts are
identical to the 0.0.1-alpha.4 result, including the sticky-unsafe
accumulation semantics and the canned [dyad-qa=...] auto-apply preservation
from the prior review round. No vendored or local copy is used.

Review in cubic

@github-actions

github-actions Bot commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


1 out of 2 committers have signed the CLA.
✅ (canblmz1)[https://github.com/canblmz1]
❌ @can
Can seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@socket-security

socket-security Bot commented Aug 21, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedprefix-safe-json@​0.2.0791001009380

View full report

@canblmz1
canblmz1 force-pushed the fix/stream-safety-gate-before-apply branch from 9f116b5 to c2f6683 Compare August 21, 2026 11:50
processFullResponseActions applies <dyad-write>/<dyad-delete>/etc. tags
straight to disk (fs.writeFileSync, git commit) once a turn finishes. The
only existing check before that call was an explicit user-abort check -
nothing looked at the stream's own finishReason. A well-formed tag can be
followed by a "length" cutoff, a content-filter stop, or a provider error,
and none of those currently stop the tags collected so far from being
applied with autoApproveChanges on.

Adds src/ipc/utils/stream_execution_guard.ts, a thin wrapper around
prefix-safe-json's createAiSdkExecutionGuard() that tracks one synthetic
call representing "this response" and resolves it against the real
finish/error/abort parts observed on fullStream. processStreamChunks now
observes every raw part (previously only chunk-producing part types were
inspected) and returns a streamSafety verdict alongside fullResponse; the
final shouldAutoApply gate only calls processFullResponseActions once that
verdict is confirmedSafe. The saved response text itself is unaffected -
only the auto-apply side effect is withheld, so nothing is silently
discarded from the visible/persisted transcript.

Scope is intentionally narrow: only the shouldAutoApply call site in
chat_stream_handlers.ts is changed; processFullResponseActions itself is
untouched.
fullResponse is cumulative across the initial generation and any
search-replace-fix / continuation retry streams within one turn, but the
previous change tracked only the most recently resolved stream's safety
verdict (lastStreamSafety = result.streamSafety on every retry). A stream
that was cut off by a length limit, followed by a clean continuation on
the same turn, would have its unsafe content laundered into an
executable response: the earlier stream's unsafe verdict was simply
overwritten by the later stream's safe one, even though both streams'
output ended up in the same fullResponse that gets applied.

combineStreamSafety() in stream_execution_guard.ts folds each stream's
verdict into an accumulator with sticky-unsafe semantics: once any
contributing stream is unsafe, the accumulated verdict for the turn
stays unsafe regardless of how safely a later stream in the same turn
completes. Only "every contributing stream safe" yields a safe
accumulated verdict. chat_stream_handlers.ts's three processStreamChunks
call sites now fold into accumulatedStreamSafety via combineStreamSafety
instead of overwriting a single lastStreamSafety.

Covered by new tests: combineStreamSafety unit tests for the fold
semantics, plus four production-path tests driving two real
processStreamChunks calls through the real accumulator and the real
processFullResponseActions/fs.writeFileSync (unsafe-then-safe,
safe-then-unsafe, safe-then-safe, and a three-stream sticky-unsafe case).
Was ^0.0.1-alpha.4, which npm's semver range treats as "no upper bound
below the next alpha.N" and would resolve later 0.0.1-alpha.x prereleases
without a corresponding review of this integration against them. Pin the
exact version actually integrated against and validated.

Resolved package is unchanged (0.0.1-alpha.4).
@canblmz1
canblmz1 force-pushed the fix/stream-safety-gate-before-apply branch from c2f6683 to 6b92633 Compare August 21, 2026 11:57
@canblmz1

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Aug 21, 2026
@canblmz1

Copy link
Copy Markdown
Author

recheck

@canblmz1
canblmz1 marked this pull request as ready for review August 21, 2026 15:00
@canblmz1
canblmz1 requested a review from a team August 21, 2026 15:00

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6b92633529

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 6 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

@canblmz1
canblmz1 force-pushed the fix/stream-safety-gate-before-apply branch 2 times, most recently from 12f1ee4 to 9a235af Compare August 21, 2026 17:42
canblmz1 pushed a commit to canblmz1/prefix-safe-json that referenced this pull request Aug 23, 2026
The 0.1.0 entry said the package "had already been integrated against
three independent real-world codebases (Dyad, CodePilot, Apache Maka)
across separate PRs" - past tense, implying settled, completed
integrations. Checked all three directly against their live PRs rather
than repeating the claim:

- dyad-sh/dyad#4341 and op7418/CodePilot#676 are both real and
  substantive - genuine PRs pinning [email protected] with
  real integration code and specific, named test suites - but both are
  still open, not merged.
- apache/maka#3434 solves the identical problem (gating tool execution
  on raw stream completion, not just JSON validity) but its own PR
  description states it "adds no new runtime package, no
  prefix-safe-json dependency" - a Maka-owned native implementation.
  It should never have been grouped with the other two as a dependency
  adopter.

None of the three amount to "integrated" in past tense, and open PRs
are not package adoption regardless of how substantive the patch
behind them is.

Added a dated correction note directly under the original paragraph
rather than editing it - the original text stays exactly as published,
readable for what it said and when, with the correction attached
immediately after it and sourced to the specific PRs it's about.
…release

prefix-safe-json 0.0.1-alpha.4 -> 0.2.0 (exact). The createAiSdkExecutionGuard()
API this integration uses is unchanged between the two, so no source changes
were needed.

Verified: resolved version is exactly 0.2.0, and stream_execution_guard.test.ts
(24/24), chat_stream_handlers.test.ts (87/87), chat_stream_handlers.actor_terminal.test.ts
(5/5), and response_processor.test.ts (11/11) all still pass unchanged, including
the sticky-unsafe accumulation semantics and the canned-QA auto-apply preservation.
@canblmz1

Copy link
Copy Markdown
Author

Replaced the old alpha dependency with the published prefix-safe-json 0.2.0 release and reran the stream-safety-guard and auto-apply suites against it. No architecture or behavior change — same sticky-unsafe semantics, same canned-QA auto-apply handling — this only removes the prerelease dependency.

@wwwillchen

Copy link
Copy Markdown
Collaborator

@canblmz1 - let's open an issue and discuss whether this fix is needed beforehand. i'm also concerned about adding a new dependency here (that I see you've published) as it introduces supply chain risk to our project.

Copy link
Copy Markdown
Author

That makes sense. I moved too quickly from a possible boundary gap to proposing an implementation.

I agree the correctness question and the dependency decision should be treated separately. I’ve opened #4383 to verify the behavior first against Dyad’s current main and real auto-apply path, without proposing any dependency.

If the behavior is not reproducible, there is nothing to fix. If it is reproducible, I’d prefer to discuss the smallest local fix and regression test first; adding another runtime dependency should be a separate decision with its own supply-chain justification.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants