refactor(core): simplify session input promotion (#33443)

This commit is contained in:
Kit Langton 2026-06-22 23:51:49 +02:00 committed by GitHub
commit f48f24ec4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 160 additions and 493 deletions

View file

@ -14,7 +14,7 @@ import sessionMessageProjectionOrderMigration from "@opencode-ai/core/database/m
import eventSourcedSessionInputMigration from "@opencode-ai/core/database/migration/20260604172448_event_sourced_session_input"
import contextEpochAgentMigration from "@opencode-ai/core/database/migration/20260605042240_add_context_epoch_agent"
import simplifyIntegrationCredentialsMigration from "@opencode-ai/core/database/migration/20260611192811_lush_chimera"
import resetV2SessionStateMigration from "@opencode-ai/core/database/migration/20260622170816_reset_v2_session_state"
import simplifySessionInputMigration from "@opencode-ai/core/database/migration/20260622202450_simplify_session_input"
import { EventV2 } from "@opencode-ai/core/event"
import { ProjectV2 } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql"
@ -264,8 +264,8 @@ describe("DatabaseMigration", () => {
yield* db.run(
sql`INSERT INTO session_context_epoch (session_id, baseline, snapshot, baseline_seq) VALUES ('session', 'baseline', '{}', 9)`,
)
yield* db.run(sql`DELETE FROM migration WHERE id = ${resetV2SessionStateMigration.id}`)
yield* DatabaseMigration.applyOnly(db, [resetV2SessionStateMigration])
yield* db.run(sql`DELETE FROM migration WHERE id = ${simplifySessionInputMigration.id}`)
yield* DatabaseMigration.applyOnly(db, [simplifySessionInputMigration])
const database = Layer.succeed(Database.Service, { db })
const events = EventV2.layer.pipe(Layer.provide(database))

View file

@ -225,7 +225,7 @@ describe("SessionV2.create", () => {
Array.from(yield* session.events({ sessionID: created.id }).pipe(Stream.take(2), Stream.runCollect)),
).toMatchObject([
{ durable: { seq: 1 }, type: "session.next.prompt.admitted", data: { prompt: { text: "Hello" } } },
{ durable: { seq: 2 }, type: "session.next.prompt.promoted" },
{ durable: { seq: 2 }, type: "session.next.prompted" },
])
}),
)
@ -308,8 +308,8 @@ describe("SessionV2.create", () => {
.pipe(Effect.orDie)).map((event) => [event.seq, event.type]),
).toEqual([
[0, EventV2.versionedType(SessionV1.Event.Created.type, 1)],
[1, EventV2.versionedType(SessionEvent.PromptLifecycle.Admitted.type, 1)],
[2, EventV2.versionedType(SessionEvent.PromptLifecycle.Promoted.type, 1)],
[1, EventV2.versionedType(SessionEvent.PromptAdmitted.type, 1)],
[2, EventV2.versionedType(SessionEvent.Prompted.type, 1)],
])
}).pipe(Effect.provide(Layer.fresh(Layer.mergeAll(targetDatabase, targetEvents, targetProjector, targetStore))))
}),

View file

