Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/bump-core-rune-fix.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion go/server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions go/server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion spec/conformance/scenarios/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<kind>` 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.

Expand Down
Original file line number Diff line number Diff line change
@@ -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." }
Expand Down
2 changes: 1 addition & 1 deletion typescript/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
Loading