Conversation
A phase-2 run whose consolidation agent changed nothing was recorded as `succeeded`: the git baseline was reset and the watermark advanced, even though the new rollouts were never promoted into MEMORY.md or memory_summary.md. Because the baseline reset is what makes the workspace diff disappear, the next pass saw zero changes, returned `no_workspace_changes`, and never invoked the consolidator again. The un-consolidated memories were then stranded in the workspace permanently and silently, while every status field reported success. The consolidation contract explicitly allows this run: "No-op content updates are allowed and preferred when there is no meaningful, reusable learning worth saving." So the host cannot infer success from a clean `consolidateViaSubagent` return, and `validateConsolidationArtifactsForVersion` cannot catch it either - a stale MEMORY.md and a `v1`-headed summary are indistinguishable from fresh ones. Fingerprint the artifacts the consolidator owns before and after the helper runs. If nothing changed while the diff carried rollout summaries or extension resources, fail the job with the new `no_artifact_changes` status and skip `resetBaseline`, so the diff survives for the next attempt and the condition is visible in `memory_inspect` instead of silent. `raw_memories.md` is excluded from the "carries learning" test on purpose: `rebuildRawMemories([])` always writes a placeholder, so on a first INIT run it would look like new material and turn a legitimately empty consolidation into a permanent retry loop. A new or updated stage-1 output always changes the rollout_summaries set too, since the file stem embeds source_updated_at.
Owner
|
Thanks for investigating. Two unchanged runs with only three searches for the diff are suspicious and deserve investigation. However, our docs explicitly allow no-ops, matching Codex ("A no-op is allowed: if nothing new is worth writing, the correct result is unchanged files."). I won’t merge this guard because it would also retry legitimate no-ops indefinitely. Could you share the three tool calls’ arguments/results and the helper’s final message, finish reason, and idle outcome, with sensitive content redacted? Also, which I added regression coverage in e1343fa, crediting this PR, but haven’t reproduced your incident yet. |
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.
Problem
A phase-2 run whose consolidation agent changed nothing was recorded as
succeeded.consolidateViaSubagentresolves as soon as the helper session closes and only throws on prompt/timeout/shutdown failure. The consolidation contract explicitly permits a no-op:So a helper can read
phase2_workspace_diff.md, change nothing, and exit cleanly.validateConsolidationArtifactsForVersioncannot catch this: a staleMEMORY.mdand av1-headedmemory_summary.mdare indistinguishable from fresh ones. The run therefore proceeded toresetBaselineandmarkPhase2Succeeded.resetBaselineis the damaging part. It folds the un-consolidated rollout summaries into the git baseline, so the next pass sees zero changes, takes theno_workspace_changesearly return, and never invokes the consolidator again. The new memories are stranded in the workspace permanently and silently, while every status field reports success.The contract also states the opposite requirement — "Every changes in
phase2_workspace_diff.mdare authoritative and must propagated and consolidated" and "You should always make sureMEMORY.mdandmemory_summary.mdexist and are up to date" — so a helper that no-ops over a diff full of new rollouts is not making a legitimate call.Observed on OpenCode 2.0.16 / Windows, plugin 0.9.0
Two consecutive production runs, both reported
donewith an emptylast_error:22
rollout_summaries/*.mdand 164 KB ofraw_memories.mdsat un-consolidated;memory_searchreturned no match for any of them. The consolidator's only tool calls in the server log were threerg phase2_workspace_diff.mdreads, then nothing.Fix
Fingerprint the artifacts the consolidator owns (
MEMORY.md,memory_summary.md,skills/) before and after the helper runs. If the fingerprint is unchanged while the diff carried learning material, fail the job with a newno_artifact_changesstatus and skipresetBaseline, so the diff survives for the next attempt and the condition shows up inmemory_inspectrather than vanishing.diffCarriesNewLearningis scoped to the two input families the contract names as consolidation triggers — files underrollout_summaries/, andextensions/<name>/resources/.raw_memories.mdis deliberately excluded:rebuildRawMemories([])always writes a"No raw memories yet."placeholder, so on a first INIT run that file alone would look like new material and turn a legitimately empty consolidation into a permanent retry loop. A new or updated stage-1 output always changes therollout_summariesset too, since the file stem embedssource_updated_at, so nothing real is lost.Note on retry behaviour: phase 2 never exhausts retries by design (
store.ts: "retry_remaining is informational only"), so a helper that keeps no-opping will retry hourly instead of being forgotten. That is the intended trade — visible and self-healing beats silent and permanent — but it is a behaviour change worth a maintainer opinion.Verification
tests/phase2.test.tsgains two cases: the no-op is rejected with the diff preserved, and a no-op is still accepted when the diff genuinely carries nothing.tests/workspace.test.tscovers the fingerprint and the learning-material predicate, including the first-INIT non-regression.Driven against pristine
0.9.0sources, a consolidated workspace, and a stubbed helper (1 = pristine 0.9.0, 2 = this patch):succeededno_artifact_changesdonefailedsucceeded, watermark advanced, baseline resetThe control case is unchanged, so the happy path is untouched.
Local test-run caveat
I could not run the upstream suite unmodified on this machine: Bun 1.2.5 on Windows fails every
fs.openSync()call that passes numeric flags — including plainO_WRONLYon an existing file — withENOENT, while the string-flag form ("w","wx") works.src/path-guard.tsuses numeric flags, soensureLayoutthrows and the suite collapses before reaching any assertion. This does not affect production: OpenCode's bundled Bun handles numeric flags correctly, and the plugin demonstrably runs. I verified with a standalone harness (one process and one memory root per scenario) plusbun run typecheck, which passes. Flagging it as a separate portability issue rather than folding it into this PR, sincepath-guard.tsis security-sensitive and the fix belongs in its own change.