@ -120,7 +120,7 @@ describe("SessionProjector", () => {
),
)
it.effect("marks an admitted lifecycle row promoted with the PromptPromoted event sequence", () =>
it.effect("marks an inbox row promoted with the Prompted event sequence", () =>
Effect.gen(function* () {
const { db } = yield* Database.Service
yield* db
@ -142,19 +142,20 @@ describe("SessionProjector", () => {
.pipe(Effect.orDie)
const events = yield* EventV2.Service
const id = SessionMessage.ID.make("msg_admitted")
yield* SessionInput.admit(db, events, {
const admitted = yield* SessionInput.admit(db, events, {
id,
sessionID,
prompt: new Prompt({ text: "promote me" }),
delivery: "steer",
})
if (!admitted) return yield* Effect.die("Prompt admission failed")
const event = yield* events.publish(SessionEvent.PromptLifecycle.Promoted, {
const event = yield* events.publish(SessionEvent.Prompted, {
sessionID,
timestamp: created,
timestamp: admitted.timeCreated,
messageID: id,
prompt: new Prompt({ text: "promote me" }),
timeCreated: created,
delivery: "steer",
})
expect(

View file

@ -179,8 +179,8 @@ describe("SessionV2.prompt", () => {
expect(streamed.map((event) => [event.durable?.seq, event.type])).toEqual([
[0, "session.next.prompt.admitted"],
[1, "session.next.prompt.admitted"],
[2, "session.next.prompt.promoted"],
[3, "session.next.prompt.promoted"],
[2, "session.next.prompted"],
[3, "session.next.prompted"],
])
expect(
Array.from(
@ -334,7 +334,7 @@ describe("SessionV2.prompt", () => {
expect(messages[1]).toEqual(messages[0])
expect(yield* session.messages({ sessionID })).toEqual([])
expect(yield* admittedCount).toBe(1)
expect(yield* eventCount(EventV2.versionedType(SessionEvent.PromptLifecycle.Admitted.type, 1))).toBe(1)
expect(yield* eventCount(EventV2.versionedType(SessionEvent.PromptAdmitted.type, 1))).toBe(1)
}),
)
@ -354,7 +354,7 @@ describe("SessionV2.prompt", () => {
{ concurrency: "unbounded" },
)
expect(yield* eventCount(EventV2.versionedType(SessionEvent.PromptLifecycle.Promoted.type, 1))).toBe(1)
expect(yield* eventCount(EventV2.versionedType(SessionEvent.Prompted.type, 1))).toBe(1)
expect(yield* admitted(messageID)).toMatchObject({ promotedSeq: 1 })
expect(yield* session.messages({ sessionID })).toMatchObject([
{ id: messageID, type: "user", text: "Promote once" },
@ -362,14 +362,14 @@ describe("SessionV2.prompt", () => {
}),
)
it.effect("promotes steers only through the captured aggregate cutoff", () =>
it.effect("promotes steers only through the captured inbox cutoff", () =>
Effect.gen(function* () {
yield* setup
const { db } = yield* Database.Service
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
const first = yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Before cutoff" }), resume: false })
const cutoff = yield* SessionInput.latestSeq(db, sessionID)
const cutoff = first.admittedSeq
const second = yield* session.prompt({ sessionID, prompt: new Prompt({ text: "After cutoff" }), resume: false })
yield* SessionInput.promoteSteers(db, events, sessionID, cutoff)
@ -379,7 +379,7 @@ describe("SessionV2.prompt", () => {
}),
)
it.effect("reprojects one pending lifecycle without scheduling execution", () =>
it.effect("reprojects pending inbox input without scheduling execution", () =>
Effect.gen(function* () {
yield* setup
const { db } = yield* Database.Service
@ -489,6 +489,27 @@ describe("SessionV2.prompt", () => {
}),
)
it.effect("rejects a prompt ID already used by visible Session history", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* events.publish(SessionEvent.Synthetic, {
sessionID,
messageID,
timestamp: yield* DateTime.now,
text: "Existing history",
})
const failure = yield* session
.prompt({ id: messageID, sessionID, prompt: new Prompt({ text: "Conflicting prompt" }), resume: false })
.pipe(Effect.flip)
expect(failure).toMatchObject({ _tag: "Session.PromptConflictError", sessionID, messageID })
expect(yield* admitted(messageID)).toBeUndefined()
}),
)
it.effect("starts execution by default after recording the prompt", () =>
Effect.gen(function* () {
yield* setup

View file

@ -176,7 +176,7 @@ describe("SessionRunnerLLM recorded", () => {
.all()).map((event) => event.type),
).toEqual([
"session.next.prompt.admitted.1",
"session.next.prompt.promoted.1",
"session.next.prompted.1",
"session.next.step.started.1",
"session.next.text.started.1",
"session.next.text.ended.1",

View file

@ -2404,7 +2404,7 @@ describe("SessionRunnerLLM", () => {
const events = yield* EventV2.Service
const defect = new Error("fail after prompt promotion")
let fail = true
yield* events.project(SessionEvent.PromptLifecycle.Promoted, () => (fail ? Effect.die(defect) : Effect.void))
yield* events.project(SessionEvent.Prompted, () => (fail ? Effect.die(defect) : Effect.void))
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Recover promoted input" }), resume: false })
expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect)
@ -2429,9 +2429,7 @@ describe("SessionRunnerLLM", () => {
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* events.listen((event) =>
event.type === SessionEvent.PromptLifecycle.Promoted.type
? Effect.die("fail after prompt promotion commits")
: Effect.void,
event.type === SessionEvent.Prompted.type ? Effect.die("fail after prompt promotion commits") : Effect.void,
)
yield* session.prompt({
sessionID,