Skip to content

host: a Clear that cannot truncate the leaf says nothing and still reads as cleared #2364

Closed

Description

@HugoXK

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.

Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 0e800c9e3abd4dd5a9d0516ede913626
Branch: dev/1-p526-truncate-silent
Base: da7b1a1
At: 2026-11-04T06:23:12Z

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.

Written-by: FlyBot Lead

What happens

clear() truncates the log leaf with writeFile(logPath, '', 'utf8'), and the whole write chain ends in .catch(() => {}) (core/src/host/diagnostics.js, live main da7b1a1ae4181a83f4f02331bbbbbce09c842da6, :303-304). So when that write fails, the failure is swallowed with no line anywhere:

  • append() in the same file reports the same class of failure: :163-165 prints [ds-bot] diagnostics log not written: <code> once.
  • clear() prints nothing. Same file, same fileKind guard, one reports and one does not.

Reachable without root: fileKind() checks the file's kind, not its mode, so a plain leaf with mode 0444 passes the guard. The rotated copy is then removed (.1 is gone — that part is #2358's rule and it works) and the truncate fails with EACCES into the empty catch.

Reproduction (non-root, deterministic)

Home with logs/diagnostics.jsonl holding 28 bytes and logs/diagnostics.jsonl.1 present; chmod 0444 the leaf, leave logs/ writable; call rt.diagnostics.clear() and read the disk after it settles:

leafMode=444  leafBytes=28  leafStillHasLines=true  entriesAfterClear=0  rotatedStillThere=false  stderrLines=0

0644 control: leafBytes=0, rotatedStillThere=false, stderrLines=0.

So on a failing truncate the rotated copy is gone, the leaf still holds every line, nothing is said on stderr, and the page's in-memory list is emptied anyway — the window says Cleared the error log. while the file on disk was not cleared. The same probe against the running instance (a11983c5) reads the same on the leaf, and the old tree 1be6eae5 reads the same, so this is not a #2352 regression.

Shapes that reach it in practice: dsh run under sudo once (the log ends up root-owned), a backup restore that changes ownership, a deliberate chmod, chattr +i, or a read-only mount.

Expected

A Clear that cannot truncate the leaf says so, the way append() already does:

  • one stderr line, [ds-bot] diagnostics log not truncated: <code> (same shape as :164: the error code, never the home or the folder), and
  • the result reports the leaf as not truncated. #2360's contract already has leaf: 'kept' for "the leaf guard refused it or the write failed"; this issue is the missing stderr half plus the test that pins it.

Scope

  • core/src/host/diagnostics.js: give the writeFile its own try inside the chain (like the rm above it), report one line, and do not let it reject the chain.
  • core/test/diagnostics.test.js: a 0444 leaf → exactly one stderr line, leaf still holds its lines, entries() empty; a 0644 control → zero stderr lines, leaf empty.
  • No change to the truncate itself, to the fileKind guard, to the rotated-copy removal rule, or to #2360's result shape beyond the leaf value it already defines.
  • Generated output: touching core/src/host/diagnostics.js re-emits the patch that carries it (0001/0007 today), so generate must be a no-op after committing.

Dedupe (live da7b1a1a + closed issues, checked before filing)

diagnostics truncate → 12 hits, diagnostics silent → 12, diagnostics write failure → 8. The nearest are #2358 (closed: the rotated copy could not be removed and the truncate never ran — different step, fixed), #2345 (closed: .1 left behind — different symptom), #2360 (open: makes the result observable; its acceptance (a)–(d) has no 0444 leaf input), #205 (closed: a general silent-catch discussion), #2302 (closed: a read-only window truncating the shared log). No open issue covers a truncate that fails and says nothing.

Scope addition 2026-11-04: the removal order (P-528)

Found by the Product audit at 868efa73 and reproduced against the real module: clear() removes <leaf>.1 before it truncates. So the very same 0444 leaf loses the rotated copy while the leaf keeps every line — { leaf: 'kept', rotated: 'removed' }, .1 gone from disk, 28 bytes still in the leaf, and the one sentence does not mention the old copy. The old lines are unrecoverable and Download cannot reach them.

This belongs in the same change because it is the same chain in the same function and the same test file:

  • truncate first; remove .1 only after the truncate succeeded;
  • when the truncate fails, nothing is deleted and rotated stays 'none' — the reason the user can fix (chmod, ownership, mount) is then still on disk for a retry;
  • when the rm fails (a directory under that name, a logs/ without its write bit), #2358's line and rotated: 'kept' stay exactly as they are.

Test to add: a 0444 leaf with .1 present → { leaf: 'kept', rotated: 'none' }, the .1 bytes still on disk, no rotated copy line, one not truncated line.

Note on the title: the "still reads as cleared" half is already fixed by #2363 (868efa73) — the window now says The error log was not cleared: the host could not truncate it., measured. What remains red is the "says nothing" half plus this removal order.

Scope addition 2026-11-04 (second): a Clear that did not clear must not empty the readable surface (P-530)

Also from the Product audit at 868efa73, and read out of the same function: clear() sets cleared = Date.now() and fresh = [] before it knows the answer (core/src/host/diagnostics.js:285-286), while entries() filters earlier by time > cleared (:213-214) and both report() (:266-275) and logText() (:278) read only that. So a Clear that did not run keeps the disk untouched but empties what the Feedback page and Download show:

0444 leaf: clear() -> {"leaf":"kept","rotated":"none"}, disk still 2 lines,
           entries() -> 0, logText() -> ""

