chore: generate
This commit is contained in:
@@ -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
@@ -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,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
@@ -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),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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 }),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user