refactor(protocol): normalize session resources

This commit is contained in:
Dax Raad
2026-09-14 17:51:37 -04:00
parent cdee29a821
commit 648d01eec7
38 changed files with 339 additions and 239 deletions
+27 -27
View File
@@ -111,7 +111,7 @@ Review endpoints in document order. For each endpoint, select one disposition an
|---|---|---|---|---|---|
| [x] 025 | `GET` | `/api/integration` | `integration.list` | Keep | Full integration inventory is consumed by authentication and integration-selection clients. |
| [x] 026 | `GET` | `/api/integration/{integrationID}` | `integration.get` | Change | Missing integration now returns typed `404` instead of optional data. |
| [ ] 027 | `POST` | `/api/experimental/integration/wellknown` | `experimental.integration.wellknown.add` | | |
| [x] | `POST` | `/api/experimental/integration/wellknown` | `experimental.integration.wellknown.add` | Experimental-only | Retained outside the stable API commitment. |
| [ ] 028 | `POST` | `/api/integration/{integrationID}/connect/key` | `integration.connect.key` | | |
| [ ] 029 | `POST` | `/api/integration/{integrationID}/connect/oauth` | `integration.oauth.connect` | | |
| [ ] 030 | `GET` | `/api/integration/{integrationID}/connect/oauth/{attemptID}` | `integration.oauth.status` | | |
@@ -175,19 +175,19 @@ Review endpoints in document order. For each endpoint, select one disposition an
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [ ] 070 | `POST` | `/api/session/import` | `session.import` | | |
| [ ] 071 | `GET` | `/api/session/{sessionID}/export` | `session.export` | | |
| [ ] 072 | `POST` | `/api/session/{sessionID}/revert/stage` | `session.revert.stage` | | |
| [ ] 073 | `POST` | `/api/session/{sessionID}/revert/clear` | `session.revert.clear` | | |
| [ ] 074 | `POST` | `/api/session/{sessionID}/revert/commit` | `session.revert.commit` | | |
| [ ] 075 | `GET` | `/api/session/{sessionID}/context` | `session.context` | | |
| [ ] 076 | `GET` | `/api/session/{sessionID}/diff` | `session.diff` | | |
| [ ] 077 | `GET` | `/api/session/{sessionID}/instructions/entries` | `session.instructions.entry.list` | | |
| [ ] 078 | `PUT` | `/api/session/{sessionID}/instructions/entries/{key}` | `session.instructions.entry.put` | | |
| [ ] 079 | `DELETE` | `/api/session/{sessionID}/instructions/entries/{key}` | `session.instructions.entry.remove` | | |
| [ ] 080 | `GET` | `/api/experimental/session/{sessionID}/log` | `session.log` | | |
| [ ] 081 | `GET` | `/api/session/{sessionID}/message/{messageID}` | `session.message` | | |
| [ ] 082 | `GET` | `/api/session/{sessionID}/message` | `message.list` | | |
| [x] 070 | `POST` | `/api/experimental/session/import` | `experimental.session.import` | Experimental-only | Existing projected transcript import contract retained outside the stable API. |
| [x] 071 | `GET` | `/api/experimental/session/{sessionID}/export` | `experimental.session.export` | Experimental-only | Existing projected transcript export contract retained outside the stable API. |
| [x] 072 | `POST` | `/api/session/{sessionID}/revert/stage` | `session.revert.stage` | Keep | Existing staged history and optional file restoration behavior retained. |
| [x] 073 | `DELETE` | `/api/session/{sessionID}/revert` | `session.revert.clear` | Change | Clearing staged revert now deletes the session revert resource. |
| [x] 074 | `POST` | `/api/session/{sessionID}/revert/commit` | `session.revert.commit` | Keep | Explicit staged-revert commit action retained. |
| [x] 075 | `GET` | `/api/session/{sessionID}/context` | `session.context` | Keep | Active model-context projection retained. |
| [x] 076 | `GET` | `/api/session/{sessionID}/diff` | `session.diff` | Keep | Turn-range structured diff contract retained. |
| [x] 077 | `GET` | `/api/experimental/session/{sessionID}/instructions/entries` | `experimental.session.instructions.entry.list` | Experimental-only | API-managed durable context entries retained outside the stable API. |
| [x] 078 | `PUT` | `/api/experimental/session/{sessionID}/instructions/entries/{key}` | `experimental.session.instructions.entry.put` | Experimental-only | API-managed durable context entries retained outside the stable API. |
| [x] 079 | `DELETE` | `/api/experimental/session/{sessionID}/instructions/entries/{key}` | `experimental.session.instructions.entry.remove` | Experimental-only | API-managed durable context entries retained outside the stable API. |
| [x] | `GET` | `/api/experimental/session/{sessionID}/log` | `session.log` | Experimental-only | Retained outside the stable API commitment. |
| [x] 081 | `GET` | `/api/session/{sessionID}/message/{messageID}` | `session.message.get` | Change | Normalized specific-message operation ID. |
| [x] 082 | `GET` | `/api/session/{sessionID}/message` | `session.message.list` | Change | Normalized session-scoped message-list operation ID. |
## Group 7: Inbox, permissions, and forms
@@ -195,7 +195,7 @@ Review endpoints in document order. For each endpoint, select one disposition an
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [ ] 083 | `GET` | `/api/session/{sessionID}/inbox` | `session.inbox.list` | | |
| [x] 083 | `GET` | `/api/session/{sessionID}/inbox` | `session.inbox.list` | Change | Inbox timestamps now use the standard nested `time.created` shape. |
| [ ] 084 | `DELETE` | `/api/session/{sessionID}/inbox/{inboxID}` | `session.inbox.cancel` | | |
| [ ] 085 | `POST` | `/api/session/{sessionID}/inbox/{inboxID}/steer` | `session.inbox.steer` | | |
| [ ] 086 | `POST` | `/api/session/{sessionID}/inbox/{inboxID}/queue` | `session.inbox.queue` | | |
@@ -247,17 +247,17 @@ Review endpoints in document order. For each endpoint, select one disposition an
| [ ] 118 | `DELETE` | `/api/pty/{ptyID}` | `pty.remove` | | |
| [ ] 119 | `POST` | `/api/pty/{ptyID}/connect-token` | `pty.connect.token` | | |
| [ ] 120 | `GET` | `/api/pty/{ptyID}/connect` | `pty.connect` | | |
| [ ] 121 | `GET` | `/api/experimental/session/{sessionID}/terminal/read` | `server.experimental.persistentPty.read` | | |
| [ ] 122 | `GET` | `/api/experimental/session/{sessionID}/terminal` | `server.experimental.persistentPty.list` | | |
| [ ] 123 | `POST` | `/api/experimental/session/{sessionID}/terminal` | `server.experimental.persistentPty.create` | | |
| [ ] 124 | `POST` | `/api/experimental/persistent-pty/shutdown` | `server.experimental.persistentPty.shutdown` | | |
| [ ] 125 | `POST` | `/api/experimental/persistent-pty/handoff` | `server.experimental.persistentPty.handoff` | | |
| [ ] 126 | `GET` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.get` | | |
| [ ] 127 | `PUT` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.update` | | |
| [ ] 128 | `DELETE` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.remove` | | |
| [ ] 129 | `GET` | `/api/experimental/persistent-pty/{ptyID}/snapshot` | `server.experimental.persistentPty.snapshot` | | |
| [ ] 130 | `POST` | `/api/experimental/persistent-pty/{ptyID}/connect-token` | `server.experimental.persistentPty.connectToken` | | |
| [ ] 131 | `GET` | `/api/experimental/persistent-pty/{ptyID}/connect` | `persistentPty.connect` | | |
| [x] | `GET` | `/api/experimental/session/{sessionID}/terminal/read` | `server.experimental.persistentPty.read` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `GET` | `/api/experimental/session/{sessionID}/terminal` | `server.experimental.persistentPty.list` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `POST` | `/api/experimental/session/{sessionID}/terminal` | `server.experimental.persistentPty.create` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `POST` | `/api/experimental/persistent-pty/shutdown` | `server.experimental.persistentPty.shutdown` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `POST` | `/api/experimental/persistent-pty/handoff` | `server.experimental.persistentPty.handoff` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `GET` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.get` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `PUT` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.update` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `DELETE` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.remove` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `GET` | `/api/experimental/persistent-pty/{ptyID}/snapshot` | `server.experimental.persistentPty.snapshot` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `POST` | `/api/experimental/persistent-pty/{ptyID}/connect-token` | `server.experimental.persistentPty.connectToken` | Experimental-only | Retained outside the stable API commitment. |
| [x] | `GET` | `/api/experimental/persistent-pty/{ptyID}/connect` | `persistentPty.connect` | Experimental-only | Retained outside the stable API commitment. |
| [ ] 132 | `GET` | `/api/shell` | `shell.list` | | |
| [ ] 133 | `POST` | `/api/shell` | `shell.create` | | |
| [ ] 134 | `GET` | `/api/shell/{id}` | `shell.get` | | |
@@ -276,4 +276,4 @@ Review endpoints in document order. For each endpoint, select one disposition an
| [ ] 140 | `GET` | `/api/event` | `event.subscribe` | | |
| [ ] 141 | `GET` | `/api/debug/location` | `debug.location.list` | | |
| [ ] 142 | `DELETE` | `/api/debug/location` | `debug.location.evict` | | |
| [ ] 143 | `GET` | `/api/experimental/migration/v1` | `experimental.migration.v1.status` | | |
| [x] | `GET` | `/api/experimental/migration/v1` | `experimental.migration.v1.status` | Experimental-only | Retained outside the stable API commitment. |
@@ -12,7 +12,7 @@ const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${pr
type InboxRow = {
id: string
sessionID: string
timeCreated: number
time: { created: number }
type: "user"
payload: { text: string; metadata?: Record<string, unknown> }
delivery: "steer" | "queue"
@@ -22,7 +22,7 @@ function createQueueMock(seed: string[], messages: SessionMessageInfo[] = []) {
const rows: InboxRow[] = seed.map((text, index) => ({
id: `inb_seed_${index + 1}`,
sessionID,
timeCreated: 1700000000000 + index,
time: { created: 1700000000000 + index },
type: "user",
payload: { text },
delivery: "queue",
@@ -59,7 +59,7 @@ function createQueueMock(seed: string[], messages: SessionMessageInfo[] = []) {
const row: InboxRow = {
id: typeof input.body.id === "string" ? input.body.id : `inb_mock_${sequence}`,
sessionID: input.sessionID,
timeCreated: Date.now(),
time: { created: Date.now() },
type: "user",
payload: {
text: typeof input.body.text === "string" ? input.body.text : "",
+1 -1
View File
@@ -254,7 +254,7 @@ const Group = HttpApiGroup.make("mock")
}),
)
.add(
HttpApiEndpoint.post("sessionRevertClear", "/api/session/:sessionID/revert/clear", {
HttpApiEndpoint.delete("sessionRevertClear", "/api/session/:sessionID/revert", {
params: SessionParams,
success: NoContent,
}),
+1 -1
View File
@@ -437,7 +437,7 @@ function mockHandlers(config: MockServerConfig, state: { cursors: Map<string, st
data: {
id: typeof body.id === "string" ? body.id : `inb_mock_${Date.now()}`,
sessionID: ctx.params.sessionID,
timeCreated: Date.now(),
time: { created: Date.now() },
type: "user",
payload: {
text: typeof body.text === "string" ? body.text : "",
@@ -6,7 +6,7 @@ const queued = [
{
id: "msg_original",
sessionID: "ses_1",
timeCreated: 1,
time: { created: 1 },
type: "user",
delivery: "queue",
payload: { text: "original" },
@@ -14,7 +14,7 @@ const queued = [
{
id: "msg_replacement",
sessionID: "ses_1",
timeCreated: 2,
time: { created: 2 },
type: "user",
delivery: "queue",
payload: { text: "edited" },
+1 -1
View File
@@ -70,7 +70,7 @@ export function createSessionRevert(input: {
.map((item) => item.id)
void server.api.session.inbox
.list({ sessionID })
.then((rows) => rows.filter((row) => row.type === "user" && row.timeCreated <= cutoff).map((row) => row.id))
.then((rows) => rows.filter((row) => row.type === "user" && row.time.created <= cutoff).map((row) => row.id))
.catch(() => [])
.then((authoritative) => {
new Set([...local, ...authoritative]).forEach(
@@ -23,7 +23,7 @@ describe("visibleTimelineMessages", () => {
const steer = {
id: "msg_3",
sessionID: "ses_1",
timeCreated: 3,
time: { created: 3 },
type: "user",
delivery: "steer",
payload: { text: "queued" },
@@ -135,7 +135,7 @@ describe("visibleTimelineMessages", () => {
{
id: "msg_3",
sessionID: "ses_1",
timeCreated: 3,
time: { created: 3 },
type: "user",
delivery: "queue",
payload: { text: "queued" },
+1 -1
View File
@@ -369,7 +369,7 @@ export async function streamTurn(input: {
}
const assistant = assistantMessageID
? await input.client.session
.message({ sessionID: input.sessionID, messageID: assistantMessageID })
.message.get({ sessionID: input.sessionID, messageID: assistantMessageID })
.catch(() => undefined)
: undefined
return response(
@@ -39,7 +39,7 @@ export default Runtime.handler(
}),
)
const response = yield* Effect.promise(() =>
fetch(new URL("/api/session/import", server.endpoint.url), {
fetch(new URL("/api/experimental/session/import", server.endpoint.url), {
method: "POST",
headers: { ...Service.headers(server.endpoint), "content-type": "application/json" },
body: JSON.stringify({
+4 -4
View File
@@ -62,7 +62,7 @@ test("export is raw by default and supports explicit sanitization", async () =>
const url = new URL(request.url)
if (url.pathname === "/api/status") return status()
if (url.pathname === `/api/session/${info.id}`) return Response.json({ data: info })
if (url.pathname === `/api/session/${info.id}/export`) {
if (url.pathname === `/api/experimental/session/${info.id}/export`) {
sanitization.push(url.searchParams.get("sanitize") ?? "")
return Response.json({ data: url.searchParams.get("sanitize") === "true" ? sanitizedTransfer : transfer })
}
@@ -128,7 +128,7 @@ test("export reports a missing session without a stack trace", async () => {
fetch(request) {
const url = new URL(request.url)
if (url.pathname === "/api/status") return status()
if (url.pathname === `/api/session/${sessionID}/export`) {
if (url.pathname === `/api/experimental/session/${sessionID}/export`) {
return Response.json(
{ _tag: "SessionNotFoundError", sessionID, message: `Session not found: ${sessionID}` },
{ status: 404 },
@@ -165,7 +165,7 @@ test("import validates a file and sends it to the resolved location", async () =
project: { id: "global", directory: root, canonical: root },
})
}
if (url.pathname === "/api/session/import") {
if (url.pathname === "/api/experimental/session/import") {
imported = await request.json()
return Response.json({ data: { ...info, location: { directory: root } } })
}
@@ -208,7 +208,7 @@ test("import reports an existing session without a stack trace", async () => {
project: { id: "global", directory: root, canonical: root },
})
}
if (url.pathname === "/api/session/import") return new Response("Conflict", { status: 409 })
if (url.pathname === "/api/experimental/session/import") return new Response("Conflict", { status: 409 })
return new Response("Not found", { status: 404 })
},
})
+1 -1
View File
@@ -265,7 +265,7 @@ async function run(input: {
values.push(...input.turn(messageID))
wake?.()
wake = undefined
return ok({ id: messageID, sessionID: "ses_1", timeCreated: 1 }) as never
return ok({ id: messageID, sessionID: "ses_1", time: { created: 1 } }) as never
})
await runNonInteractivePrompt({
client: sdk,
+6 -4
View File
@@ -1319,9 +1319,11 @@ export type SessionBackgroundOperation<E = never> = (
input: SessionBackgroundInput,
) => Effect.Effect<SessionBackgroundOutput, E>
export type SessionMessageInput = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID }
export type SessionMessageOutput = SessionMessage.Info
export type SessionMessageOperation<E = never> = (input: SessionMessageInput) => Effect.Effect<SessionMessageOutput, E>
export type SessionMessageGetInput = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID }
export type SessionMessageGetOutput = SessionMessage.Info
export type SessionMessageGetOperation<E = never> = (
input: SessionMessageGetInput,
) => Effect.Effect<SessionMessageGetOutput, E>
export type SessionEnvironmentInput = {
readonly sessionID: Session.ID
@@ -1381,7 +1383,7 @@ export interface SessionApi<E = never> {
readonly log: SessionLogOperation<E>
readonly interrupt: SessionInterruptOperation<E>
readonly background: SessionBackgroundOperation<E>
readonly message: SessionMessageOperation<E>
readonly message: { readonly get: SessionMessageGetOperation<E> }
readonly environment: SessionEnvironmentOperation<E>
readonly view: SessionViewOperation<E>
}
@@ -89,8 +89,8 @@ import type {
SessionInterruptOutput,
SessionBackgroundInput,
SessionBackgroundOutput,
SessionMessageInput,
SessionMessageOutput,
SessionMessageGetInput,
SessionMessageGetOutput,
SessionEnvironmentInput,
SessionEnvironmentOutput,
SessionViewInput,
@@ -678,8 +678,8 @@ const EndpointSessionBackground = (raw: RawClient["server.session"]) => (input:
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
)
const EndpointSessionMessage = (raw: RawClient["server.session"]) => (input: SessionMessageInput) =>
preserveEffect<SessionMessageOutput>()(
const EndpointSessionMessageGet = (raw: RawClient["server.session"]) => (input: SessionMessageGetInput) =>
preserveEffect<SessionMessageGetOutput>()(
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
@@ -746,7 +746,7 @@ const adaptGroupSession = (raw: RawClient["server.session"]) => ({
log: EndpointSessionLog(raw),
interrupt: EndpointSessionInterrupt(raw),
background: EndpointSessionBackground(raw),
message: EndpointSessionMessage(raw),
message: { get: EndpointSessionMessageGet(raw) },
environment: EndpointSessionEnvironment(raw),
view: EndpointSessionView(raw),
})
+22 -20
View File
@@ -83,8 +83,8 @@ import type {
SessionInterruptOutput,
SessionBackgroundInput,
SessionBackgroundOutput,
SessionMessageInput,
SessionMessageOutput,
SessionMessageGetInput,
SessionMessageGetOutput,
SessionEnvironmentInput,
SessionEnvironmentOutput,
SessionViewInput,
@@ -555,7 +555,7 @@ export function make(options: ClientOptions) {
request<{ readonly data: SessionImportOutput }>(
{
method: "POST",
path: `/api/session/import`,
path: `/api/experimental/session/import`,
body: { info: input["info"], messages: input["messages"], location: input["location"] },
successStatus: 200,
declaredStatuses: [400, 401, 404, 409],
@@ -567,7 +567,7 @@ export function make(options: ClientOptions) {
request<{ readonly data: SessionExportOutput }>(
{
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/export`,
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/export`,
query: { sanitize: input["sanitize"] },
successStatus: 200,
declaredStatuses: [400, 401, 404, 500],
@@ -790,8 +790,8 @@ export function make(options: ClientOptions) {
clear: (input: SessionRevertClearInput, requestOptions?: RequestOptions) =>
request<SessionRevertClearOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
method: "DELETE",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert`,
successStatus: 204,
declaredStatuses: [400, 401, 404, 409, 500],
empty: true,
@@ -885,7 +885,7 @@ export function make(options: ClientOptions) {
request<{ readonly data: SessionInstructionsEntryListOutput }>(
{
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/instructions/entries`,
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/instructions/entries`,
successStatus: 200,
declaredStatuses: [400, 401, 404],
empty: false,
@@ -896,7 +896,7 @@ export function make(options: ClientOptions) {
request<SessionInstructionsEntryPutOutput>(
{
method: "PUT",
path: `/api/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`,
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`,
body: { value: input["value"] },
successStatus: 204,
declaredStatuses: [400, 401, 404, 413],
@@ -908,7 +908,7 @@ export function make(options: ClientOptions) {
request<SessionInstructionsEntryRemoveOutput>(
{
method: "DELETE",
path: `/api/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`,
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`,
successStatus: 204,
declaredStatuses: [400, 401, 404],
empty: true,
@@ -964,17 +964,19 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
message: (input: SessionMessageInput, requestOptions?: RequestOptions) =>
request<{ readonly data: SessionMessageOutput }>(
{
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/message/${encodeURIComponent(input.messageID)}`,
successStatus: 200,
declaredStatuses: [400, 401, 404],
empty: false,
},
requestOptions,
).then((value) => value.data),
message: {
get: (input: SessionMessageGetInput, requestOptions?: RequestOptions) =>
request<{ readonly data: SessionMessageGetOutput }>(
{
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/message/${encodeURIComponent(input.messageID)}`,
successStatus: 200,
declaredStatuses: [400, 401, 404],
empty: false,
},
requestOptions,
).then((value) => value.data),
},
environment: (input: SessionEnvironmentInput, requestOptions?: RequestOptions) =>
request<SessionEnvironmentOutput>(
{
@@ -529,7 +529,7 @@ export type SessionProviderContext = { version: 1; provenance: SessionProviderCo
export type SessionInboxSynthetic = {
id: string
sessionID: string
timeCreated: number
time: { created: number }
type: "synthetic"
payload: SessionInboxSyntheticPayload
delivery: SessionInboxDelivery
@@ -538,7 +538,7 @@ export type SessionInboxSynthetic = {
export type SessionInboxCompaction = {
id: string
sessionID: string
timeCreated: number
time: { created: number }
type: "compaction"
payload: SessionInboxCompactionPayload
delivery: SessionInboxDelivery
@@ -1629,7 +1629,7 @@ export type VcsInfo = { branch: VcsBranch }
export type SessionInboxMove = {
id: string
sessionID: string
timeCreated: number
time: { created: number }
type: "move"
delivery: SessionInboxDelivery
payload: SessionInboxMovePayload
@@ -2107,7 +2107,7 @@ export type ConfigEntry =
export type SessionInboxUser = {
id: string
sessionID: string
timeCreated: number
time: { created: number }
type: "user"
payload: SessionInboxUserPayload
delivery: SessionInboxDelivery
@@ -4390,12 +4390,12 @@ export type SessionBackgroundInput = { readonly sessionID: { readonly sessionID:
export type SessionBackgroundOutput = void
export type SessionMessageInput = {
export type SessionMessageGetInput = {
readonly sessionID: { readonly sessionID: string; readonly messageID: string }["sessionID"]
readonly messageID: { readonly sessionID: string; readonly messageID: string }["messageID"]
}
export type SessionMessageOutput = { data: SessionMessageInfo }["data"]
export type SessionMessageGetOutput = { data: SessionMessageInfo }["data"]
export type SessionEnvironmentInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
+6 -6
View File
@@ -389,8 +389,8 @@ export function createData(config: CreateDataInput) {
message.update(item.sessionID, (draft, index) => {
const row =
item.type === "user"
? { id: item.id, type: "user" as const, ...item.payload, time: { created: item.timeCreated } }
: { id: item.id, type: "synthetic" as const, ...item.payload, time: { created: item.timeCreated } }
? { id: item.id, type: "user" as const, ...item.payload, time: { created: item.time.created } }
: { id: item.id, type: "synthetic" as const, ...item.payload, time: { created: item.time.created } }
const position = index.get(item.id)
if (position === undefined) return message.append(draft, index, row)
draft[position] = row
@@ -671,7 +671,7 @@ export function createData(config: CreateDataInput) {
})
refresh(() =>
api()
.session.message({ sessionID: event.data.sessionID, messageID: messageIDFromEvent(event.id) })
.session.message.get({ sessionID: event.data.sessionID, messageID: messageIDFromEvent(event.id) })
.then((item) => {
message.update(event.data.sessionID, (draft, index) => {
const position = index.get(item.id)
@@ -772,7 +772,7 @@ export function createData(config: CreateDataInput) {
admitLocal({
id: event.data.inboxID,
sessionID: event.data.sessionID,
timeCreated: event.created,
time: { created: event.created },
...event.data.item,
})
if (event.data.item.type === "compaction") {
@@ -1469,7 +1469,7 @@ export function createData(config: CreateDataInput) {
admitLocal({
id,
sessionID: input.sessionID,
timeCreated: Date.now(),
time: { created: Date.now() },
type: "compaction",
delivery: "steer",
payload: {},
@@ -1520,7 +1520,7 @@ export function createData(config: CreateDataInput) {
admitLocal({
id,
sessionID: request.sessionID,
timeCreated: Date.now(),
time: { created: Date.now() },
type: "user",
delivery: request.delivery ?? "steer",
// Files and skills stay off the optimistic row: their durable
+7 -7
View File
@@ -99,17 +99,17 @@ test("session instructions methods use the public HTTP contract", async () => {
expect(requests).toEqual([
{
method: "GET",
url: "http://localhost:3000/api/session/ses_test/instructions/entries",
url: "http://localhost:3000/api/experimental/session/ses_test/instructions/entries",
body: undefined,
},
{
method: "PUT",
url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes",
url: "http://localhost:3000/api/experimental/session/ses_test/instructions/entries/review-notes",
body: { value: { text: "Check the diff", priority: 1 } },
},
{
method: "DELETE",
url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes",
url: "http://localhost:3000/api/experimental/session/ses_test/instructions/entries/review-notes",
body: undefined,
},
])
@@ -257,7 +257,7 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
.log({ sessionID: Session.ID.make("ses_test"), after: Event.Seq.make(0) })
.pipe(Stream.runCollect)
const interrupted = yield* client.session.interrupt({ sessionID: Session.ID.make("ses_test") })
const message = yield* client.session.message({
const message = yield* client.session.message.get({
sessionID: Session.ID.make("ses_test"),
messageID: SessionMessage.ID.make("msg_model"),
})
@@ -276,7 +276,7 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
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(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000)
expect(DateTime.toEpochMillis(result.admitted.time.created)).toBe(1_717_171_717_000)
expect(result.context).toEqual([])
expect(logQueries[0]).toEqual({ after: "0" })
expect(requests).toContainEqual({ method: "POST", url: "http://localhost:3000/api/session/ses_test/view" })
@@ -336,7 +336,7 @@ const admission = {
type: "user",
payload: { text: "Hello" },
delivery: "steer",
timeCreated: 1_717_171_717_000,
time: { created: 1_717_171_717_000 },
},
}
@@ -347,7 +347,7 @@ const compactionAdmission = {
delivery: "queue",
id: "msg_compaction",
sessionID: "ses_test",
timeCreated: 1_717_171_717_000,
time: { created: 1_717_171_717_000 },
},
}
+8 -8
View File
@@ -487,17 +487,17 @@ test("session instructions methods use the public HTTP contract", async () => {
expect(requests).toEqual([
{
method: "GET",
url: "http://localhost:3000/api/session/ses_test/instructions/entries",
url: "http://localhost:3000/api/experimental/session/ses_test/instructions/entries",
body: undefined,
},
{
method: "PUT",
url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes",
url: "http://localhost:3000/api/experimental/session/ses_test/instructions/entries/review-notes",
body: { value: { text: "Check the diff", priority: 1 } },
},
{
method: "DELETE",
url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes",
url: "http://localhost:3000/api/experimental/session/ses_test/instructions/entries/review-notes",
body: undefined,
},
])
@@ -509,7 +509,7 @@ test("session.inbox.list uses the public HTTP contract", async () => {
{
id: "msg_pending",
sessionID: "ses_test",
timeCreated: 1_717_171_717_000,
time: { created: 1_717_171_717_000 },
type: "user",
payload: { text: "Fix the failing tests" },
delivery: "steer",
@@ -859,7 +859,7 @@ test("session methods use the public HTTP contract", async () => {
const log = []
for await (const item of client.session.log({ sessionID: "ses_test", after: 0 })) log.push(item)
const interrupted = await client.session.interrupt({ sessionID: "ses_test", continue: true })
const message = await client.session.message({ sessionID: "ses_test", messageID: "msg_model" })
const message = await client.session.message.get({ sessionID: "ses_test", messageID: "msg_model" })
expect(page.cursor.next).toBe("next")
expect(page.data[0].time).toMatchObject({ idle: 1_717_171_717_002, viewed: 1_717_171_717_001 })
@@ -969,7 +969,7 @@ const admission = {
type: "user",
data: { text: "Hello" },
delivery: "steer",
timeCreated: 1_717_171_717_000,
time: { created: 1_717_171_717_000 },
},
}
@@ -980,7 +980,7 @@ const syntheticAdmission = {
type: "synthetic",
data: { text: "Completed" },
delivery: "queue",
timeCreated: 1_717_171_717_000,
time: { created: 1_717_171_717_000 },
},
}
@@ -989,7 +989,7 @@ const compactionAdmission = {
type: "compaction",
id: "msg_compaction",
sessionID: "ses_test",
timeCreated: 1_717_171_717_000,
time: { created: 1_717_171_717_000 },
},
}
@@ -348,7 +348,7 @@ const event = { id: "evt_compact", created: 10, durable: { aggregateID: sessionI
const item = (id: string, timeCreated = 10): SessionInboxCompaction => ({
id,
sessionID,
timeCreated,
time: { created: timeCreated },
type: "compaction",
delivery: "steer",
payload: {},
+1 -1
View File
@@ -142,7 +142,7 @@ function fixture(read?: (url: URL) => Promise<Response | void>) {
type: "user",
delivery: "steer",
payload: { text: "pending" },
timeCreated: 1,
time: { created: 1 },
},
],
})
+3 -3
View File
@@ -77,7 +77,7 @@ 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),
time: { created: DateTime.makeUnsafe(row.time_created) },
}
if (row.type === "compaction")
return Compaction.make({
@@ -126,7 +126,7 @@ const promotedFromMessage = Effect.fn("SessionInbox.promotedFromMessage")(functi
if (row.session_id !== sessionID || (row.type !== "user" && row.type !== "synthetic"))
return yield* new LifecycleConflict({ id })
const message = decodeMessage({ ...row.data, id: row.id, type: row.type })
const base = { id, sessionID, timeCreated: message.time.created, delivery }
const base = { id, sessionID, time: { created: message.time.created }, delivery }
if (message.type === "user")
return User.make({
...base,
@@ -184,7 +184,7 @@ export const make = Effect.fn("SessionInbox.make")(function* () {
Info.make({
id: request.id,
sessionID: request.sessionID,
timeCreated: DateTime.makeUnsafe(event.created),
time: { created: DateTime.makeUnsafe(event.created) },
...request.item,
}),
),
+2 -2
View File
@@ -107,7 +107,7 @@ describe("ConfigCommandPlugin.Plugin", () => {
return SessionInbox.User.make({
id: SessionMessage.ID.make("msg_test"),
sessionID: input.sessionID,
timeCreated: DateTime.makeUnsafe(0),
time: { created: DateTime.makeUnsafe(0) },
type: "user",
payload: { text: input.text },
delivery: input.delivery ?? "steer",
@@ -178,7 +178,7 @@ Review files`,
return SessionInbox.User.make({
id: SessionMessage.ID.make("msg_test"),
sessionID: input.sessionID,
timeCreated: DateTime.makeUnsafe(0),
time: { created: DateTime.makeUnsafe(0) },
type: "user",
payload: { text: input.text },
delivery: input.delivery ?? "steer",
+1 -1
View File
@@ -55,7 +55,7 @@ describe("CommandPlugin.Plugin", () => {
return SessionInbox.User.make({
id: SessionMessage.ID.make("msg_test"),
sessionID: input.sessionID,
timeCreated: DateTime.makeUnsafe(0),
time: { created: DateTime.makeUnsafe(0) },
type: "user",
payload: { text: input.text },
delivery: input.delivery ?? "steer",
+1 -1
View File
@@ -96,7 +96,7 @@ const run = Effect.fnUntraced(function* (events: ReadonlyArray<SessionEvent.Agen
SessionInbox.Synthetic.make({
id: SessionMessage.ID.make("msg_plan_test"),
sessionID,
timeCreated: DateTime.makeUnsafe(0),
time: { created: DateTime.makeUnsafe(0) },
type: "synthetic",
payload: { text: input.text },
delivery: "steer",
+53 -25
View File
@@ -1064,10 +1064,10 @@
"summary": "Get session statistics"
}
},
"/api/session/import": {
"/api/experimental/session/import": {
"post": {
"tags": ["session"],
"operationId": "session.import",
"operationId": "experimental.session.import",
"parameters": [],
"security": [],
"responses": {
@@ -1166,10 +1166,10 @@
}
}
},
"/api/session/{sessionID}/export": {
"/api/experimental/session/{sessionID}/export": {
"get": {
"tags": ["session"],
"operationId": "session.export",
"operationId": "experimental.session.export",
"parameters": [
{
"name": "sessionID",
@@ -2804,8 +2804,8 @@
}
}
},
"/api/session/{sessionID}/revert/clear": {
"post": {
"/api/session/{sessionID}/revert": {
"delete": {
"tags": ["session"],
"operationId": "session.revert.clear",
"parameters": [
@@ -3478,10 +3478,10 @@
"summary": "Queue steered item"
}
},
"/api/session/{sessionID}/instructions/entries": {
"/api/experimental/session/{sessionID}/instructions/entries": {
"get": {
"tags": ["session"],
"operationId": "session.instructions.entry.list",
"operationId": "experimental.session.instructions.entry.list",
"parameters": [
{
"name": "sessionID",
@@ -3557,10 +3557,10 @@
"summary": "List instruction entries"
}
},
"/api/session/{sessionID}/instructions/entries/{key}": {
"/api/experimental/session/{sessionID}/instructions/entries/{key}": {
"put": {
"tags": ["session"],
"operationId": "session.instructions.entry.put",
"operationId": "experimental.session.instructions.entry.put",
"parameters": [
{
"name": "sessionID",
@@ -3653,7 +3653,7 @@
},
"delete": {
"tags": ["session"],
"operationId": "session.instructions.entry.remove",
"operationId": "experimental.session.instructions.entry.remove",
"parameters": [
{
"name": "sessionID",
@@ -4140,7 +4140,7 @@
"/api/session/{sessionID}/message/{messageID}": {
"get": {
"tags": ["session"],
"operationId": "session.message",
"operationId": "session.message.get",
"parameters": [
{
"name": "sessionID",
@@ -4372,7 +4372,7 @@
"/api/session/{sessionID}/message": {
"get": {
"tags": ["session"],
"operationId": "message.list",
"operationId": "session.message.list",
"parameters": [
{
"name": "sessionID",
@@ -17006,8 +17006,15 @@
"type": "string",
"pattern": "^ses"
},
"timeCreated": {
"type": "number"
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": ["created"],
"additionalProperties": false
},
"type": {
"type": "string",
@@ -17020,7 +17027,7 @@
"$ref": "#/components/schemas/Session.Inbox.Delivery"
}
},
"required": ["id", "sessionID", "timeCreated", "type", "payload", "delivery"],
"required": ["id", "sessionID", "time", "type", "payload", "delivery"],
"additionalProperties": false
},
"Session.Inbox.CompactionPayload": {
@@ -17064,8 +17071,15 @@
"type": "string",
"pattern": "^ses"
},
"timeCreated": {
"type": "number"
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": ["created"],
"additionalProperties": false
},
"type": {
"type": "string",
@@ -17078,7 +17092,7 @@
"$ref": "#/components/schemas/Session.Inbox.MovePayload"
}
},
"required": ["id", "sessionID", "timeCreated", "type", "delivery", "payload"],
"required": ["id", "sessionID", "time", "type", "delivery", "payload"],
"additionalProperties": false
},
"Session.Inbox.MovePayload": {
@@ -17108,8 +17122,15 @@
"type": "string",
"pattern": "^ses"
},
"timeCreated": {
"type": "number"
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": ["created"],
"additionalProperties": false
},
"type": {
"type": "string",
@@ -17122,7 +17143,7 @@
"$ref": "#/components/schemas/Session.Inbox.Delivery"
}
},
"required": ["id", "sessionID", "timeCreated", "type", "payload", "delivery"],
"required": ["id", "sessionID", "time", "type", "payload", "delivery"],
"additionalProperties": false
},
"Session.Inbox.SyntheticPayload": {
@@ -17152,8 +17173,15 @@
"type": "string",
"pattern": "^ses"
},
"timeCreated": {
"type": "number"
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": ["created"],
"additionalProperties": false
},
"type": {
"type": "string",
@@ -17166,7 +17194,7 @@
"$ref": "#/components/schemas/Session.Inbox.Delivery"
}
},
"required": ["id", "sessionID", "timeCreated", "type", "payload", "delivery"],
"required": ["id", "sessionID", "time", "type", "payload", "delivery"],
"additionalProperties": false
},
"Session.Inbox.UserPayload": {
+1 -1
View File
@@ -78,7 +78,7 @@ export const MessageGroup = HttpApiGroup.make("server.message")
error: [InvalidCursorError, SessionNotFoundError, UnknownError],
}).annotateMerge(
OpenApi.annotations({
identifier: "message.list",
identifier: "session.message.list",
summary: "Get session messages",
description:
"Retrieve projected messages for a session, optionally filtered by type. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline, passing the same type filter on each page.",
+37 -25
View File
@@ -221,7 +221,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
),
)
.add(
HttpApiEndpoint.post("session.import", "/api/session/import", {
HttpApiEndpoint.post("session.import", "/api/experimental/session/import", {
payload: Schema.Struct({
...PublicSessionTransfer.fields,
location: Location.PublicRef.pipe(Schema.optional),
@@ -230,7 +230,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
error: [ConflictError, SessionNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "session.import",
identifier: "experimental.session.import",
summary: "Import session",
description:
"Import a projected session transcript at the requested location. If parentID is supplied, the parent session must already exist; import parents before children.",
@@ -238,14 +238,14 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
),
)
.add(
HttpApiEndpoint.get("session.export", "/api/session/:sessionID/export", {
HttpApiEndpoint.get("session.export", "/api/experimental/session/:sessionID/export", {
params: { sessionID: Session.ID },
query: Schema.Struct({ sanitize: BooleanFromString.pipe(Schema.optional) }),
success: Schema.Struct({ data: PublicSessionTransfer }),
error: [SessionNotFoundError, UnknownError],
}).annotateMerge(
OpenApi.annotations({
identifier: "session.export",
identifier: "experimental.session.export",
summary: "Export session",
description: "Export a complete projected session transcript.",
}),
@@ -524,7 +524,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
),
)
.add(
HttpApiEndpoint.post("session.revert.clear", "/api/session/:sessionID/revert/clear", {
HttpApiEndpoint.delete("session.revert.clear", "/api/session/:sessionID/revert", {
params: { sessionID: Session.ID },
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, SessionBusyError, UnknownError],
@@ -635,31 +635,39 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
),
)
.add(
HttpApiEndpoint.get("session.instructions.entry.list", "/api/session/:sessionID/instructions/entries", {
params: { sessionID: Session.ID },
success: Schema.Struct({ data: Schema.Array(InstructionEntry.Info) }),
error: SessionNotFoundError,
})
HttpApiEndpoint.get(
"session.instructions.entry.list",
"/api/experimental/session/:sessionID/instructions/entries",
{
params: { sessionID: Session.ID },
success: Schema.Struct({ data: Schema.Array(InstructionEntry.Info) }),
error: SessionNotFoundError,
},
)
.middleware(sessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "session.instructions.entry.list",
identifier: "experimental.session.instructions.entry.list",
summary: "List instruction entries",
description: "List API-managed instruction entries attached to the session.",
}),
),
)
.add(
HttpApiEndpoint.put("session.instructions.entry.put", "/api/session/:sessionID/instructions/entries/:key", {
params: { sessionID: Session.ID, key: InstructionEntry.Key },
payload: Schema.Struct({ value: Schema.Json }),
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, InstructionEntry.ValueTooLargeError],
})
HttpApiEndpoint.put(
"session.instructions.entry.put",
"/api/experimental/session/:sessionID/instructions/entries/:key",
{
params: { sessionID: Session.ID, key: InstructionEntry.Key },
payload: Schema.Struct({ value: Schema.Json }),
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, InstructionEntry.ValueTooLargeError],
},
)
.middleware(sessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "session.instructions.entry.put",
identifier: "experimental.session.instructions.entry.put",
summary: "Put instruction entry",
description:
"Attach or replace one durable instruction entry. Changes announce as updates at the next step boundary.",
@@ -667,15 +675,19 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
),
)
.add(
HttpApiEndpoint.delete("session.instructions.entry.remove", "/api/session/:sessionID/instructions/entries/:key", {
params: { sessionID: Session.ID, key: InstructionEntry.Key },
success: HttpApiSchema.NoContent,
error: SessionNotFoundError,
})
HttpApiEndpoint.delete(
"session.instructions.entry.remove",
"/api/experimental/session/:sessionID/instructions/entries/:key",
{
params: { sessionID: Session.ID, key: InstructionEntry.Key },
success: HttpApiSchema.NoContent,
error: SessionNotFoundError,
},
)
.middleware(sessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "session.instructions.entry.remove",
identifier: "experimental.session.instructions.entry.remove",
summary: "Remove instruction entry",
description:
"Remove one instruction entry; the removal is announced to the model at the next step boundary.",
@@ -764,7 +776,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
error: [SessionNotFoundError, MessageNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "session.message",
identifier: "session.message.get",
summary: "Get session message",
description: "Retrieve one projected message owned by the Session.",
}),
+1 -1
View File
@@ -52,7 +52,7 @@ export type Item = typeof Item.Type
const Enqueued = {
id: SessionMessage.ID,
sessionID: SessionID,
timeCreated: DateTimeUtcFromMillis,
time: Schema.Struct({ created: DateTimeUtcFromMillis }),
}
export interface User extends Schema.Schema.Type<typeof User> {}
@@ -98,7 +98,7 @@ describe("contract hygiene", () => {
admittedSeq: 3,
id: "msg_pending",
sessionID: "ses_pending",
timeCreated: 1,
time: { created: 1 },
type: "user",
payload: { text: "hello" },
delivery: "steer",
@@ -107,7 +107,7 @@ describe("contract hygiene", () => {
).toEqual({
id: "msg_pending",
sessionID: "ses_pending",
timeCreated: 1,
time: { created: 1 },
type: "user",
payload: { text: "hello" },
delivery: "steer",
@@ -213,7 +213,7 @@ describe("contract hygiene", () => {
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 base = { id: "msg_inbox", sessionID: "ses_inbox", time: { created: 1 } }
const move = {
location: { directory: "/project" },
projectID: "global",
+3 -3
View File
@@ -148,7 +148,7 @@ it.live(
const modelMessage = Option.fromNullishOr(context.find((message) => message.type === "model-switched")).pipe(
Option.getOrThrow,
)
const message = yield* opencode.sessions.message({ sessionID: id, messageID: modelMessage.id })
const message = yield* opencode.sessions.message.get({ sessionID: id, messageID: modelMessage.id })
yield* opencode.sessions.interrupt({ sessionID: id })
const other = yield* opencode.sessions.create({ location: location(fixture) })
const missingSessionID = fixture.sdk.Session.ID.create()
@@ -156,14 +156,14 @@ it.live(
[
opencode.sessions.log({ sessionID: missingSessionID }).pipe(Stream.runHead, Effect.flip),
opencode.sessions.interrupt({ sessionID: missingSessionID }).pipe(Effect.flip),
opencode.sessions.message({ sessionID: missingSessionID, messageID: modelMessage.id }).pipe(Effect.flip),
opencode.sessions.message.get({ 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),
],
{ concurrency: "unbounded" },
)
const missingMessage = yield* Effect.flip(
opencode.sessions.message({
opencode.sessions.message.get({
sessionID: other.id,
messageID: modelMessage.id,
}),
+2 -2
View File
@@ -34,7 +34,7 @@ it.live("preserves imported parentID through HTTP import, read, and parent filte
const parent = Schema.decodeUnknownSync(SessionResponse)(yield* request("/api/session", { title: "Parent" }))
const id = Session.ID.create()
const imported = Schema.decodeUnknownSync(SessionResponse)(
yield* request("/api/session/import", {
yield* request("/api/experimental/session/import", {
info: { ...parent.data, id, parentID: parent.data.id, title: "Imported child" },
messages: [],
}),
@@ -58,7 +58,7 @@ it.live("preserves imported parentID through HTTP import, read, and parent filte
const id = Session.ID.create()
const parentID = parent === "self" ? id : Session.ID.create()
const error = yield* request(
"/api/session/import",
"/api/experimental/session/import",
{ info: { ...template.data, id, parentID }, messages: [] },
404,
)
+1 -1
View File
@@ -469,7 +469,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
for (let offset = 0; offset < messageIDs.length; offset += FAMILY_DISCOVERY_CONCURRENCY) {
const batch = messageIDs.slice(offset, offset + FAMILY_DISCOVERY_CONCURRENCY)
const messages = await Promise.allSettled(
batch.map((messageID) => sdk.session.message({ sessionID: child.sessionID, messageID }, { signal })),
batch.map((messageID) => sdk.session.message.get({ sessionID: child.sessionID, messageID }, { signal })),
)
if (!blockerCurrent(child, epoch, signal)) return permissions
if (
+2 -2
View File
@@ -926,7 +926,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
]
const messages = await Promise.allSettled(
messageIDs.map((messageID) =>
client.session.message({ sessionID: input.sessionID, messageID }, { signal: attempt.signal }),
client.session.message.get({ sessionID: input.sessionID, messageID }, { signal: attempt.signal }),
),
)
if (!current(attempt)) return permissions
@@ -1034,7 +1034,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
mergePending({
id: event.data.inboxID,
sessionID: event.data.sessionID,
timeCreated: event.created,
time: { created: event.created },
...event.data.item,
})
return
+7 -7
View File
@@ -1669,7 +1669,7 @@ test("restores queued compaction from durable pending input", async () => {
{
id: "message-compaction-queued",
sessionID,
timeCreated: 1,
time: { created: 1 },
type: "compaction" as const,
payload: {},
delivery: "queue" as const,
@@ -1677,7 +1677,7 @@ test("restores queued compaction from durable pending input", async () => {
{
id: "message-compaction-later",
sessionID,
timeCreated: 2,
time: { created: 2 },
type: "compaction" as const,
payload: {},
delivery: "queue" as const,
@@ -2921,7 +2921,7 @@ test("renders admitted prompts immediately and tracks them until promoted", asyn
{
id: messageID,
sessionID,
timeCreated: 0,
time: { created: 0 },
type: "user",
payload: { text: "hello" },
delivery: "steer",
@@ -3275,7 +3275,7 @@ test("admits prompts optimistically and reconciles with the durable echo", async
{
id: messageID,
sessionID,
timeCreated: 5,
time: { created: 5 },
type: "user",
payload: { text: "hello", files: [echoFile] },
delivery: "steer",
@@ -3303,7 +3303,7 @@ test("hydrates durable pending prompts into the visible transcript", async () =>
const item = {
id: "msg_pending_1",
sessionID,
timeCreated: 5,
time: { created: 5 },
type: "user" as const,
payload: { text: "waiting" },
delivery: "steer" as const,
@@ -3357,7 +3357,7 @@ test("keeps the row when the response lands before the echo", async () => {
const admission = {
id: messageID,
sessionID,
timeCreated: 1,
time: { created: 1 },
type: "user",
payload: { text: "hello" },
delivery: "steer",
@@ -3464,7 +3464,7 @@ test("a retry under the same client-minted ID cannot duplicate rows", async () =
const admission = {
id: messageID,
sessionID,
timeCreated: 1,
time: { created: 1 },
type: "user",
payload: { text: "hello" },
delivery: "steer",
+2 -2
View File
@@ -150,7 +150,7 @@ test.each(["first", "second"])(
id: (await request.json()).id,
sessionID,
type: "compaction",
timeCreated: 10,
time: { created: 10 },
payload: {},
delivery: "steer",
},
@@ -169,7 +169,7 @@ test.each(["first", "second"])(
id: body.id,
sessionID,
type: "user",
timeCreated: 10,
time: { created: 10 },
payload: { text: body.text },
delivery: "steer",
},
@@ -91,7 +91,7 @@ function promptAdmission(input: Parameters<OpenCodeClient["session"]["prompt"]>[
metadata: input.metadata,
},
delivery: input.delivery ?? ("steer" as const),
timeCreated: 2,
time: { created: 2 },
}
}
@@ -192,7 +192,7 @@ function sdk(input: {
spyOn(client.session, "active").mockImplementation(() => ok(input.active?.() ?? {}))
spyOn(client.session.inbox, "list").mockImplementation((request) => ok(input.pending?.[request.sessionID] ?? []))
spyOn(client.session, "wait").mockImplementation(() => input.wait?.() ?? ok(undefined))
spyOn(client.session, "message").mockImplementation((request) => {
spyOn(client.session.message, "get").mockImplementation((request) => {
const message = input.messages?.[request.sessionID]?.find((item) => item.id === request.messageID)
return message ? (ok(message) as never) : Promise.reject(new Error(`message not found: ${request.messageID}`))
})
@@ -327,7 +327,7 @@ describe("V2 mini transport", () => {
type: "user",
payload: { text: "look [Image 1]", files },
delivery: "steer",
timeCreated: 1,
time: { created: 1 },
} satisfies SessionInboxInfo
const prompt = spyOn(client.session, "prompt").mockImplementation(() => {
requested.resolve()
@@ -843,7 +843,7 @@ describe("V2 mini transport", () => {
})
const releaseSource = defer<void>()
let sourceLookups = 0
spyOn(client.session, "message").mockImplementation(async () => {
spyOn(client.session.message, "get").mockImplementation(async () => {
sourceLookups++
if (sourceLookups === 1) throw new Error("source temporarily unavailable")
await releaseSource.promise
@@ -1079,7 +1079,7 @@ describe("V2 mini transport", () => {
{
id: "msg_queued",
sessionID: "ses_1",
timeCreated: 1,
time: { created: 1 },
type: "user",
payload: {
text: "follow up",
@@ -1094,7 +1094,7 @@ describe("V2 mini transport", () => {
{
id: "msg_cancelled",
sessionID: "ses_1",
timeCreated: 2,
time: { created: 2 },
type: "user",
payload: { text: "remove me", files: [image] },
delivery,
@@ -3449,7 +3449,7 @@ describe("V2 mini transport", () => {
type: "user" as const,
payload: { text: input.text },
delivery: "steer" as const,
timeCreated: 2,
time: { created: 2 },
})
})
@@ -4049,7 +4049,7 @@ describe("V2 mini transport", () => {
type: "user",
payload: { text: "", files: [image] },
delivery: "queue",
timeCreated: 3,
time: { created: 3 },
},
],
},
+53 -25
View File
@@ -1064,10 +1064,10 @@
"summary": "Get session statistics"
}
},
"/api/session/import": {
"/api/experimental/session/import": {
"post": {
"tags": ["session"],
"operationId": "session.import",
"operationId": "experimental.session.import",
"parameters": [],
"security": [],
"responses": {
@@ -1166,10 +1166,10 @@
}
}
},
"/api/session/{sessionID}/export": {
"/api/experimental/session/{sessionID}/export": {
"get": {
"tags": ["session"],
"operationId": "session.export",
"operationId": "experimental.session.export",
"parameters": [
{
"name": "sessionID",
@@ -2804,8 +2804,8 @@
}
}
},
"/api/session/{sessionID}/revert/clear": {
"post": {
"/api/session/{sessionID}/revert": {
"delete": {
"tags": ["session"],
"operationId": "session.revert.clear",
"parameters": [
@@ -3478,10 +3478,10 @@
"summary": "Queue steered item"
}
},
"/api/session/{sessionID}/instructions/entries": {
"/api/experimental/session/{sessionID}/instructions/entries": {
"get": {
"tags": ["session"],
"operationId": "session.instructions.entry.list",
"operationId": "experimental.session.instructions.entry.list",
"parameters": [
{
"name": "sessionID",
@@ -3557,10 +3557,10 @@
"summary": "List instruction entries"
}
},
"/api/session/{sessionID}/instructions/entries/{key}": {
"/api/experimental/session/{sessionID}/instructions/entries/{key}": {
"put": {
"tags": ["session"],
"operationId": "session.instructions.entry.put",
"operationId": "experimental.session.instructions.entry.put",
"parameters": [
{
"name": "sessionID",
@@ -3653,7 +3653,7 @@
},
"delete": {
"tags": ["session"],
"operationId": "session.instructions.entry.remove",
"operationId": "experimental.session.instructions.entry.remove",
"parameters": [
{
"name": "sessionID",
@@ -4140,7 +4140,7 @@
"/api/session/{sessionID}/message/{messageID}": {
"get": {
"tags": ["session"],
"operationId": "session.message",
"operationId": "session.message.get",
"parameters": [
{
"name": "sessionID",
@@ -4372,7 +4372,7 @@
"/api/session/{sessionID}/message": {
"get": {
"tags": ["session"],
"operationId": "message.list",
"operationId": "session.message.list",
"parameters": [
{
"name": "sessionID",
@@ -17006,8 +17006,15 @@
"type": "string",
"pattern": "^ses"
},
"timeCreated": {
"type": "number"
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": ["created"],
"additionalProperties": false
},
"type": {
"type": "string",
@@ -17020,7 +17027,7 @@
"$ref": "#/components/schemas/Session.Inbox.Delivery"
}
},
"required": ["id", "sessionID", "timeCreated", "type", "payload", "delivery"],
"required": ["id", "sessionID", "time", "type", "payload", "delivery"],
"additionalProperties": false
},
"Session.Inbox.CompactionPayload": {
@@ -17064,8 +17071,15 @@
"type": "string",
"pattern": "^ses"
},
"timeCreated": {
"type": "number"
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": ["created"],
"additionalProperties": false
},
"type": {
"type": "string",
@@ -17078,7 +17092,7 @@
"$ref": "#/components/schemas/Session.Inbox.MovePayload"
}
},
"required": ["id", "sessionID", "timeCreated", "type", "delivery", "payload"],
"required": ["id", "sessionID", "time", "type", "delivery", "payload"],
"additionalProperties": false
},
"Session.Inbox.MovePayload": {
@@ -17108,8 +17122,15 @@
"type": "string",
"pattern": "^ses"
},
"timeCreated": {
"type": "number"
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": ["created"],
"additionalProperties": false
},
"type": {
"type": "string",
@@ -17122,7 +17143,7 @@
"$ref": "#/components/schemas/Session.Inbox.Delivery"
}
},
"required": ["id", "sessionID", "timeCreated", "type", "payload", "delivery"],
"required": ["id", "sessionID", "time", "type", "payload", "delivery"],
"additionalProperties": false
},
"Session.Inbox.SyntheticPayload": {
@@ -17152,8 +17173,15 @@
"type": "string",
"pattern": "^ses"
},
"timeCreated": {
"type": "number"
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": ["created"],
"additionalProperties": false
},
"type": {
"type": "string",
@@ -17166,7 +17194,7 @@
"$ref": "#/components/schemas/Session.Inbox.Delivery"
}
},
"required": ["id", "sessionID", "timeCreated", "type", "payload", "delivery"],
"required": ["id", "sessionID", "time", "type", "payload", "delivery"],
"additionalProperties": false
},
"Session.Inbox.UserPayload": {
+53 -25
View File
@@ -1064,10 +1064,10 @@
"summary": "Get session statistics"
}
},
"/api/session/import": {
"/api/experimental/session/import": {
"post": {
"tags": ["session"],
"operationId": "session.import",
"operationId": "experimental.session.import",
"parameters": [],
"security": [],
"responses": {
@@ -1166,10 +1166,10 @@
}
}
},
"/api/session/{sessionID}/export": {
"/api/experimental/session/{sessionID}/export": {
"get": {
"tags": ["session"],
"operationId": "session.export",
"operationId": "experimental.session.export",
"parameters": [
{
"name": "sessionID",
@@ -2804,8 +2804,8 @@
}
}
},
"/api/session/{sessionID}/revert/clear": {
"post": {
"/api/session/{sessionID}/revert": {
"delete": {
"tags": ["session"],
"operationId": "session.revert.clear",
"parameters": [
@@ -3478,10 +3478,10 @@
"summary": "Queue steered item"
}
},
"/api/session/{sessionID}/instructions/entries": {
"/api/experimental/session/{sessionID}/instructions/entries": {
"get": {
"tags": ["session"],
"operationId": "session.instructions.entry.list",
"operationId": "experimental.session.instructions.entry.list",
"parameters": [
{
"name": "sessionID",
@@ -3557,10 +3557,10 @@
"summary": "List instruction entries"
}
},
"/api/session/{sessionID}/instructions/entries/{key}": {
"/api/experimental/session/{sessionID}/instructions/entries/{key}": {
"put": {
"tags": ["session"],
"operationId": "session.instructions.entry.put",
"operationId": "experimental.session.instructions.entry.put",
"parameters": [
{
"name": "sessionID",
@@ -3653,7 +3653,7 @@
},
"delete": {
"tags": ["session"],
"operationId": "session.instructions.entry.remove",
"operationId": "experimental.session.instructions.entry.remove",
"parameters": [
{
"name": "sessionID",
@@ -4140,7 +4140,7 @@
"/api/session/{sessionID}/message/{messageID}": {
"get": {
"tags": ["session"],
"operationId": "session.message",
"operationId": "session.message.get",
"parameters": [
{
"name": "sessionID",
@@ -4372,7 +4372,7 @@
"/api/session/{sessionID}/message": {
"get": {
"tags": ["session"],
"operationId": "message.list",
"operationId": "session.message.list",
"parameters": [
{
"name": "sessionID",
@@ -17006,8 +17006,15 @@
"type": "string",
"pattern": "^ses"
},
"timeCreated": {
"type": "number"
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": ["created"],
"additionalProperties": false
},
"type": {
"type": "string",
@@ -17020,7 +17027,7 @@
"$ref": "#/components/schemas/Session.Inbox.Delivery"
}
},
"required": ["id", "sessionID", "timeCreated", "type", "payload", "delivery"],
"required": ["id", "sessionID", "time", "type", "payload", "delivery"],
"additionalProperties": false
},
"Session.Inbox.CompactionPayload": {
@@ -17064,8 +17071,15 @@
"type": "string",
"pattern": "^ses"
},
"timeCreated": {
"type": "number"
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": ["created"],
"additionalProperties": false
},
"type": {
"type": "string",
@@ -17078,7 +17092,7 @@
"$ref": "#/components/schemas/Session.Inbox.MovePayload"
}
},
"required": ["id", "sessionID", "timeCreated", "type", "delivery", "payload"],
"required": ["id", "sessionID", "time", "type", "delivery", "payload"],
"additionalProperties": false
},
"Session.Inbox.MovePayload": {
@@ -17108,8 +17122,15 @@
"type": "string",
"pattern": "^ses"
},
"timeCreated": {
"type": "number"
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": ["created"],
"additionalProperties": false
},
"type": {
"type": "string",
@@ -17122,7 +17143,7 @@
"$ref": "#/components/schemas/Session.Inbox.Delivery"
}
},
"required": ["id", "sessionID", "timeCreated", "type", "payload", "delivery"],
"required": ["id", "sessionID", "time", "type", "payload", "delivery"],
"additionalProperties": false
},
"Session.Inbox.SyntheticPayload": {
@@ -17152,8 +17173,15 @@
"type": "string",
"pattern": "^ses"
},
"timeCreated": {
"type": "number"
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": ["created"],
"additionalProperties": false
},
"type": {
"type": "string",
@@ -17166,7 +17194,7 @@
"$ref": "#/components/schemas/Session.Inbox.Delivery"
}
},
"required": ["id", "sessionID", "timeCreated", "type", "payload", "delivery"],
"required": ["id", "sessionID", "time", "type", "payload", "delivery"],
"additionalProperties": false
},
"Session.Inbox.UserPayload": {