diff --git a/.changeset/bump-core-rune-fix.md b/.changeset/bump-core-rune-fix.md new file mode 100644 index 00000000..fc9c6020 --- /dev/null +++ b/.changeset/bump-core-rune-fix.md @@ -0,0 +1,13 @@ +--- +'@smooai/smooth-operator': patch +--- + +go/typescript-server: bump `smooth-operator-core` to 1.13.2 and retire the last `knownDivergences` marker. + +Core 1.13.2 (th-6fdd1c) fixes the mock LLM provider's streamed text chunker, which split on **byte** boundaries in Go and **UTF-16 code unit** boundaries in TypeScript — cutting multi-byte characters in half and turning each fragment into `U+FFFD` once serialized. + +That was the one defect still keeping the shared conformance corpus from full parity: `interaction-choices-park-resume` streams `"Pro it is — pulling that quote up."`, whose 36 bytes put a 3-way chunk boundary in the middle of the em-dash, so the Go server accumulated `"Pro it is ��� pulling that quote up."`. + +With the bump, **`spec/conformance/scenarios/` now carries no `knownDivergences` marker at all** — all 18 scenarios pass on all five servers (Rust, Go, TypeScript, Python, .NET). The marker did exactly what it was designed to do: it was re-pointed at the real remaining defect rather than deleted, and CI expired it automatically the moment that defect was fixed, failing with `remove go from knownDivergences … it now passes`. + +Go moves 1.8.9 → 1.13.2 and TypeScript's lockfile 1.8.6 → 1.13.2; Python and .NET were already unaffected by the chunker bug and are untouched. diff --git a/go/server/go.mod b/go/server/go.mod index 687cdcb4..393a9de8 100644 --- a/go/server/go.mod +++ b/go/server/go.mod @@ -3,7 +3,7 @@ module github.com/SmooAI/smooth-operator/go/server go 1.26 require ( - github.com/SmooAI/smooth-operator-core/go v1.8.9 + github.com/SmooAI/smooth-operator-core/go v1.13.2 github.com/SmooAI/smooth-operator/go v0.0.0-00010101000000-000000000000 github.com/coder/websocket v1.8.14 github.com/google/uuid v1.6.0 diff --git a/go/server/go.sum b/go/server/go.sum index 1a66fa81..208b8272 100644 --- a/go/server/go.sum +++ b/go/server/go.sum @@ -8,10 +8,8 @@ github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0 github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/SmooAI/smooth-operator-core/go v1.7.17-0.20260817051723-566d5dcb033b h1:BiH6ZwtKZMDNLv9yCIzhXNT6q1mYorpiaOYzVOcPPAU= -github.com/SmooAI/smooth-operator-core/go v1.7.17-0.20260817051723-566d5dcb033b/go.mod h1:JoJNR4TrGcCGBDBHi4n7Oy6OSz3yYicZyWuadEs7x9c= -github.com/SmooAI/smooth-operator-core/go v1.8.9 h1:9qf9mRZGSuO9ZxkEVpAQ1R7dl9T517qKAJW9n5NM1zw= -github.com/SmooAI/smooth-operator-core/go v1.8.9/go.mod h1:JoJNR4TrGcCGBDBHi4n7Oy6OSz3yYicZyWuadEs7x9c= +github.com/SmooAI/smooth-operator-core/go v1.13.2 h1:fZaeZBB807i6DVCZFQKBQervXO/sM0MlHxEi0FRUZ/Y= +github.com/SmooAI/smooth-operator-core/go v1.13.2/go.mod h1:JoJNR4TrGcCGBDBHi4n7Oy6OSz3yYicZyWuadEs7x9c= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 666518ad..2019e4cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -199,8 +199,8 @@ importers: specifier: ^2.0.0 version: 2.10.0(@opentelemetry/api@1.9.1) '@smooai/smooth-operator-core': - specifier: ^1.8.4 - version: 1.8.6(ws@8.21.0) + specifier: ^1.13.2 + version: 1.13.2(ws@8.21.0) pg: specifier: ^8.23.0 version: 8.23.0 @@ -1545,8 +1545,8 @@ packages: '@rushstack/eslint-patch@1.16.1': resolution: {integrity: sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==} - '@smooai/smooth-operator-core@1.8.6': - resolution: {integrity: sha512-0KFpf8Nwcjkvqkx/hn5e6O8TmApVpVXtQAzVzv20dYLzjetsZe//IPsrvnwhAD1Q400NhxlPdap5gumJMLveQg==} + '@smooai/smooth-operator-core@1.13.2': + resolution: {integrity: sha512-0hWfHCqY7WCjIW03LgpwL/V+Ad7Z2aZjxYGdVuRkHzzTw1vhPNv5vsW7uQlqxNf+zpBTfsoZtix3V0Qtn8sTrg==} '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -5678,7 +5678,7 @@ snapshots: '@rushstack/eslint-patch@1.16.1': {} - '@smooai/smooth-operator-core@1.8.6(ws@8.21.0)': + '@smooai/smooth-operator-core@1.13.2(ws@8.21.0)': dependencies: openai: 4.104.0(ws@8.21.0) smol-toml: 1.7.0 diff --git a/spec/conformance/scenarios/README.md b/spec/conformance/scenarios/README.md index d6a3bf46..aecb8bd6 100644 --- a/spec/conformance/scenarios/README.md +++ b/spec/conformance/scenarios/README.md @@ -120,7 +120,7 @@ Implementation note per language, since `*testing.T` and panics do not catch ali Recorded here as facts, not as license to weaken the scenarios. **Do not "fix" a scenario to make a port pass.** - **~~Park event vs. the raise tool's `stream_chunk` — Rust is 1 of 5~~ — FIXED (th-ef78d0).** For a Rich Interaction, Rust emitted `interaction_required` *before* the raise tool's `toolCall` chunk while Go, TypeScript, Python and .NET emitted the chunk first. **Ruled a port bug, not a protocol variant**, on three grounds: all five already defer the gated tool's chunk until after the prompt for the *other* park type — `hitl-write-confirmation`, a scenario all five passed throughout — so the four were internally inconsistent between their own two park paths while Rust was consistent; Rust is the designated reference; and semantically a client that renders tool calls would otherwise show "calling `request_identity_intake`…" before the card appears, leaking framework internals ahead of the semantic event. **The four ports changed, and the scenarios did not.** Each port already owned the mechanism — it is the same one its write-confirmation gate uses: suppress the chunk in the engine stream loop on a tool-name predicate, then re-emit it from the park path *after* the park event. Go needed only a move (its deferral existed but re-emitted at the top of the raise tool, ahead of the park); TypeScript, Python and .NET needed the predicate added (`isInteractionRaise` / `_is_interaction_raise` / `IsInteractionRaise`, matched against the hosted kinds' `request_` names — **not** the generic `submit_interaction` tool, whose chunk would then be dropped) plus the re-emit. Every non-park exit of the raise tool (parse error, conversational fallback) re-emits the chunk immediately, so `interaction-conversational-fallback` is unchanged. -- **A second bug hid behind the first, in Go only.** Fixing the order let `interaction-choices-park-resume` run past step 2 for the first time — and it still fails on Go, now for an unrelated reason: `splitIntoChunks` in `smooth-operator-core` Go (`go/core/llm_provider.go`) slices the mock reply by **bytes**, so the em-dash in "Pro it is — pulling that quote up." (36 bytes / 3 parts puts a boundary mid-rune) is split into invalid UTF-8 and each byte arrives as U+FFFD. `interaction-park-resume`'s reply is 33 bytes and its boundaries miss the rune, which is why only this one scenario trips. That scenario therefore keeps `"knownDivergences": ["go"]` with the reason rewritten to name the real bug; the fix is one line upstream (slice `[]rune`) plus a core release. This is the marker contract working as designed — a tracked bug with an expiry, re-pointed at the defect that is actually still there. +- **~~A second bug hid behind the first, in Go only~~ — FIXED (th-6fdd1c, core v1.13.2).** Fixing the order let `interaction-choices-park-resume` run past step 2 for the first time, and it then failed on Go for an unrelated reason: `splitIntoChunks` in `smooth-operator-core` Go sliced the mock reply by **bytes**, so the em-dash in "Pro it is — pulling that quote up." (36 bytes / 3 parts puts a boundary mid-rune) was split into invalid UTF-8 and each byte arrived as U+FFFD. `interaction-park-resume`'s reply is 33 bytes and its boundaries miss the rune, which is why only this one scenario tripped — with ASCII-only fixtures the bug is invisible. Both the Go chunker and the TypeScript one (UTF-16 code units: safe for the BMP, **not** for astral characters like emoji) now split on character boundaries; Python, Rust and .NET were checked and never had it. **The corpus now carries no `knownDivergences` marker at all** — every scenario passes on all five servers. That is the marker contract closing its own loop: it was re-pointed at the real remaining defect, and expired the moment that defect was fixed. - **~~A cancelled turn keeps running in Go and .NET~~ — FIXED (th-f2ac48, PR #514).** Recorded because it is what this scenario was built to catch, and because the fix is the corpus's first end-to-end proof of itself. Both ports used to leave the turn running after a `cancel`: the write-confirmation gate returned a deny instead of unwinding, the agent loop made one more model call, and the output was merely gagged (Go: `if turnCtx.Err() != nil { return }`). Cancellation was a mute button, not a stop button — real spend and real side-effect risk after a visitor hits Stop. Two independent proofs: re-running with one extra `mockLlmScript` entry made both pass (the entry was eaten by the cancelled turn), and `go test -race` reported a `DATA RACE` in core's `MockLlmProvider.ChatStream` where the cancelled turn's goroutine and the *next* turn's goroutine popped the same unguarded FIFO concurrently. That race was the one failure `knownDivergences` deliberately did **not** tolerate — it fires outside the runner's assertion path, and suppressing a data race is the opposite of what this corpus is for. The lesson if it recurs: do not "fix" it by guarding the mock's FIFO, which silences the evidence and leaves the bug. - **Ack payloads differ, so only `status` is asserted on a `submit_interaction` ack.** The five servers put different fields in `data` (Go omits `kind`/`values`; Python omits `kind`, and its decline ack omits `interactionId`/`declined`; .NET's decline ack omits `declined`). Asserting more would pin one language's shape rather than the protocol's. diff --git a/spec/conformance/scenarios/interaction-choices-park-resume.json b/spec/conformance/scenarios/interaction-choices-park-resume.json index 1982edb8..97d85614 100644 --- a/spec/conformance/scenarios/interaction-choices-park-resume.json +++ b/spec/conformance/scenarios/interaction-choices-park-resume.json @@ -1,8 +1,6 @@ { "name": "interaction-choices-park-resume", "description": "The second Rich Interaction kind, `choices` (the AskUserQuestion-shaped multiple-choice ask), through the SAME generic envelope: `request_choices` parks behind the `choice_chips` capability, the server emits `interaction_required` with the normalized questions/options spec, and one `submit_interaction` resumes it. Adding a kind must require no new protocol action and no client release — this scenario is what proves that claim holds identically in every server, and that the kind catalog is not identity_intake-shaped by accident.", - "knownDivergences": ["go"], - "knownDivergencesReason": "th-ef78d0 — the park ORDERING is fixed in all four ports; Go alone still fails this scenario, and on a DIFFERENT bug that only became reachable once the ordering was corrected: `splitIntoChunks` in smooth-operator-core Go (`go/core/llm_provider.go`) slices the mock reply by BYTES, so the em-dash in \"Pro it is — pulling that quote up.\" (36 bytes / 3 parts puts a boundary mid-rune) is split into invalid UTF-8 and each byte surfaces as U+FFFD. `interaction-park-resume`'s reply is 33 bytes and its boundaries miss the rune, which is why only this scenario trips. Fix is one line upstream (slice `[]rune`) plus a core release — not a protocol divergence, and NOT a reason to weaken this scenario.", "mockLlmScript": [ { "kind": "toolCall", "name": "request_choices", "arguments": "{\"questions\": [{\"question\": \"Which plan fits best?\", \"header\": \"Plan\", \"options\": [{\"label\": \"Starter\"}, {\"label\": \"Pro\"}]}], \"reason\": \"to route you to the right quote\"}" }, { "kind": "text", "text": "Pro it is — pulling that quote up." } diff --git a/typescript/server/package.json b/typescript/server/package.json index 34647e21..0775f19f 100644 --- a/typescript/server/package.json +++ b/typescript/server/package.json @@ -32,7 +32,7 @@ "@opentelemetry/exporter-trace-otlp-proto": "^0.205.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", - "@smooai/smooth-operator-core": "^1.8.4", + "@smooai/smooth-operator-core": "^1.13.2", "pg": "^8.23.0", "ws": "^8.18.0" },