Skip to content

fix(ui-web): attach a picture dragged into the composer by its own path - #900

Open
LivXue wants to merge 6 commits into
mainfrom
fix/composer_drop_transcript_image
Open

LivXue wants to merge 6 commits into
mainfrom
fix/composer_drop_transcript_image

Conversation

@LivXue

@LivXue LivXue commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

Summary

Dragging a picture from the transcript into the composer did not send the picture. Chromium and Firefox on Linux hand a drag that stays inside the document over with no File at all, so the drop did nothing (measured). Chrome on macOS hands it over as a File named download: its drop code names the file from the image's Content-Disposition with no URL to fall back on, and the format survives only in File.type. The composer uploaded that File as uploads/download, and everything that tells a picture by its extension (the sent bubble, the /file route, the reloaded history) drew a file chip named download where the picture should have been.

A drop now attaches the file a dragged picture was drawn from, where it already is, by path: nothing is read and nothing goes up. The composer takes the file from text/uri-list when the list names this origin's own /file route, and prefers that address over any File the drop also carries, so Chrome on macOS no longer uploads a second copy. A sent picture that is still drawn from the upload's cached data URL puts its /file address in the list when it is dragged; a picture already drawn from /file carries it already.

An address is taken at its word only when the drag that began in this document carried the same path from its start. Any site the reader drags from can put one of these addresses in the list, so a drop that did not begin here, or names a path its drag did not carry, is not attached by address. Paths are compared rather than the address as written, since the platform's drag plumbing may respell the address on the way through.

A File that does go up is now named by its type when its name spells no picture extension (download becomes download.png), which covers images dragged in from other sites. The tray chip of a file attached by path has no size in its tooltip, since its bytes were never read.

turn.send now resolves an attachment the way the viewer does. It asks for the conversation's own working directory the way /file does, finds a relative path by the viewer's own rule (that directory first, agent home for an upload), and with tools.restrict_to_workspace on admits that directory beside agent home. Before, it fenced every attachment to agent home, so a picture attached by path from a session pinned to a folder outside agent home showed in the tray and was dropped from the turn. A path neither directory holds is still refused.

Not covered, by design or by reach:

  • Chrome on Windows and on macOS were not run; neither machine was available. The change relies only on text/uri-list. Where a platform's list matches no path the drag carried, the drop falls back to the upload path, which now names the File by its type.
  • A drag from another Raven tab is not a drag that began in this document: it falls back to the upload path (Chrome on macOS) or does nothing.
  • Anything dragged out of this document whose list names a /file address is attached by path, not only a picture, for example a link to a file.
  • The sub-agent direct chat has no drop target, so neither part applies there.

Of the three rpc commits, one corrects _resolve_media's docstring, which said the front end sends upload paths alone; one is the resolution above, added in review; and one moves viewer_root's rule and the session-root lookup into raven/rpc/files.py, because turn.send importing them from the console closed an import cycle (console, session, turn) that tests/test_import_cycle_budget.py caught in CI. The console keeps its names, so its callers are unchanged.

Type

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

