Skip to content

fix(moonshot): bound normalized tool-schema expansion - #5547

Closed
luvs01 wants to merge 6 commits into
devfrom
stack/moonshot-schema-boundaries
Closed

luvs01 wants to merge 6 commits into
devfrom
stack/moonshot-schema-boundaries

Conversation

@luvs01

@luvs01 luvs01 commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Combine #5464's Moonshot schema corrections with the request-wide expansion budget carried by 7c05f085d81eb9ae85fb7dedd9b1f6add72b575f and 3f161b8c3b21b3ee671d79168e2df90598314757.

  • Re-normalize composed properties and infer appropriate scalar/object types without turning non-object allOf schemas into objects.
  • Share the inline-byte allowance across the entire request's tool catalog, preserving the existing depth, node and reference-expansion bounds.
  • Account for growth caused by normalization before retaining an expanded reference. If type inference pushes an otherwise admissible raw target over the remaining allowance, preserve the bare reference instead of retaining an oversized copy.
  • Preserve all original contributions and follow-ups, document the contract in chat compatibility and ADR-0355, and add regressions covering the combined normalization/budget boundary.

This is an integration proposal. The original contributor PR #5464 remains open; any closure or change to its review state requires its author or maintainer to authorize it. Contributor attribution is preserved.

Verification

  • Base: dev 7bd9985963fd344a2c512fa5cf3c0f57f6ba83bd. Head: 93831de7520df0ceaff43b9dd6bb4d251683bd90.
  • On Bun 1.4.2, focused Moonshot, endpoint, xAI schema, Azure router schema and file-size checks: 50 passed / 190 assertions / 0 failures.
  • The normalization-growth regression failed before the integration follow-up; both combined cases passed after it (8 assertions).
  • bun run typecheck, bun run structure:check, bun run privacy:scan and base-to-head git diff --check passed.
  • The source budget implementation and tests match their carried state before layering the complete upstream corrections. Original author and cherry-pick provenance are preserved, including the module-split migration.
  • Existing bounds remain depth 64, 4,096 nodes, 512 reference expansions and 1 MiB shared inline bytes. No limit or source test timeout was raised.
  • Full suite, exact-head cross-platform CI, live-provider acceptance and independent review remain pending. This PR stays draft; tests build synthetic requests and make no provider call.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

devin-ai-integration Bot and others added 6 commits September 22, 2026 21:21
…ication

Resolve conflicts from the openai-chat module split: port the serialized-byte
inline budget into src/adapters/openai-chat/tool-schema.ts, move the
adapter-registry note to structure/decisions/ADR-0093, and keep the moved
Moonshot regression tests at tests/providers/moonshot-tool-schema.test.ts.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
(cherry picked from commit 9e2ca52)
Decision records are historical; the byte-budget reasoning moves out of
ADR-0093 into ADR-0355 and the chat-compat contract now states the
inline-byte bound alongside depth, node, and expansion bounds.

Co-Authored-By: Epinephrine <luvs01@hanmail.net>
(cherry picked from commit 3b8cb5e)
…t tools

A per-tool 1 MiB allowance let a large catalog multiply the cap by its
tool count, reopening the request amplification the budget exists to
bound. Hoist the allowance to one MoonshotInlineByteBudget per
toolsToChatFormat call so every tool spends from the same pool.

Adds a regression test: two tools carrying ~0.6 MB inline targets each
now share one allowance, so only the first of four sibling refs inlines
and the rest keep the bare-$ref fallback.

Co-Authored-By: Epinephrine <luvs01@hanmail.net>
(cherry picked from commit 292e7e1)
…/scalar types

When composeProperties merges a referenced target and a sibling that narrows
one of its properties (for example, supplying a const beside a $ref), the
composed property retained the $ref alongside the sibling keyword without
being re-normalized. This produced an un-normalized sibling-$ref node that
violated Moonshot's strict Draft-07 schema validator.

Additionally, Moonshot's schema validator (walle) requires object definitions
(especially those composed with allOf or properties and used as base cases in
recursive anyOf unions) to explicitly declare type: "object". Without an
explicit type, walle fails to recognize the schema as a valid termination
condition, rejecting valid recursive tools with:
`detected infinite recursion without termination condition`.

1. Recursively re-normalize the merged node in normalizeMoonshotSchemaNode so
   composed sibling refs are resolved.
2. Infer type: "object" for schemas containing properties or allOf when type is
   omitted.
3. Infer scalar types for un-typed const and enum keywords.
4. Add regression coverage in tests/providers/moonshot-tool-schema.test.ts.

