Skip to content

fix(*): the desk lists a task's files as changes and hands over only what was delivered - #656

Merged
0xKT merged 2 commits into
mainfrom
fix/desk_diff_lists_task_files
Sep 22, 2026
Merged

0xKT merged 2 commits into
mainfrom
fix/desk_diff_lists_task_files

Conversation

@0xKT

@0xKT 0xKT commented Sep 22, 2026

Copy link
Copy Markdown
Member

Summary

A spawned sub-agent that wrote ten markdown files showed up on the desk as ten "Task output" rows on the deliverables tab and a diff tab saying "No changes this turn". Two things were wrong at once: the deliverables tab listed files nobody delivered, and the diff tab hid every whole-file write a task made (it only took op === 'edit' task files).

  • The diff tab now lists every file a task's nodes wrote or edited, under a "Task changes" heading; its badge and the tab the desk opens on count them too, under the same id openDeskDiff marks read.
  • The deliverables tab lists only what deliver_files handed over. A sub-agent has no deliver_files; the main agent decides what is a product, which is what the system prompt already tells it.
  • A file the turn created draws + (kind add) instead of M: live from tool.complete.file_change without before, on reload from the stored diff's @@ -0,0 header. A whole-file write over an existing file stays M, and the transcript's "New" badge follows the same rule instead of calling every whole-file write new. A created file rewritten in the same turn stays a creation.
  • Task-file glyphs: an edit_file is always a modification (the prototype rule from feat(*): draw the desk tasks tab from a tasks.list read #509 drew + for an edit that deleted nothing); a whole-file write that replaced nothing is a creation.
  • Labels: "N products" on a task row becomes "N files changed"; "Task output" becomes "Task changes". The TUI i18n table is regenerated; the desk tasks spec (section 7) says where task files go now.
  • Second commit: prettier --write on two ui-tui files that arrived on main unformatted. The pre-commit prettier hook checks the whole ui-tui tree whenever a file under it changes (this PR regenerates the i18n table), so it fails until those two are formatted.

Not in this PR: a file deleted by exec rm still leaves no row anywhere. A follow-up branch (feat/track_file_deletions) adds deletion tracking on the backend and the red D row; it is stacked on this one.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

Run from the worktree after rebasing onto a48c4edf:

  • npm test --prefix ui-web -- 192 files, 2599 tests passed (the whole suite, gates included)

  • npm run lint --prefix ui-web -- 0 errors (4 pre-existing warnings); npm run type-check --prefix ui-web; npm run gen:check --prefix ui-web -- generated.ts matches the contract

  • npm run lint --prefix ui-tui -- 0 errors (23 pre-existing warnings); npm run type-check --prefix ui-tui; npm run lint:i18n --prefix ui-tui -- generated catalogue is up to date

  • uv run pre-commit run --from-ref origin/main --to-ref HEAD -- passes with the second commit (fails on prettier (ui-tui) without it)

  • make lint-python; make test-python -- no Python file changes; both pass

  • Real gateway: raven serve from this branch with an isolated RAVEN_HOME (channels off, default model deepseek/deepseek-v4-flash), one conversation that spawns the in-process Raven sub-agent to write_file two files and edit_file one, one conversation whose main agent write_files a new file. Before: diff tab "No changes this turn", deliverables tab listing both task files under "Task output", the main agent's new file drawn M. After: diff tab lists both task files with + under "Task changes", deliverables tab "Nothing produced yet", the main agent's new file drawn +. Same two conversations reopened after a reload read the same.

  • Mutation check on the new tests: dropping task ids from idsOf('diff'), reverting taskChangeKind to the deletion-count rule, dropping the add mark in record.ts, and widening the transcript's "new" back to every write each turn at least one new test red.

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

UI only; the wire is untouched. Files a task wrote leave the deliverables tab (they were never delivered) and appear on the diff tab, so a reader who relied on the shelf to open a sub-agent's output now finds it under diff, or in the task pane's file chips as before. Two i18n keys are renamed (gui.ws.task_output -> gui.ws.task_changes, gui.tasks.artifacts_n/_1 -> gui.tasks.files_n/_1); no other consumer of them exists. Rollback: revert the two commits.

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Related Issues

N/A

0xKT and others added 2 commits September 22, 2026 18:57
…what was delivered

A sub-agent's writes reached the desk's deliverables tab as "Task output" and
never its diff tab, so a spawn that wrote ten files showed a shelf of ten
products and a diff tab saying nothing changed. Delivery is what deliver_files
decides; a file a node wrote is a change to the working directory.

- DeskPalette: the diff tab lists every file a task's nodes wrote or edited
  under a "Task changes" heading; the deliverables tab lists only deliver_files
  rows and its empty state depends on those alone.
- desk/store: the diff tab's badge and pickTab count task files, under the same
  id openDeskDiff marks read.
- tasks/store: taskChangeKey / taskChangeKind; an edit_file is always a
  modification, a whole-file write that replaced nothing is a creation.
- workspace/record: a write_file that created the file becomes kind 'add' --
  from file_change without 'before' on the live event, from the diff's
  "@@ -0,0" header on replay -- and a created file rewritten in the same turn
  stays a creation; the transcript's "New" badge follows the same rule instead
  of calling every whole-file write new.
- i18n: gui.ws.task_output -> gui.ws.task_changes, gui.tasks.artifacts_n/_1 ->
  gui.tasks.files_n/_1 ("files changed"), gui.ws.chip.add for the new kind; the
  TUI table is regenerated.
- docs/specs/2026-09-18-desk-tasks-list-design.md section 7 says where task
  files go now.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
The pre-commit prettier hook checks the whole ui-tui tree whenever any file
under it changes, and these two arrived on main unformatted, so every PR that
touches ui-tui -- this one regenerates the i18n table -- fails that hook until
somebody runs prettier on them.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
@0xKT
0xKT requested a review from gloryfromca September 22, 2026 11:07

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; suggestions only, and they are marked inline.

The deliverables/task-changes separation is coherent, the unread identity matches openDeskDiff, and the live/replay add kind is carried through the session pipeline and transcript consumer without weakening the existing assertions. I covered AGENTS.md and the Web UI context rules, the full diff, relevant callers and persisted/live history paths, the two-commit history, backward compatibility, test changes, and the Source/Store/Seam architecture constraints.

Verification: the seven focused web test files passed (427 tests); web and TUI type checks passed; the web RPC generator check and TUI i18n catalogue check passed. The one inline item is a narrow display-classification edge case and does not hold up the central fix.

Comment thread ui-web/src/features/tasks/store.ts
@0xKT
0xKT merged commit da13bdd into main Sep 22, 2026
28 checks passed
@0xKT
0xKT deleted the fix/desk_diff_lists_task_files branch September 22, 2026 12:06
LivXue added a commit that referenced this pull request Sep 22, 2026
…der it (#658)

## Summary

The composer is an **opaque** card floating over the transcript
scroller, and
nothing ended the transcript's paint before it reached the card.
Scrolled up, a
line was cut mid-glyph at the card's top edge, and the 22px gutter below
the
card left the next line legible *underneath* the field the reader types
in.

This gives `.scroll` a mask whose alpha reaches zero at the top of
whatever is
docked, over a 40px ramp:

```css
.scroll {
  --tail-fade: 40px;
  mask-image: linear-gradient(180deg,
    #000 calc(100% - var(--lift, 96px) - var(--tail-fade)),
    transparent calc(100% - var(--lift, 96px)));
}
```

Three decisions worth stating:

- **A mask, not a ground-coloured band.** A band is a rectangle painted
*over*
the transcript, so it has to match whatever is behind it; on this page's
wash
(a gradient) it reads as a flat seam. A mask takes the alpha out of the
transcript itself, so there is no ground to match, and it needs no
second copy
per theme. `page.css` records that an earlier band above the composer
was
deleted for exactly that reason, which is why this does not reintroduce
one.
- **Anchored to `--lift`, which already exists.** `dockLift`
(`features/composer/store.ts`) publishes the distance from the chat
column's
bottom edge up to the top of the highest docked thing. `.col` spends it
as
bottom padding and `.backpill` as its offset. Spending the same number
here is
what makes the clear edge land on the composer's top edge however tall
it has
  grown (a longer field, queued rows, a clarify sheet), rather than on a
  hardcoded resting height.
- **The tail is never under the ramp.** Because `.col` reserves the same
`--lift`, the newest message sits above the ramp when the scroller is at
rest,
so the fade engages only once the reader scrolls back. Nothing is hidden
at
  the moment a message arrives.

The second commit corrects the `.dock` leading comment, which this
change
falsified: it claimed the transcript "scrolls underneath and stays
legible
through the glass" and that "the old fade band above the composer is
gone".

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

Verified in headless Chrome two ways: against `page.css` itself in a
probe page
carrying the real DOM structure, and against the running app over CDP.
Painted
ink was measured, not layout rects -- masked text still occupies its
box, so a
rect-based measurement is byte-identical before and after and cannot see
this
change at all. Ink is counted as pixels differing from the page's own
background
(the wash is not white, so the background is sampled from an empty
band).

**The clear edge lands on the composer's top edge** (`aligned` compares
the
`getBoundingClientRect().top` of `.dock-in` against the mask's
transparent stop):

| case | `--lift` | composer top | mask clears at | aligned |
|---|---|---|---|---|
| light theme | 106px | 654 | 654 | yes |
| dark theme | 106px | 654 | 654 | yes |
| composer grown to three lines | 180px | 580 | 580 | yes |
| landing page (`data-fresh`) | 106px | 654 | 654 | yes |
| live app, session "ESG..." | 139px | 721 | 721 | yes |

**Nothing is painted at or past the clear edge.** Ink in a 14px band
below it,
measured in the gutters either side of the card: `0` in all five cases.

**The fade is the only thing that changed.** Ink inside the 28px just
above the
card went `1651 -> 144` (91 percent removed), while the control band
100-600px
above it was `36133` before and `36133` after -- unchanged to the pixel.

**No scroll cost.** 240 scroll frames x 3 interleaved runs each, mask on
vs off:
median frame `16.70ms` both, p95 `16.80ms` both, worst `16.77 ->
17.17ms`.

**A mask creates a stacking context, so `position: fixed` descendants of
the
scroller would be clipped.** Enumerated all 434 descendant elements on
the live
page: zero are `fixed` and zero are `sticky`. The scrollbar thumbs
(`.sbars`),
`#deskHost`, `.backpill` and `.dock` are all siblings of `.scroll`, not
descendants. This is the commit-authors' manual check; the branch adds
no
automated guard for it.

Commands and results:

- `npx vitest run scripts/gates/` -- 39 files passed, 170 tests passed,
no
unhandled errors. The new gate is collected by the runner: `npx vitest
list
  --filesOnly` prints `scripts/gates/transcript-tail-fade-css.test.mjs`.
- The new gate was proven not to be theatre by removing the `.scroll`
block and
re-running: 4 of its 6 assertions go red. The other 2 assert
pre-existing
invariants the pair depends on (`.col` must reserve the same `--lift`;
`.col`
  must not carry a mask of its own) and pass on both trees.
- `node ui-web/scripts/check-css.mjs` -- OK. `node
  ui-web/scripts/check-class-namespace.mjs` -- OK. `npx eslint
  scripts/gates/transcript-tail-fade-css.test.mjs` -- clean.
- `node ui-web/scripts/check-page.mjs` -- OK (1,387,731 bytes, 2 scripts
parse).
- `node ui-web/build.py` -- `boot-snapshot: OK (241 nodes match golden)`
twice.
- `python3 scripts/check_commit_messages.py origin/main..HEAD`,
  `check_source_language.py origin/main...HEAD`,
  `check_large_files.py origin/main...HEAD` -- all exit 0.
- Every added line is ASCII, and `git ls-files --error-unmatch` resolves
the one
path the new CSS comment cites
(`ui-web/src/features/composer/store.ts`).

Rebased onto `origin/main` after it gained #656, which touched
`page.css`. Its
one change there is a desk change-chip selector at line 2622 and touches
none of
`.scroll`, `.col`, `.dock` or `--lift`; both changes are intact at the
head and
all gates above were re-run after the rebase.

## Risk

- [x] Security impact considered -- none. A CSS mask on an existing
element; no
new input surface, no new file, no change to what is sent anywhere.
- [x] Backward compatibility considered -- the composer keeps its
position,
size, padding and hit area. The mask removes paint only; it does not
      change layout, so nothing reflows and nothing becomes unclickable.
- [x] Rollback path is clear -- revert the two commits. No data, config
or
persisted state is touched; `ui-web/dist` is a gitignored build
artifact.

User-visible change: a message scrolled up now dissolves over the last
40px
before the composer instead of being cut off at the card's edge. A
reader who
scrolls back to the tail gets it back, since the column's own `--lift`
padding
keeps the newest message clear of the ramp.

Not covered, stated so it is not mistaken for done: `ui-web` has no test
for
this beyond the stylesheet gate (happy-dom applies no stylesheet and
does no
layout, so the pixels are browser-only evidence), and the top of the
scroller
still cuts a message at the header, which this change does not address.

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
0xKT added a commit that referenced this pull request Sep 22, 2026
…e A and M (#657)

## Summary

Nothing in Raven recorded a deleted file. There is no delete tool, and a
file an `exec rm` unlinks is gone before anyone can look, so a turn that
created three files and removed two drew three additions on the desk's
diff tab, in the transcript's changed-files card and in a task's file
list, and nothing at all for the deletions. On the task side a
whole-file write onto nothing was also indistinguishable from a rewrite:
only `write | edit` reached the wire, and #656 had to guess a creation
from a zero deletion count (the one review remark on that PR). This is
the follow-up #656 named.

Backend (first commit):

- `ExecTool` notes, before a synchronous local command runs, the paths
its tokens name that resolve to regular files under the working
directory or an allowed directory (at most 64, text kept up to 256 KB),
and reports the ones that no longer exist afterwards as `removed` on the
tool result. Not for `machine=` or `run_in_background`, and a command
that removes nothing returns exactly what it returned before.
- `RemovalWatch` (new `raven/agent/tools/removals.py`) keeps what a turn
(main lane) or a run (`raven_loop`) wrote and re-stats those paths after
every later call, so a file removed under a name the shell watch could
not resolve is still seen. Bounded per path (512 KB) and across the
watch (4 MiB); past the bound the path is still reported, only its text
is dropped.
- `tool.complete` gains `file_removed: [{path, before?}]`, emitted only
when non-empty; the stored tool entry keeps `file_removed: [{path,
del}]` so a reload draws the deletion; `session.resume` passes it
through.
- The in-process lane records `op: add` for a write whose `before` is
`None` and `op: delete` for a vanished path; `merge_file_change` folds a
run's touches like a range of commits (add outlives later writes, delete
replaces a write or edit, add then delete leaves nothing, delete then a
write is a write).
- Wire: `FileRemoval`, `ToolCompletePayload.file_removed`, `TaskFile.op`
becomes `add | write | edit | delete`, `TranscriptMessage.file_removed`;
`rpc-schema/openrpc.json` mirrors it and both generated clients are
regenerated. The ACP translator is unchanged (see Risk).

Frontend (second commit):

- A removal rebuilds the turn's row for that path as kind `delete`:
every line it held as a deleted row (from `before` when the runtime
captured it, from the turn's own last write otherwise, from the stored
count alone on reload). A file the same turn created leaves no row, the
nothing git shows for it. Rows are matched by full-segment suffix,
because the model types a relative path and the runtime reports the
resolved one.
- Desk diff tab: git's own letters, `A` / `M` / `D` in git's colours
(added green, modified amber, deleted red), on both the session's rows
and a task's; a task row hides a zero count. The file-level letter and
the `+N -N` line counts beside it are two different things, and #656's
`+` for a created file read as more of the counts. Transcript
changed-files card: "Deleted". Task pane: a removed file's chip shows
only what it took out and opens a diff that ends on the node's last
write of the file taken back out. `taskChangeKind` now reads the op off
the wire; the deletion-count heuristic from #656 is gone.
- Offline fixtures: the GTM run removes a superseded note with `rm`,
exercising the live frame and the stored shape; the tasks fixture
carries an `add` and a `delete` file.

Contract tier (third commit): `FileRemoval` and the two `removed` fields
are a declared-surface change on `raven/contracts/tool.py`, so the
ledger admits the export, `CONTRACTS_VERSION` moves to 31 with the
surface digest repinned, and the papers' line ceiling moves 3,570 ->
3,620 with the reason recorded beside the earlier moves in
`tests/test_kernel_budget.py`.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

Run from the worktree on top of `da13bddd` (main with #656):

- `.venv/bin/ruff check raven tests`; `.venv/bin/ruff format --check
raven tests` -- clean
- `.venv/bin/python -m pytest tests/test_shell_file_removals.py
tests/test_removal_watch.py tests/test_subagent_activity.py
tests/test_subagent_manager.py tests/test_rpc_spine.py
tests/test_rpc_tasks.py tests/test_rpc_schema_match.py
tests/test_acp_updates.py tests/test_filesystem_tools.py
tests/test_agent_loop_session_stamps.py tests/test_rpc_session.py
tests/test_shell_workspace_fence.py tests/test_shell_approval.py
tests/test_turn_wire_shape_conformance.py tests/test_spine_events.py -q`
-- 1532 passed; `tests/test_contracts_two_tier_ledger.py
tests/test_kernel_budget.py` -- 14 passed with the contract-tier commit
- `make lint-python`; `make test-python` -- 25023 passed, 109 skipped
(the full Python suite, which is what caught the three contract-tier
guards the focused set missed)
- `npm test --prefix ui-web` -- 192 files, 2631 tests passed (whole
suite, gates included; the fixture-shape gate is satisfied by the
fixture sending `file_removed`, no new UNSENT pin)
- `npm run lint --prefix ui-web` (0 errors, 4 pre-existing warnings);
`npm run type-check --prefix ui-web`; `npm run gen:check --prefix
ui-web`
- `npm run lint --prefix ui-tui`; `npm run type-check --prefix ui-tui`;
`npm test --prefix ui-tui` -- 143 files, 2075 tests; `npm run lint:rpc
--prefix ui-tui`; `npm run lint:i18n --prefix ui-tui`
- Review round: `rm gone.txt;` (an operator adjacent to the path) now
records the removal -- the watch splits the command on shell operators
the way the fence does before lexing (7 parametrized cases); a
deletion's body follows the file through its edits the way `edit_file`
applies them -- every occurrence with `replace_all`, the one occurrence
otherwise, given up when the reader cannot know what the tool did -- on
the task side and the conversation side (one shared helper in
`lib/hunks.ts`; 7 new cases). Real gateway rerun with `rm
work/main_old.md;` draws the same red `D`.
- Mutation checks, each restored afterwards: ExecTool never reporting
removals, RemovalWatch returning only what the tool reported, add+delete
keeping the entry, raven_loop recording write instead of add, the outlet
dropping file_removed, record.ts never producing a delete row, the D
glyph reverting to M, taskChangeKind ignoring delete, the replay
ignoring stored file_removed, fromDelete numbering the wrong side --
every one turned at least one new test red.
- Real gateway (`raven serve` from this branch, isolated `RAVEN_HOME`,
default model `deepseek/deepseek-v4-flash`), two fresh conversations.
The main agent spawns the in-process `Raven` sub-agent, which writes two
files, edits one, runs `rm work/sub_old.md` on a pre-seeded file, then
writes and removes a scratch file: the desk diff tab shows `A` for the
two files and a red `D work/sub_old.md -3`, nothing for the scratch
file, and `tasks.list` carries `op` add / add / delete. The main agent
itself writes a new file, runs `rm work/main_old.md`, then writes and
removes a scratch file: the diff tab shows a red `D work/main_old.md
-2`, `A work/e2e_main_new.md` and, for an `edit_file` of a pre-seeded
file, `M work/main_keep.md`, nothing for the scratch file. Before this
change the same runs showed no trace of either `rm`.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

## Risk

Behaviour changes: every synchronous local `exec` now stats each command
token and reads up to 64 named files (256 KB each) before running, and
every tool call re-stats the paths the turn has written so far; a
`tool.complete` for a call that removed files carries `file_removed`,
and stored transcripts of such calls carry `file_removed: [{path,
del}]`. The wire additions are optional fields and a widened enum, so
older clients keep parsing. Known limits, unchanged here: the ACP
translator (`raven/acp/updates.py`) does not surface `file_removed`, so
an ACP client sees only the result preview for a call that deleted
files; the shell watch only sees paths the command names literally (a
glob, a directory, a computed path, or `mv a b` are not read as
deletions); two absolute spellings of one file (a symlinked root) are
not matched to the same row. Rollback: revert the three commits; nothing
stored is required by older code.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca pushed a commit that referenced this pull request Sep 23, 2026
…ed and removed (#670)

## Summary

Only the in-process sub-agent lane recorded a node's files, and only
from its own file tools. A Raven-PPT or Raven-Code run (ACP) wrote a
deck, a note and a build script and its node's file list stayed empty,
so the desk's diff tab said "No changes this turn" for the whole task;
and in every lane a file a shell command produced -- a build script's
PPTX, a rendered PDF -- was invisible, because a command's result names
no files. This is the gap left open by #656 and #657, which draw
whatever the lanes record.

No wire change: everything lands in the run's file entries and reaches
the page through `tasks.list.files` as it stands, so the desk's `A / M /
D` rows, the task pane's chips and the transcript card light up with no
frontend edit.

- `raven/agent/tools/snapshot.py` (new): a bounded listing of a working
directory, path -> (size, mtime_ns), skipping machinery directories
(`.git`, `node_modules`, `.venv`, ...) and giving up past 20000 entries
rather than describing part of a tree; `diff()` names what was created,
modified and deleted between two listings.
- `activity.record_snapshot_changes` turns such a diff into file entries
(created -> `add` with its line count, modified -> `write`, deleted ->
`delete`), skipping paths the call already accounted for and, for a lane
whose calls overlap, paths an earlier listing already reported created.
`count_line_changes` and `workspace_relative` move here from
`raven_loop` so every lane shares them.
- ACP lane: the collector records the diff blocks a tool call carries,
read the way the installed adapters send them. Per the spec a block
without `oldText` is a new file, and codex-acp and raven's own agent
send it that way; claude-agent-acp announces every Write without one, so
its dialect consults the call's opening listing instead (a path already
listed is a `write`, not an `add`). One path's blocks in one frame are
summed (claude-agent-acp emits one per hunk); codex-acp's deletion (old
content, empty `newText`) is a `delete`. A listing is taken at the
earliest frame that names a call -- the permission request when the
agent asks first, else the announce -- and compared when the call
settles, for the files the agent's own commands produced; a call that
reports failure keeps that listing (a command that wrote a file and then
exited non-zero left the file) and drops only its blocks; a per-turn
`RemovalWatch` reports files it recorded that later vanished. Listings
run off the connection's read loop, are skipped for `read` / `search` /
`think` / `fetch` calls and for a call first seen already over, and the
oldest open listing is evicted at the ceiling.
- In-process lane: a listing on either side of an `exec` call (off the
loop), beside the file-tool results it already recorded.
- CLI lane: one listing before the child starts and one after it exits.

Not in this PR: the main conversation's own `exec`-produced files (the
desk builds the main lane's rows from tool arguments on the client, so
those need a wire field); the ACP translator still surfaces no file
changes to an ACP client.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

Run from the worktree on top of `5186f672` (main with #657):

- `.venv/bin/ruff check raven tests`; `.venv/bin/ruff format --check
raven tests` -- clean
- `.venv/bin/python -m pytest tests/test_workdir_snapshot.py
tests/test_subagent_activity.py tests/test_subagent_acp.py
tests/test_subagent_manager.py tests/test_subagent_third_party.py
tests/test_removal_watch.py tests/test_shell_file_removals.py
tests/test_rpc_tasks.py tests/test_acp_dialects.py
tests/test_acp_backend_cwd.py tests/test_contracts_two_tier_ledger.py
tests/test_kernel_budget.py tests/test_acp_ask_user.py
tests/test_acp_elicitation.py tests/test_acp_journal.py -q` -- 943
passed
- `make lint-python`; `make test-python` -- 25108 passed, 109 skipped.
One timing test
(`tests/test_shell_machine_channel.py::test_the_cap_fires_on_the_machine_and_takes_the_whole_group`)
failed once under load while an E2E gateway ran beside the suite; it
passes 3/3 alone and the whole file passes, and this branch does not
touch the machine channel.
- Mutation checks, each restored afterwards: the skip list dropped, the
cap returning a partial listing, `count_line_changes` returning 0/0 for
a creation, the exec listing skipped in raven_loop, the collector
recording a failed call, `already` ignored, the CLI after-listing
dropped, the rewrite-vs-creation branch disabled, the delete-block
branch disabled, the overlapping-call guard dropped, a block group
truncated to its last hunk, eviction reverted to refusal -- every one
turned at least one new test red.
- The rule about a block without `oldText` is pinned per dialect: the
spec dialect records `add` whatever the listing held, the
claude-agent-acp dialect consults the listing, and a permission frame
that precedes the announce opens the listing early (three cases, each
red under the inverse mutation).
- Review round: a failed ACP call keeps what the listing found on disk
(the `partial.txt` reproduction now records `add`; a block the failed
call never made records nothing); the same-size rewrite test stamps its
own mtime after the write instead of relying on the clock advancing.
- Adapter claims checked against the installed copies: claude-agent-acp
0.79.0 (`dist/tools.js` Write announce, one block per hunk, PostToolUse
hook frame) and codex-acp 1.1.14 (`createDeleteFileContent`).
- Real gateway (`raven serve` from this branch, isolated `RAVEN_HOME`,
default model `deepseek/deepseek-v4-flash`): the main agent spawns the
ACP `Raven-Code` sub-agent, which creates `work/acp_written.md` with its
own file tool, runs `printf ... > work/acp_generated.txt`, and runs `rm
work/acp_old.md` on a pre-seeded file. Before this change `tasks.list`
reported `files: []` for the node (three tool calls, three changes on
disk) and the desk diff tab said "No changes this turn". After:
`tasks.list.files` is `add work/acp_written.md (+3)`, `add
work/acp_generated.txt (+2)`, `delete work/acp_old.md`, and the desk
diff tab draws `A`, `A` and a red `D` under "Task changes" (this agent
asks permission before announcing the call, which is what the
permission-frame listing is for).

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

## Risk

Behaviour changes: every sub-agent run now walks its working directory
around commands (in-process: around `exec`; ACP: around every call that
could write; CLI: once around the run), bounded at 20000 entries with
the machinery directories skipped, and off the event loop; a tree past
the bound records nothing from listings (the file-tool and diff-block
records stand). A created file's line count reads the file (up to 256
KB) inline. A whole-file claude-agent-acp Write over an existing file
reads as `M` (write), not `A`, which is the truthful reading and what
keeps a later deletion of that file visible; codex and raven blocks are
read as the spec says. Snapshot-discovered rewrites and deletions carry
no line counts (the desk hides zero counts and shows a bare `M` / `D`).
No stored or wire shape changes; rollback is reverting the four commits.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca pushed a commit that referenced this pull request Sep 23, 2026
…iff (#692)

## Summary

The desk builds the main conversation's changed-files rows on the
client, from the file tools' own arguments and results. A file a shell
command created (`python3 build.py > out.txt`), rewrote (`echo more >>
notes.md`) or removed through a glob left no row, because a command
returns its output and names no file. #670 closed that gap for every
sub-agent lane by listing the working directory on either side of a
command; this does the same for the main conversation, which needs a
wire field because its rows are drawn on the page rather than read from
a run record.

- `raven/agent/loop/turn_path.py`: around a call named `exec` and only
that, the directory the command runs in is listed before and after the
call with `snapshot.take`, off the event loop. The tool says where that
is (`ExecTool.listing_root`, resolved the way `execute` resolves its
cwd): the turn's working directory unless the call names a
`working_dir`, and no listing at all for a command sent to a registered
`machine`, whose files are not on this disk. The in-process sub-agent
lane asks the same question through `snapshot.root_for`, so a
sub-agent's `working_dir` command is recorded too. Created and modified
paths the call did not name itself (its `file_change`, its removals)
become the new `tool.complete.file_written` list of `{path, created,
size, lines}`; a deletion the listing finds joins `file_removed` with no
body, since the file was gone before anything read it. Stored on the
tool entry under the same name and passed through `session.resume`, so a
reloaded page draws what the live one drew.
- Wire: `FileWritten` in `raven/rpc/models.py`, on `ToolCompletePayload`
and `TranscriptMessage`; `rpc-schema/openrpc.json` and both generated
clients regenerated. `lines` is an integer for a created text file of
256 KB or less and `null` otherwise, including every rewrite, whose old
contents the listing never held.
- Page: a created file is an `A` row with its line count, a rewritten
one a bare `M`, neither with a patch; a row a file tool already made
this turn is left alone (its arguments say more than a listing can); the
reload path replays the stored field. A change row with no patch to show
opens the file itself instead of an empty patch pane (a removal keeps
its pane; a task file's row, keyed by node rather than path, is
untouched). The offline fixture's GTM run gains one such command so the
demo desk shows both rows and the fixture-shape gate sees the field
sent.

Not in this PR: the ACP translator still surfaces no file changes to an
ACP client, the same as for `file_removed`.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

Run from the worktree on top of `0133c67c` (main with #656, #657 and
#670; the branch was rebased onto it when main's own contract additions
collided with the generated client, which was regenerated from the
merged schema):

- `.venv/bin/ruff check raven tests`; `.venv/bin/ruff format --check
raven tests` -- clean
- `.venv/bin/python -m pytest tests/test_agent_loop_session_stamps.py
tests/test_rpc_spine.py tests/test_rpc_session.py
tests/test_rpc_schema_match.py tests/test_turn_wire_shape_conformance.py
tests/test_spine_events.py tests/test_contracts_two_tier_ledger.py
tests/test_kernel_budget.py tests/test_workdir_snapshot.py
tests/test_subagent_activity.py tests/test_shell_file_removals.py
tests/test_acp_updates.py -q` -- 952 passed
- `make test-python` (`uv run --frozen --all-extras pytest -q`) -- 25798
passed, 109 skipped
- `npm test --prefix ui-web` (the whole suite, including the
fixture-shape gate) -- 200 files, 2767 tests passed; `npm run lint
--prefix ui-web` -- 0 errors (4 pre-existing hook-dependency warnings in
files this change does not touch); `npm run type-check --prefix ui-web`,
`npm run gen:check --prefix ui-web`, `npm run lint:rpc --prefix ui-tui`
-- clean
- Mutation checks, each restored afterwards: the listing taken around
every tool; a created file reported as a rewrite; the held-out result
path dropped; the storage rename disabled; `file_written` emitted when
empty; the key dropped from `session.resume`; `file_written=None` on the
served `ToolEvent`; a count of 0 on a decode failure; the 256 KB cap
dropped; the created row drawn as `M`; the existing-row guard dropped;
the ninth argument dropped in the stage; the file door removed, ignoring
the kind, ignoring a task key; the fixture no longer sending the field
(stored, then live); the row takeover removed and its removal guard
dropped; `.raven` back in the walk; the count back on the loop; the
shell ignoring its bound directory -- every one turned at least one new
test red.
- Review round two (reviewer's blocker): a command with a `working_dir`
of its own is listed there, in both lanes, and a `machine` command takes
no listing -- `tests/test_shell_approval.py` (the root follows the
arguments), `tests/test_workdir_snapshot.py` (`root_for`),
`tests/test_agent_loop_session_stamps.py` (the real `ExecTool` with
`working_dir`; a remote command takes no listing) and
`tests/test_subagent_manager.py` (a sub-agent command outside its
workspace); each red with the root pinned back to the lane's directory
or the machine guard dropped.
- Review round: one row per file when a file tool follows a command that
already left the file behind (the listing keys by the resolved path, the
tool by the path the model typed); the listing skips `.raven`, where the
checkpoint keeps a shadow git repo inside the working directory; the
line counting moved into the worker beside the walks; a test through the
real `ExecTool` (its cwd is the directory the listing walks), the served
`ToolEvent` hop, a non-text and an over-cap created file, and the live
fixture frame.
- Real gateway (`raven serve` from this branch, isolated `RAVEN_HOME`,
default model `deepseek/deepseek-v4-flash`): the main agent runs `printf
"gen one\ngen two\n" > work/main_generated.txt` and `echo more >>
work/main_shell_mod.md` beside its file-tool steps. Before this change
neither command left a row. After: the desk diff tab draws `A
work/main_generated.txt +2` and `M work/main_shell_mod.md` beside `M
work/main_keep.md +1-1` (edit_file), a red `D work/main_old.md -2`
(`rm`) and `A work/e2e_main_new.md +2` (write_file); a file one command
created and the next removed draws nothing; reopening the page
(`session.resume`) draws the identical five rows, and the stored tool
entries carry `file_written` with `created: true, lines: 2` and
`created: false, lines: null`. The same run repeated on the final tree
(after the review round) and its reload draw the same rows.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

## Risk

Behaviour change: every main-conversation `exec` now walks the turn's
working directory twice, bounded at 20000 entries with the machinery
directories (`.git`, `node_modules`, `.venv`, caches) skipped and run
off the event loop; a tree past the bound records nothing from listings,
and the file tools' own rows stand as before. A created file's line
count reads the file (up to 256 KB). Wire: one new optional key on
`tool.complete` and on `session.resume` tool entries, absent on every
call that is not a command; a client that does not read it sees what it
saw before. Rollback is reverting the commits.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
LivXue pushed a commit that referenced this pull request Oct 12, 2026
…what was delivered (#656)

## Summary

A spawned sub-agent that wrote ten markdown files showed up on the desk
as ten "Task output" rows on the deliverables tab and a diff tab saying
"No changes this turn". Two things were wrong at once: the deliverables
tab listed files nobody delivered, and the diff tab hid every whole-file
write a task made (it only took `op === 'edit'` task files).

- The diff tab now lists every file a task's nodes wrote or edited,
under a "Task changes" heading; its badge and the tab the desk opens on
count them too, under the same id `openDeskDiff` marks read.
- The deliverables tab lists only what `deliver_files` handed over. A
sub-agent has no `deliver_files`; the main agent decides what is a
product, which is what the system prompt already tells it.
- A file the turn created draws `+` (kind `add`) instead of `M`: live
from `tool.complete.file_change` without `before`, on reload from the
stored diff's `@@ -0,0` header. A whole-file write over an existing file
stays `M`, and the transcript's "New" badge follows the same rule
instead of calling every whole-file write new. A created file rewritten
in the same turn stays a creation.
- Task-file glyphs: an `edit_file` is always a modification (the
prototype rule from #509 drew `+` for an edit that deleted nothing); a
whole-file write that replaced nothing is a creation.
- Labels: "N products" on a task row becomes "N files changed"; "Task
output" becomes "Task changes". The TUI i18n table is regenerated; the
desk tasks spec (section 7) says where task files go now.
- Second commit: `prettier --write` on two ui-tui files that arrived on
main unformatted. The pre-commit prettier hook checks the whole ui-tui
tree whenever a file under it changes (this PR regenerates the i18n
table), so it fails until those two are formatted.

Not in this PR: a file deleted by `exec rm` still leaves no row
anywhere. A follow-up branch (`feat/track_file_deletions`) adds deletion
tracking on the backend and the red `D` row; it is stacked on this one.

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

Run from the worktree after rebasing onto `a48c4edf`:

- `npm test --prefix ui-web` -- 192 files, 2599 tests passed (the whole
suite, gates included)
- `npm run lint --prefix ui-web` -- 0 errors (4 pre-existing warnings);
`npm run type-check --prefix ui-web`; `npm run gen:check --prefix
ui-web` -- generated.ts matches the contract
- `npm run lint --prefix ui-tui` -- 0 errors (23 pre-existing warnings);
`npm run type-check --prefix ui-tui`; `npm run lint:i18n --prefix
ui-tui` -- generated catalogue is up to date
- `uv run pre-commit run --from-ref origin/main --to-ref HEAD` -- passes
with the second commit (fails on `prettier (ui-tui)` without it)
- `make lint-python`; `make test-python` -- no Python file changes; both
pass
- Real gateway: `raven serve` from this branch with an isolated
`RAVEN_HOME` (channels off, default model `deepseek/deepseek-v4-flash`),
one conversation that spawns the in-process `Raven` sub-agent to
`write_file` two files and `edit_file` one, one conversation whose main
agent `write_file`s a new file. Before: diff tab "No changes this turn",
deliverables tab listing both task files under "Task output", the main
agent's new file drawn `M`. After: diff tab lists both task files with
`+` under "Task changes", deliverables tab "Nothing produced yet", the
main agent's new file drawn `+`. Same two conversations reopened after a
reload read the same.
- Mutation check on the new tests: dropping task ids from
`idsOf('diff')`, reverting `taskChangeKind` to the deletion-count rule,
dropping the `add` mark in `record.ts`, and widening the transcript's
"new" back to every write each turn at least one new test red.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

## Risk

UI only; the wire is untouched. Files a task wrote leave the
deliverables tab (they were never delivered) and appear on the diff tab,
so a reader who relied on the shelf to open a sub-agent's output now
finds it under diff, or in the task pane's file chips as before. Two
i18n keys are renamed (`gui.ws.task_output` -> `gui.ws.task_changes`,
`gui.tasks.artifacts_n/_1` -> `gui.tasks.files_n/_1`); no other consumer
of them exists. Rollback: revert the two commits.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
LivXue added a commit that referenced this pull request Oct 12, 2026
…der it (#658)

## Summary

The composer is an **opaque** card floating over the transcript
scroller, and
nothing ended the transcript's paint before it reached the card.
Scrolled up, a
line was cut mid-glyph at the card's top edge, and the 22px gutter below
the
card left the next line legible *underneath* the field the reader types
in.

This gives `.scroll` a mask whose alpha reaches zero at the top of
whatever is
docked, over a 40px ramp:

```css
.scroll {
  --tail-fade: 40px;
  mask-image: linear-gradient(180deg,
    #000 calc(100% - var(--lift, 96px) - var(--tail-fade)),
    transparent calc(100% - var(--lift, 96px)));
}
```

Three decisions worth stating:

- **A mask, not a ground-coloured band.** A band is a rectangle painted
*over*
the transcript, so it has to match whatever is behind it; on this page's
wash
(a gradient) it reads as a flat seam. A mask takes the alpha out of the
transcript itself, so there is no ground to match, and it needs no
second copy
per theme. `page.css` records that an earlier band above the composer
was
deleted for exactly that reason, which is why this does not reintroduce
one.
- **Anchored to `--lift`, which already exists.** `dockLift`
(`features/composer/store.ts`) publishes the distance from the chat
column's
bottom edge up to the top of the highest docked thing. `.col` spends it
as
bottom padding and `.backpill` as its offset. Spending the same number
here is
what makes the clear edge land on the composer's top edge however tall
it has
  grown (a longer field, queued rows, a clarify sheet), rather than on a
  hardcoded resting height.
- **The tail is never under the ramp.** Because `.col` reserves the same
`--lift`, the newest message sits above the ramp when the scroller is at
rest,
so the fade engages only once the reader scrolls back. Nothing is hidden
at
  the moment a message arrives.

The second commit corrects the `.dock` leading comment, which this
change
falsified: it claimed the transcript "scrolls underneath and stays
legible
through the glass" and that "the old fade band above the composer is
gone".

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

Verified in headless Chrome two ways: against `page.css` itself in a
probe page
carrying the real DOM structure, and against the running app over CDP.
Painted
ink was measured, not layout rects -- masked text still occupies its
box, so a
rect-based measurement is byte-identical before and after and cannot see
this
change at all. Ink is counted as pixels differing from the page's own
background
(the wash is not white, so the background is sampled from an empty
band).

**The clear edge lands on the composer's top edge** (`aligned` compares
the
`getBoundingClientRect().top` of `.dock-in` against the mask's
transparent stop):

| case | `--lift` | composer top | mask clears at | aligned |
|---|---|---|---|---|
| light theme | 106px | 654 | 654 | yes |
| dark theme | 106px | 654 | 654 | yes |
| composer grown to three lines | 180px | 580 | 580 | yes |
| landing page (`data-fresh`) | 106px | 654 | 654 | yes |
| live app, session "ESG..." | 139px | 721 | 721 | yes |

**Nothing is painted at or past the clear edge.** Ink in a 14px band
below it,
measured in the gutters either side of the card: `0` in all five cases.

**The fade is the only thing that changed.** Ink inside the 28px just
above the
card went `1651 -> 144` (91 percent removed), while the control band
100-600px
above it was `36133` before and `36133` after -- unchanged to the pixel.

**No scroll cost.** 240 scroll frames x 3 interleaved runs each, mask on
vs off:
median frame `16.70ms` both, p95 `16.80ms` both, worst `16.77 ->
17.17ms`.

**A mask creates a stacking context, so `position: fixed` descendants of
the
scroller would be clipped.** Enumerated all 434 descendant elements on
the live
page: zero are `fixed` and zero are `sticky`. The scrollbar thumbs
(`.sbars`),
`#deskHost`, `.backpill` and `.dock` are all siblings of `.scroll`, not
descendants. This is the commit-authors' manual check; the branch adds
no
automated guard for it.

Commands and results:

- `npx vitest run scripts/gates/` -- 39 files passed, 170 tests passed,
no
unhandled errors. The new gate is collected by the runner: `npx vitest
list
  --filesOnly` prints `scripts/gates/transcript-tail-fade-css.test.mjs`.
- The new gate was proven not to be theatre by removing the `.scroll`
block and
re-running: 4 of its 6 assertions go red. The other 2 assert
pre-existing
invariants the pair depends on (`.col` must reserve the same `--lift`;
`.col`
  must not carry a mask of its own) and pass on both trees.
- `node ui-web/scripts/check-css.mjs` -- OK. `node
  ui-web/scripts/check-class-namespace.mjs` -- OK. `npx eslint
  scripts/gates/transcript-tail-fade-css.test.mjs` -- clean.
- `node ui-web/scripts/check-page.mjs` -- OK (1,387,731 bytes, 2 scripts
parse).
- `node ui-web/build.py` -- `boot-snapshot: OK (241 nodes match golden)`
twice.
- `python3 scripts/check_commit_messages.py origin/main..HEAD`,
  `check_source_language.py origin/main...HEAD`,
  `check_large_files.py origin/main...HEAD` -- all exit 0.
- Every added line is ASCII, and `git ls-files --error-unmatch` resolves
the one
path the new CSS comment cites
(`ui-web/src/features/composer/store.ts`).

Rebased onto `origin/main` after it gained #656, which touched
`page.css`. Its
one change there is a desk change-chip selector at line 2622 and touches
none of
`.scroll`, `.col`, `.dock` or `--lift`; both changes are intact at the
head and
all gates above were re-run after the rebase.

## Risk

- [x] Security impact considered -- none. A CSS mask on an existing
element; no
new input surface, no new file, no change to what is sent anywhere.
- [x] Backward compatibility considered -- the composer keeps its
position,
size, padding and hit area. The mask removes paint only; it does not
      change layout, so nothing reflows and nothing becomes unclickable.
- [x] Rollback path is clear -- revert the two commits. No data, config
or
persisted state is touched; `ui-web/dist` is a gitignored build
artifact.

User-visible change: a message scrolled up now dissolves over the last
40px
before the composer instead of being cut off at the card's edge. A
reader who
scrolls back to the tail gets it back, since the column's own `--lift`
padding
keeps the newest message clear of the ramp.

Not covered, stated so it is not mistaken for done: `ui-web` has no test
for
this beyond the stylesheet gate (happy-dom applies no stylesheet and
does no
layout, so the pixels are browser-only evidence), and the top of the
scroller
still cuts a message at the header, which this change does not address.

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
LivXue pushed a commit that referenced this pull request Oct 12, 2026
…e A and M (#657)

## Summary

Nothing in Raven recorded a deleted file. There is no delete tool, and a
file an `exec rm` unlinks is gone before anyone can look, so a turn that
created three files and removed two drew three additions on the desk's
diff tab, in the transcript's changed-files card and in a task's file
list, and nothing at all for the deletions. On the task side a
whole-file write onto nothing was also indistinguishable from a rewrite:
only `write | edit` reached the wire, and #656 had to guess a creation
from a zero deletion count (the one review remark on that PR). This is
the follow-up #656 named.

Backend (first commit):

- `ExecTool` notes, before a synchronous local command runs, the paths
its tokens name that resolve to regular files under the working
directory or an allowed directory (at most 64, text kept up to 256 KB),
and reports the ones that no longer exist afterwards as `removed` on the
tool result. Not for `machine=` or `run_in_background`, and a command
that removes nothing returns exactly what it returned before.
- `RemovalWatch` (new `raven/agent/tools/removals.py`) keeps what a turn
(main lane) or a run (`raven_loop`) wrote and re-stats those paths after
every later call, so a file removed under a name the shell watch could
not resolve is still seen. Bounded per path (512 KB) and across the
watch (4 MiB); past the bound the path is still reported, only its text
is dropped.
- `tool.complete` gains `file_removed: [{path, before?}]`, emitted only
when non-empty; the stored tool entry keeps `file_removed: [{path,
del}]` so a reload draws the deletion; `session.resume` passes it
through.
- The in-process lane records `op: add` for a write whose `before` is
`None` and `op: delete` for a vanished path; `merge_file_change` folds a
run's touches like a range of commits (add outlives later writes, delete
replaces a write or edit, add then delete leaves nothing, delete then a
write is a write).
- Wire: `FileRemoval`, `ToolCompletePayload.file_removed`, `TaskFile.op`
becomes `add | write | edit | delete`, `TranscriptMessage.file_removed`;
`rpc-schema/openrpc.json` mirrors it and both generated clients are
regenerated. The ACP translator is unchanged (see Risk).

Frontend (second commit):

- A removal rebuilds the turn's row for that path as kind `delete`:
every line it held as a deleted row (from `before` when the runtime
captured it, from the turn's own last write otherwise, from the stored
count alone on reload). A file the same turn created leaves no row, the
nothing git shows for it. Rows are matched by full-segment suffix,
because the model types a relative path and the runtime reports the
resolved one.
- Desk diff tab: git's own letters, `A` / `M` / `D` in git's colours
(added green, modified amber, deleted red), on both the session's rows
and a task's; a task row hides a zero count. The file-level letter and
the `+N -N` line counts beside it are two different things, and #656's
`+` for a created file read as more of the counts. Transcript
changed-files card: "Deleted". Task pane: a removed file's chip shows
only what it took out and opens a diff that ends on the node's last
write of the file taken back out. `taskChangeKind` now reads the op off
the wire; the deletion-count heuristic from #656 is gone.
- Offline fixtures: the GTM run removes a superseded note with `rm`,
exercising the live frame and the stored shape; the tasks fixture
carries an `add` and a `delete` file.

Contract tier (third commit): `FileRemoval` and the two `removed` fields
are a declared-surface change on `raven/contracts/tool.py`, so the
ledger admits the export, `CONTRACTS_VERSION` moves to 31 with the
surface digest repinned, and the papers' line ceiling moves 3,570 ->
3,620 with the reason recorded beside the earlier moves in
`tests/test_kernel_budget.py`.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

Run from the worktree on top of `da13bddd` (main with #656):

- `.venv/bin/ruff check raven tests`; `.venv/bin/ruff format --check
raven tests` -- clean
- `.venv/bin/python -m pytest tests/test_shell_file_removals.py
tests/test_removal_watch.py tests/test_subagent_activity.py
tests/test_subagent_manager.py tests/test_rpc_spine.py
tests/test_rpc_tasks.py tests/test_rpc_schema_match.py
tests/test_acp_updates.py tests/test_filesystem_tools.py
tests/test_agent_loop_session_stamps.py tests/test_rpc_session.py
tests/test_shell_workspace_fence.py tests/test_shell_approval.py
tests/test_turn_wire_shape_conformance.py tests/test_spine_events.py -q`
-- 1532 passed; `tests/test_contracts_two_tier_ledger.py
tests/test_kernel_budget.py` -- 14 passed with the contract-tier commit
- `make lint-python`; `make test-python` -- 25023 passed, 109 skipped
(the full Python suite, which is what caught the three contract-tier
guards the focused set missed)
- `npm test --prefix ui-web` -- 192 files, 2631 tests passed (whole
suite, gates included; the fixture-shape gate is satisfied by the
fixture sending `file_removed`, no new UNSENT pin)
- `npm run lint --prefix ui-web` (0 errors, 4 pre-existing warnings);
`npm run type-check --prefix ui-web`; `npm run gen:check --prefix
ui-web`
- `npm run lint --prefix ui-tui`; `npm run type-check --prefix ui-tui`;
`npm test --prefix ui-tui` -- 143 files, 2075 tests; `npm run lint:rpc
--prefix ui-tui`; `npm run lint:i18n --prefix ui-tui`
- Review round: `rm gone.txt;` (an operator adjacent to the path) now
records the removal -- the watch splits the command on shell operators
the way the fence does before lexing (7 parametrized cases); a
deletion's body follows the file through its edits the way `edit_file`
applies them -- every occurrence with `replace_all`, the one occurrence
otherwise, given up when the reader cannot know what the tool did -- on
the task side and the conversation side (one shared helper in
`lib/hunks.ts`; 7 new cases). Real gateway rerun with `rm
work/main_old.md;` draws the same red `D`.
- Mutation checks, each restored afterwards: ExecTool never reporting
removals, RemovalWatch returning only what the tool reported, add+delete
keeping the entry, raven_loop recording write instead of add, the outlet
dropping file_removed, record.ts never producing a delete row, the D
glyph reverting to M, taskChangeKind ignoring delete, the replay
ignoring stored file_removed, fromDelete numbering the wrong side --
every one turned at least one new test red.
- Real gateway (`raven serve` from this branch, isolated `RAVEN_HOME`,
default model `deepseek/deepseek-v4-flash`), two fresh conversations.
The main agent spawns the in-process `Raven` sub-agent, which writes two
files, edits one, runs `rm work/sub_old.md` on a pre-seeded file, then
writes and removes a scratch file: the desk diff tab shows `A` for the
two files and a red `D work/sub_old.md -3`, nothing for the scratch
file, and `tasks.list` carries `op` add / add / delete. The main agent
itself writes a new file, runs `rm work/main_old.md`, then writes and
removes a scratch file: the diff tab shows a red `D work/main_old.md
-2`, `A work/e2e_main_new.md` and, for an `edit_file` of a pre-seeded
file, `M work/main_keep.md`, nothing for the scratch file. Before this
change the same runs showed no trace of either `rm`.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

## Risk

Behaviour changes: every synchronous local `exec` now stats each command
token and reads up to 64 named files (256 KB each) before running, and
every tool call re-stats the paths the turn has written so far; a
`tool.complete` for a call that removed files carries `file_removed`,
and stored transcripts of such calls carry `file_removed: [{path,
del}]`. The wire additions are optional fields and a widened enum, so
older clients keep parsing. Known limits, unchanged here: the ACP
translator (`raven/acp/updates.py`) does not surface `file_removed`, so
an ACP client sees only the result preview for a call that deleted
files; the shell watch only sees paths the command names literally (a
glob, a directory, a computed path, or `mv a b` are not read as
deletions); two absolute spellings of one file (a symlinked root) are
not matched to the same row. Rollback: revert the three commits; nothing
stored is required by older code.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
LivXue pushed a commit that referenced this pull request Oct 12, 2026
…ed and removed (#670)

## Summary

Only the in-process sub-agent lane recorded a node's files, and only
from its own file tools. A Raven-PPT or Raven-Code run (ACP) wrote a
deck, a note and a build script and its node's file list stayed empty,
so the desk's diff tab said "No changes this turn" for the whole task;
and in every lane a file a shell command produced -- a build script's
PPTX, a rendered PDF -- was invisible, because a command's result names
no files. This is the gap left open by #656 and #657, which draw
whatever the lanes record.

No wire change: everything lands in the run's file entries and reaches
the page through `tasks.list.files` as it stands, so the desk's `A / M /
D` rows, the task pane's chips and the transcript card light up with no
frontend edit.

- `raven/agent/tools/snapshot.py` (new): a bounded listing of a working
directory, path -> (size, mtime_ns), skipping machinery directories
(`.git`, `node_modules`, `.venv`, ...) and giving up past 20000 entries
rather than describing part of a tree; `diff()` names what was created,
modified and deleted between two listings.
- `activity.record_snapshot_changes` turns such a diff into file entries
(created -> `add` with its line count, modified -> `write`, deleted ->
`delete`), skipping paths the call already accounted for and, for a lane
whose calls overlap, paths an earlier listing already reported created.
`count_line_changes` and `workspace_relative` move here from
`raven_loop` so every lane shares them.
- ACP lane: the collector records the diff blocks a tool call carries,
read the way the installed adapters send them. Per the spec a block
without `oldText` is a new file, and codex-acp and raven's own agent
send it that way; claude-agent-acp announces every Write without one, so
its dialect consults the call's opening listing instead (a path already
listed is a `write`, not an `add`). One path's blocks in one frame are
summed (claude-agent-acp emits one per hunk); codex-acp's deletion (old
content, empty `newText`) is a `delete`. A listing is taken at the
earliest frame that names a call -- the permission request when the
agent asks first, else the announce -- and compared when the call
settles, for the files the agent's own commands produced; a call that
reports failure keeps that listing (a command that wrote a file and then
exited non-zero left the file) and drops only its blocks; a per-turn
`RemovalWatch` reports files it recorded that later vanished. Listings
run off the connection's read loop, are skipped for `read` / `search` /
`think` / `fetch` calls and for a call first seen already over, and the
oldest open listing is evicted at the ceiling.
- In-process lane: a listing on either side of an `exec` call (off the
loop), beside the file-tool results it already recorded.
- CLI lane: one listing before the child starts and one after it exits.

Not in this PR: the main conversation's own `exec`-produced files (the
desk builds the main lane's rows from tool arguments on the client, so
those need a wire field); the ACP translator still surfaces no file
changes to an ACP client.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

Run from the worktree on top of `5186f672` (main with #657):

- `.venv/bin/ruff check raven tests`; `.venv/bin/ruff format --check
raven tests` -- clean
- `.venv/bin/python -m pytest tests/test_workdir_snapshot.py
tests/test_subagent_activity.py tests/test_subagent_acp.py
tests/test_subagent_manager.py tests/test_subagent_third_party.py
tests/test_removal_watch.py tests/test_shell_file_removals.py
tests/test_rpc_tasks.py tests/test_acp_dialects.py
tests/test_acp_backend_cwd.py tests/test_contracts_two_tier_ledger.py
tests/test_kernel_budget.py tests/test_acp_ask_user.py
tests/test_acp_elicitation.py tests/test_acp_journal.py -q` -- 943
passed
- `make lint-python`; `make test-python` -- 25108 passed, 109 skipped.
One timing test
(`tests/test_shell_machine_channel.py::test_the_cap_fires_on_the_machine_and_takes_the_whole_group`)
failed once under load while an E2E gateway ran beside the suite; it
passes 3/3 alone and the whole file passes, and this branch does not
touch the machine channel.
- Mutation checks, each restored afterwards: the skip list dropped, the
cap returning a partial listing, `count_line_changes` returning 0/0 for
a creation, the exec listing skipped in raven_loop, the collector
recording a failed call, `already` ignored, the CLI after-listing
dropped, the rewrite-vs-creation branch disabled, the delete-block
branch disabled, the overlapping-call guard dropped, a block group
truncated to its last hunk, eviction reverted to refusal -- every one
turned at least one new test red.
- The rule about a block without `oldText` is pinned per dialect: the
spec dialect records `add` whatever the listing held, the
claude-agent-acp dialect consults the listing, and a permission frame
that precedes the announce opens the listing early (three cases, each
red under the inverse mutation).
- Review round: a failed ACP call keeps what the listing found on disk
(the `partial.txt` reproduction now records `add`; a block the failed
call never made records nothing); the same-size rewrite test stamps its
own mtime after the write instead of relying on the clock advancing.
- Adapter claims checked against the installed copies: claude-agent-acp
0.79.0 (`dist/tools.js` Write announce, one block per hunk, PostToolUse
hook frame) and codex-acp 1.1.14 (`createDeleteFileContent`).
- Real gateway (`raven serve` from this branch, isolated `RAVEN_HOME`,
default model `deepseek/deepseek-v4-flash`): the main agent spawns the
ACP `Raven-Code` sub-agent, which creates `work/acp_written.md` with its
own file tool, runs `printf ... > work/acp_generated.txt`, and runs `rm
work/acp_old.md` on a pre-seeded file. Before this change `tasks.list`
reported `files: []` for the node (three tool calls, three changes on
disk) and the desk diff tab said "No changes this turn". After:
`tasks.list.files` is `add work/acp_written.md (+3)`, `add
work/acp_generated.txt (+2)`, `delete work/acp_old.md`, and the desk
diff tab draws `A`, `A` and a red `D` under "Task changes" (this agent
asks permission before announcing the call, which is what the
permission-frame listing is for).

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

## Risk

Behaviour changes: every sub-agent run now walks its working directory
around commands (in-process: around `exec`; ACP: around every call that
could write; CLI: once around the run), bounded at 20000 entries with
the machinery directories skipped, and off the event loop; a tree past
the bound records nothing from listings (the file-tool and diff-block
records stand). A created file's line count reads the file (up to 256
KB) inline. A whole-file claude-agent-acp Write over an existing file
reads as `M` (write), not `A`, which is the truthful reading and what
keeps a later deletion of that file visible; codex and raven blocks are
read as the spec says. Snapshot-discovered rewrites and deletions carry
no line counts (the desk hides zero counts and shows a bare `M` / `D`).
No stored or wire shape changes; rollback is reverting the four commits.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
LivXue pushed a commit that referenced this pull request Oct 12, 2026
…iff (#692)

## Summary

The desk builds the main conversation's changed-files rows on the
client, from the file tools' own arguments and results. A file a shell
command created (`python3 build.py > out.txt`), rewrote (`echo more >>
notes.md`) or removed through a glob left no row, because a command
returns its output and names no file. #670 closed that gap for every
sub-agent lane by listing the working directory on either side of a
command; this does the same for the main conversation, which needs a
wire field because its rows are drawn on the page rather than read from
a run record.

- `raven/agent/loop/turn_path.py`: around a call named `exec` and only
that, the directory the command runs in is listed before and after the
call with `snapshot.take`, off the event loop. The tool says where that
is (`ExecTool.listing_root`, resolved the way `execute` resolves its
cwd): the turn's working directory unless the call names a
`working_dir`, and no listing at all for a command sent to a registered
`machine`, whose files are not on this disk. The in-process sub-agent
lane asks the same question through `snapshot.root_for`, so a
sub-agent's `working_dir` command is recorded too. Created and modified
paths the call did not name itself (its `file_change`, its removals)
become the new `tool.complete.file_written` list of `{path, created,
size, lines}`; a deletion the listing finds joins `file_removed` with no
body, since the file was gone before anything read it. Stored on the
tool entry under the same name and passed through `session.resume`, so a
reloaded page draws what the live one drew.
- Wire: `FileWritten` in `raven/rpc/models.py`, on `ToolCompletePayload`
and `TranscriptMessage`; `rpc-schema/openrpc.json` and both generated
clients regenerated. `lines` is an integer for a created text file of
256 KB or less and `null` otherwise, including every rewrite, whose old
contents the listing never held.
- Page: a created file is an `A` row with its line count, a rewritten
one a bare `M`, neither with a patch; a row a file tool already made
this turn is left alone (its arguments say more than a listing can); the
reload path replays the stored field. A change row with no patch to show
opens the file itself instead of an empty patch pane (a removal keeps
its pane; a task file's row, keyed by node rather than path, is
untouched). The offline fixture's GTM run gains one such command so the
demo desk shows both rows and the fixture-shape gate sees the field
sent.

Not in this PR: the ACP translator still surfaces no file changes to an
ACP client, the same as for `file_removed`.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

Run from the worktree on top of `0133c67c` (main with #656, #657 and
#670; the branch was rebased onto it when main's own contract additions
collided with the generated client, which was regenerated from the
merged schema):

- `.venv/bin/ruff check raven tests`; `.venv/bin/ruff format --check
raven tests` -- clean
- `.venv/bin/python -m pytest tests/test_agent_loop_session_stamps.py
tests/test_rpc_spine.py tests/test_rpc_session.py
tests/test_rpc_schema_match.py tests/test_turn_wire_shape_conformance.py
tests/test_spine_events.py tests/test_contracts_two_tier_ledger.py
tests/test_kernel_budget.py tests/test_workdir_snapshot.py
tests/test_subagent_activity.py tests/test_shell_file_removals.py
tests/test_acp_updates.py -q` -- 952 passed
- `make test-python` (`uv run --frozen --all-extras pytest -q`) -- 25798
passed, 109 skipped
- `npm test --prefix ui-web` (the whole suite, including the
fixture-shape gate) -- 200 files, 2767 tests passed; `npm run lint
--prefix ui-web` -- 0 errors (4 pre-existing hook-dependency warnings in
files this change does not touch); `npm run type-check --prefix ui-web`,
`npm run gen:check --prefix ui-web`, `npm run lint:rpc --prefix ui-tui`
-- clean
- Mutation checks, each restored afterwards: the listing taken around
every tool; a created file reported as a rewrite; the held-out result
path dropped; the storage rename disabled; `file_written` emitted when
empty; the key dropped from `session.resume`; `file_written=None` on the
served `ToolEvent`; a count of 0 on a decode failure; the 256 KB cap
dropped; the created row drawn as `M`; the existing-row guard dropped;
the ninth argument dropped in the stage; the file door removed, ignoring
the kind, ignoring a task key; the fixture no longer sending the field
(stored, then live); the row takeover removed and its removal guard
dropped; `.raven` back in the walk; the count back on the loop; the
shell ignoring its bound directory -- every one turned at least one new
test red.
- Review round two (reviewer's blocker): a command with a `working_dir`
of its own is listed there, in both lanes, and a `machine` command takes
no listing -- `tests/test_shell_approval.py` (the root follows the
arguments), `tests/test_workdir_snapshot.py` (`root_for`),
`tests/test_agent_loop_session_stamps.py` (the real `ExecTool` with
`working_dir`; a remote command takes no listing) and
`tests/test_subagent_manager.py` (a sub-agent command outside its
workspace); each red with the root pinned back to the lane's directory
or the machine guard dropped.
- Review round: one row per file when a file tool follows a command that
already left the file behind (the listing keys by the resolved path, the
tool by the path the model typed); the listing skips `.raven`, where the
checkpoint keeps a shadow git repo inside the working directory; the
line counting moved into the worker beside the walks; a test through the
real `ExecTool` (its cwd is the directory the listing walks), the served
`ToolEvent` hop, a non-text and an over-cap created file, and the live
fixture frame.
- Real gateway (`raven serve` from this branch, isolated `RAVEN_HOME`,
default model `deepseek/deepseek-v4-flash`): the main agent runs `printf
"gen one\ngen two\n" > work/main_generated.txt` and `echo more >>
work/main_shell_mod.md` beside its file-tool steps. Before this change
neither command left a row. After: the desk diff tab draws `A
work/main_generated.txt +2` and `M work/main_shell_mod.md` beside `M
work/main_keep.md +1-1` (edit_file), a red `D work/main_old.md -2`
(`rm`) and `A work/e2e_main_new.md +2` (write_file); a file one command
created and the next removed draws nothing; reopening the page
(`session.resume`) draws the identical five rows, and the stored tool
entries carry `file_written` with `created: true, lines: 2` and
`created: false, lines: null`. The same run repeated on the final tree
(after the review round) and its reload draw the same rows.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

## Risk

Behaviour change: every main-conversation `exec` now walks the turn's
working directory twice, bounded at 20000 entries with the machinery
directories (`.git`, `node_modules`, `.venv`, caches) skipped and run
off the event loop; a tree past the bound records nothing from listings,
and the file tools' own rows stand as before. A created file's line
count reads the file (up to 256 KB). Wire: one new optional key on
`tool.complete` and on `session.resume` tool entries, absent on every
call that is not a command; a client that does not read it sees what it
saw before. Rollback is reverting the commits.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
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.

2 participants