Skip to content

fix: surgically relocate only OpenCode-fingerprinted system entries#156

Draft
griffinmartin wants to merge 1 commit intomainfrom
fix/surgical-system-relocation
Draft

fix: surgically relocate only OpenCode-fingerprinted system entries#156
griffinmartin wants to merge 1 commit intomainfrom
fix/surgical-system-relocation

Conversation

@griffinmartin
Copy link
Copy Markdown
Owner

Summary

Fixes #154 — v1.4.8 (#148) worked around Anthropic's OAuth content validation by bulk-relocating all non-core system[] entries to the first user message. While this fixed the 400 error from #147, it caused a real regression in long conversations: AGENTS.md instructions, environment metadata, skills blocks, and per-message system prompts lost the privileged attention priority that system[] provides, and prompt-cache efficiency dropped because static instructions were interleaved with per-turn user content.

This PR replaces the bulk relocation with a surgical relocation that only moves entries containing OpenCode fingerprints. AGENTS.md, CLAUDE.md, skills blocks, and Claude Code-format env blocks all stay in system[] where they belong.

What changed

src/transforms.ts — Added an exported isOpenCodeBrandedEntry(text) helper backed by a 4-pattern regex array, and refactored the relocation loop to call it per entry:

const OPENCODE_FEATURE_PATTERNS: RegExp[] = [
  /\bopencode\b/i,           // brand, package name, product URLs
  /anomalyco/i,              // GitHub org
  /Workspace root folder/,   // OpenCode-specific env phrase
  /<directories>/,           // OpenCode-specific env tag
]

Entries matching any pattern → relocated to the first user message (same as v1.4.8). Entries that don't match → kept in system[].

src/transforms.test.ts — Added 10 new unit tests for isOpenCodeBrandedEntry, 6 new behavioral tests for surgical relocation (keeps AGENTS.md in system[], keeps CC-format env in system[], keeps skills in system[], relocates anomalyco URLs, relocates OpenCode env blocks, mixed branded/non-branded input). Updated 4 existing tests whose assertions reflected the old bulk-relocation behavior.

How I found the detector features

I built a temporary probe script (scripts/probe-system-validation.ts, not committed) that sent real minimal requests to api.anthropic.com/v1/messages with varying system[] contents, using the existing OAuth credential path and billing header construction. 16 probes against claude-opus-4-6:

# Test Result
V1 baseline (billing + identity) 200
V2 + OpenCode env block (current v1.4.7 format) 400
V3 + generic AGENTS.md 200
V4 + generic skills block 200
V5 + env + AGENTS.md + skills (no OpenCode core) 400 (env only)
V6 + OpenCode core prompt 400
V7 + OpenCode core, brand + URL stripped 200
V8 + OpenCode core, fully reworded 200
V9 full v1.4.7 simulation 400
V10 env in exact Claude Code format 200
V11 CC env + "Workspace root folder" alone 200
V12 CC env + <directories> alone 200
V13 CC env indented 200
V14 CC env with "Here is some" wording 200
V15 OpenCode core minus anomalyco URL only 200
V16 OpenCode core minus "OpenCode" brand only 200

Key findings:

  • The classifier is multi-feature, not substring-based. No single feature (V11–V14, V15, V16) triggers the check in isolation; combinations do (V2, V6, V9). This matches the original 400 'out of extra usage' on valid Max accounts — API now validates system prompt content #147 evidence that random 30K text passes while real OpenCode content fails.
  • AGENTS.md and skills blocks are safe to keep in system[] as long as they don't mention OpenCode themselves.
  • The env block v1.4.7 sends is itself a trigger, independently of the OpenCode core prompt — because it contains Workspace root folder, <directories>, and other OpenCode-specific features. This is a finding the v1.4.8 PR didn't surface.

Rather than try to track the exact classifier threshold (which can change), the detector relocates any entry containing any known fingerprint feature. This over-relocates in rare false-positive cases (e.g. an AGENTS.md that explicitly mentions opencode) but is robust against classifier tuning.

Why this matters for long conversations

System entry v1.4.8 (bulk) This PR (surgical)
Billing header stays stays
Identity prefix stays stays
AGENTS.md / CLAUDE.md (no OpenCode mention) relocated stays in system[]
Skills block relocated stays in system[]
Claude Code-format env relocated stays in system[]
OpenCode-format env (v1.4.7) relocated relocated (matches <directories>)
OpenCode core agent prompt relocated relocated (matches \bopencode\b)

Instructions that were being buried in the first user message — and drifting further from the active context with every turn — now live in system[] again with full attention priority and cache stability.

