From 648d01eec7246d04e9f8be5581a887904653ba69 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 14 Sep 2026 17:51:37 -0400 Subject: [PATCH] refactor(protocol): normalize session resources --- V2_HTTP_API_AUDIT.md | 54 ++++++------- .../app/e2e/regression/session-queue.spec.ts | 6 +- packages/app/e2e/utils/mock-api.ts | 2 +- packages/app/e2e/utils/mock-server.ts | 2 +- .../app/src/session/composer/queue.test.ts | 4 +- packages/app/src/session/revert.ts | 2 +- .../timeline/controller-projection.test.ts | 4 +- packages/cli/src/acp/event.ts | 2 +- .../src/commands/handlers/session/import.ts | 2 +- packages/cli/test/import-export.test.ts | 8 +- packages/cli/test/run/noninteractive.test.ts | 2 +- packages/client/src/effect/api/api.ts | 10 ++- .../client/src/effect/generated/client.ts | 10 +-- .../client/src/promise/generated/client.ts | 42 +++++----- .../client/src/promise/generated/types.ts | 12 +-- packages/client/src/solid/data.ts | 12 +-- packages/client/test/effect.test.ts | 14 ++-- packages/client/test/promise.test.ts | 16 ++-- packages/client/test/solid-compaction.test.ts | 2 +- packages/client/test/solid-eviction.test.ts | 2 +- packages/core/src/session/inbox.ts | 6 +- packages/core/test/config/command.test.ts | 4 +- packages/core/test/plugin/command.test.ts | 2 +- packages/core/test/plugin/plan.test.ts | 2 +- packages/protocol/openapi.json | 78 +++++++++++++------ packages/protocol/src/groups/message.ts | 2 +- packages/protocol/src/groups/session.ts | 62 +++++++++------ packages/schema/src/session-inbox.ts | 2 +- packages/schema/test/contract-hygiene.test.ts | 6 +- packages/sdk/test/embedded.test.ts | 6 +- packages/server/test/session-import.test.ts | 4 +- packages/tui/src/mini/stream-v2.subagent.ts | 2 +- packages/tui/src/mini/stream-v2.transport.ts | 4 +- packages/tui/test/cli/tui/data.test.tsx | 14 ++-- packages/tui/test/compact-admission.test.tsx | 4 +- .../tui/test/mini/stream-v2.transport.test.ts | 16 ++-- services/www/openapi.json | 78 +++++++++++++------ services/www/public/openapi.json | 78 +++++++++++++------ 38 files changed, 339 insertions(+), 239 deletions(-) diff --git a/V2_HTTP_API_AUDIT.md b/V2_HTTP_API_AUDIT.md index 59bc9b1c2ee..58a0f68007a 100644 --- a/V2_HTTP_API_AUDIT.md +++ b/V2_HTTP_API_AUDIT.md @@ -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. | diff --git a/packages/app/e2e/regression/session-queue.spec.ts b/packages/app/e2e/regression/session-queue.spec.ts index cc8d4b8829b..11ccedac3d9 100644 --- a/packages/app/e2e/regression/session-queue.spec.ts +++ b/packages/app/e2e/regression/session-queue.spec.ts @@ -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 } 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 : "", diff --git a/packages/app/e2e/utils/mock-api.ts b/packages/app/e2e/utils/mock-api.ts index d1685121837..0ffe08ffd9f 100644 --- a/packages/app/e2e/utils/mock-api.ts +++ b/packages/app/e2e/utils/mock-api.ts @@ -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, }), diff --git a/packages/app/e2e/utils/mock-server.ts b/packages/app/e2e/utils/mock-server.ts index 208c051266b..5f931d34b92 100644 --- a/packages/app/e2e/utils/mock-server.ts +++ b/packages/app/e2e/utils/mock-server.ts @@ -437,7 +437,7 @@ function mockHandlers(config: MockServerConfig, state: { cursors: Map 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( diff --git a/packages/app/src/session/timeline/controller-projection.test.ts b/packages/app/src/session/timeline/controller-projection.test.ts index 8d85e8dd544..8c2921a9ba9 100644 --- a/packages/app/src/session/timeline/controller-projection.test.ts +++ b/packages/app/src/session/timeline/controller-projection.test.ts @@ -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" }, diff --git a/packages/cli/src/acp/event.ts b/packages/cli/src/acp/event.ts index 0f1a6fe7a17..d237dc9597a 100644 --- a/packages/cli/src/acp/event.ts +++ b/packages/cli/src/acp/event.ts @@ -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( diff --git a/packages/cli/src/commands/handlers/session/import.ts b/packages/cli/src/commands/handlers/session/import.ts index d1d5cc5f25c..59a7b62fef5 100644 --- a/packages/cli/src/commands/handlers/session/import.ts +++ b/packages/cli/src/commands/handlers/session/import.ts @@ -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({ diff --git a/packages/cli/test/import-export.test.ts b/packages/cli/test/import-export.test.ts index 4e19794cf58..2fc928e85c3 100644 --- a/packages/cli/test/import-export.test.ts +++ b/packages/cli/test/import-export.test.ts @@ -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 }) }, }) diff --git a/packages/cli/test/run/noninteractive.test.ts b/packages/cli/test/run/noninteractive.test.ts index 4b0a4da3bf1..867d317007b 100644 --- a/packages/cli/test/run/noninteractive.test.ts +++ b/packages/cli/test/run/noninteractive.test.ts @@ -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, diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index 5b1b8e4f0ea..89fec2d4228 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -1319,9 +1319,11 @@ export type SessionBackgroundOperation = ( input: SessionBackgroundInput, ) => Effect.Effect -export type SessionMessageInput = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID } -export type SessionMessageOutput = SessionMessage.Info -export type SessionMessageOperation = (input: SessionMessageInput) => Effect.Effect +export type SessionMessageGetInput = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID } +export type SessionMessageGetOutput = SessionMessage.Info +export type SessionMessageGetOperation = ( + input: SessionMessageGetInput, +) => Effect.Effect export type SessionEnvironmentInput = { readonly sessionID: Session.ID @@ -1381,7 +1383,7 @@ export interface SessionApi { readonly log: SessionLogOperation readonly interrupt: SessionInterruptOperation readonly background: SessionBackgroundOperation - readonly message: SessionMessageOperation + readonly message: { readonly get: SessionMessageGetOperation } readonly environment: SessionEnvironmentOperation readonly view: SessionViewOperation } diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index 48cb2bd5533..a83aade7faf 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -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()( +const EndpointSessionMessageGet = (raw: RawClient["server.session"]) => (input: SessionMessageGetInput) => + preserveEffect()( 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), }) diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index 711c308f4cf..529d5fb8d7c 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -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( { - 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( { 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( { 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( { diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 068ca70605c..ab8873a5475 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -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"] diff --git a/packages/client/src/solid/data.ts b/packages/client/src/solid/data.ts index b1a082ce8d0..f5108f51aca 100644 --- a/packages/client/src/solid/data.ts +++ b/packages/client/src/solid/data.ts @@ -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 diff --git a/packages/client/test/effect.test.ts b/packages/client/test/effect.test.ts index b49b85e4f11..ca7a278ce4e 100644 --- a/packages/client/test/effect.test.ts +++ b/packages/client/test/effect.test.ts @@ -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 }, }, } diff --git a/packages/client/test/promise.test.ts b/packages/client/test/promise.test.ts index 2ac4f876288..ec8c835cd28 100644 --- a/packages/client/test/promise.test.ts +++ b/packages/client/test/promise.test.ts @@ -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 }, }, } diff --git a/packages/client/test/solid-compaction.test.ts b/packages/client/test/solid-compaction.test.ts index 1ea40d5d047..76f9d4bb207 100644 --- a/packages/client/test/solid-compaction.test.ts +++ b/packages/client/test/solid-compaction.test.ts @@ -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: {}, diff --git a/packages/client/test/solid-eviction.test.ts b/packages/client/test/solid-eviction.test.ts index 95b8a1f5c72..64b233a8425 100644 --- a/packages/client/test/solid-eviction.test.ts +++ b/packages/client/test/solid-eviction.test.ts @@ -142,7 +142,7 @@ function fixture(read?: (url: URL) => Promise) { type: "user", delivery: "steer", payload: { text: "pending" }, - timeCreated: 1, + time: { created: 1 }, }, ], }) diff --git a/packages/core/src/session/inbox.ts b/packages/core/src/session/inbox.ts index a91eed65af1..3688575e2e9 100644 --- a/packages/core/src/session/inbox.ts +++ b/packages/core/src/session/inbox.ts @@ -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, }), ), diff --git a/packages/core/test/config/command.test.ts b/packages/core/test/config/command.test.ts index 7f7f04b40b8..e3e7ea265ae 100644 --- a/packages/core/test/config/command.test.ts +++ b/packages/core/test/config/command.test.ts @@ -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", diff --git a/packages/core/test/plugin/command.test.ts b/packages/core/test/plugin/command.test.ts index 1d917a02396..3044a82e6fe 100644 --- a/packages/core/test/plugin/command.test.ts +++ b/packages/core/test/plugin/command.test.ts @@ -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", diff --git a/packages/core/test/plugin/plan.test.ts b/packages/core/test/plugin/plan.test.ts index 088cb5d1111..42780803f9d 100644 --- a/packages/core/test/plugin/plan.test.ts +++ b/packages/core/test/plugin/plan.test.ts @@ -96,7 +96,7 @@ const run = Effect.fnUntraced(function* (events: ReadonlyArray(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 = (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 = (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 = (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 = (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 = (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 = (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.", }), diff --git a/packages/schema/src/session-inbox.ts b/packages/schema/src/session-inbox.ts index baadcc947b1..361c0c492ce 100644 --- a/packages/schema/src/session-inbox.ts +++ b/packages/schema/src/session-inbox.ts @@ -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 {} diff --git a/packages/schema/test/contract-hygiene.test.ts b/packages/schema/test/contract-hygiene.test.ts index 7e5546b8ce4..179568180d9 100644 --- a/packages/schema/test/contract-hygiene.test.ts +++ b/packages/schema/test/contract-hygiene.test.ts @@ -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", diff --git a/packages/sdk/test/embedded.test.ts b/packages/sdk/test/embedded.test.ts index aa1dc538851..25aabe744c1 100644 --- a/packages/sdk/test/embedded.test.ts +++ b/packages/sdk/test/embedded.test.ts @@ -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, }), diff --git a/packages/server/test/session-import.test.ts b/packages/server/test/session-import.test.ts index 23d14746c20..913a41c1589 100644 --- a/packages/server/test/session-import.test.ts +++ b/packages/server/test/session-import.test.ts @@ -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, ) diff --git a/packages/tui/src/mini/stream-v2.subagent.ts b/packages/tui/src/mini/stream-v2.subagent.ts index efe5ab72ba9..cee7d4128a8 100644 --- a/packages/tui/src/mini/stream-v2.subagent.ts +++ b/packages/tui/src/mini/stream-v2.subagent.ts @@ -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 ( diff --git a/packages/tui/src/mini/stream-v2.transport.ts b/packages/tui/src/mini/stream-v2.transport.ts index da6a1342c74..f860cf125ef 100644 --- a/packages/tui/src/mini/stream-v2.transport.ts +++ b/packages/tui/src/mini/stream-v2.transport.ts @@ -926,7 +926,7 @@ export async function createSessionTransport(input: StreamInput): Promise - 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 { { 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", diff --git a/packages/tui/test/compact-admission.test.tsx b/packages/tui/test/compact-admission.test.tsx index d11aa1d09a3..058b880a495 100644 --- a/packages/tui/test/compact-admission.test.tsx +++ b/packages/tui/test/compact-admission.test.tsx @@ -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", }, diff --git a/packages/tui/test/mini/stream-v2.transport.test.ts b/packages/tui/test/mini/stream-v2.transport.test.ts index 647c5ed3da4..4e5c13f3a4e 100644 --- a/packages/tui/test/mini/stream-v2.transport.test.ts +++ b/packages/tui/test/mini/stream-v2.transport.test.ts @@ -91,7 +91,7 @@ function promptAdmission(input: Parameters[ 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() 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 }, }, ], }, diff --git a/services/www/openapi.json b/services/www/openapi.json index e16e27ec2f3..d19c8945e09 100644 --- a/services/www/openapi.json +++ b/services/www/openapi.json @@ -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": { diff --git a/services/www/public/openapi.json b/services/www/public/openapi.json index e16e27ec2f3..d19c8945e09 100644 --- a/services/www/public/openapi.json +++ b/services/www/public/openapi.json @@ -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": {