(cherry picked from commit a986a88)
… enums, and composed property re-normalization

(cherry picked from commit 0fad764)
…udget

Reserve raw target bytes before normalization and charge inferred-type or nested-normalization growth before retaining the expanded target. Over-budget work keeps the existing bare-ref fallback; depth, node, expansion and shared 1 MiB limits stay unchanged.

Add combined regressions for type-inference growth and composed-property re-normalization across multiple tools. The growth case fails before this follow-up and passes after it. The original byte-budget implementation ab7f684 is carried through module-split resolution 9e2ca52 (second-parent delta), preserving the later request-shared allowance.

Co-authored-by: yeongjunyoo <47925973+yeongjunyoo@users.noreply.github.com>
Co-authored-by: Epinephrine <luvs01@hanmail.net>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@lidge-jun

Copy link
Copy Markdown
Owner

리뷰 · 우선순위 61 / 80

이 PR은 Moonshot/Kimi용 도구 스키마를 고치는 통합안입니다. 원래 #5464에서 하던 “형제 키워드가 붙은 $ref를 다시 정규화하고, properties/allOf/const/enum에 빠진 type을 채워 주기”와, 요청 하나에서 $ref를 펼칠 때 커지는 크기를 막는 1 MiB 바이트 예산을 한데 모았습니다. 예산은 도구마다 따로가 아니라 toolsToChatFormat 한 번당 하나를 나눠 씁니다. 펼치기 전에 원본 크기만큼 먼저 깎고, 타입 추론이나 안쪽 정규화로 커진 분량도 남은 예산에 맞으면 쓰고, 넘치면 예전처럼 맨 $ref만 남깁니다. 깊이 64·노드 4096·참조 펼침 512 한도는 그대로입니다. ADR-0355와 chat-compat에도 같은 계약을 적어 두었고, 회귀 테스트가 여러 개 붙었습니다. base는 선호하는 dev이고, draft입니다. 원작 #5464는 아직 열려 있고, 본문도 닫는 일은 원 작성자·메인테이너 허락이 필요하다고 적었습니다.

라인 - src/adapters/openai-chat/tool-schema.ts normalizeMoonshotSchemaNode — 원본 바이트를 예산에서 뺀 뒤 정규화하고, “커진 분량”이 남은 예산을 넘기면 { $ref }만 돌려줍니다. 이때 이미 깎인 원본 바이트·펼침 횟수·안쪽에서 쓴 예산을 되돌리지 않습니다. 결과는 맨 $ref인데 예산만 줄어, 같은 요청의 뒤 도구·형제 $ref가 더 일찍 막힐 수 있습니다. 실패 시 환불하거나, 커진 분량까지 맞춰 본 뒤에만 예산을 확정하는 편이 맞습니다.
라인 - serializedJsonBytesUpTo — 실제 JSON.stringify 전체가 아니라 괄호·쉼표·키를 대략 센 값입니다. 가드로 쓰기엔 충분해 보이지만, 캐시 한도를 MOONSHOT_MAX_INLINED_SCHEMA_BYTES로 자르면 아주 큰 대상의 “진짜 크기”를 작게 잡을 여지는 있습니다. 지금 테스트가 큰 맵을 막는지 한 번만 눈여겨보면 됩니다.
라인 - #5464 — 이 PR이 그 정규화·타입 추론을 이미 가져왔습니다. 둘 다 draft로 열려 있으면 나중에 같은 수정이 두 번 들어갈 수 있습니다.
라인 - draft / 검증 — 본문 기준 집중 테스트·typecheck·structure는 통과했다고 적혀 있고, 전체 스위트·호스티드 CI·보안 체크리스트는 아직입니다. 머지 전에 초록을 봐야 합니다.

메인테이너의 판단이 필요한 지점

성장 한도 초과 때 예산을 환불할지, 아니면 “맨 $ref로 떨어지는 순간에도 비용은 쓴 것으로 본다”를 의도된 정책으로 둘지 정해 주세요. 또 #5464를 이 PR이 대체한다고 보고 닫을지(원 작성자 확인 포함), draft를 언제 ready로 올릴지도 정하면 됩니다.

너의 추천

방향은 맞습니다. 요청 단위 공유 예산·성장분 과금·구성 property 재정규화·문서·회귀가 한줄로 이어집니다. 머지 전에 성장 실패 시 환불(또는 확정 전 측정)만 고치거나, 의도라면 주석·테스트로 “환불 없음”을 박아 두세요. CI가 초록이면 #5464는 중복으로 닫는 쪽을 추천합니다. types.ts/config.ts 쪼개기·preview deploy는 이번 범위와 무관합니다.

