Skip to content

fix(O128,O122): partial-read edit gate holds on retry; full re-read registers; truncated write refused; resolvable gate blocks - #127

Merged
QiaolongLi1201 merged 5 commits into
mainfrom
o128-partial-read-retry
Oct 12, 2026
Merged

QiaolongLi1201 merged 5 commits into
mainfrom
o128-partial-read-retry

Conversation

@QiaolongLi1201

Copy link
Copy Markdown
Collaborator

O128 (P1 safety) + O122: partial-read edit gate holds on retry; full re-read registers

Root cause

  • O128: O97's recovery (partialReadRecovery, src/tools/tool-helpers.ts) attached the file body to the refusal and then called recordFileState(…'full'). The refusal therefore counted as a full read, and the identical resend was written. The loop guard (tool-loop-guard.ts trackReadScopeLocks) had a matching exception: a refusal with an attached body didn't lock the path, so shell writes to it went through too.
  • O122: read_file is read-only, so an identical full re-read was answered from the loop's replay cache (agent-loop-tool-execution.ts ~166-184). execute() never ran and the full read was never registered.

Changes (gate only gets stricter)

  • The attached body is for reference only and no longer unlocks editing. Every partial-read refusal locks the path. Each refusal ends with the same next step: 「下一步:先完整读取 x 再编辑」 / "Next step: read x in full first, then edit".
  • read_file gets metadata.noReplay, so it always executes. Only read_file has it (tested); the other 22 read-only tools still replay. Its own "unchanged" short reply keeps repeat reads cheap: 48k file, full→full went from 55,994 B to 209 B.
  • Shell write lock: $PWD/${PWD}/$HOME are expanded, and targets built at run time ($D/x, $(pwd)/x, `pwd`/x) are matched against locked paths by their literal suffix.
  • PM ruling: if a full read_file of a file was truncated by the output limit, write_file can't overwrite that whole file (「x 读取时被截断,不能整文件覆盖,请改用 edit 修改」 / "x was truncated when read; whole-file overwrite is not allowed, use edit instead"). edit/multi_edit/apply_patch are unchanged. The block stays in place after edits and is cleared only by a full read that wasn't truncated.
  • O121 block record (PM ruling): gate-type refusals (partial-read edit, truncated write_file) are recorded as resolvable with a realpath key. A later successful change to the same file through the prescribed tools resolves the block and the run exits 0. For partial-read those tools are edit_file/multi_edit/apply_patch/write_file, which can only succeed after a full read. For truncated they are edit_file/multi_edit/apply_patch. If the block is still unresolved at the end, the last line says 「被拦:局部读后编辑 x」/「被拦:截断读后整文件覆盖 x」 and the run exits 9. An exec write never resolves (and fix(safety): O103 pre-block dangling symlinks, curl/wget output, git core.hooksPath #115's "modified" logic stays: exit 9). Safety-type refusals (dangerous command, protected path, hook, -p auto-deny) are not resolvable. The old partial-read clearing compared raw path strings and also cleared on move_file/delete_file; both are tightened.

Tests (process-level ones use the real dist/cli.js -p with a fake model server; zh+en)

  • test/o128-partial-read-retry.spec.mjs: identical resend, changed parameters, multi_edit, apply_patch, write_file and a small-file resend are all refused (exit 9, 被拦). After a real full read, the first edit succeeds: the O106 run2 sequence, the O122 repeated full read, and a small file. A truncated full read refuses write_file, the next edit succeeds right away, and write_file is still refused after it. An untruncated full read allows write_file.
  • test/o128-gate-resolve.spec.mjs (zh+en, 14/14): partial-read refuse→full read→edit = 0 (also via ./notes.txt, realpath); refuse→finish = 9; refuse→exec write = 9. Truncated refuse→edit = 0; refuse→finish = 9; refuse→exec write = 9.
  • test/o128-exec-bypass.spec.mjs: sed -i, cat > ./x, ../dir/x, tee, python, $PWD, $D, $(pwd), backticks and symlink aliases are all blocked.
  • test/o128-no-replay-scope.spec.mjs, plus a credential-file test: .moss/config.json, .env, .apikey-key and *.credentials get no body on the refusal or on any resend.
  • Revert-red: each of the four fixes (attach-unlock, replay, shell variables, truncated write) turns its tests red on its own.

Tightened existing tests (O128) — each only got stricter

Test Before (base 222171d2) After Stricter only?
test/partial-read-recovery.spec.mjs — "after the rejection the next edit succeeds and state is a full read" → renamed "O128: the attached body does not unlock — resend refused until a real full read" partial read → edit refused (with body attached) → read state was fresh full → full re-read got the unchanged stub → resend edit succeeded refused → state stays miss → identical resend refused (same message + same next step, file byte-identical) → real full read_file returns the body (not the stub) → edit succeeds Yes. One allow became a reject (resend); the original end state "edit eventually succeeds and the file is changed" is still asserted. The fresh/stub assertions were replaced by stricter miss/real-body ones. Nothing went from reject to allow.
test/partial-read-recovery.spec.mjs — "write_file after partial read: rejected with content, then the write succeeds" refused with body → next write succeeded refused with body → resend refused, file unchanged → full read_file → write succeeds Yes. Every original assertion kept, plus a new resend refusal and a file-unchanged check.
test/partial-read-recovery.spec.mjs — "~20k-char file: model receives the full body…" (final step) after the attached refusal, the next edit succeeded the next edit is refused; a full read_file, then the edit succeeds Yes. Every body-delivery and no-truncation/elision assertion is unchanged; one allow became a reject.
test/exec-write-lock.spec.mjs — "refusal that attached the full file does not lock" → "O128: refusal that attached the full file still locks until a real full read" after a refusal with an attached body, echo y > src/big.ts via exec was allowed (null) the same exec is blocked (shell write to edit-locked file); a full read_file unlocks it Yes. Allow → reject.

The first three rows are the three tightened O97 tests in partial-read-recovery; the fourth row is the matching loop-guard lock. No assertion was deleted. Each changed expectation turned an allow into a reject (and the old allow now happens after a real full read). Revert check: reverting the fix makes all four red.

Known limitations

  • Paths built inside an interpreter (e.g. python3 -c "open(os.getcwd()+'/x','w')", node -e with path.join(process.cwd(), …)) can't be caught by static parsing. The shell write lock covers literal paths, $PWD/$HOME, $VAR/$(…)/backtick suffixes and symlink aliases. The remaining backstop is the L3 snapshot (workspace snapshot/rollback).
  • A truncated full read still unlocks edit/multi_edit/apply_patch (PM ruling). Those edits are anchored on exact text, so they can't overwrite content the model didn't see.

Needs QA independent safety review (src/safety changed).

…r answered from replay cache

- O97 attached body no longer stamps a full read (tool-helpers partialReadRecovery); loop-guard locks on every refusal
- every partial-read refusal ends with the same next step (en+zh)
- read_file metadata.noReplay: loop replay cache skipped so a full re-read registers (O122)
- process test with 48k-char file + small-file QA repro; O97 unit tests tightened
…gets; credential + noReplay-scope + exec-bypass tests

- shell-write-detect: expand $PWD/${PWD}/$HOME before resolving; run-time-built targets match a locked path by literal suffix (only adds blocks)
- findLoopReplay exported: noReplay only on read_file, other read-only builtins still replay (test)
- process tests: sed -i, cat > ./x, ../dir/x, tee, python, symlink alias, $PWD, $D, $(pwd), backtick all blocked after partial-read refusal
- credential files (.moss/config.json, .env, .apikey-key, *.credentials): no body on refusal or any resend
…lvable by realpath via prescribed tools; exec never resolves
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@QiaolongLi1201
QiaolongLi1201 merged commit 645359a into main Oct 12, 2026
8 of 12 checks passed
@QiaolongLi1201
QiaolongLi1201 deleted the o128-partial-read-retry branch October 12, 2026 10:29
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