refactor(core): simplify session input promotion (#33443)
This commit is contained in:
parent
34b3d59a23
commit
f48f24ec4e
1
packages/core/src/database/migration.gen.ts
generated
1
packages/core/src/database/migration.gen.ts
generated
@ -39,5 +39,6 @@ export const migrations = (
|
||||
import("./migration/20260612174303_project_dir_strategy"),
|
||||
import("./migration/20260622142730_simplify_session_context_epoch"),
|
||||
import("./migration/20260622170816_reset_v2_session_state"),
|
||||
import("./migration/20260622202450_simplify_session_input"),
|
||||
])
|
||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260622202450_simplify_session_input",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`DELETE FROM \`session_context_epoch\`;`)
|
||||
yield* tx.run(`DELETE FROM \`session_input\`;`)
|
||||
yield* tx.run(`DELETE FROM \`session_message\`;`)
|
||||
yield* tx.run(`DELETE FROM \`event\`;`)
|
||||
yield* tx.run(`DELETE FROM \`event_sequence\`;`)
|
||||
yield* tx.run(`UPDATE \`session\` SET \`workspace_id\` = NULL WHERE \`workspace_id\` IS NOT NULL;`)
|
||||
yield* tx.run(`DELETE FROM \`workspace\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@ -46,6 +46,19 @@ export type Payload<D extends Definition = Definition> = {
|
||||
export type Subscriber<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void>
|
||||
export type Unsubscribe = Effect.Effect<void>
|
||||
|
||||
export const latestSequence = Effect.fn("EventV2.latestSequence")(function* (
|
||||
db: Database.Interface["db"],
|
||||
aggregateID: string,
|
||||
) {
|
||||
const row = yield* db
|
||||
.select({ seq: EventSequenceTable.seq })
|
||||
.from(EventSequenceTable)
|
||||
.where(eq(EventSequenceTable.aggregate_id, aggregateID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
return row?.seq ?? -1
|
||||
})
|
||||
|
||||
export type SerializedEvent = {
|
||||
readonly id: ID
|
||||
readonly type: string
|
||||
|
||||
@ -64,7 +64,7 @@ const prepareOnce = Effect.fnUntraced(function* (
|
||||
return { baseline: stored.baseline, baselineSeq: stored.baseline_seq }
|
||||
}
|
||||
if (result._tag === "ReplacementReady") {
|
||||
const baselineSeq = replacementSeq ?? (yield* SessionInput.latestSeq(db, sessionID))
|
||||
const baselineSeq = replacementSeq ?? (yield* EventV2.latestSequence(db, sessionID))
|
||||
yield* replace(db, sessionID, baselineSeq, result.generation)
|
||||
return { baseline: result.generation.baseline, baselineSeq }
|
||||
}
|
||||
@ -124,7 +124,7 @@ const insert = Effect.fnUntraced(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
generation: SystemContext.Generation,
|
||||
) {
|
||||
const baselineSeq = yield* SessionInput.latestSeq(db, sessionID)
|
||||
const baselineSeq = yield* EventV2.latestSequence(db, sessionID)
|
||||
yield* db
|
||||
.insert(SessionContextEpochTable)
|
||||
.values({
|
||||
|
||||
@ -25,6 +25,12 @@ const Base = {
|
||||
timestamp: V2Schema.DateTimeUtcFromMillis,
|
||||
sessionID: SessionSchema.ID,
|
||||
}
|
||||
const PromptFields = {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
prompt: Prompt,
|
||||
delivery: Schema.Literals(["steer", "queue"]),
|
||||
}
|
||||
|
||||
const options = {
|
||||
durable: {
|
||||
@ -83,40 +89,16 @@ export type Moved = typeof Moved.Type
|
||||
export const Prompted = EventV2.define({
|
||||
type: "session.next.prompted",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
prompt: Prompt,
|
||||
delivery: Schema.Literals(["steer", "queue"]),
|
||||
},
|
||||
schema: PromptFields,
|
||||
})
|
||||
export type Prompted = typeof Prompted.Type
|
||||
|
||||
export namespace PromptLifecycle {
|
||||
export const Admitted = EventV2.define({
|
||||
type: "session.next.prompt.admitted",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
prompt: Prompt,
|
||||
delivery: Schema.Literals(["steer", "queue"]),
|
||||
},
|
||||
})
|
||||
export type Admitted = typeof Admitted.Type
|
||||
|
||||
export const Promoted = EventV2.define({
|
||||
type: "session.next.prompt.promoted",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
prompt: Prompt,
|
||||
timeCreated: V2Schema.DateTimeUtcFromMillis,
|
||||
},
|
||||
})
|
||||
export type Promoted = typeof Promoted.Type
|
||||
}
|
||||
export const PromptAdmitted = EventV2.define({
|
||||
type: "session.next.prompt.admitted",
|
||||
...options,
|
||||
schema: PromptFields,
|
||||
})
|
||||
export type PromptAdmitted = typeof PromptAdmitted.Type
|
||||
|
||||
export const ContextUpdated = EventV2.define({
|
||||
type: "session.next.context.updated",
|
||||
@ -455,8 +437,7 @@ const DurableDefinitions = [
|
||||
ModelSwitched,
|
||||
Moved,
|
||||
Prompted,
|
||||
PromptLifecycle.Admitted,
|
||||
PromptLifecycle.Promoted,
|
||||
PromptAdmitted,
|
||||
ContextUpdated,
|
||||
Synthetic,
|
||||
Shell.Started,
|
||||
|
||||
@ -4,7 +4,6 @@ import { and, asc, eq, isNull, lte } from "drizzle-orm"
|
||||
import { DateTime, Effect, Schema } from "effect"
|
||||
import type { Database } from "../database/database"
|
||||
import type { EventV2 } from "../event"
|
||||
import { EventSequenceTable } from "../event/sql"
|
||||
import { NonNegativeInt } from "../schema"
|
||||
import { V2Schema } from "../v2-schema"
|
||||
import { SessionEvent } from "./event"
|
||||
@ -65,7 +64,7 @@ export const admit = Effect.fn("SessionInput.admit")(function* (
|
||||
if (existing !== undefined) return existing
|
||||
const timestamp = yield* DateTime.now
|
||||
return yield* events
|
||||
.publish(SessionEvent.PromptLifecycle.Admitted, {
|
||||
.publish(SessionEvent.PromptAdmitted, {
|
||||
messageID: input.id,
|
||||
sessionID: input.sessionID,
|
||||
timestamp,
|
||||
@ -93,19 +92,6 @@ export const admit = Effect.fn("SessionInput.admit")(function* (
|
||||
)
|
||||
})
|
||||
|
||||
export const latestSeq = Effect.fn("SessionInput.latestSeq")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
) {
|
||||
const row = yield* db
|
||||
.select({ seq: EventSequenceTable.seq })
|
||||
.from(EventSequenceTable)
|
||||
.where(eq(EventSequenceTable.aggregate_id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
return row?.seq ?? -1
|
||||
})
|
||||
|
||||
export const projectAdmitted = Effect.fn("SessionInput.projectAdmitted")(function* (
|
||||
db: DatabaseService,
|
||||
input: {
|
||||
@ -117,6 +103,13 @@ export const projectAdmitted = Effect.fn("SessionInput.projectAdmitted")(functio
|
||||
readonly timeCreated: DateTime.Utc
|
||||
},
|
||||
) {
|
||||
const message = yield* db
|
||||
.select({ id: SessionMessageTable.id })
|
||||
.from(SessionMessageTable)
|
||||
.where(eq(SessionMessageTable.id, input.id))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (message !== undefined) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
const stored = yield* db
|
||||
.insert(SessionInputTable)
|
||||
.values({
|
||||
@ -134,12 +127,13 @@ export const projectAdmitted = Effect.fn("SessionInput.projectAdmitted")(functio
|
||||
if (!stored) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
})
|
||||
|
||||
export const projectPromoted = Effect.fn("SessionInput.projectPromoted")(function* (
|
||||
export const projectPrompted = Effect.fn("SessionInput.projectPrompted")(function* (
|
||||
db: DatabaseService,
|
||||
input: {
|
||||
readonly id: SessionMessage.ID
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly prompt: Prompt
|
||||
readonly delivery: Delivery
|
||||
readonly timeCreated: DateTime.Utc
|
||||
readonly promotedSeq: number
|
||||
},
|
||||
@ -157,14 +151,32 @@ export const projectPromoted = Effect.fn("SessionInput.projectPromoted")(functio
|
||||
.returning()
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!updated) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
const stored = fromRow(updated)
|
||||
if (
|
||||
!matchesPrompt(stored, input) ||
|
||||
DateTime.toEpochMillis(stored.timeCreated) !== DateTime.toEpochMillis(input.timeCreated)
|
||||
)
|
||||
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
return toMessage(stored)
|
||||
if (updated) {
|
||||
const stored = fromRow(updated)
|
||||
if (!matchesProjection(stored, input)) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
return
|
||||
}
|
||||
|
||||
const stored = yield* find(db, input.id)
|
||||
if (stored) {
|
||||
if (!matchesProjection(stored, input) || stored.promotedSeq !== input.promotedSeq)
|
||||
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
return
|
||||
}
|
||||
|
||||
yield* db
|
||||
.insert(SessionInputTable)
|
||||
.values({
|
||||
id: input.id,
|
||||
session_id: input.sessionID,
|
||||
prompt: encodePrompt(input.prompt),
|
||||
delivery: input.delivery,
|
||||
admitted_seq: input.promotedSeq,
|
||||
promoted_seq: input.promotedSeq,
|
||||
time_created: DateTime.toEpochMillis(input.timeCreated),
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
export const hasPending = Effect.fn("SessionInput.hasPending")(function* (
|
||||
@ -201,35 +213,17 @@ const matchesPrompt = (input: Admitted, expected: { readonly sessionID: SessionS
|
||||
input.sessionID === expected.sessionID &&
|
||||
JSON.stringify(encodePrompt(input.prompt)) === JSON.stringify(encodePrompt(expected.prompt))
|
||||
|
||||
export const projectLegacyPrompted = Effect.fn("SessionInput.projectLegacyPrompted")(function* (
|
||||
db: DatabaseService,
|
||||
input: {
|
||||
readonly id: SessionMessage.ID
|
||||
const matchesProjection = (
|
||||
input: Admitted,
|
||||
expected: {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly prompt: Prompt
|
||||
readonly delivery: Delivery
|
||||
readonly timeCreated: DateTime.Utc
|
||||
readonly promotedSeq: number
|
||||
},
|
||||
) {
|
||||
const inserted = yield* db
|
||||
.insert(SessionInputTable)
|
||||
.values({
|
||||
id: input.id,
|
||||
session_id: input.sessionID,
|
||||
admitted_seq: input.promotedSeq,
|
||||
prompt: encodePrompt(input.prompt),
|
||||
delivery: input.delivery,
|
||||
promoted_seq: input.promotedSeq,
|
||||
time_created: DateTime.toEpochMillis(input.timeCreated),
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.returning()
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!inserted) return yield* Effect.die("Prompt projection conflicts with admitted input")
|
||||
return fromRow(inserted)
|
||||
})
|
||||
) =>
|
||||
equivalent(input, expected) &&
|
||||
DateTime.toEpochMillis(input.timeCreated) === DateTime.toEpochMillis(expected.timeCreated)
|
||||
|
||||
const publish = Effect.fn("SessionInput.publish")(function* (
|
||||
db: DatabaseService,
|
||||
@ -238,18 +232,19 @@ const publish = Effect.fn("SessionInput.publish")(function* (
|
||||
rows: ReadonlyArray<typeof SessionInputTable.$inferSelect>,
|
||||
) {
|
||||
for (const row of rows) {
|
||||
const id = SessionMessage.ID.make(row.id)
|
||||
yield* events
|
||||
.publish(SessionEvent.PromptLifecycle.Promoted, {
|
||||
.publish(SessionEvent.Prompted, {
|
||||
sessionID,
|
||||
timestamp: yield* DateTime.now,
|
||||
messageID: SessionMessage.ID.make(row.id),
|
||||
timestamp: DateTime.makeUnsafe(row.time_created),
|
||||
messageID: id,
|
||||
prompt: decodePrompt(row.prompt),
|
||||
timeCreated: DateTime.makeUnsafe(row.time_created),
|
||||
delivery: row.delivery,
|
||||
})
|
||||
.pipe(
|
||||
Effect.catchDefect((defect) =>
|
||||
defect instanceof LifecycleConflict
|
||||
? find(db, SessionMessage.ID.make(row.id)).pipe(
|
||||
? find(db, id).pipe(
|
||||
Effect.flatMap((stored) => (stored?.promotedSeq === undefined ? Effect.die(defect) : Effect.void)),
|
||||
)
|
||||
: Effect.die(defect),
|
||||
@ -303,13 +298,3 @@ export const promoteNextQueued = Effect.fn("SessionInput.promoteNextQueued")(fun
|
||||
.pipe(Effect.orDie)
|
||||
return row === undefined ? false : yield* publish(db, events, sessionID, [row]).pipe(Effect.as(true))
|
||||
})
|
||||
|
||||
const toMessage = (input: Admitted) =>
|
||||
new SessionMessage.User({
|
||||
id: input.id,
|
||||
type: "user",
|
||||
text: input.prompt.text,
|
||||
files: input.prompt.files,
|
||||
agents: input.prompt.agents,
|
||||
time: { created: input.timeCreated },
|
||||
})
|
||||
|
||||
@ -137,7 +137,6 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
||||
)
|
||||
},
|
||||
"session.next.prompt.admitted": () => Effect.void,
|
||||
"session.next.prompt.promoted": () => Effect.void,
|
||||
"session.next.context.updated": (event) =>
|
||||
adapter.appendMessage(
|
||||
new SessionMessage.System({
|
||||
|
||||
@ -21,7 +21,6 @@ type DatabaseService = Database.Interface["db"]
|
||||
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message)
|
||||
const encodeMessage = Schema.encodeSync(SessionMessage.Message)
|
||||
|
||||
class PromptAlreadyProjected extends Error {}
|
||||
export class SessionAlreadyProjected extends Error {}
|
||||
|
||||
type Usage = {
|
||||
@ -350,27 +349,19 @@ export const layer = Layer.effectDiscard(
|
||||
)
|
||||
yield* events.project(SessionEvent.Prompted, (event) =>
|
||||
Effect.gen(function* () {
|
||||
const messageID = event.data.messageID
|
||||
const existing = yield* db
|
||||
.select({ id: SessionMessageTable.id })
|
||||
.from(SessionMessageTable)
|
||||
.where(eq(SessionMessageTable.id, messageID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (existing) return yield* Effect.die(new PromptAlreadyProjected())
|
||||
yield* run(db, event)
|
||||
if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence")
|
||||
yield* SessionInput.projectLegacyPrompted(db, {
|
||||
id: messageID,
|
||||
yield* SessionInput.projectPrompted(db, {
|
||||
id: event.data.messageID,
|
||||
sessionID: event.data.sessionID,
|
||||
prompt: event.data.prompt,
|
||||
delivery: event.data.delivery,
|
||||
timeCreated: event.data.timestamp,
|
||||
promotedSeq: event.durable.seq,
|
||||
})
|
||||
yield* run(db, event)
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.PromptLifecycle.Admitted, (event) =>
|
||||
yield* events.project(SessionEvent.PromptAdmitted, (event) =>
|
||||
Effect.gen(function* () {
|
||||
if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence")
|
||||
yield* SessionInput.projectAdmitted(db, {
|
||||
@ -383,22 +374,6 @@ export const layer = Layer.effectDiscard(
|
||||
})
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.PromptLifecycle.Promoted, (event) =>
|
||||
Effect.gen(function* () {
|
||||
if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence")
|
||||
yield* insertMessage(
|
||||
db,
|
||||
event,
|
||||
yield* SessionInput.projectPromoted(db, {
|
||||
id: event.data.messageID,
|
||||
sessionID: event.data.sessionID,
|
||||
prompt: event.data.prompt,
|
||||
timeCreated: event.data.timeCreated,
|
||||
promotedSeq: event.durable.seq,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.ContextUpdated, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Synthetic, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Shell.Started, (event) => run(db, event))
|
||||
|
||||
@ -176,7 +176,7 @@ export const layer = Layer.effect(
|
||||
const toolFibers = yield* FiberSet.make<void, ToolOutputStore.Error>()
|
||||
let needsContinuation = false
|
||||
if (promotion) {
|
||||
const cutoff = yield* SessionInput.latestSeq(db, session.id)
|
||||
const cutoff = yield* EventV2.latestSequence(db, session.id)
|
||||
if (promotion === "steer") yield* SessionInput.promoteSteers(db, events, session.id, cutoff)
|
||||
if (promotion === "queue") {
|
||||
yield* SessionInput.promoteNextQueued(db, events, session.id)
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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))))
|
||||
}),
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -20,7 +20,6 @@ export type Event =
|
||||
| EventSessionNextMoved
|
||||
| EventSessionNextPrompted
|
||||
| EventSessionNextPromptAdmitted
|
||||
| EventSessionNextPromptPromoted
|
||||
| EventSessionNextContextUpdated
|
||||
| EventSessionNextSynthetic
|
||||
| EventSessionNextShellStarted
|
||||
@ -864,17 +863,6 @@ export type GlobalEvent = {
|
||||
delivery: "steer" | "queue"
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "session.next.prompt.promoted"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
messageID: string
|
||||
prompt: Prompt
|
||||
timeCreated: number
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "session.next.context.updated"
|
||||
@ -1628,7 +1616,6 @@ export type GlobalEvent = {
|
||||
| SyncEventSessionNextMoved
|
||||
| SyncEventSessionNextPrompted
|
||||
| SyncEventSessionNextPromptAdmitted
|
||||
| SyncEventSessionNextPromptPromoted
|
||||
| SyncEventSessionNextContextUpdated
|
||||
| SyncEventSessionNextSynthetic
|
||||
| SyncEventSessionNextShellStarted
|
||||
@ -2770,7 +2757,6 @@ export type V2Event =
|
||||
| V2EventSessionNextMoved
|
||||
| V2EventSessionNextPrompted
|
||||
| V2EventSessionNextPromptAdmitted
|
||||
| V2EventSessionNextPromptPromoted
|
||||
| V2EventSessionNextContextUpdated
|
||||
| V2EventSessionNextSynthetic
|
||||
| V2EventSessionNextShellStarted
|
||||
@ -3220,24 +3206,6 @@ export type SyncEventSessionNextPromptAdmitted = {
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextPromptPromoted = {
|
||||
type: "sync"
|
||||
id: string
|
||||
syncEvent: {
|
||||
type: "session.next.prompt.promoted.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: string
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
messageID: string
|
||||
prompt: Prompt
|
||||
timeCreated: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextContextUpdated = {
|
||||
type: "sync"
|
||||
id: string
|
||||
@ -4523,27 +4491,6 @@ export type V2EventSessionNextPromptAdmitted = {
|
||||
}
|
||||
}
|
||||
|
||||
export type V2EventSessionNextPromptPromoted = {
|
||||
id: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
durable?: {
|
||||
aggregateID: string
|
||||
seq: number
|
||||
version: number
|
||||
}
|
||||
location?: LocationRef
|
||||
type: "session.next.prompt.promoted"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
messageID: string
|
||||
prompt: Prompt
|
||||
timeCreated: number
|
||||
}
|
||||
}
|
||||
|
||||
export type V2EventSessionNextContextUpdated = {
|
||||
id: string
|
||||
metadata?: {
|
||||
@ -6168,18 +6115,6 @@ export type EventSessionNextPromptAdmitted = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextPromptPromoted = {
|
||||
id: string
|
||||
type: "session.next.prompt.promoted"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
messageID: string
|
||||
prompt: Prompt
|
||||
timeCreated: number
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextContextUpdated = {
|
||||
id: string
|
||||
type: "session.next.context.updated"
|
||||
|
||||
@ -14686,9 +14686,6 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventSessionNextPromptAdmitted"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventSessionNextPromptPromoted"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventSessionNextContextUpdated"
|
||||
},
|
||||
@ -17255,45 +17252,6 @@
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^evt_"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["session.next.prompt.promoted"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timestamp": {
|
||||
"type": "number"
|
||||
},
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"pattern": "^ses"
|
||||
},
|
||||
"messageID": {
|
||||
"type": "string",
|
||||
"pattern": "^msg_"
|
||||
},
|
||||
"prompt": {
|
||||
"$ref": "#/components/schemas/Prompt"
|
||||
},
|
||||
"timeCreated": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["timestamp", "sessionID", "messageID", "prompt", "timeCreated"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -19832,9 +19790,6 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/SyncEventSessionNextPromptAdmitted"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SyncEventSessionNextPromptPromoted"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SyncEventSessionNextContextUpdated"
|
||||
},
|
||||
@ -23073,9 +23028,6 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/V2EventSessionNextPromptAdmitted"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/V2EventSessionNextPromptPromoted"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/V2EventSessionNextContextUpdated"
|
||||
},
|
||||
@ -24400,66 +24352,6 @@
|
||||
"required": ["type", "id", "syncEvent"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SyncEventSessionNextPromptPromoted": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["sync"]
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^evt_"
|
||||
},
|
||||
"syncEvent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["session.next.prompt.promoted.1"]
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^evt_"
|
||||
},
|
||||
"seq": {
|
||||
"type": "number"
|
||||
},
|
||||
"aggregateID": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timestamp": {
|
||||
"type": "number"
|
||||
},
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"pattern": "^ses"
|
||||
},
|
||||
"messageID": {
|
||||
"type": "string",
|
||||
"pattern": "^msg_"
|
||||
},
|
||||
"prompt": {
|
||||
"$ref": "#/components/schemas/Prompt"
|
||||
},
|
||||
"timeCreated": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["timestamp", "sessionID", "messageID", "prompt", "timeCreated"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["type", "id", "seq", "aggregateID", "data"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["type", "id", "syncEvent"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SyncEventSessionNextContextUpdated": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -28622,67 +28514,6 @@
|
||||
"required": ["id", "type", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"V2EventSessionNextPromptPromoted": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^evt_"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"durable": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"aggregateID": {
|
||||
"type": "string"
|
||||
},
|
||||
"seq": {
|
||||
"type": "integer"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": ["aggregateID", "seq", "version"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"location": {
|
||||
"$ref": "#/components/schemas/LocationRef"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["session.next.prompt.promoted"]
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timestamp": {
|
||||
"type": "number"
|
||||
},
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"pattern": "^ses"
|
||||
},
|
||||
"messageID": {
|
||||
"type": "string",
|
||||
"pattern": "^msg_"
|
||||
},
|
||||
"prompt": {
|
||||
"$ref": "#/components/schemas/Prompt"
|
||||
},
|
||||
"timeCreated": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["timestamp", "sessionID", "messageID", "prompt", "timeCreated"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"V2EventSessionNextContextUpdated": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -33282,45 +33113,6 @@
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventSessionNextPromptPromoted": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^evt_"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["session.next.prompt.promoted"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timestamp": {
|
||||
"type": "number"
|
||||
},
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"pattern": "^ses"
|
||||
},
|
||||
"messageID": {
|
||||
"type": "string",
|
||||
"pattern": "^msg_"
|
||||
},
|
||||
"prompt": {
|
||||
"$ref": "#/components/schemas/Prompt"
|
||||
},
|
||||
"timeCreated": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["timestamp", "sessionID", "messageID", "prompt", "timeCreated"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventSessionNextContextUpdated": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@ -164,18 +164,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
}
|
||||
case "session.next.prompt.admitted":
|
||||
break
|
||||
case "session.next.prompt.promoted":
|
||||
message.update(event.data.sessionID, (draft) => {
|
||||
message.prepend(draft, {
|
||||
id: event.data.messageID,
|
||||
type: "user",
|
||||
text: event.data.prompt.text,
|
||||
files: event.data.prompt.files,
|
||||
agents: event.data.prompt.agents,
|
||||
time: { created: event.data.timeCreated },
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.next.context.updated":
|
||||
message.update(event.data.sessionID, (draft) => {
|
||||
message.prepend(draft, {
|
||||
|
||||
@ -370,7 +370,7 @@ test("settles pending tools when a live failure arrives", async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("renders admitted prompts only after promotion", async () => {
|
||||
test("renders admitted prompts only after they become model-visible", async () => {
|
||||
const events = createEventSource()
|
||||
const calls = createFetch(undefined, events)
|
||||
let sync!: ReturnType<typeof useData>
|
||||
@ -413,14 +413,14 @@ test("renders admitted prompts only after promotion", async () => {
|
||||
expect(sync.session.message.list("session-1") ?? []).toEqual([])
|
||||
|
||||
emitEvent(events, {
|
||||
id: "evt_promoted_1",
|
||||
type: "session.next.prompt.promoted",
|
||||
id: "evt_prompted_1",
|
||||
type: "session.next.prompted",
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
messageID: "msg_user_1",
|
||||
timestamp: 1,
|
||||
timestamp: 0,
|
||||
prompt: { text: "hello" },
|
||||
timeCreated: 0,
|
||||
delivery: "steer",
|
||||
},
|
||||
})
|
||||
|
||||
@ -434,54 +434,6 @@ test("renders admitted prompts only after promotion", async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("renders a promoted prompt when admission was missed", async () => {
|
||||
const events = createEventSource()
|
||||
const calls = createFetch(undefined, events)
|
||||
let sync!: ReturnType<typeof useData>
|
||||
let ready!: () => void
|
||||
const mounted = new Promise<void>((resolve) => {
|
||||
ready = resolve
|
||||
})
|
||||
|
||||
function Probe() {
|
||||
sync = useData()
|
||||
onMount(ready)
|
||||
return <box />
|
||||
}
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts>
|
||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
||||
<ProjectProvider>
|
||||
<DataProvider>
|
||||
<Probe />
|
||||
</DataProvider>
|
||||
</ProjectProvider>
|
||||
</SDKProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
|
||||
try {
|
||||
await mounted
|
||||
emitEvent(events, {
|
||||
id: "evt_promoted_1",
|
||||
type: "session.next.prompt.promoted",
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
messageID: "msg_user_1",
|
||||
timestamp: 1,
|
||||
prompt: { text: "hello" },
|
||||
timeCreated: 0,
|
||||
},
|
||||
})
|
||||
|
||||
await wait(() => sync.session.message.list("session-1")?.length === 1)
|
||||
expect(sync.session.message.list("session-1")?.[0]?.id).toBe("msg_user_1")
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("projects live context updates with their message ID", async () => {
|
||||
const events = createEventSource()
|
||||
const calls = createFetch(undefined, events)
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
# V2 Schema Changelog
|
||||
|
||||
## 2026-06-22: Simplify Session Input Promotion
|
||||
|
||||
- Keep `session.next.prompt.admitted.1` as the durable, client-visible record of pending Session input.
|
||||
- Replace `session.next.prompt.promoted.1` with the existing `session.next.prompted.1` event when input becomes model-visible.
|
||||
- Preserve the prompt endpoint, admission receipt, idempotency, steer/queue ordering, and atomic user-message projection.
|
||||
- Reset experimental V2 events, projections, inputs, Context Epochs, and synchronized workspace state while preserving canonical V1 `session`, `message`, and `part` rows.
|
||||
|
||||
## 2026-06-22: Reset Unpublished Compaction Event
|
||||
|
||||
- Replace the unpublished `session.next.compaction.ended.1` payload with the current checkpoint payload and remove its legacy decoder.
|
||||
|
||||
@ -12,8 +12,8 @@ sessions.create({ id?, location, ... })
|
||||
|
||||
sessions.prompt({ id?, sessionID, prompt, delivery?, resume? })
|
||||
-> omitted ID generates one internal message ID
|
||||
-> supplied ID admits one durable Session input when absent
|
||||
-> exact reuse returns the same admitted lifecycle receipt
|
||||
-> supplied ID inserts one durable Session inbox row when absent
|
||||
-> exact reuse returns the same admission receipt
|
||||
-> reusing one message ID for another Session, prompt, or delivery mode fails
|
||||
-> exact retry schedules another wake unless resume is false
|
||||
-> resume omitted or true schedules execution after admission
|
||||
@ -27,7 +27,9 @@ sessions.interrupt(sessionID)
|
||||
-> idle or missing Session is a no-op
|
||||
```
|
||||
|
||||
`session_input` is the durable admission inbox. Admitted inputs remain outside model-visible Session history until the serialized runner publishes `PromptLifecycle.Promoted`. The projector atomically writes the visible user message and marks its inbox row promoted in the same event transaction. The legacy V1-to-V2 shadow bridge continues publishing ordinary `Prompted` events for already-visible V1 prompts.
|
||||
`session_input` is the durable admission inbox. `PromptAdmitted` records and projects accepted input so pending queue state can be replayed, replicated, and observed by clients. Admitted inputs remain outside model-visible Session history until the serialized runner publishes `Prompted`. Its projector atomically writes the visible user message and marks the inbox row promoted in the same event transaction. The V1-to-V2 shadow bridge publishes the same `Prompted` event for already-visible V1 prompts.
|
||||
|
||||
`admittedSeq` is the durable Session event sequence of `PromptAdmitted`. Clients may use the admission event to represent queued input before `Prompted` makes it part of visible conversation history.
|
||||
|
||||
Execution routing starts from only the Session ID:
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user