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..1f52d760f 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,9 +14,14 @@ 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) | +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". 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/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"; 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/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 { 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; + } +}