Verification

  • node node_modules/vitest/vitest.mjs run --no-file-parallelism in ui-web/, on the branch rebased onto current main: 214 files (168 under src/, 46 under scripts/gates/), 3242 tests passed, 0 failed, 0 unhandled errors. A parallel run on this shared box times out a varying handful of state/session/* and source tests that this PR does not touch; main's own parallel run timed out two of the same tests, and run serially every file passes.

  • node node_modules/typescript/bin/tsc --noEmit -p tsconfig.json in ui-web/: exit 0.

  • make check-commits check-large-files check-source-language lint-python lint-imports lint-ui: exit 0. Import contracts 10 kept, gen:check matches 210 methods, eslint 0 errors (its 4 warnings are in files this PR does not touch).

  • uv run --frozen --all-extras pytest -q (the whole suite, at this head): 27859 passed, 120 skipped, 7 failed, all seven environment failures of this box: five proxy tests in tests/test_config_update_providers.py (the file passes 193/193 with the box proxy variables unset), tests/test_install_script.py::test_resolve_node_dir_answers_each_case_it_exists_for (an npm elsewhere on PATH) and tests/test_subagent_node_runtime.py::test_what_cannot_be_read_names_nothing (the suite runs as root). tests/test_import_cycle_budget.py passes.

  • Each new test failed before its change: the drop test saw the download File uploaded once, addPaths did not exist, and a dragged sent picture carried no address. All pass after.

  • Three of the five new turn.send tests failed before the review fix: a picture in the pinned session's folder was dropped, and a relative path was looked for under agent home only. The other two pin what must not change: a path neither directory holds is refused, and a loop that cannot name the directory leaves agent home to stand in.

  • 6 single-construct mutants of that fix each turn a new test red: a fence on agent home only, no fence, relative paths against agent home, viewer_root reading agent home from its own config, the inject path ignoring the directory, and an unguarded directory lookup.

  • 11 single-construct mutants of the composer change each turn at least one new test red: the origin check, the /file route check, the carried-path check, path versus raw-address matching, clearing the drag on drop, clearing it on dragend, the address winning over a File, a chip picture only for a picture name, the null size, both path separators, and svg in the picture table.

  • Real browsers, against an isolated raven web on this branch with model openrouter/z-ai/glm-5.3-flash and real Playwright 1.62 drags, in Chromium 151 and Firefox 153: a sent picture drawn from its data URL, then the same picture after a reload drawn from /file, each dragged into the composer and sent. Neither drag sent an fs.upload; turn.send carried media: ["uploads/shot.png"], then the absolute path; the model answered "Red" for the red test image both times; the uploads directory held only the file the first turn picked.

  • With tools.restrict_to_workspace on and a conversation pinned to a folder outside agent home, in an isolated raven web (Chromium, a real drag of a picture /file serves from that folder, same model): on the previous head turn.send logged rejected: ... outside allowed directories for the picture and the stored user message carried none, so the model answered only after its own read_file call; on this head nothing was rejected, the stored message carried [Image: plot.png ...], and the model answered with no tool call.

  • The download naming, by dropping the File Chrome on macOS builds (name download, type image/png) on the field in Chromium: before, the sent bubble drew a download file chip; after, a download.png picture.

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

No user-facing doc describes dragging a picture into the composer, so there is nothing to update.

Risk

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

Dragging a transcript picture into the composer now attaches the original file: the message lists its own path (uploads/... or an absolute path), and the uploads directory no longer gains a copy per drag. Only a drag that began in this document is attached by address; the tests cover a foreign drop, another origin, another route and a path the drag did not carry. With tools.restrict_to_workspace on, turn.send now also admits the conversation's own working directory: /file already serves it, and that conversation's file tools may already read it. A path outside both directories is still refused. Rollback is reverting the squash commit; no data or config migrates.

Related Issues

N/A

@LivXue
LivXue requested review from 0xKT and gloryfromca October 10, 2026 09:03

@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.

Blocking: preserve the session workdir when resolving path-backed attachments.

I reviewed the full github/main...HEAD diff, the composer/transcript callers, /file and turn.send path policy, relevant history, backward compatibility, tests, and the Web UI/runtime architecture and naming rules in AGENTS.md, CONTEXT-MAP.md, and ui-web/CONTEXT.md. I found one concrete blocker, marked inline. I did not find weakened tests or additional merge-blocking issues.

Verification:

  • Focused Vitest suite: 254 passed across Dock.test.tsx, ComposerPage.test.tsx, and TranscriptPage.test.tsx.
  • npm run type-check: passed.
  • npm run lint: passed with four pre-existing warnings in untouched files.
  • uv run pytest tests/test_rpc_turn_send.py -x: could not run because global test setup cannot import the optional raven_everos package (4 setup errors before test bodies).
  • Direct resolver reproduction: an absolute picture under a session project outside agent home raises PermissionError when the agent-home fence is enabled.

Comment thread ui-web/src/features/composer/store.ts
@LivXue
LivXue force-pushed the fix/composer_drop_transcript_image branch from 9d3a577 to d974e40 Compare October 10, 2026 12:46

@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; this can merge as far as I am concerned.

I reviewed the rebased github/main...HEAD diff and the new session-aware attachment fix, including the /file and turn.send callers, shared viewer_root behavior, normal and injected sends, path-policy compatibility, relevant history, and the Web UI/runtime constraints in AGENTS.md, CONTEXT-MAP.md, and ui-web/CONTEXT.md. The previous blocker is fixed and its thread is resolved. I found no weakened tests or new issue worth raising.

Verification:

  • uv run pytest tests/test_rpc_turn_send.py -x: 62 passed.
  • uv run pytest tests/test_rpc_console.py -k viewer_root -x: 3 passed.
  • Focused Vitest suite for Dock, ComposerPage, and TranscriptPage: 254 passed.
  • npm run type-check: passed.
  • Focused Ruff check: passed.
  • git diff --check github/main...HEAD: passed.

@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; this can merge as far as I am concerned.

I reviewed the delta from the previously clean revision and rechecked the full github/main...HEAD change. The shared session/viewer path rules now live in raven.rpc.files, removing the new console/turn import cycle while preserving the console-facing names, /file behavior, restricted attachment resolution, and normal/injected send paths. I also checked relevant callers and history, backward compatibility, test integrity, and the architecture and repository constraints in AGENTS.md, CONTEXT-MAP.md, and ui-web/CONTEXT.md. I found no new issue worth raising, and my prior thread remains resolved.

Verification:

  • uv run pytest tests/test_import_cycle_budget.py -x: 4 passed.
  • uv run pytest tests/test_rpc_turn_send.py -x: 62 passed.
  • uv run pytest tests/test_rpc_console.py -k 'viewer_root or file' -x: 16 passed.
  • Focused Ruff check: passed.
  • git diff --check github/main...HEAD: passed.

Comment thread raven/rpc/methods/turn.py
Comment thread ui-web/src/features/composer/mount.tsx
Comment thread ui-web/src/features/composer/store.ts

@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.

Blocking: a fresh upload can resolve to an older same-named session file.

The new review record invalidates my earlier clean stance. I independently confirmed the concrete behavior reported in discussion 4238145333: when the session root and agent home both contain uploads/image.png, viewer_root(session, rel, home) selects the session copy, while fs.upload writes and returns the agent-home copy. The composer can therefore preview the newly uploaded bytes while turn.send silently hands the model the older file. That ordinary, user-visible data substitution must be resolved before merge.

The two additional reviewer blockers about global-listener ownership and the duplicated picture-extension table remain open under their author; I am not re-grading or resolving them. My own earlier attachment-root thread remains correctly resolved.

Verification this turn:

  • uv run pytest tests/test_rpc_turn_send.py tests/test_rpc_console.py -k 'turn_send or viewer_root or upload' -x: 70 passed.
  • Focused Vitest suite for Dock, ComposerPage, and globalListeners: 88 passed.
  • Direct collision reproduction selected <session>/uploads/image.png containing OLD over <agent-home>/uploads/image.png containing NEW.

@0xKT

0xKT commented Oct 10, 2026

Copy link
Copy Markdown
Member

Not a blocker -- two smaller items from the acceptance pass on head d36d455 (merged onto main 289426c), beside the three blocking threads.

Severity of this one finding, not a verdict on the pull request. The blocking findings from this pass are review threads on the changed files; GitHub renders those collapsed, as a file name with no text.

  1. After a reload, a bubble can show one attached picture twice and lose the other. splitAttachments matches each note path to the engine's (path: ...) lines with absolute.find((abs) => slashed(abs) === want || slashed(abs).endsWith('/' + want)) and keeps the first hit (ui-web/src/lib/attachments.ts:159-162). Until this PR a note could only carry uploads/<unique name>, so the suffix match had one answer; addPaths now lets a note carry any path a same-origin /file drag names, relative to the session's root or absolute. Measured with the real splitAttachments on this head: a note listing out/chart.png then chart.png, with engine lines for /proj/out/chart.png and /proj/chart.png, comes back as /proj/out/chart.png twice, and /elsewhere/out/chart.png then out/chart.png comes back as /elsewhere/out/chart.png twice. The reverse order, and two uploads, come back right. The turn itself receives both files; only the reloaded bubble, and a drag out of it, is wrong. Letting each engine line answer one note entry, in order, would close it.
  2. No test pins the session key _media_root hands the resolver. With return workspace_root(loop, session_key) (raven/rpc/methods/turn.py:134) changed to workspace_root(loop, ""), tests/test_rpc_turn_send.py and tests/test_rpc_session.py still give 228 passed: the _PinnedLoop every new test uses answers the same root whatever key it is asked about (tests/test_rpc_turn_send.py:466-469). Control: dropping the fence (allowed = ()) fails 2 of them. With a real resolver the mutant roots a pinned session's attachments at the policy default instead of its own directory. A stub loop that answers per key would pin it.

@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; this can merge as far as I am concerned.

I reviewed the delta from the blocked revision and rechecked the full github/main...HEAD change, affected callers and history, backward compatibility, test integrity, and the repository architecture and rules in AGENTS.md, CONTEXT-MAP.md, ui-web/CONTEXT.md, and ui-web/CONTRIBUTING.md. The fresh-upload identity bug is fixed both at name allocation and at send time: upload paths avoid the current session's shadow names, and the live page hands turn.send the absolute file fs.upload wrote. The document listeners now belong to state/globalListeners, the picture-extension table is shared through lib, and the two attachment replay notes are covered. I found no new blocker or plain error.

The three open threads were raised by another reviewer and remain for that reviewer to close; my own earlier thread remains resolved.

Verification:

  • uv run pytest tests/test_rpc_turn_send.py tests/test_rpc_console.py tests/test_rpc_files.py tests/test_import_cycle_budget.py -x: 282 passed.
  • Focused Vitest suite across 9 affected files: 446 passed.
  • npm run type-check: passed.
  • Focused ESLint and Ruff checks: passed.
  • Import-direction gate through Vitest: 6 passed.
  • git diff --check github/main...HEAD: passed.
  • A direct node invocation of the Vitest gate was invalid for its runner API; rerunning with the repository test command produced the passing result above.

LivXue and others added 6 commits October 11, 2026 03:52
Dragging a picture from the transcript into the composer attached either
nothing (Chrome on Windows and Linux, and Firefox, hand an in-page drag
over with no File) or a re-upload of the File that Chrome on macOS names
`download`, with no extension, so the sent bubble drew a file chip where
the picture should have been.

A drop that began in this document and names this origin's /file route
in `text/uri-list` now attaches that file where it already is, by path,
with nothing read or uploaded; the address wins over any File the drop
also carries. A transcript picture still drawn from the upload's cached
data URL puts its /file address in the list when it is dragged. An
address is taken only when the drag that began here carried the same
path from its start, so a site the reader drags from cannot make the
composer attach an arbitrary local file.

A File that does go up is now named by its type when its name spells no
picture extension (download becomes download.png), which covers images
dragged in from other sites.

Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>
The composer now attaches a picture dragged out of the transcript by
its own absolute path instead of uploading a copy, so the paths
_resolve_media receives are not only uploads under agent home.

Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>
…ricted

With tools.restrict_to_workspace on, turn.send resolved every media path
against agent home and fenced it there. The viewer serves a picture from
the conversation's own working directory, so a picture the composer
attaches by its own path, from a session pinned outside agent home,
showed in the tray and was dropped from the turn without an error.

turn.send now asks for that directory the way /file does
(console._workspace_root), finds a relative path by the viewer's own
rule (viewer_root, which now takes agent home from its caller), and with
the setting on admits that directory beside agent home. A path that
neither root holds is still refused, and a loop that cannot name the
directory leaves agent home to stand in.

Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>
tests/test_import_cycle_budget.py counted 45 modules inside an import
cycle against a ceiling of 44. The session-aware resolver had turn.send
import viewer_root and the session-root lookup from rpc.methods.console,
and the console already reaches turn through rpc.methods.session, so
the new edge closed console -> session -> turn -> console; the guard
counts a function-local import exactly like a top-level one.

viewer_root's rule and the session-root lookup now live in
raven/rpc/files.py, which both modules import and which reaches
neither. The console keeps its names (viewer_root as a wrapper that
reads agent home where fs.upload does, _workspace_root and _UPLOAD_DIR
as imports), so its callers and tests are unchanged.

Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>
Name an upload past the session root's own uploads/ as well, so the
relative path fs.upload answers can never mean two files: viewer_root
reads the session's own root first, and both turn.send and /file follow
it, so a same-named file there would answer for the upload and the
reader would see one picture while the model was handed another.

Register the composer's two page-lifetime drag listeners with the page's
other document listeners (ui-web/CONTEXT.md's one place), and teach the
order test the two new rows.

Move the picture-extension table to lib/, read by the workspace viewer
and the composer both, so the tray and the sent bubble cannot disagree
about whether a name is a picture.

Give each engine path one note entry when a sent message is read back:
two entries can name suffixes of one path, and both reading it drew that
file twice and lost the other.

Pin the session key _media_root hands the resolver with a loop that
answers per key, which the shared-root stub could not tell from a
dropped one.

Co-authored-by: Claude (deepseek-flash) <noreply@anthropic.com>
fs.upload answers with a short path (uploads/<name>) and with the
absolute file it wrote. The note keeps the short one -- the bubble
renders it and /file re-roots it -- while the turn now receives the
absolute file: viewer_root reads the session's own root before agent
home, so a same-named file appearing there after the upload could
answer for it.

lib/uploadPaths keeps the answer by path, page-lifetime and keyed like
lib/attachmentCache, and mediaOf maps each path it recovers from the
note through it. A path no upload minted is left to viewer_root as
before, and so is a note restored after a reload, when the answer is
gone.

Pin it thrice: the store case shows an upload is remembered, the
runtime case shows the turn.send media carries the file, and the
turn.send case shows a restricted, pinned session admits the absolute
upload path.

Co-authored-by: Claude (deepseek-flash) <noreply@anthropic.com>
@LivXue
LivXue force-pushed the fix/composer_drop_transcript_image branch from 08b0c99 to 23296bc Compare October 10, 2026 19:52

@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; this can merge as far as I am concerned.

This head rewrites commit history but has the exact same tree object as the previously reviewed clean revision (6d725a38c32db1495c59568a30a9ba6079e80ec2), with an empty 08b0c99a3a86..HEAD content diff. I rechecked the full github/main...HEAD change, affected callers and compatibility, test integrity, and the repository architecture and rules; the upload identity, global-listener ownership, shared picture-extension table, import-cycle repair, and attachment replay fixes remain intact. I found no new blocker or plain error.

The three threads opened by another reviewer now contain that reviewer's re-verification that they are fixed; resolving those threads remains theirs. My own earlier thread remains resolved.

Verification:

  • uv run pytest tests/test_rpc_turn_send.py tests/test_rpc_files.py tests/test_import_cycle_budget.py -x: 133 passed.
  • Focused Vitest suite across 6 affected files: 126 passed.
  • npm run type-check: passed.
  • git diff --check github/main...HEAD: passed.

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.

Blocking: once fs.upload has answered uploads/<name> on this page, a different file with that spelling, dragged into the composer from a session root, reaches the turn as that upload

lib/uploadPaths keeps one page-lifetime map keyed by the note's spelling alone, with no session in the key (ui-web/src/lib/uploadPaths.ts:16, :24), and mediaOf sends every note path through it (ui-web/src/state/session/runtime.ts:233). But uploads/<name> can name two files: the agent-home file an upload wrote, and a session root's own uploads/<name>. The file panel and the desk viewer show the second one (/file reads the session root first), and the composer stages a drag of it with the same spelling (addPaths, ui-web/src/features/composer/store.ts:572). So the turn is handed the upload's absolute path while the chip shows the file the reader dragged.

It needs no agent action. Upload image.png in conversation A. Then, in conversation B, whose root already holds an older uploads/image.png, open that file from B's panel and drag it into B's composer: B's turn gets A's upload. The PR's own test says "every session root that predates the deposit move" holds uploads/image.png (tests/test_rpc_turn_send.py:599-600), and Chrome names every pasted picture image.png. fs.upload's name check covers only the uploading session's root (raven/rpc/methods/console.py:2107), so it cannot prevent this.

Reproduced on head 23296bc merged onto main 6126965, in a git archive copy, with the PR's own Dock.test.tsx harness (the real composer drop handler, store.fireSend and mediaOf) and an upload stub answering {path: 'uploads/shot.png', abs_path: '/home/me/.raven/workspace/uploads/shot.png'}. The steps: drop a File shot.png and send, then drag an <img src=/file?path=uploads/shot.png&session=s1> into the field and send.

  • head: mediaOf of the second message gives ['/home/me/.raven/workspace/uploads/shot.png'];
  • control, with no upload on the page first: ['uploads/shot.png'];
  • control, with mediaOf back to d36d455's form (no mediaPath): ['uploads/shot.png'] in both cases.
    On the server, uploads/shot.png resolves to the session root's file and the absolute spelling to the agent-home upload, which is the rule this layer was added for.

The rules this PR writes cover the drag. files.py:330-331 says "a picture the viewer drew is the file the turn receives". console.py:2073 says "uploads/<name> must never mean two files". uploadPaths.test.ts:16-18 says "A file dragged out of the transcript or the panel was never uploaded: its path is what the resolver reads". At d36d455 the mirror case was wrong (an upload lost to a same-named session file, the thread this layer answers). This layer moves the collision to the drag direction rather than removing it.

Whatever the fix, it has to tell an entry that came from fs.upload apart from one staged by drag. The spelling alone cannot do that, and mediaOf reads only the note text, so the distinction has to be decided where the entry is staged, or written into the note itself.

@0xKT

0xKT commented Oct 11, 2026

Copy link
Copy Markdown
Member

Not a blocker -- three items from the re-acceptance pass on head 23296bc (merged onto main 6126965), beside the blocking thread on lib/uploadPaths. Each was reproduced through the real handlers with an isolated RAVEN_HOME, and I checked the code paths below on that tree.

Severity of this one finding, not a verdict on the pull request. The blocking findings from this pass are review threads on the changed files; GitHub renders those collapsed, as a file name with no text.

  1. A file attached in a draft is name-checked against a folder the conversation will not run in. fs.upload picks the folder to check from params.session (raven/rpc/methods/console.py:2107), and the page sends session: sessionCurrent() || '' (ui-web/src/app/install.ts:259). A draft has no id yet, so every file attached to a new conversation's first message is checked against _workspace_root(loop, ''). That is the launch directory under raven serve and <session root>/_ under the gateway (raven/agent/workdir.py:192-206). The conversation itself runs in the folder picked on the draft, or <session root>/tui. A folder can be picked only on a draft, so the first message of every pinned conversation is this case. The turn is spared by abs_path, but /file for the note path serves the older same-named file, so the bubble's chip opens it. Measured: gateway draft -> uploads/image.png, served from <root>/tui/uploads/image.png (the old file); the same upload from an existing tui: conversation -> uploads/image-1.png. The comment at :2101-2105 says a session that cannot be resolved falls back to the old rule, but '' resolves without error, to the wrong folder.
  2. deck.templates.pick mints into the same uploads/ without the new rule. deck_templates.deposit checks agent home only (raven/rpc/deck_templates.py:629), and the pick carries no session (DeckTemplatesPickParams has only name, raven/rpc/models.py:4069-4070; install.ts:268). So a picked template whose name the conversation root already holds answers a relative path that /file and a relative turn.send resolve to the project's older file. Measured: the pick answered uploads/amber.pptx and /file served the project's OLD-PROJECT-COPY, while fs.upload of the same name in the same conversation got amber-1.pptx. A relative turn.send received the shipped template on main and the project copy on this head. While the page stays open it hands the turn the pick's abs_path instead (ui-web/src/features/composer/store.ts:528). One minting helper, with the session on the pick's wire, would keep the rule in one place.
  3. An unreadable conversation folder now fails every upload with a bare internal_error. Only the root lookup is guarded (:2106-2112). The new (shadow_dir / target.name).exists() in the loop (:2116) is not, and on Python 3.13 Path.exists re-raises EACCES: measured, it raised PermissionError under a mode-000 folder. A pinned folder that macOS TCC denies to the engine, or one owned by another user, then turns every upload into -32603, where main stored the file. turn.send catches the same probe on purpose (raven/rpc/methods/turn.py:107-113).

@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.

Blocking: distinguish uploaded attachments from dragged same-spelled session files.

The new cross-session reproduction changes my stance on this unchanged revision. I independently confirmed the shared page-lifetime mapping: after remember("uploads/image.png", "/agent-home/uploads/image.png"), a later mediaPath("uploads/image.png") for a dragged file from another session still returns the agent-home absolute path. Since mediaOf applies that lookup to every attachment path, the newly dragged file can be silently replaced by the earlier upload.

Focused verification remains green but does not cover this collision: npm test -- --run src/lib/uploadPaths.test.ts src/state/session/runtime.inject.test.ts src/chrome/Dock.test.tsx (46 tests passed).

My earlier clean stance is withdrawn; the open uploadPaths thread now blocks this revision from my side.

This branch has not been deployed

No deployments
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.

3 participants