Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
TypeScript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517import { describe, expect } from "bun:test"import { $ } from "bun"import fs from "fs/promises"import path from "path"import { DateTime, Effect, Layer, Stream } from "effect"import { TestClock } from "effect/testing"import { Money } from "@opencode/schema/money"import { Shell } from "@opencode/schema/shell"import { Skill } from "@opencode/schema/skill"import { Agent } from "@opencode/core/agent"import { and, asc, eq } from "drizzle-orm"import { Database } from "@opencode/core/database/database"import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder"import { LayerNode } from "@opencode/util/effect/layer-node"import { Hash } from "@opencode/util/hash"import { Bus } from "@opencode/core/bus"import { EventTable } from "@opencode/core/event/sql"import { Instructions } from "@opencode/core/instructions/index"import { Location } from "@opencode/core/location"import { Model } from "@opencode/core/model"import { Project } from "@opencode/core/project"import { ProjectTable } from "@opencode/core/project/sql"import { Provider } from "@opencode/core/provider"import { AbsolutePath, RelativePath } from "@opencode/core/schema"import { Session } from "@opencode/core/session"import { SessionMessage } from "@opencode/core/session/message"import { SessionProjector } from "@opencode/core/session/projector"import { SessionExecution } from "@opencode/core/session/execution"import { SessionInbox } from "@opencode/core/session/inbox"import { InstructionEntry } from "@opencode/core/session/instruction-entry"import { SessionEvent } from "@opencode/core/session/event"import { SessionTable } from "@opencode/core/session/sql"import { SessionStore } from "@opencode/core/session/store"import { SessionTransfer } from "@opencode/core/session/transfer"import { Workspace } from "@opencode/core/workspace"import { Expected } from "./lib/session-message"import { testEffect } from "./lib/effect"import { LocationServiceMap } from "@opencode/core/location-service-map"import { offlineModels } from "./fixture/models"import { promptLocationNode } from "./fixture/prompt-location"import { globalProjectNode } from "./lib/project"import { tmpdirScoped } from "./fixture/tmpdir"
const it = testEffect( AppNodeBuilder.build( LayerNode.group([ Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node, SessionTransfer.node, InstructionEntry.node, ]), [ Bus.node.replace(Bus.configured({ persist: true })), Project.node.replace(globalProjectNode), LocationServiceMap.node.replace(promptLocationNode), SessionExecution.node.replace(SessionExecution.noopLayer), ], ),)const liveIt = testEffect( AppNodeBuilder.build( LayerNode.group([Database.node, Bus.node, Project.node, SessionProjector.node, SessionStore.node, Session.node]), [ Bus.node.replace(Bus.configured({ persist: true })), SessionExecution.node.replace(SessionExecution.noopLayer), offlineModels, ], ),)const projectIt = testEffect( AppNodeBuilder.build( LayerNode.group([Database.node, Bus.node, Project.node, SessionProjector.node, SessionStore.node, Session.node]), [ Bus.node.replace(Bus.configured({ persist: true })), // Project adoption needs plain-prompt admission, not live plugin/provider startup. LocationServiceMap.node.replace(promptLocationNode), SessionExecution.node.replace(SessionExecution.noopLayer), ], ),)const location = Location.Ref.make({ directory: AbsolutePath.make("/project") })const id = Session.ID.create()
/** Public session events from a `log` read, without synced markers. */const logEvents = (session: Session.Interface, sessionID: Session.ID, follow?: boolean) => session .log({ sessionID, follow }) .pipe(Stream.filter((item): item is SessionEvent.DurableEvent => !Bus.isSynced(item)))
const assertCreateInputTypes = (session: Session.Interface) => { // @ts-expect-error location or parentID is required. session.create({}) // @ts-expect-error child sessions inherit their parent's location. session.create({ parentID: Session.ID.create(), location })}void assertCreateInputTypes
function withTmp<A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) { return tmpdirScoped().pipe(Effect.flatMap((tmp) => f(tmp.path)))}
describe("Session.create", () => { liveIt.live("preserves the project canonical directory when creating a session in another clone", () => withTmp((directory) => Effect.gen(function* () { const main = AbsolutePath.make(path.join(directory, "repo")) const clone = AbsolutePath.make(path.join(directory, "other-clone")) yield* Effect.promise(async () => { await $`git init -q ${main}`.cwd(directory) await $`git -c user.name=Test -c user.email=test@opencode.test -c commit.gpgsign=false commit --allow-empty -qm root` .cwd(main) .quiet() await $`git remote add origin git@github.com:owner/repo.git`.cwd(main) await $`git clone --no-hardlinks ${main} ${clone}`.quiet() await $`git remote set-url origin https://github.com/owner/repo.git`.cwd(clone) }) const sessions = yield* Session.Service const projects = yield* Project.Service const first = yield* sessions.create({ location: Location.Ref.make({ directory: main }) }) const second = yield* sessions.create({ location: Location.Ref.make({ directory: clone }) })
expect(second.projectID).toBe(first.projectID) expect((yield* projects.list()).find((project) => project.id === first.projectID)?.canonical).toBe(main) expect((yield* sessions.get(first.id)).location.directory).toBe(main) expect((yield* sessions.get(second.id)).location.directory).toBe(clone) }), ), )
projectIt.live("follows the directory's project identity established after creation", () => withTmp((directory) => Effect.gen(function* () { const session = yield* Session.Service const projects = yield* Project.Service const { db } = yield* Database.Service const ref = Location.Ref.make({ directory: AbsolutePath.make(directory) }) const nested = Location.Ref.make({ directory: AbsolutePath.make(path.join(directory, "packages", "app")) }) const aliased = Location.Ref.make({ directory: AbsolutePath.make([path.join(directory, "alias"), "..", "packages", "app"].join(path.sep)), }) const created = yield* session.create({ location: ref, title: "Before git" }) const child = yield* session.create({ location: nested, title: "Nested before git" }) const alias = yield* session.create({ location: aliased, title: "Aliased before git" }) const bus = yield* Bus.Service const store = yield* SessionStore.Service yield* session.prompt({ sessionID: created.id, text: "Preserved history", resume: false }) yield* SessionInbox.promote(db, bus, created.id, "steer") const pending = yield* session.prompt({ sessionID: created.id, text: "Preserved inbox", resume: false }) yield* store.claim(created.id) const before = yield* db .select() .from(SessionTable) .where(eq(SessionTable.id, created.id)) .get() .pipe(Effect.orDie) if (!before) return yield* Effect.die(new Error("created session not found"))
yield* Effect.promise(async () => { await $`git init -q`.cwd(directory) await $`git config user.email test@example.com`.cwd(directory) await $`git config user.name Test`.cwd(directory) }) const unborn = yield* projects.resolve(ref.directory) const unbornRoot = yield* session.get(created.id) const unbornNested = yield* session.get(child.id) const unbornAlias = yield* session.get(alias.id)
yield* Effect.promise(async () => { await fs.writeFile(path.join(directory, "README.md"), "test\n") await $`git add README.md`.cwd(directory) await $`git commit -qm initial`.cwd(directory) await $`git remote add origin git@github.com:owner/adopted.git`.cwd(directory) })
const project = yield* projects.resolve(ref.directory) const repeat = yield* projects.resolve(ref.directory) const adopted = yield* session.get(created.id) const nestedAdopted = yield* session.get(child.id) const aliasAdopted = yield* session.get(alias.id) const page = yield* session.list({ project: project.id }) const log = Array.from(yield* Stream.runCollect(logEvents(session, created.id)))
expect(created.projectID).not.toBe(Project.ID.global) expect(child.projectID).not.toBe(created.projectID) expect(alias.projectID).toBe(child.projectID) expect(unborn.id).toBe(Project.ID.global) expect(unbornRoot).toMatchObject({ projectID: Project.ID.global, subpath: undefined }) expect(unbornNested).toMatchObject({ projectID: Project.ID.global, subpath: "packages/app" }) expect(unbornAlias).toMatchObject({ projectID: Project.ID.global, subpath: "packages/app" }) expect( yield* db .select({ data: EventTable.data }) .from(EventTable) .where(eq(EventTable.aggregate_id, Project.ID.global)) .get() .pipe(Effect.orDie), ).toMatchObject({ data: { adopted: expect.arrayContaining([created.projectID, child.projectID]) } }) expect(project.id).toBe(Project.ID.make(Hash.fast("git-remote:github.com/owner/adopted"))) expect(repeat.id).toBe(project.id) expect(page.data.map((item) => item.id)).toEqual(expect.arrayContaining([created.id, child.id])) expect(adopted).toMatchObject({ projectID: project.id, location: ref, subpath: undefined, time: { updated: DateTime.makeUnsafe(before.time_updated) }, }) expect(nestedAdopted).toMatchObject({ projectID: project.id, location: nested, subpath: RelativePath.make("packages/app"), }) expect(aliasAdopted).toMatchObject({ projectID: project.id, location: aliased, subpath: RelativePath.make("packages/app"), }) // Adoption is a project-domain fact; the session log records nothing new. expect(log.map((event) => event.type)).toEqual([ "session.created", "session.inbox.enqueued", "session.inbox.delivered", "session.inbox.enqueued", ]) expect(yield* session.messages({ sessionID: created.id })).toMatchObject([ { id: expect.any(String), ...Expected.user("Preserved history") }, ]) expect(yield* SessionInbox.find(db, pending.id)).toMatchObject({ payload: { text: "Preserved inbox" } }) expect( yield* db.select().from(SessionTable).where(eq(SessionTable.id, created.id)).get().pipe(Effect.orDie), ).toMatchObject({ time_created: before.time_created, time_updated: before.time_updated, time_suspended: before.time_suspended, resume_attempts: before.resume_attempts, }) // Repeated resolution announces the directory's identity exactly once. const announced = yield* db .select({ type: EventTable.type }) .from(EventTable) .where(eq(EventTable.aggregate_id, project.id)) .all() .pipe(Effect.orDie) expect(announced.map((event) => event.type)).toEqual(["worktree.resolved.1"]) }), ), )
liveIt.live("does not adopt nested repositories or sessions in another workspace", () => withTmp((directory) => Effect.gen(function* () { const session = yield* Session.Service const projects = yield* Project.Service const outer = path.join(directory, "outer") const nested = path.join(outer, "nested") const child = AbsolutePath.make(path.join(nested, "app")) yield* Effect.promise(() => fs.mkdir(child, { recursive: true }))
const local = yield* session.create({ location: Location.Ref.make({ directory: AbsolutePath.make(outer) }) }) const inside = yield* session.create({ location: Location.Ref.make({ directory: child }) }) const remote = yield* session.create({ location: Location.Ref.make({ directory: AbsolutePath.make(outer), workspaceID: Workspace.ID.make("wrk_remote"), }), })
yield* Effect.promise(async () => { for (const root of [nested, outer]) { await $`git init -q`.cwd(root) await $`git config user.email test@example.com`.cwd(root) await $`git config user.name Test`.cwd(root) await $`git commit --allow-empty -qm initial`.cwd(root) } })
const parent = yield* projects.resolve(AbsolutePath.make(outer)) expect((yield* session.get(local.id)).projectID).toBe(parent.id) expect((yield* session.get(inside.id)).projectID).toBe(inside.projectID) expect((yield* session.get(remote.id)).projectID).toBe(remote.projectID)
const repository = yield* projects.resolve(AbsolutePath.make(nested)) expect((yield* session.get(inside.id)).projectID).toBe(repository.id) expect((yield* session.get(remote.id)).projectID).toBe(remote.projectID) }), ), )
it.effect("persists a missing title until one is generated or supplied", () => Effect.gen(function* () { const session = yield* Session.Service const { db } = yield* Database.Service
const created = yield* session.create({ location }) const row = yield* db.select().from(SessionTable).where(eq(SessionTable.id, created.id)).get().pipe(Effect.orDie) const event = yield* db .select({ data: EventTable.data }) .from(EventTable) .where(eq(EventTable.aggregate_id, created.id)) .get() .pipe(Effect.orDie)
expect(created.title).toBeUndefined() expect(row?.title).toBeNull() expect(event?.data).not.toHaveProperty("title") expect((yield* session.create({ location, title: "Explicit title" })).title).toBe("Explicit title") }), )
it.effect("creates a fresh projected session when the ID is omitted", () => Effect.gen(function* () { const session = yield* Session.Service
const first = yield* session.create({ location }) const second = yield* session.create({ location })
expect(second.id).not.toBe(first.id) expect((yield* session.list()).data).toHaveLength(2) }), )
it.effect("returns the original session when the ID is retried", () => Effect.gen(function* () { const session = yield* Session.Service const input = { id, location }
const first = yield* session.create(input) const retried = yield* session.create(input)
expect(retried).toEqual(first) expect((yield* session.list()).data).toEqual([first]) }), )
it.effect("stores supplied immutable create attributes", () => Effect.gen(function* () { const session = yield* Session.Service const workspaceID = Workspace.ID.make("wrk_test") const model = Model.Ref.make({ id: Model.ID.make("sonnet"), providerID: Provider.ID.anthropic, variant: Model.VariantID.make("fast"), })
expect( yield* session.create({ location: Location.Ref.make({ directory: location.directory, workspaceID }), agent: Agent.ID.make("build"), model, }), ).toMatchObject({ location: { directory: location.directory, workspaceID }, agent: "build", model }) }), )
it.effect("stores creation metadata and inherits it through children and forks", () => Effect.gen(function* () { const session = yield* Session.Service const bus = yield* Bus.Service const { db } = yield* Database.Service const metadata = { thread: "C123/1699999999.123", labels: ["support", 2] }
const created = yield* session.create({ location, metadata }) expect(created.metadata).toEqual(metadata) // The annotations are a durable creation fact, not just projected state. expect( yield* db .select({ data: EventTable.data }) .from(EventTable) .where(eq(EventTable.aggregate_id, created.id)) .get() .pipe(Effect.orDie), ).toMatchObject({ data: { metadata } })
const inherited = yield* session.create({ parentID: created.id }) expect(inherited.metadata).toEqual(metadata) const overridden = yield* session.create({ parentID: created.id, metadata: { thread: "other" } }) expect(overridden.metadata).toEqual({ thread: "other" })
yield* session.prompt({ sessionID: created.id, text: "Fork context", resume: false }) yield* SessionInbox.promote(db, bus, created.id, "steer") const forked = yield* session.fork({ sessionID: created.id }) expect(forked.metadata).toEqual(metadata)
// Absent stays absent: no empty-object normalization. expect((yield* session.create({ location })).metadata).toBeUndefined()
const replacement = { thread: "updated", owner: "host" } yield* session.setMetadata({ sessionID: created.id, metadata: replacement }) expect((yield* session.get(created.id)).metadata).toEqual(replacement) expect( yield* db .select({ data: EventTable.data }) .from(EventTable) .where( and( eq(EventTable.aggregate_id, created.id), eq(EventTable.type, Bus.versionedType(SessionEvent.MetadataUpdated.type, 1)), ), ) .get() .pipe(Effect.orDie), ).toMatchObject({ data: { metadata: replacement } }) expect( yield* session.setMetadata({ sessionID: Session.ID.create(), metadata: replacement }).pipe(Effect.flip), ).toBeInstanceOf(Session.NotFoundError) }), )
it.effect("stores permission rules, inherits them through children and forks, and replaces them", () => Effect.gen(function* () { const session = yield* Session.Service const bus = yield* Bus.Service const { db } = yield* Database.Service const permissions = [{ action: "edit", resource: "/original/**", effect: "deny" as const }]
const created = yield* session.create({ location, permissions }) expect(created.permissions).toEqual(permissions) expect((yield* session.create({ parentID: created.id })).permissions).toEqual(permissions) expect((yield* session.create({ parentID: created.id, permissions: [] })).permissions).toEqual([])
yield* session.prompt({ sessionID: created.id, text: "Fork context", resume: false }) yield* SessionInbox.promote(db, bus, created.id, "steer") const forked = yield* session.fork({ sessionID: created.id }) expect(forked.permissions).toEqual(permissions)
const replaced = [{ action: "shell", resource: "*", effect: "ask" as const }] yield* session.setPermissions({ sessionID: created.id, permissions: replaced }) expect((yield* session.get(created.id)).permissions).toEqual(replaced) expect( yield* session.setPermissions({ sessionID: Session.ID.create(), permissions: replaced }).pipe(Effect.flip), ).toBeInstanceOf(Session.NotFoundError) }), )
it.effect("inherits location from an existing parent when omitted", () => Effect.gen(function* () { const session = yield* Session.Service const parent = yield* session.create({ location }) const child = yield* session.create({ parentID: parent.id, title: "child" })
expect(child).toMatchObject({ parentID: parent.id, location }) }), )
it.effect("rejects child creation when the parent does not exist", () => Effect.gen(function* () { const session = yield* Session.Service const missing = Session.ID.create()
expect(yield* Effect.flip(session.create({ parentID: missing, title: "child" }))).toEqual( new Session.NotFoundError({ sessionID: missing }), ) }), )
it.effect("filters root sessions before applying the page limit", () => Effect.gen(function* () { const session = yield* Session.Service const { db } = yield* Database.Service const staleRoot = yield* session.create({ location, title: "stale root" }) const root = yield* session.create({ location, title: "root" }) const children = yield* Effect.forEach(Array.from({ length: 60 }), (_, index) => session.create({ parentID: root.id, title: `child ${index}` }), )
yield* Effect.forEach(children, (item, index) => db .update(SessionTable) .set({ time_created: index + 100, time_updated: index + 20_000 }) .where(eq(SessionTable.id, item.id)) .run(), ) yield* db .update(SessionTable) .set({ time_created: 2, time_updated: 5_000 }) .where(eq(SessionTable.id, staleRoot.id)) .run() yield* db .update(SessionTable) .set({ time_created: 1, time_updated: 10_000 }) .where(eq(SessionTable.id, root.id)) .run()
const page = yield* session.list({ directory: location.directory, parentID: null, limit: 1, order: "desc" })
expect(page.data.map((item) => item.id)).toEqual([root.id]) }), )
it.effect("orders sessions by their latest prompt", () => Effect.gen(function* () { const session = yield* Session.Service const { db } = yield* Database.Service const active = yield* session.create({ location, title: "active" }) const newer = yield* session.create({ location, title: "newer" })
yield* db .update(SessionTable) .set({ time_created: -2, time_updated: -2 }) .where(eq(SessionTable.id, active.id)) .run() yield* db .update(SessionTable) .set({ time_created: -1, time_updated: -1 }) .where(eq(SessionTable.id, newer.id)) .run()
yield* session.prompt({ sessionID: active.id, text: "continue", resume: false })
expect((yield* session.list()).data.map((item) => item.id)).toEqual([active.id, newer.id]) }), )
it.effect("filters direct child sessions by parent ID", () => Effect.gen(function* () { const session = yield* Session.Service const parent = yield* session.create({ location, title: "parent" }) const child = yield* session.create({ parentID: parent.id, title: "child" }) yield* session.create({ location, title: "other root" })
const page = yield* session.list({ parentID: parent.id })
expect(page.data.map((item) => item.id)).toEqual([child.id]) }), )
it.effect("filters project sessions by subpath", () => Effect.gen(function* () { const session = yield* Session.Service const { db } = yield* Database.Service const root = yield* session.create({ location, title: "root" }) const nested = yield* session.create({ location, title: "nested" })
yield* db.update(SessionTable).set({ path: "packages/tui" }).where(eq(SessionTable.id, nested.id)).run()
const page = yield* session.list({ project: Project.ID.global, subpath: RelativePath.make("packages/tui"), parentID: null, })
expect(page.data.map((item) => item.id)).toEqual([nested.id]) expect(page.data.map((item) => item.id)).not.toContain(root.id) }), )
it.effect("forks a session by replaying a durable fork event into copied projected rows", () => Effect.gen(function* () { const session = yield* Session.Service const bus = yield* Bus.Service const { db } = yield* Database.Service const parent = yield* session.create({ location, title: "Parent" }) const admitted = yield* session.prompt({ sessionID: parent.id, text: "First", resume: false, }) yield* SessionInbox.promote(db, bus, parent.id, "steer") yield* session.synthetic({ sessionID: parent.id, text: "parent note", resume: false }) yield* SessionInbox.promote(db, bus, parent.id, "steer")
const forked = yield* session.fork({ sessionID: parent.id }) const parentContext = yield* session.context(parent.id) const forkContext = yield* session.context(forked.id) const history = Array.from(yield* Stream.runCollect(logEvents(session, forked.id)))
expect(forked).toMatchObject({ title: "Parent (fork #1)", fork: { sessionID: parent.id } }) expect(forked.parentID).toBeUndefined() expect(forkContext).toMatchObject([Expected.user("First"), { type: "synthetic", text: "parent note" }]) expect(forkContext.map((message) => message.id)).not.toEqual(parentContext.map((message) => message.id)) expect(history).toHaveLength(1) expect(history[0]).toMatchObject({ type: "session.forked", durable: { seq: 0 }, data: { sessionID: forked.id, parentID: parent.id }, }) expect(yield* SessionInbox.find(db, forkContext[0].id)).toBeUndefined() expect(yield* SessionInbox.find(db, forkContext[1].id)).toBeUndefined() expect( yield* session.prompt({ id: forkContext[0].id, sessionID: forked.id, text: "First", resume: false }), ).toMatchObject({ id: forkContext[0].id, type: "user", payload: { text: "First" } })
yield* session.prompt({ sessionID: parent.id, text: "Parent changed", resume: false, }) yield* SessionInbox.promote(db, bus, parent.id, "steer") yield* session.prompt({ sessionID: forked.id, text: "Child continues", resume: false, }) yield* SessionInbox.promote(db, bus, forked.id, "steer")
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"]) expect((yield* session.context(forked.id)).at(-1)).toMatchObject({ text: "Child continues" }) expect( Array.from(yield* Stream.runCollect(logEvents(session, forked.id))).map( (event): number | undefined => event.durable?.seq, ), ).toEqual([0, 5, 6]) expect(yield* SessionInbox.find(db, admitted.id)).toBeUndefined() }), )
it.effect("keeps a fork untitled when its parent is untitled", () => Effect.gen(function* () { const session = yield* Session.Service const bus = yield* Bus.Service const { db } = yield* Database.Service const parent = yield* session.create({ location }) yield* session.prompt({ sessionID: parent.id, text: "First", resume: false }) yield* SessionInbox.promote(db, bus, parent.id, "steer")
const forked = yield* session.fork({ sessionID: parent.id }) const row = yield* db.select().from(SessionTable).where(eq(SessionTable.id, forked.id)).get().pipe(Effect.orDie)
expect(forked.title).toBeUndefined() expect(row?.title).toBeNull() }), )
it.effect("replays a fork with stable projected identities", () => Effect.gen(function* () { const session = yield* Session.Service const bus = yield* Bus.Service const { db } = yield* Database.Service const parent = yield* session.create({ location, title: "Parent" }) yield* session.prompt({ sessionID: parent.id, text: "First", resume: false }) yield* SessionInbox.promote(db, bus, parent.id, "steer") yield* session.synthetic({ sessionID: parent.id, text: "Second", resume: false }) yield* SessionInbox.promote(db, bus, parent.id, "steer") const forked = yield* session.fork({ sessionID: parent.id }) const original = (yield* session.context(forked.id)).map((message) => message.id) const recorded = yield* db .select() .from(EventTable) .where(eq(EventTable.aggregate_id, forked.id)) .get() .pipe(Effect.orDie) if (!recorded) return yield* Effect.die(new Error("Fork event not found"))
yield* bus.remove(forked.id) yield* db.delete(SessionTable).where(eq(SessionTable.id, forked.id)).run().pipe(Effect.orDie) yield* bus.replay({ id: recorded.id, created: recorded.created, aggregateID: recorded.aggregate_id, seq: recorded.seq, type: recorded.type, data: recorded.data, })
expect((yield* session.context(forked.id)).map((message) => message.id)).toEqual(original) }), )
it.effect("inherits instruction entries when forking", () => Effect.gen(function* () { const session = yield* Session.Service const entries = yield* InstructionEntry.Service const bus = yield* Bus.Service const { db } = yield* Database.Service const parent = yield* session.create({ location }) yield* session.prompt({ sessionID: parent.id, text: "Fork context", resume: false }) yield* SessionInbox.promote(db, bus, parent.id, "steer") yield* entries.put({ sessionID: parent.id, key: "deploy-target", value: "production" }) yield* entries.put({ sessionID: parent.id, key: "retired", value: true }) yield* entries.remove({ sessionID: parent.id, key: "retired" }) yield* Effect.forEach( Array.from({ length: 20 }, (_, index) => index), (index) => entries.put({ sessionID: parent.id, key: `entry-${String(index).padStart(2, "0")}`, value: index }), { discard: true }, )
const forked = yield* session.fork({ sessionID: parent.id }) const inheritedList = yield* entries.list(forked.id) const inheritedValues = yield* entries.load(forked.id).pipe(Effect.flatMap(Instructions.read))
expect(inheritedList).toHaveLength(21) expect(inheritedList).toContainEqual({ key: "deploy-target", value: "production" }) expect(inheritedValues).toContainEqual({ key: Instructions.Key.make("api/retired"), value: Instructions.removed, })
const recorded = yield* db .select() .from(EventTable) .where(eq(EventTable.aggregate_id, forked.id)) .get() .pipe(Effect.orDie) if (!recorded) return yield* Effect.die(new Error("Fork event not found")) yield* entries.put({ sessionID: parent.id, key: "deploy-target", value: "staging" }) yield* entries.put({ sessionID: parent.id, key: "new-parent-entry", value: true }) yield* bus.remove(forked.id) yield* db.delete(SessionTable).where(eq(SessionTable.id, forked.id)).run().pipe(Effect.orDie) yield* bus.replay({ id: recorded.id, created: recorded.created, aggregateID: recorded.aggregate_id, seq: recorded.seq, type: recorded.type, data: recorded.data, })
expect(yield* entries.list(forked.id)).toEqual(inheritedList) expect(yield* entries.load(forked.id).pipe(Effect.flatMap(Instructions.read))).toEqual(inheritedValues) }), )
it.effect("does not copy a running assistant into a fork", () => Effect.gen(function* () { const session = yield* Session.Service const bus = yield* Bus.Service const { db } = yield* Database.Service const parent = yield* session.create({ location }) yield* session.prompt({ sessionID: parent.id, text: "Run both tools", resume: false }) yield* SessionInbox.promote(db, bus, parent.id, "steer") const assistantMessageID = SessionMessage.ID.create() const model = Model.Ref.make({ id: Model.ID.make("model"), providerID: Provider.ID.make("provider") })
yield* bus.publish(SessionEvent.Step.Started, { sessionID: parent.id, assistantMessageID, agent: Agent.ID.make("build"), model, started: 0, }) yield* bus.publish(SessionEvent.Tool.Input.Started, { sessionID: parent.id, assistantMessageID, id: "call_running", name: "shell", }) yield* bus.publish(SessionEvent.Tool.Input.Ended, { sessionID: parent.id, assistantMessageID, id: "call_running", text: '{"command":"sleep 10"}', }) yield* bus.publish(SessionEvent.Tool.Called, { sessionID: parent.id, assistantMessageID, id: "call_running", input: { command: "sleep 10" }, executed: true, })
const forked = yield* session.fork({ sessionID: parent.id })
expect(yield* session.context(parent.id)).toMatchObject([ Expected.user("Run both tools"), Expected.assistant({}, [{ type: "tool", id: "call_running", state: { status: "running" } }]), ]) expect(yield* session.context(forked.id)).toMatchObject([Expected.user("Run both tools")]) }), )
it.effect("copies only settled shell messages into forks", () => Effect.gen(function* () { const session = yield* Session.Service const bus = yield* Bus.Service const { db } = yield* Database.Service const parent = yield* session.create({ location }) yield* session.prompt({ sessionID: parent.id, text: "Run a shell", resume: false }) yield* SessionInbox.promote(db, bus, parent.id, "steer") const shell = Shell.Info.make({ id: Shell.ID.make("sh_fork_running"), status: "running", command: "sleep 10", cwd: location.directory, shell: "/bin/sh", file: "/tmp/sh_fork_running.out", metadata: {}, time: { started: 0 }, }) yield* bus.publish(SessionEvent.Shell.Started, { sessionID: parent.id, shell })
const running = yield* session.fork({ sessionID: parent.id })
expect(yield* session.context(parent.id)).toMatchObject([ Expected.user("Run a shell"), { type: "shell", command: "sleep 10", status: "running" }, ]) expect(yield* session.context(running.id)).toMatchObject([Expected.user("Run a shell")])
yield* bus.publish(SessionEvent.Shell.Ended, { sessionID: parent.id, shell: { ...shell, status: "exited", exit: 0, time: { started: 0, completed: 1 } }, output: { output: "complete", cursor: 8, size: 8, truncated: false }, }) const completed = yield* session.fork({ sessionID: parent.id })
expect(yield* session.context(running.id)).toMatchObject([Expected.user("Run a shell")]) expect(yield* session.context(completed.id)).toMatchObject([ Expected.user("Run a shell"), { type: "shell", command: "sleep 10", status: "exited", output: { output: "complete" } }, ]) }), )
it.effect("rejects forking an empty session", () => Effect.gen(function* () { const session = yield* Session.Service const parent = yield* session.create({ location })
expect( yield* session.fork({ sessionID: parent.id }).pipe(Effect.flip), ).toMatchObject({ _tag: "Session.ForkEmptyError", sessionID: parent.id }) }), )
it.effect("forks before the selected boundary message", () => Effect.gen(function* () { const session = yield* Session.Service const bus = yield* Bus.Service const { db } = yield* Database.Service const parent = yield* session.create({ location }) const first = yield* session.prompt({ sessionID: parent.id, text: "First", resume: false, }) yield* SessionInbox.promote(db, bus, parent.id, "steer") const second = yield* session.prompt({ sessionID: parent.id, text: "Second", resume: false, }) yield* SessionInbox.promote(db, bus, parent.id, "steer") const assistantMessageID = SessionMessage.ID.create() const model = Model.Ref.make({ id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }) yield* bus.publish(SessionEvent.Step.Started, { sessionID: parent.id, assistantMessageID, agent: Agent.ID.make("build"), model, started: 0, }) yield* bus.publish(SessionEvent.Step.Ended, { sessionID: parent.id, assistantMessageID, finish: "stop", cost: Money.USD.make(0.75), tokens: { input: 6, output: 3, reasoning: 1, cache: { read: 2, write: 1 } }, })
const forked = yield* session.fork({ sessionID: parent.id, before: second.id, }) const beforeFirst = yield* session.fork({ sessionID: parent.id, before: first.id, }) const complete = yield* session.fork({ sessionID: parent.id })
const context = yield* session.context(forked.id) const history = Array.from(yield* Stream.runCollect(logEvents(session, forked.id))) expect(forked.fork).toEqual({ sessionID: parent.id, boundary: { type: "before", messageID: second.id }, }) expect(context).toMatchObject([{ text: "First" }]) expect(context[0]?.id).not.toBe(first.id) expect(history[0]).toMatchObject({ data: { boundary: { type: "before", messageID: second.id } }, }) expect(forked).toMatchObject({ cost: 0, tokens: { input: 0, output: 0, reasoning: 0 } }) expect(yield* session.context(beforeFirst.id)).toEqual([]) expect(beforeFirst).toMatchObject({ cost: 0, tokens: { input: 0, output: 0, reasoning: 0 } }) expect(yield* session.context(complete.id)).toMatchObject([ Expected.user("First"), Expected.user("Second"), { type: "assistant", finish: "stop" }, ]) expect(complete).toMatchObject({ cost: 0, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, }) }), )
it.effect("returns the existing Session when one ID is reused with different create arguments", () => Effect.gen(function* () { const session = yield* Session.Service const created = yield* session.create({ id, location }) const changed = [ { id, location: Location.Ref.make({ directory: AbsolutePath.make("/other") }) }, { id, location, agent: Agent.ID.make("build") }, { id, location, model: Model.Ref.make({ id: Model.ID.make("sonnet"), providerID: Provider.ID.anthropic }), }, ]
for (const input of changed) { expect(yield* session.create(input)).toEqual(created) } expect((yield* session.list()).data).toHaveLength(1) }), )
it.effect("returns one recorded session to concurrent exact retries", () => Effect.gen(function* () { const session = yield* Session.Service const input = { id, location }
const created = yield* Effect.all([session.create(input), session.create(input)], { concurrency: "unbounded" })
expect(created[1]).toEqual(created[0]) expect((yield* session.list()).data).toEqual([created[0]]) }), )
it.effect("returns the current Session projection after updates", () => Effect.gen(function* () { const session = yield* Session.Service const { db } = yield* Database.Service const input = { id, location } const created = yield* session.create(input)
yield* db.update(SessionTable).set({ agent: "build" }).where(eq(SessionTable.id, id)).run().pipe(Effect.orDie)
expect(yield* session.create(input)).toMatchObject({ id: created.id, agent: "build" }) }), )
it.effect("persists creation through the current created event", () => Effect.gen(function* () { const session = yield* Session.Service const { db } = yield* Database.Service const created = yield* session.create({ location })
expect( yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, created.id)).all().pipe(Effect.orDie), ).toMatchObject([{ type: Bus.versionedType(SessionEvent.Created.type, 1) }]) }), )
it.effect("persists caller-ID creation through the existing created event", () => Effect.gen(function* () { const session = yield* Session.Service const { db } = yield* Database.Service const created = yield* session.create({ id, location })
expect( yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, created.id)).get().pipe(Effect.orDie), ).toMatchObject({ data: { sessionID: id }, }) }), )
it.effect("includes current creation rows in the Session event stream", () => Effect.gen(function* () { const session = yield* Session.Service const bus = yield* Bus.Service const { db } = yield* Database.Service const created = yield* session.create({ location }) yield* session.prompt({ sessionID: created.id, text: "Hello", resume: false, }) yield* SessionInbox.promote(db, bus, created.id, "steer")
expect( Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(3), Stream.runCollect)), ).toMatchObject([ { durable: { seq: 0 }, type: "session.created" }, { durable: { seq: 1 }, type: "session.inbox.enqueued", data: { inboxID: expect.any(String), item: { type: "user", payload: { text: "Hello" }, delivery: "steer" }, }, }, { durable: { seq: 2 }, type: "session.inbox.delivered" }, ]) }), )
it.effect("replays one prompt lifecycle into a fresh target database", () => Effect.gen(function* () { const session = yield* Session.Service const sourceEvents = yield* Bus.Service const sourceDb = (yield* Database.Service).db const created = yield* session.create({ id: Session.ID.make("ses_fresh_target_replay"), location }) const admitted = yield* session.prompt({ sessionID: created.id, text: "Replay lifecycle", resume: false, }) yield* SessionInbox.promote(sourceDb, sourceEvents, created.id, "steer") const serialized = (yield* sourceDb .select() .from(EventTable) .where(eq(EventTable.aggregate_id, created.id)) .orderBy(asc(EventTable.seq)) .all() .pipe(Effect.orDie)).map((event) => ({ id: event.id, created: event.created, aggregateID: event.aggregate_id, seq: event.seq, type: event.type, data: event.data, }))
const tmp = yield* tmpdirScoped() const targetLayer = AppNodeBuilder.build( LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node]), [ Database.node.replace(Database.configured({ path: path.join(tmp.path, "target.sqlite") })), Bus.node.replace(Bus.configured({ persist: true })), ], )
yield* Effect.gen(function* () { const db = (yield* Database.Service).db const bus = yield* Bus.Service const store = yield* SessionStore.Service yield* db .insert(ProjectTable) .values({ id: Project.ID.global, worktree: location.directory, sandboxes: [] }) .run() .pipe(Effect.orDie)
expect(yield* store.get(created.id)).toBeUndefined() yield* Effect.forEach(serialized.slice(0, 2), (event) => bus.replay(event), { discard: true }) expect(yield* SessionInbox.find(db, admitted.id)).toMatchObject({ id: admitted.id, sessionID: created.id, type: "user", payload: { text: "Replay lifecycle" }, delivery: "steer", }) expect(yield* store.context(created.id)).toEqual([])
yield* Effect.forEach(serialized.slice(2), (event) => bus.replay(event), { discard: true }) expect(yield* SessionInbox.find(db, admitted.id)).toBeUndefined() expect(yield* store.context(created.id)).toMatchObject([ { id: admitted.id, ...Expected.user("Replay lifecycle") }, ]) expect( (yield* db .select() .from(EventTable) .where(eq(EventTable.aggregate_id, created.id)) .orderBy(asc(EventTable.seq)) .all() .pipe(Effect.orDie)).map((event) => [event.seq, event.type]), ).toEqual([ [0, Bus.versionedType(SessionEvent.Created.type, 1)], [1, Bus.versionedType(SessionEvent.InboxEnqueued.type, 1)], [2, Bus.versionedType(SessionEvent.InboxDelivered.type, 1)], ]) }).pipe(Effect.provide(Layer.fresh(targetLayer))) }), )
it.effect("does not mask unrelated created projector defects", () => Effect.gen(function* () { const session = yield* Session.Service const event = yield* Bus.Service const defect = new Error("unrelated projector defect") yield* event.project(SessionEvent.Created, () => Effect.die(defect))
expect(yield* session.create({ id, location }).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect) }), )
liveIt.live("runs a shell command and projects the started/ended shell message", () => withTmp((directory) => Effect.gen(function* () { const session = yield* Session.Service const created = yield* session.create({ location: Location.Ref.make({ directory: AbsolutePath.make(directory) }), }) yield* session.environment({ sessionID: created.id, variables: { OPENCODE_SESSION_ENV_TEST: "attached" } })
const command = process.platform === "win32" ? "[Console]::Out.Write($env:OPENCODE_SESSION_ENV_TEST)" : 'printf %s "$OPENCODE_SESSION_ENV_TEST"' yield* session.shell({ sessionID: created.id, command })
const messages = yield* session.messages({ sessionID: created.id, order: "asc" }) const shell = messages.find((message): message is SessionMessage.Shell => message.type === "shell") expect(shell).toMatchObject({ type: "shell", command, status: "exited", exit: 0 }) expect(shell?.output?.output).toContain("attached") expect(shell?.output?.truncated).toBe(false) expect(shell?.time.completed).toBeDefined() }), ), )
liveIt.live("still emits shell ended for a failing command", () => withTmp((directory) => Effect.gen(function* () { const session = yield* Session.Service const created = yield* session.create({ location: Location.Ref.make({ directory: AbsolutePath.make(directory) }), })
yield* session.shell({ sessionID: created.id, command: "false" })
const messages = yield* session.messages({ sessionID: created.id, order: "asc" }) const shell = messages.find((message): message is SessionMessage.Shell => message.type === "shell") expect(shell).toMatchObject({ type: "shell", command: "false", status: "exited" }) expect(shell?.exit).not.toBe(0) expect(shell?.time.completed).toBeDefined() }), ), )
it.effect("switches the selected agent through the durable Session event", () => Effect.gen(function* () { const session = yield* Session.Service const created = yield* session.create({ location, agent: Agent.ID.make("build") })
yield* session.switchAgent({ sessionID: created.id, agent: Agent.ID.make("plan") })
expect(yield* session.get(created.id)).toMatchObject({ agent: "plan" }) expect( Array.from(yield* logEvents(session, created.id, true).pipe(Stream.drop(1), Stream.take(1), Stream.runCollect)), ).toMatchObject([{ type: "session.agent.selected", data: { agent: "plan", previous: "build" } }]) expect(yield* session.messages({ sessionID: created.id, order: "asc" })).toMatchObject([ { type: "agent-switched", agent: "plan", previous: "build" }, ]) }), )
it.effect("rejects an agent switch for a missing Session", () => Effect.gen(function* () { const session = yield* Session.Service const missing = Session.ID.make("ses_missing_agent_switch")
expect( yield* session.switchAgent({ sessionID: missing, agent: Agent.ID.make("plan") }).pipe( Effect.flip, Effect.map((error) => error._tag), ), ).toBe("Session.NotFoundError") }), )
it.effect("switches the selected model through the durable Session event", () => Effect.gen(function* () { const session = yield* Session.Service const previous = Model.Ref.make({ id: Model.ID.make("haiku"), providerID: Provider.ID.anthropic, variant: Model.VariantID.make("default"), }) const created = yield* session.create({ location, model: previous }) const model = Model.Ref.make({ id: Model.ID.make("sonnet"), providerID: Provider.ID.anthropic, variant: Model.VariantID.make("high"), })
yield* session.switchModel({ sessionID: created.id, model })
expect(yield* session.get(created.id)).toMatchObject({ model }) const bus = Array.from( yield* logEvents(session, created.id, true).pipe(Stream.drop(1), Stream.take(1), Stream.runCollect), ) expect(bus).toMatchObject([{ type: "session.model.selected" }]) expect(bus[0]?.data).toEqual({ sessionID: created.id, model, previous }) expect(yield* session.messages({ sessionID: created.id, order: "asc" })).toMatchObject([ { type: "model-switched", model, previous }, ]) }), )
it.effect("ignores a model switch when the selected model is unchanged", () => Effect.gen(function* () { const session = yield* Session.Service const created = yield* session.create({ location }) const model = Model.Ref.make({ id: Model.ID.make("sonnet"), providerID: Provider.ID.anthropic })
yield* session.switchModel({ sessionID: created.id, model }) yield* session.switchModel({ sessionID: created.id, model })
const { db } = yield* Database.Service expect( yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, created.id)).all().pipe(Effect.orDie), ).toHaveLength(2) expect(yield* session.get(created.id)).toMatchObject({ model }) }), )
it.effect("treats an omitted variant as the default variant", () => Effect.gen(function* () { const session = yield* Session.Service const model = Model.Ref.make({ id: Model.ID.make("sonnet"), providerID: Provider.ID.anthropic }) const created = yield* session.create({ location, model })
yield* session.switchModel({ sessionID: created.id, model: Model.Ref.make({ ...model, variant: Model.VariantID.make("default") }), })
const { db } = yield* Database.Service expect( yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, created.id)).all().pipe(Effect.orDie), ).toHaveLength(1) }), )
it.effect("rejects a model switch for a missing Session", () => Effect.gen(function* () { const session = yield* Session.Service const missing = Session.ID.make("ses_missing_model_switch")
expect( yield* session .switchModel({ sessionID: missing, model: Model.Ref.make({ id: Model.ID.make("sonnet"), providerID: Provider.ID.anthropic }), }) .pipe( Effect.flip, Effect.map((error) => error._tag), ), ).toBe("Session.NotFoundError") }), )})
describe("SessionTransfer", () => { it.effect("exports only settled projected messages", () => Effect.gen(function* () { const session = yield* Session.Service const transfer = yield* SessionTransfer.Service const bus = yield* Bus.Service const { db } = yield* Database.Service const source = yield* session.create({ location }) yield* session.prompt({ sessionID: source.id, text: "Settled", resume: false }) yield* SessionInbox.promote(db, bus, source.id, "steer") yield* bus.publish(SessionEvent.Step.Started, { sessionID: source.id, assistantMessageID: SessionMessage.ID.create(), agent: Agent.ID.make("build"), model: Model.Ref.make({ id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }), started: 0, }) yield* bus.publish(SessionEvent.Shell.Started, { sessionID: source.id, shell: Shell.Info.make({ id: Shell.ID.make("sh_transfer_export"), status: "running", command: "sleep 10", cwd: location.directory, shell: "/bin/sh", file: "/tmp/sh_transfer_export.out", metadata: {}, time: { started: 0 }, }), }) yield* bus.publish(SessionEvent.Compaction.Started, { sessionID: source.id, reason: "manual", recent: "pending", })
expect((yield* transfer.export({ sessionID: source.id })).messages).toMatchObject([Expected.user("Settled")]) }), )
it.effect("imports only settled projected messages", () => Effect.gen(function* () { const session = yield* Session.Service const transfer = yield* SessionTransfer.Service const { db } = yield* Database.Service const template = yield* session.create({ location, title: "Transfer source" }) const sessionID = Session.ID.create() const userID = SessionMessage.ID.create() const runningAssistantID = SessionMessage.ID.create() const completedAssistantID = SessionMessage.ID.create() const runningShellID = SessionMessage.ID.create() const completedShellID = SessionMessage.ID.create() const runningCompactionID = SessionMessage.ID.create() const completedCompactionID = SessionMessage.ID.create() const model = Model.Ref.make({ id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }) const providerState = { responseId: "summary-response" }
yield* transfer.import({ data: { info: { ...template, id: sessionID }, messages: [ { id: userID, type: "user", text: "Settled", time: { created: DateTime.makeUnsafe(1) } }, { id: runningAssistantID, type: "assistant", agent: Agent.ID.make("build"), model, content: [], time: { created: DateTime.makeUnsafe(2) }, }, { id: completedAssistantID, type: "assistant", agent: Agent.ID.make("build"), model, content: [], time: { created: DateTime.makeUnsafe(3), completed: DateTime.makeUnsafe(4) }, }, { id: runningShellID, type: "shell", shellID: Shell.ID.make("sh_transfer_running"), command: "sleep 10", status: "running", time: { created: DateTime.makeUnsafe(5) }, }, { id: completedShellID, type: "shell", shellID: Shell.ID.make("sh_transfer_completed"), command: "pwd", status: "exited", exit: 0, output: { output: "/project", cursor: 8, size: 8, truncated: false }, time: { created: DateTime.makeUnsafe(6), completed: DateTime.makeUnsafe(7) }, }, { id: runningCompactionID, type: "compaction", status: "running", reason: "manual", summary: "pending", recent: "pending", time: { created: DateTime.makeUnsafe(8) }, }, { id: completedCompactionID, type: "compaction", status: "completed", reason: "manual", model, providerState, summary: "summary", recent: "recent", time: { created: DateTime.makeUnsafe(9) }, }, ], }, location, })
expect((yield* session.messages({ sessionID, order: "asc" })).map((message) => message.id)).toEqual([ userID, completedAssistantID, completedShellID, completedCompactionID, ]) expect(yield* Bus.latestSequence(db, sessionID)).toBe(4) expect((yield* transfer.export({ sessionID })).messages.at(-1)).toMatchObject({ model, providerState }) expect((yield* transfer.export({ sessionID, sanitize: true })).messages.at(-1)).toMatchObject({ model, providerState: { redacted: `compaction-provider-state:${completedCompactionID}` }, }) }), )
it.effect("imports projected messages and reserves their aggregate sequence", () => Effect.gen(function* () { const session = yield* Session.Service const transfer = yield* SessionTransfer.Service const bus = yield* Bus.Service const { db } = yield* Database.Service const template = yield* session.create({ location, title: "Exported", metadata: { channel: "C123" }, permissions: [{ action: "edit", resource: "*", effect: "deny" }], }) const sessionID = Session.ID.create() const sourceMessageID = SessionMessage.ID.create() const errorMessageID = SessionMessage.ID.create() yield* TestClock.setTime(1_000)
const imported = yield* transfer.import({ data: { info: { ...template, id: sessionID, time: { ...template.time, updated: DateTime.makeUnsafe(100), idle: DateTime.makeUnsafe(200), viewed: DateTime.makeUnsafe(150), }, }, messages: [ { id: sourceMessageID, type: "user", text: "Imported message", skills: [ { id: Skill.ID.make("effect"), name: Skill.Name.make("Effect"), text: "Private skill instructions from /private/project", }, ], time: { created: DateTime.makeUnsafe(100) }, }, { id: errorMessageID, type: "compaction", status: "failed", reason: "manual", error: { type: "test_error", message: "Original error" }, time: { created: DateTime.makeUnsafe(101) }, }, ], }, location, }) const messages = yield* session.messages({ sessionID, order: "asc" })
expect(imported).toMatchObject({ id: sessionID, title: "Exported", location, metadata: { channel: "C123" }, permissions: [{ action: "edit", resource: "*", effect: "deny" }], }) expect(imported.time).toMatchObject({ updated: DateTime.makeUnsafe(1_000), idle: DateTime.makeUnsafe(200), viewed: DateTime.makeUnsafe(150), }) expect(messages).toMatchObject([ { id: sourceMessageID, ...Expected.user("Imported message") }, { id: errorMessageID, type: "compaction", error: { type: "test_error", message: "Original error" } }, ]) expect(yield* Bus.latestSequence(db, sessionID)).toBe(2) const exported = yield* transfer.export({ sessionID }) expect(exported.info.time).toMatchObject({ idle: DateTime.makeUnsafe(200), viewed: DateTime.makeUnsafe(150) }) expect(exported.messages).toEqual(messages) const sanitized = yield* transfer.export({ sessionID, sanitize: true }) expect(sanitized.info.time).toMatchObject({ idle: DateTime.makeUnsafe(200), viewed: DateTime.makeUnsafe(150) }) expect(sanitized.info.metadata).toEqual({ redacted: `session-metadata:${sessionID}` }) expect(sanitized.messages).toMatchObject([ { id: sourceMessageID, text: `[redacted:text:${sourceMessageID}]`, skills: [{ id: "effect", name: "[redacted:skill-name:0]", text: "[redacted:skill:0]" }], }, { id: errorMessageID, error: { type: "test_error", message: "Original error" } }, ])
yield* session.prompt({ sessionID, text: "Continue", resume: false }) yield* SessionInbox.promote(db, bus, sessionID, "steer")
expect((yield* session.messages({ sessionID, order: "asc" })).map((message) => message.type)).toEqual([ "user", "compaction", "user", ]) expect(yield* Bus.latestSequence(db, sessionID)).toBe(4) }), )
it.effect("rejects an existing session ID without changing its transcript", () => Effect.gen(function* () { const session = yield* Session.Service const transfer = yield* SessionTransfer.Service const existing = yield* session.create({ location, title: "Existing" }) const exit = yield* Effect.exit(transfer.import({ data: { info: existing, messages: [] }, location }))
expect(exit._tag).toBe("Failure") expect((yield* session.get(existing.id)).title).toBe("Existing") expect(yield* session.messages({ sessionID: existing.id })).toEqual([]) }), )
it.effect("clamps an imported viewed watermark to its idle transition", () => Effect.gen(function* () { const session = yield* Session.Service const transfer = yield* SessionTransfer.Service const template = yield* session.create({ location }) const imported = yield* transfer.import({ data: { info: { ...template, id: Session.ID.create(), outcome: "succeeded", time: { ...template.time, idle: DateTime.makeUnsafe(200), viewed: DateTime.makeUnsafe(250), }, }, messages: [], }, location, })
expect(imported.time).toMatchObject({ idle: DateTime.makeUnsafe(200), viewed: DateTime.makeUnsafe(200) }) expect(imported.outcome).toBe("succeeded") }), )})