Test plan

  • All 227 tests pass (221 existing + 6 new behavioral tests; 10 new detector unit tests were added inside a new describe block)
  • pnpm run build compiles cleanly
  • pnpm run lint — 0 warnings, 0 errors, formatting clean
  • Manually smoke-tested locally against Anthropic API with a Max subscription account that previously hit 400 errors — plugin works and no 400 observed

Known limitations / follow-ups

  1. Env block still relocates today. The current OpenCode env block contains enough OpenCode-specific features to trigger the classifier, so it's still moved to the user message. A follow-up PR could normalize the env block (strip Workspace root folder, <directories>, and align to Claude Code's exact format) to keep it in system[] too. I kept that out of this PR to keep scope focused.
  2. False positive on opencode mentions in user AGENTS.md. If a user's AGENTS.md contains the word opencode, that entry gets relocated. This is an acceptable over-match — the content still reaches the model, just with reduced attention priority. Users can rephrase if they want it to stay in system[].
  3. Classifier may update. If Anthropic changes their detection, the patterns are trivially extensible in OPENCODE_FEATURE_PATTERNS.

Fixes #154

v1.4.8 (#148) worked around Anthropic's OAuth content validation by
bulk-relocating all non-core system entries to the first user message.
This caused a regression in long conversations (#154) because AGENTS.md,
environment metadata, and skills blocks lost system-level attention
priority and prompt-cache efficiency.

Replace bulk relocation with a feature-based detector that only moves
entries containing OpenCode fingerprints (\bopencode\b, anomalyco,
'Workspace root folder', <directories>), keeping all other system
content in system[] where it belongs.

Empirical probing against api.anthropic.com confirmed the classifier is
multi-feature rather than substring-based — AGENTS.md, skills, and
Claude Code-format env blocks all pass validation cleanly when kept in
system[].

Fixes #154
@griffinmartin griffinmartin marked this pull request as draft April 9, 2026 04:58
@ihabwahbi
Copy link
Copy Markdown

I traced this end-to-end against the actual OpenCode runtime path and there is one important gap in the current approach.

#156 is directionally right, but in normal OpenCode sessions transformBody() usually does not receive separate system[] entries for AGENTS / skills / env.

Runtime path:

  • packages/opencode/src/session/prompt.ts builds system as separate pieces: env, skills, InstructionPrompt.system(), structured-output prompt.
  • packages/opencode/src/session/llm.ts:90-102 then immediately collapses provider prompt + input.system + input.user.system into a single joined string before experimental.chat.system.transform runs.
  • This plugin's hook in src/index.ts currently only prepends the Claude Code identity.
  • So transformBody() sees something much closer to [identity?, giant OpenCode block] than the separated shape used in the new tests.

That means the current isOpenCodeBrandedEntry() logic still relocates the whole combined block in the common case, because the combined text contains OpenCode fingerprints. In practice, AGENTS.md / skills / safe env content are still moved out of system[] and into the first user message.

I verified this locally before patching by feeding a runtime-shaped combined system blob through the current code path.

What worked locally

I added a small follow-up fix in experimental.chat.system.transform that splits the combined OpenCode system blob back into separate entries before transformBody() runs:

  • OpenCode core prompt stays isolated and can be relocated
  • env block is normalized into a Claude-Code-safe entry (Working directory, Is directory a git repo, Platform, Today's date)
  • OpenCode-only env extras like Workspace root folder / <directories> are preserved as separate branded entries so they relocate cleanly instead of poisoning the safe env block
  • skills block stays separate
  • Instructions from: ... entries stay separate
  • already-prefixed cases are handled too

With that in place, the runtime-shaped flow behaved the way this PR intends:

  • OpenCode-branded content relocated
  • AGENTS / skills / normalized env stayed in system[]
  • live Anthropic smoke test returned 200 (same machine/account that was previously hitting the extra-usage 400)

I also added runtime-shaped tests locally for:

  1. combined prompt splitting in the system hook
  2. already-prefixed combined prompt splitting
  3. end-to-end system.transform -> transformBody() preserving AGENTS / skills / safe env in system[]

So I think the missing piece here is not in src/transforms.ts anymore, but in src/index.ts: the plugin needs to reconstruct segmentation before the classifier-based relocation logic can actually deliver the regression fix in real sessions.

Happy to share the exact shape/tests if helpful, but the key point is: the current tests prove the surgical relocation works on synthetic separated system[] input, while OpenCode's real runtime shape is still mostly a single combined block.

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.

System prompt relocation in v1.4.8 degrades instruction-following in long conversations

2 participants