chore: generate

This commit is contained in:
opencode-agent[bot]
2026-06-24 23:43:02 +00:00
parent 00591ebff4
commit 2a03904f0d
14 changed files with 1587 additions and 492 deletions
+10 -10
View File
@@ -96,8 +96,8 @@ export const layer = Layer.effect(
if (patch) { if (patch) {
const repository = yield* git.repo.discover(directory) const repository = yield* git.repo.discover(directory)
if (!repository) return yield* new ApplyChangesError({ message: "Destination is not a Git repository" }) if (!repository) return yield* new ApplyChangesError({ message: "Destination is not a Git repository" })
yield* git yield* git.change
.change.apply({ repository, path: directory, changes: patch }) .apply({ repository, path: directory, changes: patch })
.pipe(Effect.mapError((error) => new ApplyChangesError({ message: error.message }))) .pipe(Effect.mapError((error) => new ApplyChangesError({ message: error.message })))
} }
@@ -123,14 +123,14 @@ export const layer = Layer.effect(
untracked: "remove", untracked: "remove",
}) })
.pipe( .pipe(
Effect.mapError( Effect.mapError(
(error) => (error) =>
new ResetSourceChangesError({ new ResetSourceChangesError({
directory: current.location.directory, directory: current.location.directory,
message: error.message, message: error.message,
cause: error.cause, cause: error.cause,
}), }),
), ),
) )
} }
}) })
+180 -179
View File
@@ -244,7 +244,10 @@ export const layer = Layer.effect(
directory: AbsolutePath, directory: AbsolutePath,
args: string[], args: string[],
) { ) {
const result = yield* execute(directory, proc)(args).pipe( const result = yield* execute(
directory,
proc,
)(args).pipe(
Effect.mapError((cause) => new OperationError({ operation, directory, message: cause.message, cause })), Effect.mapError((cause) => new OperationError({ operation, directory, message: cause.message, cause })),
) )
if (result.exitCode === 0) return if (result.exitCode === 0) return
@@ -306,10 +309,7 @@ export const layer = Layer.effect(
]) ])
}) })
const reset = Effect.fn("Git.sync.resetHard")(function* ( const reset = Effect.fn("Git.sync.resetHard")(function* (repository: Repository, revision: string) {
repository: Repository,
revision: string,
) {
yield* operation("reset", repository.worktree, ["reset", "--hard", revision]) yield* operation("reset", repository.worktree, ["reset", "--hard", revision])
}) })
@@ -404,20 +404,22 @@ export const layer = Layer.effect(
}), }),
), ),
) )
yield* fs.writeFileString( yield* fs
path.join(input.gitDirectory, "objects", "info", "alternates"), .writeFileString(
path.join(input.seed.commonDirectory, "objects") + "\n", path.join(input.gitDirectory, "objects", "info", "alternates"),
).pipe( path.join(input.seed.commonDirectory, "objects") + "\n",
Effect.mapError( )
(cause) => .pipe(
new OperationError({ Effect.mapError(
operation: "create", (cause) =>
directory: input.gitDirectory, new OperationError({
message: "Failed to configure shared Git objects", operation: "create",
cause, directory: input.gitDirectory,
}), message: "Failed to configure shared Git objects",
), cause,
) }),
),
)
yield* fs yield* fs
.copyFile(path.join(input.seed.gitDirectory, "index"), path.join(input.gitDirectory, "index")) .copyFile(path.join(input.seed.gitDirectory, "index"), path.join(input.gitDirectory, "index"))
.pipe(Effect.catch(() => Effect.void)) .pipe(Effect.catch(() => Effect.void))
@@ -445,14 +447,9 @@ export const layer = Layer.effect(
if (!candidates.length) return { skipped: [] } if (!candidates.length) return { skipped: [] }
const ignored = input.ignores const ignored = input.ignores
? new Set( ? new Set(
( (yield* repositoryOperation("refresh", input.ignores, ["check-ignore", "--no-index", "--stdin", "-z"], {
yield* repositoryOperation( stdin: candidates.join("\0") + "\0",
"refresh", }).pipe(Effect.catch(() => Effect.succeed({ text: "", stderr: "" })))).text
input.ignores,
["check-ignore", "--no-index", "--stdin", "-z"],
{ stdin: candidates.join("\0") + "\0" },
).pipe(Effect.catch(() => Effect.succeed({ text: "", stderr: "" })))
).text
.split("\0") .split("\0")
.filter(Boolean), .filter(Boolean),
) )
@@ -460,19 +457,17 @@ export const layer = Layer.effect(
const allowed = candidates.filter((item) => !ignored.has(item)) const allowed = candidates.filter((item) => !ignored.has(item))
const maximum = input.maximumUntrackedFileBytes const maximum = input.maximumUntrackedFileBytes
const skipped = maximum const skipped = maximum
? ( ? (yield* Effect.forEach(
yield* Effect.forEach( untracked.filter((item) => allowed.includes(item)),
untracked.filter((item) => allowed.includes(item)), (item) =>
(item) => fs.stat(path.join(input.repository.worktree, item)).pipe(
fs.stat(path.join(input.repository.worktree, item)).pipe( Effect.map((info) =>
Effect.map((info) => info.type === "File" && Number(info.size) > maximum ? RelativePath.make(item) : undefined,
info.type === "File" && Number(info.size) > maximum ? RelativePath.make(item) : undefined,
),
Effect.catch(() => Effect.succeed(undefined)),
), ),
{ concurrency: 8 }, Effect.catch(() => Effect.succeed(undefined)),
) ),
).filter((item): item is RelativePath => item !== undefined) { concurrency: 8 },
)).filter((item): item is RelativePath => item !== undefined)
: [] : []
const stage = allowed.filter((item) => !skipped.includes(RelativePath.make(item))) const stage = allowed.filter((item) => !skipped.includes(RelativePath.make(item)))
const remove = [...ignored, ...skipped] const remove = [...ignored, ...skipped]
@@ -500,11 +495,10 @@ export const layer = Layer.effect(
if (!input.paths.length) return new Set<RelativePath>() if (!input.paths.length) return new Set<RelativePath>()
const result = yield* proc const result = yield* proc
.run( .run(
ChildProcess.make( ChildProcess.make("git", repositoryArgs(input.repository, ["check-ignore", "--no-index", "--stdin", "-z"]), {
"git", cwd: input.repository.worktree,
repositoryArgs(input.repository, ["check-ignore", "--no-index", "--stdin", "-z"]), extendEnv: true,
{ cwd: input.repository.worktree, extendEnv: true }, }),
),
{ stdin: input.paths.join("\0") + "\0" }, { stdin: input.paths.join("\0") + "\0" },
) )
.pipe( .pipe(
@@ -537,23 +531,20 @@ export const layer = Layer.effect(
return TreeID.make((yield* repositoryOperation("write_tree", repository, ["write-tree"])).text.trim()) return TreeID.make((yield* repositoryOperation("write_tree", repository, ["write-tree"])).text.trim())
}) })
const captureTree = Effect.fn("Git.tree.capture")((input: { const captureTree = Effect.fn("Git.tree.capture")(
repository: Repository (input: {
scopes: readonly RelativePath[] repository: Repository
ignores?: Repository scopes: readonly RelativePath[]
maximumUntrackedFileBytes?: number ignores?: Repository
}) => maximumUntrackedFileBytes?: number
locked( }) =>
input.repository, locked(
Effect.gen(function* () { input.repository,
yield* Effect.forEach( Effect.gen(function* () {
input.scopes, yield* Effect.forEach(input.scopes, (scope) => refresh({ ...input, scope }), { discard: true })
(scope) => refresh({ ...input, scope }), return yield* writeTree(input.repository)
{ discard: true }, }),
) ),
return yield* writeTree(input.repository)
}),
),
) )
const treeFiles = Effect.fn("Git.tree.files")(function* (input: { const treeFiles = Effect.fn("Git.tree.files")(function* (input: {
@@ -561,8 +552,14 @@ export const layer = Layer.effect(
from: TreeID from: TreeID
to: TreeID to: TreeID
}) { }) {
return (yield* repositoryOperation("list_files", input.repository, ["diff", "--name-only", "-z", input.from, input.to])) return (yield* repositoryOperation("list_files", input.repository, [
.text.split("\0") "diff",
"--name-only",
"-z",
input.from,
input.to,
])).text
.split("\0")
.filter(Boolean) .filter(Boolean)
.map((file) => RelativePath.make(file)) .map((file) => RelativePath.make(file))
}) })
@@ -577,43 +574,37 @@ export const layer = Layer.effect(
const paths = input.paths ?? (yield* treeFiles(input)) const paths = input.paths ?? (yield* treeFiles(input))
return yield* Effect.forEach(paths, (file) => return yield* Effect.forEach(paths, (file) =>
Effect.gen(function* () { Effect.gen(function* () {
const statusText = ( const statusText = (yield* repositoryOperation("diff", input.repository, [
yield* repositoryOperation("diff", input.repository, [ "diff",
"diff", "--name-status",
"--name-status", "--no-renames",
"--no-renames", input.from,
input.from, input.to,
input.to, "--",
"--", file,
file, ])).text.trim()
])
).text.trim()
const status = statusText.startsWith("A") ? "added" : statusText.startsWith("D") ? "deleted" : "modified" const status = statusText.startsWith("A") ? "added" : statusText.startsWith("D") ? "deleted" : "modified"
const stats = ( const stats = (yield* repositoryOperation("diff", input.repository, [
yield* repositoryOperation("diff", input.repository, [ "diff",
"diff", "--numstat",
"--numstat", "--no-renames",
"--no-renames", input.from,
input.from, input.to,
input.to, "--",
"--", file,
file, ])).text.split("\t")
])
).text.split("\t")
const binary = stats[0] === "-" || stats[1] === "-" const binary = stats[0] === "-" || stats[1] === "-"
const patch = binary const patch = binary
? "" ? ""
: ( : (yield* repositoryOperation("diff", input.repository, [
yield* repositoryOperation("diff", input.repository, [ "diff",
"diff", `--unified=${input.context ?? 3}`,
`--unified=${input.context ?? 3}`, "--no-renames",
"--no-renames", input.from,
input.from, input.to,
input.to, "--",
"--", file,
file, ])).text
])
).text
return { return {
path: file, path: file,
status, status,
@@ -626,87 +617,89 @@ export const layer = Layer.effect(
}) })
const entry = Effect.fnUntraced(function* (repository: Repository, tree: TreeID, file: RelativePath) { const entry = Effect.fnUntraced(function* (repository: Repository, tree: TreeID, file: RelativePath) {
const text = ( const text = (yield* repositoryOperation("restore", repository, [
yield* repositoryOperation("restore", repository, ["ls-tree", "-z", tree, "--", file]) "ls-tree",
).text.replace(/\0$/, "") "-z",
tree,
"--",
file,
])).text.replace(/\0$/, "")
if (!text) return if (!text) return
const match = text.match(/^(\d+)\s+\w+\s+([0-9a-f]+)\t/) const match = text.match(/^(\d+)\s+\w+\s+([0-9a-f]+)\t/)
if (!match) return yield* new OperationError({ if (!match)
operation: "restore", return yield* new OperationError({
directory: repository.worktree, operation: "restore",
message: `Invalid tree entry for ${file}`, directory: repository.worktree,
}) message: `Invalid tree entry for ${file}`,
})
return { mode: match[1], object: match[2] } return { mode: match[1], object: match[2] }
}) })
const preview = Effect.fn("Git.tree.preview")((input: { const preview = Effect.fn("Git.tree.preview")(
repository: Repository (input: {
current: TreeID repository: Repository
files: ReadonlyMap<RelativePath, TreeID> current: TreeID
context?: number files: ReadonlyMap<RelativePath, TreeID>
}) => context?: number
locked( }) =>
input.repository, locked(
Effect.gen(function* () { input.repository,
const index = path.join(input.repository.gitDirectory, `preview-${randomUUID()}.index`) Effect.gen(function* () {
const env = { GIT_INDEX_FILE: index } const index = path.join(input.repository.gitDirectory, `preview-${randomUUID()}.index`)
return yield* Effect.gen(function* () { const env = { GIT_INDEX_FILE: index }
yield* repositoryOperation("diff", input.repository, ["read-tree", input.current], { env }) return yield* Effect.gen(function* () {
yield* Effect.forEach( yield* repositoryOperation("diff", input.repository, ["read-tree", input.current], { env })
input.files, yield* Effect.forEach(
([file, tree]) => input.files,
Effect.gen(function* () { ([file, tree]) =>
const source = yield* entry(input.repository, tree, file) Effect.gen(function* () {
if (!source) { const source = yield* entry(input.repository, tree, file)
if (!source) {
yield* repositoryOperation(
"diff",
input.repository,
["update-index", "--force-remove", "--", file],
{ env },
)
return
}
yield* repositoryOperation( yield* repositoryOperation(
"diff", "diff",
input.repository, input.repository,
["update-index", "--force-remove", "--", file], ["update-index", "--add", "--cacheinfo", source.mode, source.object, file],
{ env }, { env },
) )
return }),
} { discard: true },
yield* repositoryOperation( )
"diff", const target = TreeID.make(
input.repository, (yield* repositoryOperation("diff", input.repository, ["write-tree"], { env })).text.trim(),
["update-index", "--add", "--cacheinfo", source.mode, source.object, file], )
{ env }, return yield* treeDiff({
) repository: input.repository,
}), from: input.current,
{ discard: true }, to: target,
) context: input.context,
const target = TreeID.make( paths: Array.from(input.files.keys()),
(yield* repositoryOperation("diff", input.repository, ["write-tree"], { env })).text.trim(), })
) }).pipe(Effect.ensuring(fs.remove(index).pipe(Effect.catch(() => Effect.void))))
return yield* treeDiff({ }),
repository: input.repository, ),
from: input.current,
to: target,
context: input.context,
paths: Array.from(input.files.keys()),
})
}).pipe(Effect.ensuring(fs.remove(index).pipe(Effect.catch(() => Effect.void))))
}),
),
) )
const restore = Effect.fn("Git.tree.restore")((input: { const restore = Effect.fn("Git.tree.restore")(
repository: Repository (input: { repository: Repository; files: ReadonlyMap<RelativePath, TreeID> }) =>
files: ReadonlyMap<RelativePath, TreeID> locked(
}) => input.repository,
locked( Effect.forEach(
input.repository, input.files,
Effect.forEach( ([file, tree]) =>
input.files, Effect.gen(function* () {
([file, tree]) => if (yield* entry(input.repository, tree, file)) {
Effect.gen(function* () { yield* repositoryOperation("restore", input.repository, ["checkout", tree, "--", file])
if (yield* entry(input.repository, tree, file)) { return
yield* repositoryOperation("restore", input.repository, ["checkout", tree, "--", file]) }
return yield* fs.remove(path.join(input.repository.worktree, file), { recursive: true, force: true }).pipe(
}
yield* fs
.remove(path.join(input.repository.worktree, file), { recursive: true, force: true })
.pipe(
Effect.mapError( Effect.mapError(
(cause) => (cause) =>
new OperationError({ new OperationError({
@@ -717,10 +710,10 @@ export const layer = Layer.effect(
}), }),
), ),
) )
}), }),
{ discard: true }, { discard: true },
),
), ),
),
) )
const checkoutTree = Effect.fn("Git.tree.checkout")((input: { repository: Repository; tree: TreeID }) => const checkoutTree = Effect.fn("Git.tree.checkout")((input: { repository: Repository; tree: TreeID }) =>
@@ -733,10 +726,7 @@ export const layer = Layer.effect(
), ),
) )
const capture = Effect.fn("Git.change.capture")(function* (input: { const capture = Effect.fn("Git.change.capture")(function* (input: { repository: Repository; path: AbsolutePath }) {
repository: Repository
path: AbsolutePath
}) {
const scope = path.relative(input.repository.worktree, input.path).replaceAll("\\", "/") || "." const scope = path.relative(input.repository.worktree, input.path).replaceAll("\\", "/") || "."
const tracked = yield* execute( const tracked = yield* execute(
input.repository.worktree, input.repository.worktree,
@@ -811,8 +801,7 @@ export const layer = Layer.effect(
) )
.pipe( .pipe(
Effect.mapError( Effect.mapError(
(cause) => (cause) => new PatchError({ operation: "apply", directory: input.path, message: cause.message, cause }),
new PatchError({ operation: "apply", directory: input.path, message: cause.message, cause }),
), ),
) )
if (result.exitCode === 0) return if (result.exitCode === 0) return
@@ -831,9 +820,10 @@ export const layer = Layer.effect(
untracked: "preserve" | "remove" untracked: "preserve" | "remove"
}) { }) {
const scope = path.relative(input.repository.worktree, input.path).replaceAll("\\", "/") || "." const scope = path.relative(input.repository.worktree, input.path).replaceAll("\\", "/") || "."
const restore = yield* execute(input.repository.worktree, proc)( const restore = yield* execute(
input.index === "reset" ? ["checkout", "HEAD", "--", scope] : ["checkout", "--", scope], input.repository.worktree,
).pipe( proc,
)(input.index === "reset" ? ["checkout", "HEAD", "--", scope] : ["checkout", "--", scope]).pipe(
Effect.mapError( Effect.mapError(
(cause) => new PatchError({ operation: "reset", directory: input.path, message: cause.message, cause }), (cause) => new PatchError({ operation: "reset", directory: input.path, message: cause.message, cause }),
), ),
@@ -846,7 +836,10 @@ export const layer = Layer.effect(
}) })
} }
if (input.untracked === "preserve") return if (input.untracked === "preserve") return
const clean = yield* execute(input.repository.worktree, proc)(["clean", "-fd", "--", scope]).pipe( const clean = yield* execute(
input.repository.worktree,
proc,
)(["clean", "-fd", "--", scope]).pipe(
Effect.mapError( Effect.mapError(
(cause) => new PatchError({ operation: "reset", directory: input.path, message: cause.message, cause }), (cause) => new PatchError({ operation: "reset", directory: input.path, message: cause.message, cause }),
), ),
@@ -937,7 +930,15 @@ export const layer = Layer.effect(
change: { capture, apply, discard }, change: { capture, apply, discard },
worktree: { create: worktreeCreate, remove: worktreeRemove, list: worktreeList }, worktree: { create: worktreeCreate, remove: worktreeRemove, list: worktreeList },
index: { refresh, ignored }, index: { refresh, ignored },
tree: { capture: captureTree, write: writeTree, files: treeFiles, diff: treeDiff, preview, restore, checkout: checkoutTree }, tree: {
capture: captureTree,
write: writeTree,
files: treeFiles,
diff: treeDiff,
preview,
restore,
checkout: checkoutTree,
},
}) })
}), }),
) )
+2 -1
View File
@@ -169,7 +169,8 @@ export const layer: Layer.Layer<Service, never, FSUtil.Service | Git.Service | E
} }
if (status === "refreshed") { if (status === "refreshed") {
if (!existing) return yield* new FetchFailedError({ repository, message: "Repository is unavailable" }) if (!existing)
return yield* new FetchFailedError({ repository, message: "Repository is unavailable" })
yield* git.sync yield* git.sync
.fetchRemotes(existing) .fetchRemotes(existing)
.pipe(Effect.mapError((error) => new FetchFailedError({ repository, message: error.message }))) .pipe(Effect.mapError((error) => new FetchFailedError({ repository, message: error.message })))
+260 -254
View File
@@ -172,261 +172,265 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
export const layer = Layer.unwrap( export const layer = Layer.unwrap(
Effect.promise(() => import("./location-layer")).pipe( Effect.promise(() => import("./location-layer")).pipe(
Effect.map(({ LocationServiceMap }) => Layer.effect( Effect.map(({ LocationServiceMap }) =>
Service, Layer.effect(
Effect.gen(function* () { Service,
const database = yield* Database.Service Effect.gen(function* () {
const db = database.db const database = yield* Database.Service
const events = yield* EventV2.Service const db = database.db
const projects = yield* ProjectV2.Service const events = yield* EventV2.Service
const execution = yield* SessionExecution.Service const projects = yield* ProjectV2.Service
const store = yield* SessionStore.Service const execution = yield* SessionExecution.Service
const locations = yield* LocationServiceMap const store = yield* SessionStore.Service
const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message) const locations = yield* LocationServiceMap
const isDurableSessionEvent = Schema.is(SessionEvent.Durable) const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message)
const decode = (row: typeof SessionMessageTable.$inferSelect) => const isDurableSessionEvent = Schema.is(SessionEvent.Durable)
decodeMessage({ ...row.data, id: row.id, type: row.type }).pipe( const decode = (row: typeof SessionMessageTable.$inferSelect) =>
Effect.mapError( decodeMessage({ ...row.data, id: row.id, type: row.type }).pipe(
() => Effect.mapError(
new MessageDecodeError({ () =>
sessionID: SessionSchema.ID.make(row.session_id), new MessageDecodeError({
messageID: SessionMessage.ID.make(row.id), sessionID: SessionSchema.ID.make(row.session_id),
}), messageID: SessionMessage.ID.make(row.id),
), }),
)
const result = Service.of({
create: Effect.fn("V2Session.create")(function* (input) {
const sessionID = input.id ?? SessionSchema.ID.create()
const recorded = yield* store.get(sessionID)
if (recorded) return recorded
const project = yield* projects.resolve(input.location.directory)
yield* db
.insert(ProjectTable)
.values({ id: project.id, worktree: project.directory, vcs: project.vcs?.type, sandboxes: [] })
.onConflictDoNothing()
.run()
.pipe(Effect.orDie)
const now = Date.now()
const info = SessionV1.SessionInfo.make({
id: sessionID,
slug: Slug.create(),
version: InstallationVersion,
projectID: project.id,
directory: input.location.directory,
path: path.relative(project.directory, input.location.directory).replaceAll("\\", "/"),
workspaceID: input.location.workspaceID ? WorkspaceV2.ID.make(input.location.workspaceID) : undefined,
title: `New session - ${new Date(now).toISOString()}`,
agent: input.agent,
model: input.model
? {
id: ModelV2.ID.make(input.model.id),
providerID: input.model.providerID,
variant: input.model.variant,
}
: undefined,
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: now, updated: now },
})
const projected = yield* events
.publish(SessionV1.Event.Created, { sessionID, info }, { location: input.location })
.pipe(
Effect.as({ type: "created" } as const),
Effect.catchDefect((defect) => {
if (!(defect instanceof SessionProjector.SessionAlreadyProjected)) {
return Effect.die(defect)
}
// Concurrent creation lost the projection race. The existing Session identity wins.
return store
.get(sessionID)
.pipe(
Effect.flatMap((session) =>
session ? Effect.succeed({ type: "existing", session } as const) : Effect.die(defect),
),
)
}),
)
if (projected.type === "existing") return projected.session
// TODO: Restore recorded sessions onto replacement synchronized workspaces in a future API slice.
return yield* result.get(sessionID).pipe(Effect.orDie)
}),
get: Effect.fn("V2Session.get")(function* (sessionID) {
const session = yield* store.get(sessionID)
if (!session) return yield* new NotFoundError({ sessionID })
return session
}),
list: Effect.fn("V2Session.list")(function* (input = {}) {
const direction = input.anchor?.direction ?? "next"
const requestedOrder = input.order ?? "desc"
const order = direction === "previous" ? (requestedOrder === "asc" ? "desc" : "asc") : requestedOrder
const sortColumn = SessionTable.time_created
const conditions: SQL[] = []
if ("directory" in input) conditions.push(eq(SessionTable.directory, input.directory))
if (input.workspaceID) conditions.push(eq(SessionTable.workspace_id, input.workspaceID))
if ("project" in input) conditions.push(eq(SessionTable.project_id, input.project))
if (input.search) conditions.push(like(SessionTable.title, `%${input.search}%`))
if (input.anchor) {
conditions.push(
order === "asc"
? or(
gt(sortColumn, input.anchor.time),
and(eq(sortColumn, input.anchor.time), gt(SessionTable.id, input.anchor.id)),
)!
: or(
lt(sortColumn, input.anchor.time),
and(eq(sortColumn, input.anchor.time), lt(SessionTable.id, input.anchor.id)),
)!,
)
}
const query = db
.select()
.from(SessionTable)
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(
order === "asc" ? asc(sortColumn) : desc(sortColumn),
order === "asc" ? asc(SessionTable.id) : desc(SessionTable.id),
)
const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe(
Effect.orDie,
)
return (direction === "previous" ? rows.toReversed() : rows).map((row) => fromRow(row))
}),
messages: Effect.fn("V2Session.messages")(function* (input) {
yield* result.get(input.sessionID)
const direction = input.cursor?.direction ?? "next"
const requestedOrder = input.order ?? "desc"
const order = direction === "previous" ? (requestedOrder === "asc" ? "desc" : "asc") : requestedOrder
const anchor = input.cursor
? yield* db
.select({ seq: SessionMessageTable.seq })
.from(SessionMessageTable)
.where(
and(eq(SessionMessageTable.session_id, input.sessionID), eq(SessionMessageTable.id, input.cursor.id)),
)
.get()
.pipe(Effect.orDie)
: undefined
if (input.cursor && !anchor) return []
const boundary = anchor
? order === "asc"
? gt(SessionMessageTable.seq, anchor.seq)
: lt(SessionMessageTable.seq, anchor.seq)
: undefined
const where = boundary
? and(eq(SessionMessageTable.session_id, input.sessionID), boundary)
: eq(SessionMessageTable.session_id, input.sessionID)
const query = db
.select()
.from(SessionMessageTable)
.where(where)
.orderBy(order === "asc" ? asc(SessionMessageTable.seq) : desc(SessionMessageTable.seq))
const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe(
Effect.orDie,
)
return yield* Effect.forEach(direction === "previous" ? rows.toReversed() : rows, decode)
}),
message: Effect.fn("V2Session.message")(function* (input) {
const stored = yield* store.message(input.messageID)
return stored?.sessionID === input.sessionID ? stored.message : undefined
}),
context: Effect.fn("V2Session.context")(function* (sessionID) {
yield* result.get(sessionID)
return yield* store.context(sessionID)
}),
events: (input) =>
Stream.unwrap(
result
.get(input.sessionID)
.pipe(Effect.as(events.durable({ aggregateID: input.sessionID, after: input.after }))),
).pipe(Stream.filter((event): event is SessionEvent.DurableEvent => isDurableSessionEvent(event))),
prompt: Effect.fn("V2Session.prompt")((input) =>
Effect.uninterruptible(
Effect.gen(function* () {
yield* result.get(input.sessionID)
const messageID = input.id ?? SessionMessage.ID.create()
const delivery = input.delivery ?? "steer"
const expected = { sessionID: input.sessionID, messageID, prompt: input.prompt, delivery }
const admitted = yield* SessionInput.admit(db, events, {
id: messageID,
sessionID: input.sessionID,
prompt: input.prompt,
delivery,
}).pipe(
Effect.catchDefect((defect) =>
defect instanceof SessionInput.LifecycleConflict
? new PromptConflictError({ sessionID: input.sessionID, messageID })
: Effect.die(defect),
), ),
) )
if (!SessionInput.equivalent(admitted, expected))
return yield* new PromptConflictError({ sessionID: input.sessionID, messageID }) const result = Service.of({
if (input.resume !== false) yield* execution.wake(admitted.sessionID) create: Effect.fn("V2Session.create")(function* (input) {
return admitted const sessionID = input.id ?? SessionSchema.ID.create()
}), const recorded = yield* store.get(sessionID)
), if (recorded) return recorded
), const project = yield* projects.resolve(input.location.directory)
shell: Effect.fn("V2Session.shell")(function* () { yield* db
return yield* new OperationUnavailableError({ operation: "shell" }) .insert(ProjectTable)
}), .values({ id: project.id, worktree: project.directory, vcs: project.vcs?.type, sandboxes: [] })
skill: Effect.fn("V2Session.skill")(function* () { .onConflictDoNothing()
return yield* new OperationUnavailableError({ operation: "skill" }) .run()
}), .pipe(Effect.orDie)
switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) { const now = Date.now()
yield* result.get(input.sessionID) const info = SessionV1.SessionInfo.make({
yield* events.publish(SessionEvent.AgentSwitched, { id: sessionID,
sessionID: input.sessionID, slug: Slug.create(),
messageID: SessionMessage.ID.create(), version: InstallationVersion,
timestamp: yield* DateTime.now, projectID: project.id,
agent: input.agent, directory: input.location.directory,
}) path: path.relative(project.directory, input.location.directory).replaceAll("\\", "/"),
}), workspaceID: input.location.workspaceID ? WorkspaceV2.ID.make(input.location.workspaceID) : undefined,
switchModel: Effect.fn("V2Session.switchModel")(function* (input) { title: `New session - ${new Date(now).toISOString()}`,
yield* result.get(input.sessionID) agent: input.agent,
yield* events.publish(SessionEvent.ModelSwitched, { model: input.model
sessionID: input.sessionID, ? {
messageID: SessionMessage.ID.create(), id: ModelV2.ID.make(input.model.id),
timestamp: yield* DateTime.now, providerID: input.model.providerID,
model: input.model, variant: input.model.variant,
}) }
}), : undefined,
compact: Effect.fn("V2Session.compact")(function* (input) { cost: 0,
yield* result.get(input.sessionID) tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
return yield* new OperationUnavailableError({ operation: "compact" }) time: { created: now, updated: now },
}), })
wait: Effect.fn("V2Session.wait")(function* (sessionID) { const projected = yield* events
yield* result.get(sessionID) .publish(SessionV1.Event.Created, { sessionID, info }, { location: input.location })
return yield* new OperationUnavailableError({ operation: "wait" }) .pipe(
}), Effect.as({ type: "created" } as const),
resume: Effect.fn("V2Session.resume")(function* (sessionID) { Effect.catchDefect((defect) => {
yield* result.get(sessionID) if (!(defect instanceof SessionProjector.SessionAlreadyProjected)) {
yield* execution.resume(sessionID) return Effect.die(defect)
}), }
interrupt: Effect.fn("V2Session.interrupt")((sessionID) => // Concurrent creation lost the projection race. The existing Session identity wins.
Effect.uninterruptible(execution.interrupt(sessionID)), return store
), .get(sessionID)
revert: { .pipe(
stage: Effect.fn("V2Session.revert.stage")(function* (input) { Effect.flatMap((session) =>
const session = yield* result.get(input.sessionID) session ? Effect.succeed({ type: "existing", session } as const) : Effect.die(defect),
return yield* SessionRevert.stage({ session, messageID: input.messageID, files: input.files }).pipe( ),
Effect.provideService(Database.Service, database), )
Effect.provideService(EventV2.Service, events), }),
Effect.provide(locations.get(session.location)), )
) if (projected.type === "existing") return projected.session
}), // TODO: Restore recorded sessions onto replacement synchronized workspaces in a future API slice.
clear: Effect.fn("V2Session.revert.clear")(function* (sessionID) { return yield* result.get(sessionID).pipe(Effect.orDie)
const session = yield* result.get(sessionID) }),
yield* SessionRevert.clear(session).pipe( get: Effect.fn("V2Session.get")(function* (sessionID) {
Effect.provideService(EventV2.Service, events), const session = yield* store.get(sessionID)
Effect.provide(locations.get(session.location)), if (!session) return yield* new NotFoundError({ sessionID })
) return session
}), }),
commit: Effect.fn("V2Session.revert.commit")(function* (sessionID) { list: Effect.fn("V2Session.list")(function* (input = {}) {
const session = yield* result.get(sessionID) const direction = input.anchor?.direction ?? "next"
yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events)) const requestedOrder = input.order ?? "desc"
}), const order = direction === "previous" ? (requestedOrder === "asc" ? "desc" : "asc") : requestedOrder
}, const sortColumn = SessionTable.time_created
}) const conditions: SQL[] = []
if ("directory" in input) conditions.push(eq(SessionTable.directory, input.directory))
if (input.workspaceID) conditions.push(eq(SessionTable.workspace_id, input.workspaceID))
if ("project" in input) conditions.push(eq(SessionTable.project_id, input.project))
if (input.search) conditions.push(like(SessionTable.title, `%${input.search}%`))
if (input.anchor) {
conditions.push(
order === "asc"
? or(
gt(sortColumn, input.anchor.time),
and(eq(sortColumn, input.anchor.time), gt(SessionTable.id, input.anchor.id)),
)!
: or(
lt(sortColumn, input.anchor.time),
and(eq(sortColumn, input.anchor.time), lt(SessionTable.id, input.anchor.id)),
)!,
)
}
const query = db
.select()
.from(SessionTable)
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(
order === "asc" ? asc(sortColumn) : desc(sortColumn),
order === "asc" ? asc(SessionTable.id) : desc(SessionTable.id),
)
const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe(
Effect.orDie,
)
return (direction === "previous" ? rows.toReversed() : rows).map((row) => fromRow(row))
}),
messages: Effect.fn("V2Session.messages")(function* (input) {
yield* result.get(input.sessionID)
const direction = input.cursor?.direction ?? "next"
const requestedOrder = input.order ?? "desc"
const order = direction === "previous" ? (requestedOrder === "asc" ? "desc" : "asc") : requestedOrder
const anchor = input.cursor
? yield* db
.select({ seq: SessionMessageTable.seq })
.from(SessionMessageTable)
.where(
and(
eq(SessionMessageTable.session_id, input.sessionID),
eq(SessionMessageTable.id, input.cursor.id),
),
)
.get()
.pipe(Effect.orDie)
: undefined
if (input.cursor && !anchor) return []
const boundary = anchor
? order === "asc"
? gt(SessionMessageTable.seq, anchor.seq)
: lt(SessionMessageTable.seq, anchor.seq)
: undefined
const where = boundary
? and(eq(SessionMessageTable.session_id, input.sessionID), boundary)
: eq(SessionMessageTable.session_id, input.sessionID)
const query = db
.select()
.from(SessionMessageTable)
.where(where)
.orderBy(order === "asc" ? asc(SessionMessageTable.seq) : desc(SessionMessageTable.seq))
const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe(
Effect.orDie,
)
return yield* Effect.forEach(direction === "previous" ? rows.toReversed() : rows, decode)
}),
message: Effect.fn("V2Session.message")(function* (input) {
const stored = yield* store.message(input.messageID)
return stored?.sessionID === input.sessionID ? stored.message : undefined
}),
context: Effect.fn("V2Session.context")(function* (sessionID) {
yield* result.get(sessionID)
return yield* store.context(sessionID)
}),
events: (input) =>
Stream.unwrap(
result
.get(input.sessionID)
.pipe(Effect.as(events.durable({ aggregateID: input.sessionID, after: input.after }))),
).pipe(Stream.filter((event): event is SessionEvent.DurableEvent => isDurableSessionEvent(event))),
prompt: Effect.fn("V2Session.prompt")((input) =>
Effect.uninterruptible(
Effect.gen(function* () {
yield* result.get(input.sessionID)
const messageID = input.id ?? SessionMessage.ID.create()
const delivery = input.delivery ?? "steer"
const expected = { sessionID: input.sessionID, messageID, prompt: input.prompt, delivery }
const admitted = yield* SessionInput.admit(db, events, {
id: messageID,
sessionID: input.sessionID,
prompt: input.prompt,
delivery,
}).pipe(
Effect.catchDefect((defect) =>
defect instanceof SessionInput.LifecycleConflict
? new PromptConflictError({ sessionID: input.sessionID, messageID })
: Effect.die(defect),
),
)
if (!SessionInput.equivalent(admitted, expected))
return yield* new PromptConflictError({ sessionID: input.sessionID, messageID })
if (input.resume !== false) yield* execution.wake(admitted.sessionID)
return admitted
}),
),
),
shell: Effect.fn("V2Session.shell")(function* () {
return yield* new OperationUnavailableError({ operation: "shell" })
}),
skill: Effect.fn("V2Session.skill")(function* () {
return yield* new OperationUnavailableError({ operation: "skill" })
}),
switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) {
yield* result.get(input.sessionID)
yield* events.publish(SessionEvent.AgentSwitched, {
sessionID: input.sessionID,
messageID: SessionMessage.ID.create(),
timestamp: yield* DateTime.now,
agent: input.agent,
})
}),
switchModel: Effect.fn("V2Session.switchModel")(function* (input) {
yield* result.get(input.sessionID)
yield* events.publish(SessionEvent.ModelSwitched, {
sessionID: input.sessionID,
messageID: SessionMessage.ID.create(),
timestamp: yield* DateTime.now,
model: input.model,
})
}),
compact: Effect.fn("V2Session.compact")(function* (input) {
yield* result.get(input.sessionID)
return yield* new OperationUnavailableError({ operation: "compact" })
}),
wait: Effect.fn("V2Session.wait")(function* (sessionID) {
yield* result.get(sessionID)
return yield* new OperationUnavailableError({ operation: "wait" })
}),
resume: Effect.fn("V2Session.resume")(function* (sessionID) {
yield* result.get(sessionID)
yield* execution.resume(sessionID)
}),
interrupt: Effect.fn("V2Session.interrupt")((sessionID) =>
Effect.uninterruptible(execution.interrupt(sessionID)),
),
revert: {
stage: Effect.fn("V2Session.revert.stage")(function* (input) {
const session = yield* result.get(input.sessionID)
return yield* SessionRevert.stage({ session, messageID: input.messageID, files: input.files }).pipe(
Effect.provideService(Database.Service, database),
Effect.provideService(EventV2.Service, events),
Effect.provide(locations.get(session.location)),
)
}),
clear: Effect.fn("V2Session.revert.clear")(function* (sessionID) {
const session = yield* result.get(sessionID)
yield* SessionRevert.clear(session).pipe(
Effect.provideService(EventV2.Service, events),
Effect.provide(locations.get(session.location)),
)
}),
commit: Effect.fn("V2Session.revert.commit")(function* (sessionID) {
const session = yield* result.get(sessionID)
yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events))
}),
},
})
return result return result
}), }),
@@ -436,7 +440,9 @@ export const layer = Layer.unwrap(
) )
export const defaultLayer = layer.pipe( export const defaultLayer = layer.pipe(
Layer.provide(Layer.unwrap(Effect.promise(() => import("./location-layer")).pipe(Effect.map((m) => m.LocationServiceMap.layer)))), Layer.provide(
Layer.unwrap(Effect.promise(() => import("./location-layer")).pipe(Effect.map((m) => m.LocationServiceMap.layer))),
),
Layer.provide(SessionExecution.noopLayer), Layer.provide(SessionExecution.noopLayer),
Layer.provide(SessionStore.defaultLayer), Layer.provide(SessionStore.defaultLayer),
Layer.provide(SessionProjector.defaultLayer), Layer.provide(SessionProjector.defaultLayer),
+1 -3
View File
@@ -40,9 +40,7 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In
workspaceID: row.workspace_id ? WorkspaceV2.ID.make(row.workspace_id) : undefined, workspaceID: row.workspace_id ? WorkspaceV2.ID.make(row.workspace_id) : undefined,
}), }),
subpath: row.path ? RelativePath.make(row.path) : undefined, subpath: row.path ? RelativePath.make(row.path) : undefined,
revert: row.revert revert: row.revert ? { ...row.revert, messageID: SessionMessageID.ID.make(row.revert.messageID) } : undefined,
? { ...row.revert, messageID: SessionMessageID.ID.make(row.revert.messageID) }
: undefined,
time: { time: {
created: DateTime.makeUnsafe(row.time_created), created: DateTime.makeUnsafe(row.time_created),
updated: DateTime.makeUnsafe(row.time_updated), updated: DateTime.makeUnsafe(row.time_updated),
+29 -4
View File
@@ -418,13 +418,38 @@ export const layer = Layer.effectDiscard(
const boundary = yield* db const boundary = yield* db
.select({ seq: SessionMessageTable.seq }) .select({ seq: SessionMessageTable.seq })
.from(SessionMessageTable) .from(SessionMessageTable)
.where(and(eq(SessionMessageTable.session_id, event.data.sessionID), eq(SessionMessageTable.id, event.data.messageID))) .where(
and(
eq(SessionMessageTable.session_id, event.data.sessionID),
eq(SessionMessageTable.id, event.data.messageID),
),
)
.get() .get()
.pipe(Effect.orDie) .pipe(Effect.orDie)
if (!boundary) return yield* Effect.die(`Revert boundary message not found: ${event.data.messageID}`) if (!boundary) return yield* Effect.die(`Revert boundary message not found: ${event.data.messageID}`)
yield* db.delete(SessionMessageTable).where(and(eq(SessionMessageTable.session_id, event.data.sessionID), gt(SessionMessageTable.seq, boundary.seq))).run().pipe(Effect.orDie) yield* db
yield* db.delete(SessionInputTable).where(and(eq(SessionInputTable.session_id, event.data.sessionID), or(gt(SessionInputTable.admitted_seq, boundary.seq), gt(SessionInputTable.promoted_seq, boundary.seq)))).run().pipe(Effect.orDie) .delete(SessionMessageTable)
yield* db.update(SessionTable).set({ revert: null, time_updated: DateTime.toEpochMillis(event.data.timestamp) }).where(eq(SessionTable.id, event.data.sessionID)).run().pipe(Effect.orDie) .where(
and(eq(SessionMessageTable.session_id, event.data.sessionID), gt(SessionMessageTable.seq, boundary.seq)),
)
.run()
.pipe(Effect.orDie)
yield* db
.delete(SessionInputTable)
.where(
and(
eq(SessionInputTable.session_id, event.data.sessionID),
or(gt(SessionInputTable.admitted_seq, boundary.seq), gt(SessionInputTable.promoted_seq, boundary.seq)),
),
)
.run()
.pipe(Effect.orDie)
yield* db
.update(SessionTable)
.set({ revert: null, time_updated: DateTime.toEpochMillis(event.data.timestamp) })
.where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie)
yield* SessionContextEpoch.reset(db, event.data.sessionID) yield* SessionContextEpoch.reset(db, event.data.sessionID)
}), }),
) )
+5 -2
View File
@@ -66,7 +66,7 @@ export const stage = Effect.fn("SessionRevert.stage")(function* (input: {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const original = input.session.revert?.snapshot const original = input.session.revert?.snapshot
? Snapshot.ID.make(input.session.revert.snapshot) ? Snapshot.ID.make(input.session.revert.snapshot)
: (yield* snapshot.capture()) : yield* snapshot.capture()
const next = yield* plan({ sessionID: input.session.id, messageID: input.messageID }) const next = yield* plan({ sessionID: input.session.id, messageID: input.messageID })
const restore = new Map<RelativePath, Snapshot.ID>() const restore = new Map<RelativePath, Snapshot.ID>()
if (original) { if (original) {
@@ -81,7 +81,10 @@ export const stage = Effect.fn("SessionRevert.stage")(function* (input: {
const revert = { const revert = {
messageID: input.messageID, messageID: input.messageID,
snapshot: original, snapshot: original,
diff: files.map((file) => file.patch).join("").trim(), diff: files
.map((file) => file.patch)
.join("")
.trim(),
files, files,
} satisfies SessionSchema.Info["revert"] } satisfies SessionSchema.Info["revert"]
yield* events.publish(SessionEvent.RevertEvent.Staged, { yield* events.publish(SessionEvent.RevertEvent.Staged, {
+18 -13
View File
@@ -309,19 +309,24 @@ export const layer = Layer.effect(
const stepSettlement = publisher.stepSettlement() const stepSettlement = publisher.stepSettlement()
if (stepSettlement && !publisher.hasProviderError()) { if (stepSettlement && !publisher.hasProviderError()) {
const endSnapshot = yield* snapshots.capture() const endSnapshot = yield* snapshots.capture()
const files = startSnapshot && endSnapshot const files =
? yield* snapshots.files({ from: startSnapshot, to: endSnapshot }).pipe(Effect.catch(() => Effect.succeed(undefined))) startSnapshot && endSnapshot
: undefined ? yield* snapshots
yield* withPublication(events.publish(SessionEvent.Step.Ended, { .files({ from: startSnapshot, to: endSnapshot })
sessionID: session.id, .pipe(Effect.catch(() => Effect.succeed(undefined)))
timestamp: yield* DateTime.now, : undefined
assistantMessageID: yield* publisher.startAssistant(), yield* withPublication(
finish: stepSettlement.finish, events.publish(SessionEvent.Step.Ended, {
cost: 0, sessionID: session.id,
tokens: stepSettlement.tokens, timestamp: yield* DateTime.now,
snapshot: endSnapshot, assistantMessageID: yield* publisher.startAssistant(),
files, finish: stepSettlement.finish,
})) cost: 0,
tokens: stepSettlement.tokens,
snapshot: endSnapshot,
files,
}),
)
} }
if (publisher.hasProviderError()) if (publisher.hasProviderError())
yield* withPublication(publisher.failUnsettledTools("Tool execution interrupted")) yield* withPublication(publisher.failUnsettledTools("Tool execution interrupted"))
+16 -14
View File
@@ -111,11 +111,13 @@ export const layer = Layer.effect(
gitDirectory, gitDirectory,
commonDirectory: gitDirectory, commonDirectory: gitDirectory,
}) })
return yield* git.repo.create({ return yield* git.repo
worktree, .create({
gitDirectory, worktree,
seed: source, gitDirectory,
}).pipe(Effect.mapError((cause) => failure("capture", cause))) seed: source,
})
.pipe(Effect.mapError((cause) => failure("capture", cause)))
}) })
const enabled = Effect.fnUntraced(function* () { const enabled = Effect.fnUntraced(function* () {
@@ -136,9 +138,7 @@ export const layer = Layer.effect(
}), }),
) )
}).pipe( }).pipe(
Effect.catch((cause) => Effect.catch((cause) => Effect.logWarning("failed to capture snapshot", { cause }).pipe(Effect.as(undefined))),
Effect.logWarning("failed to capture snapshot", { cause }).pipe(Effect.as(undefined)),
),
) )
}) })
@@ -189,12 +189,14 @@ export const layer = Layer.effect(
if (!(yield* enabled())) return yield* new Error({ operation: "preview", message: "Snapshots are disabled" }) if (!(yield* enabled())) return yield* new Error({ operation: "preview", message: "Snapshots are disabled" })
const repo = yield* repository().pipe(Effect.mapError((cause) => failure("preview", cause))) const repo = yield* repository().pipe(Effect.mapError((cause) => failure("preview", cause)))
const files = yield* plan("preview", input) const files = yield* plan("preview", input)
const current = yield* git.tree.capture({ const current = yield* git.tree
repository: repo, .capture({
scopes: Array.from(files.keys()), repository: repo,
ignores: source, scopes: Array.from(files.keys()),
maximumUntrackedFileBytes: 2 * 1024 * 1024, ignores: source,
}).pipe(Effect.mapError((cause) => failure("preview", cause))) maximumUntrackedFileBytes: 2 * 1024 * 1024,
})
.pipe(Effect.mapError((cause) => failure("preview", cause)))
return yield* git.tree return yield* git.tree
.preview({ .preview({
repository: repo, repository: repo,
+42 -8
View File
@@ -46,18 +46,52 @@ describe("SessionProjector", () => {
it.effect("projects staged, cleared, and committed reverts", () => it.effect("projects staged, cleared, and committed reverts", () =>
Effect.gen(function* () { Effect.gen(function* () {
const db = (yield* Database.Service).db const db = (yield* Database.Service).db
yield* db.insert(ProjectTable).values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }).run() yield* db
yield* db.insert(SessionTable).values({ id: sessionID, project_id: Project.ID.global, slug: "test", directory: "/project", title: "test", version: "test" }).run() .insert(ProjectTable)
.values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
.run()
yield* db
.insert(SessionTable)
.values({
id: sessionID,
project_id: Project.ID.global,
slug: "test",
directory: "/project",
title: "test",
version: "test",
})
.run()
const boundary = SessionMessage.ID.make("msg_boundary") const boundary = SessionMessage.ID.make("msg_boundary")
yield* db.insert(SessionMessageTable).values([assistantRow(boundary, 1), assistantRow(SessionMessage.ID.make("msg_later"), 2)]).run() yield* db
.insert(SessionMessageTable)
.values([assistantRow(boundary, 1), assistantRow(SessionMessage.ID.make("msg_later"), 2)])
.run()
const events = yield* EventV2.Service const events = yield* EventV2.Service
yield* events.publish(SessionEvent.RevertEvent.Staged, { sessionID, timestamp: DateTime.makeUnsafe(1), revert: { messageID: boundary, snapshot: Snapshot.ID.make("tree"), diff: "patch", files: [] } }) yield* events.publish(SessionEvent.RevertEvent.Staged, {
expect((yield* db.select({ revert: SessionTable.revert }).from(SessionTable).get())?.revert).toMatchObject({ messageID: boundary, snapshot: "tree", files: [] }) sessionID,
timestamp: DateTime.makeUnsafe(1),
revert: { messageID: boundary, snapshot: Snapshot.ID.make("tree"), diff: "patch", files: [] },
})
expect((yield* db.select({ revert: SessionTable.revert }).from(SessionTable).get())?.revert).toMatchObject({
messageID: boundary,
snapshot: "tree",
files: [],
})
yield* events.publish(SessionEvent.RevertEvent.Cleared, { sessionID, timestamp: DateTime.makeUnsafe(2) }) yield* events.publish(SessionEvent.RevertEvent.Cleared, { sessionID, timestamp: DateTime.makeUnsafe(2) })
expect((yield* db.select({ revert: SessionTable.revert }).from(SessionTable).get())?.revert).toBeNull() expect((yield* db.select({ revert: SessionTable.revert }).from(SessionTable).get())?.revert).toBeNull()
yield* events.publish(SessionEvent.RevertEvent.Staged, { sessionID, timestamp: DateTime.makeUnsafe(3), revert: { messageID: boundary, files: [] } }) yield* events.publish(SessionEvent.RevertEvent.Staged, {
yield* events.publish(SessionEvent.RevertEvent.Committed, { sessionID, messageID: boundary, timestamp: DateTime.makeUnsafe(4) }) sessionID,
expect((yield* db.select({ id: SessionMessageTable.id }).from(SessionMessageTable).all()).map((row) => row.id)).toEqual([boundary]) timestamp: DateTime.makeUnsafe(3),
revert: { messageID: boundary, files: [] },
})
yield* events.publish(SessionEvent.RevertEvent.Committed, {
sessionID,
messageID: boundary,
timestamp: DateTime.makeUnsafe(4),
})
expect(
(yield* db.select({ id: SessionMessageTable.id }).from(SessionMessageTable).all()).map((row) => row.id),
).toEqual([boundary])
}), }),
) )
+6 -2
View File
@@ -123,8 +123,12 @@ describe("Snapshot", () => {
), ),
), ),
) )
expect(yield* Effect.promise(() => fs.stat(path.join(tmp.path, "snapshot", projectID, Hash.fast(project))))).toBeDefined() expect(
expect(yield* Effect.promise(() => fs.stat(path.join(tmp.path, "snapshot", projectID, Hash.fast(linked))))).toBeDefined() yield* Effect.promise(() => fs.stat(path.join(tmp.path, "snapshot", projectID, Hash.fast(project)))),
).toBeDefined()
expect(
yield* Effect.promise(() => fs.stat(path.join(tmp.path, "snapshot", projectID, Hash.fast(linked)))),
).toBeDefined()
}), }),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
), ),
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -58,7 +58,11 @@ export const MessageHandler = HttpApiBuilder.group(Api, "server.message", (handl
const ref = `err_${crypto.randomUUID().slice(0, 8)}` const ref = `err_${crypto.randomUUID().slice(0, 8)}`
return Effect.logError("failed to decode session message").pipe( return Effect.logError("failed to decode session message").pipe(
Effect.annotateLogs({ ref, sessionID: error.sessionID, messageID: error.messageID }), Effect.annotateLogs({ ref, sessionID: error.sessionID, messageID: error.messageID }),
Effect.andThen(Effect.fail(new UnknownError({ message: "Unexpected server error. Check server logs for details.", ref }))), Effect.andThen(
Effect.fail(
new UnknownError({ message: "Unexpected server error. Check server logs for details.", ref }),
),
),
) )
}), }),
) )
+5 -1
View File
@@ -307,7 +307,11 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
const ref = `err_${crypto.randomUUID().slice(0, 8)}` const ref = `err_${crypto.randomUUID().slice(0, 8)}`
return Effect.logError("failed to decode session message").pipe( return Effect.logError("failed to decode session message").pipe(
Effect.annotateLogs({ ref, sessionID: error.sessionID, messageID: error.messageID }), Effect.annotateLogs({ ref, sessionID: error.sessionID, messageID: error.messageID }),
Effect.andThen(Effect.fail(new UnknownError({ message: "Unexpected server error. Check server logs for details.", ref }))), Effect.andThen(
Effect.fail(
new UnknownError({ message: "Unexpected server error. Check server logs for details.", ref }),
),
),
) )
}), }),
), ),