이 댓글은 grok-bot이 작성했습니다

@lidge-jun

Copy link
Copy Markdown
Owner

Carried into #5619 at 93831de with your authorship kept, plus a follow-up repair: a rejected reference expansion now returns the inline budget it consumed, and nested copies are charged only once. The provider-driven object typing from #5464 is kept and documented. Closing in favor of #5619. Thank you!

@lidge-jun lidge-jun closed this Sep 23, 2026
lidge-jun added a commit that referenced this pull request Sep 23, 2026
… fixes (#5619)

* fix(cursor): bound capability reads and buffered tool budgets (#5533)

Carries #5533 (and the closed #5233 it consolidates) onto current dev.

Co-authored-by: Epinephrine <luvs01@hanmail.net>

* fix(moonshot): bound normalized tool-schema expansion (#5547)

Carries #5547, which consolidates #5464 and the request-wide inline budget, onto current dev.

Co-authored-by: yeongjunyoo <47925973+yeongjunyoo@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Epinephrine <luvs01@hanmail.net>

* fix(moonshot): restore rejected inline budgets and charge nested growth once

A rejected sibling-reference expansion now restores the byte, node and expansion allowances it consumed, and outer growth no longer re-charges nested copies, so later independent expansions in the same request keep their allowance. Documents the provider-driven object type inference as a deliberate tradeoff and rewrites ADR-0355 in English.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>

* feat(reasoning): consolidate replay, opt-in tag parsing, and summary policy (#5566)

Carries #5566, which consolidates #5449, #5205 and #5491, onto current dev. The provider guide keeps the current bridge replay paragraph and adds the inline-tag and summary paragraphs.

Co-authored-by: Joonsuh Park <trckstr4422@gmail.com>
Co-authored-by: Daniel Sjöstrand <16033062+Danielsjostrand1979@users.noreply.github.com>
Co-authored-by: alexph-dev <alexph-dev@users.noreply.github.com>
Co-authored-by: Yum-wu <1172989563@qq.com>

* fix: bound Fernet slot runs, Kiro error-body read, and skill-path line slice (#5310)

Carries #5310 onto current dev. The follow-up commit makes the Fernet run cap fail closed and moves the Kiro regression out of the capped stream suite.

* docs(reasoning): reconcile inline-tag whitespace contract

Interleaved inline-tag parsing preserves answer whitespace; only Kiro single-block mode drops the whitespace after its leading block.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>

* fix(responses): fail closed on Fernet run overflow and keep the Kiro suite under its cap

A slot with more than 64 structurally valid Fernet runs is now treated as unreadable or omitted as a whole, so no unexamined tail reaches the provider as text. The bounded Kiro fallback error-body regression moves byte for byte into a registered sibling file, and the Kiro, Responses and inbound contracts document the new bounds.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>

* fix(reasoning): scan inline think tags with a moving cursor

The parser copied, rescanned and reserved the whole remaining response after every block, so one upstream chunk carrying many short blocks cost quadratic work. It now scans each chunk from an offset and charges the translator budget only for retained carry: undecided leading input or a trailing tag fragment.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>

* fix(reasoning): keep undecided leading whitespace incremental

Before the format was decided, every content delta rebuilt, trimmed and re-reserved the whole leading prefix, so a stream of one-character whitespace deltas cost quadratic work. Leading whitespace is now kept in segments whose bytes are reserved once and joined only when the format is decided or the stream flushes.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>

* fix(meta-muse): consolidate login admission and bounded response handling (#5591)

Carries #5591, which consolidates the closed #5234 and #5432, onto current dev. The provider contract keeps the inline-tag paragraph and adds the Meta Muse admission paragraph.

Co-authored-by: Epinephrine <luvs01@hanmail.net>

* fix(claude-desktop): keep applied state consistent across profile edits (#5590)

Carries #5590, which consolidates the closed #5337, onto current dev.

Co-authored-by: Epinephrine <luvs01@hanmail.net>
Co-authored-by: luvs01 <luvs01@users.noreply.github.com>

* fix(claude-desktop): commit applied markers only over the observed baseline

Both Desktop writers, provider-change auto-apply and client sync, now capture the desired profile and its applied marker before the Desktop write and commit the new marker only if profile presence, content, fingerprint and timestamp are unchanged. A concurrent edit, deletion or newer marker keeps its state and the write reports a skipped marker. The provider-change path no longer saves a whole stale config snapshot.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>

* fix(claude-desktop): commit profile edits against the persisted marker

The Desktop profile PUT built its response from an earlier snapshot and saved that whole snapshot, so a marker committed by another writer during the awaited state build could be replaced by an older one. The edit now commits in one persisted-config mutation that keeps the latest marker for unchanged content and answers 409 when the profile itself changed meanwhile. The Meta Muse overflow test now asserts that the bounded-body limit, not a generic failure, produced the error.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>

* fix(claude-desktop): report an unreadable config separately from an edit conflict

A missing or invalid config now answers 500 with its reason; only a concurrent profile change or exhausted rebase answers 409.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>

* feat(desktop): consolidate consent-based runtime takeover and ownership contracts (#5564)

Carries #5564, which consolidates #5459 and #5457, onto current dev. The review screenshot stays in the pull request description rather than the tree.

Co-authored-by: jun <bitkyc08@gmail.com>
Co-authored-by: sanggyulee <andy53295774@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(desktop): bind takeover stop to the approved runtime and fail closed

Desktop takeover re-resolves ownership immediately before stopping and passes the approved PID, endpoint, config home, CLI version and compatibility token to an opt-in guarded stop. The guard is checked under the ownership mutation lease before any manager or signal stop; the approved PID and endpoint must settle and the service manager must then be proven inactive, otherwise the stop answers approval-changed or manager-still-active and the desktop neither waits for silence nor claims. Unreadable or unparseable stop output is terminal as well. A second unreadable service-state read now blocks takeover, Windows managing-CLI discovery follows PATHEXT with file-only candidates and refuses command-interpreter metacharacters, the claim refusal test uses real sandbox state, and the runtime and desktop contracts record that the claim token is a consistency check rather than consent proof. Plain ocx stop is unchanged.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>

* fix(desktop): keep plain stop entry points and format the takeover changes

Desktop exit keeps its plain runtime_stop::run entry while takeover uses run_approved, AttachPlan::Ask no longer carries an unread field, the Rust changes follow rustfmt, the plain CLI stop path keeps its literal outcome return, the stop source oracles follow the reader and outcome union that now include the two guarded refusals, and the runtime contract fits its 600-line budget.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>

* fix(desktop): run takeover seam tests without tokio macros and harden manager and shim checks

The two async takeover seam tests now run on the shell runtime already used by the crate instead of tokio test macros, which this crate does not enable. Windows command-shim probes refuse command-interpreter metacharacters in every recorded argument as well as the executable, and the guarded stop re-inspects the service manager identity immediately before the manager command, answering approval-changed without stopping if it moved.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>

* fix(responses): keep effort-based reasoning visible after routing

Final-route normalization recomputed hideThinkingSummary without the validated active-effort condition, so routed Chat and Kiro requests with an active effort and an omitted summary still hid raw reasoning. It now uses the same predicate as the parser; explicit "none" and requests without an active effort stay hidden.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>

* fix(service): match the running CLI case-insensitively only on Windows

On case-sensitive filesystems a PATH executable that differs only in case is a different file, so it must get its own version probe instead of reporting the running CLI version.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>

* fix(meta-muse): require the dashboard session for manual login codes

The manual-code continuation now applies the same dashboard-session admission as the login start, so a management token cannot advance a pending Meta Muse login.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>

* fix(reasoning): reserve the joined leading-whitespace copy

Joining retained leading whitespace allocated a second copy outside the translator budget; the join is now reserved first and released once the segments are cleared.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>

* fix(service): skip CLI probes for an absent runtime and treat failed systemd units as stopped

Resolve no longer spawns managing-CLI version probes when no runtime is live, since takeover is only offered for a live runtime. A systemd unit reported failed with no main PID is stopped, so a guarded stop that leaves it failed succeeds and a leftover failed unit does not block takeover.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>

* fix(service): keep failed systemd units fail-closed and assess takeover only for a live runtime in tests

systemd can report failed before an automatic restart, so failed with no main PID is again treated as unknown rather than stopped. The resolve contract tests that assert ownership and takeover fields now use a live runtime, matching the skip of managing-CLI probes when no runtime is live.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>

---------

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
Co-authored-by: Epinephrine <luvs01@hanmail.net>
Co-authored-by: yeongjunyoo <47925973+yeongjunyoo@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Joonsuh Park <trckstr4422@gmail.com>
Co-authored-by: Daniel Sjöstrand <16033062+Danielsjostrand1979@users.noreply.github.com>
Co-authored-by: alexph-dev <alexph-dev@users.noreply.github.com>
Co-authored-by: Yum-wu <1172989563@qq.com>
Co-authored-by: luvs01 <luvs01@users.noreply.github.com>
Co-authored-by: sanggyulee <andy53295774@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants