Repository navigation
fix(pair-mcp, story-mcp, pair): JSON pred coercion, refused-event outcome, abandoned-run read-failures, bounded unknown-tool echo - #11827
Merged
Conversation
… the unknown-tool echo (rf2-lgzco.35)
A run abandoned at its lifecycle deadline has its frame torn down, so
read-failures read the empty frame and answered {:status :pass :total 0},
the never-ran green. The deadline outcome's records are now kept per
variant until its next run, and read-failures reads them in place of the
frame: {:status :error :total 1} with the :rf.error/run-failed record,
agreeing with the run-variant reply.
The unknown-tool result echoed the caller's name whole, twice, outside the
response cap, after a near-name search linear in its length. A name over
128 characters is now echoed cut, and one more than twice the longest
listed name skips the search, which could not have matched.
… watch-epochs does (rf2-lgzco.33)
JSON carries no keywords, and js->clj :keywordize-keys keywordises keys
only, so a JSON pred {"signal":0,"equals":":done"} compiled to
(= (get sample 0) (quote ":done")) and never held, and a JSON signal
{"app-db":[":upload",":status"]} sampled nil. The JSON arms now coerce
the way watch-epochs' pred does: an :app-db signal or a pred :path parses
as get-path's path, and a colon-led :equals or :sub element mints a
keyword under the id grammar. The EDN-string arms are unchanged.
record_test's held-predicate test now reads the poll form for the
keyword path and comparand, rather than letting the stub decide :held?.
…ch-epochs :timing-ms (rf2-lgzco.35, rf2-lgzco.47)
The unknown-tool refusal echoed the caller's name whole in :tool and
:hint, after a near-name search linear in its length. A name over 128
characters is now echoed cut, and one more than twice the longest
registered name skips the search, which could not have matched.
watch-epochs passed a pred :timing-ms through unchecked, and the
preload's matcher read an unparseable one (">100ms") as no constraint.
The tool now refuses it up front with :rf.mcp/invalid-arg naming :pred.
…lay 1 (rf2-i79mi) The namespace docstring said relay 1 is reached only by throws before the nREPL round-trip; a failed or interrupted round-trip reaches it too, as invoke-and-guard's docstring says.
…ing-ms matches nothing; a regex in handler meta reads back (rf2-lgzco.47, rf2-lgzco.33)
A :boundary? true schema rejection, an app-db schema rollback and a
refused effect map each settle the epoch :outcome :ok having committed
nothing, so dispatch reported them as {:outcome :ok :no-op? true} and
dispatch-dry-run as :outcome :ok. pure.cljc gains a refusal set beside
the throw set (which still mirrors Xray's): an :rf.error/effect-map-shape,
or a schema failure at :where :event or carrying :rollback? true, folds
into :errors, forcing :outcome :error and clearing :no-op?.
epoch-matches? read a present but unparseable :timing-ms (">100ms") as
no constraint, so it matched every epoch; it now matches none.
strip-fns passed a RegExp through, and EDN has no regex literal, so a
registration whose :schema held [:re #"..."] made handler-meta's whole
reply unserializable. A RegExp now projects to its source string.
…pped walk (rf2-lgzco.33) machine_describe_test evaluates the preload's strip-fns under Babashka, which has no cljs.core regexp?; the JVM predicate stands in for it.
mike-thompson-day8
marked this pull request as ready for review
October 30, 2026 01:29
mike-thompson-day8
added a commit
that referenced
this pull request
Oct 30, 2026
Merge: b0bdabd at 2026-10-30T01:33:21Z. Completeness: The JSON signal/predicate fixes, refused-event outcomes, timing rejection, deadline reread, bounded unknown-tool echoes, regex metadata readability and docstring correction are present. Full patch and all four complete source beads read. Correctness: Both run and preview routes clear deadline records on the next attempt; read-failures retains the timeout error after teardown. Parser coercion preserves EDN and literal string controls; timing grammar agrees at both ends. Reported fault plants and CI accepted. No material new finding. Implementation quality: Shared parsing avoids another divergent path, refusal classification preserves the throw-set contract, and local echo bounds keep recovery hints. Focused changes use existing projection and lifecycle seams. Findings: none. Source forward pointers: none.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the correctness findings on the Pair MCP server, the Story MCP server and the Pair preload's dispatch projection (rf2-lgzco.33, rf2-lgzco.35, rf2-lgzco.47), plus one test-docstring sentence (rf2-i79mi).
What changed
rf2-lgzco.33 (pair-mcp): JSON
pred/signalscoercion (P2)recordandwatch-untilnow coerce a JSON-objectpredor JSONsignalsthe waywatch-epochscoerces itspred. An:app-dbsignal or a pred:pathparses the wayget-pathparses a path (args/parse-path-arg). A colon-led:equalsor:subelement mints a keyword under the id grammar (args/->id-keyword). Any other string stays a string, which is option (a) in the finding. The EDN-string arms are unchanged.watch-untilnow uses the sharedrecord/parse-pred-arg.record'sstopobject coerces its nested:predthe same way.record_test's held-predicate test used to let the stub decide:held?. It now reads the poll form and checks that the keyword path and comparand reach it.registrar-describethrew on a[:re #"…"]schema.strip-fnsnow turns a RegExp into its source string, and[:re "…"]is the same Malli schema.machine_describe_test.cljevaluates the shipped walk under Babashka, which has noregexp?, so that test now defines one from the JVM predicate.rf2-lgzco.35 (story-mcp)
read-failuresused to read the torn-down frame and answer{:status :pass :total 0}. Each variant now keeps its deadline outcome's records until its next run, andread-failuresreads them instead:{:status :error :total 1}with the:rf.error/run-failedrecord. This is option (a), and it matches therun-variantreply.tools_test.clj's fence test used to pin the:pass; it now expects that error..... A name more than twice the longest listed name skips the near-name search, since no listed name could fall within its threshold. Each server has its own fix, andtools/mcp-baseis untouched.rf2-lgzco.47 (Pair preload)
pure.cljcgainscascade-refusal-opsbeside the throw set, and the throw set still mirrors Xray's. Three refusals now count::rf.error/effect-map-shape, a schema failure at:where :event, and a schema failure carrying:rollback? true. Each folds into:errors, which forces:outcome :errorand keeps the event out of:no-op?. Sodispatchanddispatch-dry-runnow return whatrecipes.md:273already promises. A schema failure at:sub-return/:fx-argsleaves the event committed and is not counted.epoch-matches?now matches nothing on a present but unparseable:timing-ms. Thewatch-epochstool also refuses such a value up front with:rf.mcp/invalid-argnaming:pred.rf2-i79mi: the
error_boundary_testnamespace docstring now says a failed or interrupted nREPL round-trip also reaches relay 1, asinvoke-and-guard's docstring does. This is prose only.Spec, one sentence per change:
003-Tool-Catalogue.md: the unknown-tool echo cut, and the:timing-msrefusal.001-Wire-Protocol.md: the unknown-tool echo cut.002-Tool-Registry.md§read-failures: what it reads after a deadline.No new tool args, no MCP-base surface, and no descriptor text changed, so
tool-descriptors.ednis untouched.Quality gates
Each exit code below was captured on the same command line as its redirect.
npm --prefix tools/re-frame2-pair-mcp testclojure -M:testintools/story-mcppure-test(shadow-cljs compile pure-test && node target/pure-test.js, run through the lane helper)tests/prompts+tests/runtime/*)scripts/check_doc_slugs.pypure.cljc/runtime.cljsedits in place but the trunk test files, so the counts are the trunk's.strip-fnschange caughtmachine_describe_test.cljat exit 1, which is how theregexp?gap above was found.Each delta matched the prediction from the diff:
Final-base gate: this PR's CI. The local runs were taken before the rebase onto
8fca3b354d, and none of the upstream commits since touch these paths. The live and hermetic MCP job (mcp_live) is left to CI.Controls (each planted, watched fail, restored)
Each restore was verified by
git hash-objectagainst the committed blob, andgit diff --quietexited 0.watch-epochsJSON-pred test;unknown-tool-refused-before-connectionunknown-tooltest; the fence assertions:sub-return/:fx-argsnon-refusals; the parseable">=0"Detail on the reds:
read-failuresread[:pass 0 []], and the refusal was 64,488 characters.