Skip to content

'truncated' drop mode turns tool inputs into "half-real" values that the model copies into new tool calls #431

Description

@tsukasa-mixer

Short description

inputstring values to **5 characters + a...[truncated]` sentinel

What happened?

Package: @cortexkit/opencode-magic-context
Severity: High — produces corrupted, persisted tool records and real side-effects (files created at wrong paths, failed tool calls, corrupted long-term memories).


Environment

Component Version
@cortexkit/opencode-magic-context 0.41.4 (= npm latest at time of report)
opencode (host) 1.18.29
Node.js v22.22.2
OS Linux
Plugin peer dep @opencode-ai/plugin >= 1.15.0

Plugin install location (npx/opencode package cache):
~/.cache/opencode/packages/@cortexkit/opencode-magic-context@0.41.4/node_modules/@cortexkit/opencode-magic-context


Summary

In the context-compaction "truncated" drop mode, the plugin shrinks a tool call's input string values to 5 characters + a ...[truncated] sentinel (and sets output to a [dropped §N§] sentinel) in the LLM-facing transcript.

The problem: the truncated input looks like real but mangled data (e.g. "command":"docke...[truncated]", "action":"writ...[truncated]"), not like an explicit "this was dropped, recover it" placeholder. When the model later wants to repeat or continue that action, it copies the truncated values into a brand-new tool call. That new call is a normal live tool part, so it gets persisted to opencode's session DB with the corrupted input — and it actually executes with the corrupted values, producing real side effects.

This is a data-integrity defect: the model is induced to emit tool calls whose inputs are not what the user/intent asked for.


Root cause (with code references)

All references are to the shipped build (0.41.4): dist/index-0y2mcg5y.js and dist/index.js.

  1. Sentinel + input truncation. TRUNCATION_SENTINEL2 = "...[truncated]" (index-0y2mcg5y.js:9547). truncateInputValues (index-0y2mcg5y.js:9557) rewrites every top-level string value of a tool input in place:

    function truncateInputValues(input) {
      for (const key of Object.keys(input)) {
        const value = input[key];
        if (typeof value === "string") {
          if (value.endsWith(TRUNCATION_SENTINEL2) || value === "[object]" || /^\[\d+ items\]$/.test(value))
            continue;
          input[key] = value.length > 5 ? `${safeSlice2(value, 5)}${TRUNCATION_SENTINEL2}` : value;
        } else if (Array.isArray(value)) {
          input[key] = `[${value.length} items]`;
        } else if (value !== null && typeof value === "object") {
          input[key] = "[object]";
        }
      }
    }

    Note input[key] = ... — this mutates the input object in place (first 5 chars + ...[truncated]). Arrays become [N items], objects become [object].

  2. Applied when a tool part is dropped in "truncated" mode. truncateToolPart (index-0y2mcg5y.js:9473) sets state.output = "[dropped §N§]" and calls truncateInputValues(state.input) (when input size > 500). It is invoked per occurrence from createToolDropTarget().truncate() (index-0y2mcg5y.js:9672):

    const truncate = () => {
      const entry = index.get(compositeKey);
      if (!entry || entry.occurrences.length === 0) return "absent";
      if (!entry.hasResult) return "incomplete";
      for (const occurrence of entry.occurrences) {
        clampCloneInPlace(occurrence, (part) => truncateToolPart(part, tagId));
      }
      clearThinkingParts(thinkingParts);
      return "truncated";
    };
  3. Runs on the live transcript. The whole transform is wired to the experimental.chat.messages.transform hook (index.js:38968), i.e. it operates on the live message objects shown to the model.

Key distinction (verified empirically)

The plugin's in-place mutation is NOT persisted to opencode's DB. Scanning the full opencode.db (all part rows of type:"tool"):

  • 0 parts have state.output === "[dropped §N§]".
  • The persisted corrupted parts all have real tool outputs (the tool genuinely ran), never the drop sentinel.

So opencode stores the original full input/output. The only path by which truncated values reach the DB is the model re-issuing a new tool call using the truncated values it sees in the transcript. The defect is therefore not "the plugin writes bad data to the DB" — it is "the plugin presents inputs in a form that reliably leads the model to emit new, corrupted tool calls."


Evidence (from a full scan of ~/.local/share/opencode/opencode.db)

Signature used: a top-level tool-input string value equal to exactly 5 chars + ...[truncated] (the truncateInputValues shape).

30 tool parts match, by tool:

tool count
ctx_memory 12
bash 7
paseo_create_agent 5
edit 3
write 2
task 1

9 of the 30 have status: "error". Time window 2026-09-02 → 2026-09-07.

Concrete persisted records (truncated input → real output proving it executed):

  • bashinput: {"command":"whic...[truncated]"} → output /usr/bin/bash: whic...[truncated]: command not found. (Also echo..., slee..., true..., docke..., chrom... variants.)
  • ctx_memoryinput: {"action":"writ...[truncated]","category":"CONFI...[truncated]","content":"YouTr...[truncated]"} → output Error: Action 'writ...[truncated]' is not allowed.
  • edit — both oldString and newString truncated to the same 5-char prefix → 43 Error: No changes to apply: oldString and newString are identical (38 edit, 5 bash).
  • writeinput.filePath truncated to /tmp/...[truncated] while content was full → a real 2203-byte file was created at the literal path /tmp/...[truncated].
  • ctx_memory (succeeded)input: {"action":"write","category":"ARCHITECTURE","content":"<5 chars>...[truncated]"} (18-char content) → output Saved memory [ID: 439] in ARCHITECTURE. — this created a corrupted long-term memory (content = 5 chars + sentinel). Several cross-session memories were created this way (5-char/sentinel content), i.e. the defect corrupts not just the session DB but the plugin's own durable memory store.

Corroborating (not counted above): ~113 parts contain ...[truncated] in their output — that is opencode's own large-output truncation, unrelated to input corruption, and should not be conflated with this bug.


Impact

  1. Corrupted persisted tool records in opencode's session DB (inputs are 5-char/sentinel, not the real command/arguments).
  2. Real side effects from executing garbage inputs: shell "command not found", file created at a wrong/truncated path, no-op edits ("identical oldString/newString").
  3. Corrupted long-term memories: re-issued ctx_memory write calls with truncated content pass validation (valid action/category) and persist 5-char/sentinel memories, degrading future-session knowledge.
  4. Wasted agent turns: the model retries the same broken call (e.g. repeated docke...[truncated]), compounding context and cost.
  5. Irreversible: the original inputs of the re-issued calls are not recoverable (the plugin's store keeps user messages, not tool inputs).

Suggested fix

The core issue is the representation of a dropped input. Options (any of which breaks the "model copies it" behavior):

  1. Do not render dropped inputs as mangled real data. Replace the input with an explicit, non-copyable marker that also names the recovery path, e.g.:
    [input dropped — original not shown; recover via ctx_expand(message=<N>)]
    rather than docke...[truncated]. The current ctx_expand/ctx_search recovery mechanism already exists; the dropped input should point to it instead of inviting a verbatim copy.
  2. Guard the model's re-issue. When a new tool call's input matches the truncation signature (5 chars + sentinel / [N items] / [object]), reject or warn instead of executing (a tool.execute.before-style guard), since such an input is always a transcription of a dropped value, never a genuine user intent.
  3. Truncate only the output, keep the input intact for tool parts that are likely to be re-issued (edits, writes, memory writes), or at minimum keep the full input available in the transcript for the turns immediately following the drop.

A minimal, safe change is option 1 (change the dropped-input rendering) — it removes the "looks like real data" property that triggers the copy behavior.


Reproduction (characteristic, not deterministic)

The trigger is model-dependent, so this is a characteristic reproduction rather than a unit test:

  1. Run a long opencode session under the magic-context plugin until context pressure causes the plugin to mark completed tool parts for drop in truncated mode (the mode that applies truncateToolPart).
  2. Observe the LLM-facing transcript: those tool calls now show input values as xxxxx...[truncated] and output as [dropped §N§].
  3. On a later turn where the model needs to repeat/continue one of those actions (retry a failed command, re-save a memory, redo an edit), it re-issues the call copying the truncated input.
  4. The new call executes and persists with the truncated input → corrupted DB record + side effect (e.g. command not found, oldString and newString are identical, file at a truncated path).

A deterministic assertion for a regression test: after a drop in truncated mode, no subsequently-persisted tool part's input may contain the ...[truncated] sentinel / [N items] / [object] markers.


Notes / scope

  • The plugin is already at the latest published version (0.41.4); there is no newer release with a fix.
  • There is currently no config option to disable input-value truncation in the drop mode (compaction in config controls the host's built-in compaction, which the plugin disables — it does not gate this behavior).
  • The in-memory mutation itself is benign to the DB (verified: 0 persisted [dropped §N§] outputs); the bug is specifically the downstream model re-issue it induces.

Diagnostics

Plugin version

0.41.4

OpenCode version

1.18.29

Platform

linux x64

Client

OpenCode Web

Log output (optional)

https://github.com/cortexkit/magic-context/issues/250

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions