From b90e2fb6cc277b7ae582469fd64ad5d5b8c669c0 Mon Sep 17 00:00:00 2001 From: "denizirgin@gmail.com" Date: Tue, 8 Sep 2026 17:51:07 +0300 Subject: [PATCH 1/4] feat: refresh the accepted snapshot to f9bc223 Upstream renamed its workspace package scope from @opencode-ai to @opencode across 1,858 files, which moved the context the SSE Restore patch depends on and left refresh-spec refusing to apply it. The patch is rebased onto the new revision: its generate-openapi.ts hunk is byte-identical to the one it replaces, because that file did not move, and the session.ts hunk changes only the three import lines it uses as context. The repair predicate still holds, since raw upstream carries no contentSchema, so this is a rebase rather than the retirement the upstream fix will eventually force. The document delta is additive. The operation set digest is unchanged, so all 140 operations and their bodies are the ones already accepted and the hand-authored admission and curation lists need no rows. Three components arrive: Provider.Compaction, a mode-tagged union, plus Session.ProviderContext and its provenance record. Six existing components gain an optional property referencing them, and none of those properties joins a required set. Regeneration follows that delta exactly. IProviderCompaction and its two variants land beside the usual unknown carrier, the two context records are emitted, and four models gain a nullable property. The public API baseline moves by sixty-eight added lines and no removed line, so a nightly consumer meets no compile break. Every watched source was reviewed rather than re-pinned blindly. Sixteen moved by hash and every one of those changes is the scope rename, four did not move, and the single semantic change is a comment in pty-binding.ts recording that the pty daemon package deliberately keeps its old scope. The hand-written PTY and persistent-PTY doors therefore rest on unchanged upstream behavior, and all twenty-one anchors still match. --- external/opencode | 2 +- spec/SNAPSHOT.md | 4 +- spec/openapi.json | 88 +++++++++++++++++++ spec/patches/001-restore-sse-payloads.json | 2 +- spec/patches/001-restore-sse-payloads.patch | 10 +-- spec/receipt.json | 46 +++++----- spec/source-watch.json | 32 +++---- src/OpenCode.Sdk/.generated-manifest.json | 8 ++ .../Serialization/OpenCodeJsonContext.cs | 6 ++ .../ProviderCompactionJsonConverter.cs | 47 ++++++++++ .../UnknownProviderCompactionJsonConverter.cs | 48 ++++++++++ .../Models/IProviderCompaction.cs | 18 ++++ src/OpenCode.Sdk/Models/ModelInfo.cs | 7 ++ .../Models/ProviderCompactionLocal.cs | 16 ++++ .../Models/ProviderCompactionProvider.cs | 23 +++++ src/OpenCode.Sdk/Models/ProviderInfo.cs | 7 ++ .../Models/SessionCompactionEndedData.cs | 7 ++ .../SessionMessageCompactionCompleted.cs | 7 ++ .../Models/SessionProviderContext.cs | 29 ++++++ .../SessionProviderContextProvenance.cs | 46 ++++++++++ .../Models/UnknownProviderCompaction.cs | 41 +++++++++ .../Snapshots/PublicApi.verified.txt | 68 ++++++++++++++ 22 files changed, 514 insertions(+), 48 deletions(-) create mode 100644 src/OpenCode.Sdk/Internal/Serialization/ProviderCompactionJsonConverter.cs create mode 100644 src/OpenCode.Sdk/Internal/Serialization/UnknownProviderCompactionJsonConverter.cs create mode 100644 src/OpenCode.Sdk/Models/IProviderCompaction.cs create mode 100644 src/OpenCode.Sdk/Models/ProviderCompactionLocal.cs create mode 100644 src/OpenCode.Sdk/Models/ProviderCompactionProvider.cs create mode 100644 src/OpenCode.Sdk/Models/SessionProviderContext.cs create mode 100644 src/OpenCode.Sdk/Models/SessionProviderContextProvenance.cs create mode 100644 src/OpenCode.Sdk/Models/UnknownProviderCompaction.cs diff --git a/external/opencode b/external/opencode index 89f1943d2..f9bc2233d 160000 --- a/external/opencode +++ b/external/opencode @@ -1 +1 @@ -Subproject commit 89f1943d2da6af6087fed06a60c57bd0f384ab9f +Subproject commit f9bc2233ddcc11503a0804b317401bc221ef0e7f diff --git a/spec/SNAPSHOT.md b/spec/SNAPSHOT.md index fb61af1d6..05edab9d9 100644 --- a/spec/SNAPSHOT.md +++ b/spec/SNAPSHOT.md @@ -1,6 +1,6 @@ # OpenAPI Snapshot -Date: 2026-09-07 +Date: 2026-09-08 `openapi.json` is the accepted snapshot of the upstream opencode OpenAPI 3.1 document — the v2 protocol surface (ADR-0005). The SDK is built against this snapshot, never against a live @@ -14,7 +14,7 @@ their hash-pinned manifests. |---|---| | Upstream file | `packages/protocol/openapi.json` | | Upstream branch | `v2` (active successor line; no release tags yet) | -| Commit | `89f1943d2da6af6087fed06a60c57bd0f384ab9f` | +| Commit | `f9bc2233ddcc11503a0804b317401bc221ef0e7f` | | Upstream product channel | `opencode2` — npm `@opencode-ai/cli@next` (pre-release) | Platform evidence for the v2 line: internal research, 2026-08-16, "The opencode v2 platform: diff --git a/spec/openapi.json b/spec/openapi.json index 5e25138cf..45a37daf7 100644 --- a/spec/openapi.json +++ b/spec/openapi.json @@ -14377,6 +14377,9 @@ "Config.ModelEncoded": { "type": "object", "properties": { + "compaction": { + "$ref": "#/components/schemas/Provider.Compaction" + }, "modelID": { "type": "string" }, @@ -14482,6 +14485,9 @@ "Config.ProviderEncoded": { "type": "object", "properties": { + "compaction": { + "$ref": "#/components/schemas/Provider.Compaction" + }, "canonical": { "type": "string" }, @@ -16463,6 +16469,9 @@ "package": { "type": "string" }, + "compaction": { + "$ref": "#/components/schemas/Provider.Compaction" + }, "settings": { "type": "object" }, @@ -17396,6 +17405,36 @@ "required": ["id"], "additionalProperties": false }, + "Provider.Compaction": { + "anyOf": [ + { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": ["local"] + } + }, + "required": ["mode"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": ["provider"] + }, + "threshold": { + "type": "integer", + "exclusiveMinimum": 0 + } + }, + "required": ["mode"], + "additionalProperties": false + } + ] + }, "Provider.Info": { "type": "object", "properties": { @@ -17418,6 +17457,9 @@ "package": { "type": "string" }, + "compaction": { + "$ref": "#/components/schemas/Provider.Compaction" + }, "settings": { "type": "object" }, @@ -18709,6 +18751,9 @@ }, "recent": { "type": "string" + }, + "providerContext": { + "$ref": "#/components/schemas/Session.ProviderContext" } }, "required": ["type", "id", "time", "status", "reason", "summary", "recent"], @@ -19371,6 +19416,46 @@ "Session.Metadata": { "type": "object" }, + "Session.ProviderContext": { + "type": "object", + "properties": { + "version": { + "type": "number", + "enum": [1] + }, + "provenance": { + "$ref": "#/components/schemas/Session.ProviderContext.Provenance" + }, + "messages": {} + }, + "required": ["version", "provenance", "messages"], + "additionalProperties": false + }, + "Session.ProviderContext.Provenance": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "modelID": { + "type": "string" + }, + "route": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "endpoint": { + "type": "string" + } + }, + "required": ["providerID", "provider", "modelID", "route", "protocol", "endpoint"], + "additionalProperties": false + }, "Session.Revert": { "type": "object", "properties": { @@ -21933,6 +22018,9 @@ "providerState": { "$ref": "#/components/schemas/Session.Message.ProviderState_5" }, + "providerContext": { + "$ref": "#/components/schemas/Session.ProviderContext" + }, "text": { "type": "string" }, diff --git a/spec/patches/001-restore-sse-payloads.json b/spec/patches/001-restore-sse-payloads.json index 84b7d2ee6..5f9423828 100644 --- a/spec/patches/001-restore-sse-payloads.json +++ b/spec/patches/001-restore-sse-payloads.json @@ -1,6 +1,6 @@ { "patch": "001-restore-sse-payloads.patch", - "sha256": "aecd1686bc9b5e70a619e579fd87c6659fa299df53bb7b1fec17d7e8802cef10", + "sha256": "99e02b5452347d2dc87f108f2f46c54e471958728c5d96db584f72968babb544", "upstreamReport": "https://github.com/anomalyco/opencode/pull/45182", "order": 1, "touches": [ diff --git a/spec/patches/001-restore-sse-payloads.patch b/spec/patches/001-restore-sse-payloads.patch index 5df824a98..3e71a68ce 100644 --- a/spec/patches/001-restore-sse-payloads.patch +++ b/spec/patches/001-restore-sse-payloads.patch @@ -1,5 +1,5 @@ diff --git a/packages/protocol/script/generate-openapi.ts b/packages/protocol/script/generate-openapi.ts -index 61c4ddecd1..799ce56e43 100644 +index 61c4dde..799ce56 100644 --- a/packages/protocol/script/generate-openapi.ts +++ b/packages/protocol/script/generate-openapi.ts @@ -1,10 +1,38 @@ @@ -44,12 +44,12 @@ index 61c4ddecd1..799ce56e43 100644 printWidth: 120, }) diff --git a/packages/protocol/src/groups/session.ts b/packages/protocol/src/groups/session.ts -index f0f2b46e8e..426c77fadf 100644 +index ffac096..c1cf2a3 100644 --- a/packages/protocol/src/groups/session.ts +++ b/packages/protocol/src/groups/session.ts -@@ -31,6 +31,10 @@ import { Location } from "@opencode-ai/schema/location" - import { SessionEvent } from "@opencode-ai/schema/session-event" - import { EventLog } from "@opencode-ai/schema/event-log" +@@ -31,6 +31,10 @@ import { Location } from "@opencode/schema/location" + import { SessionEvent } from "@opencode/schema/session-event" + import { EventLog } from "@opencode/schema/event-log" +export const SessionLogItem = Schema.Union([SessionEvent.Durable, EventLog.Synced]).annotate({ + identifier: "SessionLogItem", diff --git a/spec/receipt.json b/spec/receipt.json index 30f714fe8..e39626593 100644 --- a/spec/receipt.json +++ b/spec/receipt.json @@ -1,12 +1,12 @@ { "schemaVersion": 1, - "upstreamCommit": "89f1943d2da6af6087fed06a60c57bd0f384ab9f", - "rawDocumentSha256": "72cfde2a06296df571e83ef9880a3bc3bdeb5d9679130f230b831d6778c777b7", - "generatedBaselineSha256": "f3e6a1b430db9e036bbe8fb34f1ba6bda11f84610e0710bede38ad520abb00f9", + "upstreamCommit": "f9bc2233ddcc11503a0804b317401bc221ef0e7f", + "rawDocumentSha256": "0bef23a00ff19c711cd871c7f0f9cc592ec81552aa94344f701ae380d2d752dc", + "generatedBaselineSha256": "180707057921ff2e1103066b0993db3646b19afbddf000ff7dba74c0cd36f179", "patches": [ { "manifest": "001-restore-sse-payloads.json", - "patchSha256": "aecd1686bc9b5e70a619e579fd87c6659fa299df53bb7b1fec17d7e8802cef10", + "patchSha256": "99e02b5452347d2dc87f108f2f46c54e471958728c5d96db584f72968babb544", "preimages": [ { "path": "packages/protocol/script/generate-openapi.ts", @@ -14,33 +14,33 @@ }, { "path": "packages/protocol/src/groups/session.ts", - "sha256": "16077799284f6bdbd8190fdd912ee428ac3b2e126190f25862cf67a2e6ee216d" + "sha256": "c4659072876fc3232650439cec9dd57d23b9c0b1666e3f8d1897cf1854b83047" } ] } ], - "normalizedDocumentSha256": "2ecd7091a49a8b1505b8a7e935368129399e156fed6d344c9ca551b218b7e2f3", + "normalizedDocumentSha256": "809ad52800fa56ad266d2c5283d254c82200c64dae46b19a6edeba6e65bf41e2", "normalizedDocumentPath": null, "operationSetDigest": "038fa8d3b8cf12a9a3db9320f5945e5c7aaa5d07158cebc6a0527e4c56a0a2f8", "operationCount": 140, "addedOperations": [], "removedOperations": [], - "componentCount": 352, + "componentCount": 355, "contentSchemaCount": 2, "watchedSources": [ { "path": "packages/server/src/handlers/pty.ts", - "sha256": "4414c9709c4a66efbd2a24d49400022e4154587f3b94785871dfd2eca9220a30", + "sha256": "537e0f86abf23a5682a170bfbba88d586d53d7f0fec8f0bde317ce95ac7f92ba", "anchorMatched": true }, { "path": "packages/server/src/middleware/authorization.ts", - "sha256": "7d3f8af2408e23ae35cf45d3693db034fd73d6ac8c07843154e3c8b3449529de", + "sha256": "c84f217a02bd2d2a839673308f9e268a2a68a1ac9f8c47c9c9e52f7f666e1486", "anchorMatched": true }, { "path": "packages/protocol/src/groups/pty.ts", - "sha256": "fc5c3e39167a4d87e457365f3d36a8223a024ef345abbd26324cb7a36ca3b886", + "sha256": "0ea314dda5ea647de176ae6f43294c107d5d9f1abc8a35820c5a783d5f718fc8", "anchorMatched": true }, { @@ -50,37 +50,37 @@ }, { "path": "packages/core/src/pty/ticket.ts", - "sha256": "8a8e5ca2d5081d38e0c4200fe41de3d95d91830309c40aa26d5f4485c70853c7", + "sha256": "e9415c803919c69800c3d0555781be795df91b9cf0d01c7776ac5091bb49c116", "anchorMatched": true }, { "path": "packages/core/src/pty.ts", - "sha256": "7d5ef3ebd36c473cc2acf047b99f8566011ccb64385bc04e9c771bc291367d59", + "sha256": "86ff2b792f1829b3f82badccffc1f63a95f98c7230f268c31d0f83c6f7340e6a", "anchorMatched": true }, { "path": "packages/server/src/routes.ts", - "sha256": "4a0f61b114eb087e422dc593a58634bef2b75947dafb48f1941cdfaaa613df95", + "sha256": "880e16a03cb2d6aa1deced31f36f8ac13b4f66a522a8ec613550b22ff0d04bd7", "anchorMatched": true }, { "path": "packages/protocol/src/groups/persistent-pty.ts", - "sha256": "114bf302f9542e7d1212886e9df04087c8a4315a1394884023d7c85e44ab683e", + "sha256": "a43d1e3c142b721186fb1f77afdb1da72f3c0c75cc71cba6c9f35f883afc1a08", "anchorMatched": true }, { "path": "packages/server/src/handlers/persistent-pty.ts", - "sha256": "181266261e78117a9c09233cacd807cb853883f83e70024f612001fa0ef7885b", + "sha256": "3bb6bd5e006cf5a69cbca2231ded3922a95405e8932a8c89af6009d11ccd096e", "anchorMatched": true }, { "path": "packages/core/src/persistent-pty/index.ts", - "sha256": "d34b09f60dbe8ddc9f2ca23550f57f24a8ac240a923d3d9790b7d154d343c754", + "sha256": "8b343fd3b60dbbd78314d23eb3ab2303fe30c986204ce4c049cfe6a6d4176f9e", "anchorMatched": true }, { "path": "packages/core/src/persistent-pty/daemon.ts", - "sha256": "446c6b28268a1155d07b22f5767885f32e126a00659cee6f51723d5dbd4b81d7", + "sha256": "01e3d99d3f59dc8fc54eb31c94674b3d574169212bd35252441a3aeb20886129", "anchorMatched": true }, { @@ -100,22 +100,22 @@ }, { "path": "packages/core/src/persistent-pty/pty-binding.ts", - "sha256": "843092936a62d5fbe0c43d24604786ed77c04d0ad60a96c19ca31508b69bc1cd", + "sha256": "c4c835ddf472a7a299914341a4635be18c87bb37de556c24749d991f36c5c876", "anchorMatched": true }, { "path": "packages/core/package.json", - "sha256": "4a0c6a39b98501f09acec8bf635e27e4798db9e1c4e1539c6146d0e021f858c2", + "sha256": "78ee3886360a8ece50c1ad67564586988e52ff5a9c8b18c90a48844668c21678", "anchorMatched": true }, { "path": "packages/server/src/process.ts", - "sha256": "2e7b36c11d53e947a4fa15ce0271b03664031125ffac77036975881c6a9de6ec", + "sha256": "fb3bbb748814edfa92901e7228dfae707c3c668c4442e82648bf2fc6eaff8974", "anchorMatched": true }, { "path": "packages/server/test/persistent-pty.test.ts", - "sha256": "025b44d37795c227ccd9b7f14762ecad3ef3100c9c413f39e8ab709e3b53f22b", + "sha256": "4beaf4903b95e1ecf600a5ad36e4b0e387e42c45850b1f84167af559e28f0284", "anchorMatched": true }, { @@ -125,12 +125,12 @@ }, { "path": "packages/core/src/ripgrep/binary.ts", - "sha256": "5e0398e1fe6bb5c2f694d01997781ef73060acd725468d7d4235a08ed96c41c6", + "sha256": "bc1bdc9e6429d8f586a8346ff58f5616e90f1755740f93fa938d266b34495d21", "anchorMatched": true }, { "path": "packages/tui/src/component/terminal-pane.tsx", - "sha256": "973e352937fe66c218e7ca1c955cdd335eaedf81dc1f57f1a97ba0777e4578ae", + "sha256": "093707d4950af377f9a49d0c4f43d4ec1a289b7d04d47d894ea9f713ce026371", "anchorMatched": true } ] diff --git a/spec/source-watch.json b/spec/source-watch.json index 76f8942db..a2753d829 100644 --- a/spec/source-watch.json +++ b/spec/source-watch.json @@ -3,7 +3,7 @@ "sources": [ { "path": "packages/server/src/handlers/pty.ts", - "sha256": "4414c9709c4a66efbd2a24d49400022e4154587f3b94785871dfd2eca9220a30", + "sha256": "537e0f86abf23a5682a170bfbba88d586d53d7f0fec8f0bde317ce95ac7f92ba", "behavior": "The PTY connect handler closes 4404 when the session is not found or has exited and 1000 on a normal end, consumes the single-use ticket, accepts a cursor at or above -1, and sends the replay as PtyProtocol.chunks followed by the meta frame.", "anchor": { "type": "contains", @@ -12,7 +12,7 @@ }, { "path": "packages/server/src/middleware/authorization.ts", - "sha256": "7d3f8af2408e23ae35cf45d3693db034fd73d6ac8c07843154e3c8b3449529de", + "sha256": "c84f217a02bd2d2a839673308f9e268a2a68a1ac9f8c47c9c9e52f7f666e1486", "behavior": "A non-empty ticket query on a PTY connect skips the credential check, so the hand-written door authenticates the WebSocket upgrade with the ticket alone.", "anchor": { "type": "contains", @@ -21,7 +21,7 @@ }, { "path": "packages/protocol/src/groups/pty.ts", - "sha256": "fc5c3e39167a4d87e457365f3d36a8223a024ef345abbd26324cb7a36ca3b886", + "sha256": "0ea314dda5ea647de176ae6f43294c107d5d9f1abc8a35820c5a783d5f718fc8", "behavior": "The ticket-issuing request carries the x-opencode-ticket header and the connect URL carries the ticket query.", "anchor": { "type": "contains", @@ -39,7 +39,7 @@ }, { "path": "packages/core/src/pty/ticket.ts", - "sha256": "8a8e5ca2d5081d38e0c4200fe41de3d95d91830309c40aa26d5f4485c70853c7", + "sha256": "e9415c803919c69800c3d0555781be795df91b9cf0d01c7776ac5091bb49c116", "behavior": "PTY connect tickets are single-use and live 60 seconds.", "anchor": { "type": "contains", @@ -48,7 +48,7 @@ }, { "path": "packages/core/src/pty.ts", - "sha256": "7d5ef3ebd36c473cc2acf047b99f8566011ccb64385bc04e9c771bc291367d59", + "sha256": "86ff2b792f1829b3f82badccffc1f63a95f98c7230f268c31d0f83c6f7340e6a", "behavior": "The retained replay buffer is capped, exited sessions stay observable until they are removed, and attach replays from the requested cursor.", "anchor": { "type": "contains", @@ -57,7 +57,7 @@ }, { "path": "packages/server/src/routes.ts", - "sha256": "4a0f61b114eb087e422dc593a58634bef2b75947dafb48f1941cdfaaa613df95", + "sha256": "880e16a03cb2d6aa1deced31f36f8ac13b4f66a522a8ec613550b22ff0d04bd7", "behavior": "The PTY ticket service and the persistent-PTY service are wired into the server layer graph.", "anchor": { "type": "contains", @@ -66,7 +66,7 @@ }, { "path": "packages/protocol/src/groups/persistent-pty.ts", - "sha256": "114bf302f9542e7d1212886e9df04087c8a4315a1394884023d7c85e44ab683e", + "sha256": "a43d1e3c142b721186fb1f77afdb1da72f3c0c75cc71cba6c9f35f883afc1a08", "behavior": "The handoff response is a single-key body whose handoff value is nullable, and the persistent connect endpoint reuses the PTY ticket query and header.", "anchor": { "type": "contains", @@ -75,7 +75,7 @@ }, { "path": "packages/server/src/handlers/persistent-pty.ts", - "sha256": "181266261e78117a9c09233cacd807cb853883f83e70024f612001fa0ef7885b", + "sha256": "3bb6bd5e006cf5a69cbca2231ded3922a95405e8932a8c89af6009d11ccd096e", "behavior": "The persistent connect handler closes 4404 when the terminal is unavailable and 1000 on a normal end, consumes the ticket, and requires a cursor at or above 0.", "anchor": { "type": "contains", @@ -84,7 +84,7 @@ }, { "path": "packages/core/src/persistent-pty/index.ts", - "sha256": "d34b09f60dbe8ddc9f2ca23550f57f24a8ac240a923d3d9790b7d154d343c754", + "sha256": "8b343fd3b60dbbd78314d23eb3ab2303fe30c986204ce4c049cfe6a6d4176f9e", "behavior": "The handoff call yields null when no daemon is running, and the service reports NotFoundError and UnavailableError.", "anchor": { "type": "contains", @@ -93,7 +93,7 @@ }, { "path": "packages/core/src/persistent-pty/daemon.ts", - "sha256": "446c6b28268a1155d07b22f5767885f32e126a00659cee6f51723d5dbd4b81d7", + "sha256": "01e3d99d3f59dc8fc54eb31c94674b3d574169212bd35252441a3aeb20886129", "behavior": "The opencode-pty daemon wire protocol is version 7; the client refuses a daemon that registers another version.", "anchor": { "type": "contains", @@ -129,7 +129,7 @@ }, { "path": "packages/core/src/persistent-pty/pty-binding.ts", - "sha256": "843092936a62d5fbe0c43d24604786ed77c04d0ad60a96c19ca31508b69bc1cd", + "sha256": "c4c835ddf472a7a299914341a4635be18c87bb37de556c24749d991f36c5c876", "behavior": "The embedded daemon asset comes from the binaryPath export of the pty package.", "anchor": { "type": "contains", @@ -138,7 +138,7 @@ }, { "path": "packages/core/package.json", - "sha256": "4a0c6a39b98501f09acec8bf635e27e4798db9e1c4e1539c6146d0e021f858c2", + "sha256": "78ee3886360a8ece50c1ad67564586988e52ff5a9c8b18c90a48844668c21678", "behavior": "The opencode-pty daemon dependency is pinned at 0.1.13, which fixes the daemon protocol the persistent-PTY door speaks.", "anchor": { "type": "contains", @@ -147,7 +147,7 @@ }, { "path": "packages/server/src/process.ts", - "sha256": "2e7b36c11d53e947a4fa15ce0271b03664031125ffac77036975881c6a9de6ec", + "sha256": "fb3bbb748814edfa92901e7228dfae707c3c668c4442e82648bf2fc6eaff8974", "behavior": "A ticketed PTY connect is admitted before the server reports ready.", "anchor": { "type": "contains", @@ -156,7 +156,7 @@ }, { "path": "packages/server/test/persistent-pty.test.ts", - "sha256": "025b44d37795c227ccd9b7f14762ecad3ef3100c9c413f39e8ab709e3b53f22b", + "sha256": "4beaf4903b95e1ecf600a5ad36e4b0e387e42c45850b1f84167af559e28f0284", "behavior": "Behavioral oracle: the upstream suite asserts the 4404 close code, the null handoff body, and terminal survival across an explicit restart handoff.", "anchor": { "type": "contains", @@ -174,7 +174,7 @@ }, { "path": "packages/core/src/ripgrep/binary.ts", - "sha256": "5e0398e1fe6bb5c2f694d01997781ef73060acd725468d7d4235a08ed96c41c6", + "sha256": "bc1bdc9e6429d8f586a8346ff58f5616e90f1755740f93fa938d266b34495d21", "behavior": "The ripgrep version upstream downloads on demand. CI provisions that exact version with pinned digests so an offline simulated server never has to reach the network for it, so a moved constant must be reviewed with the refresh rather than discovered as a failing filesystem live test.", "anchor": { "type": "contains", @@ -183,7 +183,7 @@ }, { "path": "packages/tui/src/component/terminal-pane.tsx", - "sha256": "973e352937fe66c218e7ca1c955cdd335eaedf81dc1f57f1a97ba0777e4578ae", + "sha256": "093707d4950af377f9a49d0c4f43d4ec1a289b7d04d47d894ea9f713ce026371", "behavior": "Behavioral oracle: the TUI resumes a terminal from the snapshot tail cursor.", "anchor": { "type": "contains", diff --git a/src/OpenCode.Sdk/.generated-manifest.json b/src/OpenCode.Sdk/.generated-manifest.json index 6e6715f73..e66143442 100644 --- a/src/OpenCode.Sdk/.generated-manifest.json +++ b/src/OpenCode.Sdk/.generated-manifest.json @@ -230,6 +230,7 @@ "Internal/Serialization/PluginSourceJsonConverter.cs", "Internal/Serialization/PluginStateJsonConverter.cs", "Internal/Serialization/PromptFileSourceJsonConverter.cs", + "Internal/Serialization/ProviderCompactionJsonConverter.cs", "Internal/Serialization/ProviderListResponseEnvelope.cs", "Internal/Serialization/ProviderResponseEnvelope.cs", "Internal/Serialization/PtyConnectTokenPostResponseEnvelope.cs", @@ -300,6 +301,7 @@ "Internal/Serialization/UnknownPluginSourceJsonConverter.cs", "Internal/Serialization/UnknownPluginStateJsonConverter.cs", "Internal/Serialization/UnknownPromptFileSourceJsonConverter.cs", + "Internal/Serialization/UnknownProviderCompactionJsonConverter.cs", "Internal/Serialization/UnknownReferenceSourceJsonConverter.cs", "Internal/Serialization/UnknownSessionEventDurableJsonConverter.cs", "Internal/Serialization/UnknownSessionForkBoundaryJsonConverter.cs", @@ -419,6 +421,7 @@ "Models/IPluginSource.cs", "Models/IPluginState.cs", "Models/IPromptFileSource.cs", + "Models/IProviderCompaction.cs", "Models/IReferenceSource.cs", "Models/ISessionEventDurable.cs", "Models/ISessionForkBoundary.cs", @@ -572,6 +575,8 @@ "Models/PromptInputSkillAttachment.cs", "Models/PromptMention.cs", "Models/PromptSkillAttachment.cs", + "Models/ProviderCompactionLocal.cs", + "Models/ProviderCompactionProvider.cs", "Models/ProviderInfo.cs", "Models/ProviderInfoActivation.cs", "Models/ProviderNotFoundError.cs", @@ -750,6 +755,8 @@ "Models/SessionPermissionCreateRequest.cs", "Models/SessionPermissionReplyPostRequest.cs", "Models/SessionPromptPostRequest.cs", + "Models/SessionProviderContext.cs", + "Models/SessionProviderContextProvenance.cs", "Models/SessionReasoningDelta.cs", "Models/SessionReasoningDeltaData.cs", "Models/SessionReasoningEnded.cs", @@ -909,6 +916,7 @@ "Models/UnknownPluginSource.cs", "Models/UnknownPluginState.cs", "Models/UnknownPromptFileSource.cs", + "Models/UnknownProviderCompaction.cs", "Models/UnknownReferenceSource.cs", "Models/UnknownSessionEventDurable.cs", "Models/UnknownSessionForkBoundary.cs", diff --git a/src/OpenCode.Sdk/Internal/Serialization/OpenCodeJsonContext.cs b/src/OpenCode.Sdk/Internal/Serialization/OpenCodeJsonContext.cs index df96446d7..04a952df4 100644 --- a/src/OpenCode.Sdk/Internal/Serialization/OpenCodeJsonContext.cs +++ b/src/OpenCode.Sdk/Internal/Serialization/OpenCodeJsonContext.cs @@ -83,6 +83,7 @@ namespace OpenCode.Sdk.Internal.Serialization; [JsonSerializable(typeof(IPluginSource))] [JsonSerializable(typeof(IPluginState))] [JsonSerializable(typeof(IPromptFileSource))] +[JsonSerializable(typeof(IProviderCompaction))] [JsonSerializable(typeof(IReadOnlyList))] [JsonSerializable(typeof(IReferenceSource))] [JsonSerializable(typeof(ISessionEventDurable))] @@ -260,6 +261,8 @@ namespace OpenCode.Sdk.Internal.Serialization; [JsonSerializable(typeof(PromptInputSkillAttachment))] [JsonSerializable(typeof(PromptMention))] [JsonSerializable(typeof(PromptSkillAttachment))] +[JsonSerializable(typeof(ProviderCompactionLocal))] +[JsonSerializable(typeof(ProviderCompactionProvider))] [JsonSerializable(typeof(ProviderInfo))] [JsonSerializable(typeof(ProviderInfoActivation))] [JsonSerializable(typeof(ProviderListResponseEnvelope))] @@ -468,6 +471,8 @@ namespace OpenCode.Sdk.Internal.Serialization; [JsonSerializable(typeof(SessionPermissionResponseEnvelope))] [JsonSerializable(typeof(SessionPromptPostRequest))] [JsonSerializable(typeof(SessionPromptPostResponseEnvelope))] +[JsonSerializable(typeof(SessionProviderContext))] +[JsonSerializable(typeof(SessionProviderContextProvenance))] [JsonSerializable(typeof(SessionReasoningDelta))] [JsonSerializable(typeof(SessionReasoningDeltaData))] [JsonSerializable(typeof(SessionReasoningEnded))] @@ -637,6 +642,7 @@ namespace OpenCode.Sdk.Internal.Serialization; [JsonSerializable(typeof(UnknownPluginSource))] [JsonSerializable(typeof(UnknownPluginState))] [JsonSerializable(typeof(UnknownPromptFileSource))] +[JsonSerializable(typeof(UnknownProviderCompaction))] [JsonSerializable(typeof(UnknownReferenceSource))] [JsonSerializable(typeof(UnknownSessionEventDurable))] [JsonSerializable(typeof(UnknownSessionForkBoundary))] diff --git a/src/OpenCode.Sdk/Internal/Serialization/ProviderCompactionJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/ProviderCompactionJsonConverter.cs new file mode 100644 index 000000000..2447c01fd --- /dev/null +++ b/src/OpenCode.Sdk/Internal/Serialization/ProviderCompactionJsonConverter.cs @@ -0,0 +1,47 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Collections.Frozen; +using System.Text.Json; +using System.Text.Json.Serialization; +using OpenCode.Sdk.Models; + +namespace OpenCode.Sdk.Internal.Serialization; + +internal sealed class ProviderCompactionJsonConverter : JsonConverter +{ + private static readonly FrozenDictionary TypesByTag = new Dictionary(StringComparer.Ordinal) + { + ["local"] = typeof(ProviderCompactionLocal), + ["provider"] = typeof(ProviderCompactionProvider) + }.ToFrozenDictionary(StringComparer.Ordinal); + private static readonly UnionDiscriminatorReader DiscriminatorReader = new(); + public override IProviderCompaction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(typeToConvert); + ArgumentNullException.ThrowIfNull(options); + if (DiscriminatorReader.TryFindKnown(ref reader, "mode", "ProviderCompaction", TypesByTag, out var targetType, out var marker)) + { + var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(targetType) ?? throw new JsonException("The generated context has no metadata for ProviderCompaction."); + return JsonSerializer.Deserialize(ref reader, typeInfo) as IProviderCompaction ?? throw new JsonException("The ProviderCompaction payload deserialized to null."); + } + + using var document = JsonDocument.ParseValue(ref reader); + var payload = document.RootElement; + return new UnknownProviderCompaction(marker, payload); + } + + public override void Write(Utf8JsonWriter writer, IProviderCompaction value, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(writer); + ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(options); + if (value is UnknownProviderCompaction unknown) + { + unknown.Payload.WriteTo(writer); + return; + } + + var typeInfo = OpenCodeJsonContext.Default.GetTypeInfo(value.GetType()) ?? throw new JsonException("The generated context has no metadata for ProviderCompaction."); + JsonSerializer.Serialize(writer, value, typeInfo); + } +} diff --git a/src/OpenCode.Sdk/Internal/Serialization/UnknownProviderCompactionJsonConverter.cs b/src/OpenCode.Sdk/Internal/Serialization/UnknownProviderCompactionJsonConverter.cs new file mode 100644 index 000000000..995d08a06 --- /dev/null +++ b/src/OpenCode.Sdk/Internal/Serialization/UnknownProviderCompactionJsonConverter.cs @@ -0,0 +1,48 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json; +using System.Text.Json.Serialization; +using OpenCode.Sdk.Models; + +namespace OpenCode.Sdk.Internal.Serialization; + +internal sealed class UnknownProviderCompactionJsonConverter : JsonConverter +{ + public override UnknownProviderCompaction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(typeToConvert); + ArgumentNullException.ThrowIfNull(options); + using var document = JsonDocument.ParseValue(ref reader); + var payload = document.RootElement; + if (payload.ValueKind != JsonValueKind.Object) + { + throw new JsonException("The ProviderCompaction payload must be a JSON object."); + } + + if (!payload.TryGetProperty("mode", out var markerElement)) + { + throw new JsonException("The ProviderCompaction payload must contain 'mode'."); + } + + if (markerElement.ValueKind != JsonValueKind.String) + { + throw new JsonException("The 'mode' marker must be a string."); + } + + var marker = markerElement.GetString(); + if (marker is null || string.IsNullOrWhiteSpace(marker)) + { + throw new JsonException("The 'mode' marker must be a non-empty string."); + } + + return new UnknownProviderCompaction(marker, payload); + } + + public override void Write(Utf8JsonWriter writer, UnknownProviderCompaction value, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(writer); + ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(options); + value.Payload.WriteTo(writer); + } +} diff --git a/src/OpenCode.Sdk/Models/IProviderCompaction.cs b/src/OpenCode.Sdk/Models/IProviderCompaction.cs new file mode 100644 index 000000000..1d77f516c --- /dev/null +++ b/src/OpenCode.Sdk/Models/IProviderCompaction.cs @@ -0,0 +1,18 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a provider compaction union. +/// +[JsonConverter(typeof(ProviderCompactionJsonConverter))] +public interface IProviderCompaction +{ + /// + /// Gets the 'mode' union marker. + /// + [JsonPropertyName("mode")] + public string Mode { get; } +} diff --git a/src/OpenCode.Sdk/Models/ModelInfo.cs b/src/OpenCode.Sdk/Models/ModelInfo.cs index 05c1b60e5..931b047e3 100644 --- a/src/OpenCode.Sdk/Models/ModelInfo.cs +++ b/src/OpenCode.Sdk/Models/ModelInfo.cs @@ -61,6 +61,13 @@ public sealed record ModelInfo [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Package { get; init; } + /// + /// Gets the compaction value. + /// + [JsonPropertyName("compaction")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IProviderCompaction? Compaction { get; init; } + /// /// Gets the settings value. /// diff --git a/src/OpenCode.Sdk/Models/ProviderCompactionLocal.cs b/src/OpenCode.Sdk/Models/ProviderCompactionLocal.cs new file mode 100644 index 000000000..909943181 --- /dev/null +++ b/src/OpenCode.Sdk/Models/ProviderCompactionLocal.cs @@ -0,0 +1,16 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a provider compaction local value. +/// +public sealed record ProviderCompactionLocal : IProviderCompaction +{ + /// + /// Gets the mode value. + /// + [JsonPropertyName("mode")] + public string Mode => "local"; +} diff --git a/src/OpenCode.Sdk/Models/ProviderCompactionProvider.cs b/src/OpenCode.Sdk/Models/ProviderCompactionProvider.cs new file mode 100644 index 000000000..f39ae2d92 --- /dev/null +++ b/src/OpenCode.Sdk/Models/ProviderCompactionProvider.cs @@ -0,0 +1,23 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a provider compaction provider value. +/// +public sealed record ProviderCompactionProvider : IProviderCompaction +{ + /// + /// Gets the mode value. + /// + [JsonPropertyName("mode")] + public string Mode => "provider"; + + /// + /// Gets the threshold value. + /// + [JsonPropertyName("threshold")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public long? Threshold { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/ProviderInfo.cs b/src/OpenCode.Sdk/Models/ProviderInfo.cs index 26b687d05..c93acef42 100644 --- a/src/OpenCode.Sdk/Models/ProviderInfo.cs +++ b/src/OpenCode.Sdk/Models/ProviderInfo.cs @@ -47,6 +47,13 @@ public sealed record ProviderInfo [JsonPropertyName("package")] public required string Package { get; init; } + /// + /// Gets the compaction value. + /// + [JsonPropertyName("compaction")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IProviderCompaction? Compaction { get; init; } + /// /// Gets the settings value. /// diff --git a/src/OpenCode.Sdk/Models/SessionCompactionEndedData.cs b/src/OpenCode.Sdk/Models/SessionCompactionEndedData.cs index 3c0c33ab9..7b54b26d1 100644 --- a/src/OpenCode.Sdk/Models/SessionCompactionEndedData.cs +++ b/src/OpenCode.Sdk/Models/SessionCompactionEndedData.cs @@ -35,6 +35,13 @@ public sealed record SessionCompactionEndedData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IReadOnlyDictionary? ProviderState { get; init; } + /// + /// Gets the provider context value. + /// + [JsonPropertyName("providerContext")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SessionProviderContext? ProviderContext { get; init; } + /// /// Gets the text value. /// diff --git a/src/OpenCode.Sdk/Models/SessionMessageCompactionCompleted.cs b/src/OpenCode.Sdk/Models/SessionMessageCompactionCompleted.cs index 563f6c8ad..4e236deb1 100644 --- a/src/OpenCode.Sdk/Models/SessionMessageCompactionCompleted.cs +++ b/src/OpenCode.Sdk/Models/SessionMessageCompactionCompleted.cs @@ -71,4 +71,11 @@ public sealed record SessionMessageCompactionCompleted : ISessionMessageCompacti /// [JsonPropertyName("recent")] public required string Recent { get; init; } + + /// + /// Gets the provider context value. + /// + [JsonPropertyName("providerContext")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SessionProviderContext? ProviderContext { get; init; } } diff --git a/src/OpenCode.Sdk/Models/SessionProviderContext.cs b/src/OpenCode.Sdk/Models/SessionProviderContext.cs new file mode 100644 index 000000000..ad02a43cf --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionProviderContext.cs @@ -0,0 +1,29 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a session provider context value. +/// +public sealed record SessionProviderContext +{ + /// + /// Gets the version value. + /// + [JsonPropertyName("version")] + public required double Version { get; init; } + + /// + /// Gets the provenance value. + /// + [JsonPropertyName("provenance")] + public required SessionProviderContextProvenance Provenance { get; init; } + + /// + /// Gets the messages value. + /// + [JsonPropertyName("messages")] + public required JsonElement Messages { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/SessionProviderContextProvenance.cs b/src/OpenCode.Sdk/Models/SessionProviderContextProvenance.cs new file mode 100644 index 000000000..8ff95fb6d --- /dev/null +++ b/src/OpenCode.Sdk/Models/SessionProviderContextProvenance.cs @@ -0,0 +1,46 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Represents a session provider context provenance value. +/// +public sealed record SessionProviderContextProvenance +{ + /// + /// Gets the provider id value. + /// + [JsonPropertyName("providerID")] + public required string ProviderId { get; init; } + + /// + /// Gets the provider value. + /// + [JsonPropertyName("provider")] + public required string Provider { get; init; } + + /// + /// Gets the model id value. + /// + [JsonPropertyName("modelID")] + public required string ModelId { get; init; } + + /// + /// Gets the route value. + /// + [JsonPropertyName("route")] + public required string Route { get; init; } + + /// + /// Gets the protocol value. + /// + [JsonPropertyName("protocol")] + public required string Protocol { get; init; } + + /// + /// Gets the endpoint value. + /// + [JsonPropertyName("endpoint")] + public required string Endpoint { get; init; } +} diff --git a/src/OpenCode.Sdk/Models/UnknownProviderCompaction.cs b/src/OpenCode.Sdk/Models/UnknownProviderCompaction.cs new file mode 100644 index 000000000..a064bd4e8 --- /dev/null +++ b/src/OpenCode.Sdk/Models/UnknownProviderCompaction.cs @@ -0,0 +1,41 @@ +// Generated by OpenCode.Sdk.Tools from spec/openapi.json. +// Do not edit by hand — change tools/curation.json or the emitters, then regenerate. +using System.Text.Json; +using System.Text.Json.Serialization; +using OpenCode.Sdk.Internal.Serialization; + +namespace OpenCode.Sdk.Models; +/// +/// Preserves an unknown provider compaction payload. +/// +[JsonConverter(typeof(UnknownProviderCompactionJsonConverter))] +public sealed record UnknownProviderCompaction : IProviderCompaction +{ + private readonly string _marker; + /// + /// Initializes an unknown union value from its marker and raw payload. + /// + [JsonConstructor] + public UnknownProviderCompaction(string mode, JsonElement payload) + { + ArgumentException.ThrowIfNullOrWhiteSpace(mode); + if (payload.ValueKind is JsonValueKind.Undefined) + { + throw new ArgumentException("The payload must be a parsed JSON element.", nameof(payload)); + } + + UnionPayloadGuard.Instance.RequireString(payload, "mode", mode); + _marker = mode; + Payload = payload.Clone(); + } + + /// + /// Gets the unrecognized 'mode' marker. + /// + [JsonPropertyName("mode")] + public string Mode => _marker; + /// + /// Gets the preserved raw JSON payload. + /// + public JsonElement Payload { get; } +} diff --git a/tests/OpenCode.Sdk.Tests/Snapshots/PublicApi.verified.txt b/tests/OpenCode.Sdk.Tests/Snapshots/PublicApi.verified.txt index 103d44a0c..806b3ce17 100644 --- a/tests/OpenCode.Sdk.Tests/Snapshots/PublicApi.verified.txt +++ b/tests/OpenCode.Sdk.Tests/Snapshots/PublicApi.verified.txt @@ -2857,6 +2857,12 @@ namespace OpenCode.Sdk.Models [System.Text.Json.Serialization.JsonPropertyName("type")] string Type { get; } } + [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.ProviderCompactionJsonConverter))] + public interface IProviderCompaction + { + [System.Text.Json.Serialization.JsonPropertyName("mode")] + string Mode { get; } + } [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.ReferenceSourceJsonConverter))] public interface IReferenceSource { @@ -3754,6 +3760,9 @@ namespace OpenCode.Sdk.Models [System.Text.Json.Serialization.JsonPropertyName("capabilities")] public required OpenCode.Sdk.Models.ModelCapabilities Capabilities { get; init; } [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("compaction")] + public OpenCode.Sdk.Models.IProviderCompaction? Compaction { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] [System.Text.Json.Serialization.JsonPropertyName("compatibility")] public OpenCode.Sdk.Models.ModelCompatibility? Compatibility { get; init; } [System.Text.Json.Serialization.JsonPropertyName("cost")] @@ -4580,6 +4589,21 @@ namespace OpenCode.Sdk.Models [System.Text.Json.Serialization.JsonPropertyName("text")] public string? Text { get; init; } } + public sealed class ProviderCompactionLocal : OpenCode.Sdk.Models.IProviderCompaction, System.IEquatable + { + public ProviderCompactionLocal() { } + [System.Text.Json.Serialization.JsonPropertyName("mode")] + public string Mode { get; } + } + public sealed class ProviderCompactionProvider : OpenCode.Sdk.Models.IProviderCompaction, System.IEquatable + { + public ProviderCompactionProvider() { } + [System.Text.Json.Serialization.JsonPropertyName("mode")] + public string Mode { get; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("threshold")] + public long? Threshold { get; init; } + } public sealed class ProviderInfo : System.IEquatable { public ProviderInfo() { } @@ -4592,6 +4616,9 @@ namespace OpenCode.Sdk.Models [System.Text.Json.Serialization.JsonPropertyName("canonical")] public string? Canonical { get; init; } [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("compaction")] + public OpenCode.Sdk.Models.IProviderCompaction? Compaction { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] [System.Text.Json.Serialization.JsonPropertyName("headers")] public System.Collections.Generic.IReadOnlyDictionary? Headers { get; init; } [System.Text.Json.Serialization.JsonPropertyName("id")] @@ -5099,6 +5126,9 @@ namespace OpenCode.Sdk.Models [System.Text.Json.Serialization.JsonPropertyName("model")] public OpenCode.Sdk.Models.ModelRef? Model { get; init; } [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("providerContext")] + public OpenCode.Sdk.Models.SessionProviderContext? ProviderContext { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] [System.Text.Json.Serialization.JsonPropertyName("providerState")] public System.Collections.Generic.IReadOnlyDictionary? ProviderState { get; init; } [System.Text.Json.Serialization.JsonPropertyName("reason")] @@ -6304,6 +6334,9 @@ namespace OpenCode.Sdk.Models [System.Text.Json.Serialization.JsonPropertyName("model")] public OpenCode.Sdk.Models.ModelRef? Model { get; init; } [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + [System.Text.Json.Serialization.JsonPropertyName("providerContext")] + public OpenCode.Sdk.Models.SessionProviderContext? ProviderContext { get; init; } + [System.Text.Json.Serialization.JsonIgnore(Condition=System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] [System.Text.Json.Serialization.JsonPropertyName("providerState")] public System.Collections.Generic.IReadOnlyDictionary? ProviderState { get; init; } [System.Text.Json.Serialization.JsonPropertyName("reason")] @@ -6895,6 +6928,32 @@ namespace OpenCode.Sdk.Models [System.Text.Json.Serialization.JsonPropertyName("text")] public required string Text { get; init; } } + public sealed class SessionProviderContext : System.IEquatable + { + public SessionProviderContext() { } + [System.Text.Json.Serialization.JsonPropertyName("messages")] + public required System.Text.Json.JsonElement Messages { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("provenance")] + public required OpenCode.Sdk.Models.SessionProviderContextProvenance Provenance { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("version")] + public required double Version { get; init; } + } + public sealed class SessionProviderContextProvenance : System.IEquatable + { + public SessionProviderContextProvenance() { } + [System.Text.Json.Serialization.JsonPropertyName("endpoint")] + public required string Endpoint { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("modelID")] + public required string ModelId { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("protocol")] + public required string Protocol { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("provider")] + public required string Provider { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("providerID")] + public required string ProviderId { get; init; } + [System.Text.Json.Serialization.JsonPropertyName("route")] + public required string Route { get; init; } + } public sealed class SessionReasoningDelta : OpenCode.Sdk.Models.IEvent, System.IEquatable { public SessionReasoningDelta() { } @@ -8962,6 +9021,15 @@ namespace OpenCode.Sdk.Models [System.Text.Json.Serialization.JsonPropertyName("type")] public string Type { get; } } + [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.UnknownProviderCompactionJsonConverter))] + public sealed class UnknownProviderCompaction : OpenCode.Sdk.Models.IProviderCompaction, System.IEquatable + { + [System.Text.Json.Serialization.JsonConstructor] + public UnknownProviderCompaction(string mode, System.Text.Json.JsonElement payload) { } + [System.Text.Json.Serialization.JsonPropertyName("mode")] + public string Mode { get; } + public System.Text.Json.JsonElement Payload { get; } + } [System.Text.Json.Serialization.JsonConverter(typeof(OpenCode.Sdk.Internal.Serialization.UnknownReferenceSourceJsonConverter))] public sealed class UnknownReferenceSource : OpenCode.Sdk.Models.IReferenceSource, System.IEquatable { From 6b291228ffcb8e4b8d8b0995f12acec5921ff569 Mon Sep 17 00:00:00 2001 From: "denizirgin@gmail.com" Date: Tue, 8 Sep 2026 18:08:31 +0300 Subject: [PATCH 2/4] docs: record that the CLI package renamed ahead of its npm channel The refreshed pin renames the CLI package to @opencode/cli along with the rest of upstream's workspace scope, which reads like an instruction to install something that does not exist yet. npm's @opencode scope publishes a reserved placeholder on latest and moves only beta and dev, so @opencode-ai/cli@next remains the pre-release a consumer can actually install. The snapshot now says both, because the identity table names the channel rather than the source package and the two have diverged. --- spec/SNAPSHOT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/SNAPSHOT.md b/spec/SNAPSHOT.md index 05edab9d9..1f52d760f 100644 --- a/spec/SNAPSHOT.md +++ b/spec/SNAPSHOT.md @@ -17,6 +17,11 @@ their hash-pinned manifests. | Commit | `f9bc2233ddcc11503a0804b317401bc221ef0e7f` | | Upstream product channel | `opencode2` — npm `@opencode-ai/cli@next` (pre-release) | +At this pin the CLI package is named `@opencode/cli`, following upstream's rename of its workspace +scope from `@opencode-ai`. The published channel has not moved with it: npm's `@opencode` scope +carries a reserved placeholder on `latest`, so `@opencode-ai/cli@next` stays the installable +pre-release and is what the row above names. + Platform evidence for the v2 line: internal research, 2026-08-16, "The opencode v2 platform: branch, surface, architecture, distribution". From cfd55c8c99219f0707df41f4dd60689f50d165fa Mon Sep 17 00:00:00 2001 From: "denizirgin@gmail.com" Date: Tue, 8 Sep 2026 19:58:06 +0300 Subject: [PATCH 3/4] fix(test): point the simulation host at upstream's renamed scope The refreshed pin renames upstream's workspace scope from @opencode-ai to @opencode, and the persistent simulation host imports five of those packages by name. Its imports still carried the old scope, so the pinned server exited before readiness with "Cannot find module '@opencode-ai/util/effect/layer-node'" and every test that needs a real server failed. A local run did not catch this. The submodule moves under an existing node_modules tree, and the workspace symlinks from the previous revision are not pruned by bun install, so the old scope still resolved on a developer machine that had installed before the rename. Hosted CI installs into a clean checkout and failed immediately, which is the honest signal; the local suite only agreed once those stale links were removed and the dependencies reinstalled. --- .../Fixtures/Server/persistent-simulation-host.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/OpenCode.Sdk.Tests/Fixtures/Server/persistent-simulation-host.js b/tests/OpenCode.Sdk.Tests/Fixtures/Server/persistent-simulation-host.js index b75392b6e..bd20ac778 100644 --- a/tests/OpenCode.Sdk.Tests/Fixtures/Server/persistent-simulation-host.js +++ b/tests/OpenCode.Sdk.Tests/Fixtures/Server/persistent-simulation-host.js @@ -1,9 +1,9 @@ import { NodeServices } from "@effect/platform-node"; -import { LayerNode } from "@opencode-ai/util/effect/layer-node"; -import { Global } from "@opencode-ai/util/global"; -import { Observability } from "@opencode-ai/util/observability"; -import { AppProcess } from "@opencode-ai/util/process"; -import { start } from "@opencode-ai/server/process"; +import { LayerNode } from "@opencode/util/effect/layer-node"; +import { Global } from "@opencode/util/global"; +import { Observability } from "@opencode/util/observability"; +import { AppProcess } from "@opencode/util/process"; +import { start } from "@opencode/server/process"; import { Effect } from "effect"; import { HttpServer } from "effect/unstable/http"; From 16ba6d1158387addce35a70f0ac031f3e1373935 Mon Sep 17 00:00:00 2001 From: "denizirgin@gmail.com" Date: Tue, 8 Sep 2026 21:31:41 +0300 Subject: [PATCH 4/4] test: guard the fixtures' upstream package names against the pin The scope rename that broke the simulation host reached the suite as a server that exited before readiness, and nothing before that point had a reason to object. The refresh receipt watches the upstream sources the hand-written doors read; it does not watch the module identifiers a fixture writes, so a fixture that imports upstream by name is coupled to the snapshot with no guard on the coupling. This adds that guard. Every embedded JavaScript fixture is scanned for the opencode-owned package names it writes, and each one has to be a name the accepted snapshot declares: a workspace package under the submodule, or a dependency one of those manifests names. A rename now fails as "fixture names '@opencode-ai/util'" rather than as a launcher timeout. It reads the snapshot on disk rather than an installed tree deliberately. Moving the submodule does not prune node_modules, so a machine that installed before a rename keeps the previous revision's workspace links and resolves the old names anyway; that is exactly how the local suite agreed while hosted CI did not. The check is independent of install state, so both agree. --- .../PinnedFixtureContractTests.cs | 52 ++++++++++++ tests/Shared/FixtureLoader.cs | 23 +++++ tests/Shared/PinnedPackageInventory.cs | 83 +++++++++++++++++++ tests/Shared/UpstreamPackageReferences.cs | 42 ++++++++++ 4 files changed, 200 insertions(+) create mode 100644 tests/OpenCode.Sdk.Tests/PinnedFixtureContractTests.cs create mode 100644 tests/Shared/PinnedPackageInventory.cs create mode 100644 tests/Shared/UpstreamPackageReferences.cs diff --git a/tests/OpenCode.Sdk.Tests/PinnedFixtureContractTests.cs b/tests/OpenCode.Sdk.Tests/PinnedFixtureContractTests.cs new file mode 100644 index 000000000..dca66a8ae --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/PinnedFixtureContractTests.cs @@ -0,0 +1,52 @@ +using System.Globalization; +using OpenCode.Sdk.TestSupport; +using Testably.Abstractions; + +namespace OpenCode.Sdk.Tests; + +/// +/// The contract between this repository's JavaScript fixtures and the accepted snapshot. A fixture +/// that boots the pinned server imports upstream modules by name, and upstream is free to rename a +/// package under a refresh: it renamed its whole workspace scope once already. Nothing else catches +/// that, because the refresh receipt watches the sources the hand-written doors read rather than the +/// module identifiers a fixture writes, so the mismatch would otherwise reach the suite as a server +/// that exits before readiness. +/// +/// +/// This reads the snapshot on disk rather than an installed tree on purpose. A developer machine +/// that installed before a rename keeps the previous revision's workspace links alongside the new +/// ones, so the old names still resolve there and a local run agrees while hosted CI does not. +/// +public sealed class PinnedFixtureContractTests +{ + private static readonly RealFileSystem FileSystem = new(); + + [Test] + public async Task Fixtures_Should_Name_Only_Packages_The_Pinned_Snapshot_Declares( + CancellationToken cancellationToken) + { + var loader = new FixtureLoader(); + var inventory = await PinnedPackageInventory.LoadAsync(FileSystem, cancellationToken); + var fixtures = loader.Names(".js"); + + var undeclared = new List(); + var referenced = 0; + foreach (var fixture in fixtures) + { + foreach (var package in UpstreamPackageReferences.In(loader.LoadText(fixture))) + { + referenced++; + if (!inventory.Declares(package)) + { + undeclared.Add($"{fixture} names '{package}'"); + } + } + } + + await Assert.That(undeclared).IsEmpty(); + Console.WriteLine( + "pinned-fixture-contract: " + + referenced.ToString(CultureInfo.InvariantCulture) + " upstream package reference(s) across " + + fixtures.Count.ToString(CultureInfo.InvariantCulture) + " fixture(s)"); + } +} diff --git a/tests/Shared/FixtureLoader.cs b/tests/Shared/FixtureLoader.cs index e1b9b2518..2fc12be24 100644 --- a/tests/Shared/FixtureLoader.cs +++ b/tests/Shared/FixtureLoader.cs @@ -10,6 +10,29 @@ internal sealed class FixtureLoader public string LoadJson(string name) => LoadText(name); + /// + /// Every embedded fixture whose name ends with , in the form + /// takes. + /// + public IReadOnlyList Names(string extension) + { + ArgumentException.ThrowIfNullOrWhiteSpace(extension); + + var prefix = string.Concat(_assembly.GetName().Name, ".Fixtures."); + var names = new List(); + foreach (var resource in _assembly.GetManifestResourceNames()) + { + if (resource.StartsWith(prefix, StringComparison.Ordinal) && + resource.EndsWith(extension, StringComparison.Ordinal)) + { + names.Add(resource[prefix.Length..]); + } + } + + names.Sort(StringComparer.Ordinal); + return names; + } + public string LoadText(string name) { ArgumentException.ThrowIfNullOrWhiteSpace(name); diff --git a/tests/Shared/PinnedPackageInventory.cs b/tests/Shared/PinnedPackageInventory.cs new file mode 100644 index 000000000..793764c26 --- /dev/null +++ b/tests/Shared/PinnedPackageInventory.cs @@ -0,0 +1,83 @@ +using System.IO.Abstractions; +using System.Text.Json; + +namespace OpenCode.Sdk.TestSupport; + +/// +/// Every package name the pinned snapshot declares: the workspace packages under the submodule's +/// packages/ directory, plus every dependency their manifests name. Fail-fast by design +/// (ADR-0022): a missing submodule is an instructive error, never a skip. +/// +/// +/// This answers whether a name exists at the accepted pin. It deliberately does not answer whether +/// that name resolves from a particular directory, which an install decides and a stale +/// node_modules can answer wrongly in both directions. +/// +internal sealed class PinnedPackageInventory +{ + private static readonly string[] DependencySections = + [ + "dependencies", "devDependencies", "peerDependencies", "optionalDependencies", + ]; + + private readonly HashSet _names; + + private PinnedPackageInventory(HashSet names) => _names = names; + + public static async Task LoadAsync( + IFileSystem fileSystem, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(fileSystem); + + var packages = fileSystem.Path.Combine( + new PinnedServerCommand(fileSystem).RepositoryRoot, "external", "opencode", "packages"); + if (!fileSystem.Directory.Exists(packages)) + { + throw new InvalidOperationException( + $"The pinned server source is missing at '{packages}'. Run: git submodule update --init external/opencode"); + } + + var names = new HashSet(StringComparer.Ordinal); + foreach (var directory in fileSystem.Directory.GetDirectories(packages)) + { + var manifest = fileSystem.Path.Combine(directory, "package.json"); + if (fileSystem.File.Exists(manifest)) + { + using var stream = fileSystem.File.OpenRead(manifest); + using var reader = new StreamReader(stream); + AddNames(names, await reader.ReadToEndAsync(cancellationToken)); + } + } + + return new PinnedPackageInventory(names); + } + + public bool Declares(string packageName) + { + ArgumentException.ThrowIfNullOrWhiteSpace(packageName); + + return _names.Contains(packageName); + } + + private static void AddNames(HashSet names, string manifestText) + { + using var manifest = JsonDocument.Parse(manifestText); + var root = manifest.RootElement; + if (root.TryGetProperty("name", out var name) && name.ValueKind == JsonValueKind.String) + { + names.Add(name.GetString()!); + } + + foreach (var section in DependencySections) + { + if (root.TryGetProperty(section, out var dependencies) && + dependencies.ValueKind == JsonValueKind.Object) + { + foreach (var dependency in dependencies.EnumerateObject()) + { + names.Add(dependency.Name); + } + } + } + } +} diff --git a/tests/Shared/UpstreamPackageReferences.cs b/tests/Shared/UpstreamPackageReferences.cs new file mode 100644 index 000000000..ac2d57c68 --- /dev/null +++ b/tests/Shared/UpstreamPackageReferences.cs @@ -0,0 +1,42 @@ +namespace OpenCode.Sdk.TestSupport; + +/// +/// The opencode-owned package names a JavaScript fixture names in its source. A fixture that boots +/// the pinned server imports upstream modules as bare specifiers, so the fixture is coupled to +/// names the accepted snapshot owns; this reads that coupling out of the text, before any install +/// decides whether the names happen to resolve on this machine. +/// +/// +/// Specifiers are read from double-quoted literals, which is how every fixture here writes them. +/// A subpath is reduced to the package it belongs to, so @opencode/util/effect/layer-node +/// is reported as @opencode/util. +/// +internal static class UpstreamPackageReferences +{ + private const string UpstreamScope = "@opencode"; + + public static IReadOnlyCollection In(string fixtureText) + { + ArgumentNullException.ThrowIfNull(fixtureText); + + // Splitting on the quote alternates outside/inside, so every odd segment is one literal. + var segments = fixtureText.Split('"'); + var names = new SortedSet(StringComparer.Ordinal); + for (var index = 1; index < segments.Length; index += 2) + { + if (segments[index].StartsWith(UpstreamScope, StringComparison.Ordinal)) + { + names.Add(PackageName(segments[index])); + } + } + + return names; + } + + /// Reduces a specifier to its package name: a scoped name keeps its first two segments. + private static string PackageName(string specifier) + { + var segments = specifier.Split('/'); + return segments.Length >= 2 ? segments[0] + "/" + segments[1] : specifier; + } +}