mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-15 04:27:20 +03:00
fix(core): keep the instruction baseline on committed revert (#54410)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
f36cda5bc8
commit
a5bf171649
@@ -1,6 +1,6 @@
|
||||
export * as InstructionState from "./instruction-state.js"
|
||||
|
||||
import { eq, inArray, sql } from "drizzle-orm"
|
||||
import { and, eq, gte, inArray, sql } from "drizzle-orm"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import type { Database } from "../database/database.js"
|
||||
import type { Bus } from "../bus.js"
|
||||
@@ -153,10 +153,18 @@ export const advanceEpoch = Effect.fn("InstructionState.advanceEpoch")(function*
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
export const reset = Effect.fn("InstructionState.reset")(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
|
||||
/**
|
||||
* Clears the fold only when a committed revert removed an update or the epoch start. Otherwise the
|
||||
* fold still matches the surviving history, and keeping its baseline keeps the prompt cache.
|
||||
*/
|
||||
export const rewind = Effect.fn("InstructionState.rewind")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
boundary: number,
|
||||
) {
|
||||
yield* db
|
||||
.delete(InstructionStateTable)
|
||||
.where(eq(InstructionStateTable.session_id, sessionID))
|
||||
.where(and(eq(InstructionStateTable.session_id, sessionID), gte(InstructionStateTable.through_seq, boundary)))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
@@ -806,7 +806,7 @@ const layer = Layer.effectDiscard(
|
||||
.where(eq(SessionTable.id, event.data.sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* InstructionState.reset(db, event.data.sessionID)
|
||||
yield* InstructionState.rewind(db, event.data.sessionID, boundary.seq)
|
||||
}),
|
||||
)
|
||||
yield* bus.subscribe([SessionEvent.Step.Ended, SessionEvent.Step.Failed, SessionEvent.UsageRecorded]).pipe(
|
||||
|
||||
@@ -202,8 +202,8 @@ describe("SessionProjector", () => {
|
||||
tokens_cache_read: 3,
|
||||
tokens_cache_write: 1,
|
||||
})
|
||||
// A committed revert resets the fold cache so the next boundary establishes a new epoch.
|
||||
expect(yield* db.select().from(InstructionStateTable).get().pipe(Effect.orDie)).toBeUndefined()
|
||||
// No instruction update followed the boundary, so the fold survives.
|
||||
expect(yield* db.select().from(InstructionStateTable).get().pipe(Effect.orDie)).toMatchObject({ through_seq: 0 })
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -1739,6 +1739,34 @@ describe("SessionRunnerLLM", () => {
|
||||
expect(yield* s.messages).toHaveLength(3)
|
||||
})
|
||||
|
||||
scenario("keeps the system prompt stable across a committed revert", function* (s) {
|
||||
yield* s.runPrompt("First")
|
||||
s.systemBaseline = "Changed context"
|
||||
yield* s.runPrompt("Second")
|
||||
const third = yield* s.runPrompt("Third")
|
||||
const before = s.requests.at(-1)?.system.map((part) => part.text)
|
||||
expect(before).toEqual([defaultSystem, fakeIdentity, "Initial context"])
|
||||
|
||||
yield* s.bus.publish(SessionEvent.RevertEvent.Committed, { sessionID, to: third.id })
|
||||
yield* s.runPrompt("Fourth")
|
||||
|
||||
expect(s.requests.at(-1)?.system.map((part) => part.text)).toEqual(before)
|
||||
})
|
||||
|
||||
scenario("resyncs the instruction state when a committed revert removes an update", function* (s) {
|
||||
yield* s.runPrompt("First")
|
||||
s.systemBaseline = "Changed context"
|
||||
const second = yield* s.runPrompt("Second")
|
||||
s.systemBaseline = "Latest context"
|
||||
yield* s.runPrompt("Third")
|
||||
|
||||
// The "Latest context" update is delivered with "Third", after the boundary.
|
||||
yield* s.bus.publish(SessionEvent.RevertEvent.Committed, { sessionID, to: second.id })
|
||||
yield* s.runPrompt("Fourth")
|
||||
|
||||
expect(s.requests.at(-1)?.system.map((part) => part.text)).toEqual([defaultSystem, fakeIdentity, "Latest context"])
|
||||
})
|
||||
|
||||
scenario("uses the selected model family prompt when the agent does not override it", function* (s) {
|
||||
s.currentModel = LanguageModel.make({ id: "gpt-5", provider: "openai", route: OpenAIChat.route })
|
||||
yield* s.admit("First")
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@ Instruction sync persists content-addressed values and may freeze rendered chron
|
||||
|
||||
Before each Physical Attempt that reaches model execution, the runner reads every source concurrently exactly once, hashes encoded values, and admits one delta atomically with its new blobs before input delivery. The initial delta must be complete; it carries no update text. An unavailable source blocks only that initial delta and otherwise silently retains the stored value. Request assembly renders the epoch baseline from stored values. Later changes render once at admission, freeze optional `text` in the durable event, and project that text as a chronological System message; clients display changed keys rather than privileged prose.
|
||||
|
||||
An instruction epoch spans completed compactions. `session.compaction.ended` moves the epoch start to its exact sequence, making current values initial, without reading sources or authoring an instruction event. Session movement retains state so destination changes become chronological updates; committed revert clears state so the next boundary establishes a fresh baseline. A fork copies messages only through its selected boundary but adopts the parent's newest instruction values as its baseline. Model selection affects request assembly but is not itself an instruction source.
|
||||
An instruction epoch spans completed compactions. `session.compaction.ended` moves the epoch start to its exact sequence, making current values initial, without reading sources or authoring an instruction event. Session movement retains state so destination changes become chronological updates; committed revert retains state unless it removed an instruction update, in which case it clears state so the next boundary establishes a fresh baseline. A fork copies messages only through its selected boundary but adopts the parent's newest instruction values as its baseline. Model selection affects request assembly but is not itself an instruction source.
|
||||
|
||||
## Compaction Rebuilds Active History
|
||||
|
||||
|
||||
Reference in New Issue
Block a user