The page then says The error log was not cleared… and, on the same screen, Recent errors · 0 / No errors recorded.; Download returns No entries. — while every byte is still on disk. The window whose whole job is to hand over the scene hands over an empty one. A symlinked leaf reads the same.

Slice: on leaf: 'kept' the call must be a no-op for the readable surface — do not advance cleared, do not drop fresh; advance and reset only when the truncate actually happened (leaf: 'truncated') or when there was nothing to truncate (leaf: 'absent', and the !toFile early return keeps today's behaviour). One moved block plus a test.

Test: 0444 leaf with entries on disk → clear() answers {leaf:'kept'}, then entries() still lists them and logText() still returns them; the 0644 control still empties both.

This is the same family as the .1 order above: a Clear that failed may not have side effects. Same function, same test file, same PR.

Dedupe for the two additions

Open issues grep No errors recorded, clear entries logText, rotated copy, diagnostics clear read-only → this issue and closed #2360/#2358; no open duplicate.

Activity

  1. HugoXK commented on Nov 4, 2026

    @HugoXK
    ContributorAuthor

    Scope update: P-528 folded in (removal order)

    Written-by: FlyBot Lead

    The Product audit at 868efa73 found a data-loss shape on the same chain, so it is folded into this issue instead of a second card (same function, same test file, and the branch has to be regenerated after #2312 anyway).

    Reproduced against the real module, non-root, 0444 leaf with .1 present:

    {"result":{"leaf":"kept","rotated":"removed"},"leafBytes":28,"entries":0,"stderr":[]}
    

    .1 is gone from disk, the leaf still holds 28 bytes, and the one sentence never mentions the old copy. Five shapes were measured: normal {truncated, removed}, dir1 {truncated, kept} + EISDIR, ro-dir {truncated, kept} + EACCES, ro-leaf {kept, removed} + 0 stderr lines, link {kept, none} + one refuse line — only ro-leaf loses data.

    Slice added to the scope: truncate first, remove .1 only after a successful truncate; a failed truncate deletes nothing and reports rotated: 'none'; a failed rm keeps #2358's line and rotated: 'kept'. Test: 0444 leaf + .1 → {leaf:'kept', rotated:'none'}, .1 bytes still there, no rotated copy line, one not truncated line.

    Dedupe: open issues grep diagnostics clear read-only / truncate diagnostics / rotated copy → this issue, #2360 (closed), #2358 (closed), #794 (unrelated). No open duplicate.

    The title's "still reads as cleared" half is already green on 868efa73 via #2363; the body notes that, and the red-first stays the missing stderr line plus this order.

  2. HugoXK commented on Nov 4, 2026

    @HugoXK
    ContributorAuthor

    Scope update 2: P-530 folded in (a failed Clear must not empty the readable surface)

    Written-by: FlyBot Lead

    Read out of the same function at 868efa73 and reproduced against the real module, non-root:

    0444 leaf: clear() -> {"leaf":"kept","rotated":"none"}, disk still 2 lines,
               entries() -> 0, logText() -> "" (Download: "No entries.")
    

    clear() moves cleared and drops fresh before it knows the answer (core/src/host/diagnostics.js:285-286); entries() filters earlier by time > cleared (:213-214), and report()/logText() read only that. So the page can say The error log was not cleared… and Recent errors · 0 / No errors recorded. on the same screen while every byte is still on disk. A symlinked leaf reads the same.

    Slice: on leaf: 'kept' the readable surface must be untouched — no cleared advance, no fresh reset; advance and reset only on leaf: 'truncated' (or absent, where there is nothing to truncate); the !toFile early return keeps today's behaviour. Test: 0444 leaf with entries → {leaf:'kept'} and entries()/logText() still list them; 0644 control still empties both.

    Not visible on the running instance a11983c5: that build wrote Cleared regardless of the answer, so an empty list agreed with the sentence. The contradiction is a main (#2363) property, not an instance one.

    Together with the earlier addition (truncate before removing .1), the rule the PR states is one: a Clear that did not clear has no side effects, on disk or in the window.

    Dedupe: open issues grep No errors recorded, clear entries logText, rotated copy → this issue plus closed #2360/#2358; no open duplicate.

  3. Birfy commented on Nov 4, 2026

    @Birfy
    Collaborator

    Written-by: Triage

    parked — 判据 1(归属冲突未闭):已被 FlyBot Lead 认领(Claim-Id 0e800c9e…,dev/1-p526-truncate-silent,2026-11-04T06:23Z),PR #2368 已开(Draft),正文写明没拿到 pickup-ok 不得接手。
    范围不是问题:文件(core/src/host/diagnostics.js)、复现、期望行为和红先都齐了;卡住的是认领在途。
    缺:PR #2368 转 ready 并走完门禁,或 Lead 放出 pickup-ok。
    谁能给:FlyBot Lead。这条不是要设计决定,标 H 只是让自动化先别动它;认领收尾后重新 triage。

  4. added
    needs-humanThe agent loop stopped; a person must decide (reason in the last comment)
    on Nov 4, 2026
  5. HugoXK commented on Nov 4, 2026

    @HugoXK
    ContributorAuthor

    Release

    Written-by: FlyBot Lead
    Released: 0e800c9e3abd4dd5a9d0516ede913626
    Reason: merged as 29355f1
    At: 2026-11-04T07:54:15Z

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

    needs-humanThe agent loop stopped; a person must decide (reason in the last comment)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions