feat(files): scoped search, in-place edits, and ranged reads for agent memory filesystems - #7393
feat(files): scoped search, in-place edits, and ranged reads for agent memory filesystems#7393mzxchandra wants to merge 21 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThe PR adds folder-scoped file-content search, exact in-place edit and line insertion operations, and per-file ranged text reads across workflow tools, v2 APIs, generated clients, and documentation.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/workspace-files/application/edit-workspace-file-content.ts | Introduces the authorized, locked read-modify-write use case for exact replacement and line insertion with optimistic concurrency checks. |
| apps/sim/lib/workspace-files/application/search-workspace-file-content.ts | Adds runtime folder-scope resolution and passes the authorized workspace scope to both search matches and coverage reporting. |
| apps/sim/lib/workspace-files/search/repository.ts | Applies workspace, folder, root-file, and recursive-scope predicates consistently to indexed matches and coverage queries. |
| apps/sim/lib/workspace-files/application/read-workspace-file-text.ts | Adds per-file line slicing and range metadata while retaining the existing parsed-text read boundary. |
| apps/sim/app/api/v2/files/search/route.ts | Exposes scoped content search through the v2 route and delegates workspace authorization to the authorized application use case. |
| apps/sim/lib/workspace-files/application/edit-workspace-file-content.test.ts | Covers edit behavior with a valid typed session principal and an edit helper bound to the exported use-case union. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
Caller[Workflow tool or v2 client] --> Contract[Validated file operation contract]
Contract --> Auth[Workspace authorization]
Auth --> Scope[Resolve folder and root scope]
Scope --> Search[Scoped indexed search]
Auth --> Read[Read stored file content]
Read --> Range[Return per-file line window]
Read --> Edit[Validate UTF-8 and edit request]
Edit --> Lock[Acquire advisory lock]
Lock --> CAS[Persist with content timestamp CAS]
CAS --> Result[Return updated line count]
Reviews (7): Last reviewed commit: "fix(files): keep multi-folder search ser..." | Re-trigger Greptile
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
…ontent.test.ts Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
|
@cubic review |
@mzxchandra I have started the AI code review. It will take a few minutes to complete. |
…o feat/file-memory-ops # Conflicts: # apps/sim/lib/workspace-files/application/workspace-file-folders.test.ts
…o feat/file-memory-ops # Conflicts: # apps/sim/blocks/blocks/file-folders.test.ts
|
@cubic review |
@mzxchandra I have started the AI code review. It will take a few minutes to complete. |
|
@cubic review |
@mzxchandra I have started the AI code review. It will take a few minutes to complete. |
|
@cubic review |
@mzxchandra I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
No issues found across 41 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Re-trigger cubic
|
@cubic review |
@mzxchandra I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 41 files
You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Fix all with cubic | Re-trigger cubic
|
@cubic review |
@mzxchandra I have started the AI code review. It will take a few minutes to complete. |
Stacked on #7388 (
feat/file-folder-operations) — review the last 3 commits, or wait for #7388 to merge and this rebases to a clean diff.Why
An Agent block maintaining a per-user memory filesystem:
Three gaps make that unworkable today.
Search is workspace-wide. An agent maintaining
/memory/{userId}needs its grep confined to that subtree, or every lookup returns other users' notes.To be precise about what this is not: there is no per-folder authorization in Sim. Any principal with workspace read access can read every file in the workspace, with or without this change.
folderPathsis a query filter, not an access boundary — it narrows what a caller asked for, not what they are permitted to see. It makes the agent's behaviour correct; it is not a security control.There is no way to change one line. Writes are append-only or whole-file overwrite, so every correction regenerates the whole note — the LongWriter output ceiling and cumulative drift, on every note, every night. Scoped grep without an in-place edit is a fast finder bolted to a blunt writer.
Reads are all-or-nothing. Fine at four notes, wasteful at two hundred.
What
1.
folderPaths+includeSubfoldersonfile_searchThe segment table carries no folder id, so the predicate travels through the
workspaceFilesjoin both queries already make.It is applied to the coverage query too, not just the match query, so
complete/indexStatusdescribe the scope searched. That is the half that matters for the driving use case: an agent choosing between add a note and update the existing one cannot distinguish an unindexed file from a missing fact, and adds a duplicate. A scopedcomplete: trueis a far stronger signal than a workspace-wide one. The tool description now says so explicitly — a non-ready index means unknown, not absent.Search takes its own branch through
execute-tool.tsand never touchesfileManageContract, which is why it was skipped in #7388. Resolution happens in the use case, so the v2 route inherits it.2.
file_editandfile_insert— in-place editingfile_edit(folderPath, fileName, oldString, newString)andfile_insert(folderPath, fileName, afterLine, content).oldStringmust match exactly once, or the edit is refused naming the line numbers each match sits on. Taking the first match rewrites an arbitrary line; replacing all of them rewrites lines the caller never saw. This is the same refusal we landed on append after.find()picking arbitrarily was a P1 twice running, and matches Letta'smemory_replaceand Anthropic'sstr_replace.afterLinepast the end is refused, not clamped — clamping turns an agent's off-by-one into a silent write to the wrong end of the file.Architecture: the read-modify-write is a compound mutation, so it lives in one application use case (
editWorkspaceFileContent) with the tool case and the v2 route as thin adapters, rather than being sequenced in a surface. It reusesfiles.update_content; an edit is a content update, and a new catalog entry would fragment the policy. Append inlines the same shape inoperations.tstoday; moving it onto this use case is a clean follow-up, deliberately not done here.Concurrency: the Redis advisory lock plus
expectedUpdatedAtCAS, exactly as append does. DeliberatelycontentUpdatedAt, notupdatedAt— a rename or move bumps the latter and would fail an edit that raced nothing.Binary guard (new): edits operate on stored bytes, never on parser-extracted text — extraction is one-way, so writing it back would replace a DOCX with a transcript of itself. Non-UTF-8 input is refused. Append has the same gap today (
toString('utf-8')with no check); pre-existing and shipped, so it is flagged rather than changed inside a PR about new operations.3.
offset/limitonfile_get_contentApplied per file, since a selection can be several files and one running offset across them would depend on an ordering the caller cannot see. Returns
lineRangeswithtotalLines— without it a caller cannot tell a file that ended from a window that stopped early, the same absent-vs-unknown confusion as the search index.Also: the workspace root was not addressable
parseFolderPath('/')returns[], and no folder row has an empty segment list, sofolderPaths: ['/']resolved asFolder not found: /on read, get_content, compress, and append — everything shipped in #7388. The UI hides it; the contract accepts it and an LLM will try it.Fixed in
resolveFolderIdsForPaths, reported asincludeRootFilesrather than a sentinel in the id set, because the root is the absence of a folder id and a magic string would survive every type check then match nothing on reaching a SQLin (...).This changes behaviour on four shipped operations (they error today, they return files after). Not needed by the driving use case — it is here only because Op 1 refactors the function carrying it. Happy to lift it into a follow-up if you'd rather.
V2
GET /api/v2/files/searchPATCH /api/v2/files/[fileId]/contentPUTon the same path is whole-file replace, soPATCHis the partial counterpartGET /api/v2/files/[fileId]/textoffset/limit, joining the existingmaxBytesWhere to spend review attention
file_editis a new mutation primitive. Everything before it appended or replaced wholesale. The uniqueness refusal is the only thing between a sloppyoldStringand silent corruption on an unattended nightly path.lib/workspace-files/folder-path-selection.test.tspins that/memory/user-adoes not reach/memory/user-a-2(shared prefix) or/memory/user-b, and never climbs to a parent.countLines).insertaccepts exactly the range that search, a ranged read, and the post-edit count all report. An earlier draft counted the trailing newline as a line, which told an agent the file was one line longer thaninsertwould take.Fixed along the way
operations.test.tsmockedresolveWorkspaceFileReferencewith an object signature; the manager takes(workspaceId, reference)positionally, so the mock returnedundefinedfor anything reaching that path. Nothing exercised it before. Corrected rather than worked around — same partial-mock trap that hid three P1s in #7388.Verification
tscclean · 45 audits · api-validation · client-boundary · biome · 12,596 tests.Generated surfaces regenerated: integration docs, tool metadata, OpenAPI (29 file-audit operations, 215 total), CLI API + CLI docs.
Not yet done: browser E2E. Every P1 in #7388 was a disconnected wire that mocked tests cannot see. Running it against the real
/memory/{userId}shape next.