mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-13 15:03:43 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e27dd7b3e |
@@ -103,7 +103,7 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
|
||||
const base = pickerRoot(cleaned) || root() || start()
|
||||
if (!base) return { query: value, items: directories.slice(0, 5) }
|
||||
const files = await sdk.api.file
|
||||
.get({
|
||||
.find({
|
||||
location: { directory: base },
|
||||
query: pickerFileSearchQuery(base, value, home()),
|
||||
type: "file",
|
||||
|
||||
@@ -135,7 +135,7 @@ test("resolves directory autocomplete from the current browser root", async () =
|
||||
const sdk = {
|
||||
api: {
|
||||
file: {
|
||||
get: (input: { location?: { directory?: string } }) => {
|
||||
find: (input: { location?: { directory?: string } }) => {
|
||||
directories.push(input.location?.directory ?? "")
|
||||
return Promise.resolve({ data: [] })
|
||||
},
|
||||
@@ -157,7 +157,7 @@ test("keeps indexed directory results for servers that support empty search", as
|
||||
const sdk = {
|
||||
api: {
|
||||
file: {
|
||||
get: () => Promise.resolve({ data: [{ path: "projects/", type: "directory" }] }),
|
||||
find: () => Promise.resolve({ data: [{ path: "projects/", type: "directory" }] }),
|
||||
list: () => Promise.reject(new Error("listing should not run when search returns results")),
|
||||
},
|
||||
},
|
||||
@@ -176,7 +176,7 @@ test("lists the default directory when empty search is unsupported", async () =>
|
||||
const sdk = {
|
||||
api: {
|
||||
file: {
|
||||
get: () => Promise.resolve({ data: [] }),
|
||||
find: () => Promise.resolve({ data: [] }),
|
||||
list: (input: { location?: { directory?: string } }) => {
|
||||
calls.push(input.location?.directory ?? "")
|
||||
return Promise.resolve({
|
||||
@@ -198,7 +198,7 @@ test("matches the default directory listing when typed search is unsupported", a
|
||||
const sdk = {
|
||||
api: {
|
||||
file: {
|
||||
get: () => Promise.resolve({ data: [] }),
|
||||
find: () => Promise.resolve({ data: [] }),
|
||||
list: () =>
|
||||
Promise.resolve({
|
||||
data: [
|
||||
|
||||
@@ -375,7 +375,7 @@ export function createDirectorySearch(args: { sdk: ServerSDK; base: () => string
|
||||
const query = normalizePickerDrive(input.path)
|
||||
if (!pathInput) {
|
||||
const results = await args.sdk.api.file
|
||||
.get({ location: { directory: input.directory }, query, type: "directory", limit: 50 })
|
||||
.find({ location: { directory: input.directory }, query, type: "directory", limit: 50 })
|
||||
.then((result) => result.data.map((entry) => entry.path))
|
||||
.catch(() => [])
|
||||
if (!active()) return []
|
||||
|
||||
@@ -212,7 +212,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
|
||||
const search = (query: string, dirs: "true" | "false", options?: { limit?: number; signal?: AbortSignal }) =>
|
||||
serverSDK()
|
||||
.api.file.get(
|
||||
.api.file.find(
|
||||
{
|
||||
location: { directory: sdk().directory },
|
||||
query,
|
||||
|
||||
@@ -18,18 +18,18 @@ describe("v2 session reducer", () => {
|
||||
apply({
|
||||
...base,
|
||||
id: "evt_admitted",
|
||||
type: "session.inbox.enqueued",
|
||||
type: "session.input.admitted",
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
inboxID: "msg_user",
|
||||
item: { type: "user", delivery: "steer", payload: { text: "hello" } },
|
||||
inputID: "msg_user",
|
||||
input: { type: "user", delivery: "steer", data: { text: "hello" } },
|
||||
},
|
||||
})
|
||||
apply({
|
||||
...base,
|
||||
id: "evt_promoted",
|
||||
type: "session.inbox.delivered",
|
||||
data: { sessionID: "ses_1", inboxID: "msg_user" },
|
||||
type: "session.input.promoted",
|
||||
data: { sessionID: "ses_1", inputID: "msg_user" },
|
||||
})
|
||||
apply({
|
||||
...base,
|
||||
@@ -203,8 +203,8 @@ describe("v2 session reducer", () => {
|
||||
event({
|
||||
...base,
|
||||
id: "evt_promoted",
|
||||
type: "session.inbox.delivered",
|
||||
data: { sessionID: "ses_1", inboxID: "msg_user" },
|
||||
type: "session.input.promoted",
|
||||
data: { sessionID: "ses_1", inputID: "msg_user" },
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -218,11 +218,11 @@ describe("v2 session reducer", () => {
|
||||
event({
|
||||
...base,
|
||||
id: "evt_admitted",
|
||||
type: "session.inbox.enqueued",
|
||||
type: "session.input.admitted",
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
inboxID: "msg_user",
|
||||
item: { type: "user", delivery: "queue", payload: { text: "cancel me" } },
|
||||
inputID: "msg_user",
|
||||
input: { type: "user", delivery: "queue", data: { text: "cancel me" } },
|
||||
},
|
||||
}),
|
||||
)
|
||||
@@ -231,8 +231,8 @@ describe("v2 session reducer", () => {
|
||||
event({
|
||||
...base,
|
||||
id: "evt_cancelled",
|
||||
type: "session.inbox.cancelled",
|
||||
data: { sessionID: "ses_1", inboxID: "msg_user" },
|
||||
type: "session.input.cancelled",
|
||||
data: { sessionID: "ses_1", inputID: "msg_user" },
|
||||
}),
|
||||
)
|
||||
const result = reducer.reduce(
|
||||
@@ -240,8 +240,8 @@ describe("v2 session reducer", () => {
|
||||
event({
|
||||
...base,
|
||||
id: "evt_promoted",
|
||||
type: "session.inbox.delivered",
|
||||
data: { sessionID: "ses_1", inboxID: "msg_user" },
|
||||
type: "session.input.promoted",
|
||||
data: { sessionID: "ses_1", inputID: "msg_user" },
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -255,11 +255,11 @@ describe("v2 session reducer", () => {
|
||||
event({
|
||||
...base,
|
||||
id: "evt_admitted",
|
||||
type: "session.inbox.enqueued",
|
||||
type: "session.input.admitted",
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
inboxID: "msg_user",
|
||||
item: { type: "user", delivery: "queue", payload: { text: "steer me" } },
|
||||
inputID: "msg_user",
|
||||
input: { type: "user", delivery: "queue", data: { text: "steer me" } },
|
||||
},
|
||||
}),
|
||||
)
|
||||
@@ -268,8 +268,8 @@ describe("v2 session reducer", () => {
|
||||
event({
|
||||
...base,
|
||||
id: "evt_steered",
|
||||
type: "session.inbox.delivery.changed",
|
||||
data: { sessionID: "ses_1", inboxID: "msg_user", delivery: "steer" },
|
||||
type: "session.input.steered",
|
||||
data: { sessionID: "ses_1", inputID: "msg_user" },
|
||||
}),
|
||||
)
|
||||
reducer.reduce(
|
||||
@@ -277,8 +277,8 @@ describe("v2 session reducer", () => {
|
||||
event({
|
||||
...base,
|
||||
id: "evt_queued",
|
||||
type: "session.inbox.delivery.changed",
|
||||
data: { sessionID: "ses_1", inboxID: "msg_user", delivery: "queue" },
|
||||
type: "session.input.queued",
|
||||
data: { sessionID: "ses_1", inputID: "msg_user" },
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -287,8 +287,8 @@ describe("v2 session reducer", () => {
|
||||
event({
|
||||
...base,
|
||||
id: "evt_promoted",
|
||||
type: "session.inbox.delivered",
|
||||
data: { sessionID: "ses_1", inboxID: "msg_user" },
|
||||
type: "session.input.promoted",
|
||||
data: { sessionID: "ses_1", inputID: "msg_user" },
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OpenCodeEvent, SessionInboxItem, SessionMessageInfo } from "@opencode-ai/client/promise"
|
||||
import type { OpenCodeEvent, SessionMessageInfo, SessionPendingMessage } from "@opencode-ai/client/promise"
|
||||
|
||||
type Assistant = Extract<SessionMessageInfo, { type: "assistant" }>
|
||||
type Compaction = Extract<SessionMessageInfo, { type: "compaction" }>
|
||||
@@ -12,7 +12,7 @@ export type V2SessionReduction = {
|
||||
}
|
||||
|
||||
export function createV2SessionReducer() {
|
||||
const pending = new Map<string, SessionInboxItem>()
|
||||
const pending = new Map<string, SessionPendingMessage>()
|
||||
|
||||
const reduce = (source: readonly SessionMessageInfo[], event: OpenCodeEvent): V2SessionReduction | undefined => {
|
||||
if (!("data" in event) || !("sessionID" in event.data) || typeof event.data.sessionID !== "string") return
|
||||
@@ -26,33 +26,32 @@ export function createV2SessionReducer() {
|
||||
result(source.some((item) => item.id === message.id) ? [...source] : [...source, message], [message.id])
|
||||
|
||||
switch (event.type) {
|
||||
case "session.inbox.enqueued":
|
||||
pending.set(key(sessionID, event.data.inboxID), event.data.item)
|
||||
case "session.input.admitted":
|
||||
pending.set(key(sessionID, event.data.inputID), event.data.input)
|
||||
return result([...source])
|
||||
case "session.inbox.cancelled":
|
||||
pending.delete(key(sessionID, event.data.inboxID))
|
||||
case "session.input.cancelled":
|
||||
pending.delete(key(sessionID, event.data.inputID))
|
||||
return
|
||||
case "session.inbox.delivered": {
|
||||
const input = pending.get(key(sessionID, event.data.inboxID))
|
||||
pending.delete(key(sessionID, event.data.inboxID))
|
||||
if (!input) return { ...result([...source]), missing: event.data.inboxID }
|
||||
case "session.input.promoted": {
|
||||
const input = pending.get(key(sessionID, event.data.inputID))
|
||||
pending.delete(key(sessionID, event.data.inputID))
|
||||
if (!input) return { ...result([...source]), missing: event.data.inputID }
|
||||
if (input.type === "user")
|
||||
return append({
|
||||
id: event.data.inboxID,
|
||||
id: event.data.inputID,
|
||||
type: "user",
|
||||
metadata: input.payload.metadata,
|
||||
text: input.payload.text,
|
||||
files: input.payload.files,
|
||||
agents: input.payload.agents,
|
||||
metadata: input.data.metadata,
|
||||
text: input.data.text,
|
||||
files: input.data.files,
|
||||
agents: input.data.agents,
|
||||
time: { created: event.created },
|
||||
})
|
||||
if (input.type !== "synthetic") return result([...source])
|
||||
return append({
|
||||
id: event.data.inboxID,
|
||||
id: event.data.inputID,
|
||||
type: "synthetic",
|
||||
metadata: input.payload.metadata,
|
||||
text: input.payload.text,
|
||||
description: input.payload.description,
|
||||
metadata: input.data.metadata,
|
||||
text: input.data.text,
|
||||
description: input.data.description,
|
||||
time: { created: event.created },
|
||||
})
|
||||
}
|
||||
@@ -352,7 +351,7 @@ export function createV2SessionReducer() {
|
||||
metadata: event.metadata,
|
||||
reason: event.data.reason,
|
||||
summary: "",
|
||||
recent: event.data.recent ?? "",
|
||||
recent: event.data.recent,
|
||||
time: { created: event.created },
|
||||
})
|
||||
case "session.compaction.delta":
|
||||
|
||||
@@ -538,8 +538,9 @@ async function replayMessage(
|
||||
}
|
||||
|
||||
function matchesStart(event: EventSubscribeOutput, start: TurnStart) {
|
||||
if (start.type === "input") return event.type === "session.inbox.delivered" && event.data.inboxID === start.id
|
||||
if (start.type === "compaction") return event.type === "session.inbox.delivered" && event.data.inboxID === start.id
|
||||
if (start.type === "input") return event.type === "session.input.promoted" && event.data.inputID === start.id
|
||||
if (start.type === "compaction")
|
||||
return event.type === "session.compaction.admitted" && event.data.inputID === start.id
|
||||
return event.type === "session.skill.activated" && event.id === start.id.replace(/^msg_/, "evt_")
|
||||
}
|
||||
|
||||
|
||||
@@ -195,8 +195,8 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||
if (!("sessionID" in event.data) || event.data.sessionID !== input.sessionID) continue
|
||||
const time = toMillis("created" in event ? event.created : undefined)
|
||||
|
||||
if (event.type === "session.inbox.delivered") {
|
||||
if (event.data.inboxID === messageID) {
|
||||
if (event.type === "session.input.promoted") {
|
||||
if (event.data.inputID === messageID) {
|
||||
promoted = true
|
||||
prePromotionError = undefined
|
||||
continue
|
||||
|
||||
@@ -22,8 +22,8 @@ describe("acp event behavior", () => {
|
||||
delta: "before admission",
|
||||
}),
|
||||
)
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_b", inboxID: id }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_a", inboxID: "input_other" }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_b", inputID: id }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_a", inputID: "input_other" }))
|
||||
send(
|
||||
ephemeralEvent("session.text.delta", {
|
||||
sessionID: "ses_a",
|
||||
@@ -32,7 +32,7 @@ describe("acp event behavior", () => {
|
||||
delta: "wrong input",
|
||||
}),
|
||||
)
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_a", inboxID: id }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_a", inputID: id }))
|
||||
send(
|
||||
ephemeralEvent("session.text.delta", {
|
||||
sessionID: "ses_b",
|
||||
@@ -68,7 +68,7 @@ describe("acp event behavior", () => {
|
||||
fixture,
|
||||
connection: recordingConnection(updates),
|
||||
sessionID: "ses_a",
|
||||
inboxID: "input_a",
|
||||
inputID: "input_a",
|
||||
})
|
||||
|
||||
expect(fixture.requests.slice(0, 2).map((request) => request.path)).toEqual([
|
||||
@@ -99,7 +99,7 @@ describe("acp event behavior", () => {
|
||||
const updates: SessionUpdateParams[] = []
|
||||
const fixture = createSseFixture({
|
||||
async onPrompt({ id, send }) {
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_order", inboxID: id }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_order", inputID: id }))
|
||||
send(
|
||||
ephemeralEvent("session.reasoning.delta", {
|
||||
sessionID: "ses_order",
|
||||
@@ -148,7 +148,7 @@ describe("acp event behavior", () => {
|
||||
},
|
||||
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
|
||||
} satisfies Connection
|
||||
const result = turn({ fixture, connection, sessionID: "ses_order", inboxID: "input_order" })
|
||||
const result = turn({ fixture, connection, sessionID: "ses_order", inputID: "input_order" })
|
||||
|
||||
try {
|
||||
await withTimeout(firstUpdate.promise, "first ordered update was not delivered")
|
||||
@@ -195,7 +195,7 @@ describe("acp event behavior", () => {
|
||||
const updates: SessionUpdateParams[] = []
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_parent", inboxID: id }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_parent", inputID: id }))
|
||||
send(
|
||||
durableEvent("session.created", {
|
||||
sessionID: "ses_child",
|
||||
@@ -240,7 +240,7 @@ describe("acp event behavior", () => {
|
||||
fixture,
|
||||
connection: recordingConnection(updates),
|
||||
sessionID: "ses_parent",
|
||||
inboxID: "input_parent",
|
||||
inputID: "input_parent",
|
||||
})
|
||||
|
||||
expect(updates.map((item) => [item.sessionId, item.update.sessionUpdate])).toEqual([
|
||||
@@ -276,7 +276,7 @@ describe("acp event behavior", () => {
|
||||
const completed = Promise.withResolvers<void>()
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_parent", inboxID: id }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_parent", inputID: id }))
|
||||
send(
|
||||
durableEvent("session.created", {
|
||||
sessionID: "ses_background",
|
||||
@@ -292,7 +292,7 @@ describe("acp event behavior", () => {
|
||||
fixture,
|
||||
connection: recordingConnection(updates),
|
||||
sessionID: "ses_parent",
|
||||
inboxID: "input_parent",
|
||||
inputID: "input_parent",
|
||||
childSessionUpdate: async (update) => {
|
||||
childUpdates.push(update)
|
||||
if (update.type === "status" && update.status === "completed") completed.resolve()
|
||||
@@ -370,7 +370,7 @@ describe("acp event behavior", () => {
|
||||
const updates: SessionUpdateParams[] = []
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_tools", inboxID: id }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_tools", inputID: id }))
|
||||
send(
|
||||
durableEvent("session.tool.input.started", {
|
||||
sessionID: "ses_tools",
|
||||
@@ -460,7 +460,7 @@ describe("acp event behavior", () => {
|
||||
fixture,
|
||||
connection: recordingConnection(updates),
|
||||
sessionID: "ses_tools",
|
||||
inboxID: "input_tools",
|
||||
inputID: "input_tools",
|
||||
})
|
||||
|
||||
expect(
|
||||
@@ -597,7 +597,7 @@ describe("acp event behavior", () => {
|
||||
const control: TurnControl = { cancelled: false, admission: new AbortController() }
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_cancel", inboxID: id }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_cancel", inputID: id }))
|
||||
},
|
||||
onInterrupt({ sessionID, send }) {
|
||||
send(durableEvent("session.execution.interrupted", { sessionID, reason: "user" }))
|
||||
@@ -680,7 +680,7 @@ describe("acp event behavior", () => {
|
||||
test("cancels unsupported session forms so execution can continue", async () => {
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_form", inboxID: id }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_form", inputID: id }))
|
||||
send(
|
||||
ephemeralEvent("form.created", {
|
||||
form: {
|
||||
@@ -704,7 +704,7 @@ describe("acp event behavior", () => {
|
||||
fixture,
|
||||
connection: recordingConnection([]),
|
||||
sessionID: "ses_form",
|
||||
inboxID: "input_form",
|
||||
inputID: "input_form",
|
||||
})
|
||||
|
||||
expect(response.stopReason).toBe("end_turn")
|
||||
@@ -730,7 +730,7 @@ function turn(input: {
|
||||
readonly fixture: Fixture
|
||||
readonly connection: Connection
|
||||
readonly sessionID: string
|
||||
readonly inboxID: string
|
||||
readonly inputID: string
|
||||
readonly childSessionUpdate?: (update: ChildSessionUpdate) => Promise<void>
|
||||
}) {
|
||||
return streamTurn({
|
||||
@@ -738,12 +738,12 @@ function turn(input: {
|
||||
connection: input.connection,
|
||||
sessionID: input.sessionID,
|
||||
cwd: "/workspace",
|
||||
start: { type: "input", id: input.inboxID },
|
||||
start: { type: "input", id: input.inputID },
|
||||
writeTextFile: false,
|
||||
control: { cancelled: false, admission: new AbortController() },
|
||||
childSessionUpdate: input.childSessionUpdate,
|
||||
submit: (signal) =>
|
||||
input.fixture.client.session.prompt({ sessionID: input.sessionID, id: input.inboxID, text: "hello" }, { signal }),
|
||||
input.fixture.client.session.prompt({ sessionID: input.sessionID, id: input.inputID, text: "hello" }, { signal }),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ test("acp prompt resolves after ordered turn updates", async () => {
|
||||
send(events, {
|
||||
id: "evt_promoted",
|
||||
created: 1,
|
||||
type: "session.inbox.delivered",
|
||||
data: { sessionID: "ses_test", inboxID: id },
|
||||
type: "session.input.promoted",
|
||||
data: { sessionID: "ses_test", inputID: id },
|
||||
})
|
||||
send(events, {
|
||||
id: "evt_text",
|
||||
|
||||
@@ -38,7 +38,7 @@ describe("acp permission behavior", () => {
|
||||
const permissionRequests: RequestPermissionRequest[] = []
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_allow", inboxID: id }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_allow", inputID: id }))
|
||||
send(
|
||||
permissionAsked("ses_allow", "perm_once", {
|
||||
action: "shell",
|
||||
@@ -112,7 +112,7 @@ describe("acp permission behavior", () => {
|
||||
const permissionRequests: RequestPermissionRequest[] = []
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_external", inboxID: id }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_external", inputID: id }))
|
||||
send(
|
||||
permissionAsked("ses_external", "perm_external", {
|
||||
action: "external_directory",
|
||||
@@ -157,7 +157,7 @@ describe("acp permission behavior", () => {
|
||||
const permissionRequests: RequestPermissionRequest[] = []
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_parent", inboxID: id }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_parent", inputID: id }))
|
||||
send(
|
||||
durableEvent("session.created", {
|
||||
sessionID: "ses_child",
|
||||
@@ -219,7 +219,7 @@ describe("acp permission behavior", () => {
|
||||
const writes: Parameters<AgentSideConnection["writeTextFile"]>[0][] = []
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_edit", inboxID: id }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_edit", inputID: id }))
|
||||
send(
|
||||
durableEvent("session.tool.input.started", {
|
||||
sessionID: "ses_edit",
|
||||
@@ -309,7 +309,7 @@ describe("acp permission behavior", () => {
|
||||
const writes: Parameters<AgentSideConnection["writeTextFile"]>[0][] = []
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_patch", inboxID: id }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_patch", inputID: id }))
|
||||
send(
|
||||
durableEvent("session.tool.input.started", {
|
||||
sessionID: "ses_patch",
|
||||
@@ -389,7 +389,7 @@ describe("acp permission behavior", () => {
|
||||
test("rejects explicit rejection, cancellation, and permission UI failure", async () => {
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_reject", inboxID: id }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_reject", inputID: id }))
|
||||
send(permissionAsked("ses_reject", "perm_selected_reject"))
|
||||
send(permissionAsked("ses_reject", "perm_cancelled"))
|
||||
send(permissionAsked("ses_reject", "perm_failed"))
|
||||
@@ -426,7 +426,7 @@ describe("acp permission behavior", () => {
|
||||
const permissionRequests: RequestPermissionRequest[] = []
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_serial", inboxID: id }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_serial", inputID: id }))
|
||||
send(permissionAsked("ses_serial", "perm_1"))
|
||||
send(permissionAsked("ses_serial", "perm_2"))
|
||||
send(durableEvent("session.execution.succeeded", { sessionID: "ses_serial" }))
|
||||
@@ -477,8 +477,8 @@ describe("acp permission behavior", () => {
|
||||
const blockedID = promptIDs.get("ses_blocked")
|
||||
const freeID = promptIDs.get("ses_free")
|
||||
if (!blockedID || !freeID) throw new Error("both permission test prompts must be registered")
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_blocked", inboxID: blockedID }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_free", inboxID: freeID }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_blocked", inputID: blockedID }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_free", inputID: freeID }))
|
||||
send(permissionAsked("ses_blocked", "perm_blocked"))
|
||||
send(
|
||||
ephemeralEvent("session.text.delta", {
|
||||
@@ -538,16 +538,16 @@ describe("acp permission behavior", () => {
|
||||
})
|
||||
})
|
||||
|
||||
function startTurn(fixture: Fixture, connection: Connection, sessionID: string, inboxID: string, cwd = "/workspace") {
|
||||
function startTurn(fixture: Fixture, connection: Connection, sessionID: string, inputID: string, cwd = "/workspace") {
|
||||
return streamTurn({
|
||||
client: fixture.client,
|
||||
connection,
|
||||
sessionID,
|
||||
cwd,
|
||||
start: { type: "input", id: inboxID },
|
||||
start: { type: "input", id: inputID },
|
||||
writeTextFile: true,
|
||||
control: { cancelled: false, admission: new AbortController() },
|
||||
submit: (signal) => fixture.client.session.prompt({ sessionID, id: inboxID, text: "hello" }, { signal }),
|
||||
submit: (signal) => fixture.client.session.prompt({ sessionID, id: inputID, text: "hello" }, { signal }),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ describe("acp service prompt routing and usage", () => {
|
||||
const id = requestID(request)
|
||||
completeTurn(context, "ses_routes", {
|
||||
id: `evt_${id}`,
|
||||
type: "session.inbox.delivered",
|
||||
data: { sessionID: "ses_routes", inboxID: id },
|
||||
type: "session.input.promoted",
|
||||
data: { sessionID: "ses_routes", inputID: id },
|
||||
})
|
||||
return Response.json({ data: {} })
|
||||
}
|
||||
@@ -33,8 +33,8 @@ describe("acp service prompt routing and usage", () => {
|
||||
const id = requestID(request)
|
||||
completeTurn(context, "ses_routes", {
|
||||
id: `evt_${id}`,
|
||||
type: "session.inbox.delivered",
|
||||
data: { sessionID: "ses_routes", inboxID: id },
|
||||
type: "session.compaction.admitted",
|
||||
data: { sessionID: "ses_routes", inputID: id },
|
||||
})
|
||||
return Response.json({ data: {} })
|
||||
}
|
||||
@@ -95,8 +95,8 @@ describe("acp service prompt routing and usage", () => {
|
||||
const id = requestID(request)
|
||||
context.send({
|
||||
id: `evt_${id}`,
|
||||
type: "session.inbox.delivered",
|
||||
data: { sessionID: "ses_usage", inboxID: id },
|
||||
type: "session.input.promoted",
|
||||
data: { sessionID: "ses_usage", inputID: id },
|
||||
})
|
||||
context.send({
|
||||
id: "evt_step",
|
||||
@@ -189,8 +189,8 @@ describe("acp service prompt routing and usage", () => {
|
||||
const id = requestID(request)
|
||||
context.send({
|
||||
id: `evt_${id}`,
|
||||
type: "session.inbox.delivered",
|
||||
data: { sessionID: "ses_usage_failure", inboxID: id },
|
||||
type: "session.input.promoted",
|
||||
data: { sessionID: "ses_usage_failure", inputID: id },
|
||||
})
|
||||
context.send({
|
||||
id: "evt_step_failure",
|
||||
|
||||
@@ -28,13 +28,13 @@ function formCreated(info: FormInfo, eventLocation = location): V2Event {
|
||||
return { id: `evt_${info.id}`, created: 0, type: "form.created", location: eventLocation, data: { form: info } }
|
||||
}
|
||||
|
||||
function prompted(inboxID: string): V2Event {
|
||||
function prompted(inputID: string): V2Event {
|
||||
return {
|
||||
id: "evt_prompted",
|
||||
created: 0,
|
||||
type: "session.inbox.delivered",
|
||||
type: "session.input.promoted",
|
||||
durable: { aggregateID: "ses_1", seq: 0, version: 1 },
|
||||
data: { sessionID: "ses_1", inboxID },
|
||||
data: { sessionID: "ses_1", inputID },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,9 +98,9 @@ function executionFailed(message: string): V2Event {
|
||||
}
|
||||
}
|
||||
|
||||
function failedTool(inboxID: string): V2Event[] {
|
||||
function failedTool(inputID: string): V2Event[] {
|
||||
return [
|
||||
prompted(inboxID),
|
||||
prompted(inputID),
|
||||
{
|
||||
id: "evt_failed_tool_input",
|
||||
created: 1,
|
||||
@@ -156,10 +156,10 @@ function failedTool(inboxID: string): V2Event[] {
|
||||
]
|
||||
}
|
||||
|
||||
function successfulGrep(inboxID: string): V2Event[] {
|
||||
function successfulGrep(inputID: string): V2Event[] {
|
||||
const text = "Found 2 matches\n/src/a.ts:\n Line 1: needle\n/src/b.ts:\n Line 2: needle"
|
||||
return [
|
||||
prompted(inboxID),
|
||||
prompted(inputID),
|
||||
{
|
||||
id: "evt_grep_input",
|
||||
created: 1,
|
||||
@@ -206,7 +206,7 @@ function successfulGrep(inboxID: string): V2Event[] {
|
||||
// Runs one non-interactive prompt against a mocked SDK. `turn` produces the
|
||||
// live events the prompt admission triggers, keyed by the generated message ID.
|
||||
async function run(input: {
|
||||
turn: (inboxID: string) => V2Event[]
|
||||
turn: (inputID: string) => V2Event[]
|
||||
pendingForms?: FormInfo[]
|
||||
attached?: boolean
|
||||
format?: "default" | "json"
|
||||
@@ -214,7 +214,7 @@ async function run(input: {
|
||||
cancel?: (input: { sessionID: string; formID: string }) => Promise<void>
|
||||
renderTool?: (part: SessionMessageAssistantTool) => Promise<void>
|
||||
renderToolError?: (part: SessionMessageAssistantTool) => Promise<void>
|
||||
messages?: (inboxID: string) => SessionMessageInfo[]
|
||||
messages?: (inputID: string) => SessionMessageInfo[]
|
||||
wait?: () => Promise<void>
|
||||
terminalDelay?: number
|
||||
}) {
|
||||
@@ -515,7 +515,7 @@ describe("runNonInteractivePrompt", () => {
|
||||
const rendered: SessionMessageAssistantTool[] = []
|
||||
const failed: SessionMessageAssistantTool[] = []
|
||||
await capture({
|
||||
turn: (inboxID) => failedTool(inboxID).filter((event) => event.type !== "session.tool.progress"),
|
||||
turn: (inputID) => failedTool(inputID).filter((event) => event.type !== "session.tool.progress"),
|
||||
renderTool: (part) => {
|
||||
rendered.push(part)
|
||||
return Promise.resolve()
|
||||
|
||||
@@ -30,7 +30,7 @@ import { Reference } from "@opencode-ai/schema/reference"
|
||||
import { AbsolutePath, PositiveInt, RelativePath } from "@opencode-ai/schema/schema"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
import { SessionInbox } from "@opencode-ai/schema/session-inbox"
|
||||
import { SessionPending } from "@opencode-ai/schema/session-pending"
|
||||
import { Shell } from "@opencode-ai/schema/shell"
|
||||
import { Skill } from "@opencode-ai/schema/skill"
|
||||
import { Vcs } from "@opencode-ai/schema/vcs"
|
||||
@@ -69,7 +69,7 @@ const effectTypeReferences = [
|
||||
...namespaceTypes("Reference", "@opencode-ai/schema/reference", Reference),
|
||||
...namespaceTypes("Session", "@opencode-ai/schema/session", Session),
|
||||
...namespaceTypes("SessionMessage", "@opencode-ai/schema/session-message", SessionMessage),
|
||||
...namespaceTypes("SessionInbox", "@opencode-ai/schema/session-inbox", SessionInbox),
|
||||
...namespaceTypes("SessionPending", "@opencode-ai/schema/session-pending", SessionPending),
|
||||
...namespaceTypes("Shell", "@opencode-ai/schema/shell", Shell),
|
||||
...namespaceTypes("Skill", "@opencode-ai/schema/skill", Skill),
|
||||
...namespaceTypes("Vcs", "@opencode-ai/schema/vcs", Vcs),
|
||||
|
||||
@@ -11,9 +11,9 @@ import type { RelativePath } from "@opencode-ai/schema/schema"
|
||||
import type { Brand } from "effect"
|
||||
import type { Model } from "@opencode-ai/schema/model"
|
||||
import type { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
import type { SessionInbox } from "@opencode-ai/schema/session-inbox"
|
||||
import type { PromptInput } from "@opencode-ai/schema/prompt-input"
|
||||
import type { AgentAttachment } from "@opencode-ai/schema/prompt"
|
||||
import type { SessionPending } from "@opencode-ai/schema/session-pending"
|
||||
import type { Skill } from "@opencode-ai/schema/skill"
|
||||
import type { Event } from "@opencode-ai/schema/event"
|
||||
import type { InstructionEntry } from "@opencode-ai/schema/instruction-entry"
|
||||
@@ -170,7 +170,6 @@ export type Endpoint5_11Input = {
|
||||
readonly sessionID: Session.ID
|
||||
readonly directory: AbsolutePath
|
||||
readonly workspaceID?: Workspace.ID | undefined
|
||||
readonly delivery?: SessionInbox.Delivery | undefined
|
||||
}
|
||||
export type Endpoint5_11Output = void
|
||||
export type SessionMoveOperation<E = never> = (input: Endpoint5_11Input) => Effect.Effect<Endpoint5_11Output, E>
|
||||
@@ -183,10 +182,10 @@ export type Endpoint5_12Input = {
|
||||
readonly agents?: ReadonlyArray<AgentAttachment> | undefined
|
||||
readonly skills?: ReadonlyArray<PromptInput.SkillAttachment> | undefined
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly delivery?: SessionInbox.Delivery | undefined
|
||||
readonly delivery?: "steer" | "queue" | undefined
|
||||
readonly resume?: boolean | undefined
|
||||
}
|
||||
export type Endpoint5_12Output = SessionInbox.User
|
||||
export type Endpoint5_12Output = SessionPending.User
|
||||
export type SessionPromptOperation<E = never> = (input: Endpoint5_12Input) => Effect.Effect<Endpoint5_12Output, E>
|
||||
|
||||
export type Endpoint5_13Input = {
|
||||
@@ -199,10 +198,10 @@ export type Endpoint5_13Input = {
|
||||
readonly files?: ReadonlyArray<PromptInput.FileAttachment> | undefined
|
||||
readonly agents?: ReadonlyArray<AgentAttachment> | undefined
|
||||
readonly skills?: ReadonlyArray<PromptInput.SkillAttachment> | undefined
|
||||
readonly delivery?: SessionInbox.Delivery | undefined
|
||||
readonly delivery?: "steer" | "queue" | undefined
|
||||
readonly resume?: boolean | undefined
|
||||
}
|
||||
export type Endpoint5_13Output = SessionInbox.User
|
||||
export type Endpoint5_13Output = SessionPending.User
|
||||
export type SessionCommandOperation<E = never> = (input: Endpoint5_13Input) => Effect.Effect<Endpoint5_13Output, E>
|
||||
|
||||
export type Endpoint5_14Input = {
|
||||
@@ -220,10 +219,10 @@ export type Endpoint5_15Input = {
|
||||
readonly text: string
|
||||
readonly description?: string | undefined
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly delivery?: SessionInbox.Delivery | undefined
|
||||
readonly delivery?: "steer" | "queue" | undefined
|
||||
readonly resume?: boolean | undefined
|
||||
}
|
||||
export type Endpoint5_15Output = SessionInbox.Synthetic
|
||||
export type Endpoint5_15Output = SessionPending.Synthetic
|
||||
export type SessionSyntheticOperation<E = never> = (input: Endpoint5_15Input) => Effect.Effect<Endpoint5_15Output, E>
|
||||
|
||||
export type Endpoint5_16Input = {
|
||||
@@ -234,12 +233,8 @@ export type Endpoint5_16Input = {
|
||||
export type Endpoint5_16Output = void
|
||||
export type SessionShellOperation<E = never> = (input: Endpoint5_16Input) => Effect.Effect<Endpoint5_16Output, E>
|
||||
|
||||
export type Endpoint5_17Input = {
|
||||
readonly sessionID: Session.ID
|
||||
readonly id?: SessionMessage.ID | undefined
|
||||
readonly delivery?: SessionInbox.Delivery | undefined
|
||||
}
|
||||
export type Endpoint5_17Output = SessionInbox.Compaction
|
||||
export type Endpoint5_17Input = { readonly sessionID: Session.ID; readonly id?: SessionMessage.ID | undefined }
|
||||
export type Endpoint5_17Output = SessionPending.Compaction
|
||||
export type SessionCompactOperation<E = never> = (input: Endpoint5_17Input) => Effect.Effect<Endpoint5_17Output, E>
|
||||
|
||||
export type Endpoint5_18Input = { readonly sessionID: Session.ID }
|
||||
@@ -267,20 +262,22 @@ export type Endpoint5_22Output = ReadonlyArray<SessionMessage.Info>
|
||||
export type SessionContextOperation<E = never> = (input: Endpoint5_22Input) => Effect.Effect<Endpoint5_22Output, E>
|
||||
|
||||
export type Endpoint5_23Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_23Output = ReadonlyArray<SessionInbox.Info>
|
||||
export type SessionInboxListOperation<E = never> = (input: Endpoint5_23Input) => Effect.Effect<Endpoint5_23Output, E>
|
||||
export type Endpoint5_23Output = ReadonlyArray<SessionPending.Info>
|
||||
export type SessionPendingListOperation<E = never> = (input: Endpoint5_23Input) => Effect.Effect<Endpoint5_23Output, E>
|
||||
|
||||
export type Endpoint5_24Input = { readonly sessionID: Session.ID; readonly inboxID: SessionMessage.ID }
|
||||
export type Endpoint5_24Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
|
||||
export type Endpoint5_24Output = void
|
||||
export type SessionInboxCancelOperation<E = never> = (input: Endpoint5_24Input) => Effect.Effect<Endpoint5_24Output, E>
|
||||
export type SessionPendingCancelOperation<E = never> = (
|
||||
input: Endpoint5_24Input,
|
||||
) => Effect.Effect<Endpoint5_24Output, E>
|
||||
|
||||
export type Endpoint5_25Input = { readonly sessionID: Session.ID; readonly inboxID: SessionMessage.ID }
|
||||
export type Endpoint5_25Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
|
||||
export type Endpoint5_25Output = void
|
||||
export type SessionInboxSteerOperation<E = never> = (input: Endpoint5_25Input) => Effect.Effect<Endpoint5_25Output, E>
|
||||
export type SessionPendingSteerOperation<E = never> = (input: Endpoint5_25Input) => Effect.Effect<Endpoint5_25Output, E>
|
||||
|
||||
export type Endpoint5_26Input = { readonly sessionID: Session.ID; readonly inboxID: SessionMessage.ID }
|
||||
export type Endpoint5_26Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
|
||||
export type Endpoint5_26Output = void
|
||||
export type SessionInboxQueueOperation<E = never> = (input: Endpoint5_26Input) => Effect.Effect<Endpoint5_26Output, E>
|
||||
export type SessionPendingQueueOperation<E = never> = (input: Endpoint5_26Input) => Effect.Effect<Endpoint5_26Output, E>
|
||||
|
||||
export type Endpoint5_27Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_27Output = ReadonlyArray<InstructionEntry.Info>
|
||||
@@ -371,7 +368,7 @@ export type Endpoint5_31Output =
|
||||
readonly data: {
|
||||
readonly sessionID: Session.ID
|
||||
readonly location: Location.Ref
|
||||
readonly projectID: Project.ID
|
||||
readonly projectID?: Project.ID | undefined
|
||||
readonly subpath?: RelativePath | undefined
|
||||
}
|
||||
}
|
||||
@@ -413,45 +410,50 @@ export type Endpoint5_31Output =
|
||||
readonly id: Event.ID
|
||||
readonly created: DateTime.Utc
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly type: "session.inbox.delivered"
|
||||
readonly type: "session.input.promoted"
|
||||
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: { readonly sessionID: Session.ID; readonly inboxID: SessionMessage.ID }
|
||||
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: DateTime.Utc
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly type: "session.inbox.enqueued"
|
||||
readonly type: "session.input.admitted"
|
||||
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: {
|
||||
readonly sessionID: Session.ID
|
||||
readonly inboxID: SessionMessage.ID
|
||||
readonly item: SessionInbox.Item
|
||||
readonly inputID: SessionMessage.ID
|
||||
readonly input: SessionPending.Message
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: DateTime.Utc
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly type: "session.inbox.cancelled"
|
||||
readonly type: "session.input.cancelled"
|
||||
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: { readonly sessionID: Session.ID; readonly inboxID: SessionMessage.ID }
|
||||
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: DateTime.Utc
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly type: "session.inbox.delivery.changed"
|
||||
readonly type: "session.input.steered"
|
||||
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: {
|
||||
readonly sessionID: Session.ID
|
||||
readonly inboxID: SessionMessage.ID
|
||||
readonly delivery: SessionInbox.Delivery
|
||||
}
|
||||
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: DateTime.Utc
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly type: "session.input.queued"
|
||||
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
@@ -593,6 +595,7 @@ export type Endpoint5_31Output =
|
||||
readonly reasoning: number
|
||||
readonly cache: { readonly read: number; readonly write: number }
|
||||
}
|
||||
readonly generated?: DateTime.Utc | undefined
|
||||
readonly snapshot?: (string & Brand.Brand<"Snapshot.ID">) | undefined
|
||||
readonly files?: ReadonlyArray<RelativePath> | undefined
|
||||
}
|
||||
@@ -617,6 +620,7 @@ export type Endpoint5_31Output =
|
||||
readonly cache: { readonly read: number; readonly write: number }
|
||||
}
|
||||
| undefined
|
||||
readonly generated?: DateTime.Utc | undefined
|
||||
readonly snapshot?: (string & Brand.Brand<"Snapshot.ID">) | undefined
|
||||
readonly files?: ReadonlyArray<RelativePath> | undefined
|
||||
}
|
||||
@@ -812,6 +816,15 @@ export type Endpoint5_31Output =
|
||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: DateTime.Utc
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly type: "session.compaction.admitted"
|
||||
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: DateTime.Utc
|
||||
@@ -942,11 +955,11 @@ export interface SessionApi<E = never> {
|
||||
readonly commit: SessionRevertCommitOperation<E>
|
||||
}
|
||||
readonly context: SessionContextOperation<E>
|
||||
readonly inbox: {
|
||||
readonly list: SessionInboxListOperation<E>
|
||||
readonly cancel: SessionInboxCancelOperation<E>
|
||||
readonly steer: SessionInboxSteerOperation<E>
|
||||
readonly queue: SessionInboxQueueOperation<E>
|
||||
readonly pending: {
|
||||
readonly list: SessionPendingListOperation<E>
|
||||
readonly cancel: SessionPendingCancelOperation<E>
|
||||
readonly steer: SessionPendingSteerOperation<E>
|
||||
readonly queue: SessionPendingQueueOperation<E>
|
||||
}
|
||||
readonly instructions: {
|
||||
readonly entry: {
|
||||
@@ -1362,11 +1375,11 @@ export type Endpoint16_1Input = {
|
||||
readonly limit?: number | undefined
|
||||
}
|
||||
export type Endpoint16_1Output = { readonly location: Location.Info; readonly data: ReadonlyArray<FileSystem.Entry> }
|
||||
export type FileGetOperation<E = never> = (input: Endpoint16_1Input) => Effect.Effect<Endpoint16_1Output, E>
|
||||
export type FileFindOperation<E = never> = (input: Endpoint16_1Input) => Effect.Effect<Endpoint16_1Output, E>
|
||||
|
||||
export interface FileApi<E = never> {
|
||||
readonly list: FileListOperation<E>
|
||||
readonly get: FileGetOperation<E>
|
||||
readonly find: FileFindOperation<E>
|
||||
}
|
||||
|
||||
export type Endpoint17_0Input = {
|
||||
|
||||
@@ -399,7 +399,7 @@ const Endpoint5_11 = (raw: RawClient["server.session"]) => (input: Endpoint5_11I
|
||||
preserveEffect<Endpoint5_11Output>()(
|
||||
raw["session.move"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { directory: input["directory"], workspaceID: input["workspaceID"], delivery: input["delivery"] },
|
||||
payload: { directory: input["directory"], workspaceID: input["workspaceID"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
@@ -481,10 +481,7 @@ const Endpoint5_16 = (raw: RawClient["server.session"]) => (input: Endpoint5_16I
|
||||
|
||||
const Endpoint5_17 = (raw: RawClient["server.session"]) => (input: Endpoint5_17Input) =>
|
||||
preserveEffect<Endpoint5_17Output>()(
|
||||
raw["session.compact"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { id: input["id"], delivery: input["delivery"] },
|
||||
}).pipe(
|
||||
raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
@@ -526,7 +523,7 @@ const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22I
|
||||
|
||||
const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23Input) =>
|
||||
preserveEffect<Endpoint5_23Output>()(
|
||||
raw["session.inbox.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
raw["session.pending.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
@@ -534,21 +531,21 @@ const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23I
|
||||
|
||||
const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) =>
|
||||
preserveEffect<Endpoint5_24Output>()(
|
||||
raw["session.inbox.cancel"]({ params: { sessionID: input["sessionID"], inboxID: input["inboxID"] } }).pipe(
|
||||
raw["session.pending.cancel"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) =>
|
||||
preserveEffect<Endpoint5_25Output>()(
|
||||
raw["session.inbox.steer"]({ params: { sessionID: input["sessionID"], inboxID: input["inboxID"] } }).pipe(
|
||||
raw["session.pending.steer"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) =>
|
||||
preserveEffect<Endpoint5_26Output>()(
|
||||
raw["session.inbox.queue"]({ params: { sessionID: input["sessionID"], inboxID: input["inboxID"] } }).pipe(
|
||||
raw["session.pending.queue"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
),
|
||||
)
|
||||
@@ -640,7 +637,7 @@ const adaptGroup5 = (raw: RawClient["server.session"]) => ({
|
||||
wait: Endpoint5_18(raw),
|
||||
revert: { stage: Endpoint5_19(raw), clear: Endpoint5_20(raw), commit: Endpoint5_21(raw) },
|
||||
context: Endpoint5_22(raw),
|
||||
inbox: { list: Endpoint5_23(raw), cancel: Endpoint5_24(raw), steer: Endpoint5_25(raw), queue: Endpoint5_26(raw) },
|
||||
pending: { list: Endpoint5_23(raw), cancel: Endpoint5_24(raw), steer: Endpoint5_25(raw), queue: Endpoint5_26(raw) },
|
||||
instructions: { entry: { list: Endpoint5_27(raw), put: Endpoint5_28(raw), remove: Endpoint5_29(raw) } },
|
||||
generate: Endpoint5_30(raw),
|
||||
log: Endpoint5_31(raw),
|
||||
@@ -1034,12 +1031,12 @@ const Endpoint16_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint16_0Input
|
||||
|
||||
const Endpoint16_1 = (raw: RawClient["server.fs"]) => (input: Endpoint16_1Input) =>
|
||||
preserveEffect<Endpoint16_1Output>()(
|
||||
raw["fs.get"]({
|
||||
raw["fs.find"]({
|
||||
query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const adaptGroup16 = (raw: RawClient["server.fs"]) => ({ list: Endpoint16_0(raw), get: Endpoint16_1(raw) })
|
||||
const adaptGroup16 = (raw: RawClient["server.fs"]) => ({ list: Endpoint16_0(raw), find: Endpoint16_1(raw) })
|
||||
|
||||
const Endpoint17_0 = (raw: RawClient["server.command"]) => (input?: Endpoint17_0Input) =>
|
||||
preserveEffect<Endpoint17_0Output>()(
|
||||
|
||||
@@ -41,7 +41,7 @@ export { Reference } from "@opencode-ai/schema/reference"
|
||||
export { WebSearch } from "@opencode-ai/schema/websearch"
|
||||
export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema"
|
||||
export { Session } from "@opencode-ai/schema/session"
|
||||
export { SessionInbox } from "@opencode-ai/schema/session-inbox"
|
||||
export { SessionPending } from "@opencode-ai/schema/session-pending"
|
||||
export { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
export { Skill } from "@opencode-ai/schema/skill"
|
||||
export { Prompt } from "@opencode-ai/schema/prompt"
|
||||
|
||||
@@ -56,14 +56,14 @@ import type {
|
||||
SessionRevertCommitOutput,
|
||||
SessionContextInput,
|
||||
SessionContextOutput,
|
||||
SessionInboxListInput,
|
||||
SessionInboxListOutput,
|
||||
SessionInboxCancelInput,
|
||||
SessionInboxCancelOutput,
|
||||
SessionInboxSteerInput,
|
||||
SessionInboxSteerOutput,
|
||||
SessionInboxQueueInput,
|
||||
SessionInboxQueueOutput,
|
||||
SessionPendingListInput,
|
||||
SessionPendingListOutput,
|
||||
SessionPendingCancelInput,
|
||||
SessionPendingCancelOutput,
|
||||
SessionPendingSteerInput,
|
||||
SessionPendingSteerOutput,
|
||||
SessionPendingQueueInput,
|
||||
SessionPendingQueueOutput,
|
||||
SessionInstructionsEntryListInput,
|
||||
SessionInstructionsEntryListOutput,
|
||||
SessionInstructionsEntryPutInput,
|
||||
@@ -167,8 +167,8 @@ import type {
|
||||
FileReadOutput,
|
||||
FileListInput,
|
||||
FileListOutput,
|
||||
FileGetInput,
|
||||
FileGetOutput,
|
||||
FileFindInput,
|
||||
FileFindOutput,
|
||||
CommandListInput,
|
||||
CommandListOutput,
|
||||
SkillListInput,
|
||||
@@ -598,7 +598,7 @@ export function make(options: ClientOptions) {
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/move`,
|
||||
body: { directory: input["directory"], workspaceID: input["workspaceID"], delivery: input["delivery"] },
|
||||
body: { directory: input["directory"], workspaceID: input["workspaceID"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
@@ -697,7 +697,7 @@ export function make(options: ClientOptions) {
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/compact`,
|
||||
body: { id: input["id"], delivery: input["delivery"] },
|
||||
body: { id: input["id"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [409, 404, 400, 401],
|
||||
empty: false,
|
||||
@@ -762,45 +762,45 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
inbox: {
|
||||
list: (input: SessionInboxListInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionInboxListOutput }>(
|
||||
pending: {
|
||||
list: (input: SessionPendingListInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionPendingListOutput }>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/inbox`,
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/pending`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
cancel: (input: SessionInboxCancelInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionInboxCancelOutput>(
|
||||
cancel: (input: SessionPendingCancelInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionPendingCancelOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/inbox/${encodeURIComponent(input.inboxID)}`,
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/pending/${encodeURIComponent(input.inputID)}`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [409, 404, 401, 400],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
steer: (input: SessionInboxSteerInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionInboxSteerOutput>(
|
||||
steer: (input: SessionPendingSteerInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionPendingSteerOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/inbox/${encodeURIComponent(input.inboxID)}/steer`,
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/pending/${encodeURIComponent(input.inputID)}/steer`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [409, 404, 401, 400],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
queue: (input: SessionInboxQueueInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionInboxQueueOutput>(
|
||||
queue: (input: SessionPendingQueueInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionPendingQueueOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/inbox/${encodeURIComponent(input.inboxID)}/queue`,
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/pending/${encodeURIComponent(input.inputID)}/queue`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [409, 404, 401, 400],
|
||||
empty: true,
|
||||
@@ -1473,8 +1473,8 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
get: (input: FileGetInput, requestOptions?: RequestOptions) =>
|
||||
request<FileGetOutput>(
|
||||
find: (input: FileFindInput, requestOptions?: RequestOptions) =>
|
||||
request<FileFindOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/fs/find`,
|
||||
|
||||
@@ -130,17 +130,15 @@ export type SessionMessageCompactionCompleted = {
|
||||
|
||||
export type SessionActive = { type: "running" }
|
||||
|
||||
export type SessionInboxDelivery = "steer" | "queue"
|
||||
export type SessionPendingSyntheticData = { text: string; description?: string; metadata?: { [x: string]: JsonValue } }
|
||||
|
||||
export type SessionInboxSyntheticPayload = { text: string; description?: string; metadata?: { [x: string]: JsonValue } }
|
||||
|
||||
export type SessionInboxCompactionPayload = {}
|
||||
export type SessionPendingCompaction = { id: string; sessionID: string; timeCreated: number; type: "compaction" }
|
||||
|
||||
export type InstructionEntryKey = string
|
||||
|
||||
export type SessionGenerateResponse = { data: { text: string } }
|
||||
|
||||
export type SessionInboxSyntheticPayload1 = { text: string; description?: string; metadata?: { [x: string]: any } }
|
||||
export type SessionPendingSyntheticData1 = { text: string; description?: string; metadata?: { [x: string]: any } }
|
||||
|
||||
export type ShellInfo = {
|
||||
id: string
|
||||
@@ -422,8 +420,6 @@ export type SessionMessageLocationSwitched = {
|
||||
previous?: { location: LocationRef; projectID?: string; subpath?: string }
|
||||
}
|
||||
|
||||
export type SessionInboxMovePayload = { location: LocationRef; projectID: string; subpath?: string }
|
||||
|
||||
export type SessionCreated = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -472,7 +468,7 @@ export type SessionMoved = {
|
||||
type: "session.moved"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; location: LocationRef; projectID: string; subpath?: string }
|
||||
data: { sessionID: string; location: LocationRef; projectID?: string; subpath?: string }
|
||||
}
|
||||
|
||||
export type SessionRenamed = {
|
||||
@@ -505,24 +501,44 @@ export type SessionForked = {
|
||||
data: { sessionID: string; parentID: string; boundary: SessionForkBoundary; instructions?: { [x: string]: string } }
|
||||
}
|
||||
|
||||
export type SessionInboxDelivered = {
|
||||
export type SessionInputPromoted = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.inbox.delivered"
|
||||
type: "session.input.promoted"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; inboxID: string }
|
||||
data: { sessionID: string; inputID: string }
|
||||
}
|
||||
|
||||
export type SessionInboxCancelled = {
|
||||
export type SessionInputCancelled = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.inbox.cancelled"
|
||||
type: "session.input.cancelled"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; inboxID: string }
|
||||
data: { sessionID: string; inputID: string }
|
||||
}
|
||||
|
||||
export type SessionInputSteered = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.input.steered"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; inputID: string }
|
||||
}
|
||||
|
||||
export type SessionInputQueued = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.input.queued"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; inputID: string }
|
||||
}
|
||||
|
||||
export type SessionExecutionStarted = {
|
||||
@@ -608,6 +624,7 @@ export type SessionStepEnded = {
|
||||
finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
|
||||
cost: MoneyUSD
|
||||
tokens: TokenUsageInfo
|
||||
generated?: number
|
||||
snapshot?: string
|
||||
files?: Array<string>
|
||||
}
|
||||
@@ -643,6 +660,16 @@ export type SessionToolInputEnded = {
|
||||
data: { sessionID: string; assistantMessageID: string; id: string; text: string }
|
||||
}
|
||||
|
||||
export type SessionCompactionAdmitted = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.compaction.admitted"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; inputID: string }
|
||||
}
|
||||
|
||||
export type SessionCompactionStarted = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -1111,6 +1138,7 @@ export type SessionStepFailed = {
|
||||
error: SessionStructuredError
|
||||
cost?: MoneyUSD
|
||||
tokens?: TokenUsageInfo
|
||||
generated?: number
|
||||
snapshot?: string
|
||||
files?: Array<string>
|
||||
}
|
||||
@@ -1136,36 +1164,23 @@ export type SessionCompactionFailed = {
|
||||
data: { sessionID: string; reason: "auto" | "manual"; error: SessionStructuredError; inputID?: string }
|
||||
}
|
||||
|
||||
export type SessionInboxDeliveryChanged = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.inbox.delivery.changed"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; inboxID: string; delivery: SessionInboxDelivery }
|
||||
}
|
||||
|
||||
export type SessionInboxSynthetic = {
|
||||
export type SessionPendingSynthetic = {
|
||||
id: string
|
||||
sessionID: string
|
||||
timeCreated: number
|
||||
type: "synthetic"
|
||||
payload: SessionInboxSyntheticPayload
|
||||
delivery: SessionInboxDelivery
|
||||
}
|
||||
|
||||
export type SessionInboxCompaction = {
|
||||
id: string
|
||||
sessionID: string
|
||||
timeCreated: number
|
||||
type: "compaction"
|
||||
payload: SessionInboxCompactionPayload
|
||||
delivery: SessionInboxDelivery
|
||||
data: SessionPendingSyntheticData
|
||||
delivery: "steer" | "queue"
|
||||
}
|
||||
|
||||
export type InstructionEntryInfo = { key: InstructionEntryKey; value: JsonValue }
|
||||
|
||||
export type SessionPendingSyntheticMessage = {
|
||||
type: "synthetic"
|
||||
data: SessionPendingSyntheticData1
|
||||
delivery: "steer" | "queue"
|
||||
}
|
||||
|
||||
export type SessionShellStarted = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -1514,15 +1529,6 @@ export type VcsInfo = { branch: VcsBranch }
|
||||
|
||||
export type PermissionRuleset = Array<PermissionRule>
|
||||
|
||||
export type SessionInboxMove = {
|
||||
id: string
|
||||
sessionID: string
|
||||
timeCreated: number
|
||||
type: "move"
|
||||
payload: SessionInboxMovePayload
|
||||
delivery: SessionInboxDelivery
|
||||
}
|
||||
|
||||
export type SessionInfo = {
|
||||
id: string
|
||||
parentID?: string
|
||||
@@ -1560,7 +1566,7 @@ export type SessionMessageUser = {
|
||||
type: "user"
|
||||
}
|
||||
|
||||
export type SessionInboxUserPayload = {
|
||||
export type SessionPendingUserData = {
|
||||
text: string
|
||||
files?: Array<PromptFileAttachment>
|
||||
agents?: Array<PromptAgentAttachment>
|
||||
@@ -1568,7 +1574,7 @@ export type SessionInboxUserPayload = {
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
}
|
||||
|
||||
export type SessionInboxUserPayload1 = {
|
||||
export type SessionPendingUserData1 = {
|
||||
text: string
|
||||
files?: Array<PromptFileAttachment>
|
||||
agents?: Array<PromptAgentAttachment>
|
||||
@@ -1880,20 +1886,16 @@ export type ConfigEntry =
|
||||
|
||||
export type SessionsResponse = { data: Array<SessionInfo>; cursor: { previous?: string | null; next?: string | null } }
|
||||
|
||||
export type SessionInboxUser = {
|
||||
export type SessionPendingUser = {
|
||||
id: string
|
||||
sessionID: string
|
||||
timeCreated: number
|
||||
type: "user"
|
||||
payload: SessionInboxUserPayload
|
||||
delivery: SessionInboxDelivery
|
||||
data: SessionPendingUserData
|
||||
delivery: "steer" | "queue"
|
||||
}
|
||||
|
||||
export type SessionInboxItem =
|
||||
| { type: "user"; payload: SessionInboxUserPayload1; delivery: SessionInboxDelivery }
|
||||
| { type: "synthetic"; payload: SessionInboxSyntheticPayload1; delivery: SessionInboxDelivery }
|
||||
| { type: "compaction"; payload: SessionInboxCompactionPayload; delivery: SessionInboxDelivery }
|
||||
| { type: "move"; payload: SessionInboxMovePayload; delivery: SessionInboxDelivery }
|
||||
export type SessionPendingUserMessage = { type: "user"; data: SessionPendingUserData1; delivery: "steer" | "queue" }
|
||||
|
||||
export type SessionMessageAssistantTool = {
|
||||
type: "tool"
|
||||
@@ -1914,22 +1916,14 @@ export type FormFields = [FormField, ...Array<FormField>]
|
||||
|
||||
export type FormFields3 = [FormField1, ...Array<FormField1>]
|
||||
|
||||
export type SessionInboxInfo = SessionInboxUser | SessionInboxSynthetic | SessionInboxCompaction | SessionInboxMove
|
||||
export type SessionPendingInfo = SessionPendingUser | SessionPendingSynthetic | SessionPendingCompaction
|
||||
|
||||
export type SessionInboxEnqueued = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.inbox.enqueued"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; inboxID: string; item: SessionInboxItem }
|
||||
}
|
||||
export type SessionPendingMessage = SessionPendingUserMessage | SessionPendingSyntheticMessage
|
||||
|
||||
export type SessionMessageAssistant = {
|
||||
id: string
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
time: { created: number; completed?: number }
|
||||
time: { created: number; started?: number; generated?: number; completed?: number }
|
||||
type: "assistant"
|
||||
agent: string
|
||||
model: ModelRef
|
||||
@@ -1950,47 +1944,15 @@ export type FormInfo = { id: string; sessionID: string; title: string; metadata?
|
||||
|
||||
export type FormInfo1 = { id: string; sessionID: string; title: string; metadata?: FormMetadata1; fields: FormFields3 }
|
||||
|
||||
export type SessionEventDurable =
|
||||
| SessionCreated
|
||||
| SessionAgentSelected
|
||||
| SessionModelSelected
|
||||
| SessionMoved
|
||||
| SessionRenamed
|
||||
| SessionDeleted
|
||||
| SessionForked
|
||||
| SessionInboxDelivered
|
||||
| SessionInboxEnqueued
|
||||
| SessionInboxCancelled
|
||||
| SessionInboxDeliveryChanged
|
||||
| SessionExecutionStarted
|
||||
| SessionExecutionSucceeded
|
||||
| SessionExecutionFailed
|
||||
| SessionExecutionInterrupted
|
||||
| SessionInstructionsUpdated
|
||||
| SessionSynthetic
|
||||
| SessionSkillActivated
|
||||
| SessionShellStarted
|
||||
| SessionShellEnded
|
||||
| SessionStepStarted
|
||||
| SessionStepEnded
|
||||
| SessionStepFailed
|
||||
| SessionTextStarted
|
||||
| SessionTextEnded
|
||||
| SessionReasoningStarted
|
||||
| SessionReasoningEnded
|
||||
| SessionToolInputStarted
|
||||
| SessionToolInputEnded
|
||||
| SessionToolCalled
|
||||
| SessionToolSuccess
|
||||
| SessionToolFailed
|
||||
| SessionRetryScheduled
|
||||
| SessionCompactionStarted
|
||||
| SessionCompactionEnded
|
||||
| SessionCompactionFailed
|
||||
| SessionRevertStaged
|
||||
| SessionRevertCleared
|
||||
| SessionRevertCommitted
|
||||
| SessionUsageRecorded
|
||||
export type SessionInputAdmitted = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.input.admitted"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; inputID: string; input: SessionPendingMessage }
|
||||
}
|
||||
|
||||
export type SessionMessageInfo =
|
||||
| SessionMessageAgentSelected
|
||||
@@ -2019,7 +1981,49 @@ export type FormCreated = {
|
||||
data: { form: FormInfo1 }
|
||||
}
|
||||
|
||||
export type SessionLogItem = SessionEventDurable | EventLogSynced
|
||||
export type SessionEventDurable =
|
||||
| SessionCreated
|
||||
| SessionAgentSelected
|
||||
| SessionModelSelected
|
||||
| SessionMoved
|
||||
| SessionRenamed
|
||||
| SessionDeleted
|
||||
| SessionForked
|
||||
| SessionInputPromoted
|
||||
| SessionInputAdmitted
|
||||
| SessionInputCancelled
|
||||
| SessionInputSteered
|
||||
| SessionInputQueued
|
||||
| SessionExecutionStarted
|
||||
| SessionExecutionSucceeded
|
||||
| SessionExecutionFailed
|
||||
| SessionExecutionInterrupted
|
||||
| SessionInstructionsUpdated
|
||||
| SessionSynthetic
|
||||
| SessionSkillActivated
|
||||
| SessionShellStarted
|
||||
| SessionShellEnded
|
||||
| SessionStepStarted
|
||||
| SessionStepEnded
|
||||
| SessionStepFailed
|
||||
| SessionTextStarted
|
||||
| SessionTextEnded
|
||||
| SessionReasoningStarted
|
||||
| SessionReasoningEnded
|
||||
| SessionToolInputStarted
|
||||
| SessionToolInputEnded
|
||||
| SessionToolCalled
|
||||
| SessionToolSuccess
|
||||
| SessionToolFailed
|
||||
| SessionRetryScheduled
|
||||
| SessionCompactionAdmitted
|
||||
| SessionCompactionStarted
|
||||
| SessionCompactionEnded
|
||||
| SessionCompactionFailed
|
||||
| SessionRevertStaged
|
||||
| SessionRevertCleared
|
||||
| SessionRevertCommitted
|
||||
| SessionUsageRecorded
|
||||
|
||||
export type SessionTransferData = { info: SessionInfo; messages: Array<SessionMessageInfo> }
|
||||
|
||||
@@ -2049,10 +2053,11 @@ export type V2Event =
|
||||
| SessionUsageUpdated
|
||||
| SessionDeleted
|
||||
| SessionForked
|
||||
| SessionInboxDelivered
|
||||
| SessionInboxEnqueued
|
||||
| SessionInboxCancelled
|
||||
| SessionInboxDeliveryChanged
|
||||
| SessionInputPromoted
|
||||
| SessionInputAdmitted
|
||||
| SessionInputCancelled
|
||||
| SessionInputSteered
|
||||
| SessionInputQueued
|
||||
| SessionExecutionStarted
|
||||
| SessionExecutionSucceeded
|
||||
| SessionExecutionFailed
|
||||
@@ -2079,6 +2084,7 @@ export type V2Event =
|
||||
| SessionToolSuccess
|
||||
| SessionToolFailed
|
||||
| SessionRetryScheduled
|
||||
| SessionCompactionAdmitted
|
||||
| SessionCompactionStarted
|
||||
| SessionCompactionDelta
|
||||
| SessionCompactionEnded
|
||||
@@ -2123,6 +2129,8 @@ export type V2Event =
|
||||
| McpResourcesChanged
|
||||
| V2EventServerConnected
|
||||
|
||||
export type SessionLogItem = SessionEventDurable | EventLogSynced
|
||||
|
||||
export type UnauthorizedError = { readonly _tag: "UnauthorizedError"; readonly message: string }
|
||||
export const isUnauthorizedError = (value: unknown): value is UnauthorizedError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnauthorizedError"
|
||||
@@ -2636,7 +2644,12 @@ export type SessionImportInput = {
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly time: { readonly created: number; readonly completed?: number }
|
||||
readonly time: {
|
||||
readonly created: number
|
||||
readonly started?: number
|
||||
readonly generated?: number
|
||||
readonly completed?: number
|
||||
}
|
||||
readonly type: "assistant"
|
||||
readonly agent: string
|
||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||
@@ -2903,7 +2916,12 @@ export type SessionImportInput = {
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly time: { readonly created: number; readonly completed?: number }
|
||||
readonly time: {
|
||||
readonly created: number
|
||||
readonly started?: number
|
||||
readonly generated?: number
|
||||
readonly completed?: number
|
||||
}
|
||||
readonly type: "assistant"
|
||||
readonly agent: string
|
||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||
@@ -3170,7 +3188,12 @@ export type SessionImportInput = {
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly time: { readonly created: number; readonly completed?: number }
|
||||
readonly time: {
|
||||
readonly created: number
|
||||
readonly started?: number
|
||||
readonly generated?: number
|
||||
readonly completed?: number
|
||||
}
|
||||
readonly type: "assistant"
|
||||
readonly agent: string
|
||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||
@@ -3355,21 +3378,8 @@ export type SessionRenameOutput = void
|
||||
|
||||
export type SessionMoveInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly directory: {
|
||||
readonly directory: string
|
||||
readonly workspaceID?: string
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
}["directory"]
|
||||
readonly workspaceID?: {
|
||||
readonly directory: string
|
||||
readonly workspaceID?: string
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
}["workspaceID"]
|
||||
readonly delivery?: {
|
||||
readonly directory: string
|
||||
readonly workspaceID?: string
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
}["delivery"]
|
||||
readonly directory: { readonly directory: string; readonly workspaceID?: string }["directory"]
|
||||
readonly workspaceID?: { readonly directory: string; readonly workspaceID?: string }["workspaceID"]
|
||||
}
|
||||
|
||||
export type SessionMoveOutput = void
|
||||
@@ -3394,7 +3404,7 @@ export type SessionPromptInput = {
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["id"]
|
||||
readonly text: {
|
||||
@@ -3415,7 +3425,7 @@ export type SessionPromptInput = {
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["text"]
|
||||
readonly files?: {
|
||||
@@ -3436,7 +3446,7 @@ export type SessionPromptInput = {
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["files"]
|
||||
readonly agents?: {
|
||||
@@ -3457,7 +3467,7 @@ export type SessionPromptInput = {
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["agents"]
|
||||
readonly skills?: {
|
||||
@@ -3478,7 +3488,7 @@ export type SessionPromptInput = {
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["skills"]
|
||||
readonly metadata?: {
|
||||
@@ -3499,7 +3509,7 @@ export type SessionPromptInput = {
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["metadata"]
|
||||
readonly delivery?: {
|
||||
@@ -3520,7 +3530,7 @@ export type SessionPromptInput = {
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["delivery"]
|
||||
readonly resume?: {
|
||||
@@ -3541,12 +3551,12 @@ export type SessionPromptInput = {
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["resume"]
|
||||
}
|
||||
|
||||
export type SessionPromptOutput = { data: SessionInboxUser }["data"]
|
||||
export type SessionPromptOutput = { data: SessionPendingUser }["data"]
|
||||
|
||||
export type SessionCommandInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
@@ -3570,7 +3580,7 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["id"]
|
||||
readonly command: {
|
||||
@@ -3593,7 +3603,7 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["command"]
|
||||
readonly arguments?: {
|
||||
@@ -3616,7 +3626,7 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["arguments"]
|
||||
readonly agent?: {
|
||||
@@ -3639,7 +3649,7 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["agent"]
|
||||
readonly model?: {
|
||||
@@ -3662,7 +3672,7 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["model"]
|
||||
readonly files?: {
|
||||
@@ -3685,7 +3695,7 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["files"]
|
||||
readonly agents?: {
|
||||
@@ -3708,7 +3718,7 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["agents"]
|
||||
readonly skills?: {
|
||||
@@ -3731,7 +3741,7 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["skills"]
|
||||
readonly delivery?: {
|
||||
@@ -3754,7 +3764,7 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["delivery"]
|
||||
readonly resume?: {
|
||||
@@ -3777,12 +3787,12 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["resume"]
|
||||
}
|
||||
|
||||
export type SessionCommandOutput = { data: SessionInboxUser }["data"]
|
||||
export type SessionCommandOutput = { data: SessionPendingUser }["data"]
|
||||
|
||||
export type SessionSkillInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
@@ -3812,7 +3822,7 @@ export type SessionSyntheticInput = {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["id"]
|
||||
readonly text: {
|
||||
@@ -3820,7 +3830,7 @@ export type SessionSyntheticInput = {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["text"]
|
||||
readonly description?: {
|
||||
@@ -3828,7 +3838,7 @@ export type SessionSyntheticInput = {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["description"]
|
||||
readonly metadata?: {
|
||||
@@ -3836,7 +3846,7 @@ export type SessionSyntheticInput = {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["metadata"]
|
||||
readonly delivery?: {
|
||||
@@ -3844,7 +3854,7 @@ export type SessionSyntheticInput = {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["delivery"]
|
||||
readonly resume?: {
|
||||
@@ -3852,12 +3862,12 @@ export type SessionSyntheticInput = {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["resume"]
|
||||
}
|
||||
|
||||
export type SessionSyntheticOutput = { data: SessionInboxSynthetic }["data"]
|
||||
export type SessionSyntheticOutput = { data: SessionPendingSynthetic }["data"]
|
||||
|
||||
export type SessionShellInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
@@ -3869,14 +3879,10 @@ export type SessionShellOutput = void
|
||||
|
||||
export type SessionCompactInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly id?: { readonly id?: string | undefined; readonly delivery?: ("steer" | "queue") | undefined }["id"]
|
||||
readonly delivery?: {
|
||||
readonly id?: string | undefined
|
||||
readonly delivery?: ("steer" | "queue") | undefined
|
||||
}["delivery"]
|
||||
readonly id?: { readonly id?: string | undefined }["id"]
|
||||
}
|
||||
|
||||
export type SessionCompactOutput = { data: SessionInboxCompaction }["data"]
|
||||
export type SessionCompactOutput = { data: SessionPendingCompaction }["data"]
|
||||
|
||||
export type SessionWaitInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
|
||||
@@ -3902,30 +3908,30 @@ export type SessionContextInput = { readonly sessionID: { readonly sessionID: st
|
||||
|
||||
export type SessionContextOutput = { data: Array<SessionMessageInfo> }["data"]
|
||||
|
||||
export type SessionInboxListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
export type SessionPendingListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
|
||||
export type SessionInboxListOutput = { data: Array<SessionInboxInfo> }["data"]
|
||||
export type SessionPendingListOutput = { data: Array<SessionPendingInfo> }["data"]
|
||||
|
||||
export type SessionInboxCancelInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly inboxID: string }["sessionID"]
|
||||
readonly inboxID: { readonly sessionID: string; readonly inboxID: string }["inboxID"]
|
||||
export type SessionPendingCancelInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"]
|
||||
readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"]
|
||||
}
|
||||
|
||||
export type SessionInboxCancelOutput = void
|
||||
export type SessionPendingCancelOutput = void
|
||||
|
||||
export type SessionInboxSteerInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly inboxID: string }["sessionID"]
|
||||
readonly inboxID: { readonly sessionID: string; readonly inboxID: string }["inboxID"]
|
||||
export type SessionPendingSteerInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"]
|
||||
readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"]
|
||||
}
|
||||
|
||||
export type SessionInboxSteerOutput = void
|
||||
export type SessionPendingSteerOutput = void
|
||||
|
||||
export type SessionInboxQueueInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly inboxID: string }["sessionID"]
|
||||
readonly inboxID: { readonly sessionID: string; readonly inboxID: string }["inboxID"]
|
||||
export type SessionPendingQueueInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"]
|
||||
readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"]
|
||||
}
|
||||
|
||||
export type SessionInboxQueueOutput = void
|
||||
export type SessionPendingQueueOutput = void
|
||||
|
||||
export type SessionInstructionsEntryListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
|
||||
@@ -5335,7 +5341,7 @@ export type FileListOutput = {
|
||||
data: Array<FileSystemEntry>
|
||||
}
|
||||
|
||||
export type FileGetInput = {
|
||||
export type FileFindInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
readonly query: string
|
||||
@@ -5362,7 +5368,7 @@ export type FileGetInput = {
|
||||
}["limit"]
|
||||
}
|
||||
|
||||
export type FileGetOutput = {
|
||||
export type FileFindOutput = {
|
||||
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } }
|
||||
data: Array<FileSystemEntry>
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
||||
expect(Object.getPrototypeOf(result.created)).toBe(Object.prototype)
|
||||
expect(result.created.id).toBe("ses_test")
|
||||
expect(Object.getPrototypeOf(result.admitted)).toBe(Object.prototype)
|
||||
expect(Object.getPrototypeOf(result.admitted.payload)).toBe(Object.prototype)
|
||||
expect(Object.getPrototypeOf(result.admitted.data)).toBe(Object.prototype)
|
||||
expect(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000)
|
||||
expect(result.context).toEqual([])
|
||||
expect(logQueries[0]).toEqual({ after: "0" })
|
||||
@@ -271,7 +271,7 @@ const admission = {
|
||||
id: "msg_test",
|
||||
sessionID: "ses_test",
|
||||
type: "user",
|
||||
payload: { text: "Hello" },
|
||||
data: { text: "Hello" },
|
||||
delivery: "steer",
|
||||
timeCreated: 1_717_171_717_000,
|
||||
},
|
||||
@@ -280,8 +280,6 @@ const admission = {
|
||||
const compactionAdmission = {
|
||||
data: {
|
||||
type: "compaction",
|
||||
payload: {},
|
||||
delivery: "queue",
|
||||
id: "msg_compaction",
|
||||
sessionID: "ses_test",
|
||||
timeCreated: 1_717_171_717_000,
|
||||
|
||||
@@ -373,7 +373,7 @@ test("session instructions methods use the public HTTP contract", async () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("session.inbox.list uses the public HTTP contract", async () => {
|
||||
test("session.pending.list uses the public HTTP contract", async () => {
|
||||
const requests: Array<{ method: string; url: string }> = []
|
||||
const pending = [
|
||||
{
|
||||
@@ -381,7 +381,7 @@ test("session.inbox.list uses the public HTTP contract", async () => {
|
||||
sessionID: "ses_test",
|
||||
timeCreated: 1_717_171_717_000,
|
||||
type: "user",
|
||||
payload: { text: "Fix the failing tests" },
|
||||
data: { text: "Fix the failing tests" },
|
||||
delivery: "steer",
|
||||
},
|
||||
]
|
||||
@@ -394,13 +394,13 @@ test("session.inbox.list uses the public HTTP contract", async () => {
|
||||
},
|
||||
})
|
||||
|
||||
const result = await client.session.inbox.list({ sessionID: "ses_test" })
|
||||
const result = await client.session.pending.list({ sessionID: "ses_test" })
|
||||
|
||||
expect(result).toEqual(pending)
|
||||
expect(requests).toEqual([{ method: "GET", url: "http://localhost:3000/api/session/ses_test/inbox" }])
|
||||
expect(requests).toEqual([{ method: "GET", url: "http://localhost:3000/api/session/ses_test/pending" }])
|
||||
})
|
||||
|
||||
test("session.inbox mutations use the public HTTP contract", async () => {
|
||||
test("session.pending mutations use the public HTTP contract", async () => {
|
||||
const requests: Array<{ method: string; url: string }> = []
|
||||
const client = OpenCode.make({
|
||||
baseUrl: "http://localhost:3000",
|
||||
@@ -411,14 +411,14 @@ test("session.inbox mutations use the public HTTP contract", async () => {
|
||||
},
|
||||
})
|
||||
|
||||
await client.session.inbox.cancel({ sessionID: "ses_test", inboxID: "msg_cancel" })
|
||||
await client.session.inbox.steer({ sessionID: "ses_test", inboxID: "msg_steer" })
|
||||
await client.session.inbox.queue({ sessionID: "ses_test", inboxID: "msg_queue" })
|
||||
await client.session.pending.cancel({ sessionID: "ses_test", inputID: "msg_cancel" })
|
||||
await client.session.pending.steer({ sessionID: "ses_test", inputID: "msg_steer" })
|
||||
await client.session.pending.queue({ sessionID: "ses_test", inputID: "msg_queue" })
|
||||
|
||||
expect(requests).toEqual([
|
||||
{ method: "DELETE", url: "http://localhost:3000/api/session/ses_test/inbox/msg_cancel" },
|
||||
{ method: "POST", url: "http://localhost:3000/api/session/ses_test/inbox/msg_steer/steer" },
|
||||
{ method: "POST", url: "http://localhost:3000/api/session/ses_test/inbox/msg_queue/queue" },
|
||||
{ method: "DELETE", url: "http://localhost:3000/api/session/ses_test/pending/msg_cancel" },
|
||||
{ method: "POST", url: "http://localhost:3000/api/session/ses_test/pending/msg_steer/steer" },
|
||||
{ method: "POST", url: "http://localhost:3000/api/session/ses_test/pending/msg_queue/queue" },
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
@@ -6866,7 +6866,7 @@
|
||||
"/api/fs/find": {
|
||||
"get": {
|
||||
"tags": ["filesystem"],
|
||||
"operationId": "v2.fs.get",
|
||||
"operationId": "v2.fs.find",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "location",
|
||||
|
||||
+2
-134
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "5c1aa56b-c3ee-4283-9a84-c0bf626dc604",
|
||||
"prevIds": ["00924d88-1842-4d71-ac74-5682ddc47e1c"],
|
||||
"id": "00924d88-1842-4d71-ac74-5682ddc47e1c",
|
||||
"prevIds": ["15060ec5-05f7-4b86-b2a5-9108609432b3"],
|
||||
"ddl": [
|
||||
{
|
||||
"name": "account_state",
|
||||
@@ -56,10 +56,6 @@
|
||||
"name": "instruction_state",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_inbox",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_message",
|
||||
"entityType": "tables"
|
||||
@@ -846,76 +842,6 @@
|
||||
"entityType": "columns",
|
||||
"table": "instruction_state"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "type",
|
||||
"entityType": "columns",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "payload",
|
||||
"entityType": "columns",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "delivery",
|
||||
"entityType": "columns",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "enqueued_seq",
|
||||
"entityType": "columns",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
@@ -1502,17 +1428,6 @@
|
||||
"entityType": "fks",
|
||||
"table": "instruction_state"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session_v2",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_inbox_session_id_session_v2_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session_v2",
|
||||
@@ -1637,13 +1552,6 @@
|
||||
"table": "instruction_state",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "session_inbox_pk",
|
||||
"table": "session_inbox",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
@@ -1734,46 +1642,6 @@
|
||||
"entityType": "indexes",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "delivery",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "enqueued_seq",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_inbox_session_delivery_seq_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "enqueued_seq",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_inbox_session_enqueued_seq_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
|
||||
+30
-196
@@ -7,7 +7,6 @@ import { and, asc, eq, gt, lte, sql } from "drizzle-orm"
|
||||
import { Database } from "./database/database.js"
|
||||
import { EventSequenceTable, EventTable } from "./event/sql.js"
|
||||
import { Location } from "./location.js"
|
||||
import { KeyedMutex } from "./effect/keyed-mutex.js"
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { isDeepStrictEqual } from "node:util"
|
||||
import { Durable } from "@opencode-ai/schema/durable-event-manifest"
|
||||
@@ -93,16 +92,6 @@ export interface PublishOptions {
|
||||
readonly commit?: (seq: number) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export type PublishInput<D extends Event.DurableDefinition = Event.DurableDefinition> = readonly [
|
||||
definition: D,
|
||||
data: Event.Data<D>,
|
||||
options?: PublishOptions,
|
||||
]
|
||||
|
||||
export type PublishResult<I extends readonly PublishInput[]> = {
|
||||
readonly [K in keyof I]: I[K] extends PublishInput<infer D> ? Event.Payload<D> : never
|
||||
}
|
||||
|
||||
/** Marker/event union emitted by `log`. */
|
||||
export type LogItem = Event.Payload | EventLog.Synced
|
||||
|
||||
@@ -135,9 +124,6 @@ export interface Interface {
|
||||
data: Event.Data<D>,
|
||||
options?: PublishOptions,
|
||||
) => Effect.Effect<Event.Payload<D>>
|
||||
readonly publishAll: <const I extends readonly [PublishInput, ...PublishInput[]]>(
|
||||
events: I,
|
||||
) => Effect.Effect<PublishResult<I>>
|
||||
readonly subscribe: Subscribe
|
||||
/**
|
||||
* Durable, ordered per-aggregate log read. Forked aggregates may reserve an
|
||||
@@ -190,7 +176,6 @@ export function configured(options?: Options) {
|
||||
}
|
||||
const projectors = new Map<string, Subscriber[]>()
|
||||
const listeners = new Array<Subscriber>()
|
||||
const durableLocks = KeyedMutex.makeUnsafe<string>()
|
||||
const { db } = yield* Database.Service
|
||||
const logReadPageSize = options?.logReadPageSize ?? 512
|
||||
const persist = options?.persist ?? false
|
||||
@@ -400,23 +385,15 @@ export function configured(options?: Options) {
|
||||
}),
|
||||
)
|
||||
if (definition?.durable) {
|
||||
const aggregateID = (event.data as Record<string, unknown>)[definition.durable.aggregate]
|
||||
if (typeof aggregateID !== "string")
|
||||
return yield* commitDurableEvent(definition, event as Event.Payload, undefined, commit).pipe(
|
||||
Effect.as(event),
|
||||
)
|
||||
return yield* durableLocks.withLock(aggregateID)(
|
||||
Effect.gen(function* () {
|
||||
const committed = yield* commitDurableEvent(definition, event as Event.Payload, undefined, commit)
|
||||
if (!committed) return event
|
||||
event = {
|
||||
...event,
|
||||
durable: envelope(committed.aggregateID, committed.seq, definition.durable.version),
|
||||
}
|
||||
yield* notify(event as Event.Payload, true)
|
||||
return event
|
||||
}),
|
||||
)
|
||||
const committed = yield* commitDurableEvent(definition, event as Event.Payload, undefined, commit)
|
||||
if (committed) {
|
||||
event = {
|
||||
...event,
|
||||
durable: envelope(committed.aggregateID, committed.seq, definition.durable.version),
|
||||
}
|
||||
yield* notify(event as Event.Payload, true)
|
||||
return event
|
||||
}
|
||||
}
|
||||
yield* notify(event as Event.Payload, false)
|
||||
return event
|
||||
@@ -467,144 +444,6 @@ export function configured(options?: Options) {
|
||||
})
|
||||
}
|
||||
|
||||
function publishAll<const I extends readonly [PublishInput, ...PublishInput[]]>(events: I) {
|
||||
return Effect.gen(function* () {
|
||||
const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
|
||||
const payloads = yield* Effect.forEach(events, ([definition, data, options]) =>
|
||||
Effect.gen(function* () {
|
||||
const aggregateID = (data as Record<string, unknown>)[definition.durable.aggregate]
|
||||
if (typeof aggregateID !== "string") {
|
||||
return yield* Effect.die(
|
||||
new InvalidDurableEventError({
|
||||
type: definition.type,
|
||||
message: `Expected string aggregate field ${definition.durable.aggregate}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
const location =
|
||||
options?.location ??
|
||||
(serviceLocation
|
||||
? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
|
||||
: undefined)
|
||||
return {
|
||||
definition,
|
||||
aggregateID,
|
||||
commit: options?.commit,
|
||||
event: {
|
||||
id: options?.id ?? Event.ID.create(),
|
||||
created: yield* DateTime.now,
|
||||
...(options?.metadata ? { metadata: options.metadata } : {}),
|
||||
type: definition.type,
|
||||
...(location ? { location } : {}),
|
||||
data,
|
||||
} as Event.Payload,
|
||||
}
|
||||
}),
|
||||
)
|
||||
const aggregateID = payloads[0].aggregateID
|
||||
if (payloads.some((item) => item.aggregateID !== aggregateID)) {
|
||||
return yield* Effect.die(
|
||||
new InvalidDurableEventError({
|
||||
type: payloads[0].definition.type,
|
||||
message: "Published events must belong to the same aggregate",
|
||||
}),
|
||||
)
|
||||
}
|
||||
return yield* durableLocks.withLock(aggregateID)(
|
||||
Effect.uninterruptible(
|
||||
Effect.gen(function* () {
|
||||
const committed = yield* db
|
||||
.transaction(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const row = yield* db
|
||||
.select({ seq: EventSequenceTable.seq })
|
||||
.from(EventSequenceTable)
|
||||
.where(eq(EventSequenceTable.aggregate_id, aggregateID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
const firstSeq = (row?.seq ?? -1) + 1
|
||||
const finalSeq = firstSeq + payloads.length - 1
|
||||
const result = new Array<Event.Payload>()
|
||||
const rows = new Array<typeof EventTable.$inferInsert>()
|
||||
const ids = new Set<Event.ID>()
|
||||
for (const [index, item] of payloads.entries()) {
|
||||
const seq = firstSeq + index
|
||||
const encoded = Schema.encodeUnknownSync(item.definition.data)(item.event.data) as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
if (persist) {
|
||||
if (ids.has(item.event.id))
|
||||
yield* Effect.die(
|
||||
new InvalidDurableEventError({
|
||||
type: item.event.type,
|
||||
message: `Event ${item.event.id} appears more than once in the batch`,
|
||||
}),
|
||||
)
|
||||
ids.add(item.event.id)
|
||||
const stored = yield* db
|
||||
.select({ aggregateID: EventTable.aggregate_id, seq: EventTable.seq })
|
||||
.from(EventTable)
|
||||
.where(eq(EventTable.id, item.event.id))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (stored)
|
||||
yield* Effect.die(
|
||||
new InvalidDurableEventError({
|
||||
type: item.event.type,
|
||||
message: `Event ${item.event.id} already exists at aggregate ${stored.aggregateID} sequence ${stored.seq}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
const event = {
|
||||
...item.event,
|
||||
durable: envelope(aggregateID, seq, item.definition.durable.version),
|
||||
} as Event.Payload
|
||||
for (const projector of projectors.get(
|
||||
versionedType(item.definition.type, item.definition.durable.version),
|
||||
) ?? []) {
|
||||
yield* projector(event)
|
||||
}
|
||||
if (item.commit) yield* item.commit(seq)
|
||||
if (persist)
|
||||
rows.push({
|
||||
id: event.id,
|
||||
aggregate_id: aggregateID,
|
||||
seq,
|
||||
created: DateTime.toEpochMillis(event.created),
|
||||
type: versionedType(item.definition.type, item.definition.durable.version),
|
||||
data: encoded,
|
||||
})
|
||||
result.push(event)
|
||||
}
|
||||
yield* db
|
||||
.insert(EventSequenceTable)
|
||||
.values([{ aggregate_id: aggregateID, seq: finalSeq }])
|
||||
.onConflictDoUpdate({ target: EventSequenceTable.aggregate_id, set: { seq: finalSeq } })
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
if (persist) yield* db.insert(EventTable).values(rows).run().pipe(Effect.orDie)
|
||||
return result
|
||||
}),
|
||||
{ behavior: "immediate" },
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
yield* Effect.forEach(
|
||||
pubsub.durable.get(aggregateID) ?? [],
|
||||
(wake) => PubSub.publish(wake, undefined),
|
||||
{
|
||||
discard: true,
|
||||
},
|
||||
)
|
||||
yield* Effect.forEach(committed, (event) => notify(event, true), { discard: true })
|
||||
return committed as PublishResult<I>
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function replay(
|
||||
event: SerializedEvent,
|
||||
options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
|
||||
@@ -616,31 +455,27 @@ export function configured(options?: Options) {
|
||||
new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }),
|
||||
)
|
||||
} else {
|
||||
yield* durableLocks.withLock(event.aggregateID)(
|
||||
Effect.gen(function* () {
|
||||
const payload = {
|
||||
id: event.id,
|
||||
created: event.created ?? DateTime.makeUnsafe(0),
|
||||
type: definition.type,
|
||||
data: Schema.decodeUnknownSync(definition.data)(event.data),
|
||||
} as Event.Payload
|
||||
const committed = yield* commitDurableEvent(definition, payload, {
|
||||
seq: event.seq,
|
||||
aggregateID: event.aggregateID,
|
||||
ownerID: options?.ownerID,
|
||||
strictOwner: options?.strictOwner,
|
||||
})
|
||||
if (committed && options?.publish) {
|
||||
yield* notify(
|
||||
{
|
||||
...payload,
|
||||
durable: envelope(committed.aggregateID, committed.seq, definition.durable.version),
|
||||
},
|
||||
true,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
const payload = {
|
||||
id: event.id,
|
||||
created: event.created ?? DateTime.makeUnsafe(0),
|
||||
type: definition.type,
|
||||
data: Schema.decodeUnknownSync(definition.data)(event.data),
|
||||
} as Event.Payload
|
||||
const committed = yield* commitDurableEvent(definition, payload, {
|
||||
seq: event.seq,
|
||||
aggregateID: event.aggregateID,
|
||||
ownerID: options?.ownerID,
|
||||
strictOwner: options?.strictOwner,
|
||||
})
|
||||
if (committed && options?.publish) {
|
||||
yield* notify(
|
||||
{
|
||||
...payload,
|
||||
durable: envelope(committed.aggregateID, committed.seq, definition.durable.version),
|
||||
},
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -857,7 +692,6 @@ export function configured(options?: Options) {
|
||||
|
||||
return Service.of({
|
||||
publish,
|
||||
publishAll,
|
||||
subscribe,
|
||||
log,
|
||||
listen,
|
||||
|
||||
-2
@@ -41,7 +41,6 @@ import m38 from "./migration/20260804233008_loose_psylocke.js"
|
||||
import m39 from "./migration/20260805200742_import_legacy_credentials.js"
|
||||
import m40 from "./migration/20260808023530_workspace_domain.js"
|
||||
import m41 from "./migration/20260811161259_execution_claim_attempts.js"
|
||||
import m42 from "./migration/20260812181746_session_inbox.js"
|
||||
|
||||
export const migrations = [
|
||||
m00,
|
||||
@@ -86,5 +85,4 @@ export const migrations = [
|
||||
m39,
|
||||
m40,
|
||||
m41,
|
||||
m42,
|
||||
] satisfies DatabaseMigration.Migration[]
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration.js"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260812181746_session_inbox",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_inbox\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`session_id\` text NOT NULL,
|
||||
\`type\` text NOT NULL,
|
||||
\`payload\` text NOT NULL,
|
||||
\`delivery\` text NOT NULL,
|
||||
\`enqueued_seq\` integer NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
CONSTRAINT \`fk_session_inbox_session_id_session_v2_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session_v2\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(
|
||||
`CREATE INDEX \`session_inbox_session_delivery_seq_idx\` ON \`session_inbox\` (\`session_id\`,\`delivery\`,\`enqueued_seq\`);`,
|
||||
)
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`session_inbox_session_enqueued_seq_idx\` ON \`session_inbox\` (\`session_id\`,\`enqueued_seq\`);`,
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
@@ -142,18 +142,6 @@ const schema: Omit<DatabaseMigration.Migration, "id"> = {
|
||||
CONSTRAINT \`fk_instruction_state_session_id_session_v2_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session_v2\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_inbox\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`session_id\` text NOT NULL,
|
||||
\`type\` text NOT NULL,
|
||||
\`payload\` text NOT NULL,
|
||||
\`delivery\` text NOT NULL,
|
||||
\`enqueued_seq\` integer NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
CONSTRAINT \`fk_session_inbox_session_id_session_v2_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session_v2\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_message\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
@@ -230,12 +218,6 @@ const schema: Omit<DatabaseMigration.Migration, "id"> = {
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`,
|
||||
)
|
||||
yield* tx.run(
|
||||
`CREATE INDEX \`session_inbox_session_delivery_seq_idx\` ON \`session_inbox\` (\`session_id\`,\`delivery\`,\`enqueued_seq\`);`,
|
||||
)
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`session_inbox_session_enqueued_seq_idx\` ON \`session_inbox\` (\`session_id\`,\`enqueued_seq\`);`,
|
||||
)
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`,
|
||||
)
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Effect, Layer, PlatformError } from "effect"
|
||||
import { ChildProcessSpawner, make } from "effect/unstable/process/ChildProcessSpawner"
|
||||
|
||||
export const spawner = make(() =>
|
||||
Effect.fail(
|
||||
PlatformError.systemError({
|
||||
_tag: "Unknown",
|
||||
module: "Environment",
|
||||
method: "spawn",
|
||||
description: "This location has no execution plane: no workspace is attached and the host cannot spawn processes",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
export const layer = Layer.succeed(ChildProcessSpawner, spawner)
|
||||
|
||||
export * as EnvironmentUnavailable from "./unavailable.js"
|
||||
@@ -165,6 +165,8 @@ export const Options = Schema.Struct({
|
||||
version: Schema.String,
|
||||
}),
|
||||
),
|
||||
/** Set false on runtimes that cannot spawn child processes; local (stdio) servers report failed instead of connecting. */
|
||||
stdio: Schema.optional(Schema.Boolean),
|
||||
})
|
||||
export type Options = typeof Options.Type
|
||||
|
||||
@@ -498,6 +500,11 @@ export const layer = (options?: Options) =>
|
||||
|
||||
const startServer = (name: ServerName, entry: ServerEntry) =>
|
||||
Effect.gen(function* () {
|
||||
if (options?.stdio === false && entry.config.type === "local") {
|
||||
entry.status = { status: "failed", error: "stdio MCP servers are unavailable in this runtime" }
|
||||
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
|
||||
return
|
||||
}
|
||||
// Announce the handshake so connect() and credential reconnects don't show a stale
|
||||
// disabled/failed status for the duration of the connection attempt.
|
||||
entry.status = { status: "pending" }
|
||||
|
||||
@@ -204,11 +204,10 @@ export const GithubCopilotPlugin = define({
|
||||
for (const [id, model] of loaded.models) {
|
||||
evt.model.update(item.provider.id, id, (draft) => Object.assign(draft, structuredClone(model)))
|
||||
}
|
||||
} else {
|
||||
} else if (loaded.baseURL) {
|
||||
for (const id of item.models.keys()) {
|
||||
evt.model.update(item.provider.id, id, (model) => {
|
||||
model.package = "@ai-sdk/github-copilot"
|
||||
if (loaded.baseURL) model.settings = Provider.mergeOverlay(model.settings, { baseURL: loaded.baseURL })
|
||||
model.settings = Provider.mergeOverlay(model.settings, { baseURL: loaded.baseURL })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import { ForkEmptyError, MessageDecodeError, NotFoundError } from "./session/err
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { LocationServiceMap } from "./location-service-map.js"
|
||||
import { SessionEvent } from "./session/event.js"
|
||||
import { SessionInbox } from "./session/inbox.js"
|
||||
import { SessionPending } from "./session/pending.js"
|
||||
import { InstructionState } from "./session/instruction-state.js"
|
||||
import { SessionGenerate } from "./session/generate.js"
|
||||
import { Snapshot } from "./snapshot.js"
|
||||
@@ -99,7 +99,6 @@ type CreateInput = CreateBaseInput &
|
||||
type CompactInput = {
|
||||
id?: SessionMessage.ID
|
||||
sessionID: SessionSchema.ID
|
||||
delivery?: SessionInbox.Delivery
|
||||
}
|
||||
|
||||
type ForkInput = {
|
||||
@@ -134,11 +133,14 @@ export class CompactionConflictError extends Schema.TaggedErrorClass<CompactionC
|
||||
export class BusyError extends Schema.TaggedErrorClass<BusyError>()("Session.BusyError", {
|
||||
sessionID: SessionSchema.ID,
|
||||
}) {}
|
||||
export class InboxConflictError extends Schema.TaggedErrorClass<InboxConflictError>()("Session.InboxConflictError", {
|
||||
sessionID: SessionSchema.ID,
|
||||
inboxID: SessionMessage.ID,
|
||||
}) {}
|
||||
type InboxItemRef = { readonly sessionID: SessionSchema.ID; readonly inboxID: SessionMessage.ID }
|
||||
export class PendingInputConflictError extends Schema.TaggedErrorClass<PendingInputConflictError>()(
|
||||
"Session.PendingInputConflictError",
|
||||
{
|
||||
sessionID: SessionSchema.ID,
|
||||
inputID: SessionMessage.ID,
|
||||
},
|
||||
) {}
|
||||
type PendingInputRef = { readonly sessionID: SessionSchema.ID; readonly inputID: SessionMessage.ID }
|
||||
export class SkillNotFoundError extends Schema.TaggedErrorClass<SkillNotFoundError>()("Session.SkillNotFoundError", {
|
||||
skill: Skill.ID,
|
||||
}) {}
|
||||
@@ -186,10 +188,10 @@ export interface Interface {
|
||||
* ordered by admission. Includes unpromoted user and synthetic inputs and
|
||||
* unhandled compaction barriers.
|
||||
*/
|
||||
readonly inbox: (sessionID: SessionSchema.ID) => Effect.Effect<SessionInbox.Info[], NotFoundError>
|
||||
readonly cancelInbox: (input: InboxItemRef) => Effect.Effect<void, NotFoundError | InboxConflictError>
|
||||
readonly steerInbox: (input: InboxItemRef) => Effect.Effect<void, NotFoundError | InboxConflictError>
|
||||
readonly queueInbox: (input: InboxItemRef) => Effect.Effect<void, NotFoundError | InboxConflictError>
|
||||
readonly pending: (sessionID: SessionSchema.ID) => Effect.Effect<SessionPending.Info[], NotFoundError>
|
||||
readonly cancelPending: (input: PendingInputRef) => Effect.Effect<void, NotFoundError | PendingInputConflictError>
|
||||
readonly steerPending: (input: PendingInputRef) => Effect.Effect<void, NotFoundError | PendingInputConflictError>
|
||||
readonly queuePending: (input: PendingInputRef) => Effect.Effect<void, NotFoundError | PendingInputConflictError>
|
||||
/**
|
||||
* Durable, ordered session log read. Replays durable session bus after
|
||||
* the exclusive `after` cursor, emits a `Synced` marker at the captured
|
||||
@@ -209,7 +211,6 @@ export interface Interface {
|
||||
sessionID: SessionSchema.ID
|
||||
directory: AbsolutePath
|
||||
workspaceID?: Location.Ref["workspaceID"]
|
||||
delivery?: SessionInbox.Delivery
|
||||
}) => Effect.Effect<void, NotFoundError | DestinationNotFoundError | DestinationNotDirectoryError>
|
||||
readonly prompt: (input: {
|
||||
id?: SessionMessage.ID
|
||||
@@ -219,9 +220,9 @@ export interface Interface {
|
||||
agents?: PromptInput.Prompt["agents"]
|
||||
skills?: PromptInput.Prompt["skills"]
|
||||
metadata?: Record<string, unknown>
|
||||
delivery?: SessionInbox.Delivery
|
||||
delivery?: SessionPending.Delivery
|
||||
resume?: boolean
|
||||
}) => Effect.Effect<SessionInbox.User, NotFoundError | PromptConflictError | AttachmentError | SkillNotFoundError>
|
||||
}) => Effect.Effect<SessionPending.User, NotFoundError | PromptConflictError | AttachmentError | SkillNotFoundError>
|
||||
/** Generates text from current Session context without admitting input or mutating history. */
|
||||
readonly generate: (input: {
|
||||
sessionID: SessionSchema.ID
|
||||
@@ -237,10 +238,10 @@ export interface Interface {
|
||||
files?: PromptInput.Prompt["files"]
|
||||
agents?: PromptInput.Prompt["agents"]
|
||||
skills?: PromptInput.Prompt["skills"]
|
||||
delivery?: SessionInbox.Delivery
|
||||
delivery?: SessionPending.Delivery
|
||||
resume?: boolean
|
||||
}) => Effect.Effect<
|
||||
SessionInbox.User,
|
||||
SessionPending.User,
|
||||
| NotFoundError
|
||||
| PromptConflictError
|
||||
| AttachmentError
|
||||
@@ -261,7 +262,7 @@ export interface Interface {
|
||||
}) => Effect.Effect<void, NotFoundError | SkillNotFoundError>
|
||||
readonly compact: (
|
||||
input: CompactInput,
|
||||
) => Effect.Effect<SessionInbox.Compaction, NotFoundError | CompactionConflictError>
|
||||
) => Effect.Effect<SessionPending.Compaction, NotFoundError | CompactionConflictError>
|
||||
readonly wait: (id: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
|
||||
readonly active: Effect.Effect<ReadonlySet<SessionSchema.ID>>
|
||||
readonly background: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
|
||||
@@ -273,9 +274,9 @@ export interface Interface {
|
||||
text: string
|
||||
description?: string
|
||||
metadata?: Record<string, unknown>
|
||||
delivery?: SessionInbox.Delivery
|
||||
delivery?: SessionPending.Delivery
|
||||
resume?: boolean
|
||||
}) => Effect.Effect<SessionInbox.Synthetic, NotFoundError | SyntheticConflictError>
|
||||
}) => Effect.Effect<SessionPending.Synthetic, NotFoundError | SyntheticConflictError>
|
||||
readonly revert: {
|
||||
readonly stage: (input: {
|
||||
sessionID: SessionSchema.ID
|
||||
@@ -320,12 +321,12 @@ const layer = Layer.effect(
|
||||
),
|
||||
)
|
||||
|
||||
const pendingConflict = Effect.fn("Session.pendingConflict")(function* (input: InboxItemRef) {
|
||||
const pendingConflict = Effect.fn("Session.pendingConflict")(function* (input: PendingInputRef) {
|
||||
yield* result.get(input.sessionID)
|
||||
return yield* new InboxConflictError(input)
|
||||
return yield* new PendingInputConflictError(input)
|
||||
})
|
||||
const mutatePending = (
|
||||
input: InboxItemRef,
|
||||
input: PendingInputRef,
|
||||
mutation: (
|
||||
bus: Bus.Interface,
|
||||
input: { readonly id: SessionMessage.ID; readonly sessionID: SessionSchema.ID },
|
||||
@@ -334,9 +335,9 @@ const layer = Layer.effect(
|
||||
) =>
|
||||
Effect.uninterruptible(
|
||||
Effect.gen(function* () {
|
||||
yield* mutation(bus, { sessionID: input.sessionID, id: input.inboxID }).pipe(
|
||||
yield* mutation(bus, { sessionID: input.sessionID, id: input.inputID }).pipe(
|
||||
Effect.catchDefect((defect) =>
|
||||
defect instanceof SessionInbox.LifecycleConflict ? pendingConflict(input) : Effect.die(defect),
|
||||
defect instanceof SessionPending.LifecycleConflict ? pendingConflict(input) : Effect.die(defect),
|
||||
),
|
||||
)
|
||||
if (wake) yield* execution.wake(input.sessionID)
|
||||
@@ -528,13 +529,13 @@ const layer = Layer.effect(
|
||||
yield* result.get(sessionID)
|
||||
return yield* store.context(sessionID)
|
||||
}),
|
||||
inbox: Effect.fn("Session.inbox")(function* (sessionID) {
|
||||
pending: Effect.fn("Session.pending")(function* (sessionID) {
|
||||
yield* result.get(sessionID)
|
||||
return yield* SessionInbox.list(db, sessionID)
|
||||
return yield* SessionPending.list(db, sessionID)
|
||||
}),
|
||||
cancelInbox: Effect.fn("Session.cancelInbox")((input) => mutatePending(input, SessionInbox.cancel)),
|
||||
steerInbox: Effect.fn("Session.steerInbox")((input) => mutatePending(input, SessionInbox.steer, true)),
|
||||
queueInbox: Effect.fn("Session.queueInbox")((input) => mutatePending(input, SessionInbox.queue)),
|
||||
cancelPending: Effect.fn("Session.cancelPending")((input) => mutatePending(input, SessionPending.cancel)),
|
||||
steerPending: Effect.fn("Session.steerPending")((input) => mutatePending(input, SessionPending.steer, true)),
|
||||
queuePending: Effect.fn("Session.queuePending")((input) => mutatePending(input, SessionPending.queue)),
|
||||
log: (input) =>
|
||||
Stream.unwrap(
|
||||
result
|
||||
@@ -563,25 +564,25 @@ const layer = Layer.effect(
|
||||
skills,
|
||||
).pipe(Effect.provideService(FSUtil.Service, fs))
|
||||
const messageID = input.id ?? SessionMessage.ID.create()
|
||||
const admittedInput = SessionInbox.Item.make({
|
||||
const admittedInput = SessionPending.Message.make({
|
||||
type: "user",
|
||||
payload: { ...prompt, metadata: input.metadata },
|
||||
data: { ...prompt, metadata: input.metadata },
|
||||
delivery: input.delivery ?? "steer",
|
||||
})
|
||||
const admitted = yield* SessionInbox.admit(db, bus, {
|
||||
const admitted = yield* SessionPending.admit(db, bus, {
|
||||
id: messageID,
|
||||
sessionID: input.sessionID,
|
||||
item: admittedInput,
|
||||
input: admittedInput,
|
||||
}).pipe(
|
||||
Effect.catchDefect((defect) =>
|
||||
defect instanceof SessionInbox.LifecycleConflict
|
||||
defect instanceof SessionPending.LifecycleConflict
|
||||
? new PromptConflictError({ sessionID: input.sessionID, messageID })
|
||||
: Effect.die(defect),
|
||||
),
|
||||
)
|
||||
if (
|
||||
admitted.type !== "user" ||
|
||||
!SessionInbox.equivalent(admitted, { sessionID: input.sessionID, item: admittedInput })
|
||||
!SessionPending.equivalent(admitted, { sessionID: input.sessionID, input: admittedInput })
|
||||
)
|
||||
return yield* new PromptConflictError({ sessionID: input.sessionID, messageID })
|
||||
if (input.resume !== false) {
|
||||
@@ -639,9 +640,7 @@ const layer = Layer.effect(
|
||||
yield* execution.awaitIdle(input.sessionID)
|
||||
const started = yield* Effect.gen(function* () {
|
||||
const shell = yield* Shell.Service
|
||||
return yield* shell
|
||||
.create({ command: input.command, cwd: session.location.directory, timeout: 0 })
|
||||
.pipe(Effect.orDie)
|
||||
return yield* shell.create({ command: input.command, cwd: session.location.directory, timeout: 0 })
|
||||
}).pipe(Effect.provide(locations.get(session.location)))
|
||||
yield* bus.publish(
|
||||
SessionEvent.Shell.Started,
|
||||
@@ -739,35 +738,33 @@ const layer = Layer.effect(
|
||||
const info = yield* fs.stat(directory).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
if (!info) return yield* new DestinationNotFoundError({ directory })
|
||||
if (info.type !== "Directory") return yield* new DestinationNotDirectoryError({ directory })
|
||||
if (current.location.directory === directory && current.location.workspaceID === input.workspaceID) return
|
||||
const project = yield* projects.resolve(directory)
|
||||
yield* persistProject(project)
|
||||
const item = SessionInbox.Item.make({
|
||||
type: "move",
|
||||
payload: {
|
||||
if ((yield* execution.active).has(input.sessionID)) {
|
||||
yield* execution.interrupt(input.sessionID)
|
||||
yield* execution.awaitIdle(input.sessionID)
|
||||
}
|
||||
yield* bus.publish(
|
||||
SessionEvent.Moved,
|
||||
{
|
||||
sessionID: input.sessionID,
|
||||
location: Location.Ref.make({ directory, workspaceID: input.workspaceID }),
|
||||
projectID: project.id,
|
||||
subpath: RelativePath.make(path.relative(project.directory, directory).replaceAll("\\", "/")),
|
||||
},
|
||||
delivery: input.delivery ?? "queue",
|
||||
})
|
||||
const inboxID = SessionMessage.ID.create()
|
||||
yield* SessionInbox.admit(db, bus, {
|
||||
id: inboxID,
|
||||
sessionID: input.sessionID,
|
||||
item,
|
||||
})
|
||||
yield* execution.wake(input.sessionID)
|
||||
{ location: current.location },
|
||||
)
|
||||
}),
|
||||
compact: Effect.fn("Session.compact")(function* (input) {
|
||||
yield* result.get(input.sessionID)
|
||||
const inputID = input.id ?? SessionMessage.ID.create()
|
||||
const admitted = yield* SessionInbox.admitCompaction(db, bus, {
|
||||
const admitted = yield* SessionPending.admitCompaction(db, bus, {
|
||||
id: inputID,
|
||||
sessionID: input.sessionID,
|
||||
delivery: input.delivery ?? "queue",
|
||||
}).pipe(
|
||||
Effect.catchDefect((defect) =>
|
||||
defect instanceof SessionInbox.LifecycleConflict
|
||||
defect instanceof SessionPending.LifecycleConflict
|
||||
? new CompactionConflictError({ sessionID: input.sessionID, inputID })
|
||||
: Effect.die(defect),
|
||||
),
|
||||
@@ -807,29 +804,29 @@ const layer = Layer.effect(
|
||||
Effect.gen(function* () {
|
||||
yield* result.get(input.sessionID)
|
||||
const inputID = input.id ?? SessionMessage.ID.create()
|
||||
const admittedInput = SessionInbox.Item.make({
|
||||
const admittedInput = SessionPending.Message.make({
|
||||
type: "synthetic",
|
||||
payload: {
|
||||
data: {
|
||||
text: input.text,
|
||||
description: input.description,
|
||||
metadata: input.metadata,
|
||||
},
|
||||
delivery: input.delivery ?? "steer",
|
||||
})
|
||||
const admitted = yield* SessionInbox.admit(db, bus, {
|
||||
const admitted = yield* SessionPending.admit(db, bus, {
|
||||
id: inputID,
|
||||
sessionID: input.sessionID,
|
||||
item: admittedInput,
|
||||
input: admittedInput,
|
||||
}).pipe(
|
||||
Effect.catchDefect((defect) =>
|
||||
defect instanceof SessionInbox.LifecycleConflict
|
||||
defect instanceof SessionPending.LifecycleConflict
|
||||
? new SyntheticConflictError({ sessionID: input.sessionID, inputID })
|
||||
: Effect.die(defect),
|
||||
),
|
||||
)
|
||||
if (
|
||||
admitted.type !== "synthetic" ||
|
||||
!SessionInbox.equivalent(admitted, { sessionID: input.sessionID, item: admittedInput })
|
||||
!SessionPending.equivalent(admitted, { sessionID: input.sessionID, input: admittedInput })
|
||||
)
|
||||
return yield* new SyntheticConflictError({ sessionID: input.sessionID, inputID })
|
||||
if (input.resume !== false && !(yield* result.get(input.sessionID)).revert)
|
||||
@@ -842,7 +839,7 @@ const layer = Layer.effect(
|
||||
Effect.uninterruptible(
|
||||
Effect.gen(function* () {
|
||||
yield* execution.interrupt(sessionID)
|
||||
if (options?.continue && (yield* SessionInbox.has(db, sessionID, "any"))) yield* execution.wake(sessionID)
|
||||
if (options?.continue && (yield* SessionPending.has(db, sessionID, "any"))) yield* execution.wake(sessionID)
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -89,7 +89,6 @@ export type ManualInput = {
|
||||
readonly session: SessionSchema.Info
|
||||
readonly messages: readonly SessionMessage.Info[]
|
||||
readonly inputID: SessionMessage.ID
|
||||
readonly started?: boolean
|
||||
}
|
||||
|
||||
type RequiredInput = Omit<AutoInput, "ref">
|
||||
@@ -103,7 +102,6 @@ type Plan = {
|
||||
readonly prompt: string
|
||||
readonly recent: string
|
||||
readonly inputID?: SessionMessage.ID
|
||||
readonly started?: boolean
|
||||
}
|
||||
|
||||
export type Outcome =
|
||||
@@ -250,13 +248,12 @@ const make = (dependencies: Dependencies) => {
|
||||
return { status: "failed" as const, error: input.error }
|
||||
})
|
||||
const execute = Effect.fn("SessionCompaction.execute")(function* (plan: Plan) {
|
||||
if (!plan.started)
|
||||
yield* dependencies.bus.publish(SessionEvent.Compaction.Started, {
|
||||
sessionID: plan.session.id,
|
||||
reason: plan.reason,
|
||||
recent: plan.recent,
|
||||
inputID: plan.inputID,
|
||||
})
|
||||
yield* dependencies.bus.publish(SessionEvent.Compaction.Started, {
|
||||
sessionID: plan.session.id,
|
||||
reason: plan.reason,
|
||||
recent: plan.recent,
|
||||
inputID: plan.inputID,
|
||||
})
|
||||
|
||||
const chunks: string[] = []
|
||||
let failure: SessionError.Error | undefined
|
||||
@@ -411,7 +408,6 @@ const make = (dependencies: Dependencies) => {
|
||||
cost: resolved.cost,
|
||||
reason: "manual",
|
||||
inputID: input.inputID,
|
||||
started: input.started,
|
||||
...content,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as SessionExecution from "./execution.js"
|
||||
|
||||
import { Cause, Context, Effect, Exit, Layer, Stream } from "effect"
|
||||
import { Cause, Context, Effect, Exit, Layer } from "effect"
|
||||
import { Bus } from "../bus.js"
|
||||
import { LocationServiceMap } from "../location-service-map.js"
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
@@ -116,10 +116,6 @@ export const layer = Layer.effect(
|
||||
}),
|
||||
),
|
||||
})
|
||||
yield* bus.subscribe(SessionEvent.Moved).pipe(
|
||||
Stream.runForEach((event) => coordinator.wake(event.data.sessionID)),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
|
||||
return Service.of({
|
||||
active: coordinator.active,
|
||||
|
||||
@@ -1,505 +0,0 @@
|
||||
export * as SessionInbox from "./inbox.js"
|
||||
|
||||
import { and, asc, eq, or } from "drizzle-orm"
|
||||
import { DateTime, Effect, Schema } from "effect"
|
||||
import {
|
||||
Compaction,
|
||||
CompactionPayload,
|
||||
Delivery,
|
||||
Info,
|
||||
Item,
|
||||
Move,
|
||||
MovePayload,
|
||||
Synthetic,
|
||||
SyntheticPayload,
|
||||
User,
|
||||
UserPayload,
|
||||
} from "@opencode-ai/schema/session-inbox"
|
||||
import type { Database } from "../database/database.js"
|
||||
import { Bus } from "../bus.js"
|
||||
import { KeyedMutex } from "../effect/keyed-mutex.js"
|
||||
import { SessionEvent } from "./event.js"
|
||||
import { SessionMessage } from "./message.js"
|
||||
import { SessionSchema } from "./schema.js"
|
||||
import { SessionInboxTable, SessionMessageTable } from "./sql.js"
|
||||
|
||||
type DatabaseService = Database.Interface["db"]
|
||||
|
||||
export {
|
||||
Compaction,
|
||||
CompactionPayload,
|
||||
Delivery,
|
||||
Info,
|
||||
Item,
|
||||
Move,
|
||||
MovePayload,
|
||||
Synthetic,
|
||||
SyntheticPayload,
|
||||
User,
|
||||
UserPayload,
|
||||
}
|
||||
|
||||
/**
|
||||
* Which pending input `promote` may consume: "steer" promotes steers only (a step
|
||||
* boundary mid-work), while "input" also allows one queued input when no steers are
|
||||
* waiting (the idle boundary, where the Session picks up fresh work).
|
||||
*/
|
||||
export type Promotable = "input" | "steer"
|
||||
|
||||
const decodeUser = Schema.decodeUnknownSync(UserPayload)
|
||||
const encodeUser = Schema.encodeSync(UserPayload)
|
||||
const decodeSynthetic = Schema.decodeUnknownSync(SyntheticPayload)
|
||||
const encodeSynthetic = Schema.encodeSync(SyntheticPayload)
|
||||
const decodeCompaction = Schema.decodeUnknownSync(CompactionPayload)
|
||||
const encodeCompaction = Schema.encodeSync(CompactionPayload)
|
||||
const decodeMove = Schema.decodeUnknownSync(MovePayload)
|
||||
const encodeMove = Schema.encodeSync(MovePayload)
|
||||
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Info)
|
||||
const inboxLocks = KeyedMutex.makeUnsafe<SessionSchema.ID>()
|
||||
type PendingRef = { readonly id: SessionMessage.ID; readonly sessionID: SessionSchema.ID }
|
||||
|
||||
export const serialized = <A, E, R>(sessionID: SessionSchema.ID, effect: Effect.Effect<A, E, R>) =>
|
||||
inboxLocks.withLock(sessionID)(effect)
|
||||
|
||||
export class LifecycleConflict extends Schema.TaggedErrorClass<LifecycleConflict>()("SessionInbox.LifecycleConflict", {
|
||||
id: SessionMessage.ID,
|
||||
}) {}
|
||||
|
||||
const fromRow = (row: typeof SessionInboxTable.$inferSelect): Info => {
|
||||
const base = {
|
||||
id: SessionMessage.ID.make(row.id),
|
||||
sessionID: SessionSchema.ID.make(row.session_id),
|
||||
timeCreated: DateTime.makeUnsafe(row.time_created),
|
||||
}
|
||||
if (row.type === "compaction")
|
||||
return Compaction.make({
|
||||
...base,
|
||||
type: "compaction",
|
||||
payload: decodeCompaction(row.payload),
|
||||
delivery: row.delivery,
|
||||
})
|
||||
if (row.type === "move")
|
||||
return Move.make({ ...base, type: "move", payload: decodeMove(row.payload), delivery: row.delivery })
|
||||
if (row.type === "user")
|
||||
return User.make({
|
||||
...base,
|
||||
type: "user",
|
||||
payload: decodeUser(row.payload),
|
||||
delivery: row.delivery,
|
||||
})
|
||||
if (row.type === "synthetic")
|
||||
return Synthetic.make({
|
||||
...base,
|
||||
type: "synthetic",
|
||||
payload: decodeSynthetic(row.payload),
|
||||
delivery: row.delivery,
|
||||
})
|
||||
throw new LifecycleConflict({ id: base.id })
|
||||
}
|
||||
|
||||
export const find = Effect.fn("SessionInbox.find")(function* (db: DatabaseService, id: SessionMessage.ID) {
|
||||
const row = yield* db.select().from(SessionInboxTable).where(eq(SessionInboxTable.id, id)).get().pipe(Effect.orDie)
|
||||
return row === undefined ? undefined : fromRow(row)
|
||||
})
|
||||
|
||||
const promotedFromMessage = Effect.fn("SessionInbox.promotedFromMessage")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
id: SessionMessage.ID,
|
||||
delivery: Delivery,
|
||||
) {
|
||||
const row = yield* db
|
||||
.select()
|
||||
.from(SessionMessageTable)
|
||||
.where(eq(SessionMessageTable.id, id))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (row === undefined) return undefined
|
||||
if (row.session_id !== sessionID || (row.type !== "user" && row.type !== "synthetic"))
|
||||
return yield* Effect.die(new LifecycleConflict({ id }))
|
||||
const message = decodeMessage({ ...row.data, id: row.id, type: row.type })
|
||||
const base = { id, sessionID, timeCreated: message.time.created, delivery }
|
||||
if (message.type === "user")
|
||||
return User.make({
|
||||
...base,
|
||||
type: "user",
|
||||
payload: decodeUser(message),
|
||||
})
|
||||
if (message.type === "synthetic")
|
||||
return Synthetic.make({
|
||||
...base,
|
||||
type: "synthetic",
|
||||
payload: decodeSynthetic(message),
|
||||
})
|
||||
return yield* Effect.die(new LifecycleConflict({ id }))
|
||||
})
|
||||
|
||||
export const admit = Effect.fn("SessionInbox.admit")(function* (
|
||||
db: DatabaseService,
|
||||
bus: Bus.Interface,
|
||||
request: {
|
||||
readonly id: SessionMessage.ID
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly item: Item
|
||||
},
|
||||
) {
|
||||
const existing = yield* find(db, request.id)
|
||||
if (existing !== undefined) {
|
||||
if (existing.type === "compaction") return yield* Effect.die(new LifecycleConflict({ id: request.id }))
|
||||
return existing
|
||||
}
|
||||
const promoted = yield* promotedFromMessage(db, request.sessionID, request.id, request.item.delivery)
|
||||
if (promoted !== undefined) return promoted
|
||||
return yield* bus
|
||||
.publish(SessionEvent.InboxEnqueued, {
|
||||
inboxID: request.id,
|
||||
sessionID: request.sessionID,
|
||||
item: request.item,
|
||||
})
|
||||
.pipe(
|
||||
Effect.flatMap((event) => {
|
||||
const base = {
|
||||
id: request.id,
|
||||
sessionID: request.sessionID,
|
||||
timeCreated: event.created,
|
||||
}
|
||||
return Effect.succeed(Info.make({ ...base, ...request.item }))
|
||||
}),
|
||||
Effect.catchDefect((defect) =>
|
||||
find(db, request.id).pipe(
|
||||
Effect.flatMap((stored) =>
|
||||
stored?.type === request.item.type ? Effect.succeed(stored) : Effect.die(defect),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
export const admitCompaction = Effect.fn("SessionInbox.admitCompaction")(function* (
|
||||
db: DatabaseService,
|
||||
bus: Bus.Interface,
|
||||
input: { readonly id: SessionMessage.ID; readonly sessionID: SessionSchema.ID; readonly delivery: Delivery },
|
||||
) {
|
||||
const admitted = yield* admit(db, bus, {
|
||||
id: input.id,
|
||||
sessionID: input.sessionID,
|
||||
item: Item.make({ type: "compaction", payload: {}, delivery: input.delivery }),
|
||||
})
|
||||
if (admitted.type === "compaction") return admitted
|
||||
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
})
|
||||
|
||||
export const projectAdmitted = Effect.fn("SessionInbox.projectAdmitted")(function* (
|
||||
db: DatabaseService,
|
||||
request: {
|
||||
readonly enqueuedSeq: number
|
||||
readonly id: SessionMessage.ID
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly item: Item
|
||||
readonly timeCreated: DateTime.Utc
|
||||
},
|
||||
) {
|
||||
const message = yield* db
|
||||
.select({ id: SessionMessageTable.id })
|
||||
.from(SessionMessageTable)
|
||||
.where(eq(SessionMessageTable.id, request.id))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (message !== undefined) return yield* Effect.die(new LifecycleConflict({ id: request.id }))
|
||||
const stored = yield* db
|
||||
.insert(SessionInboxTable)
|
||||
.values({
|
||||
id: request.id,
|
||||
session_id: request.sessionID,
|
||||
type: request.item.type,
|
||||
payload:
|
||||
request.item.type === "user"
|
||||
? encodeUser(request.item.payload)
|
||||
: request.item.type === "synthetic"
|
||||
? encodeSynthetic(request.item.payload)
|
||||
: request.item.type === "compaction"
|
||||
? encodeCompaction(request.item.payload)
|
||||
: encodeMove(request.item.payload),
|
||||
delivery: request.item.delivery,
|
||||
enqueued_seq: request.enqueuedSeq,
|
||||
time_created: DateTime.toEpochMillis(request.timeCreated),
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.returning({ id: SessionInboxTable.id })
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!stored) return yield* Effect.die(new LifecycleConflict({ id: request.id }))
|
||||
})
|
||||
|
||||
/**
|
||||
* Consume one pending row at promotion. The row's content feeds the projected
|
||||
* message insert inside the same event transaction; the deleted row is what
|
||||
* makes the table pending-only.
|
||||
*/
|
||||
export const projectDelivered = Effect.fn("SessionInbox.projectDelivered")(function* (
|
||||
db: DatabaseService,
|
||||
input: PendingRef,
|
||||
) {
|
||||
const deleted = yield* db
|
||||
.delete(SessionInboxTable)
|
||||
.where(and(eq(SessionInboxTable.id, input.id), eq(SessionInboxTable.session_id, input.sessionID)))
|
||||
.returning()
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!deleted) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
return fromRow(deleted)
|
||||
})
|
||||
|
||||
export const projectCancelled = Effect.fn("SessionInbox.projectCancelled")(function* (
|
||||
db: DatabaseService,
|
||||
input: PendingRef,
|
||||
) {
|
||||
const deleted = yield* db
|
||||
.delete(SessionInboxTable)
|
||||
.where(
|
||||
and(
|
||||
eq(SessionInboxTable.id, input.id),
|
||||
eq(SessionInboxTable.session_id, input.sessionID),
|
||||
or(eq(SessionInboxTable.delivery, "queue"), eq(SessionInboxTable.delivery, "steer")),
|
||||
),
|
||||
)
|
||||
.returning({ id: SessionInboxTable.id })
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!deleted) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
})
|
||||
|
||||
const projectDelivery = Effect.fn("SessionInbox.projectDelivery")(function* (
|
||||
db: DatabaseService,
|
||||
input: PendingRef & { readonly from: Delivery; readonly to: Delivery },
|
||||
) {
|
||||
const updated = yield* db
|
||||
.update(SessionInboxTable)
|
||||
.set({ delivery: input.to })
|
||||
.where(
|
||||
and(
|
||||
eq(SessionInboxTable.id, input.id),
|
||||
eq(SessionInboxTable.session_id, input.sessionID),
|
||||
eq(SessionInboxTable.delivery, input.from),
|
||||
),
|
||||
)
|
||||
.returning({ id: SessionInboxTable.id })
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!updated) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
})
|
||||
|
||||
export const projectDeliveryChanged = Effect.fn("SessionInbox.projectDeliveryChanged")(
|
||||
(db: DatabaseService, input: PendingRef & { readonly delivery: Delivery }) =>
|
||||
projectDelivery(db, {
|
||||
...input,
|
||||
from: input.delivery === "steer" ? "queue" : "steer",
|
||||
to: input.delivery,
|
||||
}),
|
||||
)
|
||||
|
||||
export const list = Effect.fn("SessionInbox.list")(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
|
||||
const rows = yield* db
|
||||
.select()
|
||||
.from(SessionInboxTable)
|
||||
.where(eq(SessionInboxTable.session_id, sessionID))
|
||||
.orderBy(asc(SessionInboxTable.enqueued_seq))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
return rows.map(fromRow)
|
||||
})
|
||||
|
||||
export const nextQueued = Effect.fn("SessionInbox.nextQueued")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
) {
|
||||
const row = yield* db
|
||||
.select()
|
||||
.from(SessionInboxTable)
|
||||
.where(and(eq(SessionInboxTable.session_id, sessionID), eq(SessionInboxTable.delivery, "queue")))
|
||||
.orderBy(asc(SessionInboxTable.enqueued_seq))
|
||||
.limit(1)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
return row ? fromRow(row) : undefined
|
||||
})
|
||||
|
||||
export const nextSteer = Effect.fn("SessionInbox.nextSteer")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
) {
|
||||
const row = yield* db
|
||||
.select()
|
||||
.from(SessionInboxTable)
|
||||
.where(and(eq(SessionInboxTable.session_id, sessionID), eq(SessionInboxTable.delivery, "steer")))
|
||||
.orderBy(asc(SessionInboxTable.enqueued_seq))
|
||||
.limit(1)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
return row ? fromRow(row) : undefined
|
||||
})
|
||||
|
||||
/**
|
||||
* Which pending rows count: "any" counts every row, while "input" means any
|
||||
* item in either delivery mode.
|
||||
*/
|
||||
export type Scope = "any" | "input" | Delivery
|
||||
|
||||
export const has = Effect.fn("SessionInbox.has")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
scope: Scope,
|
||||
) {
|
||||
const row = yield* db
|
||||
.select({ id: SessionInboxTable.id })
|
||||
.from(SessionInboxTable)
|
||||
.where(
|
||||
and(
|
||||
eq(SessionInboxTable.session_id, sessionID),
|
||||
scope === "any"
|
||||
? undefined
|
||||
: scope === "input"
|
||||
? or(eq(SessionInboxTable.delivery, "steer"), eq(SessionInboxTable.delivery, "queue"))
|
||||
: eq(SessionInboxTable.delivery, scope),
|
||||
),
|
||||
)
|
||||
.limit(1)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
return row !== undefined
|
||||
})
|
||||
|
||||
export const equivalent = (input: Info, expected: { readonly sessionID: SessionSchema.ID; readonly item: Item }) => {
|
||||
if (
|
||||
input.type !== expected.item.type ||
|
||||
input.delivery !== expected.item.delivery ||
|
||||
input.sessionID !== expected.sessionID
|
||||
)
|
||||
return false
|
||||
if (input.type === "user" && expected.item.type === "user")
|
||||
return JSON.stringify(encodeUser(input.payload)) === JSON.stringify(encodeUser(expected.item.payload))
|
||||
if (input.type === "synthetic" && expected.item.type === "synthetic")
|
||||
return JSON.stringify(encodeSynthetic(input.payload)) === JSON.stringify(encodeSynthetic(expected.item.payload))
|
||||
if (input.type === "compaction" && expected.item.type === "compaction") return true
|
||||
if (input.type === "move" && expected.item.type === "move")
|
||||
return JSON.stringify(encodeMove(input.payload)) === JSON.stringify(encodeMove(expected.item.payload))
|
||||
return false
|
||||
}
|
||||
|
||||
const publishMutation = <A, E, R>(input: PendingRef, effect: Effect.Effect<A, E, R>) =>
|
||||
serialized(input.sessionID, effect).pipe(Effect.asVoid)
|
||||
|
||||
export const cancel = Effect.fn("SessionInbox.cancel")((bus: Bus.Interface, input: PendingRef) =>
|
||||
publishMutation(
|
||||
input,
|
||||
bus.publish(SessionEvent.InboxCancelled, {
|
||||
sessionID: input.sessionID,
|
||||
inboxID: input.id,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
export const steer = Effect.fn("SessionInbox.steer")((bus: Bus.Interface, input: PendingRef) =>
|
||||
publishMutation(
|
||||
input,
|
||||
bus.publish(SessionEvent.InboxDeliveryChanged, {
|
||||
sessionID: input.sessionID,
|
||||
inboxID: input.id,
|
||||
delivery: "steer",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
export const queue = Effect.fn("SessionInbox.queue")((bus: Bus.Interface, input: PendingRef) =>
|
||||
publishMutation(
|
||||
input,
|
||||
bus.publish(SessionEvent.InboxDeliveryChanged, {
|
||||
sessionID: input.sessionID,
|
||||
inboxID: input.id,
|
||||
delivery: "queue",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const publish = Effect.fn("SessionInbox.publish")(function* (
|
||||
db: DatabaseService,
|
||||
bus: Bus.Interface,
|
||||
sessionID: SessionSchema.ID,
|
||||
rows: ReadonlyArray<typeof SessionInboxTable.$inferSelect>,
|
||||
) {
|
||||
yield* Effect.forEach(
|
||||
rows,
|
||||
(row) => {
|
||||
const entry = fromRow(row)
|
||||
if (entry.type === "compaction") return Effect.die(new LifecycleConflict({ id: entry.id }))
|
||||
return bus
|
||||
.publish(SessionEvent.InboxDelivered, {
|
||||
sessionID,
|
||||
inboxID: entry.id,
|
||||
})
|
||||
.pipe(
|
||||
Effect.catchDefect((defect) =>
|
||||
defect instanceof LifecycleConflict
|
||||
? promotedFromMessage(db, sessionID, entry.id, entry.delivery).pipe(
|
||||
Effect.flatMap((stored) => (stored !== undefined ? Effect.void : Effect.die(defect))),
|
||||
)
|
||||
: Effect.die(defect),
|
||||
),
|
||||
)
|
||||
},
|
||||
{ discard: true },
|
||||
)
|
||||
return rows.length
|
||||
})
|
||||
|
||||
/**
|
||||
* Promotes pending input into visible messages and returns the promoted count.
|
||||
* Steers always go first; only the "input" scope may fall through to one queued
|
||||
* input, and it then collects steers that arrived during promotion.
|
||||
*/
|
||||
export const promote = Effect.fn("SessionInbox.promote")(function* (
|
||||
db: DatabaseService,
|
||||
bus: Bus.Interface,
|
||||
sessionID: SessionSchema.ID,
|
||||
scope: Promotable,
|
||||
) {
|
||||
return yield* serialized(
|
||||
sessionID,
|
||||
Effect.gen(function* () {
|
||||
const steers = yield* db
|
||||
.select()
|
||||
.from(SessionInboxTable)
|
||||
.where(and(eq(SessionInboxTable.session_id, sessionID), eq(SessionInboxTable.delivery, "steer")))
|
||||
.orderBy(asc(SessionInboxTable.enqueued_seq))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
if (steers.length > 0 || scope === "steer") {
|
||||
const control = steers.findIndex((row) => row.type === "compaction" || row.type === "move")
|
||||
return yield* publish(db, bus, sessionID, control === -1 ? steers : steers.slice(0, control))
|
||||
}
|
||||
|
||||
const queued = yield* db
|
||||
.select()
|
||||
.from(SessionInboxTable)
|
||||
.where(and(eq(SessionInboxTable.session_id, sessionID), eq(SessionInboxTable.delivery, "queue")))
|
||||
.orderBy(asc(SessionInboxTable.enqueued_seq))
|
||||
.limit(1)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!queued) return 0
|
||||
const promoted = yield* publish(db, bus, sessionID, [queued])
|
||||
const arrivedSteers = yield* db
|
||||
.select()
|
||||
.from(SessionInboxTable)
|
||||
.where(and(eq(SessionInboxTable.session_id, sessionID), eq(SessionInboxTable.delivery, "steer")))
|
||||
.orderBy(asc(SessionInboxTable.enqueued_seq))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
const control = arrivedSteers.findIndex((row) => row.type === "compaction" || row.type === "move")
|
||||
return (
|
||||
promoted +
|
||||
(yield* publish(db, bus, sessionID, control === -1 ? arrivedSteers : arrivedSteers.slice(0, control)))
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -109,10 +109,11 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
"session.renamed": () => Effect.void,
|
||||
"session.deleted": () => Effect.void,
|
||||
"session.forked": () => Effect.void,
|
||||
"session.inbox.delivered": () => Effect.void,
|
||||
"session.inbox.enqueued": () => Effect.void,
|
||||
"session.inbox.cancelled": () => Effect.void,
|
||||
"session.inbox.delivery.changed": () => Effect.void,
|
||||
"session.input.promoted": () => Effect.void,
|
||||
"session.input.admitted": () => Effect.void,
|
||||
"session.input.cancelled": () => Effect.void,
|
||||
"session.input.steered": () => Effect.void,
|
||||
"session.input.queued": () => Effect.void,
|
||||
"session.execution.started": () => Effect.void,
|
||||
"session.execution.succeeded": () => clearCurrentRetry,
|
||||
"session.execution.failed": () => clearCurrentRetry,
|
||||
@@ -194,6 +195,8 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
draft.retry = undefined
|
||||
draft.error = undefined
|
||||
draft.finish = undefined
|
||||
draft.time.started = undefined
|
||||
draft.time.generated = undefined
|
||||
draft.time.completed = undefined
|
||||
if (event.data.snapshot) draft.snapshot = { ...draft.snapshot, start: event.data.snapshot }
|
||||
}),
|
||||
@@ -229,6 +232,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
draft.finish = event.data.finish
|
||||
draft.cost = event.data.cost
|
||||
draft.tokens = event.data.tokens
|
||||
draft.time.generated = event.data.generated
|
||||
if (event.data.snapshot || event.data.files)
|
||||
draft.snapshot = {
|
||||
...draft.snapshot,
|
||||
@@ -247,6 +251,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
draft.cost = event.data.cost
|
||||
draft.tokens = castDraft(event.data.tokens)
|
||||
}
|
||||
draft.time.generated = event.data.generated
|
||||
if (event.data.snapshot || event.data.files)
|
||||
draft.snapshot = {
|
||||
...draft.snapshot,
|
||||
@@ -257,6 +262,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
},
|
||||
"session.text.started": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
if (draft.time.completed === undefined) draft.time.started ??= event.created
|
||||
draft.content.push(castDraft(SessionMessage.AssistantText.make({ type: "text", text: "" })))
|
||||
})
|
||||
},
|
||||
@@ -379,6 +385,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
}
|
||||
})
|
||||
},
|
||||
"session.compaction.admitted": () => Effect.void,
|
||||
"session.compaction.started": (event) =>
|
||||
adapter.appendMessage(
|
||||
SessionMessage.CompactionRunning.make({
|
||||
|
||||
@@ -0,0 +1,545 @@
|
||||
export * as SessionPending from "./pending.js"
|
||||
|
||||
import { and, asc, eq, or } from "drizzle-orm"
|
||||
import { DateTime, Effect, Schema } from "effect"
|
||||
import {
|
||||
Compaction,
|
||||
Delivery,
|
||||
Info,
|
||||
Message,
|
||||
Synthetic,
|
||||
SyntheticData,
|
||||
User,
|
||||
UserData,
|
||||
} from "@opencode-ai/schema/session-pending"
|
||||
import type { Database } from "../database/database.js"
|
||||
import { Bus } from "../bus.js"
|
||||
import { KeyedMutex } from "../effect/keyed-mutex.js"
|
||||
import { SessionEvent } from "./event.js"
|
||||
import { SessionMessage } from "./message.js"
|
||||
import { SessionSchema } from "./schema.js"
|
||||
import { SessionMessageTable, SessionPendingTable } from "./sql.js"
|
||||
|
||||
type DatabaseService = Database.Interface["db"]
|
||||
|
||||
export { Compaction, Delivery, Info, Message, Synthetic, SyntheticData, User, UserData }
|
||||
|
||||
/**
|
||||
* Which pending input `promote` may consume: "steer" promotes steers only (a step
|
||||
* boundary mid-work), while "input" also allows one queued input when no steers are
|
||||
* waiting (the idle boundary, where the Session picks up fresh work).
|
||||
*/
|
||||
export type Promotable = "input" | "steer"
|
||||
|
||||
const decodeUser = Schema.decodeUnknownSync(UserData)
|
||||
const encodeUser = Schema.encodeSync(UserData)
|
||||
const decodeSynthetic = Schema.decodeUnknownSync(SyntheticData)
|
||||
const encodeSynthetic = Schema.encodeSync(SyntheticData)
|
||||
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Info)
|
||||
const inboxLocks = KeyedMutex.makeUnsafe<SessionSchema.ID>()
|
||||
type PendingRef = { readonly id: SessionMessage.ID; readonly sessionID: SessionSchema.ID }
|
||||
|
||||
export class LifecycleConflict extends Schema.TaggedErrorClass<LifecycleConflict>()(
|
||||
"SessionPending.LifecycleConflict",
|
||||
{
|
||||
id: SessionMessage.ID,
|
||||
},
|
||||
) {}
|
||||
|
||||
const fromRow = (row: typeof SessionPendingTable.$inferSelect): Info => {
|
||||
const base = {
|
||||
id: SessionMessage.ID.make(row.id),
|
||||
sessionID: SessionSchema.ID.make(row.session_id),
|
||||
timeCreated: DateTime.makeUnsafe(row.time_created),
|
||||
}
|
||||
if (row.type === "compaction") return Compaction.make({ ...base, type: "compaction" })
|
||||
if (!row.delivery) throw new LifecycleConflict({ id: base.id })
|
||||
if (row.type === "user")
|
||||
return User.make({
|
||||
...base,
|
||||
type: "user",
|
||||
data: decodeUser(row.data),
|
||||
delivery: row.delivery,
|
||||
})
|
||||
if (row.type === "synthetic")
|
||||
return Synthetic.make({
|
||||
...base,
|
||||
type: "synthetic",
|
||||
data: decodeSynthetic(row.data),
|
||||
delivery: row.delivery,
|
||||
})
|
||||
throw new LifecycleConflict({ id: base.id })
|
||||
}
|
||||
|
||||
export const find = Effect.fn("SessionPending.find")(function* (db: DatabaseService, id: SessionMessage.ID) {
|
||||
const row = yield* db
|
||||
.select()
|
||||
.from(SessionPendingTable)
|
||||
.where(eq(SessionPendingTable.id, id))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
return row === undefined ? undefined : fromRow(row)
|
||||
})
|
||||
|
||||
export const compaction = Effect.fn("SessionPending.compaction")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
) {
|
||||
const row = yield* db
|
||||
.select()
|
||||
.from(SessionPendingTable)
|
||||
.where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.type, "compaction")))
|
||||
.orderBy(asc(SessionPendingTable.admitted_seq))
|
||||
.limit(1)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row) return
|
||||
const entry = fromRow(row)
|
||||
return entry.type === "compaction" ? entry : undefined
|
||||
})
|
||||
|
||||
const promotedFromMessage = Effect.fn("SessionPending.promotedFromMessage")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
id: SessionMessage.ID,
|
||||
delivery: Delivery,
|
||||
) {
|
||||
const row = yield* db
|
||||
.select()
|
||||
.from(SessionMessageTable)
|
||||
.where(eq(SessionMessageTable.id, id))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (row === undefined) return undefined
|
||||
if (row.session_id !== sessionID || (row.type !== "user" && row.type !== "synthetic"))
|
||||
return yield* Effect.die(new LifecycleConflict({ id }))
|
||||
const message = decodeMessage({ ...row.data, id: row.id, type: row.type })
|
||||
const base = { id, sessionID, timeCreated: message.time.created, delivery }
|
||||
if (message.type === "user")
|
||||
return User.make({
|
||||
...base,
|
||||
type: "user",
|
||||
data: decodeUser(message),
|
||||
})
|
||||
if (message.type === "synthetic")
|
||||
return Synthetic.make({
|
||||
...base,
|
||||
type: "synthetic",
|
||||
data: decodeSynthetic(message),
|
||||
})
|
||||
return yield* Effect.die(new LifecycleConflict({ id }))
|
||||
})
|
||||
|
||||
export const admit = Effect.fn("SessionPending.admit")(function* (
|
||||
db: DatabaseService,
|
||||
bus: Bus.Interface,
|
||||
request: {
|
||||
readonly id: SessionMessage.ID
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly input: Message
|
||||
},
|
||||
) {
|
||||
const existing = yield* find(db, request.id)
|
||||
if (existing !== undefined) {
|
||||
if (existing.type === "compaction") return yield* Effect.die(new LifecycleConflict({ id: request.id }))
|
||||
return existing
|
||||
}
|
||||
const promoted = yield* promotedFromMessage(db, request.sessionID, request.id, request.input.delivery)
|
||||
if (promoted !== undefined) return promoted
|
||||
return yield* bus
|
||||
.publish(SessionEvent.InputAdmitted, {
|
||||
inputID: request.id,
|
||||
sessionID: request.sessionID,
|
||||
input: request.input,
|
||||
})
|
||||
.pipe(
|
||||
Effect.flatMap((event) => {
|
||||
const base = {
|
||||
id: request.id,
|
||||
sessionID: request.sessionID,
|
||||
timeCreated: event.created,
|
||||
}
|
||||
return Effect.succeed(
|
||||
request.input.type === "user"
|
||||
? User.make({ ...base, ...request.input })
|
||||
: Synthetic.make({ ...base, ...request.input }),
|
||||
)
|
||||
}),
|
||||
Effect.catchDefect((defect) =>
|
||||
find(db, request.id).pipe(
|
||||
Effect.flatMap((stored) =>
|
||||
stored?.type === request.input.type ? Effect.succeed(stored) : Effect.die(defect),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
export const admitCompaction = Effect.fn("SessionPending.admitCompaction")(function* (
|
||||
db: DatabaseService,
|
||||
bus: Bus.Interface,
|
||||
input: { readonly id: SessionMessage.ID; readonly sessionID: SessionSchema.ID },
|
||||
) {
|
||||
return yield* inboxLocks.withLock(input.sessionID)(
|
||||
Effect.gen(function* () {
|
||||
const exact = yield* find(db, input.id)
|
||||
if (exact) {
|
||||
if (exact.type === "compaction" && exact.sessionID === input.sessionID) return exact
|
||||
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
}
|
||||
const pending = yield* compaction(db, input.sessionID)
|
||||
if (pending) return pending
|
||||
return yield* bus
|
||||
.publish(SessionEvent.Compaction.Admitted, {
|
||||
inputID: input.id,
|
||||
sessionID: input.sessionID,
|
||||
})
|
||||
.pipe(
|
||||
Effect.flatMap((event) => {
|
||||
if (event.durable === undefined)
|
||||
return Effect.die(new Error("Compaction admission event is missing aggregate sequence"))
|
||||
return compaction(db, input.sessionID).pipe(
|
||||
Effect.flatMap((stored) =>
|
||||
stored ? Effect.succeed(stored) : Effect.die(new LifecycleConflict({ id: input.id })),
|
||||
),
|
||||
)
|
||||
}),
|
||||
Effect.catchDefect((defect) =>
|
||||
compaction(db, input.sessionID).pipe(
|
||||
Effect.flatMap((stored) => (stored ? Effect.succeed(stored) : Effect.die(defect))),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
export const projectAdmitted = Effect.fn("SessionPending.projectAdmitted")(function* (
|
||||
db: DatabaseService,
|
||||
request: {
|
||||
readonly admittedSeq: number
|
||||
readonly id: SessionMessage.ID
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly input: Message
|
||||
readonly timeCreated: DateTime.Utc
|
||||
},
|
||||
) {
|
||||
const message = yield* db
|
||||
.select({ id: SessionMessageTable.id })
|
||||
.from(SessionMessageTable)
|
||||
.where(eq(SessionMessageTable.id, request.id))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (message !== undefined) return yield* Effect.die(new LifecycleConflict({ id: request.id }))
|
||||
const stored = yield* db
|
||||
.insert(SessionPendingTable)
|
||||
.values({
|
||||
id: request.id,
|
||||
session_id: request.sessionID,
|
||||
type: request.input.type,
|
||||
data: request.input.type === "user" ? encodeUser(request.input.data) : encodeSynthetic(request.input.data),
|
||||
delivery: request.input.delivery,
|
||||
admitted_seq: request.admittedSeq,
|
||||
time_created: DateTime.toEpochMillis(request.timeCreated),
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.returning({ id: SessionPendingTable.id })
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!stored) return yield* Effect.die(new LifecycleConflict({ id: request.id }))
|
||||
})
|
||||
|
||||
export const projectCompactionAdmitted = Effect.fn("SessionPending.projectCompactionAdmitted")(function* (
|
||||
db: DatabaseService,
|
||||
input: {
|
||||
readonly admittedSeq: number
|
||||
readonly id: SessionMessage.ID
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly timeCreated: DateTime.Utc
|
||||
},
|
||||
) {
|
||||
const message = yield* db
|
||||
.select({ id: SessionMessageTable.id })
|
||||
.from(SessionMessageTable)
|
||||
.where(eq(SessionMessageTable.id, input.id))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (message !== undefined) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
const stored = yield* db
|
||||
.insert(SessionPendingTable)
|
||||
.values({
|
||||
id: input.id,
|
||||
session_id: input.sessionID,
|
||||
type: "compaction",
|
||||
data: {},
|
||||
admitted_seq: input.admittedSeq,
|
||||
time_created: DateTime.toEpochMillis(input.timeCreated),
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.returning()
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (stored) {
|
||||
const entry = fromRow(stored)
|
||||
return entry.type === "compaction" ? entry : yield* Effect.die(new LifecycleConflict({ id: entry.id }))
|
||||
}
|
||||
const pending = yield* compaction(db, input.sessionID)
|
||||
if (pending) return pending
|
||||
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
})
|
||||
|
||||
/**
|
||||
* Consume one pending row at promotion. The row's content feeds the projected
|
||||
* message insert inside the same event transaction; the deleted row is what
|
||||
* makes the table pending-only.
|
||||
*/
|
||||
export const projectPromoted = Effect.fn("SessionPending.projectPromoted")(function* (
|
||||
db: DatabaseService,
|
||||
input: PendingRef,
|
||||
) {
|
||||
if (yield* compaction(db, input.sessionID)) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
const deleted = yield* db
|
||||
.delete(SessionPendingTable)
|
||||
.where(and(eq(SessionPendingTable.id, input.id), eq(SessionPendingTable.session_id, input.sessionID)))
|
||||
.returning()
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!deleted) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
const stored = fromRow(deleted)
|
||||
if (stored.type === "compaction") return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
return stored
|
||||
})
|
||||
|
||||
export const projectCancelled = Effect.fn("SessionPending.projectCancelled")(function* (
|
||||
db: DatabaseService,
|
||||
input: PendingRef,
|
||||
) {
|
||||
const deleted = yield* db
|
||||
.delete(SessionPendingTable)
|
||||
.where(
|
||||
and(
|
||||
eq(SessionPendingTable.id, input.id),
|
||||
eq(SessionPendingTable.session_id, input.sessionID),
|
||||
or(eq(SessionPendingTable.delivery, "queue"), eq(SessionPendingTable.delivery, "steer")),
|
||||
),
|
||||
)
|
||||
.returning({ id: SessionPendingTable.id })
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!deleted) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
})
|
||||
|
||||
const projectDelivery = Effect.fn("SessionPending.projectDelivery")(function* (
|
||||
db: DatabaseService,
|
||||
input: PendingRef & { readonly from: Delivery; readonly to: Delivery },
|
||||
) {
|
||||
const updated = yield* db
|
||||
.update(SessionPendingTable)
|
||||
.set({ delivery: input.to })
|
||||
.where(
|
||||
and(
|
||||
eq(SessionPendingTable.id, input.id),
|
||||
eq(SessionPendingTable.session_id, input.sessionID),
|
||||
eq(SessionPendingTable.delivery, input.from),
|
||||
),
|
||||
)
|
||||
.returning({ id: SessionPendingTable.id })
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!updated) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
})
|
||||
|
||||
export const projectSteered = Effect.fn("SessionPending.projectSteered")((db: DatabaseService, input: PendingRef) =>
|
||||
projectDelivery(db, { ...input, from: "queue", to: "steer" }),
|
||||
)
|
||||
|
||||
export const projectQueued = Effect.fn("SessionPending.projectQueued")((db: DatabaseService, input: PendingRef) =>
|
||||
projectDelivery(db, { ...input, from: "steer", to: "queue" }),
|
||||
)
|
||||
|
||||
export const settleCompaction = Effect.fn("SessionPending.settleCompaction")(function* (
|
||||
db: DatabaseService,
|
||||
input: { readonly sessionID: SessionSchema.ID },
|
||||
) {
|
||||
const deleted = yield* db
|
||||
.delete(SessionPendingTable)
|
||||
.where(and(eq(SessionPendingTable.session_id, input.sessionID), eq(SessionPendingTable.type, "compaction")))
|
||||
.returning()
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (deleted) {
|
||||
const stored = fromRow(deleted)
|
||||
return stored.type === "compaction" ? stored : yield* Effect.die(new LifecycleConflict({ id: stored.id }))
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
export const list = Effect.fn("SessionPending.list")(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
|
||||
const rows = yield* db
|
||||
.select()
|
||||
.from(SessionPendingTable)
|
||||
.where(eq(SessionPendingTable.session_id, sessionID))
|
||||
.orderBy(asc(SessionPendingTable.admitted_seq))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
return rows.map(fromRow)
|
||||
})
|
||||
|
||||
/**
|
||||
* Which pending rows count: "any" counts every row including compaction, while
|
||||
* delivery scopes are blocked behind a pending compaction barrier. "input" means
|
||||
* any model-facing input, steered or queued.
|
||||
*/
|
||||
export type Scope = "any" | "input" | Delivery
|
||||
|
||||
export const has = Effect.fn("SessionPending.has")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
scope: Scope,
|
||||
) {
|
||||
if (scope !== "any" && (yield* compaction(db, sessionID))) return false
|
||||
const row = yield* db
|
||||
.select({ id: SessionPendingTable.id })
|
||||
.from(SessionPendingTable)
|
||||
.where(
|
||||
and(
|
||||
eq(SessionPendingTable.session_id, sessionID),
|
||||
scope === "any"
|
||||
? undefined
|
||||
: scope === "input"
|
||||
? or(eq(SessionPendingTable.delivery, "steer"), eq(SessionPendingTable.delivery, "queue"))
|
||||
: eq(SessionPendingTable.delivery, scope),
|
||||
),
|
||||
)
|
||||
.limit(1)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
return row !== undefined
|
||||
})
|
||||
|
||||
export const equivalent = (
|
||||
input: User | Synthetic,
|
||||
expected: { readonly sessionID: SessionSchema.ID; readonly input: Message },
|
||||
) => {
|
||||
if (
|
||||
input.type !== expected.input.type ||
|
||||
input.delivery !== expected.input.delivery ||
|
||||
input.sessionID !== expected.sessionID
|
||||
)
|
||||
return false
|
||||
if (input.type === "user" && expected.input.type === "user")
|
||||
return JSON.stringify(encodeUser(input.data)) === JSON.stringify(encodeUser(expected.input.data))
|
||||
if (input.type === "synthetic" && expected.input.type === "synthetic")
|
||||
return JSON.stringify(encodeSynthetic(input.data)) === JSON.stringify(encodeSynthetic(expected.input.data))
|
||||
return false
|
||||
}
|
||||
|
||||
const publishMutation = <A, E, R>(input: PendingRef, effect: Effect.Effect<A, E, R>) =>
|
||||
inboxLocks.withLock(input.sessionID)(effect).pipe(Effect.asVoid)
|
||||
|
||||
export const cancel = Effect.fn("SessionPending.cancel")((bus: Bus.Interface, input: PendingRef) =>
|
||||
publishMutation(
|
||||
input,
|
||||
bus.publish(SessionEvent.InputCancelled, {
|
||||
sessionID: input.sessionID,
|
||||
inputID: input.id,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
export const steer = Effect.fn("SessionPending.steer")((bus: Bus.Interface, input: PendingRef) =>
|
||||
publishMutation(
|
||||
input,
|
||||
bus.publish(SessionEvent.InputSteered, {
|
||||
sessionID: input.sessionID,
|
||||
inputID: input.id,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
export const queue = Effect.fn("SessionPending.queue")((bus: Bus.Interface, input: PendingRef) =>
|
||||
publishMutation(
|
||||
input,
|
||||
bus.publish(SessionEvent.InputQueued, {
|
||||
sessionID: input.sessionID,
|
||||
inputID: input.id,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const publish = Effect.fn("SessionPending.publish")(function* (
|
||||
db: DatabaseService,
|
||||
bus: Bus.Interface,
|
||||
sessionID: SessionSchema.ID,
|
||||
rows: ReadonlyArray<typeof SessionPendingTable.$inferSelect>,
|
||||
) {
|
||||
if (yield* compaction(db, sessionID)) return 0
|
||||
yield* Effect.forEach(
|
||||
rows,
|
||||
(row) => {
|
||||
const entry = fromRow(row)
|
||||
if (entry.type === "compaction") return Effect.die(new LifecycleConflict({ id: entry.id }))
|
||||
return bus
|
||||
.publish(SessionEvent.InputPromoted, {
|
||||
sessionID,
|
||||
inputID: entry.id,
|
||||
})
|
||||
.pipe(
|
||||
Effect.catchDefect((defect) =>
|
||||
defect instanceof LifecycleConflict
|
||||
? promotedFromMessage(db, sessionID, entry.id, entry.delivery).pipe(
|
||||
Effect.flatMap((stored) => (stored !== undefined ? Effect.void : Effect.die(defect))),
|
||||
)
|
||||
: Effect.die(defect),
|
||||
),
|
||||
)
|
||||
},
|
||||
{ discard: true },
|
||||
)
|
||||
return rows.length
|
||||
})
|
||||
|
||||
/**
|
||||
* Promotes pending input into visible messages and returns the promoted count.
|
||||
* Steers always go first; only the "input" scope may fall through to one queued
|
||||
* input, and it then collects steers that arrived during promotion.
|
||||
*/
|
||||
export const promote = Effect.fn("SessionPending.promote")(function* (
|
||||
db: DatabaseService,
|
||||
bus: Bus.Interface,
|
||||
sessionID: SessionSchema.ID,
|
||||
scope: Promotable,
|
||||
) {
|
||||
return yield* inboxLocks.withLock(sessionID)(
|
||||
Effect.gen(function* () {
|
||||
if (yield* compaction(db, sessionID)) return 0
|
||||
const steers = yield* db
|
||||
.select()
|
||||
.from(SessionPendingTable)
|
||||
.where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.delivery, "steer")))
|
||||
.orderBy(asc(SessionPendingTable.admitted_seq))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
if (steers.length > 0 || scope === "steer") return yield* publish(db, bus, sessionID, steers)
|
||||
|
||||
const queued = yield* db
|
||||
.select()
|
||||
.from(SessionPendingTable)
|
||||
.where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.delivery, "queue")))
|
||||
.orderBy(asc(SessionPendingTable.admitted_seq))
|
||||
.limit(1)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!queued) return 0
|
||||
const promoted = yield* publish(db, bus, sessionID, [queued])
|
||||
const arrivedSteers = yield* db
|
||||
.select()
|
||||
.from(SessionPendingTable)
|
||||
.where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.delivery, "steer")))
|
||||
.orderBy(asc(SessionPendingTable.admitted_seq))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
return promoted + (yield* publish(db, bus, sessionID, arrivedSteers))
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -10,10 +10,10 @@ import { Model } from "../model.js"
|
||||
import { SessionEvent } from "./event.js"
|
||||
import { SessionMessage } from "./message.js"
|
||||
import { SessionMessageUpdater } from "./message-updater.js"
|
||||
import { SessionInbox } from "./inbox.js"
|
||||
import { SessionPending } from "./pending.js"
|
||||
import { Workspace } from "../workspace.js"
|
||||
import { InstructionState } from "./instruction-state.js"
|
||||
import { SessionInboxTable, SessionMessageTable, SessionTable } from "./sql.js"
|
||||
import { SessionPendingTable, SessionMessageTable, SessionTable } from "./sql.js"
|
||||
import { Slug } from "../util/slug.js"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { AbsolutePath, RelativePath } from "../schema.js"
|
||||
@@ -470,15 +470,14 @@ const layer = Layer.effectDiscard(
|
||||
)
|
||||
yield* bus.project(SessionEvent.UsageRecorded, (event) => applyUsage(db, event.data.sessionID, event.data))
|
||||
yield* bus.project(SessionEvent.Forked, (event) => projectFork(db, event))
|
||||
yield* bus.project(SessionEvent.InboxDelivered, (event) =>
|
||||
yield* bus.project(SessionEvent.InputPromoted, (event) =>
|
||||
Effect.gen(function* () {
|
||||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
const input = yield* SessionInbox.projectDelivered(db, {
|
||||
id: event.data.inboxID,
|
||||
const input = yield* SessionPending.projectPromoted(db, {
|
||||
id: event.data.inputID,
|
||||
sessionID: event.data.sessionID,
|
||||
})
|
||||
if (input.type === "compaction" || input.type === "move") return
|
||||
yield* insertMessage(
|
||||
db,
|
||||
event,
|
||||
@@ -486,33 +485,33 @@ const layer = Layer.effectDiscard(
|
||||
? {
|
||||
id: input.id,
|
||||
type: "user",
|
||||
metadata: input.payload.metadata,
|
||||
text: input.payload.text,
|
||||
files: input.payload.files,
|
||||
agents: input.payload.agents,
|
||||
skills: input.payload.skills,
|
||||
metadata: input.data.metadata,
|
||||
text: input.data.text,
|
||||
files: input.data.files,
|
||||
agents: input.data.agents,
|
||||
skills: input.data.skills,
|
||||
time: { created: event.created },
|
||||
}
|
||||
: {
|
||||
id: input.id,
|
||||
type: "synthetic",
|
||||
text: input.payload.text,
|
||||
description: input.payload.description,
|
||||
metadata: input.payload.metadata,
|
||||
text: input.data.text,
|
||||
description: input.data.description,
|
||||
metadata: input.data.metadata,
|
||||
time: { created: event.created },
|
||||
},
|
||||
)
|
||||
}),
|
||||
)
|
||||
yield* bus.project(SessionEvent.InboxEnqueued, (event) =>
|
||||
yield* bus.project(SessionEvent.InputAdmitted, (event) =>
|
||||
Effect.gen(function* () {
|
||||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
yield* SessionInbox.projectAdmitted(db, {
|
||||
enqueuedSeq: event.durable.seq,
|
||||
id: event.data.inboxID,
|
||||
yield* SessionPending.projectAdmitted(db, {
|
||||
admittedSeq: event.durable.seq,
|
||||
id: event.data.inputID,
|
||||
sessionID: event.data.sessionID,
|
||||
item: event.data.item,
|
||||
input: event.data.input,
|
||||
timeCreated: event.created,
|
||||
})
|
||||
yield* db
|
||||
@@ -523,17 +522,34 @@ const layer = Layer.effectDiscard(
|
||||
.pipe(Effect.orDie)
|
||||
}),
|
||||
)
|
||||
yield* bus.project(SessionEvent.InboxCancelled, (event) =>
|
||||
SessionInbox.projectCancelled(db, {
|
||||
id: event.data.inboxID,
|
||||
yield* bus.project(SessionEvent.InputCancelled, (event) =>
|
||||
SessionPending.projectCancelled(db, {
|
||||
id: event.data.inputID,
|
||||
sessionID: event.data.sessionID,
|
||||
}),
|
||||
)
|
||||
yield* bus.project(SessionEvent.InboxDeliveryChanged, (event) =>
|
||||
SessionInbox.projectDeliveryChanged(db, {
|
||||
id: event.data.inboxID,
|
||||
yield* bus.project(SessionEvent.InputSteered, (event) =>
|
||||
SessionPending.projectSteered(db, {
|
||||
id: event.data.inputID,
|
||||
sessionID: event.data.sessionID,
|
||||
delivery: event.data.delivery,
|
||||
}),
|
||||
)
|
||||
yield* bus.project(SessionEvent.InputQueued, (event) =>
|
||||
SessionPending.projectQueued(db, {
|
||||
id: event.data.inputID,
|
||||
sessionID: event.data.sessionID,
|
||||
}),
|
||||
)
|
||||
yield* bus.project(SessionEvent.Compaction.Admitted, (event) =>
|
||||
Effect.gen(function* () {
|
||||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
yield* SessionPending.projectCompactionAdmitted(db, {
|
||||
admittedSeq: event.durable.seq,
|
||||
id: event.data.inputID,
|
||||
sessionID: event.data.sessionID,
|
||||
timeCreated: event.created,
|
||||
})
|
||||
}),
|
||||
)
|
||||
yield* bus.project(SessionEvent.Execution.Succeeded, (event) => run(db, event))
|
||||
@@ -580,6 +596,8 @@ const layer = Layer.effectDiscard(
|
||||
yield* InstructionState.advanceEpoch(db, event.data.sessionID, event.durable.seq)
|
||||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
if (event.data.reason === "manual")
|
||||
yield* SessionPending.settleCompaction(db, { sessionID: event.data.sessionID })
|
||||
}),
|
||||
)
|
||||
yield* bus.project(SessionEvent.Compaction.Failed, (event) =>
|
||||
@@ -587,6 +605,8 @@ const layer = Layer.effectDiscard(
|
||||
yield* run(db, event)
|
||||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
if (event.data.reason === "manual")
|
||||
yield* SessionPending.settleCompaction(db, { sessionID: event.data.sessionID })
|
||||
}),
|
||||
)
|
||||
yield* bus.project(SessionEvent.RevertEvent.Staged, (event) =>
|
||||
@@ -630,11 +650,11 @@ const layer = Layer.effectDiscard(
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* db
|
||||
.delete(SessionInboxTable)
|
||||
.delete(SessionPendingTable)
|
||||
.where(
|
||||
and(
|
||||
eq(SessionInboxTable.session_id, event.data.sessionID),
|
||||
gte(SessionInboxTable.enqueued_seq, boundary.seq),
|
||||
eq(SessionPendingTable.session_id, event.data.sessionID),
|
||||
gte(SessionPendingTable.admitted_seq, boundary.seq),
|
||||
),
|
||||
)
|
||||
.run()
|
||||
|
||||
@@ -17,7 +17,7 @@ import { InstructionState } from "../instruction-state.js"
|
||||
import { SessionCompaction } from "../compaction.js"
|
||||
import { SessionContext } from "../context.js"
|
||||
import { SessionEvent } from "../event.js"
|
||||
import { SessionInbox } from "../inbox.js"
|
||||
import { SessionPending } from "../pending.js"
|
||||
import { SessionModelRequest } from "../model-request.js"
|
||||
import { SessionMessage } from "../message.js"
|
||||
import { SessionSchema } from "../schema.js"
|
||||
@@ -125,19 +125,13 @@ const layer = Layer.effect(
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly force: boolean
|
||||
}) {
|
||||
let force = input.force
|
||||
if (!force && !(yield* SessionInbox.has(db, input.sessionID, "any"))) return
|
||||
if (!input.force && !(yield* SessionPending.has(db, input.sessionID, "any"))) return
|
||||
yield* settleStaleToolCalls(input.sessionID)
|
||||
while (true) {
|
||||
if (yield* runPendingCompaction(input.sessionID)) {
|
||||
force = false
|
||||
continue
|
||||
}
|
||||
if (yield* runPendingMove(input.sessionID)) return
|
||||
if (!force && !(yield* SessionInbox.has(db, input.sessionID, "input"))) return
|
||||
if (yield* runSteps(input.sessionID)) return
|
||||
force = false
|
||||
}
|
||||
yield* runPendingCompaction(input.sessionID)
|
||||
if (!input.force && !(yield* SessionPending.has(db, input.sessionID, "input"))) return
|
||||
do {
|
||||
yield* runSteps(input.sessionID)
|
||||
} while (yield* SessionPending.has(db, input.sessionID, "input"))
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -146,13 +140,12 @@ const layer = Layer.effect(
|
||||
*/
|
||||
const runSteps = Effect.fn("SessionRunner.runSteps")(function* (sessionID: SessionSchema.ID) {
|
||||
// Fresh work may promote queued input; later steps absorb steers only.
|
||||
let promotable: SessionInbox.Promotable = "input"
|
||||
let promotable: SessionPending.Promotable = "input"
|
||||
let step = 1
|
||||
while (true) {
|
||||
if (yield* runPendingCompaction(sessionID)) continue
|
||||
if (yield* runPendingMove(sessionID)) return true
|
||||
const result = yield* runStep(sessionID, promotable, step)
|
||||
if (!result.needsContinuation && !(yield* SessionInbox.has(db, sessionID, "steer"))) return false
|
||||
yield* runPendingCompaction(sessionID)
|
||||
if (!result.needsContinuation && !(yield* SessionPending.has(db, sessionID, "steer"))) return
|
||||
promotable = "steer"
|
||||
step = result.step + 1
|
||||
}
|
||||
@@ -161,7 +154,7 @@ const layer = Layer.effect(
|
||||
/** Completes one logical model step, transparently retrying or rebuilding after compaction. */
|
||||
const runStep = Effect.fnUntraced(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
promotable: SessionInbox.Promotable,
|
||||
promotable: SessionPending.Promotable,
|
||||
step: number,
|
||||
) {
|
||||
// Minting message identity before any attempt lets retries resume the same durable
|
||||
@@ -189,7 +182,7 @@ const layer = Layer.effect(
|
||||
.pipe(Effect.andThen(Effect.fail(failure.cause))),
|
||||
),
|
||||
)
|
||||
let currentPromotable: SessionInbox.Promotable | undefined = promotable
|
||||
let currentPromotable: SessionPending.Promotable | undefined = promotable
|
||||
let currentStep = step
|
||||
// Overflow recovery is one-shot: a call after recovery must not recover another overflow.
|
||||
let recoverOverflow = true
|
||||
@@ -232,7 +225,7 @@ const layer = Layer.effect(
|
||||
*/
|
||||
const callModel = Effect.fn("SessionRunner.callModel")(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
promotable: SessionInbox.Promotable | undefined,
|
||||
promotable: SessionPending.Promotable | undefined,
|
||||
step: number,
|
||||
recoverOverflow: boolean,
|
||||
assistantMessageID: SessionMessage.ID,
|
||||
@@ -241,7 +234,7 @@ const layer = Layer.effect(
|
||||
// Establish what the model knows before admitting what the user said, so
|
||||
// a blocked first step leaves pending inputs untouched.
|
||||
yield* InstructionState.prepare(db, bus, selected.instructions, selected.session.id)
|
||||
const promoted = promotable ? yield* SessionInbox.promote(db, bus, selected.session.id, promotable) : 0
|
||||
const promoted = promotable ? yield* SessionPending.promote(db, bus, selected.session.id, promotable) : 0
|
||||
if (promoted > 0) yield* startTitle(sessionID)
|
||||
// Promoted input opens a fresh step allowance.
|
||||
const currentStep = promoted > 0 ? 1 : step
|
||||
@@ -252,7 +245,7 @@ const layer = Layer.effect(
|
||||
// Make room: history must fit the context window before the call. A pending manual
|
||||
// compaction owns this instead; the runner executes it between steps.
|
||||
const compactionInput = { session, messages: loaded.messages, model, ref: resolved.ref, cost: resolved.cost }
|
||||
if (compaction.required(compactionInput)) {
|
||||
if (compaction.required(compactionInput) && !(yield* SessionPending.compaction(db, session.id))) {
|
||||
const compacted = yield* compaction.compact(compactionInput)
|
||||
if (compacted.status === "completed")
|
||||
return CallOutcome.Restart({ step: currentStep, recoveredOverflow: false })
|
||||
@@ -282,6 +275,7 @@ const layer = Layer.effect(
|
||||
const stepUsage = (finish: NonNullable<StepRecord["finish"]>) => ({
|
||||
cost: SessionUsage.calculateCost(resolved.cost, finish.tokens),
|
||||
tokens: finish.tokens,
|
||||
generated: finish.generated,
|
||||
})
|
||||
|
||||
const captureStepEnd = Effect.fnUntraced(function* () {
|
||||
@@ -489,71 +483,36 @@ const layer = Layer.effect(
|
||||
const runPendingCompaction = Effect.fn("SessionRunner.runPendingCompaction")(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
) {
|
||||
const pending = yield* SessionPending.compaction(db, sessionID)
|
||||
if (!pending) return
|
||||
const session = yield* getSession(sessionID)
|
||||
return yield* Effect.uninterruptibleMask((restore) =>
|
||||
Effect.gen(function* () {
|
||||
const pending = yield* SessionInbox.serialized(
|
||||
sessionID,
|
||||
Effect.gen(function* () {
|
||||
const selected =
|
||||
(yield* SessionInbox.nextSteer(db, sessionID)) ?? (yield* SessionInbox.nextQueued(db, sessionID))
|
||||
if (selected?.type !== "compaction") return
|
||||
yield* bus.publishAll([
|
||||
[SessionEvent.InboxDelivered, { sessionID, inboxID: selected.id }],
|
||||
[SessionEvent.Compaction.Started, { sessionID, reason: "manual", recent: "", inputID: selected.id }],
|
||||
])
|
||||
return selected
|
||||
}),
|
||||
)
|
||||
if (pending?.type !== "compaction") return false
|
||||
const session = yield* getSession(sessionID)
|
||||
const compacted = yield* restore(
|
||||
Effect.gen(function* () {
|
||||
return yield* compaction.compactManual({
|
||||
session,
|
||||
messages: yield* store.context(sessionID),
|
||||
inputID: pending.id,
|
||||
started: true,
|
||||
})
|
||||
}),
|
||||
).pipe(Effect.exit)
|
||||
if (Exit.isSuccess(compacted)) return true
|
||||
yield* bus.publish(SessionEvent.Compaction.Failed, {
|
||||
sessionID,
|
||||
reason: "manual",
|
||||
error: Cause.hasInterruptsOnly(compacted.cause)
|
||||
? { type: "aborted", message: "Compaction cancelled" }
|
||||
: { type: "compaction.failed", message: Cause.pretty(compacted.cause) },
|
||||
inputID: pending.id,
|
||||
})
|
||||
if (Exit.isSuccess(compacted)) return
|
||||
const unsettled = yield* SessionPending.compaction(db, sessionID)
|
||||
if (unsettled)
|
||||
yield* bus.publish(SessionEvent.Compaction.Failed, {
|
||||
sessionID,
|
||||
reason: "manual",
|
||||
error: Cause.hasInterruptsOnly(compacted.cause)
|
||||
? { type: "aborted", message: "Compaction cancelled" }
|
||||
: { type: "compaction.failed", message: Cause.pretty(compacted.cause) },
|
||||
inputID: unsettled.id,
|
||||
})
|
||||
return yield* Effect.failCause(compacted.cause)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const runPendingMove = Effect.fn("SessionRunner.runPendingMove")(function* (sessionID: SessionSchema.ID) {
|
||||
return yield* SessionInbox.serialized(
|
||||
sessionID,
|
||||
Effect.gen(function* () {
|
||||
const pending =
|
||||
(yield* SessionInbox.nextSteer(db, sessionID)) ?? (yield* SessionInbox.nextQueued(db, sessionID))
|
||||
if (pending?.type !== "move") return false
|
||||
yield* bus.publishAll([
|
||||
[SessionEvent.InboxDelivered, { sessionID, inboxID: pending.id }],
|
||||
[
|
||||
SessionEvent.Moved,
|
||||
{
|
||||
sessionID,
|
||||
location: pending.payload.location,
|
||||
projectID: pending.payload.projectID,
|
||||
subpath: pending.payload.subpath,
|
||||
},
|
||||
],
|
||||
])
|
||||
return true
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
/** Closes stale tool calls left active by an earlier interrupted drain. */
|
||||
const settleStaleToolCalls = Effect.fn("SessionRunner.settleStaleToolCalls")(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type LLMEvent, type ProviderMetadata, type ToolResultValue } from "@opencode-ai/ai"
|
||||
import { Effect } from "effect"
|
||||
import { DateTime, Effect } from "effect"
|
||||
import { Bus } from "../../bus.js"
|
||||
import { Model } from "../../model.js"
|
||||
import { SessionEvent } from "../event.js"
|
||||
@@ -36,6 +36,7 @@ export interface StepRecord {
|
||||
readonly finish?: {
|
||||
readonly finish: Extract<LLMEvent, { type: "step-finish" }>["reason"]["normalized"]
|
||||
readonly tokens: ReturnType<typeof SessionUsage.tokens>
|
||||
readonly generated: DateTime.Utc
|
||||
}
|
||||
readonly calls: ReadonlyArray<{
|
||||
readonly id: string
|
||||
@@ -310,6 +311,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
const publishStepFailure = Effect.fnUntraced(function* (details?: {
|
||||
readonly cost?: Money.USD
|
||||
readonly tokens?: ReturnType<typeof SessionUsage.tokens>
|
||||
readonly generated?: DateTime.Utc
|
||||
readonly snapshot?: Snapshot.ID
|
||||
readonly files?: readonly RelativePath[]
|
||||
}) {
|
||||
@@ -493,9 +495,14 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
return
|
||||
}
|
||||
case "step-finish":
|
||||
const generated = yield* DateTime.now
|
||||
yield* flush()
|
||||
if (stepSettlement) return yield* Effect.die(new Error("Duplicate step finish"))
|
||||
stepSettlement = { finish: event.reason.normalized, tokens: SessionUsage.tokens(event.usage) }
|
||||
stepSettlement = {
|
||||
finish: event.reason.normalized,
|
||||
tokens: SessionUsage.tokens(event.usage),
|
||||
generated,
|
||||
}
|
||||
if (event.reason.normalized === "content-filter") {
|
||||
providerFailed = true
|
||||
yield* failAssistant({ type: "provider.content-filter", message: "Provider blocked the response" })
|
||||
|
||||
@@ -3,7 +3,7 @@ import { sql } from "drizzle-orm"
|
||||
import { directoryColumn, pathColumn } from "../database/path.js"
|
||||
import { ProjectTable } from "../project/sql.js"
|
||||
import type { SessionMessage } from "./message.js"
|
||||
import type { SessionInbox } from "./inbox.js"
|
||||
import type { SessionPending } from "./pending.js"
|
||||
import type { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||
import { PermissionV1 } from "../v1/permission.js"
|
||||
import { Project } from "../project.js"
|
||||
@@ -12,7 +12,7 @@ import { Workspace } from "../workspace.js"
|
||||
import { Timestamps } from "../database/schema.sql.js"
|
||||
import type { Instruction } from "@opencode-ai/schema/instruction"
|
||||
import type { Session } from "@opencode-ai/schema/session"
|
||||
import type { CompactionPayload, MovePayload, SyntheticPayload, UserPayload } from "@opencode-ai/schema/session-inbox"
|
||||
import type { SyntheticData, UserData } from "@opencode-ai/schema/session-pending"
|
||||
import type { RevertV1 } from "@opencode-ai/schema/session-revert"
|
||||
import type { Schema } from "effect"
|
||||
|
||||
@@ -101,9 +101,9 @@ export const SessionPendingTable = sqliteTable(
|
||||
.$type<SessionSchema.ID>()
|
||||
.notNull()
|
||||
.references(() => SessionTable.id, { onDelete: "cascade" }),
|
||||
type: text().$type<SessionInbox.Info["type"]>().notNull(),
|
||||
data: text({ mode: "json" }).$type<UserPayload | SyntheticPayload | Record<string, never>>().notNull(),
|
||||
delivery: text().$type<SessionInbox.Delivery>(),
|
||||
type: text().$type<SessionPending.Info["type"]>().notNull(),
|
||||
data: text({ mode: "json" }).$type<UserData | SyntheticData | Record<string, never>>().notNull(),
|
||||
delivery: text().$type<SessionPending.Delivery>(),
|
||||
admitted_seq: integer().notNull(),
|
||||
time_created: integer()
|
||||
.notNull()
|
||||
@@ -118,28 +118,6 @@ export const SessionPendingTable = sqliteTable(
|
||||
],
|
||||
)
|
||||
|
||||
export const SessionInboxTable = sqliteTable(
|
||||
"session_inbox",
|
||||
{
|
||||
id: text().$type<SessionMessage.ID>().primaryKey(),
|
||||
session_id: text()
|
||||
.$type<SessionSchema.ID>()
|
||||
.notNull()
|
||||
.references(() => SessionTable.id, { onDelete: "cascade" }),
|
||||
type: text().$type<SessionInbox.Info["type"]>().notNull(),
|
||||
payload: text({ mode: "json" }).$type<UserPayload | SyntheticPayload | CompactionPayload | MovePayload>().notNull(),
|
||||
delivery: text().$type<SessionInbox.Delivery>().notNull(),
|
||||
enqueued_seq: integer().notNull(),
|
||||
time_created: integer()
|
||||
.notNull()
|
||||
.$default(() => Date.now()),
|
||||
},
|
||||
(table) => [
|
||||
index("session_inbox_session_delivery_seq_idx").on(table.session_id, table.delivery, table.enqueued_seq),
|
||||
uniqueIndex("session_inbox_session_enqueued_seq_idx").on(table.session_id, table.enqueued_seq),
|
||||
],
|
||||
)
|
||||
|
||||
export const InstructionEntryTable = sqliteTable(
|
||||
"instruction_entry",
|
||||
{
|
||||
|
||||
+12
-17
@@ -5,7 +5,6 @@ import { Context, Deferred, Duration, Effect, Fiber, Layer, Schema, Stream } fro
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { produce } from "immer"
|
||||
import { Shell } from "@opencode-ai/schema/shell"
|
||||
import { AppProcess } from "@opencode-ai/util/process"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { Config } from "./config.js"
|
||||
import { Bus } from "./bus.js"
|
||||
@@ -51,7 +50,7 @@ export interface Interface {
|
||||
readonly create: <E = never, R = never>(
|
||||
input: Shell.CreateInput,
|
||||
before?: (input: ShellCreateBefore) => Effect.Effect<void, E, R>,
|
||||
) => Effect.Effect<Shell.Info, E | AppProcess.AppProcessError, R>
|
||||
) => Effect.Effect<Shell.Info, E, R>
|
||||
// Currently running commands only; exited shells are retained for get/output but excluded here.
|
||||
readonly list: () => Effect.Effect<Shell.Info[]>
|
||||
readonly get: (id: Shell.ID) => Effect.Effect<Shell.Info, NotFoundError>
|
||||
@@ -216,23 +215,19 @@ export const layer = (options?: ShellSelect.Options) =>
|
||||
// Spawn through the Environment and stream combined output to the file. The handle is scope-bound, so
|
||||
// the managing fiber keeps its scope open until the command terminates (it awaits `done` at the
|
||||
// end). `create` returns once `ready` resolves with the registered session.
|
||||
const ready = Deferred.makeUnsafe<Active, AppProcess.AppProcessError>()
|
||||
const ready = Deferred.makeUnsafe<Active>()
|
||||
runFork(
|
||||
Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const handle = yield* environment.spawner
|
||||
.spawn(
|
||||
ChildProcess.make(invocation.shell, args, {
|
||||
cwd: invocation.cwd,
|
||||
env: invocation.env,
|
||||
stdin: "ignore",
|
||||
detached: process.platform !== "win32",
|
||||
forceKillAfter: Duration.seconds(3),
|
||||
}),
|
||||
)
|
||||
.pipe(
|
||||
Effect.mapError((cause) => new AppProcess.AppProcessError({ command: invocation.command, cause })),
|
||||
)
|
||||
const handle = yield* environment.spawner.spawn(
|
||||
ChildProcess.make(invocation.shell, args, {
|
||||
cwd: invocation.cwd,
|
||||
env: invocation.env,
|
||||
stdin: "ignore",
|
||||
detached: process.platform !== "win32",
|
||||
forceKillAfter: Duration.seconds(3),
|
||||
}),
|
||||
)
|
||||
const session: Active = {
|
||||
info: produce(info, (draft) => {
|
||||
draft.pid = handle.pid
|
||||
@@ -334,7 +329,7 @@ export const layer = (options?: ShellSelect.Options) =>
|
||||
// release (kill) the process before its exit is observed.
|
||||
yield* Deferred.await(session.done).pipe(Effect.catch(() => Effect.void))
|
||||
}),
|
||||
).pipe(Effect.catchTag("AppProcessError", (error) => Deferred.fail(ready, error))),
|
||||
).pipe(Effect.catch(() => Effect.void)),
|
||||
)
|
||||
|
||||
const session = yield* Deferred.await(ready)
|
||||
|
||||
@@ -2,7 +2,7 @@ export * as Tool from "./tool.js"
|
||||
export { CallID, Content, Error, FileContent, TextContent } from "@opencode-ai/schema/tool"
|
||||
export type { Context, Metadata, Options, Result } from "@opencode-ai/schema/tool"
|
||||
|
||||
import { ToolDefinition, type ToolCall } from "@opencode-ai/ai"
|
||||
import type { ToolCall, ToolDefinition } from "@opencode-ai/ai"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Context, Effect, Layer, Schema, Scope, Semaphore } from "effect"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
@@ -165,19 +165,6 @@ const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
if (entries.length === 0) return
|
||||
yield* Effect.forEach(
|
||||
entries,
|
||||
(entry) =>
|
||||
Effect.try({
|
||||
try: () => ToolDefinition.make(definition(entry.tool)),
|
||||
catch: (error) =>
|
||||
new RegistrationError({
|
||||
name: entry.key,
|
||||
message: `Invalid tool definition ${entry.key}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}),
|
||||
}),
|
||||
{ discard: true },
|
||||
)
|
||||
yield* Effect.uninterruptible(
|
||||
lock.withPermit(
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -455,120 +455,6 @@ describe("Bus", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("publishes a durable batch atomically in provided order", () =>
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
const { db } = yield* Database.Service
|
||||
const aggregateID = Event.ID.create()
|
||||
const observed = new Array<string>()
|
||||
yield* bus.project(SyncMessage, (event) =>
|
||||
Effect.sync(() => {
|
||||
observed.push(`project:${event.data.text}`)
|
||||
}),
|
||||
)
|
||||
yield* bus.listen((event) =>
|
||||
event.type === SyncMessage.type
|
||||
? Effect.gen(function* () {
|
||||
const text = (event.data as { readonly text: string }).text
|
||||
const row = yield* db
|
||||
.select({ seq: EventSequenceTable.seq })
|
||||
.from(EventSequenceTable)
|
||||
.where(eq(EventSequenceTable.aggregate_id, aggregateID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
observed.push(`notify:${text}:${row?.seq}`)
|
||||
})
|
||||
: Effect.void,
|
||||
)
|
||||
|
||||
const events = yield* bus.publishAll([
|
||||
[SyncMessage, { id: aggregateID, text: "first" }],
|
||||
[SyncMessage, { id: aggregateID, text: "second" }],
|
||||
])
|
||||
|
||||
expect(events.map((event) => event.durable.seq)).toEqual([Event.Seq.make(0), Event.Seq.make(1)])
|
||||
expect(observed).toEqual(["project:first", "project:second", "notify:first:1", "notify:second:1"])
|
||||
expect(
|
||||
(yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).all()).map(
|
||||
(row) => row.seq,
|
||||
),
|
||||
).toEqual([0, 1])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rolls back every batch event when a projector fails", () =>
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
const { db } = yield* Database.Service
|
||||
const aggregateID = Event.ID.create()
|
||||
const notifications = new Array<string>()
|
||||
yield* db.run("CREATE TABLE IF NOT EXISTS event_batch_probe (value text NOT NULL)")
|
||||
yield* db.run("DELETE FROM event_batch_probe")
|
||||
yield* bus.project(SyncMessage, (event) =>
|
||||
db
|
||||
.run(`INSERT INTO event_batch_probe (value) VALUES ('${event.data.text}')`)
|
||||
.pipe(
|
||||
Effect.orDie,
|
||||
Effect.andThen(event.data.text === "second" ? Effect.die("projector failed") : Effect.void),
|
||||
),
|
||||
)
|
||||
yield* bus.listen((event) =>
|
||||
Effect.sync(() => {
|
||||
notifications.push(event.type)
|
||||
}),
|
||||
)
|
||||
|
||||
const exit = yield* bus
|
||||
.publishAll([
|
||||
[SyncMessage, { id: aggregateID, text: "first" }],
|
||||
[SyncMessage, { id: aggregateID, text: "second" }],
|
||||
])
|
||||
.pipe(Effect.exit)
|
||||
|
||||
expect(String(exit)).toContain("projector failed")
|
||||
expect(yield* db.all("SELECT value FROM event_batch_probe")).toEqual([])
|
||||
expect(yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).all()).toEqual([])
|
||||
expect(
|
||||
yield* db.select().from(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, aggregateID)).all(),
|
||||
).toEqual([])
|
||||
expect(notifications).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("does not interleave a concurrent publish with batch notifications", () =>
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
const aggregateID = Event.ID.create()
|
||||
const firstObserved = yield* Deferred.make<void>()
|
||||
const continueNotifications = yield* Deferred.make<void>()
|
||||
const observed = new Array<string>()
|
||||
yield* bus.listen((event) => {
|
||||
if (event.type !== SyncMessage.type) return Effect.void
|
||||
const text = (event.data as { readonly text: string }).text
|
||||
return Effect.sync(() => observed.push(text)).pipe(
|
||||
Effect.andThen(text === "first" ? Deferred.succeed(firstObserved, undefined) : Effect.void),
|
||||
Effect.andThen(text === "first" ? Deferred.await(continueNotifications) : Effect.void),
|
||||
)
|
||||
})
|
||||
|
||||
const batch = yield* bus
|
||||
.publishAll([
|
||||
[SyncMessage, { id: aggregateID, text: "first" }],
|
||||
[SyncMessage, { id: aggregateID, text: "second" }],
|
||||
])
|
||||
.pipe(Effect.forkScoped)
|
||||
yield* Deferred.await(firstObserved)
|
||||
const single = yield* bus.publish(SyncMessage, { id: aggregateID, text: "third" }).pipe(Effect.forkScoped)
|
||||
yield* Effect.yieldNow
|
||||
|
||||
expect(observed).toEqual(["first"])
|
||||
yield* Deferred.succeed(continueNotifications, undefined)
|
||||
yield* Fiber.join(batch)
|
||||
yield* Fiber.join(single)
|
||||
expect(observed).toEqual(["first", "second", "third"])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("replays durable aggregate events after a sequence and tails new events", () =>
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import fs from "node:fs/promises"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/util/cross-spawn-spawner"
|
||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { EnvironmentUnavailable } from "../src/environment/unavailable"
|
||||
import {
|
||||
execDefaults,
|
||||
Failed,
|
||||
@@ -36,17 +35,6 @@ describe("typeFollowing", () => {
|
||||
)
|
||||
})
|
||||
|
||||
describe("no execution plane", () => {
|
||||
it.effect("fails spawn with a typed location error", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* EnvironmentUnavailable.spawner.spawn(ChildProcess.make("echo", ["hello"])).pipe(Effect.flip)
|
||||
|
||||
expect(error._tag).toBe("PlatformError")
|
||||
expect(error.message).toContain("location has no execution plane")
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
environmentConformance("memory environment", () =>
|
||||
Effect.sync(() => {
|
||||
const driver = makeMemoryDriver()
|
||||
|
||||
@@ -23,7 +23,6 @@ import { ID, type Payload } from "@opencode-ai/schema/event"
|
||||
import { Form } from "@opencode-ai/core/form"
|
||||
import { Integration } from "@opencode-ai/core/integration"
|
||||
import { Environment } from "@opencode-ai/core/environment/index"
|
||||
import { EnvironmentUnavailable } from "@opencode-ai/core/environment/unavailable"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { MCP } from "@opencode-ai/core/mcp/index"
|
||||
import { MCPClient } from "@opencode-ai/core/mcp/client"
|
||||
@@ -479,27 +478,6 @@ test("spawns local MCP servers through the location environment", async () => {
|
||||
expect(command.options.env).toEqual({ MCP_LOCATION_TEST: "configured" })
|
||||
})
|
||||
|
||||
test("reports a local MCP server as failed when the location has no execution plane", async () => {
|
||||
const config = new ConfigMCP.Local({ type: "local", command: ["example-mcp"] })
|
||||
const driver = Environment.makeMemoryDriver()
|
||||
const environment = Layer.succeed(
|
||||
Environment.Service,
|
||||
Environment.Service.of({ files: Environment.makeFiles(driver), spawner: EnvironmentUnavailable.spawner }),
|
||||
)
|
||||
|
||||
await Effect.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const service = yield* MCP.Service
|
||||
yield* service.tools()
|
||||
const status = (yield* service.servers()).find((server) => server.name === "resources")?.status
|
||||
expect(status).toEqual({
|
||||
status: "failed",
|
||||
error: expect.stringContaining("location has no execution plane"),
|
||||
})
|
||||
}).pipe(Effect.provide(resourceMcpLayer(config, undefined, undefined, { environment }))),
|
||||
)
|
||||
})
|
||||
|
||||
test("rejects sends before the stdio transport is started", async () => {
|
||||
await Effect.runPromise(
|
||||
Effect.scoped(
|
||||
|
||||
@@ -11,7 +11,7 @@ import { PluginPromise } from "@opencode-ai/core/plugin/promise"
|
||||
import { WebSearch } from "@opencode-ai/core/websearch"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { SessionInbox } from "@opencode-ai/core/session/inbox"
|
||||
import { SessionPending } from "@opencode-ai/core/session/pending"
|
||||
import { Tool } from "@opencode-ai/core/tool"
|
||||
import { Provider } from "@opencode-ai/core/provider"
|
||||
import { define } from "@opencode-ai/plugin/promise/plugin"
|
||||
@@ -61,12 +61,12 @@ describe("fromPromise", () => {
|
||||
synthetic: (value) => {
|
||||
seen = value
|
||||
return Effect.succeed(
|
||||
SessionInbox.Synthetic.make({
|
||||
SessionPending.Synthetic.make({
|
||||
id: SessionMessage.ID.make(input.id),
|
||||
sessionID: Session.ID.make(input.sessionID),
|
||||
timeCreated: DateTime.makeUnsafe(0),
|
||||
type: "synthetic",
|
||||
payload: {
|
||||
data: {
|
||||
text: input.text,
|
||||
metadata: input.metadata,
|
||||
},
|
||||
|
||||
@@ -195,22 +195,6 @@ describe("GithubCopilotPlugin", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rewrites models.dev fallback models to the GitHub Copilot package", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((catalog) => {
|
||||
catalog.provider.update(Provider.ID.githubCopilot, () => {})
|
||||
catalog.model.update(Provider.ID.githubCopilot, Model.ID.make("gpt-5.6-sol"), (model) => {
|
||||
model.package = "@ai-sdk/openai-compatible"
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.model.get(Provider.ID.githubCopilot, Model.ID.make("gpt-5.6-sol"))).package).toBe(
|
||||
"@ai-sdk/github-copilot",
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("selects languageModel when responses and chat are absent", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* Plugin.Service
|
||||
|
||||
@@ -15,6 +15,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
import { SessionCompaction } from "@opencode-ai/core/session/compaction"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { SessionPending } from "@opencode-ai/core/session/pending"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
@@ -80,7 +81,7 @@ const it = testEffect(
|
||||
)
|
||||
|
||||
describe("Session.compact", () => {
|
||||
it.effect("durably stacks manual compaction", () =>
|
||||
it.effect("durably admits and coalesces manual compaction", () =>
|
||||
Effect.gen(function* () {
|
||||
requests = []
|
||||
const session = yield* Session.Service
|
||||
@@ -88,18 +89,18 @@ describe("Session.compact", () => {
|
||||
const created = yield* session.create({ location })
|
||||
|
||||
const messageID = SessionMessage.ID.create()
|
||||
yield* bus.publish(SessionEvent.InboxEnqueued, {
|
||||
yield* bus.publish(SessionEvent.InputAdmitted, {
|
||||
sessionID: created.id,
|
||||
inboxID: messageID,
|
||||
item: {
|
||||
inputID: messageID,
|
||||
input: {
|
||||
type: "user",
|
||||
payload: { text: "Please compact this session history." },
|
||||
data: { text: "Please compact this session history." },
|
||||
delivery: "steer",
|
||||
},
|
||||
})
|
||||
yield* bus.publish(SessionEvent.InboxDelivered, {
|
||||
yield* bus.publish(SessionEvent.InputPromoted, {
|
||||
sessionID: created.id,
|
||||
inboxID: messageID,
|
||||
inputID: messageID,
|
||||
})
|
||||
|
||||
expect(yield* session.compact({ id: messageID, sessionID: created.id }).pipe(Effect.flip)).toMatchObject({
|
||||
@@ -109,17 +110,12 @@ describe("Session.compact", () => {
|
||||
const first = yield* session.compact({ sessionID: created.id })
|
||||
const second = yield* session.compact({ sessionID: created.id })
|
||||
|
||||
expect(second.id).not.toBe(first.id)
|
||||
expect(second.id).toBe(first.id)
|
||||
expect(requests).toHaveLength(0)
|
||||
expect(yield* session.inbox(created.id)).toEqual([
|
||||
expect.objectContaining({ id: first.id, type: "compaction", delivery: "queue" }),
|
||||
expect.objectContaining({ id: second.id, type: "compaction", delivery: "queue" }),
|
||||
])
|
||||
expect(yield* SessionPending.compaction((yield* Database.Service).db, created.id)).toMatchObject({
|
||||
id: first.id,
|
||||
})
|
||||
expect((yield* session.context(created.id)).find((message) => message.id === first.id)).toBeUndefined()
|
||||
|
||||
const steered = yield* session.create({ location })
|
||||
const steer = yield* session.compact({ sessionID: steered.id, delivery: "steer" })
|
||||
expect(steer).toMatchObject({ type: "compaction", delivery: "steer" })
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -19,7 +19,7 @@ import { Session } from "@opencode-ai/core/session"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { SessionInbox } from "@opencode-ai/core/session/inbox"
|
||||
import { SessionPending } from "@opencode-ai/core/session/pending"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
@@ -267,9 +267,9 @@ describe("Session.create", () => {
|
||||
text: "First",
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInbox.promote(db, bus, parent.id, "steer")
|
||||
yield* SessionPending.promote(db, bus, parent.id, "steer")
|
||||
yield* session.synthetic({ sessionID: parent.id, text: "parent note", resume: false })
|
||||
yield* SessionInbox.promote(db, bus, parent.id, "steer")
|
||||
yield* SessionPending.promote(db, bus, parent.id, "steer")
|
||||
|
||||
const forked = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
|
||||
const parentContext = yield* session.context(parent.id)
|
||||
@@ -289,24 +289,24 @@ describe("Session.create", () => {
|
||||
durable: { seq: 0 },
|
||||
data: { sessionID: forked.id, parentID: parent.id },
|
||||
})
|
||||
expect(yield* SessionInbox.find(db, forkContext[0].id)).toBeUndefined()
|
||||
expect(yield* SessionInbox.find(db, forkContext[1].id)).toBeUndefined()
|
||||
expect(yield* SessionPending.find(db, forkContext[0].id)).toBeUndefined()
|
||||
expect(yield* SessionPending.find(db, forkContext[1].id)).toBeUndefined()
|
||||
expect(
|
||||
yield* session.prompt({ id: forkContext[0].id, sessionID: forked.id, text: "First", resume: false }),
|
||||
).toMatchObject({ id: forkContext[0].id, type: "user", payload: { text: "First" } })
|
||||
).toMatchObject({ id: forkContext[0].id, type: "user", data: { text: "First" } })
|
||||
|
||||
yield* session.prompt({
|
||||
sessionID: parent.id,
|
||||
text: "Parent changed",
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInbox.promote(db, bus, parent.id, "steer")
|
||||
yield* SessionPending.promote(db, bus, parent.id, "steer")
|
||||
yield* session.prompt({
|
||||
sessionID: forked.id,
|
||||
text: "Child continues",
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInbox.promote(db, bus, forked.id, "steer")
|
||||
yield* SessionPending.promote(db, bus, forked.id, "steer")
|
||||
|
||||
expect((yield* session.context(parent.id)).map((message) => message.type)).toEqual(["user", "synthetic", "user"])
|
||||
expect((yield* session.context(forked.id)).map((message) => message.type)).toEqual(["user", "synthetic", "user"])
|
||||
@@ -316,7 +316,7 @@ describe("Session.create", () => {
|
||||
(event): number | undefined => event.durable?.seq,
|
||||
),
|
||||
).toEqual([0, 5, 6])
|
||||
expect(yield* SessionInbox.find(db, admitted.id)).toBeUndefined()
|
||||
expect(yield* SessionPending.find(db, admitted.id)).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -327,7 +327,7 @@ describe("Session.create", () => {
|
||||
const { db } = yield* Database.Service
|
||||
const parent = yield* session.create({ location })
|
||||
yield* session.prompt({ sessionID: parent.id, text: "First", resume: false })
|
||||
yield* SessionInbox.promote(db, bus, parent.id, "steer")
|
||||
yield* SessionPending.promote(db, bus, parent.id, "steer")
|
||||
|
||||
const forked = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
|
||||
const row = yield* db.select().from(SessionTable).where(eq(SessionTable.id, forked.id)).get().pipe(Effect.orDie)
|
||||
@@ -359,13 +359,13 @@ describe("Session.create", () => {
|
||||
text: "First",
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInbox.promote(db, bus, parent.id, "steer")
|
||||
yield* SessionPending.promote(db, bus, parent.id, "steer")
|
||||
const second = yield* session.prompt({
|
||||
sessionID: parent.id,
|
||||
text: "Second",
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInbox.promote(db, bus, parent.id, "steer")
|
||||
yield* SessionPending.promote(db, bus, parent.id, "steer")
|
||||
const assistantMessageID = SessionMessage.ID.create()
|
||||
const model = Model.Ref.make({ id: Model.ID.make("model"), providerID: Provider.ID.make("provider") })
|
||||
yield* bus.publish(SessionEvent.Step.Started, {
|
||||
@@ -496,7 +496,7 @@ describe("Session.create", () => {
|
||||
text: "Hello",
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInbox.promote(db, bus, created.id, "steer")
|
||||
yield* SessionPending.promote(db, bus, created.id, "steer")
|
||||
|
||||
expect(
|
||||
Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(3), Stream.runCollect)),
|
||||
@@ -504,13 +504,10 @@ describe("Session.create", () => {
|
||||
{ durable: { seq: 0 }, type: "session.created" },
|
||||
{
|
||||
durable: { seq: 1 },
|
||||
type: "session.inbox.enqueued",
|
||||
data: {
|
||||
inboxID: expect.any(String),
|
||||
item: { type: "user", payload: { text: "Hello" }, delivery: "steer" },
|
||||
},
|
||||
type: "session.input.admitted",
|
||||
data: { input: { type: "user", data: { text: "Hello" }, delivery: "steer" } },
|
||||
},
|
||||
{ durable: { seq: 2 }, type: "session.inbox.delivered" },
|
||||
{ durable: { seq: 2 }, type: "session.input.promoted" },
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -526,7 +523,7 @@ describe("Session.create", () => {
|
||||
text: "Replay lifecycle",
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInbox.promote(sourceDb, sourceEvents, created.id, "steer")
|
||||
yield* SessionPending.promote(sourceDb, sourceEvents, created.id, "steer")
|
||||
const serialized = (yield* sourceDb
|
||||
.select()
|
||||
.from(EventTable)
|
||||
@@ -566,17 +563,17 @@ describe("Session.create", () => {
|
||||
|
||||
expect(yield* store.get(created.id)).toBeUndefined()
|
||||
expect(yield* bus.replayAll(serialized.slice(0, 2))).toBe(created.id)
|
||||
expect(yield* SessionInbox.find(db, admitted.id)).toMatchObject({
|
||||
expect(yield* SessionPending.find(db, admitted.id)).toMatchObject({
|
||||
id: admitted.id,
|
||||
sessionID: created.id,
|
||||
type: "user",
|
||||
payload: { text: "Replay lifecycle" },
|
||||
data: { text: "Replay lifecycle" },
|
||||
delivery: "steer",
|
||||
})
|
||||
expect(yield* store.context(created.id)).toEqual([])
|
||||
|
||||
expect(yield* bus.replayAll(serialized.slice(2))).toBe(created.id)
|
||||
expect(yield* SessionInbox.find(db, admitted.id)).toBeUndefined()
|
||||
expect(yield* SessionPending.find(db, admitted.id)).toBeUndefined()
|
||||
expect(yield* store.context(created.id)).toMatchObject([
|
||||
{ id: admitted.id, type: "user", text: "Replay lifecycle" },
|
||||
])
|
||||
@@ -590,8 +587,8 @@ describe("Session.create", () => {
|
||||
.pipe(Effect.orDie)).map((event) => [event.seq, event.type]),
|
||||
).toEqual([
|
||||
[0, Bus.versionedType(SessionEvent.Created.type, 1)],
|
||||
[1, Bus.versionedType(SessionEvent.InboxEnqueued.type, 1)],
|
||||
[2, Bus.versionedType(SessionEvent.InboxDelivered.type, 1)],
|
||||
[1, Bus.versionedType(SessionEvent.InputAdmitted.type, 1)],
|
||||
[2, Bus.versionedType(SessionEvent.InputPromoted.type, 1)],
|
||||
])
|
||||
}).pipe(Effect.provide(Layer.fresh(targetLayer)))
|
||||
}),
|
||||
@@ -811,7 +808,7 @@ describe("SessionTransfer", () => {
|
||||
])
|
||||
|
||||
yield* session.prompt({ sessionID, text: "Continue", resume: false })
|
||||
yield* SessionInbox.promote(db, bus, sessionID, "steer")
|
||||
yield* SessionPending.promote(db, bus, sessionID, "steer")
|
||||
|
||||
expect((yield* session.messages({ sessionID, order: "asc" })).map((message) => message.type)).toEqual([
|
||||
"user",
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
InstructionBlobTable,
|
||||
InstructionStateTable,
|
||||
SessionMessageTable,
|
||||
SessionInboxTable,
|
||||
SessionPendingTable,
|
||||
SessionTable,
|
||||
} from "@opencode-ai/core/session/sql"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
@@ -170,9 +170,9 @@ const durableState = (db: Database.Interface["db"], sessionID: SessionSchema.ID)
|
||||
.pipe(Effect.orDie),
|
||||
pending: db
|
||||
.select()
|
||||
.from(SessionInboxTable)
|
||||
.where(eq(SessionInboxTable.session_id, sessionID))
|
||||
.orderBy(asc(SessionInboxTable.enqueued_seq))
|
||||
.from(SessionPendingTable)
|
||||
.where(eq(SessionPendingTable.session_id, sessionID))
|
||||
.orderBy(asc(SessionPendingTable.admitted_seq))
|
||||
.all()
|
||||
.pipe(Effect.orDie),
|
||||
instructions: db
|
||||
@@ -225,15 +225,12 @@ it.effect("generates from fresh settled Session context without durable mutation
|
||||
const { db, bus, instructions } = yield* setup
|
||||
yield* InstructionState.prepare(db, bus, instructions, sessionID)
|
||||
const existing = SessionMessage.ID.create()
|
||||
yield* bus.publish(SessionEvent.InboxEnqueued, {
|
||||
yield* bus.publish(SessionEvent.InputAdmitted, {
|
||||
sessionID,
|
||||
inboxID: existing,
|
||||
item: { type: "user", payload: { text: "Existing durable context" }, delivery: "steer" },
|
||||
})
|
||||
yield* bus.publish(SessionEvent.InboxDelivered, {
|
||||
sessionID,
|
||||
inboxID: existing,
|
||||
inputID: existing,
|
||||
input: { type: "user", data: { text: "Existing durable context" }, delivery: "steer" },
|
||||
})
|
||||
yield* bus.publish(SessionEvent.InputPromoted, { sessionID, inputID: existing })
|
||||
const settledAssistant = SessionMessage.ID.create()
|
||||
yield* bus.publish(SessionEvent.Step.Started, {
|
||||
sessionID,
|
||||
@@ -278,10 +275,10 @@ it.effect("generates from fresh settled Session context without durable mutation
|
||||
input: {},
|
||||
executed: false,
|
||||
})
|
||||
yield* bus.publish(SessionEvent.InboxEnqueued, {
|
||||
yield* bus.publish(SessionEvent.InputAdmitted, {
|
||||
sessionID,
|
||||
inboxID: SessionMessage.ID.create(),
|
||||
item: { type: "user", payload: { text: "Queued input must remain invisible" }, delivery: "queue" },
|
||||
inputID: SessionMessage.ID.create(),
|
||||
input: { type: "user", data: { text: "Queued input must remain invisible" }, delivery: "queue" },
|
||||
})
|
||||
instruction = "Changed context"
|
||||
const before = yield* durableState(db, sessionID)
|
||||
|
||||
@@ -34,7 +34,7 @@ const it = testEffect(
|
||||
)
|
||||
|
||||
describe("Session.move", () => {
|
||||
it.effect("enqueues one move when the source directory no longer exists", () =>
|
||||
it.effect("moves a session whose source directory no longer exists", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
@@ -49,28 +49,24 @@ describe("Session.move", () => {
|
||||
|
||||
yield* session.move({ sessionID: created.id, directory: destination })
|
||||
|
||||
expect((yield* session.get(created.id)).location.directory).toBe(
|
||||
AbsolutePath.make(path.join(tmp.path, "deleted")),
|
||||
)
|
||||
expect(yield* session.inbox(created.id)).toMatchObject([
|
||||
{
|
||||
type: "move",
|
||||
delivery: "queue",
|
||||
payload: {
|
||||
location: { directory: destination },
|
||||
expect((yield* session.get(created.id)).location.directory).toBe(destination)
|
||||
const messages = yield* session.messages({ sessionID: created.id, order: "asc" })
|
||||
expect(messages).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "location-switched",
|
||||
location: { directory: destination },
|
||||
projectID: Project.ID.global,
|
||||
previous: {
|
||||
location: { directory: path.join(tmp.path, "deleted") },
|
||||
projectID: Project.ID.global,
|
||||
subpath: "",
|
||||
},
|
||||
},
|
||||
subpath: "",
|
||||
}),
|
||||
])
|
||||
|
||||
yield* session.move({ sessionID: created.id, directory: destination })
|
||||
expect(yield* session.inbox(created.id)).toHaveLength(2)
|
||||
|
||||
const steered = yield* session.create({
|
||||
location: Location.Ref.make({ directory: AbsolutePath.make(path.join(tmp.path, "other")) }),
|
||||
})
|
||||
yield* session.move({ sessionID: steered.id, directory: destination, delivery: "steer" })
|
||||
expect(yield* session.inbox(steered.id)).toMatchObject([{ type: "move", delivery: "steer" }])
|
||||
expect(yield* session.messages({ sessionID: created.id, order: "asc" })).toEqual(messages)
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -20,11 +20,11 @@ import { Money } from "@opencode-ai/schema/money"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { fromRow } from "@opencode-ai/core/session/info"
|
||||
import { SessionInbox } from "@opencode-ai/core/session/inbox"
|
||||
import { SessionPending } from "@opencode-ai/core/session/pending"
|
||||
import { Shell } from "@opencode-ai/schema/shell"
|
||||
import {
|
||||
InstructionStateTable,
|
||||
SessionInboxTable,
|
||||
SessionPendingTable,
|
||||
SessionMessageTable,
|
||||
SessionTable,
|
||||
} from "@opencode-ai/core/session/sql"
|
||||
@@ -47,7 +47,9 @@ const build = Agent.defaultID
|
||||
const assistantRow = (
|
||||
id: SessionMessage.ID,
|
||||
seq: number,
|
||||
time: { created: DateTime.Utc; completed?: DateTime.Utc } = { created },
|
||||
time: { created: DateTime.Utc; started?: DateTime.Utc; generated?: DateTime.Utc; completed?: DateTime.Utc } = {
|
||||
created,
|
||||
},
|
||||
usage?: Pick<SessionMessage.Assistant, "cost" | "tokens">,
|
||||
) => {
|
||||
const {
|
||||
@@ -81,7 +83,7 @@ describe("SessionProjector", () => {
|
||||
.run()
|
||||
const bus = yield* Bus.Service
|
||||
const inputID = SessionMessage.ID.make("msg_manual_compaction")
|
||||
yield* SessionInbox.admitCompaction(db, bus, { id: inputID, sessionID, delivery: "queue" })
|
||||
yield* SessionPending.admitCompaction(db, bus, { id: inputID, sessionID })
|
||||
|
||||
yield* bus.publish(SessionEvent.Compaction.Failed, {
|
||||
sessionID,
|
||||
@@ -89,7 +91,7 @@ describe("SessionProjector", () => {
|
||||
error: { type: "compaction.failed", message: "Auto compaction failed" },
|
||||
})
|
||||
|
||||
expect(yield* SessionInbox.find(db, inputID)).toMatchObject({ id: inputID })
|
||||
expect(yield* SessionPending.compaction(db, sessionID)).toMatchObject({ id: inputID })
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -247,29 +249,29 @@ describe("SessionProjector", () => {
|
||||
.pipe(Effect.orDie)
|
||||
const bus = yield* Bus.Service
|
||||
|
||||
yield* bus.publish(SessionEvent.InboxEnqueued, {
|
||||
yield* bus.publish(SessionEvent.InputAdmitted, {
|
||||
sessionID,
|
||||
inboxID: SessionMessage.ID.make("msg_first"),
|
||||
item: { type: "user", payload: { text: "first" }, delivery: "steer" },
|
||||
inputID: SessionMessage.ID.make("msg_first"),
|
||||
input: { type: "user", data: { text: "first" }, delivery: "steer" },
|
||||
})
|
||||
yield* bus.publish(
|
||||
SessionEvent.InboxDelivered,
|
||||
SessionEvent.InputPromoted,
|
||||
{
|
||||
sessionID,
|
||||
inboxID: SessionMessage.ID.make("msg_first"),
|
||||
inputID: SessionMessage.ID.make("msg_first"),
|
||||
},
|
||||
{ id: Event.ID.make("evt_z") },
|
||||
)
|
||||
yield* bus.publish(SessionEvent.InboxEnqueued, {
|
||||
yield* bus.publish(SessionEvent.InputAdmitted, {
|
||||
sessionID,
|
||||
inboxID: SessionMessage.ID.make("msg_second"),
|
||||
item: { type: "user", payload: { text: "second" }, delivery: "steer" },
|
||||
inputID: SessionMessage.ID.make("msg_second"),
|
||||
input: { type: "user", data: { text: "second" }, delivery: "steer" },
|
||||
})
|
||||
yield* bus.publish(
|
||||
SessionEvent.InboxDelivered,
|
||||
SessionEvent.InputPromoted,
|
||||
{
|
||||
sessionID,
|
||||
inboxID: SessionMessage.ID.make("msg_second"),
|
||||
inputID: SessionMessage.ID.make("msg_second"),
|
||||
},
|
||||
{ id: Event.ID.make("evt_a") },
|
||||
)
|
||||
@@ -320,20 +322,20 @@ describe("SessionProjector", () => {
|
||||
.pipe(Effect.orDie)
|
||||
const bus = yield* Bus.Service
|
||||
const id = SessionMessage.ID.make("msg_admitted")
|
||||
const admitted = yield* SessionInbox.admit(db, bus, {
|
||||
const admitted = yield* SessionPending.admit(db, bus, {
|
||||
id,
|
||||
sessionID,
|
||||
item: { type: "user", payload: { text: "promote me" }, delivery: "steer" },
|
||||
input: { type: "user", data: { text: "promote me" }, delivery: "steer" },
|
||||
})
|
||||
if (!admitted) return yield* Effect.die("Prompt admission failed")
|
||||
|
||||
const event = yield* bus.publish(SessionEvent.InboxDelivered, {
|
||||
const event = yield* bus.publish(SessionEvent.InputPromoted, {
|
||||
sessionID,
|
||||
inboxID: id,
|
||||
inputID: id,
|
||||
})
|
||||
|
||||
expect(
|
||||
yield* db.select().from(SessionInboxTable).where(eq(SessionInboxTable.id, id)).get().pipe(Effect.orDie),
|
||||
yield* db.select().from(SessionPendingTable).where(eq(SessionPendingTable.id, id)).get().pipe(Effect.orDie),
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
yield* db.select().from(SessionMessageTable).where(eq(SessionMessageTable.id, id)).get().pipe(Effect.orDie),
|
||||
@@ -667,12 +669,18 @@ describe("SessionProjector", () => {
|
||||
const usageUpdated = yield* service
|
||||
.subscribe(SessionEvent.UsageUpdated)
|
||||
.pipe(Stream.runHead, Effect.forkScoped({ startImmediately: true }))
|
||||
yield* service.publish(SessionEvent.Text.Started, {
|
||||
sessionID,
|
||||
assistantMessageID: SessionMessage.ID.make("msg_assistant_2"),
|
||||
ordinal: 0,
|
||||
})
|
||||
yield* service.publish(SessionEvent.Step.Ended, {
|
||||
sessionID,
|
||||
assistantMessageID: SessionMessage.ID.make("msg_assistant_2"),
|
||||
finish: "stop",
|
||||
cost: Money.USD.make(1.25),
|
||||
tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 1 } },
|
||||
generated: DateTime.makeUnsafe(0),
|
||||
})
|
||||
|
||||
const rows = yield* db
|
||||
@@ -691,7 +699,11 @@ describe("SessionProjector", () => {
|
||||
finish: "stop",
|
||||
cost: Money.USD.make(1.25),
|
||||
tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 1 } },
|
||||
time: { completed: DateTime.makeUnsafe(0) },
|
||||
time: {
|
||||
started: DateTime.makeUnsafe(0),
|
||||
generated: DateTime.makeUnsafe(0),
|
||||
completed: DateTime.makeUnsafe(0),
|
||||
},
|
||||
})
|
||||
expect(
|
||||
yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie),
|
||||
|
||||
@@ -21,8 +21,8 @@ import { Session } from "@opencode-ai/core/session"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { SessionInbox } from "@opencode-ai/core/session/inbox"
|
||||
import { SessionInboxTable, SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionPending } from "@opencode-ai/core/session/pending"
|
||||
import { SessionPendingTable, SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
|
||||
import type { LocationServices } from "@opencode-ai/core/location-services"
|
||||
@@ -100,11 +100,11 @@ const setup = Effect.gen(function* () {
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const admitted = (id: SessionMessage.ID) => Database.Service.use(({ db }) => SessionInbox.find(db, id))
|
||||
const admitted = (id: SessionMessage.ID) => Database.Service.use(({ db }) => SessionPending.find(db, id))
|
||||
const admittedCount = Database.Service.use(({ db }) =>
|
||||
db
|
||||
.select()
|
||||
.from(SessionInboxTable)
|
||||
.from(SessionPendingTable)
|
||||
.all()
|
||||
.pipe(
|
||||
Effect.orDie,
|
||||
@@ -227,13 +227,13 @@ describe("Session.prompt", () => {
|
||||
resume: false,
|
||||
})
|
||||
|
||||
expect(message.payload.text).toBe("Fix the failing tests")
|
||||
expect(message.data.text).toBe("Fix the failing tests")
|
||||
expect(yield* session.messages({ sessionID })).toEqual([])
|
||||
expect(yield* admitted(message.id)).toMatchObject({
|
||||
id: message.id,
|
||||
sessionID,
|
||||
type: "user",
|
||||
payload: { text: "Fix the failing tests" },
|
||||
data: { text: "Fix the failing tests" },
|
||||
delivery: "steer",
|
||||
})
|
||||
}),
|
||||
@@ -251,7 +251,7 @@ describe("Session.prompt", () => {
|
||||
text: "boundary",
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInbox.promote(db, bus, sessionID, "steer")
|
||||
yield* SessionPending.promote(db, bus, sessionID, "steer")
|
||||
const stale = SessionMessage.ID.make("msg_stale_assistant")
|
||||
yield* db.insert(SessionMessageTable).values(assistantRow(stale, 100)).run().pipe(Effect.orDie)
|
||||
yield* bus.publish(SessionEvent.RevertEvent.Staged, {
|
||||
@@ -268,7 +268,7 @@ describe("Session.prompt", () => {
|
||||
(row) => row.id,
|
||||
),
|
||||
).not.toContainAnyValues([boundary.id, stale])
|
||||
expect(yield* SessionInbox.find(db, boundary.id)).toBeUndefined()
|
||||
expect(yield* SessionPending.find(db, boundary.id)).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -283,7 +283,7 @@ describe("Session.prompt", () => {
|
||||
text: "boundary",
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInbox.promote(db, bus, sessionID, "steer")
|
||||
yield* SessionPending.promote(db, bus, sessionID, "steer")
|
||||
yield* bus.publish(SessionEvent.RevertEvent.Staged, {
|
||||
sessionID,
|
||||
revert: { messageID: boundary.id, files: [] },
|
||||
@@ -293,11 +293,11 @@ describe("Session.prompt", () => {
|
||||
const completion = yield* session.synthetic({ sessionID, text: "stale completion" })
|
||||
|
||||
expect(wakeCalls).toEqual([])
|
||||
expect(yield* SessionInbox.find(db, completion.id)).toMatchObject({ type: "synthetic" })
|
||||
expect(yield* SessionPending.find(db, completion.id)).toMatchObject({ type: "synthetic" })
|
||||
|
||||
yield* session.revert.commit(sessionID)
|
||||
|
||||
expect(yield* SessionInbox.find(db, completion.id)).toBeUndefined()
|
||||
expect(yield* SessionPending.find(db, completion.id)).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -315,7 +315,7 @@ describe("Session.prompt", () => {
|
||||
resume: false,
|
||||
})
|
||||
|
||||
expect(message.payload.files).toEqual([
|
||||
expect(message.data.files).toEqual([
|
||||
{
|
||||
data: uri.slice(uri.indexOf(",") + 1),
|
||||
mime: "image/png",
|
||||
@@ -326,7 +326,7 @@ describe("Session.prompt", () => {
|
||||
])
|
||||
const stored = yield* admitted(message.id)
|
||||
expect(stored?.type).toBe("user")
|
||||
if (stored?.type === "user") expect(stored.payload.files).toEqual(message.payload.files)
|
||||
if (stored?.type === "user") expect(stored.data.files).toEqual(message.data.files)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -347,14 +347,14 @@ describe("Session.prompt", () => {
|
||||
resume: false,
|
||||
})
|
||||
|
||||
expect(message.payload.files).toHaveLength(1)
|
||||
expect(message.payload.files?.[0]).toMatchObject({
|
||||
expect(message.data.files).toHaveLength(1)
|
||||
expect(message.data.files?.[0]).toMatchObject({
|
||||
mime: "text/plain",
|
||||
source: { type: "uri", uri: sourceUri.href },
|
||||
name: "main.ts",
|
||||
})
|
||||
expect(
|
||||
Buffer.from(message.payload.files?.[0]?.data ?? "", "base64")
|
||||
Buffer.from(message.data.files?.[0]?.data ?? "", "base64")
|
||||
.toString("utf8")
|
||||
.replace(/\r$/, ""),
|
||||
).toBe('import { describe, expect } from "bun:test"')
|
||||
@@ -374,13 +374,13 @@ describe("Session.prompt", () => {
|
||||
resume: false,
|
||||
})
|
||||
|
||||
expect(message.payload.files).toHaveLength(1)
|
||||
expect(message.payload.files?.[0]).toMatchObject({
|
||||
expect(message.data.files).toHaveLength(1)
|
||||
expect(message.data.files?.[0]).toMatchObject({
|
||||
mime: "application/x-directory",
|
||||
source: { type: "uri", uri },
|
||||
name: "source",
|
||||
})
|
||||
expect(Buffer.from(message.payload.files?.[0]?.data ?? "", "base64").toString("utf8")).toContain(
|
||||
expect(Buffer.from(message.data.files?.[0]?.data ?? "", "base64").toString("utf8")).toContain(
|
||||
"session-prompt.test.ts",
|
||||
)
|
||||
}),
|
||||
@@ -408,7 +408,7 @@ describe("Session.prompt", () => {
|
||||
resume: false,
|
||||
})
|
||||
|
||||
expect(message.payload.files).toEqual([
|
||||
expect(message.data.files).toEqual([
|
||||
{
|
||||
data: bytes.toString("base64"),
|
||||
mime: "image/png",
|
||||
@@ -417,7 +417,7 @@ describe("Session.prompt", () => {
|
||||
},
|
||||
])
|
||||
const stored = yield* admitted(message.id)
|
||||
expect(stored?.type === "user" ? stored.payload.files : undefined).toEqual(message.payload.files)
|
||||
expect(stored?.type === "user" ? stored.data.files : undefined).toEqual(message.data.files)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -440,7 +440,7 @@ describe("Session.prompt", () => {
|
||||
resume: false,
|
||||
})
|
||||
|
||||
expect(message.payload.files).toEqual([
|
||||
expect(message.data.files).toEqual([
|
||||
{
|
||||
data: "AA==",
|
||||
mime: "image/png",
|
||||
@@ -463,7 +463,7 @@ describe("Session.prompt", () => {
|
||||
resume: false,
|
||||
})
|
||||
|
||||
expect(message.payload.files).toEqual([
|
||||
expect(message.data.files).toEqual([
|
||||
{
|
||||
data: Buffer.from("export const value = 1\n").toString("base64"),
|
||||
mime: "text/plain",
|
||||
@@ -512,20 +512,20 @@ describe("Session.prompt", () => {
|
||||
|
||||
yield* session.prompt({ sessionID, text: "First", resume: false })
|
||||
yield* session.prompt({ sessionID, text: "Second", resume: false })
|
||||
yield* SessionInbox.promote(db, bus, sessionID, "steer")
|
||||
yield* SessionPending.promote(db, bus, sessionID, "steer")
|
||||
const streamed = Array.from(yield* Fiber.join(fiber))
|
||||
|
||||
expect(streamed.map((event): [number | undefined, string] => [event.durable?.seq, event.type])).toEqual([
|
||||
[0, "session.inbox.enqueued"],
|
||||
[1, "session.inbox.enqueued"],
|
||||
[2, "session.inbox.delivered"],
|
||||
[3, "session.inbox.delivered"],
|
||||
[0, "session.input.admitted"],
|
||||
[1, "session.input.admitted"],
|
||||
[2, "session.input.promoted"],
|
||||
[3, "session.input.promoted"],
|
||||
])
|
||||
expect(
|
||||
Array.from(
|
||||
yield* publicEvents({ sessionID, after: streamed[0].durable?.seq }).pipe(Stream.take(1), Stream.runCollect),
|
||||
).map((event): [number | undefined, string] => [event.durable?.seq, event.type]),
|
||||
).toEqual([[1, "session.inbox.enqueued"]])
|
||||
).toEqual([[1, "session.input.admitted"]])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -593,12 +593,12 @@ describe("Session.prompt", () => {
|
||||
const { db } = yield* Database.Service
|
||||
const input = { sessionID, id: messageID, text: "Fix the failing tests", resume: false }
|
||||
const first = yield* session.prompt(input)
|
||||
yield* SessionInbox.promote(db, bus, sessionID, "steer")
|
||||
yield* SessionPending.promote(db, bus, sessionID, "steer")
|
||||
yield* db.delete(EventTable).where(eq(EventTable.aggregate_id, sessionID)).run().pipe(Effect.orDie)
|
||||
|
||||
const retried = yield* session.prompt(input)
|
||||
|
||||
expect(retried).toMatchObject({ id: first.id, type: "user", payload: { text: first.payload.text } })
|
||||
expect(retried).toMatchObject({ id: first.id, type: "user", data: { text: first.data.text } })
|
||||
expect(yield* session.messages({ sessionID })).toMatchObject([
|
||||
{ id: messageID, type: "user", text: "Fix the failing tests" },
|
||||
])
|
||||
@@ -613,11 +613,11 @@ describe("Session.prompt", () => {
|
||||
const { db } = yield* Database.Service
|
||||
const input = { sessionID, id: messageID, text: "Fix the failing tests", resume: false }
|
||||
yield* session.prompt(input)
|
||||
yield* SessionInbox.promote(db, bus, sessionID, "steer")
|
||||
yield* SessionPending.promote(db, bus, sessionID, "steer")
|
||||
|
||||
const retried = yield* session.prompt({ ...input, delivery: "queue" })
|
||||
|
||||
expect(retried).toMatchObject({ id: messageID, type: "user", payload: { text: input.text } })
|
||||
expect(retried).toMatchObject({ id: messageID, type: "user", data: { text: input.text } })
|
||||
expect(yield* admitted(messageID)).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
@@ -708,7 +708,7 @@ describe("Session.prompt", () => {
|
||||
expect(messages[1]).toEqual(messages[0])
|
||||
expect(yield* session.messages({ sessionID })).toEqual([])
|
||||
expect(yield* admittedCount).toBe(1)
|
||||
expect(yield* eventCount(Bus.versionedType(SessionEvent.InboxEnqueued.type, 1))).toBe(1)
|
||||
expect(yield* eventCount(Bus.versionedType(SessionEvent.InputAdmitted.type, 1))).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -726,11 +726,11 @@ describe("Session.prompt", () => {
|
||||
})
|
||||
|
||||
yield* Effect.all(
|
||||
[SessionInbox.promote(db, bus, sessionID, "steer"), SessionInbox.promote(db, bus, sessionID, "steer")],
|
||||
[SessionPending.promote(db, bus, sessionID, "steer"), SessionPending.promote(db, bus, sessionID, "steer")],
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
|
||||
expect(yield* eventCount(Bus.versionedType(SessionEvent.InboxDelivered.type, 1))).toBe(1)
|
||||
expect(yield* eventCount(Bus.versionedType(SessionEvent.InputPromoted.type, 1))).toBe(1)
|
||||
expect(yield* admitted(messageID)).toBeUndefined()
|
||||
expect(yield* session.messages({ sessionID })).toMatchObject([
|
||||
{ id: messageID, type: "user", text: "Promote once" },
|
||||
@@ -761,7 +761,11 @@ describe("Session.prompt", () => {
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
yield* bus.remove(sessionID)
|
||||
yield* db.delete(SessionInboxTable).where(eq(SessionInboxTable.session_id, sessionID)).run().pipe(Effect.orDie)
|
||||
yield* db
|
||||
.delete(SessionPendingTable)
|
||||
.where(eq(SessionPendingTable.session_id, sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* db
|
||||
.delete(SessionMessageTable)
|
||||
.where(eq(SessionMessageTable.session_id, sessionID))
|
||||
@@ -781,12 +785,12 @@ describe("Session.prompt", () => {
|
||||
expect(yield* admitted(messageID)).toMatchObject({
|
||||
id: messageID,
|
||||
type: "user",
|
||||
payload: { text: "Replay pending" },
|
||||
data: { text: "Replay pending" },
|
||||
})
|
||||
expect(yield* admitted(syntheticID)).toMatchObject({
|
||||
id: syntheticID,
|
||||
type: "synthetic",
|
||||
payload: { text: "Replay synthetic" },
|
||||
data: { text: "Replay synthetic" },
|
||||
})
|
||||
expect(yield* session.messages({ sessionID })).toEqual([])
|
||||
expect(wakeCalls).toEqual([])
|
||||
@@ -919,7 +923,7 @@ describe("Session.prompt", () => {
|
||||
const failure = yield* session.prompt({ ...input, metadata: { source: "plugin" } }).pipe(Effect.flip)
|
||||
|
||||
expect(retried).toEqual(first)
|
||||
expect(first.payload.metadata).toEqual({ source: "api" })
|
||||
expect(first.data.metadata).toEqual({ source: "api" })
|
||||
expect(failure._tag).toBe("Session.PromptConflictError")
|
||||
}),
|
||||
)
|
||||
@@ -945,14 +949,14 @@ describe("Session.prompt", () => {
|
||||
type: "synthetic",
|
||||
sessionID,
|
||||
delivery: "steer",
|
||||
payload: {
|
||||
data: {
|
||||
text: "Background work completed",
|
||||
description: "shell completion",
|
||||
metadata: { job: "shell" },
|
||||
},
|
||||
})
|
||||
|
||||
yield* SessionInbox.promote(db, bus, sessionID, "steer")
|
||||
yield* SessionPending.promote(db, bus, sessionID, "steer")
|
||||
|
||||
expect(yield* session.messages({ sessionID })).toMatchObject([
|
||||
{
|
||||
@@ -977,15 +981,15 @@ describe("Session.prompt", () => {
|
||||
const entries = yield* Effect.all([session.synthetic(input), session.synthetic(input)], {
|
||||
concurrency: "unbounded",
|
||||
})
|
||||
yield* SessionInbox.promote(database.db, bus, sessionID, "steer")
|
||||
yield* SessionPending.promote(database.db, bus, sessionID, "steer")
|
||||
const promotedRetry = yield* session.synthetic(input)
|
||||
const failure = yield* session.synthetic({ ...input, text: "Different completion" }).pipe(Effect.flip)
|
||||
|
||||
expect(entries[1]).toEqual(entries[0])
|
||||
expect(promotedRetry).toMatchObject({ id: messageID, type: "synthetic", payload: { text: "Completed" } })
|
||||
expect(promotedRetry).toMatchObject({ id: messageID, type: "synthetic", data: { text: "Completed" } })
|
||||
expect(failure).toMatchObject({ _tag: "Session.SyntheticConflictError", sessionID, inputID: messageID })
|
||||
expect(yield* admittedCount).toBe(0)
|
||||
expect(yield* eventCount(Bus.versionedType(SessionEvent.InboxEnqueued.type, 1))).toBe(1)
|
||||
expect(yield* eventCount(Bus.versionedType(SessionEvent.InputAdmitted.type, 1))).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1004,11 +1008,11 @@ describe("Session.prompt", () => {
|
||||
})
|
||||
|
||||
expect(input.delivery).toBe("queue")
|
||||
expect(yield* SessionInbox.has(db, sessionID, "input")).toBe(true)
|
||||
expect(yield* SessionInbox.promote(db, bus, sessionID, "steer")).toBe(0)
|
||||
expect(yield* SessionPending.has(db, sessionID, "input")).toBe(true)
|
||||
expect(yield* SessionPending.promote(db, bus, sessionID, "steer")).toBe(0)
|
||||
expect(yield* session.messages({ sessionID })).toEqual([])
|
||||
expect(yield* SessionInbox.promote(db, bus, sessionID, "input")).toBe(1)
|
||||
expect(yield* SessionInbox.has(db, sessionID, "input")).toBe(false)
|
||||
expect(yield* SessionPending.promote(db, bus, sessionID, "input")).toBe(1)
|
||||
expect(yield* SessionPending.has(db, sessionID, "input")).toBe(false)
|
||||
expect(yield* session.messages({ sessionID })).toMatchObject([
|
||||
{ id: input.id, type: "synthetic", text: "Queued completion" },
|
||||
])
|
||||
@@ -1034,7 +1038,7 @@ describe("Session.prompt", () => {
|
||||
resume: false,
|
||||
})
|
||||
|
||||
yield* SessionInbox.promote(db, bus, sessionID, "steer")
|
||||
yield* SessionPending.promote(db, bus, sessionID, "steer")
|
||||
|
||||
expect(
|
||||
(yield* session.messages({ sessionID, order: "asc" })).map((message) =>
|
||||
@@ -1045,11 +1049,11 @@ describe("Session.prompt", () => {
|
||||
)
|
||||
})
|
||||
|
||||
describe("Session.inbox", () => {
|
||||
describe("Session.pending", () => {
|
||||
it.effect("fails for an unknown session", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Session.Service
|
||||
expect(yield* session.inbox(Session.ID.make("ses_missing")).pipe(Effect.flip)).toMatchObject({
|
||||
expect(yield* session.pending(Session.ID.make("ses_missing")).pipe(Effect.flip)).toMatchObject({
|
||||
_tag: "Session.NotFoundError",
|
||||
})
|
||||
}),
|
||||
@@ -1071,34 +1075,34 @@ describe("Session.inbox", () => {
|
||||
})
|
||||
const second = yield* session.prompt({ sessionID, text: "Second steer", resume: false })
|
||||
|
||||
expect(yield* session.inbox(sessionID)).toMatchObject([
|
||||
expect(yield* session.pending(sessionID)).toMatchObject([
|
||||
{ id: first.id, type: "user", delivery: "steer" },
|
||||
{ id: queued.id, type: "synthetic", delivery: "queue" },
|
||||
{ id: second.id, type: "user", delivery: "steer" },
|
||||
])
|
||||
|
||||
expect(yield* SessionInbox.promote(db, bus, sessionID, "input")).toBe(2)
|
||||
expect(yield* session.inbox(sessionID)).toMatchObject([{ id: queued.id, type: "synthetic" }])
|
||||
expect(yield* SessionPending.promote(db, bus, sessionID, "input")).toBe(2)
|
||||
expect(yield* session.pending(sessionID)).toMatchObject([{ id: queued.id, type: "synthetic" }])
|
||||
|
||||
expect(yield* SessionInbox.promote(db, bus, sessionID, "input")).toBe(1)
|
||||
expect(yield* session.inbox(sessionID)).toEqual([])
|
||||
expect(yield* SessionPending.promote(db, bus, sessionID, "input")).toBe(1)
|
||||
expect(yield* session.pending(sessionID)).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("lists an unhandled compaction until it is cancelled", () =>
|
||||
it.effect("lists an unhandled compaction barrier until it settles", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* Session.Service
|
||||
const { db } = yield* Database.Service
|
||||
|
||||
const barrier = yield* session.compact({ sessionID })
|
||||
expect(yield* SessionInbox.has(db, sessionID, "any")).toBe(true)
|
||||
expect(yield* SessionInbox.has(db, sessionID, "input")).toBe(true)
|
||||
expect(yield* session.inbox(sessionID)).toMatchObject([{ id: barrier.id, type: "compaction" }])
|
||||
expect(yield* SessionPending.has(db, sessionID, "any")).toBe(true)
|
||||
expect(yield* SessionPending.has(db, sessionID, "input")).toBe(false)
|
||||
expect(yield* session.pending(sessionID)).toMatchObject([{ id: barrier.id, type: "compaction" }])
|
||||
|
||||
yield* session.cancelInbox({ sessionID, inboxID: barrier.id })
|
||||
expect(yield* SessionInbox.has(db, sessionID, "any")).toBe(false)
|
||||
expect(yield* session.inbox(sessionID)).toEqual([])
|
||||
yield* SessionPending.settleCompaction(db, { sessionID })
|
||||
expect(yield* SessionPending.has(db, sessionID, "any")).toBe(false)
|
||||
expect(yield* session.pending(sessionID)).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1115,16 +1119,16 @@ describe("Session.inbox", () => {
|
||||
resume: false,
|
||||
})
|
||||
|
||||
yield* session.cancelInbox({ sessionID, inboxID: inputID })
|
||||
yield* session.cancelPending({ sessionID, inputID })
|
||||
|
||||
expect(yield* session.inbox(sessionID)).toEqual([])
|
||||
expect(yield* eventCount(Bus.versionedType(SessionEvent.InboxCancelled.type, 1))).toBe(1)
|
||||
expect(yield* session.cancelInbox({ sessionID, inboxID: inputID }).pipe(Effect.flip)).toMatchObject({
|
||||
_tag: "Session.InboxConflictError",
|
||||
expect(yield* session.pending(sessionID)).toEqual([])
|
||||
expect(yield* eventCount(Bus.versionedType(SessionEvent.InputCancelled.type, 1))).toBe(1)
|
||||
expect(yield* session.cancelPending({ sessionID, inputID }).pipe(Effect.flip)).toMatchObject({
|
||||
_tag: "Session.PendingInputConflictError",
|
||||
sessionID,
|
||||
inboxID: inputID,
|
||||
inputID,
|
||||
})
|
||||
expect(yield* eventCount(Bus.versionedType(SessionEvent.InboxCancelled.type, 1))).toBe(1)
|
||||
expect(yield* eventCount(Bus.versionedType(SessionEvent.InputCancelled.type, 1))).toBe(1)
|
||||
|
||||
const retried = yield* session.prompt({
|
||||
id: inputID,
|
||||
@@ -1150,33 +1154,33 @@ describe("Session.inbox", () => {
|
||||
const alreadySteered = yield* session.prompt({ sessionID, text: "Already steer", resume: false })
|
||||
wakeCalls.length = 0
|
||||
|
||||
yield* session.steerInbox({ sessionID, inboxID: queued.id })
|
||||
yield* session.steerPending({ sessionID, inputID: queued.id })
|
||||
|
||||
expect(yield* session.inbox(sessionID)).toMatchObject([
|
||||
expect(yield* session.pending(sessionID)).toMatchObject([
|
||||
{ id: queued.id, delivery: "steer" },
|
||||
{ id: alreadySteered.id, delivery: "steer" },
|
||||
])
|
||||
expect(wakeCalls).toEqual([sessionID])
|
||||
expect(yield* eventCount(Bus.versionedType(SessionEvent.InboxDeliveryChanged.type, 1))).toBe(1)
|
||||
expect(yield* eventCount(Bus.versionedType(SessionEvent.InputSteered.type, 1))).toBe(1)
|
||||
|
||||
wakeCalls.length = 0
|
||||
yield* session.queueInbox({ sessionID, inboxID: queued.id })
|
||||
expect(yield* session.inbox(sessionID)).toMatchObject([
|
||||
yield* session.queuePending({ sessionID, inputID: queued.id })
|
||||
expect(yield* session.pending(sessionID)).toMatchObject([
|
||||
{ id: queued.id, delivery: "queue" },
|
||||
{ id: alreadySteered.id, delivery: "steer" },
|
||||
])
|
||||
expect(wakeCalls).toEqual([])
|
||||
expect(yield* eventCount(Bus.versionedType(SessionEvent.InboxDeliveryChanged.type, 1))).toBe(2)
|
||||
expect(yield* eventCount(Bus.versionedType(SessionEvent.InputQueued.type, 1))).toBe(1)
|
||||
|
||||
expect(yield* session.steerInbox({ sessionID, inboxID: alreadySteered.id }).pipe(Effect.flip)).toMatchObject({
|
||||
_tag: "Session.InboxConflictError",
|
||||
expect(yield* session.steerPending({ sessionID, inputID: alreadySteered.id }).pipe(Effect.flip)).toMatchObject({
|
||||
_tag: "Session.PendingInputConflictError",
|
||||
sessionID,
|
||||
inboxID: alreadySteered.id,
|
||||
inputID: alreadySteered.id,
|
||||
})
|
||||
yield* session.cancelInbox({ sessionID, inboxID: alreadySteered.id })
|
||||
yield* session.cancelPending({ sessionID, inputID: alreadySteered.id })
|
||||
expect(wakeCalls).toEqual([])
|
||||
expect(yield* eventCount(Bus.versionedType(SessionEvent.InboxDeliveryChanged.type, 1))).toBe(2)
|
||||
expect(yield* eventCount(Bus.versionedType(SessionEvent.InboxCancelled.type, 1))).toBe(1)
|
||||
expect(yield* eventCount(Bus.versionedType(SessionEvent.InputSteered.type, 1))).toBe(1)
|
||||
expect(yield* eventCount(Bus.versionedType(SessionEvent.InputCancelled.type, 1))).toBe(1)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -243,9 +243,9 @@ describe("SessionRunnerLLM recorded", () => {
|
||||
.orderBy(EventTable.seq)
|
||||
.all()).map((event) => event.type),
|
||||
).toEqual([
|
||||
"session.inbox.enqueued.1",
|
||||
"session.input.admitted.1",
|
||||
"session.instructions.updated.2",
|
||||
"session.inbox.delivered.1",
|
||||
"session.input.promoted.1",
|
||||
"session.step.started.1",
|
||||
"session.text.started.1",
|
||||
"session.text.ended.1",
|
||||
|
||||
@@ -266,6 +266,7 @@ test("step finish records settlement without publishing step ended", async () =>
|
||||
|
||||
expect(published.some((event) => event.type === "step.ended.2")).toBe(false)
|
||||
expect(publisher.record().finish).toMatchObject({ finish: "stop" })
|
||||
expect(publisher.record().finish).toHaveProperty("generated")
|
||||
})
|
||||
|
||||
test("content-filter finish retains failure evidence until step closeout", async () => {
|
||||
|
||||
@@ -110,28 +110,6 @@ describe("Tool", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects invalid tool definitions before installing any tools", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Tool.Service
|
||||
const error = yield* service
|
||||
.transform((draft) => {
|
||||
draft.add({ ...make(), name: "healthy", options: { codemode: false } })
|
||||
draft.add({
|
||||
name: "phone_type",
|
||||
input: Schema.Struct({}),
|
||||
execute: () => Effect.succeed({ content: "ok" }),
|
||||
options: { codemode: false },
|
||||
} as unknown as Info)
|
||||
})
|
||||
.pipe(Effect.flip)
|
||||
|
||||
expect(error).toBeInstanceOf(Tool.RegistrationError)
|
||||
expect(error.name).toBe("phone_type")
|
||||
expect(error.message).toContain('Expected string, got undefined\n at ["description"]')
|
||||
expect((yield* service.snapshot()).definitions.map((tool) => tool.name)).toEqual(["execute"])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("canonicalizes effective definitions and keeps Code Mode last", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Tool.Service
|
||||
|
||||
@@ -32,7 +32,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
import { Snapshot } from "@opencode-ai/core/snapshot"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { SessionInbox } from "@opencode-ai/core/session/inbox"
|
||||
import { SessionPending } from "@opencode-ai/core/session/pending"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
@@ -55,7 +55,7 @@ import { Tool } from "@opencode-ai/core/tool"
|
||||
import type { Info as ToolInfo } from "@opencode-ai/schema/tool"
|
||||
import {
|
||||
InstructionStateTable,
|
||||
SessionInboxTable,
|
||||
SessionPendingTable,
|
||||
SessionMessageTable,
|
||||
SessionTable,
|
||||
} from "@opencode-ai/core/session/sql"
|
||||
@@ -72,7 +72,7 @@ import { Location } from "@opencode-ai/core/location"
|
||||
import { Provider } from "@opencode-ai/core/provider"
|
||||
import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Schema, Scope, Stream } from "effect"
|
||||
import { TestClock } from "effect/testing"
|
||||
import { asc, desc, eq } from "drizzle-orm"
|
||||
import { asc, eq } from "drizzle-orm"
|
||||
import { testEffect } from "./lib/effect"
|
||||
import { agentHost, catalogHost, host } from "./plugin/host"
|
||||
import PROMPT_DEFAULT from "../src/session/runner/prompt/base.txt"
|
||||
@@ -629,7 +629,7 @@ const replaySessionProjection = (id: Session.ID) =>
|
||||
|
||||
yield* bus.remove(id)
|
||||
yield* db.delete(InstructionStateTable).where(eq(InstructionStateTable.session_id, id)).run().pipe(Effect.orDie)
|
||||
yield* db.delete(SessionInboxTable).where(eq(SessionInboxTable.session_id, id)).run().pipe(Effect.orDie)
|
||||
yield* db.delete(SessionPendingTable).where(eq(SessionPendingTable.session_id, id)).run().pipe(Effect.orDie)
|
||||
yield* db.delete(SessionMessageTable).where(eq(SessionMessageTable.session_id, id)).run().pipe(Effect.orDie)
|
||||
yield* bus.replayAll(
|
||||
recorded.map((event) => ({
|
||||
@@ -1186,7 +1186,7 @@ describe("SessionRunnerLLM", () => {
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Instructions.InitializationBlocked)
|
||||
expect(requests).toHaveLength(0)
|
||||
expect(yield* SessionInbox.has(db, sessionID, "steer")).toBe(true)
|
||||
expect(yield* SessionPending.has(db, sessionID, "steer")).toBe(true)
|
||||
expect(
|
||||
yield* db.select().from(InstructionStateTable).where(eq(InstructionStateTable.session_id, sessionID)).get(),
|
||||
).toBeUndefined()
|
||||
@@ -1210,7 +1210,6 @@ describe("SessionRunnerLLM", () => {
|
||||
yield* bus.publish(SessionEvent.Moved, {
|
||||
sessionID,
|
||||
location: Location.Ref.make({ directory: AbsolutePath.make("/moved") }),
|
||||
projectID: Project.ID.global,
|
||||
})
|
||||
expect(
|
||||
yield* db.select().from(InstructionStateTable).where(eq(InstructionStateTable.session_id, sessionID)).get(),
|
||||
@@ -1221,43 +1220,7 @@ describe("SessionRunnerLLM", () => {
|
||||
|
||||
expect(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)).toBe(true)
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(yield* SessionInbox.has(db, sessionID, "steer")).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("delivers a queued move atomically at the idle boundary", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* setup
|
||||
const bus = yield* Bus.Service
|
||||
const { db } = yield* Database.Service
|
||||
const inboxID = SessionMessage.ID.create()
|
||||
yield* SessionInbox.admit(db, bus, {
|
||||
id: inboxID,
|
||||
sessionID,
|
||||
item: {
|
||||
type: "move",
|
||||
payload: {
|
||||
location: Location.Ref.make({ directory: AbsolutePath.make("/moved") }),
|
||||
projectID: Project.ID.global,
|
||||
},
|
||||
delivery: "queue",
|
||||
},
|
||||
})
|
||||
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect((yield* session.get(sessionID)).location.directory).toBe(AbsolutePath.make("/moved"))
|
||||
expect(yield* session.inbox(sessionID)).toEqual([])
|
||||
expect(requests).toEqual([])
|
||||
expect(
|
||||
(yield* db
|
||||
.select({ type: EventTable.type })
|
||||
.from(EventTable)
|
||||
.where(eq(EventTable.aggregate_id, sessionID))
|
||||
.orderBy(desc(EventTable.seq))
|
||||
.limit(2)
|
||||
.all()).map((event) => event.type),
|
||||
).toEqual([Bus.versionedType(SessionEvent.Moved.type, 1), Bus.versionedType(SessionEvent.InboxDelivered.type, 1)])
|
||||
expect(yield* SessionPending.has(db, sessionID, "steer")).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1862,15 +1825,15 @@ describe("SessionRunnerLLM", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("runs steers before queued compaction and later queued input", () =>
|
||||
it.effect("runs one durable compaction barrier after tool settlement and before later inputs", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* setup
|
||||
currentModel = recoveryModel
|
||||
const stream = yield* TestLLM.gate
|
||||
yield* TestLLM.push(
|
||||
TestLLM.tool("call-active", "echo", { text: "active" }),
|
||||
TestLLM.text("Steer complete", "text-steer"),
|
||||
[LLMEvent.textDelta({ id: "summary", text: "durable summary" })],
|
||||
TestLLM.text("Steer complete", "text-steer"),
|
||||
TestLLM.text("Queue complete", "text-queue"),
|
||||
)
|
||||
yield* admit(session, "Active work")
|
||||
@@ -1878,7 +1841,9 @@ describe("SessionRunnerLLM", () => {
|
||||
yield* stream.started
|
||||
|
||||
const first = yield* session.compact({ sessionID })
|
||||
expect(yield* SessionInbox.find((yield* Database.Service).db, first.id)).toMatchObject({
|
||||
const second = yield* session.compact({ sessionID })
|
||||
expect(second.id).toBe(first.id)
|
||||
expect(yield* SessionPending.compaction((yield* Database.Service).db, sessionID)).toMatchObject({
|
||||
id: first.id,
|
||||
})
|
||||
expect((yield* session.messages({ sessionID })).find((message) => message.id === first.id)).toBeUndefined()
|
||||
@@ -1891,17 +1856,17 @@ describe("SessionRunnerLLM", () => {
|
||||
delivery: "queue",
|
||||
resume: false,
|
||||
})
|
||||
expect(yield* SessionInbox.has((yield* Database.Service).db, sessionID, "steer")).toBe(true)
|
||||
expect(yield* SessionPending.has((yield* Database.Service).db, sessionID, "steer")).toBe(false)
|
||||
|
||||
yield* stream.release
|
||||
yield* Fiber.join(active)
|
||||
|
||||
expect(requests).toHaveLength(4)
|
||||
expect(userTexts(requests[1])).toContain("Steer after compaction")
|
||||
expect(userTexts(requests[1])).toContain("Completion after compaction")
|
||||
expect(userTexts(requests[2])[0]).toContain("Create a new anchored summary")
|
||||
expect(userTexts(requests[1])[0]).toContain("Create a new anchored summary")
|
||||
expect(userTexts(requests[2])).toContain("Steer after compaction")
|
||||
expect(userTexts(requests[2])).toContain("Completion after compaction")
|
||||
expect(userTexts(requests[3])).toContain("Queue after compaction")
|
||||
expect(yield* SessionInbox.find((yield* Database.Service).db, first.id)).toBeUndefined()
|
||||
expect(yield* SessionPending.compaction((yield* Database.Service).db, sessionID)).toBeUndefined()
|
||||
expect((yield* session.messages({ sessionID })).find((message) => message.id === first.id)).toMatchObject({
|
||||
type: "compaction",
|
||||
status: "completed",
|
||||
@@ -1936,7 +1901,7 @@ describe("SessionRunnerLLM", () => {
|
||||
|
||||
expect(requests).toHaveLength(3)
|
||||
expect(userTexts(requests[2])).toContain("Continue after failure")
|
||||
expect(yield* SessionInbox.find((yield* Database.Service).db, compaction.id)).toBeUndefined()
|
||||
expect(yield* SessionPending.compaction((yield* Database.Service).db, sessionID)).toBeUndefined()
|
||||
expect((yield* session.messages({ sessionID })).find((message) => message.id === compaction.id)).toMatchObject({
|
||||
type: "compaction",
|
||||
status: "failed",
|
||||
@@ -1958,7 +1923,7 @@ describe("SessionRunnerLLM", () => {
|
||||
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect(yield* SessionInbox.find((yield* Database.Service).db, compaction.id)).toBeUndefined()
|
||||
expect(yield* SessionPending.compaction((yield* Database.Service).db, sessionID)).toBeUndefined()
|
||||
expect((yield* session.messages({ sessionID })).find((message) => message.id === compaction.id)).toMatchObject({
|
||||
type: "compaction",
|
||||
status: "failed",
|
||||
@@ -1973,7 +1938,7 @@ describe("SessionRunnerLLM", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("delivers steered manual compaction when the model has no context limit", () =>
|
||||
it.effect("manually compacts when the model has no context limit", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* setup
|
||||
yield* TestLLM.push(TestLLM.text("Earlier answer", "text-manual-unknown-history"))
|
||||
@@ -1981,7 +1946,7 @@ describe("SessionRunnerLLM", () => {
|
||||
|
||||
requests.length = 0
|
||||
yield* TestLLM.push(TestLLM.text("Manual summary", "text-manual-unknown-summary"))
|
||||
const compaction = yield* session.compact({ sessionID, delivery: "steer" })
|
||||
const compaction = yield* session.compact({ sessionID })
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect(requests).toHaveLength(1)
|
||||
@@ -2050,7 +2015,7 @@ describe("SessionRunnerLLM", () => {
|
||||
yield* session.interrupt(sessionID)
|
||||
|
||||
yield* Fiber.await(run)
|
||||
expect(yield* SessionInbox.find((yield* Database.Service).db, compaction.id)).toBeUndefined()
|
||||
expect(yield* SessionPending.compaction((yield* Database.Service).db, sessionID)).toBeUndefined()
|
||||
expect((yield* session.messages({ sessionID })).find((message) => message.id === compaction.id)).toMatchObject({
|
||||
type: "compaction",
|
||||
status: "failed",
|
||||
@@ -2071,7 +2036,7 @@ describe("SessionRunnerLLM", () => {
|
||||
|
||||
expect(yield* Effect.exit(session.resume(sessionID))).toMatchObject({ _tag: "Failure" })
|
||||
|
||||
expect(yield* SessionInbox.find((yield* Database.Service).db, compaction.id)).toBeUndefined()
|
||||
expect(yield* SessionPending.compaction((yield* Database.Service).db, sessionID)).toBeUndefined()
|
||||
expect((yield* session.messages({ sessionID })).find((message) => message.id === compaction.id)).toMatchObject({
|
||||
type: "compaction",
|
||||
status: "failed",
|
||||
@@ -2899,7 +2864,7 @@ describe("SessionRunnerLLM", () => {
|
||||
yield* session.interrupt(sessionID)
|
||||
expect(yield* Fiber.await(run)).toMatchObject({ _tag: "Failure" })
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(yield* SessionInbox.has(db, sessionID, "queue")).toBe(true)
|
||||
expect(yield* SessionPending.has(db, sessionID, "queue")).toBe(true)
|
||||
const resumed = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||
yield* stream.started
|
||||
yield* stream.release
|
||||
@@ -2929,7 +2894,7 @@ describe("SessionRunnerLLM", () => {
|
||||
yield* session.interrupt(sessionID)
|
||||
expect(yield* Fiber.await(run)).toMatchObject({ _tag: "Failure" })
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(yield* SessionInbox.has(db, sessionID, "steer")).toBe(true)
|
||||
expect(yield* SessionPending.has(db, sessionID, "steer")).toBe(true)
|
||||
|
||||
const resumed = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||
yield* stream.started
|
||||
@@ -3082,7 +3047,7 @@ describe("SessionRunnerLLM", () => {
|
||||
const session = yield* setup
|
||||
const bus = yield* Bus.Service
|
||||
yield* admit(session, "Recover interrupted tool")
|
||||
yield* SessionInbox.promote((yield* Database.Service).db, bus, sessionID, "steer")
|
||||
yield* SessionPending.promote((yield* Database.Service).db, bus, sessionID, "steer")
|
||||
const assistantMessageID = SessionMessage.ID.create()
|
||||
yield* bus.publish(SessionEvent.Step.Started, {
|
||||
sessionID,
|
||||
@@ -3139,7 +3104,7 @@ describe("SessionRunnerLLM", () => {
|
||||
const session = yield* setup
|
||||
const bus = yield* Bus.Service
|
||||
yield* admit(session, "Recover interrupted hosted tool")
|
||||
yield* SessionInbox.promote((yield* Database.Service).db, bus, sessionID, "steer")
|
||||
yield* SessionPending.promote((yield* Database.Service).db, bus, sessionID, "steer")
|
||||
const assistantMessageID = SessionMessage.ID.create()
|
||||
yield* bus.publish(SessionEvent.Step.Started, {
|
||||
sessionID,
|
||||
@@ -3190,7 +3155,7 @@ describe("SessionRunnerLLM", () => {
|
||||
const session = yield* setup
|
||||
const bus = yield* Bus.Service
|
||||
yield* admit(session, "Recover interrupted tool input")
|
||||
yield* SessionInbox.promote((yield* Database.Service).db, bus, sessionID, "steer")
|
||||
yield* SessionPending.promote((yield* Database.Service).db, bus, sessionID, "steer")
|
||||
const assistantMessageID = SessionMessage.ID.create()
|
||||
yield* bus.publish(SessionEvent.Step.Started, {
|
||||
sessionID,
|
||||
@@ -3243,7 +3208,7 @@ describe("SessionRunnerLLM", () => {
|
||||
const bus = yield* Bus.Service
|
||||
const defect = new Error("fail after prompt promotion")
|
||||
let fail = true
|
||||
yield* bus.project(SessionEvent.InboxDelivered, () => (fail ? Effect.die(defect) : Effect.void))
|
||||
yield* bus.project(SessionEvent.InputPromoted, () => (fail ? Effect.die(defect) : Effect.void))
|
||||
yield* admit(session, "Recover promoted input")
|
||||
|
||||
expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect)
|
||||
@@ -3265,7 +3230,7 @@ describe("SessionRunnerLLM", () => {
|
||||
const session = yield* setup
|
||||
const bus = yield* Bus.Service
|
||||
yield* bus.listen((event) =>
|
||||
event.type === SessionEvent.InboxDelivered.type
|
||||
event.type === SessionEvent.InputPromoted.type
|
||||
? Effect.die("fail after prompt promotion commits")
|
||||
: Effect.void,
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
import { SessionInbox } from "@opencode-ai/core/session/inbox"
|
||||
import { SessionPending } from "@opencode-ai/core/session/pending"
|
||||
import { Skill } from "@opencode-ai/core/skill"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
@@ -71,7 +71,7 @@ describe("Session.skill", () => {
|
||||
skills: [{ id: Skill.ID.make("effect"), mention: { start: 20, end: 27, text: "/effect" } }],
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInbox.promote(database.db, bus, session.id, "steer")
|
||||
yield* SessionPending.promote(database.db, bus, session.id, "steer")
|
||||
|
||||
expect(yield* sessions.messages({ sessionID: session.id })).toContainEqual(
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -114,14 +114,14 @@ const prompt = (sessionID: Session.ID, text: string) =>
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
const messageID = SessionMessage.ID.create()
|
||||
yield* bus.publish(SessionEvent.InboxEnqueued, {
|
||||
yield* bus.publish(SessionEvent.InputAdmitted, {
|
||||
sessionID,
|
||||
inboxID: messageID,
|
||||
item: { type: "user", payload: { text }, delivery: "steer" },
|
||||
inputID: messageID,
|
||||
input: { type: "user", data: { text }, delivery: "steer" },
|
||||
})
|
||||
yield* bus.publish(SessionEvent.InboxDelivered, {
|
||||
yield* bus.publish(SessionEvent.InputPromoted, {
|
||||
sessionID,
|
||||
inboxID: messageID,
|
||||
inputID: messageID,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Project } from "@opencode-ai/schema/project"
|
||||
import { ProjectDirectories } from "@opencode-ai/schema/project-directories"
|
||||
import { PermissionV1 } from "@opencode-ai/schema/permission-v1"
|
||||
import { Prompt } from "@opencode-ai/schema/prompt"
|
||||
import { SessionInbox } from "@opencode-ai/schema/session-inbox"
|
||||
import { SessionPending } from "@opencode-ai/schema/session-pending"
|
||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
import { Workspace } from "@opencode-ai/schema/workspace"
|
||||
import { Command } from "@opencode-ai/schema/command"
|
||||
@@ -44,7 +44,7 @@ test("Core reuses the canonical shared schemas", async () => {
|
||||
coreProject,
|
||||
coreProvider,
|
||||
coreReference,
|
||||
coreSessionInbox,
|
||||
coreSessionPending,
|
||||
coreSessionMessage,
|
||||
coreSkill,
|
||||
coreSchema,
|
||||
@@ -65,7 +65,7 @@ test("Core reuses the canonical shared schemas", async () => {
|
||||
import("@opencode-ai/core/project/schema"),
|
||||
import("@opencode-ai/core/provider"),
|
||||
import("@opencode-ai/core/reference"),
|
||||
import("@opencode-ai/core/session/inbox"),
|
||||
import("@opencode-ai/core/session/pending"),
|
||||
import("@opencode-ai/core/session/message"),
|
||||
import("@opencode-ai/core/skill"),
|
||||
import("@opencode-ai/core/schema"),
|
||||
@@ -126,10 +126,10 @@ test("Core reuses the canonical shared schemas", async () => {
|
||||
[Session.ID, schemaSession.Session.ID],
|
||||
[Session.Info, schemaSession.Session.Info],
|
||||
[Session.ListAnchor, schemaSession.Session.ListAnchor],
|
||||
[coreSessionInbox.Delivery, SessionInbox.Delivery],
|
||||
[coreSessionInbox.Item, SessionInbox.Item],
|
||||
[coreSessionInbox.User, SessionInbox.User],
|
||||
[coreSessionInbox.Synthetic, SessionInbox.Synthetic],
|
||||
[coreSessionPending.Delivery, SessionPending.Delivery],
|
||||
[coreSessionPending.Message, SessionPending.Message],
|
||||
[coreSessionPending.User, SessionPending.User],
|
||||
[coreSessionPending.Synthetic, SessionPending.Synthetic],
|
||||
[coreSessionMessage.ID, SessionMessage.ID],
|
||||
[coreSessionMessage.AssistantRetry, SessionMessage.AssistantRetry],
|
||||
[coreSessionMessage.AgentSelected, SessionMessage.AgentSelected],
|
||||
|
||||
@@ -669,8 +669,8 @@ describe("ShellTool", () => {
|
||||
return withSession(tmp.path, (registry) =>
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
const admitted = yield* bus.subscribe(SessionEvent.InboxEnqueued).pipe(
|
||||
Stream.filter((event) => event.data.sessionID === sessionID && event.data.item.type === "synthetic"),
|
||||
const admitted = yield* bus.subscribe(SessionEvent.InputAdmitted).pipe(
|
||||
Stream.filter((event) => event.data.sessionID === sessionID && event.data.input.type === "synthetic"),
|
||||
Stream.runHead,
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
@@ -684,7 +684,7 @@ describe("ShellTool", () => {
|
||||
const id = ShellSchema.ID.make(shellID)
|
||||
expect((yield* shell.list()).map((info) => info.id)).toContain(id)
|
||||
expect((yield* shell.wait(id)).status).toBe("timeout")
|
||||
expect((yield* Fiber.join(admitted)).valueOrUndefined?.data.item.payload).toMatchObject({
|
||||
expect((yield* Fiber.join(admitted)).valueOrUndefined?.data.input.data).toMatchObject({
|
||||
description: idleCommand,
|
||||
metadata: {
|
||||
source: "shell",
|
||||
|
||||
@@ -19,7 +19,7 @@ import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { SessionInbox } from "@opencode-ai/core/session/inbox"
|
||||
import { SessionPending } from "@opencode-ai/core/session/pending"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
@@ -304,7 +304,7 @@ describe("SubagentTool", () => {
|
||||
agent: "reviewer",
|
||||
model: childModel,
|
||||
})
|
||||
expect((yield* sessions.inbox(child.id)).find((message) => message.type === "user")?.payload.text).toBe(
|
||||
expect((yield* sessions.pending(child.id)).find((message) => message.type === "user")?.data.text).toBe(
|
||||
"You are a subagent spawned by another session.\nreview this",
|
||||
)
|
||||
|
||||
@@ -376,8 +376,8 @@ describe("SubagentTool", () => {
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
const registry = yield* Tool.Service.pipe(Effect.provide(locations.get(parent.location)))
|
||||
const bus = yield* Bus.Service
|
||||
const admitted = yield* bus.subscribe(SessionEvent.InboxEnqueued).pipe(
|
||||
Stream.filter((event) => event.data.sessionID === parent.id && event.data.item.type === "synthetic"),
|
||||
const admitted = yield* bus.subscribe(SessionEvent.InputAdmitted).pipe(
|
||||
Stream.filter((event) => event.data.sessionID === parent.id && event.data.input.type === "synthetic"),
|
||||
Stream.take(1),
|
||||
Stream.runCollect,
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
@@ -401,10 +401,8 @@ describe("SubagentTool", () => {
|
||||
expect(settled.content).toEqual([{ type: "text", text: expect.stringContaining(`id: ${childID}`) }])
|
||||
|
||||
const admission = Array.from(yield* Fiber.join(admitted))[0]
|
||||
expect(admission?.data.item.type).toBe("synthetic")
|
||||
if (admission?.data.item.type !== "synthetic") return yield* Effect.die("Expected synthetic inbox item")
|
||||
expect(admission?.data.item.payload.text).toContain(`<subagent id="${childID}" state="completed"`)
|
||||
expect(admission?.data.item.payload).toMatchObject({
|
||||
expect(admission?.data.input.data.text).toContain(`<subagent id="${childID}" state="completed"`)
|
||||
expect(admission?.data.input.data).toMatchObject({
|
||||
description: "background review",
|
||||
metadata: {
|
||||
source: "subagent",
|
||||
@@ -414,7 +412,7 @@ describe("SubagentTool", () => {
|
||||
},
|
||||
})
|
||||
const database = yield* Database.Service
|
||||
yield* SessionInbox.promote(database.db, bus, parent.id, "steer")
|
||||
yield* SessionPending.promote(database.db, bus, parent.id, "steer")
|
||||
const synthetic = (yield* sessions.context(parent.id)).filter((message) => message.type === "synthetic")
|
||||
expect(synthetic).toHaveLength(1)
|
||||
expect(synthetic[0]?.text).toContain(`<subagent id="${childID}" state="completed"`)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DiagramCanvas, DiagramCanvasSizeError, type DiagramCanvasCell } from ".
|
||||
|
||||
describe("DiagramCanvas", () => {
|
||||
test("rejects canvases that exceed the rendering budget", () => {
|
||||
expect(() => new DiagramCanvas(1_000, 251)).toThrow(DiagramCanvasSizeError)
|
||||
expect(() => new DiagramCanvas(2_000, 1_000)).toThrow(DiagramCanvasSizeError)
|
||||
})
|
||||
|
||||
test("rejects invalid canvas dimensions", () => {
|
||||
@@ -38,12 +38,6 @@ describe("DiagramCanvas", () => {
|
||||
expect(stringWidth(canvas.toString())).toBe(4)
|
||||
})
|
||||
|
||||
test("does not emit partial wide graphemes", () => {
|
||||
const clipped = new DiagramCanvas<"label">(1, 1)
|
||||
clipped.setText(0, 0, "界", "label")
|
||||
expect(clipped.toString()).toBe("")
|
||||
})
|
||||
|
||||
test("keeps custom measurement for ASCII text", () => {
|
||||
let measurements = 0
|
||||
const canvas = new DiagramCanvas<"label">(5, 1, {
|
||||
|
||||
@@ -40,7 +40,7 @@ export interface DiagramCanvasRunOptions<Style extends string, Metadata extends
|
||||
trimBottom?: boolean
|
||||
}
|
||||
|
||||
const MAX_DIAGRAM_CELLS = 250_000
|
||||
const MAX_DIAGRAM_CELLS = 1_000_000
|
||||
|
||||
export class DiagramCanvasSizeError extends Error {
|
||||
constructor(
|
||||
@@ -122,6 +122,7 @@ export class DiagramCanvas<Style extends string, Metadata extends object = objec
|
||||
merge: boolean,
|
||||
): void {
|
||||
if (y < 0 || y >= this.cells.length || x < 0 || x >= this.cells[y]!.length) return
|
||||
|
||||
const incoming = { char, style, ...metadata } as DiagramCanvasCell<Style, Metadata>
|
||||
const cell = merge ? (this.mergeCell?.(this.cells[y]![x]!, incoming) ?? incoming) : incoming
|
||||
this.cells[y]![x] = cell
|
||||
@@ -150,10 +151,6 @@ export class DiagramCanvas<Style extends string, Metadata extends object = objec
|
||||
let offset = 0
|
||||
for (const grapheme of diagramTextGraphemes(text)) {
|
||||
const width = Math.max(1, this.measure(grapheme))
|
||||
if (x + offset < 0 || x + offset + width > this.width) {
|
||||
offset += width
|
||||
continue
|
||||
}
|
||||
this.setCell(x + offset, y, grapheme, style, metadataAt(x + offset))
|
||||
for (let continuation = 1; continuation < width; continuation++) {
|
||||
this.setCell(x + offset + continuation, y, "", style, metadataAt(x + offset + continuation))
|
||||
|
||||
@@ -30,7 +30,6 @@ const BOX_NODE_RE = new RegExp(`^(${ID_RE})\\[(.+)\\]$`)
|
||||
const ID_ONLY_RE = new RegExp(`^${ID_RE}$`)
|
||||
const EXPLICIT_NODE_SHAPE_RE = new RegExp(`^${ID_RE}(?:\\[|\\(|\\{)`)
|
||||
const CIRCLE_NODE_RE = new RegExp(`^${ID_RE}\\(\\(.+\\)\\)$`)
|
||||
const MAX_FLOWCHART_LINE_LENGTH = 10_000
|
||||
const EDGE_OPERATOR_RE =
|
||||
/(-\.(?!->)(.+?)\.(?:->|-))|(--|==|-\.)\s+(.+?)\s+(-->|==>|\.->|-\.->|\.-)|(<-->|-->|==>|-\.->|---|~~~)\s*(?:\|([^|]*)\|\s*)?/dg
|
||||
|
||||
@@ -253,9 +252,6 @@ export function parseMermaidFlowchartDiagram(content: string): FlowchartDiagram
|
||||
|
||||
for (const source of meaningfulNumberedMermaidLines(content)) {
|
||||
const line = source.text
|
||||
if (line.length > MAX_FLOWCHART_LINE_LENGTH) {
|
||||
throw new MermaidSyntaxError("flowchart", source.lineNumber, line, "Flowchart statement is too long")
|
||||
}
|
||||
if (hasInternalStatementSeparator(line)) throw new MermaidSyntaxError("flowchart", source.lineNumber, line)
|
||||
const header = line.match(FLOWCHART_HEADER_RE)
|
||||
if (header) {
|
||||
|
||||
@@ -37,8 +37,6 @@ interface PreparedDiagram {
|
||||
|
||||
export interface MermaidMarkdownRendererOptions {
|
||||
compact?: boolean
|
||||
/** Fold horizontal flowcharts that exceed this width. Defaults to 120 columns. */
|
||||
layoutMaxWidth?: number
|
||||
colors?: {
|
||||
text?: ColorInput
|
||||
primary?: ColorInput
|
||||
@@ -103,19 +101,11 @@ class StaticDiagramRenderable extends TextRenderable {
|
||||
}
|
||||
}
|
||||
|
||||
function prepareDiagram(
|
||||
kind: DiagramKind,
|
||||
source: string,
|
||||
options: MermaidMarkdownRendererOptions,
|
||||
layoutMaxWidth: number,
|
||||
): PreparedDiagram {
|
||||
function prepareDiagram(kind: DiagramKind, source: string, options: MermaidMarkdownRendererOptions): PreparedDiagram {
|
||||
const colors = options.colors ?? {}
|
||||
switch (kind) {
|
||||
case "flowchart": {
|
||||
const grid = drawFlowchartDiagramGrid(parseMermaidFlowchartDiagram(source), {
|
||||
compact: options.compact,
|
||||
layoutMaxWidth,
|
||||
})
|
||||
const grid = drawFlowchartDiagramGrid(parseMermaidFlowchartDiagram(source), { compact: options.compact })
|
||||
const size = grid.getTextSize({ trimTop: true, trimBottom: true })
|
||||
return {
|
||||
kind,
|
||||
@@ -202,12 +192,9 @@ export function createMermaidCodeBlockRenderer(
|
||||
// OpenTUI's default block ID is the stable identity available for this fence across streaming updates.
|
||||
const key = context.defaultRender()?.id
|
||||
const options = typeof input === "function" ? input() : input
|
||||
const configuredMaxWidth =
|
||||
options.layoutMaxWidth === undefined ? 120 : Math.max(1, Math.trunc(options.layoutMaxWidth))
|
||||
const layoutMaxWidth = Math.min(configuredMaxWidth, Math.max(1, Math.trunc(ctx.width)))
|
||||
|
||||
try {
|
||||
const prepared = prepareDiagram(kind, token.text, options, layoutMaxWidth)
|
||||
const prepared = prepareDiagram(kind, token.text, options)
|
||||
const diagram = new StaticDiagramRenderable(ctx, prepared)
|
||||
if (key) claimLastGood(key, prepared, diagram, lastGood)
|
||||
return diagram
|
||||
|
||||
@@ -23,7 +23,6 @@ const ELSE_RE = /^else(?:\s+(.+))?$/i
|
||||
const LOOP_RE = /^loop\s+(.+)$/i
|
||||
const AUTONUMBER_RE = /^autonumber(?:\s+(\d+)(?:\s+(\d+))?)?$/i
|
||||
const UNSUPPORTED_BIDIRECTIONAL_MESSAGE_RE = /<<-{1,2}>>/
|
||||
const MESSAGE_OPERATOR_RE = /-->>|->>|--x|-x|--\)|-\)|-->|->/
|
||||
const CSS_COLOR_NAMES = new Set([
|
||||
"black",
|
||||
"white",
|
||||
@@ -238,9 +237,6 @@ export function parseMermaidSequenceDiagram(content: string): SequenceDiagram {
|
||||
const arrow = messageMatch[2]!
|
||||
const activationMarker = messageMatch[3]!
|
||||
const to = stripQuotes(messageMatch[4]!)
|
||||
if (MESSAGE_OPERATOR_RE.test(from) || MESSAGE_OPERATOR_RE.test(to)) {
|
||||
throw new MermaidSyntaxError("sequence", source.lineNumber, line)
|
||||
}
|
||||
const message: SequenceMessage = {
|
||||
from,
|
||||
to,
|
||||
|
||||
@@ -176,9 +176,6 @@ export function parseMermaidStateDiagram(content: string): StateDiagram {
|
||||
if (transitionMatch) {
|
||||
const rawFrom = transitionMatch[1]!
|
||||
const rawTo = transitionMatch[2]!
|
||||
if (rawFrom.includes("-->") || rawTo.includes("-->")) {
|
||||
throw new MermaidSyntaxError("state", source.lineNumber, line)
|
||||
}
|
||||
const from = normalizeStateDiagramEndpoint(rawFrom, "from", parentId)
|
||||
const to = normalizeStateDiagramEndpoint(rawTo, "to", parentId)
|
||||
ensureState(states, from, rawFrom === "[*]" ? "●" : from, rawFrom === "[*]" ? "start" : "state", parentId)
|
||||
|
||||
@@ -41,11 +41,6 @@ describe("parser diagnostics", () => {
|
||||
expect(diagram.edges).toHaveLength(1)
|
||||
})
|
||||
|
||||
test("rejects pathological flowchart statements before parsing edge operators", () => {
|
||||
const statement = "-.a".repeat(4_000)
|
||||
expect(() => parseMermaidFlowchartDiagram(`flowchart LR\n${statement}`)).toThrow("Flowchart statement is too long")
|
||||
})
|
||||
|
||||
test("exposes structured syntax errors through top-level rendering", () => {
|
||||
try {
|
||||
renderSequenceDiagram(`sequenceDiagram
|
||||
@@ -67,11 +62,6 @@ describe("parser diagnostics", () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("rejects chained sequence and state transitions instead of creating phantom endpoints", () => {
|
||||
expect(() => parseMermaidSequenceDiagram("sequenceDiagram\n A->>B->>C: hello")).toThrow(MermaidSyntaxError)
|
||||
expect(() => parseMermaidStateDiagram("stateDiagram-v2\n A-->B-->C")).toThrow(MermaidSyntaxError)
|
||||
})
|
||||
|
||||
test("reports unclosed state constructs at their opening line", () => {
|
||||
expect(() =>
|
||||
parseMermaidStateDiagram(`stateDiagram-v2
|
||||
|
||||
@@ -289,28 +289,6 @@ flowchart TB
|
||||
expect(frame).not.toMatch(/<\/?i>|<br|events persist|mcp stdio/)
|
||||
})
|
||||
|
||||
test("folds a horizontal flowchart to fit the Markdown viewport", async () => {
|
||||
const testRenderer = await createTestRenderer({ width: 160, height: 30 })
|
||||
renderer = testRenderer.renderer
|
||||
const markdown = new MarkdownRenderable(renderer, {
|
||||
id: "markdown-horizontal-flowchart",
|
||||
content: `\`\`\`mermaid
|
||||
flowchart LR
|
||||
A["per TURN<br/>fresh sandbox each turn"] --- B["per SESSION/thread<br/>one sandbox per Slack thread"] --- C["per REPO<br/>threads share a sandbox"] --- D["GLOBAL registry<br/>(upstream today: hardwired at boot)"]
|
||||
\`\`\``,
|
||||
syntaxStyle,
|
||||
renderNode: createMermaidMarkdownRenderer(renderer),
|
||||
})
|
||||
|
||||
renderer.root.add(markdown)
|
||||
await renderMarkdown(markdown, testRenderer.renderOnce)
|
||||
|
||||
const diagram = markdown.getChildren()[0] as CodeRenderable
|
||||
expect(diagram.scrollWidth).toBeLessThanOrEqual(diagram.width)
|
||||
expect(diagram.scrollWidth).toBeLessThanOrEqual(120)
|
||||
expect(testRenderer.captureCharFrame()).toContain("GLOBAL registry")
|
||||
})
|
||||
|
||||
test("renders a Mermaid state fence inside MarkdownRenderable", async () => {
|
||||
const testRenderer = await createTestRenderer({ width: 80, height: 14 })
|
||||
renderer = testRenderer.renderer
|
||||
|
||||
@@ -16,7 +16,7 @@ import type {
|
||||
ReferenceInfo,
|
||||
SessionInfo,
|
||||
SessionMessageInfo,
|
||||
SessionInboxInfo,
|
||||
SessionPendingInfo,
|
||||
ShellInfo,
|
||||
SkillInfo,
|
||||
VcsInfo,
|
||||
@@ -70,7 +70,7 @@ export interface Data {
|
||||
cost(sessionID: string): number
|
||||
status(sessionID: string): "idle" | "running"
|
||||
readonly pending: {
|
||||
list(sessionID: string): SessionInboxInfo[]
|
||||
list(sessionID: string): SessionPendingInfo[]
|
||||
sync(sessionID: string): Promise<void>
|
||||
invalidate(sessionID: string): void
|
||||
}
|
||||
|
||||
+279
-255
@@ -1670,32 +1670,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"type": "string"
|
||||
},
|
||||
"workspaceID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^wrk"
|
||||
}
|
||||
]
|
||||
},
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Session.Inbox.Delivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["directory"],
|
||||
"additionalProperties": false
|
||||
"$ref": "#/components/schemas/Location.Ref"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1732,7 +1707,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.User"
|
||||
"$ref": "#/components/schemas/SessionPending.User"
|
||||
}
|
||||
},
|
||||
"required": ["data"],
|
||||
@@ -1846,7 +1821,8 @@
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Session.Inbox.Delivery"
|
||||
"type": "string",
|
||||
"enum": ["steer", "queue"]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -1902,7 +1878,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.User"
|
||||
"$ref": "#/components/schemas/SessionPending.User"
|
||||
}
|
||||
},
|
||||
"required": ["data"],
|
||||
@@ -2056,7 +2032,8 @@
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Session.Inbox.Delivery"
|
||||
"type": "string",
|
||||
"enum": ["steer", "queue"]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -2223,7 +2200,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.Synthetic"
|
||||
"$ref": "#/components/schemas/SessionPending.Synthetic"
|
||||
}
|
||||
},
|
||||
"required": ["data"],
|
||||
@@ -2322,7 +2299,8 @@
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Session.Inbox.Delivery"
|
||||
"type": "string",
|
||||
"enum": ["steer", "queue"]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -2476,7 +2454,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.Compaction"
|
||||
"$ref": "#/components/schemas/SessionPending.Compaction"
|
||||
}
|
||||
},
|
||||
"required": ["data"],
|
||||
@@ -2555,16 +2533,6 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"delivery": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Session.Inbox.Delivery"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -3032,10 +3000,10 @@
|
||||
"summary": "Get session context"
|
||||
}
|
||||
},
|
||||
"/api/session/{sessionID}/inbox": {
|
||||
"/api/session/{sessionID}/pending": {
|
||||
"get": {
|
||||
"tags": ["session"],
|
||||
"operationId": "v2.session.inbox.list",
|
||||
"operationId": "v2.session.pending.list",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sessionID",
|
||||
@@ -3063,7 +3031,7 @@
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.Info"
|
||||
"$ref": "#/components/schemas/SessionPending.Info"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3104,14 +3072,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "List durable enqueued session work not yet delivered, ordered by enqueue sequence. Includes user, synthetic, compaction, and move items.",
|
||||
"summary": "List session inbox"
|
||||
"description": "List durable admitted session work not yet visible in projected history, ordered by admission. Includes unpromoted user and synthetic inputs and unhandled compaction barriers. The runner owns consumption; items disappear once promoted or handled.",
|
||||
"summary": "List pending session work"
|
||||
}
|
||||
},
|
||||
"/api/session/{sessionID}/inbox/{inboxID}": {
|
||||
"/api/session/{sessionID}/pending/{inputID}": {
|
||||
"delete": {
|
||||
"tags": ["session"],
|
||||
"operationId": "v2.session.inbox.cancel",
|
||||
"operationId": "v2.session.pending.cancel",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sessionID",
|
||||
@@ -3127,7 +3095,7 @@
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "inboxID",
|
||||
"name": "inputID",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
@@ -3186,14 +3154,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Cancel an inbox item that has not yet been delivered.",
|
||||
"summary": "Cancel inbox input"
|
||||
"description": "Cancel an input that has not yet been promoted into session history.",
|
||||
"summary": "Cancel pending input"
|
||||
}
|
||||
},
|
||||
"/api/session/{sessionID}/inbox/{inboxID}/steer": {
|
||||
"/api/session/{sessionID}/pending/{inputID}/steer": {
|
||||
"post": {
|
||||
"tags": ["session"],
|
||||
"operationId": "v2.session.inbox.steer",
|
||||
"operationId": "v2.session.pending.steer",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sessionID",
|
||||
@@ -3209,7 +3177,7 @@
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "inboxID",
|
||||
"name": "inputID",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
@@ -3268,14 +3236,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Change a queued inbox item to steer delivery and wake session execution.",
|
||||
"summary": "Steer queued item"
|
||||
"description": "Change a queued input to steer delivery and wake session execution.",
|
||||
"summary": "Steer queued input"
|
||||
}
|
||||
},
|
||||
"/api/session/{sessionID}/inbox/{inboxID}/queue": {
|
||||
"/api/session/{sessionID}/pending/{inputID}/queue": {
|
||||
"post": {
|
||||
"tags": ["session"],
|
||||
"operationId": "v2.session.inbox.queue",
|
||||
"operationId": "v2.session.pending.queue",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sessionID",
|
||||
@@ -3291,7 +3259,7 @@
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "inboxID",
|
||||
"name": "inputID",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
@@ -3350,8 +3318,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Change a steered inbox item to queued delivery.",
|
||||
"summary": "Queue steered item"
|
||||
"description": "Change a pending steer to queued delivery.",
|
||||
"summary": "Queue pending steer"
|
||||
}
|
||||
},
|
||||
"/api/session/{sessionID}/instructions/entries": {
|
||||
@@ -3964,7 +3932,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Interrupt active execution owned by this OpenCode process. Idle interruption is a no-op. When continue=true, execution resumes if durable inbox work remains after interruption.",
|
||||
"description": "Interrupt active execution owned by this OpenCode process. Idle interruption is a no-op. When continue=true, execution resumes if durable pending work remains after interruption.",
|
||||
"summary": "Interrupt session execution"
|
||||
}
|
||||
},
|
||||
@@ -8534,7 +8502,7 @@
|
||||
"/api/fs/find": {
|
||||
"get": {
|
||||
"tags": ["filesystem"],
|
||||
"operationId": "v2.fs.get",
|
||||
"operationId": "v2.fs.find",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "location",
|
||||
@@ -13983,10 +13951,6 @@
|
||||
"required": ["_tag", "sessionID", "messageID", "message"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Session.Inbox.Delivery": {
|
||||
"type": "string",
|
||||
"enum": ["steer", "queue"]
|
||||
},
|
||||
"PromptInput.FileAttachment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -14019,7 +13983,7 @@
|
||||
"required": ["id"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Session.Inbox.UserPayload": {
|
||||
"SessionPending.UserData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
@@ -14050,7 +14014,7 @@
|
||||
"required": ["text"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Session.Inbox.User": {
|
||||
"SessionPending.User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -14076,14 +14040,15 @@
|
||||
"type": "string",
|
||||
"enum": ["user"]
|
||||
},
|
||||
"payload": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.UserPayload"
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/SessionPending.UserData"
|
||||
},
|
||||
"delivery": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.Delivery"
|
||||
"type": "string",
|
||||
"enum": ["steer", "queue"]
|
||||
}
|
||||
},
|
||||
"required": ["id", "sessionID", "timeCreated", "type", "payload", "delivery"],
|
||||
"required": ["id", "sessionID", "timeCreated", "type", "data", "delivery"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"CommandNotFoundError": {
|
||||
@@ -14137,7 +14102,7 @@
|
||||
"required": ["_tag", "skill", "message"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Session.Inbox.SyntheticPayload": {
|
||||
"SessionPending.SyntheticData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
@@ -14153,7 +14118,7 @@
|
||||
"required": ["text"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Session.Inbox.Synthetic": {
|
||||
"SessionPending.Synthetic": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -14179,27 +14144,18 @@
|
||||
"type": "string",
|
||||
"enum": ["synthetic"]
|
||||
},
|
||||
"payload": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.SyntheticPayload"
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/SessionPending.SyntheticData"
|
||||
},
|
||||
"delivery": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.Delivery"
|
||||
"type": "string",
|
||||
"enum": ["steer", "queue"]
|
||||
}
|
||||
},
|
||||
"required": ["id", "sessionID", "timeCreated", "type", "payload", "delivery"],
|
||||
"required": ["id", "sessionID", "timeCreated", "type", "data", "delivery"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Session.Inbox.CompactionPayload": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Session.Inbox.Compaction": {
|
||||
"SessionPending.Compaction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -14224,15 +14180,9 @@
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["compaction"]
|
||||
},
|
||||
"payload": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.CompactionPayload"
|
||||
},
|
||||
"delivery": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.Delivery"
|
||||
}
|
||||
},
|
||||
"required": ["id", "sessionID", "timeCreated", "type", "payload", "delivery"],
|
||||
"required": ["id", "sessionID", "timeCreated", "type"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ServiceUnavailableError": {
|
||||
@@ -14276,71 +14226,16 @@
|
||||
"required": ["_tag", "sessionID", "message"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Session.Inbox.MovePayload": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"$ref": "#/components/schemas/Location.Ref"
|
||||
},
|
||||
"projectID": {
|
||||
"type": "string"
|
||||
},
|
||||
"subpath": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["location", "projectID"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Session.Inbox.Move": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^msg_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"timeCreated": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["move"]
|
||||
},
|
||||
"payload": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.MovePayload"
|
||||
},
|
||||
"delivery": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.Delivery"
|
||||
}
|
||||
},
|
||||
"required": ["id", "sessionID", "timeCreated", "type", "payload", "delivery"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Session.Inbox.Info": {
|
||||
"SessionPending.Info": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Session.Inbox.User"
|
||||
"$ref": "#/components/schemas/SessionPending.User"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Session.Inbox.Synthetic"
|
||||
"$ref": "#/components/schemas/SessionPending.Synthetic"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Session.Inbox.Compaction"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Session.Inbox.Move"
|
||||
"$ref": "#/components/schemas/SessionPending.Compaction"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -14707,7 +14602,7 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "location", "projectID"],
|
||||
"required": ["sessionID", "location"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
@@ -14936,7 +14831,7 @@
|
||||
"required": ["id", "created", "type", "durable", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"session.inbox.delivered": {
|
||||
"session.input.promoted": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -14955,7 +14850,7 @@
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["session.inbox.delivered"]
|
||||
"enum": ["session.input.promoted"]
|
||||
},
|
||||
"durable": {
|
||||
"type": "object",
|
||||
@@ -14993,7 +14888,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"inboxID": {
|
||||
"inputID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
@@ -15002,14 +14897,14 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "inboxID"],
|
||||
"required": ["sessionID", "inputID"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "created", "type", "durable", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Session.Inbox.UserPayload1": {
|
||||
"SessionPending.UserData1": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
@@ -15040,7 +14935,25 @@
|
||||
"required": ["text"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Session.Inbox.SyntheticPayload1": {
|
||||
"SessionPending.UserMessage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["user"]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/SessionPending.UserData1"
|
||||
},
|
||||
"delivery": {
|
||||
"type": "string",
|
||||
"enum": ["steer", "queue"]
|
||||
}
|
||||
},
|
||||
"required": ["type", "data", "delivery"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SessionPending.SyntheticData1": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
@@ -15056,79 +14969,35 @@
|
||||
"required": ["text"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Session.Inbox.Item": {
|
||||
"SessionPending.SyntheticMessage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["synthetic"]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/SessionPending.SyntheticData1"
|
||||
},
|
||||
"delivery": {
|
||||
"type": "string",
|
||||
"enum": ["steer", "queue"]
|
||||
}
|
||||
},
|
||||
"required": ["type", "data", "delivery"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SessionPending.Message": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["user"]
|
||||
},
|
||||
"payload": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.UserPayload1"
|
||||
},
|
||||
"delivery": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.Delivery"
|
||||
}
|
||||
},
|
||||
"required": ["type", "payload", "delivery"],
|
||||
"additionalProperties": false
|
||||
"$ref": "#/components/schemas/SessionPending.UserMessage"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["synthetic"]
|
||||
},
|
||||
"payload": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.SyntheticPayload1"
|
||||
},
|
||||
"delivery": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.Delivery"
|
||||
}
|
||||
},
|
||||
"required": ["type", "payload", "delivery"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["compaction"]
|
||||
},
|
||||
"payload": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.CompactionPayload"
|
||||
},
|
||||
"delivery": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.Delivery"
|
||||
}
|
||||
},
|
||||
"required": ["type", "payload", "delivery"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["move"]
|
||||
},
|
||||
"payload": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.MovePayload"
|
||||
},
|
||||
"delivery": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.Delivery"
|
||||
}
|
||||
},
|
||||
"required": ["type", "payload", "delivery"],
|
||||
"additionalProperties": false
|
||||
"$ref": "#/components/schemas/SessionPending.SyntheticMessage"
|
||||
}
|
||||
]
|
||||
},
|
||||
"session.inbox.enqueued": {
|
||||
"session.input.admitted": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -15147,7 +15016,7 @@
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["session.inbox.enqueued"]
|
||||
"enum": ["session.input.admitted"]
|
||||
},
|
||||
"durable": {
|
||||
"type": "object",
|
||||
@@ -15185,7 +15054,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"inboxID": {
|
||||
"inputID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
@@ -15193,18 +15062,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"item": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.Item"
|
||||
"input": {
|
||||
"$ref": "#/components/schemas/SessionPending.Message"
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "inboxID", "item"],
|
||||
"required": ["sessionID", "inputID", "input"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "created", "type", "durable", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"session.inbox.cancelled": {
|
||||
"session.input.cancelled": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -15223,7 +15092,7 @@
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["session.inbox.cancelled"]
|
||||
"enum": ["session.input.cancelled"]
|
||||
},
|
||||
"durable": {
|
||||
"type": "object",
|
||||
@@ -15261,7 +15130,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"inboxID": {
|
||||
"inputID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
@@ -15270,14 +15139,14 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "inboxID"],
|
||||
"required": ["sessionID", "inputID"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "created", "type", "durable", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"session.inbox.delivery.changed": {
|
||||
"session.input.steered": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -15296,7 +15165,7 @@
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["session.inbox.delivery.changed"]
|
||||
"enum": ["session.input.steered"]
|
||||
},
|
||||
"durable": {
|
||||
"type": "object",
|
||||
@@ -15334,19 +15203,89 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"inboxID": {
|
||||
"inputID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^msg_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"delivery": {
|
||||
"$ref": "#/components/schemas/Session.Inbox.Delivery"
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "inboxID", "delivery"],
|
||||
"required": ["sessionID", "inputID"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "created", "type", "durable", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"session.input.queued": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^evt_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"created": {
|
||||
"type": "number"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["session.input.queued"]
|
||||
},
|
||||
"durable": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"aggregateID": {
|
||||
"type": "string"
|
||||
},
|
||||
"seq": {
|
||||
"type": "integer",
|
||||
"allOf": [
|
||||
{
|
||||
"minimum": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "number",
|
||||
"enum": [1]
|
||||
}
|
||||
},
|
||||
"required": ["aggregateID", "seq", "version"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"location": {
|
||||
"$ref": "#/components/schemas/Location.Ref"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inputID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^msg_"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "inputID"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
@@ -17269,6 +17208,79 @@
|
||||
"required": ["id", "created", "type", "durable", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"session.compaction.admitted": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^evt_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"created": {
|
||||
"type": "number"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["session.compaction.admitted"]
|
||||
},
|
||||
"durable": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"aggregateID": {
|
||||
"type": "string"
|
||||
},
|
||||
"seq": {
|
||||
"type": "integer",
|
||||
"allOf": [
|
||||
{
|
||||
"minimum": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "number",
|
||||
"enum": [1]
|
||||
}
|
||||
},
|
||||
"required": ["aggregateID", "seq", "version"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"location": {
|
||||
"$ref": "#/components/schemas/Location.Ref"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inputID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^msg_"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "inputID"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "created", "type", "durable", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"session.compaction.started": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -17809,16 +17821,19 @@
|
||||
"$ref": "#/components/schemas/session.forked"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.inbox.delivered"
|
||||
"$ref": "#/components/schemas/session.input.promoted"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.inbox.enqueued"
|
||||
"$ref": "#/components/schemas/session.input.admitted"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.inbox.cancelled"
|
||||
"$ref": "#/components/schemas/session.input.cancelled"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.inbox.delivery.changed"
|
||||
"$ref": "#/components/schemas/session.input.steered"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.input.queued"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.execution.started"
|
||||
@@ -17886,6 +17901,9 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/session.retry.scheduled"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.compaction.admitted"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.compaction.started"
|
||||
},
|
||||
@@ -23363,16 +23381,19 @@
|
||||
"$ref": "#/components/schemas/session.forked"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.inbox.delivered"
|
||||
"$ref": "#/components/schemas/session.input.promoted"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.inbox.enqueued"
|
||||
"$ref": "#/components/schemas/session.input.admitted"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.inbox.cancelled"
|
||||
"$ref": "#/components/schemas/session.input.cancelled"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.inbox.delivery.changed"
|
||||
"$ref": "#/components/schemas/session.input.steered"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.input.queued"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.execution.started"
|
||||
@@ -23452,6 +23473,9 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/session.retry.scheduled"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.compaction.admitted"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.compaction.started"
|
||||
},
|
||||
|
||||
@@ -47,14 +47,14 @@ export const FileSystemGroup = HttpApiGroup.make("server.fs")
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("fs.get", "/api/fs/find", {
|
||||
HttpApiEndpoint.get("fs.find", "/api/fs/find", {
|
||||
query: FindQuery,
|
||||
success: Location.response(Schema.Array(FileSystem.Entry)),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.fs.get",
|
||||
identifier: "v2.fs.find",
|
||||
summary: "Find files",
|
||||
description: "Find recursively ranked filesystem entries relative to the requested location.",
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
import { SessionTransfer } from "@opencode-ai/schema/session-transfer"
|
||||
import { SessionInbox } from "@opencode-ai/schema/session-inbox"
|
||||
import { SessionPending } from "@opencode-ai/schema/session-pending"
|
||||
import { PromptInput } from "@opencode-ai/schema/prompt-input"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
import { InstructionEntry } from "@opencode-ai/schema/instruction-entry"
|
||||
@@ -301,7 +301,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.move", "/api/session/:sessionID/move", {
|
||||
params: { sessionID: Session.ID },
|
||||
payload: Schema.Struct({ ...Location.Ref.fields, delivery: SessionInbox.Delivery.pipe(Schema.optional) }),
|
||||
payload: Location.Ref,
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [SessionNotFoundError, InvalidRequestError],
|
||||
}).annotateMerge(
|
||||
@@ -318,11 +318,11 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
payload: Schema.Struct({
|
||||
id: SessionMessage.ID.pipe(Schema.optional),
|
||||
...PromptInput.Prompt.fields,
|
||||
metadata: SessionInbox.UserPayload.fields.metadata,
|
||||
delivery: SessionInbox.Delivery.pipe(Schema.optional),
|
||||
metadata: SessionPending.UserData.fields.metadata,
|
||||
delivery: SessionPending.Delivery.pipe(Schema.optional),
|
||||
resume: Schema.Boolean.pipe(Schema.optional),
|
||||
}),
|
||||
success: Schema.Struct({ data: SessionInbox.User }),
|
||||
success: Schema.Struct({ data: SessionPending.User }),
|
||||
error: [ConflictError, InvalidRequestError, SessionNotFoundError],
|
||||
})
|
||||
.middleware(sessionLocationMiddleware)
|
||||
@@ -346,10 +346,10 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
files: PromptInput.Prompt.fields.files,
|
||||
agents: PromptInput.Prompt.fields.agents,
|
||||
skills: PromptInput.Prompt.fields.skills,
|
||||
delivery: SessionInbox.Delivery.pipe(Schema.optional),
|
||||
delivery: SessionPending.Delivery.pipe(Schema.optional),
|
||||
resume: Schema.Boolean.pipe(Schema.optional),
|
||||
}),
|
||||
success: Schema.Struct({ data: SessionInbox.User }),
|
||||
success: Schema.Struct({ data: SessionPending.User }),
|
||||
error: [ConflictError, InvalidRequestError, SessionNotFoundError, CommandNotFoundError, CommandEvaluationError],
|
||||
})
|
||||
.middleware(sessionLocationMiddleware)
|
||||
@@ -390,10 +390,10 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
text: Schema.String,
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
metadata: SessionMessage.Synthetic.fields.metadata,
|
||||
delivery: SessionInbox.Delivery.pipe(Schema.optional),
|
||||
delivery: SessionPending.Delivery.pipe(Schema.optional),
|
||||
resume: Schema.Boolean.pipe(Schema.optional),
|
||||
}),
|
||||
success: Schema.Struct({ data: SessionInbox.Synthetic }),
|
||||
success: Schema.Struct({ data: SessionPending.Synthetic }),
|
||||
error: [ConflictError, SessionNotFoundError],
|
||||
})
|
||||
.middleware(sessionLocationMiddleware)
|
||||
@@ -428,11 +428,8 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.compact", "/api/session/:sessionID/compact", {
|
||||
params: { sessionID: Session.ID },
|
||||
payload: Schema.Struct({
|
||||
id: SessionMessage.ID.pipe(Schema.optional),
|
||||
delivery: SessionInbox.Delivery.pipe(Schema.optional),
|
||||
}),
|
||||
success: Schema.Struct({ data: SessionInbox.Compaction }),
|
||||
payload: Schema.Struct({ id: SessionMessage.ID.pipe(Schema.optional) }),
|
||||
success: Schema.Struct({ data: SessionPending.Compaction }),
|
||||
error: [ConflictError, SessionNotFoundError],
|
||||
})
|
||||
.middleware(sessionLocationMiddleware)
|
||||
@@ -509,55 +506,55 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("session.inbox.list", "/api/session/:sessionID/inbox", {
|
||||
HttpApiEndpoint.get("session.pending.list", "/api/session/:sessionID/pending", {
|
||||
params: { sessionID: Session.ID },
|
||||
success: Schema.Struct({ data: Schema.Array(SessionInbox.Info) }),
|
||||
success: Schema.Struct({ data: Schema.Array(SessionPending.Info) }),
|
||||
error: SessionNotFoundError,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.inbox.list",
|
||||
summary: "List session inbox",
|
||||
identifier: "v2.session.pending.list",
|
||||
summary: "List pending session work",
|
||||
description:
|
||||
"List durable enqueued session work not yet delivered, ordered by enqueue sequence. Includes user, synthetic, compaction, and move items.",
|
||||
"List durable admitted session work not yet visible in projected history, ordered by admission. Includes unpromoted user and synthetic inputs and unhandled compaction barriers. The runner owns consumption; items disappear once promoted or handled.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.delete("session.inbox.cancel", "/api/session/:sessionID/inbox/:inboxID", {
|
||||
params: { sessionID: Session.ID, inboxID: SessionMessage.ID },
|
||||
HttpApiEndpoint.delete("session.pending.cancel", "/api/session/:sessionID/pending/:inputID", {
|
||||
params: { sessionID: Session.ID, inputID: SessionMessage.ID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [ConflictError, SessionNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.inbox.cancel",
|
||||
summary: "Cancel inbox input",
|
||||
description: "Cancel an inbox item that has not yet been delivered.",
|
||||
identifier: "v2.session.pending.cancel",
|
||||
summary: "Cancel pending input",
|
||||
description: "Cancel an input that has not yet been promoted into session history.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.inbox.steer", "/api/session/:sessionID/inbox/:inboxID/steer", {
|
||||
params: { sessionID: Session.ID, inboxID: SessionMessage.ID },
|
||||
HttpApiEndpoint.post("session.pending.steer", "/api/session/:sessionID/pending/:inputID/steer", {
|
||||
params: { sessionID: Session.ID, inputID: SessionMessage.ID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [ConflictError, SessionNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.inbox.steer",
|
||||
summary: "Steer queued item",
|
||||
description: "Change a queued inbox item to steer delivery and wake session execution.",
|
||||
identifier: "v2.session.pending.steer",
|
||||
summary: "Steer queued input",
|
||||
description: "Change a queued input to steer delivery and wake session execution.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.inbox.queue", "/api/session/:sessionID/inbox/:inboxID/queue", {
|
||||
params: { sessionID: Session.ID, inboxID: SessionMessage.ID },
|
||||
HttpApiEndpoint.post("session.pending.queue", "/api/session/:sessionID/pending/:inputID/queue", {
|
||||
params: { sessionID: Session.ID, inputID: SessionMessage.ID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [ConflictError, SessionNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.inbox.queue",
|
||||
summary: "Queue steered item",
|
||||
description: "Change a steered inbox item to queued delivery.",
|
||||
identifier: "v2.session.pending.queue",
|
||||
summary: "Queue pending steer",
|
||||
description: "Change a pending steer to queued delivery.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -660,7 +657,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
identifier: "v2.session.interrupt",
|
||||
summary: "Interrupt session execution",
|
||||
description:
|
||||
"Interrupt active execution owned by this OpenCode process. Idle interruption is a no-op. When continue=true, execution resumes if durable inbox work remains after interruption.",
|
||||
"Interrupt active execution owned by this OpenCode process. Idle interruption is a no-op. When continue=true, execution resumes if durable pending work remains after interruption.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ export { Reference } from "./reference.js"
|
||||
export { WebSearch } from "./websearch.js"
|
||||
export { Session } from "./session.js"
|
||||
export { Vcs } from "./vcs.js"
|
||||
export { SessionInbox } from "./session-inbox.js"
|
||||
export { SessionPending } from "./session-pending.js"
|
||||
export { SessionError } from "./session-error.js"
|
||||
export { SessionMessage } from "./session-message.js"
|
||||
export { SessionTransfer } from "./session-transfer.js"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export * as SessionDelivery from "./session-delivery.js"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
export const Delivery = Schema.Literals(["steer", "queue"])
|
||||
export type Delivery = typeof Delivery.Type
|
||||
@@ -6,7 +6,7 @@ import { Event } from "./event.js"
|
||||
import { FinishReason } from "./llm.js"
|
||||
import { Content } from "./tool.js"
|
||||
import { Model } from "./model.js"
|
||||
import { NonNegativeInt, PositiveInt, RelativePath } from "./schema.js"
|
||||
import { DateTimeUtcFromMillis, NonNegativeInt, PositiveInt, RelativePath } from "./schema.js"
|
||||
import { FileAttachment } from "./prompt.js"
|
||||
import { SessionID } from "./session-id.js"
|
||||
import { Location } from "./location.js"
|
||||
@@ -20,7 +20,7 @@ import { Skill as SkillSchema } from "./skill.js"
|
||||
import { Money } from "./money.js"
|
||||
import { Snapshot } from "./snapshot.js"
|
||||
import { TokenUsage } from "./token-usage.js"
|
||||
import { SessionInbox } from "./session-inbox.js"
|
||||
import { SessionPending } from "./session-pending.js"
|
||||
import { Project } from "./project.js"
|
||||
import { SessionFork } from "./session-fork.js"
|
||||
|
||||
@@ -90,7 +90,9 @@ export const Moved = Event.durable({
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
...SessionInbox.MovePayload.fields,
|
||||
location: Location.Ref,
|
||||
projectID: Project.ID.pipe(optional),
|
||||
subpath: RelativePath.pipe(optional),
|
||||
},
|
||||
})
|
||||
export type Moved = typeof Moved.Type
|
||||
@@ -152,41 +154,48 @@ export const Forked = Event.durable({
|
||||
})
|
||||
export type Forked = typeof Forked.Type
|
||||
|
||||
const InboxRef = {
|
||||
const InputRef = {
|
||||
...Base,
|
||||
inboxID: SessionMessage.ID,
|
||||
inputID: SessionMessage.ID,
|
||||
}
|
||||
|
||||
export const InboxDelivered = Event.durable({
|
||||
type: "session.inbox.delivered",
|
||||
export const InputPromoted = Event.durable({
|
||||
type: "session.input.promoted",
|
||||
...options,
|
||||
schema: InboxRef,
|
||||
schema: InputRef,
|
||||
})
|
||||
export type InboxDelivered = typeof InboxDelivered.Type
|
||||
export type InputPromoted = typeof InputPromoted.Type
|
||||
|
||||
export const InboxEnqueued = Event.durable({
|
||||
type: "session.inbox.enqueued",
|
||||
export const InputAdmitted = Event.durable({
|
||||
type: "session.input.admitted",
|
||||
...options,
|
||||
schema: {
|
||||
...InboxRef,
|
||||
item: SessionInbox.Item,
|
||||
...InputRef,
|
||||
input: SessionPending.Message,
|
||||
},
|
||||
})
|
||||
export type InboxEnqueued = typeof InboxEnqueued.Type
|
||||
export type InputAdmitted = typeof InputAdmitted.Type
|
||||
|
||||
export const InboxCancelled = Event.durable({
|
||||
type: "session.inbox.cancelled",
|
||||
export const InputCancelled = Event.durable({
|
||||
type: "session.input.cancelled",
|
||||
...options,
|
||||
schema: InboxRef,
|
||||
schema: InputRef,
|
||||
})
|
||||
export type InboxCancelled = typeof InboxCancelled.Type
|
||||
export type InputCancelled = typeof InputCancelled.Type
|
||||
|
||||
export const InboxDeliveryChanged = Event.durable({
|
||||
type: "session.inbox.delivery.changed",
|
||||
export const InputSteered = Event.durable({
|
||||
type: "session.input.steered",
|
||||
...options,
|
||||
schema: { ...InboxRef, delivery: SessionInbox.Delivery },
|
||||
schema: InputRef,
|
||||
})
|
||||
export type InboxDeliveryChanged = typeof InboxDeliveryChanged.Type
|
||||
export type InputSteered = typeof InputSteered.Type
|
||||
|
||||
export const InputQueued = Event.durable({
|
||||
type: "session.input.queued",
|
||||
...options,
|
||||
schema: InputRef,
|
||||
})
|
||||
export type InputQueued = typeof InputQueued.Type
|
||||
|
||||
export namespace Execution {
|
||||
export const Started = Event.durable({ type: "session.execution.started", ...options, schema: Base })
|
||||
@@ -300,6 +309,7 @@ export namespace Step {
|
||||
finish: FinishReason,
|
||||
cost: Money.USD,
|
||||
tokens: TokenUsage.Info,
|
||||
generated: DateTimeUtcFromMillis.pipe(optional),
|
||||
snapshot: Snapshot.ID.pipe(optional),
|
||||
files: Schema.Array(RelativePath).pipe(optional),
|
||||
},
|
||||
@@ -315,6 +325,7 @@ export namespace Step {
|
||||
error: SessionError.Error,
|
||||
cost: Money.USD.pipe(optional),
|
||||
tokens: TokenUsage.Info.pipe(optional),
|
||||
generated: DateTimeUtcFromMillis.pipe(optional),
|
||||
snapshot: Snapshot.ID.pipe(optional),
|
||||
files: Schema.Array(RelativePath).pipe(optional),
|
||||
},
|
||||
@@ -514,6 +525,16 @@ export const RetryScheduled = Event.durable({
|
||||
export type RetryScheduled = typeof RetryScheduled.Type
|
||||
|
||||
export namespace Compaction {
|
||||
export const Admitted = Event.durable({
|
||||
type: "session.compaction.admitted",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
inputID: SessionMessage.ID,
|
||||
},
|
||||
})
|
||||
export type Admitted = typeof Admitted.Type
|
||||
|
||||
export const Started = Event.durable({
|
||||
type: "session.compaction.started",
|
||||
...options,
|
||||
@@ -583,10 +604,11 @@ export const Definitions = Event.inventory(
|
||||
UsageUpdated,
|
||||
Deleted,
|
||||
Forked,
|
||||
InboxDelivered,
|
||||
InboxEnqueued,
|
||||
InboxCancelled,
|
||||
InboxDeliveryChanged,
|
||||
InputPromoted,
|
||||
InputAdmitted,
|
||||
InputCancelled,
|
||||
InputSteered,
|
||||
InputQueued,
|
||||
Execution.Started,
|
||||
Execution.Succeeded,
|
||||
Execution.Failed,
|
||||
@@ -613,6 +635,7 @@ export const Definitions = Event.inventory(
|
||||
Tool.Success,
|
||||
Tool.Failed,
|
||||
RetryScheduled,
|
||||
Compaction.Admitted,
|
||||
Compaction.Started,
|
||||
Compaction.Delta,
|
||||
Compaction.Ended,
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
export * as SessionInbox from "./session-inbox.js"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Location } from "./location.js"
|
||||
import { Project } from "./project.js"
|
||||
import { Prompt } from "./prompt.js"
|
||||
import { DateTimeUtcFromMillis, optional, RelativePath } from "./schema.js"
|
||||
import { SessionID } from "./session-id.js"
|
||||
import { SessionMessage } from "./session-message.js"
|
||||
|
||||
export const Delivery = Schema.Literals(["steer", "queue"]).annotate({ identifier: "Session.Inbox.Delivery" })
|
||||
export type Delivery = typeof Delivery.Type
|
||||
|
||||
export interface UserPayload extends Schema.Schema.Type<typeof UserPayload> {}
|
||||
export const UserPayload = Schema.Struct({
|
||||
...Prompt.fields,
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(optional),
|
||||
}).annotate({ identifier: "Session.Inbox.UserPayload" })
|
||||
|
||||
export interface SyntheticPayload extends Schema.Schema.Type<typeof SyntheticPayload> {}
|
||||
export const SyntheticPayload = Schema.Struct({
|
||||
text: Schema.String,
|
||||
description: Schema.String.pipe(optional),
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(optional),
|
||||
}).annotate({ identifier: "Session.Inbox.SyntheticPayload" })
|
||||
|
||||
export interface CompactionPayload extends Schema.Schema.Type<typeof CompactionPayload> {}
|
||||
export const CompactionPayload = Schema.Struct({}).annotate({ identifier: "Session.Inbox.CompactionPayload" })
|
||||
|
||||
export interface MovePayload extends Schema.Schema.Type<typeof MovePayload> {}
|
||||
export const MovePayload = Schema.Struct({
|
||||
location: Location.Ref,
|
||||
projectID: Project.ID,
|
||||
subpath: RelativePath.pipe(optional),
|
||||
}).annotate({ identifier: "Session.Inbox.MovePayload" })
|
||||
|
||||
const UserItem = Schema.Struct({ type: Schema.tag("user"), payload: UserPayload, delivery: Delivery })
|
||||
const SyntheticItem = Schema.Struct({ type: Schema.tag("synthetic"), payload: SyntheticPayload, delivery: Delivery })
|
||||
const CompactionItem = Schema.Struct({
|
||||
type: Schema.tag("compaction"),
|
||||
payload: CompactionPayload,
|
||||
delivery: Delivery,
|
||||
})
|
||||
const MoveItem = Schema.Struct({ type: Schema.tag("move"), payload: MovePayload, delivery: Delivery })
|
||||
|
||||
export const Item = Schema.Union([UserItem, SyntheticItem, CompactionItem, MoveItem]).pipe(
|
||||
Schema.toTaggedUnion("type"),
|
||||
Schema.annotate({ identifier: "Session.Inbox.Item" }),
|
||||
)
|
||||
export type Item = typeof Item.Type
|
||||
|
||||
const Enqueued = {
|
||||
id: SessionMessage.ID,
|
||||
sessionID: SessionID,
|
||||
timeCreated: DateTimeUtcFromMillis,
|
||||
}
|
||||
|
||||
export interface User extends Schema.Schema.Type<typeof User> {}
|
||||
export const User = Schema.Struct({ ...Enqueued, ...UserItem.fields }).annotate({ identifier: "Session.Inbox.User" })
|
||||
|
||||
export interface Synthetic extends Schema.Schema.Type<typeof Synthetic> {}
|
||||
export const Synthetic = Schema.Struct({ ...Enqueued, ...SyntheticItem.fields }).annotate({
|
||||
identifier: "Session.Inbox.Synthetic",
|
||||
})
|
||||
|
||||
export interface Compaction extends Schema.Schema.Type<typeof Compaction> {}
|
||||
export const Compaction = Schema.Struct({ ...Enqueued, ...CompactionItem.fields }).annotate({
|
||||
identifier: "Session.Inbox.Compaction",
|
||||
})
|
||||
|
||||
export interface Move extends Schema.Schema.Type<typeof Move> {}
|
||||
export const Move = Schema.Struct({ ...Enqueued, ...MoveItem.fields }).annotate({ identifier: "Session.Inbox.Move" })
|
||||
|
||||
export const Info = Schema.Union([User, Synthetic, Compaction, Move]).pipe(
|
||||
Schema.toTaggedUnion("type"),
|
||||
Schema.annotate({ identifier: "Session.Inbox.Info" }),
|
||||
)
|
||||
export type Info = typeof Info.Type
|
||||
@@ -221,6 +221,8 @@ export const Assistant = Schema.Struct({
|
||||
retry: AssistantRetry.pipe(optional),
|
||||
time: Schema.Struct({
|
||||
created: DateTimeUtcFromMillis,
|
||||
started: DateTimeUtcFromMillis.pipe(optional),
|
||||
generated: DateTimeUtcFromMillis.pipe(optional),
|
||||
completed: DateTimeUtcFromMillis.pipe(optional),
|
||||
}),
|
||||
}).annotate({ identifier: "Session.Message.Assistant" })
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
export * as SessionPending from "./session-pending.js"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { optional } from "./schema.js"
|
||||
import { Prompt } from "./prompt.js"
|
||||
import { DateTimeUtcFromMillis } from "./schema.js"
|
||||
import { SessionDelivery } from "./session-delivery.js"
|
||||
import { SessionID } from "./session-id.js"
|
||||
import { SessionMessage } from "./session-message.js"
|
||||
|
||||
export const Delivery = SessionDelivery.Delivery
|
||||
export type Delivery = SessionDelivery.Delivery
|
||||
|
||||
export interface UserData extends Schema.Schema.Type<typeof UserData> {}
|
||||
export const UserData = Schema.Struct({
|
||||
...Prompt.fields,
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(optional),
|
||||
}).annotate({ identifier: "SessionPending.UserData" })
|
||||
|
||||
export interface SyntheticData extends Schema.Schema.Type<typeof SyntheticData> {}
|
||||
export const SyntheticData = Schema.Struct({
|
||||
text: Schema.String,
|
||||
description: Schema.String.pipe(optional),
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(optional),
|
||||
}).annotate({ identifier: "SessionPending.SyntheticData" })
|
||||
|
||||
export interface UserMessage extends Schema.Schema.Type<typeof UserMessage> {}
|
||||
export const UserMessage = Schema.Struct({
|
||||
type: Schema.tag("user"),
|
||||
data: UserData,
|
||||
delivery: Delivery,
|
||||
}).annotate({ identifier: "SessionPending.UserMessage" })
|
||||
|
||||
export interface SyntheticMessage extends Schema.Schema.Type<typeof SyntheticMessage> {}
|
||||
export const SyntheticMessage = Schema.Struct({
|
||||
type: Schema.tag("synthetic"),
|
||||
data: SyntheticData,
|
||||
delivery: Delivery,
|
||||
}).annotate({ identifier: "SessionPending.SyntheticMessage" })
|
||||
|
||||
export const Message = Schema.Union([UserMessage, SyntheticMessage]).pipe(
|
||||
Schema.toTaggedUnion("type"),
|
||||
Schema.annotate({ identifier: "SessionPending.Message" }),
|
||||
)
|
||||
export type Message = typeof Message.Type
|
||||
|
||||
const Admitted = {
|
||||
id: SessionMessage.ID,
|
||||
sessionID: SessionID,
|
||||
timeCreated: DateTimeUtcFromMillis,
|
||||
}
|
||||
|
||||
export interface User extends Schema.Schema.Type<typeof User> {}
|
||||
export const User = Schema.Struct({
|
||||
...Admitted,
|
||||
...UserMessage.fields,
|
||||
}).annotate({ identifier: "SessionPending.User" })
|
||||
|
||||
export interface Synthetic extends Schema.Schema.Type<typeof Synthetic> {}
|
||||
export const Synthetic = Schema.Struct({
|
||||
...Admitted,
|
||||
...SyntheticMessage.fields,
|
||||
}).annotate({ identifier: "SessionPending.Synthetic" })
|
||||
|
||||
export interface Compaction extends Schema.Schema.Type<typeof Compaction> {}
|
||||
export const Compaction = Schema.Struct({
|
||||
...Admitted,
|
||||
type: Schema.tag("compaction"),
|
||||
}).annotate({ identifier: "SessionPending.Compaction" })
|
||||
|
||||
export const Info = Schema.Union([User, Synthetic, Compaction]).pipe(
|
||||
Schema.toTaggedUnion("type"),
|
||||
Schema.annotate({ identifier: "SessionPending.Info" }),
|
||||
)
|
||||
export type Info = typeof Info.Type
|
||||
@@ -11,7 +11,7 @@ import { Pty } from "../src/pty.js"
|
||||
import { Question } from "../src/question.js"
|
||||
import { Session } from "../src/session.js"
|
||||
import { SessionMessage } from "../src/session-message.js"
|
||||
import { SessionInbox } from "../src/session-inbox.js"
|
||||
import { SessionPending } from "../src/session-pending.js"
|
||||
import { FileDiff } from "../src/file-diff.js"
|
||||
import { Money } from "../src/money.js"
|
||||
import { Skill } from "../src/skill.js"
|
||||
@@ -47,7 +47,7 @@ describe("contract hygiene", () => {
|
||||
expect(Schema.encodeSync(Value)({ value: 1 })).toEqual({ value: "1" })
|
||||
expect(Schema.encodeSync(Value)({ value: undefined })).toEqual({})
|
||||
expect(
|
||||
Schema.encodeSync(SessionInbox.SyntheticPayload)({
|
||||
Schema.encodeSync(SessionPending.SyntheticData)({
|
||||
text: "completed",
|
||||
description: undefined,
|
||||
metadata: undefined,
|
||||
@@ -67,16 +67,16 @@ describe("contract hygiene", () => {
|
||||
).not.toHaveProperty("title")
|
||||
})
|
||||
|
||||
test("session inbox items omit the internal enqueue sequence", () => {
|
||||
test("pending session items omit the internal admission sequence", () => {
|
||||
expect(
|
||||
Schema.encodeSync(SessionInbox.Info)(
|
||||
Schema.decodeUnknownSync(SessionInbox.Info)({
|
||||
Schema.encodeSync(SessionPending.Info)(
|
||||
Schema.decodeUnknownSync(SessionPending.Info)({
|
||||
admittedSeq: 3,
|
||||
id: "msg_pending",
|
||||
sessionID: "ses_pending",
|
||||
timeCreated: 1,
|
||||
type: "user",
|
||||
payload: { text: "hello" },
|
||||
data: { text: "hello" },
|
||||
delivery: "steer",
|
||||
}),
|
||||
),
|
||||
@@ -85,7 +85,7 @@ describe("contract hygiene", () => {
|
||||
sessionID: "ses_pending",
|
||||
timeCreated: 1,
|
||||
type: "user",
|
||||
payload: { text: "hello" },
|
||||
data: { text: "hello" },
|
||||
delivery: "steer",
|
||||
})
|
||||
})
|
||||
@@ -167,17 +167,10 @@ describe("contract hygiene", () => {
|
||||
Pty.Info,
|
||||
Session.ListAnchor,
|
||||
Session.Revert,
|
||||
SessionInbox.Delivery,
|
||||
SessionInbox.UserPayload,
|
||||
SessionInbox.SyntheticPayload,
|
||||
SessionInbox.CompactionPayload,
|
||||
SessionInbox.MovePayload,
|
||||
SessionInbox.Item,
|
||||
SessionInbox.User,
|
||||
SessionInbox.Synthetic,
|
||||
SessionInbox.Compaction,
|
||||
SessionInbox.Move,
|
||||
SessionInbox.Info,
|
||||
SessionPending.UserData,
|
||||
SessionPending.SyntheticData,
|
||||
SessionPending.User,
|
||||
SessionPending.Synthetic,
|
||||
Vcs.Branch,
|
||||
Vcs.Info,
|
||||
].map((schema) => schema.ast.annotations?.identifier)
|
||||
@@ -186,21 +179,6 @@ describe("contract hygiene", () => {
|
||||
expect(new Set(identifiers).size).toBe(identifiers.length)
|
||||
})
|
||||
|
||||
test("all session inbox item types accept both delivery modes", () => {
|
||||
const decode = Schema.decodeUnknownSync(SessionInbox.Info)
|
||||
const base = { id: "msg_inbox", sessionID: "ses_inbox", timeCreated: 1 }
|
||||
const move = {
|
||||
location: { directory: "/project" },
|
||||
projectID: "global",
|
||||
}
|
||||
for (const delivery of ["steer", "queue"] as const) {
|
||||
expect(decode({ ...base, type: "user", payload: { text: "hello" }, delivery }).delivery).toBe(delivery)
|
||||
expect(decode({ ...base, type: "synthetic", payload: { text: "context" }, delivery }).delivery).toBe(delivery)
|
||||
expect(decode({ ...base, type: "compaction", payload: {}, delivery }).delivery).toBe(delivery)
|
||||
expect(decode({ ...base, type: "move", payload: move, delivery }).delivery).toBe(delivery)
|
||||
}
|
||||
})
|
||||
|
||||
test("current source limits Any to provider options and avoids mutable contract wrappers", async () => {
|
||||
const files = [...new Bun.Glob("*.ts").scanSync(new URL("../src", import.meta.url).pathname)].filter(
|
||||
(file) => !file.endsWith("-v1.ts"),
|
||||
@@ -243,7 +221,7 @@ describe("contract hygiene", () => {
|
||||
|
||||
test("reviewed session contracts use their canonical current shapes", () => {
|
||||
expect(SessionMessage.Info.ast.annotations?.identifier).toBe("Session.Message.Info")
|
||||
expect(SessionInbox.Info.ast.annotations?.identifier).toBe("Session.Inbox.Info")
|
||||
expect(SessionPending.Info.ast.annotations?.identifier).toBe("SessionPending.Info")
|
||||
expect(Money.USD).not.toBe(Money.USDPerMillionTokens)
|
||||
expect(
|
||||
FileDiff.Info.make({ file: "src/index.ts", patch: "@@", additions: 1, deletions: 0, status: "modified" }),
|
||||
|
||||
@@ -82,10 +82,11 @@ describe("public event manifest", () => {
|
||||
"session.renamed.1",
|
||||
"session.usage.recorded.1",
|
||||
"session.forked.2",
|
||||
"session.inbox.delivered.1",
|
||||
"session.inbox.enqueued.1",
|
||||
"session.inbox.cancelled.1",
|
||||
"session.inbox.delivery.changed.1",
|
||||
"session.input.promoted.1",
|
||||
"session.input.admitted.1",
|
||||
"session.input.cancelled.1",
|
||||
"session.input.steered.1",
|
||||
"session.input.queued.1",
|
||||
"session.execution.started.1",
|
||||
"session.execution.succeeded.1",
|
||||
"session.execution.failed.1",
|
||||
@@ -108,6 +109,7 @@ describe("public event manifest", () => {
|
||||
"session.reasoning.started.1",
|
||||
"session.reasoning.ended.1",
|
||||
"session.retry.scheduled.1",
|
||||
"session.compaction.admitted.1",
|
||||
"session.compaction.started.1",
|
||||
"session.compaction.ended.1",
|
||||
"session.compaction.failed.1",
|
||||
|
||||
@@ -24,6 +24,6 @@ export { Reference } from "@opencode-ai/schema/reference"
|
||||
export { WebSearch } from "@opencode-ai/schema/websearch"
|
||||
export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema"
|
||||
export { Session } from "@opencode-ai/schema/session"
|
||||
export { SessionInbox } from "@opencode-ai/schema/session-inbox"
|
||||
export { SessionPending } from "@opencode-ai/schema/session-pending"
|
||||
export { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
export { Skill } from "@opencode-ai/schema/skill"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { Location as CoreLocation } from "@opencode-ai/core/location"
|
||||
import { SessionInbox as CoreSessionInbox } from "@opencode-ai/core/session/inbox"
|
||||
import { SessionPending as CoreSessionPending } from "@opencode-ai/core/session/pending"
|
||||
import { SessionMessage as CoreSessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { Agent } from "@opencode-ai/schema/agent"
|
||||
import { Config } from "@opencode-ai/schema/config"
|
||||
@@ -10,7 +10,7 @@ import { Project } from "@opencode-ai/schema/project"
|
||||
import { Provider } from "@opencode-ai/schema/provider"
|
||||
import { WebSearch } from "@opencode-ai/schema/websearch"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
import { SessionInbox } from "@opencode-ai/schema/session-inbox"
|
||||
import { SessionPending } from "@opencode-ai/schema/session-pending"
|
||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
import { Workspace } from "@opencode-ai/schema/workspace"
|
||||
import { Api } from "@opencode-ai/server/api"
|
||||
@@ -53,8 +53,8 @@ test("re-exports canonical contracts directly from Schema", () => {
|
||||
"Reference",
|
||||
"RelativePath",
|
||||
"Session",
|
||||
"SessionInbox",
|
||||
"SessionMessage",
|
||||
"SessionPending",
|
||||
"Skill",
|
||||
"Tool",
|
||||
"WebSearch",
|
||||
@@ -69,9 +69,9 @@ test("Core and Server reuse the authoritative Schema and Protocol values", () =>
|
||||
expect(CoreProject.Current).toBe(Project.Current)
|
||||
expect(CoreProject.Directory).toBe(Project.Directory)
|
||||
expect(CoreProject.Directories).toBe(Project.Directories)
|
||||
expect(CoreSessionInbox.Item).toBe(SessionInbox.Item)
|
||||
expect(CoreSessionInbox.User).toBe(SessionInbox.User)
|
||||
expect(CoreSessionInbox.Synthetic).toBe(SessionInbox.Synthetic)
|
||||
expect(CoreSessionPending.Message).toBe(SessionPending.Message)
|
||||
expect(CoreSessionPending.User).toBe(SessionPending.User)
|
||||
expect(CoreSessionPending.Synthetic).toBe(SessionPending.Synthetic)
|
||||
expect(CoreSessionMessage.Info).toBe(SessionMessage.Info)
|
||||
expect(CoreSessionMessage.AssistantText).toBe(SessionMessage.AssistantText)
|
||||
expect(Api.groups["server.session"].identifier).toBe("server.session")
|
||||
|
||||
@@ -241,7 +241,7 @@ it.live(
|
||||
resume: false,
|
||||
})
|
||||
const context = yield* opencode.sessions.context({ sessionID: id })
|
||||
const pendingAfterAdmit = yield* opencode.sessions.inbox.list({ sessionID: id })
|
||||
const pendingAfterAdmit = yield* opencode.sessions.pending.list({ sessionID: id })
|
||||
yield* opencode.sessions.instructions.entry.put({ sessionID: id, key: "deploy-target", value: "production" })
|
||||
yield* opencode.sessions.instructions.entry.put({ sessionID: id, key: "flags", value: { beta: true } })
|
||||
const contextEntries = yield* opencode.sessions.instructions.entry.list({ sessionID: id })
|
||||
@@ -252,13 +252,13 @@ it.live(
|
||||
text: "Promote this input",
|
||||
})
|
||||
const prompted = yield* opencode.sessions.log({ sessionID: id, follow: true }).pipe(
|
||||
Stream.filter((event) => event.type === "session.inbox.delivered" && event.data.inboxID === wake.id),
|
||||
Stream.filter((event) => event.type === "session.input.promoted" && event.data.inputID === wake.id),
|
||||
Stream.runHead,
|
||||
Effect.timeout("10 seconds"),
|
||||
Effect.map(Option.getOrThrow),
|
||||
)
|
||||
const wakeContext = yield* opencode.sessions.context({ sessionID: id })
|
||||
const pendingAfterPromote = yield* opencode.sessions.inbox.list({ sessionID: id })
|
||||
const pendingAfterPromote = yield* opencode.sessions.pending.list({ sessionID: id })
|
||||
const event = yield* opencode.sessions.log({ sessionID: id }).pipe(
|
||||
Stream.filter((item) => item.type === "session.model.selected"),
|
||||
Stream.take(1),
|
||||
@@ -278,7 +278,7 @@ it.live(
|
||||
opencode.sessions.interrupt({ sessionID: missingSessionID }).pipe(Effect.flip),
|
||||
opencode.sessions.message({ sessionID: missingSessionID, messageID: modelMessage.id }).pipe(Effect.flip),
|
||||
opencode.sessions.instructions.entry.list({ sessionID: missingSessionID }).pipe(Effect.flip),
|
||||
opencode.sessions.inbox.list({ sessionID: missingSessionID }).pipe(Effect.flip),
|
||||
opencode.sessions.pending.list({ sessionID: missingSessionID }).pipe(Effect.flip),
|
||||
],
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
@@ -298,7 +298,7 @@ it.live(
|
||||
expect(pendingAfterAdmit).toContainEqual(
|
||||
expect.objectContaining({ id: admitted.id, type: "user", delivery: "steer" }),
|
||||
)
|
||||
expect(prompted.type).toBe("session.inbox.delivered")
|
||||
expect(prompted.type).toBe("session.input.promoted")
|
||||
expect(pendingAfterPromote.map((item) => item.id)).not.toContainAnyValues([admitted.id, wake.id])
|
||||
expect(wakeContext).toContainEqual(expect.objectContaining({ id: wake.id, type: "user" }))
|
||||
expect(contextEntries).toEqual([
|
||||
@@ -362,13 +362,13 @@ it.live(
|
||||
const opencode = yield* fixture.sdk.OpenCode.create()
|
||||
const id = sessionID(fixture)
|
||||
const connected = yield* Latch.make(false)
|
||||
const prompted = yield* Deferred.make<Extract<OpenCodeEvent, { type: "session.inbox.delivered" }>>()
|
||||
const prompted = yield* Deferred.make<Extract<OpenCodeEvent, { type: "session.input.promoted" }>>()
|
||||
|
||||
yield* opencode.events.subscribe().pipe(
|
||||
Stream.runForEach((event) =>
|
||||
event.type === "server.connected"
|
||||
? connected.open
|
||||
: event.type === "session.inbox.delivered" && event.data.sessionID === id
|
||||
: event.type === "session.input.promoted" && event.data.sessionID === id
|
||||
? Deferred.succeed(prompted, event).pipe(Effect.asVoid)
|
||||
: Effect.void,
|
||||
),
|
||||
|
||||
@@ -28,7 +28,7 @@ export const FileSystemHandler = HttpApiBuilder.group(Api, "server.fs", (handler
|
||||
}),
|
||||
),
|
||||
)
|
||||
.handle("fs.get", (ctx) =>
|
||||
.handle("fs.find", (ctx) =>
|
||||
response(
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.Service
|
||||
|
||||
@@ -26,7 +26,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Session.Service
|
||||
const transfer = yield* SessionTransfer.Service
|
||||
const pendingMutation = (effect: ReturnType<typeof session.cancelInbox>, conflict: string) =>
|
||||
const pendingMutation = (effect: ReturnType<typeof session.cancelPending>, conflict: string) =>
|
||||
effect.pipe(
|
||||
Effect.catchTag(
|
||||
"Session.NotFoundError",
|
||||
@@ -37,8 +37,8 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
}),
|
||||
),
|
||||
Effect.catchTag(
|
||||
"Session.InboxConflictError",
|
||||
(error) => new ConflictError({ resource: error.inboxID, message: `${conflict}: ${error.inboxID}` }),
|
||||
"Session.PendingInputConflictError",
|
||||
(error) => new ConflictError({ resource: error.inputID, message: `${conflict}: ${error.inputID}` }),
|
||||
),
|
||||
Effect.as(HttpApiSchema.NoContent.make()),
|
||||
)
|
||||
@@ -282,7 +282,6 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
sessionID: ctx.params.sessionID,
|
||||
directory: ctx.payload.directory,
|
||||
workspaceID: ctx.payload.workspaceID,
|
||||
delivery: ctx.payload.delivery,
|
||||
})
|
||||
.pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
@@ -489,26 +488,24 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
"session.compact",
|
||||
Effect.fn(function* (ctx) {
|
||||
return {
|
||||
data: yield* session
|
||||
.compact({ sessionID: ctx.params.sessionID, id: ctx.payload.id, delivery: ctx.payload.delivery })
|
||||
.pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
Effect.fail(
|
||||
new SessionNotFoundError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session not found: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
Effect.catchTag("Session.CompactionConflictError", (error) =>
|
||||
Effect.fail(
|
||||
new ConflictError({
|
||||
message: `Compaction input ID conflicts with an existing durable record: ${error.inputID}`,
|
||||
resource: error.inputID,
|
||||
}),
|
||||
),
|
||||
data: yield* session.compact({ sessionID: ctx.params.sessionID, id: ctx.payload.id }).pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
Effect.fail(
|
||||
new SessionNotFoundError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session not found: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
Effect.catchTag("Session.CompactionConflictError", (error) =>
|
||||
Effect.fail(
|
||||
new ConflictError({
|
||||
message: `Compaction input ID conflicts with an existing durable record: ${error.inputID}`,
|
||||
resource: error.inputID,
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -672,10 +669,10 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.inbox.list",
|
||||
"session.pending.list",
|
||||
Effect.fn(function* (ctx) {
|
||||
return {
|
||||
data: yield* session.inbox(ctx.params.sessionID).pipe(
|
||||
data: yield* session.pending(ctx.params.sessionID).pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
Effect.fail(
|
||||
new SessionNotFoundError({
|
||||
@@ -689,28 +686,28 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.inbox.cancel",
|
||||
"session.pending.cancel",
|
||||
Effect.fn(function* (ctx) {
|
||||
return yield* pendingMutation(
|
||||
session.cancelInbox({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID }),
|
||||
session.cancelPending({ sessionID: ctx.params.sessionID, inputID: ctx.params.inputID }),
|
||||
"Pending input can no longer be cancelled",
|
||||
)
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.inbox.steer",
|
||||
"session.pending.steer",
|
||||
Effect.fn(function* (ctx) {
|
||||
return yield* pendingMutation(
|
||||
session.steerInbox({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID }),
|
||||
session.steerPending({ sessionID: ctx.params.sessionID, inputID: ctx.params.inputID }),
|
||||
"Pending input is no longer queued",
|
||||
)
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.inbox.queue",
|
||||
"session.pending.queue",
|
||||
Effect.fn(function* (ctx) {
|
||||
return yield* pendingMutation(
|
||||
session.queueInbox({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID }),
|
||||
session.queuePending({ sessionID: ctx.params.sessionID, inputID: ctx.params.inputID }),
|
||||
"Pending input is no longer a steer",
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -21,9 +21,7 @@ export const ShellHandler = HttpApiBuilder.group(Api, "server.shell", (handlers)
|
||||
Effect.fn(function* (ctx) {
|
||||
const shell = yield* Shell.Service
|
||||
const location = yield* Location.Service
|
||||
return yield* response(
|
||||
shell.create({ ...ctx.payload, cwd: ctx.payload.cwd || location.directory }).pipe(Effect.orDie),
|
||||
)
|
||||
return yield* response(shell.create({ ...ctx.payload, cwd: ctx.payload.cwd || location.directory }))
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
|
||||
@@ -40,5 +40,11 @@ export const ServerOptions = Schema.Struct({
|
||||
fff: Schema.optional(Schema.Boolean),
|
||||
}),
|
||||
),
|
||||
mcp: Schema.optional(
|
||||
Schema.Struct({
|
||||
/** Set false on runtimes that cannot spawn child processes; local (stdio) MCP servers report failed instead of connecting. */
|
||||
stdio: Schema.optional(Schema.Boolean),
|
||||
}),
|
||||
),
|
||||
})
|
||||
export type ServerOptions = typeof ServerOptions.Type
|
||||
|
||||
@@ -120,6 +120,7 @@ function makeRoutes<AuthError, AuthServices>(
|
||||
name: options.app?.name ?? "opencode",
|
||||
version: options.app?.version ?? "unknown",
|
||||
},
|
||||
stdio: options.mcp?.stdio,
|
||||
}),
|
||||
],
|
||||
[PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)],
|
||||
|
||||
@@ -5,13 +5,12 @@ import { ConfigPluginSource } from "@opencode-ai/core/config/plugin/source"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { sqliteLayer } from "@opencode-ai/core/database/sqlite.workerd"
|
||||
import type { DurableObjectStorage } from "@opencode-ai/core/database/sqlite.workerd"
|
||||
import { EnvironmentUnavailable } from "@opencode-ai/core/environment/unavailable"
|
||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { FileSystemSearch } from "@opencode-ai/core/filesystem/search"
|
||||
import { Pty } from "@opencode-ai/core/pty"
|
||||
import { Shell } from "@opencode-ai/core/shell"
|
||||
import { Snapshot } from "@opencode-ai/core/snapshot"
|
||||
import { Vcs } from "@opencode-ai/core/vcs"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/util/cross-spawn-spawner"
|
||||
import type { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { ServerFetch } from "./fetch"
|
||||
import type { ServerOptions } from "./options"
|
||||
@@ -25,11 +24,10 @@ import type { ServerOptions } from "./options"
|
||||
* - Watcher and fff are disabled through their existing option flags; pty, fff,
|
||||
* shell-parser, photon, and process-lock native modules resolve to inert
|
||||
* stubs under the `workerd` bundle condition.
|
||||
* - Bare locations use a typed no-execution-plane process spawner; FileSystem,
|
||||
* FileSystemSearch, and Pty fail with a clear defect until a remote sandbox
|
||||
* backs them; Snapshot and Vcs degrade to no-op results.
|
||||
* - Shell, FileSystem, FileSystemSearch, and Pty fail with a clear defect until
|
||||
* a remote sandbox backs them; Snapshot and Vcs degrade to no-op results.
|
||||
* - Config is injected as a string (no filesystem); plugin discovery is
|
||||
* precompiled-only, and stdio MCP reports the same no-plane failure as Shell.
|
||||
* precompiled-only and MCP is restricted to remote transports.
|
||||
*
|
||||
* Bundle with the `workerd` condition, e.g.
|
||||
* `bun build src/workerd.ts --conditions=workerd --target=node`
|
||||
@@ -69,6 +67,9 @@ export function serverOptions(options: Options): ServerOptions {
|
||||
events: { persist: true },
|
||||
config: { content: options.config?.content },
|
||||
models: options.models,
|
||||
// No child processes on workerd: local (stdio) MCP servers report failed
|
||||
// instead of connecting; remote transports work unchanged.
|
||||
mcp: { stdio: false },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,9 +77,9 @@ export function serverOptions(options: Options): ServerOptions {
|
||||
export function replacements(options: Options): LayerNode.Replacements {
|
||||
return [
|
||||
[Database.node, Database.configuredClient(sqliteLayer({ storage: options.storage }))],
|
||||
[CrossSpawnSpawner.node, EnvironmentUnavailable.layer],
|
||||
[Snapshot.node, Snapshot.noopLayer],
|
||||
[Vcs.node, vcsLayer],
|
||||
[Shell.node, shellLayer],
|
||||
[FileSystem.node, fileSystemLayer],
|
||||
[FileSystemSearch.node, fileSystemSearchLayer],
|
||||
[Pty.node, ptyLayer],
|
||||
@@ -101,6 +102,22 @@ const vcsLayer = Layer.succeed(
|
||||
}),
|
||||
)
|
||||
|
||||
// Shell commands need a real process; queries for unknown IDs stay typed while
|
||||
// creation is a defect until a remote sandbox backs them.
|
||||
const shellLayer = Layer.succeed(
|
||||
Shell.Service,
|
||||
Shell.Service.of({
|
||||
name: () => Effect.succeed("unsupported"),
|
||||
create: () => unavailable("Shell.create"),
|
||||
list: () => Effect.succeed([]),
|
||||
get: (id) => Effect.fail(new Shell.NotFoundError({ id })),
|
||||
wait: (id) => Effect.fail(new Shell.NotFoundError({ id })),
|
||||
timeout: (id) => Effect.fail(new Shell.NotFoundError({ id })),
|
||||
output: (id) => Effect.fail(new Shell.NotFoundError({ id })),
|
||||
remove: (id) => Effect.fail(new Shell.NotFoundError({ id })),
|
||||
}),
|
||||
)
|
||||
|
||||
// The Location-scoped filesystem has no local worktree to serve until a remote
|
||||
// sandbox backs it.
|
||||
const fileSystemLayer = Layer.succeed(
|
||||
|
||||
@@ -182,7 +182,7 @@ export function DevToolsBar() {
|
||||
location: sessionLocation,
|
||||
status: data.session.status(sessionID),
|
||||
pending: data.session.pending.list(sessionID),
|
||||
inboxIDs: data.session.input.list(sessionID),
|
||||
inputIDs: data.session.input.list(sessionID),
|
||||
permissions: data.session.permission.list(sessionID) ?? [],
|
||||
forms: data.session.form.list(sessionID) ?? [],
|
||||
}
|
||||
|
||||
@@ -141,11 +141,11 @@ export function DialogSessionList() {
|
||||
const option = (session: SessionInfo, category: string) => {
|
||||
const directory = session.location.directory
|
||||
const project = data.project.get(session.projectID)
|
||||
const root = session.subpath ? path.resolve(directory, ...session.subpath.split("/").map(() => "..")) : directory
|
||||
const relative = path.relative(project?.canonical ?? root, root)
|
||||
const footer =
|
||||
relative.startsWith("..") || path.isAbsolute(relative)
|
||||
? Locale.truncate(path.basename(relative), 25)
|
||||
const relative = path.relative(project?.canonical ?? directory, directory)
|
||||
const footer = allProjects()
|
||||
? Locale.truncate(projectName(project, directory) ?? "", 20)
|
||||
: relative.startsWith("..") || path.isAbsolute(relative)
|
||||
? Locale.truncate(path.basename(directory), 20)
|
||||
: undefined
|
||||
const slot = sessionTabs.enabled() ? undefined : slotByID.get(session.id)
|
||||
const deleting = toDelete() === session.id
|
||||
@@ -227,9 +227,8 @@ export function DialogSessionList() {
|
||||
</box>
|
||||
}
|
||||
onMove={() => setToDelete(undefined)}
|
||||
onSelect={(option, activation) => {
|
||||
if (activation.shift) route.navigate({ type: "session", sessionID: option.value })
|
||||
else sessionTabs.replace(option.value)
|
||||
onSelect={(option) => {
|
||||
route.navigate({ type: "session", sessionID: option.value })
|
||||
dialog.clear()
|
||||
}}
|
||||
actions={[
|
||||
|
||||
@@ -353,7 +353,7 @@ export function Autocomplete(props: {
|
||||
const result = await (
|
||||
input.visible === "directory"
|
||||
? client.api.file.list({ location: requestLocation })
|
||||
: client.api.file.get({ query: base, limit: 20, location: requestLocation })
|
||||
: client.api.file.find({ query: base, limit: 20, location: requestLocation })
|
||||
).then(
|
||||
(result) => result,
|
||||
() => undefined,
|
||||
|
||||
@@ -61,7 +61,7 @@ import { useLocation } from "../../context/location"
|
||||
import { Keymap, type KeymapCommand } from "../../context/keymap"
|
||||
import { abbreviateHome } from "../../runtime"
|
||||
import { Slot } from "../../plugin/render"
|
||||
import type { SessionInbox } from "@opencode-ai/schema/session-inbox"
|
||||
import type { SessionPending } from "@opencode-ai/schema/session-pending"
|
||||
import {
|
||||
deduplicatePromptImages,
|
||||
preserveMentionlessPromptAttachments,
|
||||
@@ -1047,7 +1047,7 @@ export function Prompt(props: PromptProps) {
|
||||
})
|
||||
|
||||
let submitting = false
|
||||
async function submit(delivery: SessionInbox.Delivery = "steer") {
|
||||
async function submit(delivery: SessionPending.Delivery = "steer") {
|
||||
// Prevent overlapping invocations (e.g. a double-pressed Enter, or the
|
||||
// input's native onSubmit racing another dispatch). Without this guard,
|
||||
// a second call slips past the empty-input check before the first call
|
||||
@@ -1063,7 +1063,7 @@ export function Prompt(props: PromptProps) {
|
||||
}
|
||||
}
|
||||
|
||||
async function submitInner(delivery: SessionInbox.Delivery) {
|
||||
async function submitInner(delivery: SessionPending.Delivery) {
|
||||
// IME: double-defer may fire before onContentChange flushes the last
|
||||
// composed character (e.g. Korean hangul) to the store, so read
|
||||
// plainText directly and sync before any downstream reads.
|
||||
|
||||
@@ -72,13 +72,14 @@ function fadeTitleColor(color: RGBA, background: RGBA, index: number, length: nu
|
||||
return opacity === 0 ? color : tint(color, background, opacity)
|
||||
}
|
||||
|
||||
export function createMarquee(animations: () => boolean) {
|
||||
function createMarquee(animations: () => boolean) {
|
||||
const [offset, setOffset] = createSignal(0)
|
||||
const [active, setActive] = createSignal<string>()
|
||||
const leading = createAnimatable({ opacity: 0 }, { enabled: animations, transition: tween({ duration: 0.25 }) })
|
||||
let delay: ReturnType<typeof setTimeout> | undefined
|
||||
let interval: ReturnType<typeof setInterval> | undefined
|
||||
let cycleWidth = 0
|
||||
let returning = false
|
||||
|
||||
const clear = () => {
|
||||
if (delay) clearTimeout(delay)
|
||||
@@ -90,15 +91,17 @@ export function createMarquee(animations: () => boolean) {
|
||||
interval = setInterval(() => setOffset((value) => (value + 1) % cycleWidth), MARQUEE_INTERVAL)
|
||||
}
|
||||
const enter = (sessionID: string, title: string, width: number) => {
|
||||
if (!marqueeOverflows(title, width)) {
|
||||
reset()
|
||||
return
|
||||
}
|
||||
if (active() === sessionID) return
|
||||
if (active() === sessionID && !returning) return
|
||||
clear()
|
||||
if (active() === sessionID) {
|
||||
returning = false
|
||||
return scroll()
|
||||
}
|
||||
if (!marqueeOverflows(title, width)) return
|
||||
cycleWidth = marqueeCycleWidth(title)
|
||||
setActive(sessionID)
|
||||
setOffset(0)
|
||||
returning = false
|
||||
leading.jump({ opacity: 0 })
|
||||
delay = setTimeout(() => {
|
||||
setOffset(1)
|
||||
@@ -108,10 +111,27 @@ export function createMarquee(animations: () => boolean) {
|
||||
}
|
||||
const leave = (sessionID: string) => {
|
||||
if (active() !== sessionID) return
|
||||
reset()
|
||||
clear()
|
||||
if (offset() === 0) {
|
||||
setActive(undefined)
|
||||
return
|
||||
}
|
||||
returning = true
|
||||
interval = setInterval(() => {
|
||||
setOffset((value) => {
|
||||
const next = (value + 1) % cycleWidth
|
||||
if (next !== 0) return next
|
||||
clear()
|
||||
returning = false
|
||||
setActive(undefined)
|
||||
leading.animate({ opacity: 0 })
|
||||
return 0
|
||||
})
|
||||
}, MARQUEE_INTERVAL)
|
||||
}
|
||||
const reset = () => {
|
||||
clear()
|
||||
returning = false
|
||||
setActive(undefined)
|
||||
setOffset(0)
|
||||
leading.jump({ opacity: 0 })
|
||||
@@ -121,7 +141,7 @@ export function createMarquee(animations: () => boolean) {
|
||||
return { offset, active, enter, leave, reset, leading: () => leading.value().opacity }
|
||||
}
|
||||
|
||||
export function createTabMarquee(animations: () => boolean) {
|
||||
function createTabMarquee(animations: () => boolean) {
|
||||
const [hovered, setHovered] = createSignal<string>()
|
||||
const marquee = createMarquee(animations)
|
||||
let hoverClear: ReturnType<typeof setTimeout> | undefined
|
||||
@@ -139,21 +159,11 @@ export function createTabMarquee(animations: () => boolean) {
|
||||
marquee.leave(sessionID)
|
||||
})
|
||||
}
|
||||
const leaveHovered = () => {
|
||||
const sessionID = hovered()
|
||||
if (sessionID) leave(sessionID)
|
||||
}
|
||||
const reset = () => {
|
||||
if (hoverClear) clearTimeout(hoverClear)
|
||||
hoverClear = undefined
|
||||
setHovered(undefined)
|
||||
marquee.reset()
|
||||
}
|
||||
onCleanup(() => {
|
||||
if (hoverClear) clearTimeout(hoverClear)
|
||||
})
|
||||
|
||||
return { ...marquee, hovered, enter, leave, leaveHovered, reset }
|
||||
return { ...marquee, hovered, enter, leave }
|
||||
}
|
||||
|
||||
function TabContextMenu(props: { state: TabContextMenuState; tabs: SessionTabsController; onClose: () => void }) {
|
||||
@@ -338,7 +348,6 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
position="relative"
|
||||
paddingTop={1}
|
||||
backgroundColor={theme.background.default}
|
||||
onMouseOut={marquee.leaveHovered}
|
||||
>
|
||||
<scrollbox ref={(element) => (scroll = element)} flexGrow={1} scrollbarOptions={{ visible: false }}>
|
||||
<box flexShrink={0} flexDirection="column" gap={1}>
|
||||
@@ -347,7 +356,6 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
const selected = () => activeID() === tab.sessionID
|
||||
const status = createMemo(() => itemStatus(tab))
|
||||
const [sweepLevel, setSweepLevel] = createSignal(0)
|
||||
const [closeHovered, setCloseHovered] = createSignal(false)
|
||||
const session = createMemo(() => data.session.get(tab.sessionID))
|
||||
const project = createMemo(() => {
|
||||
const value = session()
|
||||
@@ -355,17 +363,18 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
})
|
||||
const numberWidth = () => 2
|
||||
const restingTitleWidth = () => Math.max(1, width() - numberWidth() - 2)
|
||||
const hoveredTitleWidth = () => Math.max(1, restingTitleWidth() - 1)
|
||||
const titleWidth = () => (hovered() === tab.sessionID ? hoveredTitleWidth() : restingTitleWidth())
|
||||
const titleWidth = () => Math.max(1, restingTitleWidth() - (hovered() === tab.sessionID ? 1 : 0))
|
||||
const title = () => tab.title ?? "Untitled session"
|
||||
const scrolling = () => marquee.active() === tab.sessionID
|
||||
const scrolling = () => marquee.active() === tab.sessionID && marquee.offset() > 0
|
||||
const visibleTitle = createMemo(() =>
|
||||
scrolling()
|
||||
? marqueeText(title(), titleWidth(), marquee.offset())
|
||||
: Locale.takeWidth(title(), titleWidth()),
|
||||
)
|
||||
const visibleTitleParts = createMemo(() => Locale.graphemes(visibleTitle()))
|
||||
const titleFades = createMemo(() => marqueeOverflows(title(), titleWidth()) && titleWidth() > FADE_WIDTH)
|
||||
const titleFades = createMemo(
|
||||
() => marqueeOverflows(title(), restingTitleWidth()) && titleWidth() > FADE_WIDTH,
|
||||
)
|
||||
const detail = createMemo(() => {
|
||||
const value = session()
|
||||
return Locale.takeWidth(projectName(project(), value?.location.directory) ?? "", titleWidth())
|
||||
@@ -447,7 +456,7 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
position="relative"
|
||||
flexDirection="column"
|
||||
backgroundColor={background()}
|
||||
onMouseOver={() => marquee.enter(tab.sessionID, title(), hoveredTitleWidth())}
|
||||
onMouseOver={() => marquee.enter(tab.sessionID, title(), restingTitleWidth())}
|
||||
onMouseOut={() => marquee.leave(tab.sessionID)}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button === RIGHT_MOUSE_BUTTON) {
|
||||
@@ -463,7 +472,7 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
marquee.enter(tab.sessionID, title(), hoveredTitleWidth())
|
||||
marquee.enter(tab.sessionID, title(), restingTitleWidth())
|
||||
setDragging(tab.sessionID)
|
||||
}}
|
||||
onMouseUp={(event) => {
|
||||
@@ -575,10 +584,8 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
right={1}
|
||||
zIndex={2}
|
||||
width={1}
|
||||
fg={closeHovered() ? theme.text.default : theme.text.subdued}
|
||||
fg={theme.text.subdued}
|
||||
selectable={false}
|
||||
onMouseOver={() => setCloseHovered(true)}
|
||||
onMouseOut={() => setCloseHovered(false)}
|
||||
onMouseUp={(event) => {
|
||||
if (event.button === RIGHT_MOUSE_BUTTON) return
|
||||
if (hovered() !== tab.sessionID) return
|
||||
@@ -586,7 +593,7 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
tabs.close(tab.sessionID)
|
||||
}}
|
||||
>
|
||||
{hovered() === tab.sessionID ? "✕" : ""}
|
||||
{hovered() === tab.sessionID ? "×" : ""}
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
@@ -835,7 +842,6 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
position="relative"
|
||||
flexDirection="row"
|
||||
zIndex={1}
|
||||
onMouseOut={marquee.leaveHovered}
|
||||
renderAfter={function (buffer) {
|
||||
const x = Math.max(0, this.screenX)
|
||||
const y = this.screenY + this.height
|
||||
@@ -891,9 +897,8 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
const numberWidth = () => 2
|
||||
// Hovering reveals the close mark, so the title's right bound shifts left of it.
|
||||
const restingTitleWidth = () => Math.max(1, width() - 1 - numberWidth())
|
||||
const hoveredTitleWidth = () => Math.max(1, restingTitleWidth() - 2)
|
||||
const availableTitleWidth = () => (hovered() === tab.sessionID ? hoveredTitleWidth() : restingTitleWidth())
|
||||
const scrolling = () => marquee.active() === tab.sessionID
|
||||
const availableTitleWidth = () => Math.max(1, restingTitleWidth() - (hovered() === tab.sessionID ? 2 : 0))
|
||||
const scrolling = () => marquee.active() === tab.sessionID && marquee.offset() > 0
|
||||
const visibleTitle = createMemo(() =>
|
||||
scrolling()
|
||||
? marqueeText(title(), availableTitleWidth(), marquee.offset())
|
||||
@@ -901,7 +906,7 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
)
|
||||
const visibleTitleParts = createMemo(() => Locale.graphemes(visibleTitle()))
|
||||
const titleFades = createMemo(
|
||||
() => marqueeOverflows(title(), availableTitleWidth()) && availableTitleWidth() > FADE_WIDTH,
|
||||
() => marqueeOverflows(title(), restingTitleWidth()) && availableTitleWidth() > FADE_WIDTH,
|
||||
)
|
||||
const foreground = () => {
|
||||
if (hovered() === tab.sessionID) return theme.text.default
|
||||
@@ -924,7 +929,6 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
}
|
||||
// The running sweep's level under the number cell, reported by the pulse renderable.
|
||||
const [sweepLevel, setSweepLevel] = createSignal(0)
|
||||
const [closeHovered, setCloseHovered] = createSignal(false)
|
||||
const numberColor = () => {
|
||||
const feedback = feedbackColor()
|
||||
if (feedback) return feedback
|
||||
@@ -953,7 +957,7 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
position="relative"
|
||||
flexDirection="row"
|
||||
backgroundColor={background()}
|
||||
onMouseOver={() => marquee.enter(tab.sessionID, title(), hoveredTitleWidth())}
|
||||
onMouseOver={() => marquee.enter(tab.sessionID, title(), restingTitleWidth())}
|
||||
onMouseOut={() => marquee.leave(tab.sessionID)}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button === RIGHT_MOUSE_BUTTON) {
|
||||
@@ -968,7 +972,7 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
marquee.enter(tab.sessionID, title(), hoveredTitleWidth())
|
||||
marquee.enter(tab.sessionID, title(), restingTitleWidth())
|
||||
setDragging(tab.sessionID)
|
||||
}}
|
||||
onMouseUp={(event) => {
|
||||
@@ -1022,10 +1026,8 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
right={1}
|
||||
zIndex={2}
|
||||
width={1}
|
||||
fg={closeHovered() ? theme.text.default : closeColor()}
|
||||
fg={closeColor()}
|
||||
selectable={false}
|
||||
onMouseOver={() => setCloseHovered(true)}
|
||||
onMouseOut={() => setCloseHovered(false)}
|
||||
onMouseUp={(event) => {
|
||||
if (event.button === RIGHT_MOUSE_BUTTON) return
|
||||
// The close mark only renders while hovered; without motion events a click can
|
||||
@@ -1035,7 +1037,7 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
tabs.close(tab === NEW_SESSION_TAB ? undefined : tab.sessionID)
|
||||
}}
|
||||
>
|
||||
{hovered() === tab.sessionID ? "✕" : ""}
|
||||
{hovered() === tab.sessionID ? "×" : ""}
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
|
||||
@@ -241,7 +241,7 @@ export const Definitions = {
|
||||
"dialog.select.page_down": keybind("pagedown", "Move down one page in dialog"),
|
||||
"dialog.select.home": keybind("home", "Move to first dialog item"),
|
||||
"dialog.select.end": keybind("end", "Move to last dialog item"),
|
||||
"dialog.select.submit": keybind("return,shift+return,linefeed", "Submit selected dialog item"),
|
||||
"dialog.select.submit": keybind("return", "Submit selected dialog item"),
|
||||
"dialog.prompt.submit": keybind("return", "Submit dialog prompt"),
|
||||
"dialog.project_copy.generate": keybind("tab", "Generate project copy name"),
|
||||
"dialog.move_session.new": keybind("ctrl+m", "New project copy"),
|
||||
|
||||
@@ -218,7 +218,7 @@ export const Definitions = {
|
||||
"dialog.select.page_down": keybind("pagedown", "Move down one page in dialog"),
|
||||
"dialog.select.home": keybind("home", "Move to first dialog item"),
|
||||
"dialog.select.end": keybind("end", "Move to last dialog item"),
|
||||
"dialog.select.submit": keybind("return,shift+return,linefeed", "Submit selected dialog item"),
|
||||
"dialog.select.submit": keybind("return", "Submit selected dialog item"),
|
||||
"dialog.prompt.submit": keybind("return", "Submit dialog prompt"),
|
||||
"dialog.project_copy.generate": keybind("tab", "Generate project copy name"),
|
||||
"dialog.move_session.new": keybind("ctrl+m", "New project copy"),
|
||||
|
||||
@@ -25,7 +25,7 @@ import type {
|
||||
SessionMessageAssistantText,
|
||||
SessionMessageAssistantTool,
|
||||
SessionInfo,
|
||||
SessionInboxInfo,
|
||||
SessionPendingInfo,
|
||||
ShellInfo,
|
||||
SkillInfo,
|
||||
VcsInfo,
|
||||
@@ -38,7 +38,7 @@ import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { useClient } from "./client"
|
||||
import { nonEmptyToolContent } from "../util/tool-display"
|
||||
import type { SessionInbox } from "@opencode-ai/schema/session-inbox"
|
||||
import type { SessionPending } from "@opencode-ai/schema/session-pending"
|
||||
import { createEffect, createSignal, onCleanup } from "solid-js"
|
||||
|
||||
export type DataSessionStatus = "idle" | "running"
|
||||
@@ -80,7 +80,7 @@ type Store = {
|
||||
family: Record<string, string[]>
|
||||
active: Record<string, DataSessionStatus>
|
||||
message: Record<string, SessionMessageInfo[]>
|
||||
pending: Record<string, SessionInboxInfo[]>
|
||||
pending: Record<string, SessionPendingInfo[]>
|
||||
input: Record<string, string[]>
|
||||
permission: Record<string, PermissionRequest[]>
|
||||
// Pending forms keyed by owner: a session ID or the temporary "global" elicitation sentinel.
|
||||
@@ -164,26 +164,26 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
setStore("session", "active", sessionID, status)
|
||||
}
|
||||
|
||||
function addPending(item: SessionInboxInfo) {
|
||||
function addPending(item: SessionPendingInfo) {
|
||||
if (store.session.pending[item.sessionID]?.some((pending) => pending.id === item.id)) return
|
||||
setStore("session", "pending", item.sessionID, [...(store.session.pending[item.sessionID] ?? []), item])
|
||||
}
|
||||
|
||||
function removePending(sessionID: string, inboxID?: string) {
|
||||
if (!inboxID) return
|
||||
if (store.session.pending[sessionID]?.some((item) => item.id === inboxID))
|
||||
function removePending(sessionID: string, inputID?: string) {
|
||||
if (!inputID) return
|
||||
if (store.session.pending[sessionID]?.some((item) => item.id === inputID))
|
||||
setStore(
|
||||
"session",
|
||||
"pending",
|
||||
sessionID,
|
||||
(store.session.pending[sessionID] ?? []).filter((item) => item.id !== inboxID),
|
||||
(store.session.pending[sessionID] ?? []).filter((item) => item.id !== inputID),
|
||||
)
|
||||
if (store.session.input[sessionID]?.includes(inboxID))
|
||||
if (store.session.input[sessionID]?.includes(inputID))
|
||||
setStore(
|
||||
"session",
|
||||
"input",
|
||||
sessionID,
|
||||
(store.session.input[sessionID] ?? []).filter((id) => id !== inboxID),
|
||||
(store.session.input[sessionID] ?? []).filter((id) => id !== inputID),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -198,10 +198,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
)
|
||||
}
|
||||
|
||||
function updatePending(sessionID: string, inboxID: string, delivery: SessionInbox.Delivery) {
|
||||
const index = store.session.pending[sessionID]?.findIndex((item) => item.id === inboxID) ?? -1
|
||||
function updatePending(sessionID: string, inputID: string, delivery: SessionPending.Delivery) {
|
||||
const index = store.session.pending[sessionID]?.findIndex((item) => item.id === inputID) ?? -1
|
||||
const item = store.session.pending[sessionID]?.[index]
|
||||
if (index < 0 || !item || item.delivery === delivery) return
|
||||
if (index < 0 || !item || item.type === "compaction" || item.delivery === delivery) return
|
||||
setStore("session", "pending", sessionID, index, { ...item, delivery })
|
||||
}
|
||||
|
||||
@@ -457,11 +457,11 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
}
|
||||
break
|
||||
}
|
||||
case "session.inbox.delivered": {
|
||||
const admitted = store.session.input[event.data.sessionID]?.includes(event.data.inboxID) ?? false
|
||||
removePending(event.data.sessionID, event.data.inboxID)
|
||||
case "session.input.promoted": {
|
||||
const admitted = store.session.input[event.data.sessionID]?.includes(event.data.inputID) ?? false
|
||||
removePending(event.data.sessionID, event.data.inputID)
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const position = index.get(event.data.inboxID)
|
||||
const position = index.get(event.data.inputID)
|
||||
if (position === undefined) return
|
||||
const existing = draft[position]
|
||||
if (!existing || !admitted) return
|
||||
@@ -472,56 +472,56 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
})
|
||||
break
|
||||
}
|
||||
case "session.inbox.delivery.changed":
|
||||
updatePending(event.data.sessionID, event.data.inboxID, event.data.delivery)
|
||||
case "session.input.steered":
|
||||
updatePending(event.data.sessionID, event.data.inputID, "steer")
|
||||
break
|
||||
case "session.inbox.cancelled": {
|
||||
removePending(event.data.sessionID, event.data.inboxID)
|
||||
if (messageIndex.get(event.data.sessionID)?.has(event.data.inboxID))
|
||||
case "session.input.queued":
|
||||
updatePending(event.data.sessionID, event.data.inputID, "queue")
|
||||
break
|
||||
case "session.input.cancelled": {
|
||||
removePending(event.data.sessionID, event.data.inputID)
|
||||
if (messageIndex.get(event.data.sessionID)?.has(event.data.inputID))
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const position = index.get(event.data.inboxID)
|
||||
const position = index.get(event.data.inputID)
|
||||
if (position === undefined) return
|
||||
draft.splice(position, 1)
|
||||
index.delete(event.data.inboxID)
|
||||
index.delete(event.data.inputID)
|
||||
message.reindex(draft, index, position)
|
||||
})
|
||||
break
|
||||
}
|
||||
case "session.inbox.enqueued": {
|
||||
const item = event.data.item
|
||||
case "session.input.admitted":
|
||||
addPending({
|
||||
id: event.data.inboxID,
|
||||
id: event.data.inputID,
|
||||
sessionID: event.data.sessionID,
|
||||
timeCreated: event.created,
|
||||
...item,
|
||||
...event.data.input,
|
||||
})
|
||||
if (!store.session.input[event.data.sessionID]?.includes(event.data.inboxID))
|
||||
if (!store.session.input[event.data.sessionID]?.includes(event.data.inputID))
|
||||
setStore("session", "input", event.data.sessionID, [
|
||||
...(store.session.input[event.data.sessionID] ?? []),
|
||||
event.data.inboxID,
|
||||
event.data.inputID,
|
||||
])
|
||||
if (item.type !== "user" && item.type !== "synthetic") break
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
message.append(
|
||||
draft,
|
||||
index,
|
||||
item.type === "user"
|
||||
event.data.input.type === "user"
|
||||
? {
|
||||
id: event.data.inboxID,
|
||||
id: event.data.inputID,
|
||||
type: "user",
|
||||
...item.payload,
|
||||
...event.data.input.data,
|
||||
time: { created: event.created },
|
||||
}
|
||||
: {
|
||||
id: event.data.inboxID,
|
||||
id: event.data.inputID,
|
||||
type: "synthetic",
|
||||
...item.payload,
|
||||
...event.data.input.data,
|
||||
time: { created: event.created },
|
||||
},
|
||||
)
|
||||
})
|
||||
break
|
||||
}
|
||||
case "session.instructions.updated":
|
||||
// Mirror the projector: the initial baseline and empty-rendering deltas carry no text
|
||||
// and produce no transcript message.
|
||||
@@ -584,6 +584,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
existing.retry = undefined
|
||||
existing.error = undefined
|
||||
existing.finish = undefined
|
||||
existing.time.started = undefined
|
||||
existing.time.generated = undefined
|
||||
existing.time.completed = undefined
|
||||
if (event.data.snapshot) existing.snapshot = { ...existing.snapshot, start: event.data.snapshot }
|
||||
return
|
||||
@@ -613,6 +615,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
currentAssistant.finish = event.data.finish
|
||||
currentAssistant.cost = event.data.cost
|
||||
currentAssistant.tokens = event.data.tokens
|
||||
currentAssistant.time.generated = event.data.generated
|
||||
if (event.data.snapshot)
|
||||
currentAssistant.snapshot = { ...currentAssistant.snapshot, end: event.data.snapshot }
|
||||
})
|
||||
@@ -630,11 +633,15 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
currentAssistant.cost = event.data.cost
|
||||
currentAssistant.tokens = event.data.tokens
|
||||
}
|
||||
currentAssistant.time.generated = event.data.generated
|
||||
})
|
||||
break
|
||||
case "session.text.started":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
message.assistant(draft, index, event.data.assistantMessageID)?.content.push({
|
||||
const assistant = message.assistant(draft, index, event.data.assistantMessageID)
|
||||
if (!assistant) return
|
||||
if (assistant.time.completed === undefined) assistant.time.started ??= event.created
|
||||
assistant.content.push({
|
||||
type: "text",
|
||||
text: "",
|
||||
})
|
||||
@@ -654,7 +661,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
break
|
||||
case "session.tool.input.started":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
message.assistant(draft, index, event.data.assistantMessageID)?.content.push({
|
||||
const assistant = message.assistant(draft, index, event.data.assistantMessageID)
|
||||
if (!assistant) return
|
||||
assistant.content.push({
|
||||
type: "tool",
|
||||
id: event.data.id,
|
||||
name: event.data.name,
|
||||
@@ -743,7 +752,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
break
|
||||
case "session.reasoning.started":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
message.assistant(draft, index, event.data.assistantMessageID)?.content.push({
|
||||
const assistant = message.assistant(draft, index, event.data.assistantMessageID)
|
||||
if (!assistant) return
|
||||
assistant.content.push({
|
||||
type: "reasoning",
|
||||
text: "",
|
||||
state: event.data.state,
|
||||
@@ -781,8 +792,16 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
case "session.execution.started":
|
||||
setSessionActive(event.data.sessionID, "running")
|
||||
break
|
||||
case "session.compaction.admitted":
|
||||
addPending({
|
||||
id: event.data.inputID,
|
||||
sessionID: event.data.sessionID,
|
||||
timeCreated: event.created,
|
||||
type: "compaction",
|
||||
})
|
||||
break
|
||||
case "session.compaction.started":
|
||||
if (event.data.inputID) removePending(event.data.sessionID, event.data.inputID)
|
||||
removePending(event.data.sessionID, event.data.inputID)
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
message.append(draft, index, {
|
||||
id: event.data.inputID ?? messageIDFromEvent(event.id),
|
||||
@@ -835,6 +854,12 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
})
|
||||
break
|
||||
case "session.compaction.ended":
|
||||
setStore(
|
||||
"session",
|
||||
"pending",
|
||||
event.data.sessionID,
|
||||
(store.session.pending[event.data.sessionID] ?? []).filter((item) => item.type !== "compaction"),
|
||||
)
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running")
|
||||
const current = draft[position]
|
||||
@@ -859,7 +884,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
})
|
||||
break
|
||||
case "session.compaction.failed":
|
||||
if (event.data.inputID) removePending(event.data.sessionID, event.data.inputID)
|
||||
removePending(event.data.sessionID, event.data.inputID)
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running")
|
||||
const current = draft[position]
|
||||
@@ -996,8 +1021,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
list(sessionID: string) {
|
||||
return store.session.input[sessionID] ?? []
|
||||
},
|
||||
has(sessionID: string, inboxID: string) {
|
||||
return store.session.input[sessionID]?.includes(inboxID) ?? false
|
||||
has(sessionID: string, inputID: string) {
|
||||
return store.session.input[sessionID]?.includes(inputID) ?? false
|
||||
},
|
||||
},
|
||||
pending: {
|
||||
@@ -1006,7 +1031,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
},
|
||||
sync(sessionID: string) {
|
||||
return sync.run(`session.pending:${sessionID}`, async () => {
|
||||
const pending = await client.api.session.inbox.list({ sessionID })
|
||||
const pending = await client.api.session.pending.list({ sessionID })
|
||||
setStore("session", "pending", sessionID, reconcile(pending))
|
||||
setStore(
|
||||
"session",
|
||||
|
||||
@@ -35,13 +35,6 @@ export function openSessionTab(tabs: SessionTab[], tab: SessionTab): SessionTab[
|
||||
return tabs.map((item, position) => (position === index ? { ...item, title: tab.title } : item))
|
||||
}
|
||||
|
||||
export function replaceSessionTab(tabs: SessionTab[], current: string | undefined, tab: SessionTab): SessionTab[] {
|
||||
if (tabs.some((item) => item.sessionID === tab.sessionID)) return tabs
|
||||
const index = current ? tabs.findIndex((item) => item.sessionID === current) : -1
|
||||
if (index === -1) return [...tabs, tab]
|
||||
return tabs.map((item, position) => (position === index ? tab : item))
|
||||
}
|
||||
|
||||
export function closeSessionTab(tabs: SessionTab[], sessionID: string) {
|
||||
const index = tabs.findIndex((tab) => tab.sessionID === sessionID)
|
||||
// Like openSessionTab and moveSessionTab, a no-op returns the same reference so callers can
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user