Skip to content

fix: coerce mis-wrapped compress content args and teach format on rejection - #616

Open
kido5217 wants to merge 5 commits into
Opencode-DCP:masterfrom
kido5217:format_retry_fix
Open

fix: coerce mis-wrapped compress content args and teach format on rejection#616
kido5217 wants to merge 5 commits into
Opencode-DCP:masterfrom
kido5217:format_retry_fix

Conversation

@kido5217

Copy link
Copy Markdown

Models sometimes pass content to the compress tool as a plain summary string instead of an array of entries. The bare validation error ("content is required and must be a non-empty array") gave no format hint, so the model paid a full extra turn to retry (10 such failures observed in opencode sessions, all this shape).

  • Coerce unambiguous mis-wraps before validation (a single entry object, or a JSON string encoding an entry / entry array)
  • Plain strings now get a guiding error showing the exact expected shape and where to find boundary IDs (mNNNN/bN message-ID tags in context)
  • JSON-encoded empty array ("[]") gets the non-empty-array error instead of a misdiagnosis
  • Applies to both the range and message compress tools

Follow-up commits address code-review findings: dedupe the arg guards/normalization into lib/compress/args.ts, hoist the shared error message to a constant, drop a redundant test.

Tests: 114/114; typecheck, prettier, and build all green.

…ection

Some models emit compress 'content' as a plain summary string instead of the
required array of entry objects (no array wrapper, no startId/endId).
validateArgs then failed with the bare 'content is required and must be a
non-empty array' error, so the model had to spend a full extra turn
reformatting the call (10 such failures observed in recent opencode
sessions, every one this shape).

Root cause: model-side arg mis-wrap, not a validation bug. The compressed
range is genuinely unknowable from a plain string, so it cannot be
defaulted. The fix:
- coerce unambiguous mis-wraps at runtime (a single entry object, or a JSON
  string encoding an entry / entry array) before validation
- for the rest (plain strings), return a guiding error that shows the exact
  expected shape and where to find boundary IDs (mNNNN / bN, from
  <dcp-message-id> tags in context)

Applies to both the range and message compress tools. Regression tests
replay the captured failing payload plus the coercion cases.
coerceContentArray treated a "[]" string like an unparseable or plain
string and threw "content must be a JSON array, not a plain string" —
but the input IS a JSON array, just empty. A model reading that
diagnosis gets no signal about the real problem (nothing to compress).

An empty parsed array now throws the same "content is required and must
be a non-empty array" error a raw [] gets, in both the range and
message compress tools.
The range and message tools carried two copies of the same logic: the
whole-args shape guard with a re-send example (normalizeRangeArgs /
normalizeMessageArgs) and the per-field entry guards (isRangeEntry /
isMessageEntry). Extract the shared shape into args.ts, which already
owns the content coercion:

- isStringFields: plain-object guard with named string fields, used by
  both entry guards (each now states its field list once)
- normalizeCompressArgs: the whole-args guard plus content coercion,
  parameterized by entry guard, content noun, shape example, and the
  per-mode re-send guidance

Behavior is unchanged; the per-mode guidance strings are preserved
verbatim. Also document in args.ts that coerceContentArray leaves
element-shape checks to each tool's validateArgs.
The literal 'content is required and must be a non-empty array' lived in
four places: both throw sites in coerceContentArray (args.ts) and both
validateArgs copies (range-utils.ts, message-utils.ts). It is a single
model-facing message, so it now has one source of truth
(NON_EMPTY_ARRAY_ERROR_MESSAGE in args.ts); wording changes are a
one-place edit. Behaviour is byte-identical - the wording-pinning tests
assert the literal and still pass.
The KISS-lens review of the branch judged the message-mode execute
replay test unnecessary: execute() calls normalizeMessageArgs on its
first line, so the test verifies call wiring, not behavior, and the
plain-string rejection it replays is already pinned by the unit test
directly above. The range-mode replay is kept because it embeds the
real captured payload from opencode sessions.

Behavior unchanged: 114/114 tests, typecheck, prettier, and build all
pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant