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
|
|
@ -11,14 +11,15 @@ import { migrations } from "@opencode-ai/core/database/migration.gen"
|
|||
import sessionUsageMigration from "@opencode-ai/core/database/migration/20260510033149_session_usage"
|
||||
import normalizeStoragePathsMigration from "@opencode-ai/core/database/migration/20260601010001_normalize_storage_paths"
|
||||
import sessionMessageProjectionOrderMigration from "@opencode-ai/core/database/migration/20260603040000_session_message_projection_order"
|
||||
import eventSourcedSessionInputMigration from "@opencode-ai/core/database/migration/20260604172448_event_sourced_session_input"
|
||||
import eventSourcedSessionPendingMigration 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 simplifySessionInputMigration from "@opencode-ai/core/database/migration/20260622202450_simplify_session_input"
|
||||
import simplifySessionPendingMigration from "@opencode-ai/core/database/migration/20260622202450_simplify_session_input"
|
||||
import resetSessionEventsMigration from "@opencode-ai/core/database/migration/20260703200000_reset_v2_session_events"
|
||||
import durableSessionInboxMigration from "@opencode-ai/core/database/migration/20260707010146_durable_session_inbox"
|
||||
import migratePrelaunchV2StateMigration from "@opencode-ai/core/database/migration/20260707120000_migrate_prelaunch_v2_state"
|
||||
import genericSessionInputMigration from "@opencode-ai/core/database/migration/20260709013000_generic_session_input"
|
||||
import genericSessionPendingMigration from "@opencode-ai/core/database/migration/20260709013000_generic_session_input"
|
||||
import sessionPendingTableMigration from "@opencode-ai/core/database/migration/20260709190621_session_pending_table"
|
||||
import renameInstructionsMigration from "@opencode-ai/core/database/migration/20260705180000_rename_instructions"
|
||||
import addSessionForkMigration from "@opencode-ai/core/database/migration/20260706223930_add-session-fork"
|
||||
import timeSuspendedMigration from "@opencode-ai/core/database/migration/20260709163752_time_suspended"
|
||||
|
|
@ -352,7 +353,10 @@ describe("DatabaseMigration", () => {
|
|||
})
|
||||
expect(
|
||||
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session_input'`),
|
||||
).toEqual({ name: "session_input" })
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session_pending'`),
|
||||
).toEqual({ name: "session_pending" })
|
||||
expect(
|
||||
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'instruction_checkpoint'`),
|
||||
).toEqual({ name: "instruction_checkpoint" })
|
||||
|
|
@ -364,18 +368,17 @@ describe("DatabaseMigration", () => {
|
|||
expect(yield* db.get(sql`SELECT count(*) as count FROM migration`)).toEqual({ count: migrations.length })
|
||||
expect(
|
||||
yield* db.all(
|
||||
sql`SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('event_aggregate_seq_idx', 'event_aggregate_type_seq_idx', 'session_input_session_pending_seq_idx', 'session_input_session_pending_delivery_seq_idx', 'session_input_session_pending_type_delivery_seq_idx', 'session_input_session_pending_compaction_idx', 'session_input_session_admitted_seq_idx', 'session_input_session_promoted_seq_idx', 'session_message_session_idx', 'session_message_session_type_idx', 'session_message_session_seq_idx', 'session_message_session_type_seq_idx', 'session_message_session_time_created_id_idx') ORDER BY name`,
|
||||
sql`SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('event_aggregate_seq_idx', 'event_aggregate_type_seq_idx', 'session_input_session_pending_seq_idx', 'session_input_session_pending_delivery_seq_idx', 'session_input_session_pending_type_delivery_seq_idx', 'session_input_session_pending_compaction_idx', 'session_input_session_admitted_seq_idx', 'session_input_session_promoted_seq_idx', 'session_pending_session_delivery_seq_idx', 'session_pending_session_compaction_idx', 'session_pending_session_admitted_seq_idx', 'session_message_session_idx', 'session_message_session_type_idx', 'session_message_session_seq_idx', 'session_message_session_type_seq_idx', 'session_message_session_time_created_id_idx') ORDER BY name`,
|
||||
),
|
||||
).toEqual([
|
||||
{ name: "event_aggregate_seq_idx" },
|
||||
{ name: "event_aggregate_type_seq_idx" },
|
||||
{ name: "session_input_session_admitted_seq_idx" },
|
||||
{ name: "session_input_session_pending_compaction_idx" },
|
||||
{ name: "session_input_session_pending_delivery_seq_idx" },
|
||||
{ name: "session_input_session_promoted_seq_idx" },
|
||||
{ name: "session_message_session_seq_idx" },
|
||||
{ name: "session_message_session_time_created_id_idx" },
|
||||
{ name: "session_message_session_type_seq_idx" },
|
||||
{ name: "session_pending_session_admitted_seq_idx" },
|
||||
{ name: "session_pending_session_compaction_idx" },
|
||||
{ name: "session_pending_session_delivery_seq_idx" },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
|
@ -568,7 +571,7 @@ describe("DatabaseMigration", () => {
|
|||
sql`INSERT INTO session_input (id, session_id, prompt, delivery, time_created) VALUES ('msg_pending', 'session', '{}', 'steer', 1)`,
|
||||
)
|
||||
|
||||
yield* DatabaseMigration.applyOnly(db, [eventSourcedSessionInputMigration])
|
||||
yield* DatabaseMigration.applyOnly(db, [eventSourcedSessionPendingMigration])
|
||||
|
||||
expect(yield* db.all(sql`SELECT id, workspace_id FROM session`)).toEqual([
|
||||
{ id: "session", workspace_id: null },
|
||||
|
|
@ -631,7 +634,7 @@ describe("DatabaseMigration", () => {
|
|||
sql`INSERT INTO event (id, aggregate_id, seq, type, data, created) VALUES ('event', 'session', 9, 'session.updated.1', '{}', 1)`,
|
||||
)
|
||||
yield* db.run(
|
||||
sql`INSERT INTO session_input (id, session_id, type, data, delivery, admitted_seq, time_created) VALUES ('input', 'session', 'user', '{}', 'steer', 9, 1)`,
|
||||
sql`INSERT INTO session_pending (id, session_id, type, data, delivery, admitted_seq, time_created) VALUES ('input', 'session', 'user', '{}', 'steer', 9, 1)`,
|
||||
)
|
||||
yield* db.run(
|
||||
sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('projected', 'session', 'user', 9, 1, 1, '{}')`,
|
||||
|
|
@ -640,9 +643,17 @@ describe("DatabaseMigration", () => {
|
|||
sql`INSERT INTO instruction_checkpoint (session_id, baseline, snapshot, baseline_seq) VALUES ('session', 'baseline', '{}', 9)`,
|
||||
)
|
||||
yield* db.run(sql`ALTER TABLE instruction_checkpoint RENAME TO session_context_epoch`)
|
||||
yield* db.run(sql`DELETE FROM migration WHERE id = ${simplifySessionInputMigration.id}`)
|
||||
yield* DatabaseMigration.applyOnly(db, [simplifySessionInputMigration])
|
||||
// The partial compaction index embeds the qualified table name, so it
|
||||
// must drop before the historical rename dance and recreate after.
|
||||
yield* db.run(sql`DROP INDEX session_pending_session_compaction_idx`)
|
||||
yield* db.run(sql`ALTER TABLE session_pending RENAME TO session_input`)
|
||||
yield* db.run(sql`DELETE FROM migration WHERE id = ${simplifySessionPendingMigration.id}`)
|
||||
yield* DatabaseMigration.applyOnly(db, [simplifySessionPendingMigration])
|
||||
yield* db.run(sql`ALTER TABLE session_context_epoch RENAME TO instruction_checkpoint`)
|
||||
yield* db.run(sql`ALTER TABLE session_input RENAME TO session_pending`)
|
||||
yield* db.run(
|
||||
sql`CREATE UNIQUE INDEX session_pending_session_compaction_idx ON session_pending (session_id) WHERE "session_pending"."type" = 'compaction'`,
|
||||
)
|
||||
|
||||
const database = Layer.succeed(Database.Service, { db })
|
||||
yield* EventV2.Service.use((service) =>
|
||||
|
|
@ -672,7 +683,7 @@ describe("DatabaseMigration", () => {
|
|||
(SELECT COUNT(*) FROM message WHERE id = 'message') AS messages,
|
||||
(SELECT COUNT(*) FROM part WHERE id = 'part') AS parts,
|
||||
(SELECT COUNT(*) FROM workspace) AS workspaces,
|
||||
(SELECT COUNT(*) FROM session_input) AS sessionInputs,
|
||||
(SELECT COUNT(*) FROM session_pending) AS sessionInputs,
|
||||
(SELECT COUNT(*) FROM session_message) AS sessionMessages,
|
||||
(SELECT COUNT(*) FROM instruction_checkpoint) AS instructionCheckpoints,
|
||||
(SELECT seq FROM event_sequence WHERE aggregate_id = 'session') AS seq,
|
||||
|
|
@ -756,7 +767,7 @@ describe("DatabaseMigration", () => {
|
|||
sql`INSERT INTO event (id, aggregate_id, seq, created, type, data) VALUES ('empty-promoted', 'session', 7, 2, 'session.prompt.promoted.1', '{"sessionID":"session","inputID":"empty"}')`,
|
||||
)
|
||||
|
||||
yield* DatabaseMigration.applyOnly(db, [genericSessionInputMigration])
|
||||
yield* DatabaseMigration.applyOnly(db, [genericSessionPendingMigration])
|
||||
|
||||
expect(yield* db.all(sql`SELECT id, type, data, delivery FROM session_input ORDER BY admitted_seq`)).toEqual([
|
||||
{ id: "input", type: "user", data: '{"text":"hello"}', delivery: "queue" },
|
||||
|
|
@ -775,6 +786,47 @@ describe("DatabaseMigration", () => {
|
|||
)
|
||||
})
|
||||
|
||||
test("replaces the durable inbox with the empty session_pending table", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDb
|
||||
yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY)`)
|
||||
yield* db.run(sql`INSERT INTO session (id) VALUES ('session')`)
|
||||
yield* db.run(
|
||||
sql`CREATE TABLE session_input (id text PRIMARY KEY, session_id text NOT NULL REFERENCES session(id) ON DELETE CASCADE, type text NOT NULL, data text NOT NULL, delivery text, admitted_seq integer NOT NULL, promoted_seq integer, time_created integer NOT NULL)`,
|
||||
)
|
||||
// Interim v2 builds shipped differing index sets on real databases;
|
||||
// dropping the table removes whatever variant exists.
|
||||
yield* db.run(
|
||||
sql`CREATE INDEX session_input_session_pending_type_delivery_seq_idx ON session_input (session_id, promoted_seq, type, delivery, admitted_seq)`,
|
||||
)
|
||||
yield* db.run(
|
||||
sql`INSERT INTO session_input (id, session_id, type, data, delivery, admitted_seq, promoted_seq, time_created) VALUES ('pending', 'session', 'user', '{"text":"hello"}', 'steer', 4, NULL, 1)`,
|
||||
)
|
||||
|
||||
yield* DatabaseMigration.applyOnly(db, [sessionPendingTableMigration])
|
||||
|
||||
expect(
|
||||
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session_input'`),
|
||||
).toBeUndefined()
|
||||
expect(yield* db.all(sql`SELECT id FROM session_pending`)).toEqual([])
|
||||
expect(
|
||||
(yield* db.all<{ name: string }>(sql`PRAGMA table_info(session_pending)`)).map((column) => column.name),
|
||||
).toEqual(["id", "session_id", "type", "data", "delivery", "admitted_seq", "time_created"])
|
||||
expect(
|
||||
(yield* db.all<{ name: string; unique: number }>(sql`PRAGMA index_list(session_pending)`))
|
||||
.filter((index) => index.name.startsWith("session_"))
|
||||
.map((index) => ({ name: index.name, unique: index.unique }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
).toEqual([
|
||||
{ name: "session_pending_session_admitted_seq_idx", unique: 1 },
|
||||
{ name: "session_pending_session_compaction_idx", unique: 1 },
|
||||
{ name: "session_pending_session_delivery_seq_idx", unique: 0 },
|
||||
])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("resets incompatible projected Session messages before adding sequence order", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
|
|||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { SessionCompaction } from "@opencode-ai/core/session/compaction"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { SessionInput } from "@opencode-ai/core/session/input"
|
||||
import { SessionPending } from "@opencode-ai/core/session/pending"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
|
|
@ -106,7 +106,7 @@ describe("SessionV2.compact", () => {
|
|||
|
||||
expect(second.id).toBe(first.id)
|
||||
expect(requests).toHaveLength(0)
|
||||
expect(yield* SessionInput.pendingCompaction((yield* Database.Service).db, created.id)).toMatchObject({
|
||||
expect(yield* SessionPending.compaction((yield* Database.Service).db, created.id)).toMatchObject({
|
||||
id: first.id,
|
||||
})
|
||||
expect((yield* session.context(created.id)).find((message) => message.id === first.id)).toBeUndefined()
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { SessionV1 } from "@opencode-ai/core/v1/session"
|
|||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { SessionInput } from "@opencode-ai/core/session/input"
|
||||
import { SessionPending } from "@opencode-ai/core/session/pending"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
|
|
@ -195,9 +195,9 @@ describe("SessionV2.create", () => {
|
|||
text: "First",
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInput.promoteSteers(db, events, parent.id)
|
||||
yield* SessionPending.promoteSteers(db, events, parent.id)
|
||||
yield* session.synthetic({ sessionID: parent.id, text: "parent note", resume: false })
|
||||
yield* SessionInput.promoteSteers(db, events, parent.id)
|
||||
yield* SessionPending.promoteSteers(db, events, parent.id)
|
||||
|
||||
const forked = yield* session.fork({ sessionID: parent.id })
|
||||
const parentContext = yield* session.context(parent.id)
|
||||
|
|
@ -217,30 +217,28 @@ describe("SessionV2.create", () => {
|
|||
durable: { seq: 0 },
|
||||
data: { sessionID: forked.id, parentID: parent.id },
|
||||
})
|
||||
expect(yield* SessionInput.find(db, forkContext[0].id)).toMatchObject({
|
||||
sessionID: forked.id,
|
||||
type: "user",
|
||||
data: { text: "First" },
|
||||
promotedSeq: 2,
|
||||
})
|
||||
expect(yield* SessionInput.find(db, forkContext[1].id)).toMatchObject({
|
||||
sessionID: forked.id,
|
||||
type: "synthetic",
|
||||
data: { text: "parent note" },
|
||||
})
|
||||
expect(yield* SessionPending.find(db, forkContext[0].id)).toBeUndefined()
|
||||
expect(yield* SessionPending.find(db, forkContext[1].id)).toBeUndefined()
|
||||
// Fork-copied messages have no admitted event in the fork aggregate, so
|
||||
// reusing their IDs as prompt IDs is conflicting reuse, not a retry.
|
||||
expect(
|
||||
yield* session
|
||||
.prompt({ id: forkContext[0].id, sessionID: forked.id, text: "First", resume: false })
|
||||
.pipe(Effect.flip),
|
||||
).toMatchObject({ _tag: "Session.PromptConflictError", messageID: forkContext[0].id })
|
||||
|
||||
yield* session.prompt({
|
||||
sessionID: parent.id,
|
||||
text: "Parent changed",
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInput.promoteSteers(db, events, parent.id)
|
||||
yield* SessionPending.promoteSteers(db, events, parent.id)
|
||||
yield* session.prompt({
|
||||
sessionID: forked.id,
|
||||
text: "Child continues",
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInput.promoteSteers(db, events, forked.id)
|
||||
yield* SessionPending.promoteSteers(db, events, forked.id)
|
||||
|
||||
expect((yield* session.context(parent.id)).map((message) => message.type)).toEqual(["user", "synthetic", "user"])
|
||||
expect((yield* session.context(forked.id)).map((message) => message.type)).toEqual(["user", "synthetic", "user"])
|
||||
|
|
@ -250,7 +248,7 @@ describe("SessionV2.create", () => {
|
|||
(event): number | undefined => event.durable?.seq,
|
||||
),
|
||||
).toEqual([0, 5, 6])
|
||||
expect(yield* SessionInput.find(db, admitted.id)).toMatchObject({ sessionID: parent.id })
|
||||
expect(yield* SessionPending.find(db, admitted.id)).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -265,13 +263,13 @@ describe("SessionV2.create", () => {
|
|||
text: "First",
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInput.promoteSteers(db, events, parent.id)
|
||||
yield* SessionPending.promoteSteers(db, events, parent.id)
|
||||
const second = yield* session.prompt({
|
||||
sessionID: parent.id,
|
||||
text: "Second",
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInput.promoteSteers(db, events, parent.id)
|
||||
yield* SessionPending.promoteSteers(db, events, parent.id)
|
||||
const assistantMessageID = SessionMessage.ID.create()
|
||||
const model = ModelV2.Ref.make({ id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") })
|
||||
yield* events.publish(SessionEvent.Step.Started, {
|
||||
|
|
@ -416,7 +414,7 @@ describe("SessionV2.create", () => {
|
|||
text: "Hello",
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInput.promoteSteers(db, events, created.id)
|
||||
yield* SessionPending.promoteSteers(db, events, created.id)
|
||||
|
||||
expect(
|
||||
Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(2), Stream.runCollect)),
|
||||
|
|
@ -442,7 +440,7 @@ describe("SessionV2.create", () => {
|
|||
text: "Replay lifecycle",
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInput.promoteSteers(sourceDb, sourceEvents, created.id)
|
||||
yield* SessionPending.promoteSteers(sourceDb, sourceEvents, created.id)
|
||||
const serialized = (yield* sourceDb
|
||||
.select()
|
||||
.from(EventTable)
|
||||
|
|
@ -480,7 +478,7 @@ describe("SessionV2.create", () => {
|
|||
|
||||
expect(yield* store.get(created.id)).toBeUndefined()
|
||||
expect(yield* events.replayAll(serialized.slice(0, 2))).toBe(created.id)
|
||||
expect(yield* SessionInput.find(db, admitted.id)).toMatchObject({
|
||||
expect(yield* SessionPending.find(db, admitted.id)).toMatchObject({
|
||||
id: admitted.id,
|
||||
sessionID: created.id,
|
||||
type: "user",
|
||||
|
|
@ -491,15 +489,7 @@ describe("SessionV2.create", () => {
|
|||
expect(yield* store.context(created.id)).toEqual([])
|
||||
|
||||
expect(yield* events.replayAll(serialized.slice(2))).toBe(created.id)
|
||||
expect(yield* SessionInput.find(db, admitted.id)).toMatchObject({
|
||||
id: admitted.id,
|
||||
sessionID: created.id,
|
||||
type: "user",
|
||||
data: { text: "Replay lifecycle" },
|
||||
delivery: "steer",
|
||||
admittedSeq: 1,
|
||||
promotedSeq: 2,
|
||||
})
|
||||
expect(yield* SessionPending.find(db, admitted.id)).toBeUndefined()
|
||||
expect(yield* store.context(created.id)).toMatchObject([
|
||||
{ id: admitted.id, type: "user", text: "Replay lifecycle" },
|
||||
])
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ import { SessionMessageUpdater } from "@opencode-ai/core/session/message-updater
|
|||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { fromRow } from "@opencode-ai/core/session/info"
|
||||
import { SessionInput } from "@opencode-ai/core/session/input"
|
||||
import { SessionPending } from "@opencode-ai/core/session/pending"
|
||||
import { Shell } from "@opencode-ai/schema/shell"
|
||||
import {
|
||||
InstructionCheckpointTable,
|
||||
SessionInputTable,
|
||||
SessionPendingTable,
|
||||
SessionMessageTable,
|
||||
SessionTable,
|
||||
} from "@opencode-ai/core/session/sql"
|
||||
|
|
@ -77,7 +77,7 @@ describe("SessionProjector", () => {
|
|||
.run()
|
||||
const events = yield* EventV2.Service
|
||||
const inputID = SessionMessage.ID.make("msg_manual_compaction")
|
||||
yield* SessionInput.admitCompaction(db, events, { id: inputID, sessionID })
|
||||
yield* SessionPending.admitCompaction(db, events, { id: inputID, sessionID })
|
||||
|
||||
yield* events.publish(SessionEvent.Compaction.Failed, {
|
||||
sessionID,
|
||||
|
|
@ -85,7 +85,7 @@ describe("SessionProjector", () => {
|
|||
error: { type: "compaction.failed", message: "Auto compaction failed" },
|
||||
})
|
||||
|
||||
expect(yield* SessionInput.pendingCompaction(db, sessionID)).toMatchObject({ id: inputID })
|
||||
expect(yield* SessionPending.compaction(db, sessionID)).toMatchObject({ id: inputID })
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -316,7 +316,7 @@ describe("SessionProjector", () => {
|
|||
}).pipe(Effect.provide(sessionsLayer)),
|
||||
)
|
||||
|
||||
it.effect("marks an inbox row promoted with the PromptPromoted event sequence", () =>
|
||||
it.effect("consumes the pending row and projects the message at promotion", () =>
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
yield* db
|
||||
|
|
@ -338,7 +338,7 @@ describe("SessionProjector", () => {
|
|||
.pipe(Effect.orDie)
|
||||
const events = yield* EventV2.Service
|
||||
const id = SessionMessage.ID.make("msg_admitted")
|
||||
const admitted = yield* SessionInput.admit(db, events, {
|
||||
const admitted = yield* SessionPending.admit(db, events, {
|
||||
id,
|
||||
sessionID,
|
||||
input: { type: "user", data: { text: "promote me" }, delivery: "steer" },
|
||||
|
|
@ -351,8 +351,11 @@ describe("SessionProjector", () => {
|
|||
})
|
||||
|
||||
expect(
|
||||
yield* db.select().from(SessionInputTable).where(eq(SessionInputTable.id, id)).get().pipe(Effect.orDie),
|
||||
).toMatchObject({ promoted_seq: event.durable?.seq })
|
||||
yield* db.select().from(SessionPendingTable).where(eq(SessionPendingTable.id, id)).get().pipe(Effect.orDie),
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
yield* db.select().from(SessionMessageTable).where(eq(SessionMessageTable.id, id)).get().pipe(Effect.orDie),
|
||||
).toMatchObject({ session_id: sessionID, type: "user", seq: event.durable?.seq })
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ import { SessionV2 } from "@opencode-ai/core/session"
|
|||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { SessionInput } from "@opencode-ai/core/session/input"
|
||||
import { SessionInputTable, SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionPending } from "@opencode-ai/core/session/pending"
|
||||
import { SessionPendingTable, SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
|
|
@ -81,11 +81,11 @@ const setup = Effect.gen(function* () {
|
|||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const admitted = (id: SessionMessage.ID) => Database.Service.use(({ db }) => SessionInput.find(db, id))
|
||||
const admitted = (id: SessionMessage.ID) => Database.Service.use(({ db }) => SessionPending.find(db, id))
|
||||
const admittedCount = Database.Service.use(({ db }) =>
|
||||
db
|
||||
.select()
|
||||
.from(SessionInputTable)
|
||||
.from(SessionPendingTable)
|
||||
.all()
|
||||
.pipe(
|
||||
Effect.orDie,
|
||||
|
|
@ -201,7 +201,7 @@ describe("SessionV2.prompt", () => {
|
|||
text: "boundary",
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInput.promoteSteers(db, events, sessionID)
|
||||
yield* SessionPending.promoteSteers(db, events, sessionID)
|
||||
const stale = SessionMessage.ID.make("msg_stale_assistant")
|
||||
yield* db.insert(SessionMessageTable).values(assistantRow(stale, 100)).run().pipe(Effect.orDie)
|
||||
yield* events.publish(SessionEvent.RevertEvent.Staged, {
|
||||
|
|
@ -218,7 +218,7 @@ describe("SessionV2.prompt", () => {
|
|||
(row) => row.id,
|
||||
),
|
||||
).not.toContainAnyValues([boundary.id, stale])
|
||||
expect(yield* SessionInput.find(db, boundary.id)).toBeUndefined()
|
||||
expect(yield* SessionPending.find(db, boundary.id)).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -233,7 +233,7 @@ describe("SessionV2.prompt", () => {
|
|||
text: "boundary",
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInput.promoteSteers(db, events, sessionID)
|
||||
yield* SessionPending.promoteSteers(db, events, sessionID)
|
||||
yield* events.publish(SessionEvent.RevertEvent.Staged, {
|
||||
sessionID,
|
||||
revert: { messageID: boundary.id, files: [] },
|
||||
|
|
@ -243,11 +243,11 @@ describe("SessionV2.prompt", () => {
|
|||
const completion = yield* session.synthetic({ sessionID, text: "stale completion" })
|
||||
|
||||
expect(wakeCalls).toEqual([])
|
||||
expect(yield* SessionInput.find(db, completion.id)).toMatchObject({ type: "synthetic" })
|
||||
expect(yield* SessionPending.find(db, completion.id)).toMatchObject({ type: "synthetic" })
|
||||
|
||||
yield* session.revert.commit(sessionID)
|
||||
|
||||
expect(yield* SessionInput.find(db, completion.id)).toBeUndefined()
|
||||
expect(yield* SessionPending.find(db, completion.id)).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -433,7 +433,7 @@ describe("SessionV2.prompt", () => {
|
|||
|
||||
yield* session.prompt({ sessionID, text: "First", resume: false })
|
||||
yield* session.prompt({ sessionID, text: "Second", resume: false })
|
||||
yield* SessionInput.promoteSteers(db, events, sessionID)
|
||||
yield* SessionPending.promoteSteers(db, events, sessionID)
|
||||
const streamed = Array.from(yield* Fiber.join(fiber))
|
||||
|
||||
expect(streamed.map((event): [number | undefined, string] => [event.durable?.seq, event.type])).toEqual([
|
||||
|
|
@ -610,12 +610,12 @@ describe("SessionV2.prompt", () => {
|
|||
})
|
||||
|
||||
yield* Effect.all(
|
||||
[SessionInput.promoteSteers(db, events, sessionID), SessionInput.promoteSteers(db, events, sessionID)],
|
||||
[SessionPending.promoteSteers(db, events, sessionID), SessionPending.promoteSteers(db, events, sessionID)],
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
|
||||
expect(yield* eventCount(EventV2.versionedType(SessionEvent.InputPromoted.type, 1))).toBe(1)
|
||||
expect(yield* admitted(messageID)).toMatchObject({ promotedSeq: 1 })
|
||||
expect(yield* admitted(messageID)).toBeUndefined()
|
||||
expect(yield* session.messages({ sessionID })).toMatchObject([
|
||||
{ id: messageID, type: "user", text: "Promote once" },
|
||||
])
|
||||
|
|
@ -645,7 +645,11 @@ describe("SessionV2.prompt", () => {
|
|||
.pipe(Effect.orDie)
|
||||
|
||||
yield* events.remove(sessionID)
|
||||
yield* db.delete(SessionInputTable).where(eq(SessionInputTable.session_id, sessionID)).run().pipe(Effect.orDie)
|
||||
yield* db
|
||||
.delete(SessionPendingTable)
|
||||
.where(eq(SessionPendingTable.session_id, sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* db
|
||||
.delete(SessionMessageTable)
|
||||
.where(eq(SessionMessageTable.session_id, sessionID))
|
||||
|
|
@ -836,7 +840,7 @@ describe("SessionV2.prompt", () => {
|
|||
},
|
||||
})
|
||||
|
||||
yield* SessionInput.promoteSteers(db, events, sessionID)
|
||||
yield* SessionPending.promoteSteers(db, events, sessionID)
|
||||
|
||||
expect(yield* session.messages({ sessionID })).toMatchObject([
|
||||
{
|
||||
|
|
@ -861,14 +865,14 @@ describe("SessionV2.prompt", () => {
|
|||
const entries = yield* Effect.all([session.synthetic(input), session.synthetic(input)], {
|
||||
concurrency: "unbounded",
|
||||
})
|
||||
yield* SessionInput.promoteSteers(database.db, events, sessionID)
|
||||
yield* SessionPending.promoteSteers(database.db, events, sessionID)
|
||||
const promotedRetry = yield* session.synthetic(input)
|
||||
const failure = yield* session.synthetic({ ...input, text: "Different completion" }).pipe(Effect.flip)
|
||||
|
||||
expect(entries[1]).toEqual(entries[0])
|
||||
expect(promotedRetry).toMatchObject({ id: messageID, type: "synthetic", promotedSeq: expect.any(Number) })
|
||||
expect(promotedRetry).toMatchObject({ id: messageID, type: "synthetic", data: { text: "Completed" } })
|
||||
expect(failure).toMatchObject({ _tag: "Session.SyntheticConflictError", sessionID, inputID: messageID })
|
||||
expect(yield* admittedCount).toBe(1)
|
||||
expect(yield* admittedCount).toBe(0)
|
||||
expect(yield* eventCount(EventV2.versionedType(SessionEvent.InputAdmitted.type, 1))).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
|
@ -888,9 +892,9 @@ describe("SessionV2.prompt", () => {
|
|||
})
|
||||
|
||||
expect(input.delivery).toBe("queue")
|
||||
expect(yield* SessionInput.promoteSteers(db, events, sessionID)).toBe(0)
|
||||
expect(yield* SessionPending.promoteSteers(db, events, sessionID)).toBe(0)
|
||||
expect(yield* session.messages({ sessionID })).toEqual([])
|
||||
expect(yield* SessionInput.promoteNextQueued(db, events, sessionID)).toBe(true)
|
||||
expect(yield* SessionPending.promoteNextQueued(db, events, sessionID)).toBe(true)
|
||||
expect(yield* session.messages({ sessionID })).toMatchObject([
|
||||
{ id: input.id, type: "synthetic", text: "Queued completion" },
|
||||
])
|
||||
|
|
@ -916,7 +920,7 @@ describe("SessionV2.prompt", () => {
|
|||
resume: false,
|
||||
})
|
||||
|
||||
yield* SessionInput.promoteSteers(db, events, sessionID)
|
||||
yield* SessionPending.promoteSteers(db, events, sessionID)
|
||||
|
||||
expect(
|
||||
(yield* session.messages({ sessionID, order: "asc" })).map((message) =>
|
||||
|
|
@ -926,3 +930,58 @@ describe("SessionV2.prompt", () => {
|
|||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe("SessionV2.pending", () => {
|
||||
it.effect("fails for an unknown session", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* SessionV2.Service
|
||||
expect(yield* session.pending(SessionV2.ID.make("ses_missing")).pipe(Effect.flip)).toMatchObject({
|
||||
_tag: "Session.NotFoundError",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("lists admitted work in admission order until promotion", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
const { db } = yield* Database.Service
|
||||
|
||||
const first = yield* session.prompt({ sessionID, text: "First steer", resume: false })
|
||||
const queued = yield* session.synthetic({
|
||||
sessionID,
|
||||
text: "Queued completion",
|
||||
delivery: "queue",
|
||||
resume: false,
|
||||
})
|
||||
const second = yield* session.prompt({ sessionID, text: "Second steer", resume: false })
|
||||
|
||||
expect(yield* session.pending(sessionID)).toMatchObject([
|
||||
{ id: first.id, type: "user", delivery: "steer" },
|
||||
{ id: queued.id, type: "synthetic", delivery: "queue" },
|
||||
{ id: second.id, type: "user", delivery: "steer" },
|
||||
])
|
||||
|
||||
yield* SessionPending.promoteSteers(db, events, sessionID)
|
||||
expect(yield* session.pending(sessionID)).toMatchObject([{ id: queued.id, type: "synthetic" }])
|
||||
|
||||
yield* SessionPending.promoteNextQueued(db, events, sessionID)
|
||||
expect(yield* session.pending(sessionID)).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("lists an unhandled compaction barrier until it settles", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const { db } = yield* Database.Service
|
||||
|
||||
const barrier = yield* session.compact({ sessionID })
|
||||
expect(yield* session.pending(sessionID)).toMatchObject([{ id: barrier.id, type: "compaction" }])
|
||||
|
||||
yield* SessionPending.settleCompaction(db, { sessionID })
|
||||
expect(yield* session.pending(sessionID)).toEqual([])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
|
|||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { Snapshot } from "@opencode-ai/core/snapshot"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { SessionInput } from "@opencode-ai/core/session/input"
|
||||
import { SessionPending } from "@opencode-ai/core/session/pending"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
|
|
@ -47,7 +47,7 @@ import { ConfigCompaction } from "@opencode-ai/core/config/compaction"
|
|||
import { Tool } from "@opencode-ai/core/tool/tool"
|
||||
import {
|
||||
InstructionCheckpointTable,
|
||||
SessionInputTable,
|
||||
SessionPendingTable,
|
||||
SessionMessageTable,
|
||||
SessionTable,
|
||||
} from "@opencode-ai/core/session/sql"
|
||||
|
|
@ -578,7 +578,7 @@ const replaySessionProjection = (id: SessionV2.ID) =>
|
|||
.pipe(Effect.orDie)
|
||||
|
||||
yield* events.remove(id)
|
||||
yield* db.delete(SessionInputTable).where(eq(SessionInputTable.session_id, id)).run().pipe(Effect.orDie)
|
||||
yield* db.delete(SessionPendingTable).where(eq(SessionPendingTable.session_id, id)).run().pipe(Effect.orDie)
|
||||
yield* db.delete(SessionMessageTable).where(eq(SessionMessageTable.session_id, id)).run().pipe(Effect.orDie)
|
||||
yield* events.replayAll(
|
||||
recorded.map((event) => ({
|
||||
|
|
@ -881,7 +881,7 @@ describe("SessionRunnerLLM", () => {
|
|||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Instructions.InitializationBlocked)
|
||||
expect(requests).toHaveLength(0)
|
||||
expect(yield* SessionInput.hasPending(db, sessionID, "steer")).toBe(true)
|
||||
expect(yield* SessionPending.has(db, sessionID, "steer")).toBe(true)
|
||||
expect(
|
||||
yield* db
|
||||
.select()
|
||||
|
|
@ -924,7 +924,7 @@ describe("SessionRunnerLLM", () => {
|
|||
|
||||
expect(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)).toBe(true)
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(yield* SessionInput.hasPending(db, sessionID, "steer")).toBe(true)
|
||||
expect(yield* SessionPending.has(db, sessionID, "steer")).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -1462,7 +1462,7 @@ describe("SessionRunnerLLM", () => {
|
|||
const first = yield* session.compact({ sessionID })
|
||||
const second = yield* session.compact({ sessionID })
|
||||
expect(second.id).toBe(first.id)
|
||||
expect(yield* SessionInput.pendingCompaction((yield* Database.Service).db, sessionID)).toMatchObject({
|
||||
expect(yield* SessionPending.compaction((yield* Database.Service).db, sessionID)).toMatchObject({
|
||||
id: first.id,
|
||||
})
|
||||
expect((yield* session.messages({ sessionID })).find((message) => message.id === first.id)).toBeUndefined()
|
||||
|
|
@ -1475,7 +1475,7 @@ describe("SessionRunnerLLM", () => {
|
|||
delivery: "queue",
|
||||
resume: false,
|
||||
})
|
||||
expect(yield* SessionInput.hasPending((yield* Database.Service).db, sessionID, "steer")).toBe(false)
|
||||
expect(yield* SessionPending.has((yield* Database.Service).db, sessionID, "steer")).toBe(false)
|
||||
|
||||
yield* Deferred.succeed(streamGate, undefined)
|
||||
yield* Fiber.join(active)
|
||||
|
|
@ -1485,7 +1485,7 @@ describe("SessionRunnerLLM", () => {
|
|||
expect(userTexts(requests[2])).toContain("Steer after compaction")
|
||||
expect(userTexts(requests[2])).toContain("Completion after compaction")
|
||||
expect(userTexts(requests[3])).toContain("Queue after compaction")
|
||||
expect(yield* SessionInput.pendingCompaction((yield* Database.Service).db, sessionID)).toBeUndefined()
|
||||
expect(yield* SessionPending.compaction((yield* Database.Service).db, sessionID)).toBeUndefined()
|
||||
expect((yield* session.messages({ sessionID })).find((message) => message.id === first.id)).toMatchObject({
|
||||
type: "compaction",
|
||||
status: "completed",
|
||||
|
|
@ -1521,7 +1521,7 @@ describe("SessionRunnerLLM", () => {
|
|||
|
||||
expect(requests).toHaveLength(3)
|
||||
expect(userTexts(requests[2])).toContain("Continue after failure")
|
||||
expect(yield* SessionInput.pendingCompaction((yield* Database.Service).db, sessionID)).toBeUndefined()
|
||||
expect(yield* SessionPending.compaction((yield* Database.Service).db, sessionID)).toBeUndefined()
|
||||
expect((yield* session.messages({ sessionID })).find((message) => message.id === compaction.id)).toMatchObject({
|
||||
type: "compaction",
|
||||
status: "failed",
|
||||
|
|
@ -1542,7 +1542,7 @@ describe("SessionRunnerLLM", () => {
|
|||
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect(yield* SessionInput.pendingCompaction((yield* Database.Service).db, sessionID)).toBeUndefined()
|
||||
expect(yield* SessionPending.compaction((yield* Database.Service).db, sessionID)).toBeUndefined()
|
||||
expect((yield* session.messages({ sessionID })).find((message) => message.id === compaction.id)).toMatchObject({
|
||||
type: "compaction",
|
||||
status: "failed",
|
||||
|
|
@ -1566,7 +1566,7 @@ describe("SessionRunnerLLM", () => {
|
|||
|
||||
expect(yield* Effect.exit(session.resume(sessionID))).toMatchObject({ _tag: "Failure" })
|
||||
|
||||
expect(yield* SessionInput.pendingCompaction((yield* Database.Service).db, sessionID)).toBeUndefined()
|
||||
expect(yield* SessionPending.compaction((yield* Database.Service).db, sessionID)).toBeUndefined()
|
||||
expect((yield* session.messages({ sessionID })).find((message) => message.id === compaction.id)).toMatchObject({
|
||||
type: "compaction",
|
||||
status: "failed",
|
||||
|
|
@ -2317,7 +2317,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* session.interrupt(sessionID)
|
||||
expect(yield* Fiber.await(run)).toMatchObject({ _tag: "Failure" })
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(yield* SessionInput.hasPending(db, sessionID, "queue")).toBe(true)
|
||||
expect(yield* SessionPending.has(db, sessionID, "queue")).toBe(true)
|
||||
const resumed = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||
while (requests.length < 2) yield* Effect.yieldNow
|
||||
yield* Deferred.succeed(streamGate, undefined)
|
||||
|
|
@ -2350,7 +2350,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* session.interrupt(sessionID)
|
||||
expect(yield* Fiber.await(run)).toMatchObject({ _tag: "Failure" })
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(yield* SessionInput.hasPending(db, sessionID, "steer")).toBe(true)
|
||||
expect(yield* SessionPending.has(db, sessionID, "steer")).toBe(true)
|
||||
|
||||
const resumed = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||
while (requests.length < 2) yield* Effect.yieldNow
|
||||
|
|
@ -2516,7 +2516,7 @@ describe("SessionRunnerLLM", () => {
|
|||
const session = yield* setup
|
||||
const events = yield* EventV2.Service
|
||||
yield* admit(session, "Recover interrupted tool")
|
||||
yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID)
|
||||
yield* SessionPending.promoteSteers((yield* Database.Service).db, events, sessionID)
|
||||
const assistantMessageID = SessionMessage.ID.create()
|
||||
yield* events.publish(SessionEvent.Step.Started, {
|
||||
sessionID,
|
||||
|
|
@ -2573,7 +2573,7 @@ describe("SessionRunnerLLM", () => {
|
|||
const session = yield* setup
|
||||
const events = yield* EventV2.Service
|
||||
yield* admit(session, "Recover interrupted hosted tool")
|
||||
yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID)
|
||||
yield* SessionPending.promoteSteers((yield* Database.Service).db, events, sessionID)
|
||||
const assistantMessageID = SessionMessage.ID.create()
|
||||
yield* events.publish(SessionEvent.Step.Started, {
|
||||
sessionID,
|
||||
|
|
@ -2624,7 +2624,7 @@ describe("SessionRunnerLLM", () => {
|
|||
const session = yield* setup
|
||||
const events = yield* EventV2.Service
|
||||
yield* admit(session, "Recover interrupted tool input")
|
||||
yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID)
|
||||
yield* SessionPending.promoteSteers((yield* Database.Service).db, events, sessionID)
|
||||
const assistantMessageID = SessionMessage.ID.create()
|
||||
yield* events.publish(SessionEvent.Step.Started, {
|
||||
sessionID,
|
||||
|
|
@ -3056,13 +3056,11 @@ describe("SessionRunnerLLM", () => {
|
|||
const releaseLateEvent = yield* Deferred.make<void>()
|
||||
yield* registry.register({ permissionfail: permissionFail })
|
||||
const events = yield* EventV2.Service
|
||||
const permissionFailed = yield* events
|
||||
.subscribe(SessionEvent.Tool.Failed)
|
||||
.pipe(
|
||||
Stream.filter((event) => event.data.sessionID === sessionID && event.data.callID === "call-permission"),
|
||||
Stream.runHead,
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
const permissionFailed = yield* events.subscribe(SessionEvent.Tool.Failed).pipe(
|
||||
Stream.filter((event) => event.data.sessionID === sessionID && event.data.callID === "call-permission"),
|
||||
Stream.runHead,
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* admit(session, "Reject permission while another tool input streams")
|
||||
responseStream = Stream.concat(
|
||||
Stream.fromIterable([
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { ProjectDirectories } from "@opencode-ai/schema/project-directories"
|
|||
import { PermissionV1 } from "@opencode-ai/schema/permission-v1"
|
||||
import { Prompt } from "@opencode-ai/schema/prompt"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
import { SessionInput } from "@opencode-ai/schema/session-input"
|
||||
import { SessionPending } from "@opencode-ai/schema/session-pending"
|
||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
import { Workspace } from "@opencode-ai/schema/workspace"
|
||||
import { Command } from "@opencode-ai/schema/command"
|
||||
|
|
@ -43,7 +43,7 @@ test("Core reuses the canonical shared schemas", async () => {
|
|||
corePty,
|
||||
coreProject,
|
||||
coreReference,
|
||||
coreSessionInput,
|
||||
coreSessionPending,
|
||||
coreSessionMessage,
|
||||
coreSkill,
|
||||
coreV2Schema,
|
||||
|
|
@ -63,7 +63,7 @@ test("Core reuses the canonical shared schemas", async () => {
|
|||
import("@opencode-ai/core/pty"),
|
||||
import("@opencode-ai/core/project/schema"),
|
||||
import("@opencode-ai/core/reference"),
|
||||
import("@opencode-ai/core/session/input"),
|
||||
import("@opencode-ai/core/session/pending"),
|
||||
import("@opencode-ai/core/session/message"),
|
||||
import("@opencode-ai/core/skill"),
|
||||
import("@opencode-ai/core/v2-schema"),
|
||||
|
|
@ -133,10 +133,10 @@ test("Core reuses the canonical shared schemas", async () => {
|
|||
[SessionV2.ID, Session.ID],
|
||||
[SessionV2.Info, Session.Info],
|
||||
[SessionV2.ListAnchor, Session.ListAnchor],
|
||||
[coreSessionInput.Delivery, SessionInput.Delivery],
|
||||
[coreSessionInput.Message, SessionInput.Message],
|
||||
[coreSessionInput.User, SessionInput.User],
|
||||
[coreSessionInput.Synthetic, SessionInput.Synthetic],
|
||||
[coreSessionPending.Delivery, SessionPending.Delivery],
|
||||
[coreSessionPending.Message, SessionPending.Message],
|
||||
[coreSessionPending.User, SessionPending.User],
|
||||
[coreSessionPending.Synthetic, SessionPending.Synthetic],
|
||||
[coreSessionMessage.ID, SessionMessage.ID],
|
||||
[coreSessionMessage.AssistantRetry, SessionMessage.AssistantRetry],
|
||||
[coreSessionMessage.AgentSelected, SessionMessage.AgentSelected],
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
|
|||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { SessionInput } from "@opencode-ai/core/session/input"
|
||||
import { SessionPending } from "@opencode-ai/core/session/pending"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
|
|
@ -288,7 +288,7 @@ describe("SubagentTool", () => {
|
|||
const admission = Array.from(yield* Fiber.join(admitted))[0]
|
||||
expect(admission?.data.input.data.text).toContain(`<subagent id="${childID}" state="completed"`)
|
||||
const database = yield* Database.Service
|
||||
yield* SessionInput.promoteSteers(database.db, events, parent.id)
|
||||
yield* SessionPending.promoteSteers(database.db, events, parent.id)
|
||||
const synthetic = (yield* sessions.context(parent.id)).filter((message) => message.type === "synthetic")
|
||||
expect(synthetic).toHaveLength(1)
|
||||
expect(synthetic[0]?.text).toContain(`<subagent id="${childID}" state="completed"`)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue