feat(core): session.pending.list API with pending-only session_pending storage (#36126)
This commit is contained in:
parent
99156c10e6
commit
a72992e00f
43 changed files with 986 additions and 649 deletions
|
|
@ -54,4 +54,10 @@ export function path() {
|
|||
return join(Global.Path.data, `opencode-${InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`)
|
||||
}
|
||||
|
||||
export const node = makeGlobalNode({ service: Service, layer: layerFromPath(path()), deps: [] })
|
||||
// Resolve the database path lazily so tests and embedders that set
|
||||
// Flag.OPENCODE_DB after module evaluation still control the storage target.
|
||||
export const node = makeGlobalNode({
|
||||
service: Service,
|
||||
layer: Layer.suspend(() => layerFromPath(path())),
|
||||
deps: [],
|
||||
})
|
||||
|
|
|
|||
1
packages/core/src/database/migration.gen.ts
generated
1
packages/core/src/database/migration.gen.ts
generated
|
|
@ -52,5 +52,6 @@ export const migrations = (
|
|||
import("./migration/20260709013000_generic_session_input"),
|
||||
import("./migration/20260709025533_drop-todo"),
|
||||
import("./migration/20260709163752_time_suspended"),
|
||||
import("./migration/20260709190621_session_pending_table"),
|
||||
])
|
||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260709190621_session_pending_table",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
// Beta reset: session_input becomes the pending-only session_pending
|
||||
// table. Dropping the old table discards consumed ledger rows and any
|
||||
// in-flight pending work along with every historical index variant.
|
||||
yield* tx.run(`DROP TABLE \`session_input\`;`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_pending\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`session_id\` text NOT NULL,
|
||||
\`type\` text NOT NULL,
|
||||
\`data\` text NOT NULL,
|
||||
\`delivery\` text,
|
||||
\`admitted_seq\` integer NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
CONSTRAINT \`fk_session_pending_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(
|
||||
`CREATE INDEX \`session_pending_session_delivery_seq_idx\` ON \`session_pending\` (\`session_id\`,\`delivery\`,\`admitted_seq\`);`,
|
||||
)
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`session_pending_session_compaction_idx\` ON \`session_pending\` (\`session_id\`) WHERE "session_pending"."type" = 'compaction';`,
|
||||
)
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`session_pending_session_admitted_seq_idx\` ON \`session_pending\` (\`session_id\`,\`admitted_seq\`);`,
|
||||
)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -166,19 +166,6 @@ export default {
|
|||
CONSTRAINT \`fk_part_message_id_message_id_fk\` FOREIGN KEY (\`message_id\`) REFERENCES \`message\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_input\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`session_id\` text NOT NULL,
|
||||
\`type\` text NOT NULL,
|
||||
\`data\` text NOT NULL,
|
||||
\`delivery\` text,
|
||||
\`admitted_seq\` integer NOT NULL,
|
||||
\`promoted_seq\` integer,
|
||||
\`time_created\` integer NOT NULL,
|
||||
CONSTRAINT \`fk_session_input_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_message\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
|
|
@ -191,6 +178,18 @@ export default {
|
|||
CONSTRAINT \`fk_session_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_pending\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`session_id\` text NOT NULL,
|
||||
\`type\` text NOT NULL,
|
||||
\`data\` text NOT NULL,
|
||||
\`delivery\` text,
|
||||
\`admitted_seq\` integer NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
CONSTRAINT \`fk_session_pending_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
|
|
@ -249,18 +248,6 @@ export default {
|
|||
)
|
||||
yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`)
|
||||
yield* tx.run(
|
||||
`CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`,
|
||||
)
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`session_input_session_pending_compaction_idx\` ON \`session_input\` (\`session_id\`) WHERE "session_input"."type" = 'compaction' and "session_input"."promoted_seq" is null;`,
|
||||
)
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`,
|
||||
)
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`,
|
||||
)
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`,
|
||||
)
|
||||
|
|
@ -271,6 +258,15 @@ export default {
|
|||
`CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`,
|
||||
)
|
||||
yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`)
|
||||
yield* tx.run(
|
||||
`CREATE INDEX \`session_pending_session_delivery_seq_idx\` ON \`session_pending\` (\`session_id\`,\`delivery\`,\`admitted_seq\`);`,
|
||||
)
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`session_pending_session_compaction_idx\` ON \`session_pending\` (\`session_id\`) WHERE "session_pending"."type" = 'compaction';`,
|
||||
)
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`session_pending_session_admitted_seq_idx\` ON \`session_pending\` (\`session_id\`,\`admitted_seq\`);`,
|
||||
)
|
||||
yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_parent_idx\` ON \`session\` (\`parent_id\`);`)
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import { makeGlobalNode } from "./effect/app-node"
|
|||
import { LocationServiceMap } from "./location-service-map"
|
||||
import { MessageDecodeError } from "./session/error"
|
||||
import { SessionEvent } from "./session/event"
|
||||
import { SessionInput } from "./session/input"
|
||||
import { SessionPending } from "./session/pending"
|
||||
import { Snapshot } from "./snapshot"
|
||||
import { SessionRevert } from "./session/revert"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
|
|
@ -187,6 +187,12 @@ export interface Interface {
|
|||
readonly context: (
|
||||
sessionID: SessionSchema.ID,
|
||||
) => Effect.Effect<SessionMessage.Info[], NotFoundError | MessageDecodeError>
|
||||
/**
|
||||
* Durable admitted session work not yet visible in projected history,
|
||||
* ordered by admission. Includes unpromoted user and synthetic inputs and
|
||||
* unhandled compaction barriers.
|
||||
*/
|
||||
readonly pending: (sessionID: SessionSchema.ID) => Effect.Effect<SessionPending.Info[], NotFoundError>
|
||||
/**
|
||||
* Durable, ordered, gap-free session log read. Replays public durable
|
||||
* session events after the exclusive `after` cursor, emits a `Synced`
|
||||
|
|
@ -216,9 +222,9 @@ export interface Interface {
|
|||
files?: PromptInput.Prompt["files"]
|
||||
agents?: PromptInput.Prompt["agents"]
|
||||
metadata?: Record<string, unknown>
|
||||
delivery?: SessionInput.Delivery
|
||||
delivery?: SessionPending.Delivery
|
||||
resume?: boolean
|
||||
}) => Effect.Effect<SessionInput.User, NotFoundError | PromptConflictError | AttachmentError>
|
||||
}) => Effect.Effect<SessionPending.User, NotFoundError | PromptConflictError | AttachmentError>
|
||||
readonly command: (input: {
|
||||
id?: SessionMessage.ID
|
||||
sessionID: SessionSchema.ID
|
||||
|
|
@ -228,10 +234,10 @@ export interface Interface {
|
|||
model?: ModelV2.Ref
|
||||
files?: PromptInput.Prompt["files"]
|
||||
agents?: PromptInput.Prompt["agents"]
|
||||
delivery?: SessionInput.Delivery
|
||||
delivery?: SessionPending.Delivery
|
||||
resume?: boolean
|
||||
}) => Effect.Effect<
|
||||
SessionInput.User,
|
||||
SessionPending.User,
|
||||
NotFoundError | PromptConflictError | AttachmentError | CommandV2.NotFoundError | CommandV2.EvaluationError
|
||||
>
|
||||
readonly shell: (input: {
|
||||
|
|
@ -247,7 +253,7 @@ export interface Interface {
|
|||
}) => Effect.Effect<void, NotFoundError | SkillNotFoundError>
|
||||
readonly compact: (
|
||||
input: CompactInput,
|
||||
) => Effect.Effect<SessionInput.Compaction, NotFoundError | CompactionConflictError>
|
||||
) => Effect.Effect<SessionPending.Compaction, NotFoundError | CompactionConflictError>
|
||||
readonly wait: (id: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
|
||||
readonly active: Effect.Effect<ReadonlySet<SessionSchema.ID>>
|
||||
readonly background: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
|
||||
|
|
@ -259,9 +265,9 @@ export interface Interface {
|
|||
text: string
|
||||
description?: string
|
||||
metadata?: Record<string, unknown>
|
||||
delivery?: SessionInput.Delivery
|
||||
delivery?: SessionPending.Delivery
|
||||
resume?: boolean
|
||||
}) => Effect.Effect<SessionInput.Synthetic, NotFoundError | SyntheticConflictError>
|
||||
}) => Effect.Effect<SessionPending.Synthetic, NotFoundError | SyntheticConflictError>
|
||||
readonly revert: {
|
||||
readonly stage: (input: {
|
||||
sessionID: SessionSchema.ID
|
||||
|
|
@ -481,6 +487,10 @@ const layer = Layer.effect(
|
|||
yield* result.get(sessionID)
|
||||
return yield* store.context(sessionID)
|
||||
}),
|
||||
pending: Effect.fn("V2Session.pending")(function* (sessionID) {
|
||||
yield* result.get(sessionID)
|
||||
return yield* SessionPending.list(db, sessionID)
|
||||
}),
|
||||
log: (input) =>
|
||||
Stream.unwrap(
|
||||
result
|
||||
|
|
@ -504,25 +514,25 @@ const layer = Layer.effect(
|
|||
Effect.provideService(FSUtil.Service, fs),
|
||||
)
|
||||
const messageID = input.id ?? SessionMessage.ID.create()
|
||||
const admittedInput = SessionInput.Message.make({
|
||||
const admittedInput = SessionPending.Message.make({
|
||||
type: "user",
|
||||
data: { ...prompt, metadata: input.metadata },
|
||||
delivery: input.delivery ?? "steer",
|
||||
})
|
||||
const admitted = yield* SessionInput.admit(db, events, {
|
||||
const admitted = yield* SessionPending.admit(db, events, {
|
||||
id: messageID,
|
||||
sessionID: input.sessionID,
|
||||
input: admittedInput,
|
||||
}).pipe(
|
||||
Effect.catchDefect((defect) =>
|
||||
defect instanceof SessionInput.LifecycleConflict
|
||||
defect instanceof SessionPending.LifecycleConflict
|
||||
? new PromptConflictError({ sessionID: input.sessionID, messageID })
|
||||
: Effect.die(defect),
|
||||
),
|
||||
)
|
||||
if (
|
||||
admitted.type !== "user" ||
|
||||
!SessionInput.equivalent(admitted, { sessionID: input.sessionID, input: admittedInput })
|
||||
!SessionPending.equivalent(admitted, { sessionID: input.sessionID, input: admittedInput })
|
||||
)
|
||||
return yield* new PromptConflictError({ sessionID: input.sessionID, messageID })
|
||||
if (input.resume !== false) {
|
||||
|
|
@ -664,12 +674,12 @@ const layer = Layer.effect(
|
|||
compact: Effect.fn("V2Session.compact")(function* (input) {
|
||||
yield* result.get(input.sessionID)
|
||||
const inputID = input.id ?? SessionMessage.ID.create()
|
||||
const admitted = yield* SessionInput.admitCompaction(db, events, {
|
||||
const admitted = yield* SessionPending.admitCompaction(db, events, {
|
||||
id: inputID,
|
||||
sessionID: input.sessionID,
|
||||
}).pipe(
|
||||
Effect.catchDefect((defect) =>
|
||||
defect instanceof SessionInput.LifecycleConflict
|
||||
defect instanceof SessionPending.LifecycleConflict
|
||||
? new CompactionConflictError({ sessionID: input.sessionID, inputID })
|
||||
: Effect.die(defect),
|
||||
),
|
||||
|
|
@ -709,7 +719,7 @@ const layer = Layer.effect(
|
|||
Effect.gen(function* () {
|
||||
yield* result.get(input.sessionID)
|
||||
const inputID = input.id ?? SessionMessage.ID.create()
|
||||
const admittedInput = SessionInput.Message.make({
|
||||
const admittedInput = SessionPending.Message.make({
|
||||
type: "synthetic",
|
||||
data: {
|
||||
text: input.text,
|
||||
|
|
@ -718,20 +728,20 @@ const layer = Layer.effect(
|
|||
},
|
||||
delivery: input.delivery ?? "steer",
|
||||
})
|
||||
const admitted = yield* SessionInput.admit(db, events, {
|
||||
const admitted = yield* SessionPending.admit(db, events, {
|
||||
id: inputID,
|
||||
sessionID: input.sessionID,
|
||||
input: admittedInput,
|
||||
}).pipe(
|
||||
Effect.catchDefect((defect) =>
|
||||
defect instanceof SessionInput.LifecycleConflict
|
||||
defect instanceof SessionPending.LifecycleConflict
|
||||
? new SyntheticConflictError({ sessionID: input.sessionID, inputID })
|
||||
: Effect.die(defect),
|
||||
),
|
||||
)
|
||||
if (
|
||||
admitted.type !== "synthetic" ||
|
||||
!SessionInput.equivalent(admitted, { sessionID: input.sessionID, input: admittedInput })
|
||||
!SessionPending.equivalent(admitted, { sessionID: input.sessionID, input: admittedInput })
|
||||
)
|
||||
return yield* new SyntheticConflictError({ sessionID: input.sessionID, inputID })
|
||||
if (input.resume !== false && !(yield* result.get(input.sessionID)).revert)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export * as SessionInput from "./input"
|
||||
export * as SessionPending from "./pending"
|
||||
|
||||
import { and, asc, eq, isNull } from "drizzle-orm"
|
||||
import { and, asc, eq } from "drizzle-orm"
|
||||
import { DateTime, Effect, Schema } from "effect"
|
||||
import {
|
||||
Compaction,
|
||||
|
|
@ -11,14 +11,16 @@ import {
|
|||
SyntheticData,
|
||||
User,
|
||||
UserData,
|
||||
} from "@opencode-ai/schema/session-input"
|
||||
} from "@opencode-ai/schema/session-pending"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
import type { Database } from "../database/database"
|
||||
import type { EventV2 } from "../event"
|
||||
import { EventTable } from "../event/sql"
|
||||
import { KeyedMutex } from "../effect/keyed-mutex"
|
||||
import { SessionEvent } from "./event"
|
||||
import { SessionMessage } from "./message"
|
||||
import { SessionSchema } from "./schema"
|
||||
import { SessionInputTable, SessionMessageTable } from "./sql"
|
||||
import { SessionMessageTable, SessionPendingTable } from "./sql"
|
||||
|
||||
type DatabaseService = Database.Interface["db"]
|
||||
|
||||
|
|
@ -28,25 +30,28 @@ const decodeUser = Schema.decodeUnknownSync(UserData)
|
|||
const encodeUser = Schema.encodeSync(UserData)
|
||||
const decodeSynthetic = Schema.decodeUnknownSync(SyntheticData)
|
||||
const encodeSynthetic = Schema.encodeSync(SyntheticData)
|
||||
const decodeAdmittedEvent = Schema.decodeUnknownOption(SessionEvent.InputAdmitted.data)
|
||||
const admittedEventType = Event.versionedType(
|
||||
SessionEvent.InputAdmitted.type,
|
||||
SessionEvent.InputAdmitted.durable.version,
|
||||
)
|
||||
const inboxLocks = KeyedMutex.makeUnsafe<SessionSchema.ID>()
|
||||
|
||||
export class LifecycleConflict extends Schema.TaggedErrorClass<LifecycleConflict>()("SessionInput.LifecycleConflict", {
|
||||
id: SessionMessage.ID,
|
||||
}) {}
|
||||
export class LifecycleConflict extends Schema.TaggedErrorClass<LifecycleConflict>()(
|
||||
"SessionPending.LifecycleConflict",
|
||||
{
|
||||
id: SessionMessage.ID,
|
||||
},
|
||||
) {}
|
||||
|
||||
const fromRow = (row: typeof SessionInputTable.$inferSelect): Info => {
|
||||
const fromRow = (row: typeof SessionPendingTable.$inferSelect): Info => {
|
||||
const base = {
|
||||
admittedSeq: row.admitted_seq,
|
||||
id: SessionMessage.ID.make(row.id),
|
||||
sessionID: SessionSchema.ID.make(row.session_id),
|
||||
timeCreated: DateTime.makeUnsafe(row.time_created),
|
||||
}
|
||||
if (row.type === "compaction")
|
||||
return Compaction.make({
|
||||
...base,
|
||||
type: "compaction",
|
||||
...(row.promoted_seq === null ? {} : { handledSeq: row.promoted_seq }),
|
||||
})
|
||||
if (row.type === "compaction") return Compaction.make({ ...base, type: "compaction" })
|
||||
if (!row.delivery) throw new LifecycleConflict({ id: base.id })
|
||||
if (row.type === "user")
|
||||
return User.make({
|
||||
|
|
@ -54,7 +59,6 @@ const fromRow = (row: typeof SessionInputTable.$inferSelect): Info => {
|
|||
type: "user",
|
||||
data: decodeUser(row.data),
|
||||
delivery: row.delivery,
|
||||
...(row.promoted_seq === null ? {} : { promotedSeq: row.promoted_seq }),
|
||||
})
|
||||
if (row.type === "synthetic")
|
||||
return Synthetic.make({
|
||||
|
|
@ -62,31 +66,29 @@ const fromRow = (row: typeof SessionInputTable.$inferSelect): Info => {
|
|||
type: "synthetic",
|
||||
data: decodeSynthetic(row.data),
|
||||
delivery: row.delivery,
|
||||
...(row.promoted_seq === null ? {} : { promotedSeq: row.promoted_seq }),
|
||||
})
|
||||
throw new LifecycleConflict({ id: base.id })
|
||||
}
|
||||
|
||||
export const find = Effect.fn("SessionInput.find")(function* (db: DatabaseService, id: SessionMessage.ID) {
|
||||
const row = yield* db.select().from(SessionInputTable).where(eq(SessionInputTable.id, id)).get().pipe(Effect.orDie)
|
||||
export const find = Effect.fn("SessionPending.find")(function* (db: DatabaseService, id: SessionMessage.ID) {
|
||||
const row = yield* db
|
||||
.select()
|
||||
.from(SessionPendingTable)
|
||||
.where(eq(SessionPendingTable.id, id))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
return row === undefined ? undefined : fromRow(row)
|
||||
})
|
||||
|
||||
export const pendingCompaction = Effect.fn("SessionInput.pendingCompaction")(function* (
|
||||
export const compaction = Effect.fn("SessionPending.compaction")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
) {
|
||||
const row = yield* db
|
||||
.select()
|
||||
.from(SessionInputTable)
|
||||
.where(
|
||||
and(
|
||||
eq(SessionInputTable.session_id, sessionID),
|
||||
eq(SessionInputTable.type, "compaction"),
|
||||
isNull(SessionInputTable.promoted_seq),
|
||||
),
|
||||
)
|
||||
.orderBy(asc(SessionInputTable.admitted_seq))
|
||||
.from(SessionPendingTable)
|
||||
.where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.type, "compaction")))
|
||||
.orderBy(asc(SessionPendingTable.admitted_seq))
|
||||
.limit(1)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
|
|
@ -95,7 +97,51 @@ export const pendingCompaction = Effect.fn("SessionInput.pendingCompaction")(fun
|
|||
return entry.type === "compaction" ? entry : undefined
|
||||
})
|
||||
|
||||
export const admit = Effect.fn("SessionInput.admit")(function* (
|
||||
/**
|
||||
* Reconstruct the admitted record for a pending row that was already consumed
|
||||
* by promotion. The projected `session_message` row proves promotion happened;
|
||||
* the durable `session.input.admitted` event retains the exact admitted
|
||||
* message, including delivery.
|
||||
*/
|
||||
const promotedFromHistory = Effect.fn("SessionPending.promotedFromHistory")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
id: SessionMessage.ID,
|
||||
) {
|
||||
const message = yield* db
|
||||
.select()
|
||||
.from(SessionMessageTable)
|
||||
.where(eq(SessionMessageTable.id, id))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (message === undefined) return undefined
|
||||
if (message.session_id !== sessionID || (message.type !== "user" && message.type !== "synthetic"))
|
||||
return yield* Effect.die(new LifecycleConflict({ id }))
|
||||
const rows = yield* db
|
||||
.select()
|
||||
.from(EventTable)
|
||||
.where(and(eq(EventTable.aggregate_id, sessionID), eq(EventTable.type, admittedEventType)))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
for (const row of rows) {
|
||||
const decoded = decodeAdmittedEvent(row.data)
|
||||
if (decoded._tag !== "Some" || decoded.value.inputID !== id) continue
|
||||
const base = {
|
||||
admittedSeq: row.seq,
|
||||
id,
|
||||
sessionID,
|
||||
timeCreated: DateTime.makeUnsafe(row.created),
|
||||
}
|
||||
return decoded.value.input.type === "user"
|
||||
? User.make({ ...base, ...decoded.value.input })
|
||||
: Synthetic.make({ ...base, ...decoded.value.input })
|
||||
}
|
||||
// A projected message without an admitted event in this aggregate (for
|
||||
// example fork-copied history) is not a retryable admission.
|
||||
return yield* Effect.die(new LifecycleConflict({ id }))
|
||||
})
|
||||
|
||||
export const admit = Effect.fn("SessionPending.admit")(function* (
|
||||
db: DatabaseService,
|
||||
events: EventV2.Interface,
|
||||
request: {
|
||||
|
|
@ -109,6 +155,8 @@ export const admit = Effect.fn("SessionInput.admit")(function* (
|
|||
if (existing.type === "compaction") return yield* Effect.die(new LifecycleConflict({ id: request.id }))
|
||||
return existing
|
||||
}
|
||||
const promoted = yield* promotedFromHistory(db, request.sessionID, request.id)
|
||||
if (promoted !== undefined) return promoted
|
||||
return yield* events
|
||||
.publish(SessionEvent.InputAdmitted, {
|
||||
inputID: request.id,
|
||||
|
|
@ -141,7 +189,7 @@ export const admit = Effect.fn("SessionInput.admit")(function* (
|
|||
)
|
||||
})
|
||||
|
||||
export const admitCompaction = Effect.fn("SessionInput.admitCompaction")(function* (
|
||||
export const admitCompaction = Effect.fn("SessionPending.admitCompaction")(function* (
|
||||
db: DatabaseService,
|
||||
events: EventV2.Interface,
|
||||
input: { readonly id: SessionMessage.ID; readonly sessionID: SessionSchema.ID },
|
||||
|
|
@ -153,7 +201,7 @@ export const admitCompaction = Effect.fn("SessionInput.admitCompaction")(functio
|
|||
if (exact.type === "compaction" && exact.sessionID === input.sessionID) return exact
|
||||
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
}
|
||||
const pending = yield* pendingCompaction(db, input.sessionID)
|
||||
const pending = yield* compaction(db, input.sessionID)
|
||||
if (pending) return pending
|
||||
return yield* events
|
||||
.publish(SessionEvent.Compaction.Admitted, {
|
||||
|
|
@ -164,14 +212,14 @@ export const admitCompaction = Effect.fn("SessionInput.admitCompaction")(functio
|
|||
Effect.flatMap((event) => {
|
||||
if (event.durable === undefined)
|
||||
return Effect.die(new Error("Compaction admission event is missing aggregate sequence"))
|
||||
return pendingCompaction(db, input.sessionID).pipe(
|
||||
return compaction(db, input.sessionID).pipe(
|
||||
Effect.flatMap((stored) =>
|
||||
stored ? Effect.succeed(stored) : Effect.die(new LifecycleConflict({ id: input.id })),
|
||||
),
|
||||
)
|
||||
}),
|
||||
Effect.catchDefect((defect) =>
|
||||
pendingCompaction(db, input.sessionID).pipe(
|
||||
compaction(db, input.sessionID).pipe(
|
||||
Effect.flatMap((stored) => (stored ? Effect.succeed(stored) : Effect.die(defect))),
|
||||
),
|
||||
),
|
||||
|
|
@ -180,7 +228,7 @@ export const admitCompaction = Effect.fn("SessionInput.admitCompaction")(functio
|
|||
)
|
||||
})
|
||||
|
||||
export const projectAdmitted = Effect.fn("SessionInput.projectAdmitted")(function* (
|
||||
export const projectAdmitted = Effect.fn("SessionPending.projectAdmitted")(function* (
|
||||
db: DatabaseService,
|
||||
request: {
|
||||
readonly admittedSeq: number
|
||||
|
|
@ -198,7 +246,7 @@ export const projectAdmitted = Effect.fn("SessionInput.projectAdmitted")(functio
|
|||
.pipe(Effect.orDie)
|
||||
if (message !== undefined) return yield* Effect.die(new LifecycleConflict({ id: request.id }))
|
||||
const stored = yield* db
|
||||
.insert(SessionInputTable)
|
||||
.insert(SessionPendingTable)
|
||||
.values({
|
||||
id: request.id,
|
||||
session_id: request.sessionID,
|
||||
|
|
@ -209,13 +257,13 @@ export const projectAdmitted = Effect.fn("SessionInput.projectAdmitted")(functio
|
|||
time_created: DateTime.toEpochMillis(request.timeCreated),
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.returning({ id: SessionInputTable.id })
|
||||
.returning({ id: SessionPendingTable.id })
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!stored) return yield* Effect.die(new LifecycleConflict({ id: request.id }))
|
||||
})
|
||||
|
||||
export const projectCompactionAdmitted = Effect.fn("SessionInput.projectCompactionAdmitted")(function* (
|
||||
export const projectCompactionAdmitted = Effect.fn("SessionPending.projectCompactionAdmitted")(function* (
|
||||
db: DatabaseService,
|
||||
input: {
|
||||
readonly admittedSeq: number
|
||||
|
|
@ -232,7 +280,7 @@ export const projectCompactionAdmitted = Effect.fn("SessionInput.projectCompacti
|
|||
.pipe(Effect.orDie)
|
||||
if (message !== undefined) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
const stored = yield* db
|
||||
.insert(SessionInputTable)
|
||||
.insert(SessionPendingTable)
|
||||
.values({
|
||||
id: input.id,
|
||||
session_id: input.sessionID,
|
||||
|
|
@ -249,84 +297,74 @@ export const projectCompactionAdmitted = Effect.fn("SessionInput.projectCompacti
|
|||
const entry = fromRow(stored)
|
||||
return entry.type === "compaction" ? entry : yield* Effect.die(new LifecycleConflict({ id: entry.id }))
|
||||
}
|
||||
const pending = yield* pendingCompaction(db, input.sessionID)
|
||||
const pending = yield* compaction(db, input.sessionID)
|
||||
if (pending) return pending
|
||||
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
})
|
||||
|
||||
export const projectPromoted = Effect.fn("SessionInput.projectPromoted")(function* (
|
||||
/**
|
||||
* Consume one pending row at promotion. The row's content feeds the projected
|
||||
* message insert inside the same event transaction; the deleted row is what
|
||||
* makes the table pending-only.
|
||||
*/
|
||||
export const projectPromoted = Effect.fn("SessionPending.projectPromoted")(function* (
|
||||
db: DatabaseService,
|
||||
input: {
|
||||
readonly id: SessionMessage.ID
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly promotedSeq: number
|
||||
},
|
||||
) {
|
||||
if (yield* pendingCompaction(db, input.sessionID)) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
const updated = yield* db
|
||||
.update(SessionInputTable)
|
||||
.set({ promoted_seq: input.promotedSeq })
|
||||
.where(
|
||||
and(
|
||||
eq(SessionInputTable.id, input.id),
|
||||
eq(SessionInputTable.session_id, input.sessionID),
|
||||
isNull(SessionInputTable.promoted_seq),
|
||||
),
|
||||
)
|
||||
if (yield* compaction(db, input.sessionID)) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
const deleted = yield* db
|
||||
.delete(SessionPendingTable)
|
||||
.where(and(eq(SessionPendingTable.id, input.id), eq(SessionPendingTable.session_id, input.sessionID)))
|
||||
.returning()
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
const stored = updated ? fromRow(updated) : yield* find(db, input.id)
|
||||
if (
|
||||
!stored ||
|
||||
stored.type === "compaction" ||
|
||||
stored.sessionID !== input.sessionID ||
|
||||
stored.promotedSeq !== input.promotedSeq
|
||||
)
|
||||
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
if (!deleted) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
const stored = fromRow(deleted)
|
||||
if (stored.type === "compaction") return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
return stored
|
||||
})
|
||||
|
||||
export const settleCompaction = Effect.fn("SessionInput.settleCompaction")(function* (
|
||||
export const settleCompaction = Effect.fn("SessionPending.settleCompaction")(function* (
|
||||
db: DatabaseService,
|
||||
input: { readonly sessionID: SessionSchema.ID; readonly handledSeq: number },
|
||||
input: { readonly sessionID: SessionSchema.ID },
|
||||
) {
|
||||
const updated = yield* db
|
||||
.update(SessionInputTable)
|
||||
.set({ promoted_seq: input.handledSeq })
|
||||
.where(
|
||||
and(
|
||||
eq(SessionInputTable.session_id, input.sessionID),
|
||||
eq(SessionInputTable.type, "compaction"),
|
||||
isNull(SessionInputTable.promoted_seq),
|
||||
),
|
||||
)
|
||||
const deleted = yield* db
|
||||
.delete(SessionPendingTable)
|
||||
.where(and(eq(SessionPendingTable.session_id, input.sessionID), eq(SessionPendingTable.type, "compaction")))
|
||||
.returning()
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (updated) {
|
||||
const stored = fromRow(updated)
|
||||
if (deleted) {
|
||||
const stored = fromRow(deleted)
|
||||
return stored.type === "compaction" ? stored : yield* Effect.die(new LifecycleConflict({ id: stored.id }))
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
export const hasPending = Effect.fn("SessionInput.hasPending")(function* (
|
||||
export const list = Effect.fn("SessionPending.list")(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
|
||||
const rows = yield* db
|
||||
.select()
|
||||
.from(SessionPendingTable)
|
||||
.where(eq(SessionPendingTable.session_id, sessionID))
|
||||
.orderBy(asc(SessionPendingTable.admitted_seq))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
return rows.map(fromRow)
|
||||
})
|
||||
|
||||
export const has = Effect.fn("SessionPending.has")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
delivery: Delivery,
|
||||
) {
|
||||
if (yield* pendingCompaction(db, sessionID)) return false
|
||||
if (yield* compaction(db, sessionID)) return false
|
||||
const row = yield* db
|
||||
.select({ id: SessionInputTable.id })
|
||||
.from(SessionInputTable)
|
||||
.where(
|
||||
and(
|
||||
eq(SessionInputTable.session_id, sessionID),
|
||||
isNull(SessionInputTable.promoted_seq),
|
||||
eq(SessionInputTable.delivery, delivery),
|
||||
),
|
||||
)
|
||||
.select({ id: SessionPendingTable.id })
|
||||
.from(SessionPendingTable)
|
||||
.where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.delivery, delivery)))
|
||||
.limit(1)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
|
|
@ -350,15 +388,15 @@ export const equivalent = (
|
|||
return false
|
||||
}
|
||||
|
||||
const publish = Effect.fn("SessionInput.publish")(function* (
|
||||
const publish = Effect.fn("SessionPending.publish")(function* (
|
||||
db: DatabaseService,
|
||||
events: EventV2.Interface,
|
||||
sessionID: SessionSchema.ID,
|
||||
rows: ReadonlyArray<typeof SessionInputTable.$inferSelect>,
|
||||
rows: ReadonlyArray<typeof SessionPendingTable.$inferSelect>,
|
||||
) {
|
||||
return yield* inboxLocks.withLock(sessionID)(
|
||||
Effect.gen(function* () {
|
||||
if (yield* pendingCompaction(db, sessionID)) return 0
|
||||
if (yield* compaction(db, sessionID)) return 0
|
||||
yield* Effect.forEach(
|
||||
rows,
|
||||
(row) => {
|
||||
|
|
@ -372,12 +410,8 @@ const publish = Effect.fn("SessionInput.publish")(function* (
|
|||
.pipe(
|
||||
Effect.catchDefect((defect) =>
|
||||
defect instanceof LifecycleConflict
|
||||
? find(db, entry.id).pipe(
|
||||
Effect.flatMap((stored) =>
|
||||
stored?.type !== "compaction" && stored?.promotedSeq !== undefined
|
||||
? Effect.void
|
||||
: Effect.die(defect),
|
||||
),
|
||||
? promotedFromHistory(db, sessionID, entry.id).pipe(
|
||||
Effect.flatMap((stored) => (stored !== undefined ? Effect.void : Effect.die(defect))),
|
||||
)
|
||||
: Effect.die(defect),
|
||||
),
|
||||
|
|
@ -390,45 +424,33 @@ const publish = Effect.fn("SessionInput.publish")(function* (
|
|||
)
|
||||
})
|
||||
|
||||
export const promoteSteers = Effect.fn("SessionInput.promoteSteers")(function* (
|
||||
export const promoteSteers = Effect.fn("SessionPending.promoteSteers")(function* (
|
||||
db: DatabaseService,
|
||||
events: EventV2.Interface,
|
||||
sessionID: SessionSchema.ID,
|
||||
) {
|
||||
if (yield* pendingCompaction(db, sessionID)) return 0
|
||||
if (yield* compaction(db, sessionID)) return 0
|
||||
const rows = yield* db
|
||||
.select()
|
||||
.from(SessionInputTable)
|
||||
.where(
|
||||
and(
|
||||
eq(SessionInputTable.session_id, sessionID),
|
||||
isNull(SessionInputTable.promoted_seq),
|
||||
eq(SessionInputTable.delivery, "steer"),
|
||||
),
|
||||
)
|
||||
.orderBy(asc(SessionInputTable.admitted_seq))
|
||||
.from(SessionPendingTable)
|
||||
.where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.delivery, "steer")))
|
||||
.orderBy(asc(SessionPendingTable.admitted_seq))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
return yield* publish(db, events, sessionID, rows)
|
||||
})
|
||||
|
||||
export const promoteNextQueued = Effect.fn("SessionInput.promoteNextQueued")(function* (
|
||||
export const promoteNextQueued = Effect.fn("SessionPending.promoteNextQueued")(function* (
|
||||
db: DatabaseService,
|
||||
events: EventV2.Interface,
|
||||
sessionID: SessionSchema.ID,
|
||||
) {
|
||||
if (yield* pendingCompaction(db, sessionID)) return false
|
||||
if (yield* compaction(db, sessionID)) return false
|
||||
const row = yield* db
|
||||
.select()
|
||||
.from(SessionInputTable)
|
||||
.where(
|
||||
and(
|
||||
eq(SessionInputTable.session_id, sessionID),
|
||||
isNull(SessionInputTable.promoted_seq),
|
||||
eq(SessionInputTable.delivery, "queue"),
|
||||
),
|
||||
)
|
||||
.orderBy(asc(SessionInputTable.admitted_seq))
|
||||
.from(SessionPendingTable)
|
||||
.where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.delivery, "queue")))
|
||||
.orderBy(asc(SessionPendingTable.admitted_seq))
|
||||
.limit(1)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
export * as SessionProjector from "./projector"
|
||||
|
||||
import { and, asc, desc, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm"
|
||||
import { and, asc, desc, eq, gt, gte, inArray, lt, sql } from "drizzle-orm"
|
||||
import { DateTime, Effect, Layer, Schema, Stream } from "effect"
|
||||
import { Database } from "../database/database"
|
||||
import { EventV2 } from "../event"
|
||||
|
|
@ -11,14 +11,14 @@ import { SessionV1 } from "../v1/session"
|
|||
import { WorkspaceTable } from "../control-plane/workspace.sql"
|
||||
import { SessionMessage } from "./message"
|
||||
import { SessionMessageUpdater } from "./message-updater"
|
||||
import { SessionInput } from "./input"
|
||||
import { SessionPending } from "./pending"
|
||||
import { WorkspaceV2 } from "../workspace"
|
||||
import { InstructionCheckpoint } from "./instruction-checkpoint"
|
||||
import {
|
||||
MessageTable,
|
||||
PartTable,
|
||||
InstructionCheckpointTable,
|
||||
SessionInputTable,
|
||||
SessionPendingTable,
|
||||
SessionMessageTable,
|
||||
SessionTable,
|
||||
} from "./sql"
|
||||
|
|
@ -293,25 +293,25 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
|||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
const inputRows = yield* db
|
||||
const pendingRows = yield* db
|
||||
.select()
|
||||
.from(SessionInputTable)
|
||||
.from(SessionPendingTable)
|
||||
.where(
|
||||
and(
|
||||
eq(SessionInputTable.session_id, event.data.parentID),
|
||||
eq(SessionPendingTable.session_id, event.data.parentID),
|
||||
inArray(
|
||||
SessionInputTable.id,
|
||||
SessionPendingTable.id,
|
||||
rows.map((row) => row.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
if (inputRows.length > 0) {
|
||||
if (pendingRows.length > 0) {
|
||||
yield* db
|
||||
.insert(SessionInputTable)
|
||||
.insert(SessionPendingTable)
|
||||
.values(
|
||||
inputRows.flatMap((row) => {
|
||||
pendingRows.flatMap((row) => {
|
||||
const id = idMap.get(row.id)
|
||||
return id && row.type !== "compaction"
|
||||
? [
|
||||
|
|
@ -322,7 +322,6 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
|||
data: row.data,
|
||||
delivery: row.delivery,
|
||||
admitted_seq: row.admitted_seq,
|
||||
promoted_seq: row.promoted_seq,
|
||||
time_created: row.time_created,
|
||||
},
|
||||
]
|
||||
|
|
@ -633,10 +632,9 @@ const layer = Layer.effectDiscard(
|
|||
Effect.gen(function* () {
|
||||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
const input = yield* SessionInput.projectPromoted(db, {
|
||||
const input = yield* SessionPending.projectPromoted(db, {
|
||||
id: event.data.inputID,
|
||||
sessionID: event.data.sessionID,
|
||||
promotedSeq: event.durable.seq,
|
||||
})
|
||||
yield* insertMessage(
|
||||
db,
|
||||
|
|
@ -666,7 +664,7 @@ const layer = Layer.effectDiscard(
|
|||
Effect.gen(function* () {
|
||||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
yield* SessionInput.projectAdmitted(db, {
|
||||
yield* SessionPending.projectAdmitted(db, {
|
||||
admittedSeq: event.durable.seq,
|
||||
id: event.data.inputID,
|
||||
sessionID: event.data.sessionID,
|
||||
|
|
@ -679,7 +677,7 @@ const layer = Layer.effectDiscard(
|
|||
Effect.gen(function* () {
|
||||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
yield* SessionInput.projectCompactionAdmitted(db, {
|
||||
yield* SessionPending.projectCompactionAdmitted(db, {
|
||||
admittedSeq: event.durable.seq,
|
||||
id: event.data.inputID,
|
||||
sessionID: event.data.sessionID,
|
||||
|
|
@ -727,10 +725,7 @@ const layer = Layer.effectDiscard(
|
|||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
if (event.data.reason === "manual")
|
||||
yield* SessionInput.settleCompaction(db, {
|
||||
sessionID: event.data.sessionID,
|
||||
handledSeq: event.durable.seq,
|
||||
})
|
||||
yield* SessionPending.settleCompaction(db, { sessionID: event.data.sessionID })
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.Compaction.Failed, (event) =>
|
||||
|
|
@ -739,10 +734,7 @@ const layer = Layer.effectDiscard(
|
|||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
if (event.data.reason === "manual")
|
||||
yield* SessionInput.settleCompaction(db, {
|
||||
sessionID: event.data.sessionID,
|
||||
handledSeq: event.durable.seq,
|
||||
})
|
||||
yield* SessionPending.settleCompaction(db, { sessionID: event.data.sessionID })
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.RevertEvent.Staged, (event) =>
|
||||
|
|
@ -786,11 +778,11 @@ const layer = Layer.effectDiscard(
|
|||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* db
|
||||
.delete(SessionInputTable)
|
||||
.delete(SessionPendingTable)
|
||||
.where(
|
||||
and(
|
||||
eq(SessionInputTable.session_id, event.data.sessionID),
|
||||
or(gte(SessionInputTable.admitted_seq, boundary.seq), gte(SessionInputTable.promoted_seq, boundary.seq)),
|
||||
eq(SessionPendingTable.session_id, event.data.sessionID),
|
||||
gte(SessionPendingTable.admitted_seq, boundary.seq),
|
||||
),
|
||||
)
|
||||
.run()
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import { InstructionCheckpoint } from "../instruction-checkpoint"
|
|||
import { SessionCompaction } from "../compaction"
|
||||
import { SessionEvent } from "../event"
|
||||
import { SessionHistory } from "../history"
|
||||
import { SessionInput } from "../input"
|
||||
import { SessionPending } from "../pending"
|
||||
import { SessionMessage } from "../message"
|
||||
import { SessionSchema } from "../schema"
|
||||
import { SessionStore } from "../store"
|
||||
|
|
@ -203,7 +203,7 @@ const layer = Layer.effect(
|
|||
|
||||
const attemptStep = Effect.fn("SessionRunner.attemptStep")(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
promotion: SessionInput.Delivery | undefined,
|
||||
promotion: SessionPending.Delivery | undefined,
|
||||
step: number,
|
||||
recoverOverflow?: typeof compaction.compactAfterOverflow,
|
||||
assistantMessageID?: SessionMessage.ID,
|
||||
|
|
@ -226,10 +226,10 @@ const layer = Layer.effect(
|
|||
let currentStep = step
|
||||
if (promotion) {
|
||||
let promoted = 0
|
||||
if (promotion === "steer") promoted = yield* SessionInput.promoteSteers(db, events, session.id)
|
||||
if (promotion === "steer") promoted = yield* SessionPending.promoteSteers(db, events, session.id)
|
||||
if (promotion === "queue") {
|
||||
promoted += Number(yield* SessionInput.promoteNextQueued(db, events, session.id))
|
||||
promoted += yield* SessionInput.promoteSteers(db, events, session.id)
|
||||
promoted += Number(yield* SessionPending.promoteNextQueued(db, events, session.id))
|
||||
promoted += yield* SessionPending.promoteSteers(db, events, session.id)
|
||||
}
|
||||
if (promoted > 0) currentStep = 1
|
||||
}
|
||||
|
|
@ -285,7 +285,7 @@ const layer = Layer.effect(
|
|||
const advertisedTools = new Set(hookedRequest.tools.map((tool) => tool.name))
|
||||
// Automatic compaction completed; rebuild the request from compacted history.
|
||||
if (
|
||||
!(yield* SessionInput.pendingCompaction(db, session.id)) &&
|
||||
!(yield* SessionPending.compaction(db, session.id)) &&
|
||||
(yield* compaction.compactIfNeeded({ sessionID: session.id, messages: context, request: hookedRequest }))
|
||||
)
|
||||
return { _tag: "RestartAfterCompaction", step: currentStep } as const
|
||||
|
|
@ -550,7 +550,7 @@ const layer = Layer.effect(
|
|||
|
||||
const runStep = Effect.fnUntraced(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
promotion: SessionInput.Delivery | undefined,
|
||||
promotion: SessionPending.Delivery | undefined,
|
||||
step: number,
|
||||
) {
|
||||
// Compaction restarts rebuild the request from compacted history without re-promoting.
|
||||
|
|
@ -595,7 +595,7 @@ const layer = Layer.effect(
|
|||
const runPendingCompaction = Effect.fn("SessionRunner.runPendingCompaction")(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
) {
|
||||
const pending = yield* SessionInput.pendingCompaction(db, sessionID)
|
||||
const pending = yield* SessionPending.compaction(db, sessionID)
|
||||
if (!pending) return false
|
||||
const session = yield* getSession(sessionID)
|
||||
return yield* Effect.uninterruptibleMask((restore) =>
|
||||
|
|
@ -611,7 +611,7 @@ const layer = Layer.effect(
|
|||
).pipe(Effect.exit)
|
||||
if (Exit.isSuccess(compacted) && compacted.value) return true
|
||||
if (Exit.isFailure(compacted)) {
|
||||
const unsettled = yield* SessionInput.pendingCompaction(db, sessionID)
|
||||
const unsettled = yield* SessionPending.compaction(db, sessionID)
|
||||
if (unsettled)
|
||||
yield* events.publish(SessionEvent.Compaction.Failed, {
|
||||
sessionID,
|
||||
|
|
@ -621,7 +621,7 @@ const layer = Layer.effect(
|
|||
})
|
||||
return yield* Effect.failCause(compacted.cause)
|
||||
}
|
||||
const unsettled = yield* SessionInput.pendingCompaction(db, sessionID)
|
||||
const unsettled = yield* SessionPending.compaction(db, sessionID)
|
||||
if (unsettled)
|
||||
yield* events.publish(SessionEvent.Compaction.Failed, {
|
||||
sessionID,
|
||||
|
|
@ -640,11 +640,11 @@ const layer = Layer.effect(
|
|||
readonly force: boolean
|
||||
}) {
|
||||
yield* runPendingCompaction(input.sessionID)
|
||||
const hasSteer = yield* SessionInput.hasPending(db, input.sessionID, "steer")
|
||||
const hasQueue = hasSteer ? false : yield* SessionInput.hasPending(db, input.sessionID, "queue")
|
||||
const hasSteer = yield* SessionPending.has(db, input.sessionID, "steer")
|
||||
const hasQueue = hasSteer ? false : yield* SessionPending.has(db, input.sessionID, "queue")
|
||||
if (!input.force && !hasSteer && !hasQueue) return
|
||||
yield* failInterruptedTools(input.sessionID)
|
||||
let promotion: SessionInput.Delivery | undefined = hasSteer ? "steer" : hasQueue ? "queue" : undefined
|
||||
let promotion: SessionPending.Delivery | undefined = hasSteer ? "steer" : hasQueue ? "queue" : undefined
|
||||
let shouldRun = input.force || hasSteer || hasQueue
|
||||
while (shouldRun) {
|
||||
let needsContinuation = true
|
||||
|
|
@ -664,16 +664,16 @@ const layer = Layer.effect(
|
|||
needsContinuation = result.needsContinuation
|
||||
step = result.step + 1
|
||||
if (needsContinuation) {
|
||||
promotion = (yield* SessionInput.pendingCompaction(db, input.sessionID)) ? undefined : "steer"
|
||||
promotion = (yield* SessionPending.compaction(db, input.sessionID)) ? undefined : "steer"
|
||||
continue
|
||||
}
|
||||
yield* runPendingCompaction(input.sessionID)
|
||||
promotion = "steer"
|
||||
needsContinuation = yield* SessionInput.hasPending(db, input.sessionID, "steer")
|
||||
needsContinuation = yield* SessionPending.has(db, input.sessionID, "steer")
|
||||
}
|
||||
yield* runPendingCompaction(input.sessionID)
|
||||
const hasSteer = yield* SessionInput.hasPending(db, input.sessionID, "steer")
|
||||
const hasQueue = hasSteer ? false : yield* SessionInput.hasPending(db, input.sessionID, "queue")
|
||||
const hasSteer = yield* SessionPending.has(db, input.sessionID, "steer")
|
||||
const hasQueue = hasSteer ? false : yield* SessionPending.has(db, input.sessionID, "queue")
|
||||
shouldRun = hasSteer || hasQueue
|
||||
promotion = hasSteer ? "steer" : hasQueue ? "queue" : undefined
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { sql } from "drizzle-orm"
|
|||
import { directoryColumn, pathColumn } from "../database/path"
|
||||
import { ProjectTable } from "../project/sql"
|
||||
import type { SessionMessage } from "./message"
|
||||
import type { SessionInput } from "./input"
|
||||
import type { SessionPending } from "./pending"
|
||||
import type { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||
import { PermissionV1 } from "../v1/permission"
|
||||
import { ProjectV2 } from "../project"
|
||||
|
|
@ -13,7 +13,7 @@ import { WorkspaceV2 } from "../workspace"
|
|||
import { Timestamps } from "../database/schema.sql"
|
||||
import type { Instructions } from "../instructions/index"
|
||||
import type { Session } from "@opencode-ai/schema/session"
|
||||
import type { SyntheticData, UserData } from "@opencode-ai/schema/session-input"
|
||||
import type { SyntheticData, UserData } from "@opencode-ai/schema/session-pending"
|
||||
import type { RevertV1 } from "@opencode-ai/schema/session-revert"
|
||||
import type { Schema } from "effect"
|
||||
|
||||
|
|
@ -126,35 +126,28 @@ export const SessionMessageTable = sqliteTable(
|
|||
],
|
||||
)
|
||||
|
||||
export const SessionInputTable = sqliteTable(
|
||||
"session_input",
|
||||
export const SessionPendingTable = sqliteTable(
|
||||
"session_pending",
|
||||
{
|
||||
id: text().$type<SessionMessage.ID>().primaryKey(),
|
||||
session_id: text()
|
||||
.$type<SessionSchema.ID>()
|
||||
.notNull()
|
||||
.references(() => SessionTable.id, { onDelete: "cascade" }),
|
||||
type: text().$type<SessionInput.Info["type"]>().notNull(),
|
||||
type: text().$type<SessionPending.Info["type"]>().notNull(),
|
||||
data: text({ mode: "json" }).$type<UserData | SyntheticData | Record<string, never>>().notNull(),
|
||||
delivery: text().$type<SessionInput.Delivery>(),
|
||||
delivery: text().$type<SessionPending.Delivery>(),
|
||||
admitted_seq: integer().notNull(),
|
||||
promoted_seq: integer(),
|
||||
time_created: integer()
|
||||
.notNull()
|
||||
.$default(() => Date.now()),
|
||||
},
|
||||
(table) => [
|
||||
index("session_input_session_pending_delivery_seq_idx").on(
|
||||
table.session_id,
|
||||
table.promoted_seq,
|
||||
table.delivery,
|
||||
table.admitted_seq,
|
||||
),
|
||||
uniqueIndex("session_input_session_pending_compaction_idx")
|
||||
index("session_pending_session_delivery_seq_idx").on(table.session_id, table.delivery, table.admitted_seq),
|
||||
uniqueIndex("session_pending_session_compaction_idx")
|
||||
.on(table.session_id)
|
||||
.where(sql`${table.type} = 'compaction' and ${table.promoted_seq} is null`),
|
||||
uniqueIndex("session_input_session_admitted_seq_idx").on(table.session_id, table.admitted_seq),
|
||||
uniqueIndex("session_input_session_promoted_seq_idx").on(table.session_id, table.promoted_seq),
|
||||
.where(sql`${table.type} = 'compaction'`),
|
||||
uniqueIndex("session_pending_session_admitted_seq_idx").on(table.session_id, table.admitted_seq),
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue