[FIX] skills: file-write results are read back from disk (add write-file-b64)#243
Open
MartinEbner wants to merge 1 commit into
Open
[FIX] skills: file-write results are read back from disk (add write-file-b64)#243MartinEbner wants to merge 1 commit into
MartinEbner wants to merge 1 commit into
Conversation
3 tasks
write-file and append-file returned a static success atom (WRITE-FILE-SUCCESS) that certifies only that the Prolog open/write/close calls succeeded - not that the intended content is on disk. Two documented failure classes pass straight through it: content mangled on the way to disk (nested quotes, backslashes) while the write itself "succeeds", and the agent later claiming writes or byte counts with nothing in its context to check against. Read-back-after-write discipline has proven unenforceable by prompting alone. The file-writing skills now route through a new src/fileio.py (same py-call pattern as websearch/logger) and return a result built exclusively from a read-back of the file on disk: WRITE-VERIFIED file=/tmp/x.txt bytes=201 sha256=1f9a2b... head='...' tail='...' APPEND-VERIFIED file=/tmp/log.txt bytes=12 sha256=... head='line1\nline2\n' tail='' WRITE-FAILED file=/nope/x.txt: [Errno 2] No such file or directory APPEND-FAILED file=/tmp/absent.txt: file does not exist The agent can only relay these values, not compose them, and can spot a mangled write in the same cycle by comparing head/tail against what it intended. Failures return explicit *-FAILED strings; today a failed open surfaces as a generic multi-command error with no file detail. New skill write-file-b64 takes base64-encoded content and writes the decoded bytes, closing the quote/backslash-mangling class at the input side (binary-safe, tolerates line-wrapped base64; registered in helper.py command parsing and lib_omegaclaw.metta like the other py-call modules). Design notes: - append-file keeps its contract: the file must exist, a newline is added. - appendToHistory (memory.metta) keeps the Prolog-backed append under the name append-file-raw: it passes a (library ...) file-spec term that only the Prolog open/4 path resolves. Its body drops the old exists_file guard (rebase note below). - Nothing consumes the WRITE-FILE-SUCCESS/APPEND-FILE-SUCCESS atoms (definition sites only), and string results are already the norm for skills (read-file, shell, metta, websearch return strings). - Result strings are single-line: head/tail snippets are 80 bytes with backslashes, newlines and carriage returns escaped, so a literal backslash-n in content is distinguishable from a real newline; sha256 is truncated to 16 hex chars and skipped for files over 2 MB. Docs: reference-skills-io.md Returns sections updated (they said "True", which was already out of date) and a write-file-b64 entry added; README reference line extended. Tests: Autotests/unit/test_fileio_verified_writes.py - 11 in-process unit tests (module loaded by file path; no container, network, or token), wired into Autotests/run_mandatory. The existing mock/ container tests (test_create_file_mock etc.) exercise the rewired skills end-to-end. Rebased onto main after OMEGA-239 (safe history/prompt reading) merged: its appendToHistory exists-check + create collapses into a single append-file-raw call, with append-file-raw dropping the old body's leading exists_file guard so that SWI open/4 in append mode creates a missing history file (verified in PeTTa: with the guard the clause fails silently on a missing file; without it the file is created and appended). Net behavior on the harness history path matches main, including the recreate scenario of mock/test_memory_missing_history_file_mock.py. OMEGA-239's getPrompt/getHistory safe-reads are untouched.
MartinEbner
force-pushed
the
feature/verified-file-writes
branch
from
July 17, 2026 16:13
be47a42 to
b1598ba
Compare
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.
write-fileandappend-filereturn a static success atom (WRITE-FILE-SUCCESS) that certifiesonly that the Prolog
open/write/closecalls succeeded — not that the intended content is ondisk. Two failure classes pass straight through it, both extensively documented by users running
agents on this stack: content mangled on the way to disk (nested quotes, backslashes) while the
write itself "succeeds", and the agent later claiming writes or byte counts with nothing in its
context to check against. "Read the file back after every write" as a prompt rule has repeatedly
proven unenforceable — the model skips it, and when asked to confirm it invents plausible numbers.
The check has to live in the harness, where it cannot be skipped.
This PR makes the skill result the verification. The file-writing skills route through a new
src/fileio.py(same py-call pattern aswebsearch/logger) and return a result builtexclusively from a read-back of the file on disk:
(real output of the implementation, not composed for the PR)
These values come from disk, not from the model — the agent relays them and can spot a mangled
write in the same cycle by comparing
head/tailagainst what it intended. Failures returnexplicit
*-FAILEDstrings instead of raising into the loop (today a failed open surfaces as ageneric multi-command error with no file detail).
A new skill
write-file-b64takes base64-encoded content and writes the decoded bytes —closing the quote/backslash-mangling class at the input side entirely (binary-safe; whitespace
inside the argument is tolerated, which future-proofs for #223's multi-line parsing — today's
line-based parser requires the argument on a single line, and the skill description says so).
The verified result then proves the round trip. Registered in
helper.pycommand parsing andlib_omegaclaw.mettalike the other py-call modules;docs/reference-skills-io.mdupdated(the Returns sections there said
True, which was already out of date) with awrite-file-b64entry added.
Design notes:
append-filekeeps its contract: the file must exist, a trailing newline is added. Itsbytes=reports the file size after the append (read-back ground truth), not the appended chunk size.
On a missing file the result changes from a silent Prolog goal failure (no result at all) to
the explicit
APPEND-FAILED file=…: file does not exist.appendToHistory(memory.metta) keeps the Prolog-backed append under the nameappend-file-raw— it passes a(library …)file-spec term that only the Prologopen/4path resolves. After the rebase onto the merged OMEGA-239 (safe history/prompt reading), its
exists-check + create collapses into the single
append-file-rawcall:append-file-rawdrops the old body's leading
exists_fileguard, soopen/4in append mode creates amissing history file and the history path still self-heals (verified in PeTTa; exercised by
mock/test_memory_missing_history_file_mock.pyinrun_mandatory). OMEGA-239'sgetPrompt/getHistorysafe-reads are untouched.WRITE-FILE-SUCCESS/APPEND-FILE-SUCCESSatoms (definition sitesonly), and string results are already the norm for skills (
read-file,shell,metta,websearch).and carriage returns escaped (
\\,\n,\r) — a literal backslash-n in content staysdistinguishable from a real newline; sha256 is truncated to 16 hex chars and skipped for
files over 2 MB. For files of 81–160 bytes the tail is omitted (head plus hash cover
verification).
permissions changed in the same instant), the result is
*-FAILEDalthough bytes are on disk —erring on the side of the agent re-checking rather than trusting.
write-filewrites the exact bytes it was given (no implicit trailing newline).Tests:
Autotests/unit/test_fileio_verified_writes.py— 11 in-process unit tests (moduleloaded by file path; no container, network, or token), wired into
Autotests/run_mandatory:disk read-back correctness (bytes/sha256/head/tail), quotes-and-backslashes land byte-exact,
UTF-8 byte counting, long-content snippets, a literal backslash-n stays distinguishable from a
real newline in snippets, append adds the newline and verifies the whole file, write-failure and
append-to-missing-file return explicit failure strings, b64 round-trip of hostile content,
line-wrapped b64 accepted, invalid b64 rejected without writing. The existing
mock/containertests (
test_create_file_mocketc.) exercise the rewired skills end-to-end.Run:
pytest --confcutdir=Autotests/unit Autotests/unit/(the--confcutdirskips thecontainer-session fixture in the Autotests root conftest; CI runs the file via
run_mandatory).Related open PRs:
write-fileisuntouched there now and stays exact-bytes per the maintainer call on Bug 9: append-file Concatenates Without Newline After write-file #204 — so the design
question originally flagged here is settled. The reworked Fix append-file concatenating without a newline (#204) #233 adds a Prolog
line_separator/2so
append-filestarts on a fresh line when the file doesn't already end with a newline.Complementary: this PR keeps
append-file's existing trailing-newline-only behavior, so itdoes not by itself fix Bug 9: append-file Concatenates Without Newline After write-file #204 (the read-back makes the concatenation visible, not prevented).
Both PRs touch the same
skills.mettalines, so whichever lands second resolves a small realconflict: if Fix append-file concatenating without a newline (#204) #233 lands first, this PR ports the line-start guard into
fileio.append_fileatrebase (read the last byte, prepend the newline when missing — the verified result then covers
it); if this PR lands first, the same port applies on Fix append-file concatenating without a newline (#204) #233's side.
often content arrives mangled;
write-file-b64bypasses the quoting layer entirely and theverified result proves byte exactness. The two-set registration in
helper.pyrebasesmechanically onto [OMEGA-174] Multi-line command argument parsing of MeTTaClaw #223's rework (it keeps both sets).
getSkillslines this PR rewrites (theappend-filedescription line) — a small textual conflict, whichever lands second rebases mechanically.
No semantic coupling:
get-io-policyis read-only policy introspection.Autotests/run_mandatory—trivial union rebase for whichever lands later.
skills.metta/helper.py— mechanical merges either way.(This branch is rebased onto current
main, which already includes the merged #222 plugin APIand OMEGA-239 — both merged cleanly.)