Repository navigation
fix(coding-agent): keep the reply language after compaction and long tool output - #4540
Open
code-yeongyu wants to merge 6 commits into
Open
code-yeongyu wants to merge 6 commits into
code-yeongyu wants to merge 6 commits into
Conversation
…st and read only the user's own prose Review folds for senpi#4540 (omo#11527): an explicit reply-language request wins and survives compaction; the tag strip is linear and each message's reading is cached; a note can only name a fixed language; no note follows a run whose result carries the user's words; pasted, quoted and log lines no longer decide the language.
…own directive Review fold for senpi#4540 (omo#11527): the request reader is an allow-list of whole directive sentences on the user's own line; quoted, attributed, speaker-prefixed and pasted text never counts. Quoted spans no longer decide the written language. The reading cache also keys on the content's shape, and the linear-scan and cache tests are deterministic.
Review fold for senpi#4540 (omo#11527): a request counts only in a short message (3 lines, 300 chars) whose first or last line is the directive and where nothing marks pasted or reported content (colon-terminated labels, speaker prefixes, quote or blockquote marks, forwarded or original-message headers, attribution verbs). The reading cache keys on an FNV-1a content hash.
…message Review fold for senpi#4540 (omo#11527): a request needs the whole message to be one line of at most 120 characters that is the directive, with no quote, speaker prefix, dash attribution or attribution verb (now also 말씀하셨, 요청하셨, 그러시, mentioned). Any multi-line message has no request by design; every multi-line form from the earlier reviews is a regression case.
This branch has not been deployed
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 code-yeongyu/oh-my-openagent#11527
Reported by @miny6121-sys. Thanks for the detailed report: the timeline of when the drift happened (after compactions, in tool-heavy turns, and even with a "reply in Korean" memory note in place) pointed straight at the context the model sees after a compaction.
Problem
A Korean-speaking user got English replies once a long session had compacted and in turns dominated by English tool output. Nothing in the context named the user's language at that point:
extensions/builtin/compaction/prompts.ts:31-94) asks for an English-template summary and has no language field. After a compaction, the user's own Korean turns are gone. What remains is English summary prose, and then English tool output.convertToLlm(src/core/messages.ts) rendered that summary and the tool results as-is. Long English text then sat between the user's last Korean message and every reply, with no language signal near the point of generation.dynamic-prompt/handoff.ts:27). senpi#4107 tracks that prompt-side gap (mid-turn switches and English todo labels) and is left open by this PR.Measured on a synthetic Korean session (the first regression test below): after a compaction cut in the middle of a tool-heavy turn, the model context on
maincontains no Korean user text and no text that names the user's language.Change
src/core/user-language.ts(new):readReplyLanguagereads the language of one user message from its script. It counts words after dropping code, links and tagged blocks such as<environment_context>, and scripts written without spaces count two characters as a word. A non-Latin script makes up at least 30% of the words names the language. Latin-dominant text is not named: English needs no note, and Latin-script languages cannot be told apart safely. A script shared by several languages is named by its script (Cyrillic script).insertReplyLanguageNotesadds one short user-role note naming that language after a tool-result run that is followed by the next assistant message (or the end of the context). The note is added once 4000 characters of tool output, notices, bash output or branch summaries have built up since the user's last message. The user's own message, or words typed into a tool's answer, resets the count.src/core/messages.ts:convertToLlmapplies the notes. A compaction summary with auserLanguagecarries the same note inside its own text block after</summary>, so no two user messages become adjacent.src/core/session-manager.ts: the session projection setsuserLanguageon the newest compaction summary from the latest user message before that compaction. It is derived at projection time, so sessions that were already compacted get it too, and the session file format is unchanged.src/core/tool-result-user-words.ts: the existing provenance rule for the message after a tool result (wordMessage) is exported asfollowerMessageand reused by the note.packages/agent/src/harness/messages.ts:CompactionSummaryMessagegains the optionaluserLanguage(theAgentMessageunion takes the type from here).Tests
test/suite/regressions/11527-reply-language-after-compaction.test.ts(15 cases):main+ stub module (RED)expected [] to have a length of 1expected [] to deeply equal [ Array(1) ]readReplyLanguagetable (Korean, Korean mixed with English terms, injected tags stripped, Japanese, Chinese, Cyrillic, English quoting one Korean word, French)noneRED was run with the new test and a stub
user-language.tsthat has the real exported signatures, on unchangedmaincode, so every failure is an assertion, not an import error. 11 of 15 fail there. The other 4 are guards that pass on both trees.Verification
convertToLlmexactly as each provider request is built. No live-model run is claimed.npx tsc --noEmit(root): exit 0.oxfmt --checkandoxlint --deny-warningson every changed file: clean.convertToLlm,buildSessionContext, the compaction summary or the SDK prompt bridge (117 files, 1025 tests). One malformed-envelope fallback test (test/compaction/required-compaction-deterministic-fallback.test.ts) caught a crash on content that is not an array, which session files can contain. It is fixed: content is read as untrusted. The only remaining failure,issue-3518-open-session-yields, fails identically onmainin this checkout because it needs a builtdist.Summary by cubic
The coding agent now keeps replies in the user's language after a compaction and after long stretches of English tool output, fixing code-yeongyu/oh-my-openagent#11527. Each request carries a short note naming the reply language once 4000 characters of tool output or summaries have built up, and compaction summaries carry the same note inside their own text.
<environment_context>are ignored.Migration
CompactionSummaryMessagegains optionaluserLanguageanduserLanguageRequestedfields.Written for commit 57349b3. Summary will update on new commits.
Review folds (head 701a198)
readReplyLanguageRequestreads requests such as "앞으로 답변은 영어로 해줘", "영어로 답해줘" and "reply in Korean from now on". It skips negated requests ("영어로 답하지 마", "don't reply in English") and other tasks (translate, write the README in English). The note then names the requested language, marked "(the user asked for it)". The summary carries it asuserLanguage+userLanguageRequested.reply-language-reading.ts,withoutTaggedBlocks). Each message's reading is cached per message and content object, so repeatedconvertToLlmcalls do not re-read history. Perf tests cover three inputs over 100 KB (generics paste, unclosed tags, one long tag name) with a 150 ms bound, plus 20 conversions of a session holding a 120 KB paste.REPLY_LANGUAGE_NAMESis the only text a note can carry; a forged "Reply language: (...)" in a user message is ignored.Tests:
11527-reply-language-reading.test.ts(36 table cases: written language, explicit request in Korean and English, negation, mixed, pasted/quoted foreign text, code, perf). On the previous head, with a shim exposing its detector under the new module name, 15 of the 36 fail on assertions.11527-reply-language-after-compaction.test.ts(13 cases: compaction with and without an explicit request, the per-case note table, alternation, prefix stability, re-conversion, cached re-reads). Related suites at this head: 119 files, 1032 passed, 16 skipped, 0 failed. No live model calls.Review folds, round 2 (head 6bbe85a)
Requests are an allow-list (
reply-language-request.ts). A request counts only when a whole sentence on the user's own line is the directive, optionally after a few lead words ("앞으로", "please"). Reported speech never counts:「」,『』,«», backticks);When in doubt it is not a request.
Quoted spans no longer decide the written language (
user-prose.ts,ownLines).Cache. No caller edits user content in place. The in-place
content.pushcalls are on streaming assistant output only:anthropic-subscription/stream-events.ts:28,anthropic-subscription/stream.ts:263andcursor-cli-oauth/stream.ts:182,184. Context edits and the environment fold build new messages. The cache still keys on the content's shape (block count and text lengths), so an in-place edit by a hook is read again; a test covers it.Deterministic tests. The tag pass counts the characters it visits (
ProseScan) and the test bounds it at 3x the text length for three adversarial inputs over 100 KB. The cache test countsreadReplyLanguagecalls (one across three conversions, two after an in-place edit). No wall-clock assertions remain.Tests:
11527-reply-language-request.test.tsenumerates every quote style and attribution form in Korean and English, each with a positive control. On the previous head, with shims for the new module names, 34 assertion failures are real regressions. The 3 step-counter tests also fail there, but only because the shim has no counter. Related suites at this head: 121 files, 1110 passed, 0 failed. No live model calls.Review folds, round 3 (head 8f96d94)
A request is decided for the whole message (
reply-language-request.ts). It counts only when all three hold::/:, a speaker prefix, any quote or blockquote mark (which also covers a lazy>continuation), a forwarded, original-message or mail header, or an attribution verb;When in doubt there is no request.
Cache. The key is a 32-bit FNV-1a hash of the text parts, so an in-place edit that keeps every length is read again; a test covers it.
Tests. The request table adds colon-terminated labels (Korean, English, fullwidth), forwarded, original-message, Korean-forwarded and mail headers, a lazy
>continuation, a paste, a long message and a middle-line directive, each in Korean and English. The positive controls are영어로 답해줘,please reply in Korean from now on, and a short message with the directive first, then a question. On the previous head, 21 of these cases fail (every new form plus the same-length cache edit). Related suites at this head: 121 files, 1136 passed, 0 failed. No live model calls.